如何写一个健壮且高效的串口接收程序?

学单片机的大概最先、最常写的通信程序应该就是串口程序了,但是如何写出一个健壮且高效的串口接收程序呢?接下来鱼鹰将根据多年的开发经验教你如何编写串口接收程序。

1、传入参数指针

2、互斥锁释放顺序

3、数据帧检查

4、串口空闲

5、通信吞吐量

为了更好的理解接下来的知识点,鱼鹰将设计一个串口框架,让道友心中有一个参考方向。

本篇重点在于解决如何写一个健壮、高效的串口接收数据,发送与接收处理过程略讲。

帧格式

先聊聊帧格式,一般来说,一个数据帧有以下几部分内容:

帧头

帧头用于分辨一个数据帧的起始,这个帧头必须足够特殊才行,因为它是分辨一个帧的起始,那么什么样的帧头是足够特殊的数据呢?保证这个数据在一个帧内最好只出现一次的数据,那就是帧头,比如 0x55、0xAA 之类的。而且最好有两个字节以上,这样帧头才更加独一无二。

但是数据域内的数据你是没办法保障不包含和帧头一样的数据。

那么如果不凑巧,除了帧头外其他部分也有这样的两个字节的帧头,那会出现什么问题?

几乎不会出现问题。因为一般来说数据都是一帧一帧发送的,只要你前面的数据帧传输正确,那么即使下一帧的数据中有和帧头一样的数据(包括帧头)也没有问题,因为帧头判断已经在开始就判断成功了,就不会继续判断后面的数据是否是帧头了。

那么为什么说是几乎,因为如果上一帧数据接收错误,那么程序必须再找一次帧头才行(单字节接收时是如此,采用空闲中断的话就不需要这么麻烦),这就导致找帧头的时候在帧头数据之外寻找了,很可能这些数据就有帧头。

但是即使帧头数据之外的假帧头真的存在,也没关系,还有第二重保障,那就是校验,即使找到了一个错误的帧头,那么数据校验这一关也很难过去,所以放宽心。

如果校验也凑巧通过了,那还有第三重保障:帧尾。应该到不了这里吧,毕竟这比中彩票还难。

又要上一帧数据接收错误,还要当前帧除了帧头之外还有帧头,另外你还能跳过校验的检查(还有功能字、长度信息的检查),太难了。所以只要通过了这些检查,你就可以认为这个数据帧是可用的了。所以一帧数据接收错误,导致的问题最多只是丢失了这帧数据,对后续接收是不会有影响的(前提是你这个接收程序设计的足够好),发送端在发送超时后再发送一次即可,所以重发机制很重要。

事实上,如果你采用串口空闲中断,帧头、帧尾都可以不用,但一般来说,帧头都会保留,帧尾可以不需要,这是为了当单片机没有串口空闲中断时考虑,当然也可能有其他考虑,所以帧头得保留。

功能字

功能字主要用于说明该数据帧的功能,当然也可以作为函数指针的索引,一个索引值代表了一个具体功能,据此可找到对应的功能函数。

比如,设计一个函数指针数组,通过功能字进行索引,进而跳转到对应的功能函数中处理。

特别注意的是,设计功能字的时候,要考虑兼容性,对数据帧的功能进行划分,不要想到一个算一个,功能字也不要随便安排,不然在以后增加数据帧的时候会很麻烦。

比如说,只有一个字节的功能字,前四位作为一个大类,后四位作为大类中具体类。这样就可以将系统数据通信帧分为 16 个大类,每个大类下有 16 个可用的具体类,当你增加功能字的时候,就可以根据你的设计来确定属于哪个大类了,然后再插入进去。这样在管理、维护这些通信数据时你会发现很方便。

这个思想其实在 ARM 内核的中断系统和设计 uCOS II 任务优先级的时候都有,而鱼鹰在设计项目的通信协议的时候就是运用了这些思想。

(图片来源于《权威指南》)

长度

长度信息也是一个非常关键的数据,别小看了它,因为它,鱼鹰用了将近一个星期的时间才把一个 HardFaul 问题解决了,虽然这个程序 bug 不是我写的(鱼鹰一直用的是串口空闲接收方式,这个 bug 自然而然就跳过了),但确实很容易出错。

因为它是决定了你这个数据域长度的关键信息(一般长度信息代表数据域的长度,而不包含其它部分长度),也是这个数据帧的长度信息(加上固定字节长度就是帧长度了),更是接收程序还要接收多少数据的关键信息(对于空闲中断接收方式不算关键,这里的不关键是指不会造成程序异常问题)。

比如说你的程序刚好将帧头、帧尾、功能字判断完毕,然后中断程序因为种种原因导致没有及时接收串口数据,那么你可能得到的就是错误的数据,然后这个错误的长度数据就可能导致你的栈帧或者全局变量被破坏(单字节接收情况下就可能出现,因为鱼鹰碰到过),这是很严重的事情。所以在接收数据域的数据之前一定一定要判断这个长度信息(空闲中断除外)是否合法,不合法的话及时扔掉这帧数据,开始下一帧的数据检查。

所以为了保证及时接收数据,最好采用 DMA 传输。

数据域

这个没啥好说的,就是整个帧你真正需要发送的数据。而为了让你的发送函数能接收各种类型的数据,那么把参数类型设置为 void * 会是不错的选择。

校验

一个数据在接收过程中可能会被干扰,导致接收到错误的数据,那么如何保证这帧数据的完整与准确性呢,就在校验这一关了。

校验有很多方式,和校验、CRC 校验等(奇偶校验是针对一个字节的,不是数据帧)。

和校验算法简单,CPU 运算量小,累加最后只取最低字节即可(注意不是高字节,想想为什么),或者保存累加和的变量就是一个字节空间,这样就不需要额外操作了。

CRC 校验,这个算法复杂,理解起来比较困难,但一般来说可以直接拿来用,因为它是对每一位(bit)进行校验,所以纠错率很高,几乎不存在发现不了的数据错误,但正因为对每一位进行检查,所以 CPU 运算量较大,但是有的单片机是可以硬件计算 CRC 校验值的(比如 stm32)。不过现在 CPU 运算速度都挺快的,软件运算也是可以接受的。

那么该怎么校验呢?是从帧头开始到数据域部分,还是说直接校验数据部分?其实都可以,区别就是运算量问题,不过问题不大(最好是从头开始校验,以保证整帧数据的准确性)。

帧尾

前面说了,帧尾在空闲中断中可以不用,RXNE 中断接收时其实也可以不用,当然也可以加上,好处就是当你用串口助手查看数据流时,可以观察出一帧数据是否发送完整了。

最后再说说为什么在数据域前面设计四个字节大小,除了协议本身需要外,还有一个原因就是强制类型转化需要,我们知道,一般来说,赋值时都有字节对齐的限制(实际上有的 CPU 可以不对齐进行赋值),stm32 是 32 位的,那么四字节对齐是最合适的,这样就可以直接将我们收到的数据转化为需要的数据类型了。

传输过程

聊完了帧格式,再从大的方向看串口的传输过程:

当发送端发送第一帧数据包时,接收端通过某种方式接收(串口接收非空 RXNE 中断、串口空闲 IDLE 中断),为了让串口能够触发空闲中断,必须在发送端两个发送帧之间插入一段空闲时间(就是在此时间内不发数据,红色部分),保证空闲中断的准确触发。

同理,为了让发送端也能正常接收接收端的数据,也需要控制接收端的发送,不能在返回一帧数据时立马发送下一帧数据,不然触发不了发送端的空闲中断。

事实上,有些程序员设计的发送、接收过程比这个简单一些。即只有当接收端接收到一帧数据并返回一帧数据之后,发送端才能继续发送数据,这样一来,我们只需要控制好接收端的频率,就可以控制整个通信过程,也能控制通信频率。

但为什么还要设计成第一种传输情况呢?这是为了充分利用串口,增大数据吞吐率(这个后面再说)。

另外,不知道你是否观察到图中的每个数据帧占用的时间是不一样的,这是因为每个数据帧不可能都是一样长的,它们是不定长的数据包,所以你的定时不能从发送开始定时,而是从发送完成后开始定时控制空闲时间。

软件设计

上面所有的内容都是设定一些条件、需求,那么该如何实现软件设计呢?毕竟说的再多,如果不能实现这些,又有什么用呢?talk is cheap, show me the code。

下图设计了三个数据帧:GetVision(),GetSN(),GetMsg()。

GetVision()用于获取硬件版本号、软件版本号。

GetSN()用于获取产品序列号,用于识别唯一设备。

GetMsg()用于获取消息,可以获取各种传感器数据,事实上,如果数据量多的话,根据传感器的不同,会根据需要设计各种不同的数据帧(功能字不同)。

在软件设计上一般都会对这些函数设计一个统一的函数类型,用函数指针数组统一管理。

既要统一,又要体现差异性,函数参数就显得很有必要了。

这里设计了两个参数,一个是 void* (无类型指针),一个是 length(长度)。

无类型指针主要是用于传递数据域的数据地址,而数据域的数据可能是整型、浮点型、结构体、枚举、联合体等,为了保证传入的各种数据类型在不通过强制转化情况下都能兼容,设计为 void * 就显得很有必要了。

实际上为了显得更专业性,加上 const 修饰会是不错的选择,因为这可以保证缓存数据不被修改(事实上只能保证不被程序员修改,而不能保证程序本身,这个后面会解释)。

长度,长度参数是一个很关键的参数,为了保证长度的准确性,建议使用 sizeof 获取。

有人觉得 sizeof 好像一个函数,会不会导致效率低下啊,毕竟每次通信都要计算一次长度,那你就大特大错了。事实上,只要你的类型定义定义好了(不管是内置的类型定义还是自定义的结构体、枚举、联合体),编译器都能确定 sizeof 最终的数据长度,根本不存在计算过程。

用 sizeof 的两个好处:

1、可以忽略字节对齐问题(不同平台不能忽略,比如 window 和单片机通信)。因为编译器为了数据读写效率更高,一般会对数据进行地址对齐,这样一来手工计算一个数据类型的长度变得麻烦(当然你可以说使用某些手段让数据不进行对齐,这个另说),而 sizeof 将智能且准确计算数据大小。

2、当你使用 sizeof 时,兼容性更强,也显得更专业。程序修修改改很正常,一个数据结构改来改去也很正常,特别是开发初期更是如此。但是不管你怎么改,只要在编译器看来是固定长度的数据类型,那么 sizeof 就能在链接程序前计算出来;并且即使你后来加了数据不对齐的限制(加了这个限制后,很可能数据大小变小),也能准确计算。别问为什么,就是这么任性。

所以为了减小出错的可能性、减少劳动量,sizeof 是不错的选择。

当接收到数据地址和长度信息后,就可以进行发送了。

因为只有数据域的数据,为了组成一帧完整的数据,就必须加入打包过程。加上数据帧头、功能字、数据长度、校验等数据。

当一帧数据打包好之后,就可以进行发送了,发送可以采用循环查询发送,也可使用发送空 TXE 中断,当然还是建议使用 DMA 发送,这样你可以还没等它发送完就可处理其它事情了。

以上就是发送过程,接收过程也是同理,根据功能字来调用相应的函数进行回复。

事实上,如果只是数据的传输过程,完全可以使用一个发送函数实现数据的特异性传输,这样就可以减少一层数据传递,但是有些通信帧不只是数据的传输,可能在接收、发送时作一些其他处理(比如清除、设置某些标志位),所以需要再增加一层,用于进行差异性处理。

以上就是本篇内容的基础内容了,你以为快完了?你错了,现在只是刚开始而已,鱼鹰写本篇笔记的最终目的还在后面。这只是前菜,正文才刚开始。

串口接收遇到的那些问题

以下内容不会用太多的笔墨描述如何写发送、接收函数,而是重点关注串口接收过程中可能遇到的一些问题,如果说描述到了发送、接收函数,别会错意,顺带的。

以下大部分问题都是因为采用 RXNE(接收不为空)中断方式导致的问题,只有一个问题是鱼鹰从前没有考虑到,也是 IDLE + DMA 方式不可忽略的问题。

这就是为什么鱼鹰建议采用 IDLE + DMA 的原因,不仅是因为效率问题,更因为它能避免很多问题,当然水平足够高的话,采用 RXNE 也是完全(“完全”就未必,里面有一个问题是 RXNE 方式难以避免的问题)没有问题。

事实上,即使鱼鹰采用 RXNE 方式接收数据,也能避免以下大部分的问题,因为鱼鹰的基础足够扎实,会在一开始编写代码的时候自然而然避免一些问题的出现。

但是看完以下内容后,相信各位道友写出一个高效且健壮的串口接收程序根本不是问题,因为这就是所谓的经验啊。

传入参数指针

前面鱼鹰已经提到了需要一个指针作为函数的参数,这里说说这个指针问题。

我们知道,为了维护方便,也是为了节省空间,一般都会将类似的功能整合成一个函数,比如串口经常要用的发送、接收功能,但是所发送的数据内存空间可能就处于五湖四海了,他们通过指针来指向将要发送的数据。

为了节省 RAM 空间或者其它不为人知的原因,传递给发送函数的指针就是实际发送数据的地址,并且在计算校验值的时候也是直接使用这个地址进行校验计算,然后采用循环查询的方式发送数据,这样一来,就不必拷贝一个数据的副本进行发送,而是直接从数据源的地址进行发送,节省了部分 RAM 空间。

但是这样真的好吗?

你是否考虑过在计算数据帧校验值的时候,数据源改变了的问题呢?

比如说你采用和校验,数据一开始是 0x55,计算数据帧的校验和值为 tx_sum,然后被中断程序或者 DMA 修改了这个数据源,变成了 0xAA,此时你再使用这个数据地址进行发送,接收端接收到了 0xAA,接收端计算校验和的时候是 rx_sum,那么 rx_sum 必然不等于 tx_sum,然后接收端就认为该数据帧是错误的,然后丢失这帧数据,而这种情况是比较少见的,但确实是会偶尔出现接收错误的情况(当时发现这个问题时始终不得其解,明明我发送的是这个校验值,为什么你计算的校验值是另一个?开始怀疑是校验函数的问题,但其他数据帧计算时没有问题,只有一种数据帧会出现问题,然后鱼鹰怀疑是数据源的问题,是的,鱼鹰很快就怀疑数据源的问题,但当时验证时,只改了校验那部分地址,发送时的地址还是使用源地址,导致问题还是没解决,过了好久之后才发现这个发送地址没改,囧。所以说,即使你的思路是对的,但如果你解决时错了,问题也很严重)。

如果说接收端(从机)具有重发机制,那么问题不是很大,丢失一帧数据而已,再重发就是,但事实是,一般串口设计成主从模式,主机会在没有接收到从机的应答数据时会进行重发,但是从机一般不会主动重发数据的,它无法判断主机是否成功接收,而从机一般会在成功发送完数据后开始清除一些标志位(比如键盘按键数据清空,不然主机下次获取按键信息时还是同一个按键数据),事实上这个动作必须在对方成功接收才能进行(否则这次按键信息就丢失了),从这个角度来看,我们必须设计一个机制用于判断主机是否成功接收。

I2C、CAN 总线都有应答信号,但这是这些是总线自带的特性,我们不可能在接收到一个字节后发送一个应答信号给主机,那么是否有其他办法呢。

人们很容易想到的一个办法就是在主机收到正确数据后,主动发送一帧专用数据帧用于清除这个标志(这个帧和普通帧一样,所以可以确保主机数据能准确送达从机,因为如果超时没有送达,会触发重发机制)。这样只要在获取完这帧数据后,再额外的发送一帧数据用于对方确认即可,从机接收到后,即可开始着手清除一些标志位。

但这样会有一个问题,因为这种特殊的需要从机确认的数据包(其他类型数据不需要确认是因为如果主机没有正确收到数据还可以继续获取,获取的数据是一样并没有关系,但这种需要从机确认,一旦从机认为发送成功了,数据就被清除了这种情况就需要确认,典型的就是按键信息了),我们需要额外处理并占用发送带宽。这是鱼鹰不愿忍受的。

那么是否有更好的办法?

或许我们可以从 USB 协议中获得启发(这是在写这篇笔记的时候想到的,当时写按键板代码的时候没有想到过,但因为当时测试时传输成功率 100%,所以就放弃处理这种情况了)。

USB 协议是典型的主从机制,主机不主动获取数据,从机是无法主动发送数据的。那么从机是如何确定对方成功接收数据了呢?

一个 bit 的翻转位。

每当主机成功发送一帧数据后再发送下一帧数据时,就会翻转这个位,从机就可以根据这个位判断主机是属于重发数据(重发数据表示主机接收失败了)还是新数据了,这样从机就能从下一帧数据确定上一帧数据是否成功发送了。

而主机发送的数据是由从机发送应答包确定的,和上面的串口协议类似,所以这个方向的数据是没有问题的。

那么我们该如何重新设计这个协议呢?可以尽可能的不改变原来协议的情况下实现吗?

或许可以从功能字出发。

为了保证对功能字的原有定义保持基本不变,使用最高 bit 作为这个特殊的位,这个 bit 开始是 0,之后主机每接收一个从机应答数据就进行翻转,如果因为没有接收到从机的应答数据,就会使用相同的翻转位重复发送;而从机也根据这个 bit 来确定自己的上一帧数据对方是否接收(对比上一帧数据的翻转位),如果主机没有成功接收,就不清除标志位(之后主机会重发数据再次获取),否则清除标志位,。

因为是鱼鹰刚想到的,就不多说了,仅提供一个思路。

现在回到指针那块的问题。

现在已经知道,如果你在计算校验和与发送的过程中出现源数据改变的情况,就会导致数据帧校验失败,那么有什么解决办法?

如果说你坚持使用查询方式发送来节省部分空间,那么只要在计算校验值之前拷贝一份源数据,然后用这份数据计算并发送即可。

另一种方法就是,直接把整帧数据拷贝到一个数据缓存中,使用 DMA 发送。

现在还有一个问题,那就是如果我想发送一个数据域为空的数据该怎么发送?

一般来说,在使用指针的时候,不会使用 NULL 空指针,但是在数据为空的情况下,就需要使用 NULL 指针了,并长度设置为 0,这个时候在检查指针的时候,不能看到空指针就退出函数,还要判断长度信息,当长度为 0 时在打包时就不拷贝源数据,但最终还是要发送数据帧的(当时别人写的代码将指针和长度判断同时放到了 for 循环的条件里面,鱼鹰觉得效率太低,导致修改代码是没考虑指针为空的情况,所以导致了一个小 bug)。

互斥锁释放顺序

现在考虑第二个问题:互斥锁释放顺序问题。

如果没有采用队列方式接收数据,而是主机发送完成后等待接收从机数据后再发送下一帧数据,那么该如何处理互斥锁的问题?

我们知道为了保证数据的同步,保证在接收到一帧数据进行处理时,不能被新的数据帧冲掉,这时就要加入一个互斥锁,表示我正在处理数据,下面的数据我接收不了,这样就能保证你正在处理的数据不会被新来的数据修改掉,从而进行正确的处理。

那么这个标志位(互斥锁)该什么时候清掉(释放掉)呢?

一般来说标志位,一般越早清掉越好,比如外部中断标志位,进入中断后,一般首先会清理标志位,这样即使你正在处理本次中断的程序,那么即使这时再来了中断,也不会丢失中断信息(有悬起标志位),这样就可以在处理完这次中断后,立马进行下一次中断的处理了(前提是优先级足够高)。

但是如果你清理太早或者清理太晚会怎样?

比如说你在接收到一帧数据后(数据帧所有检查完成),开始设置标志位,当主程序查询到这个标志时(一般数据处理不会放在中断中),如果马上开始清除这个标志……嗯,一般来说不会有问题。

那么什么时候会出现问题?当你的主程序查询到这这个标志时开始清除标志,然后处理、返回数据给主机,如果此时主机超时重发数据时,,因为这个时候你虽然在处理数据,但是因为你的标志位已经被清除了,所以接收程序就会开始往接收缓存区存数据了,当你存完之后再回到数据处理那里,你的缓存区可能就不是你想要的数据了。

可能你会说,既然是重发,那么数据应该是相同的吧?好吧,你赢了,鱼鹰编不下去了,这种情况(有重发机制)下清理太早好像是不会出现问题,但你怎么知道对方是采用副本进行重发数据的呢,如果重发时它又从源数据中拷贝后再进行重发会出现什么问题?比如时间信息,开始第一帧数据是 11:59,CPU 刚把 11 拷贝到用户空间,被串口中断程序打断,导致下一帧接收的数据是 12:00,此时回到主程序继续拷贝,拷贝 00,数据的完整性被破坏,这样导致的结果就是 11:00,而实际上时间是 12:00,这就是你打断数据处理过程的后果。

现在再说说清理太晚会怎么样。

当你的主程序查询到这个标志后,暂时不清除,而是等到从机发送完应答数据之后再清除标志,此时因为从机采用查询方式(查询方式表明从机发送完最后一个字节后后开始清除标志位,也就代表了主机就差最后一个字节没有接收了,这样发送和清除之间间隔时间较短,而采用 DMA 方式的话,发送和清除的间隔时间更短,因为可能 DMA 还没开始发送第一帧数据,清除工作就已经完成了),或者因为其他原因(比如中断处理)导致发送和清除之间的时间很长这种特殊情况,这样可能主机已经开始下发下一帧数据了,但是因为此时标志还没有清除,不能接收数据,所以主机这一帧数据就这样丢失了。

那么这个清除标志位最合适的时机是什么时候?

你锁定资源利用完的时候。

现在来看看,这个互斥锁锁定的是什么资源?对,就是接收缓存,那么接收缓存什么时候用完?当然是在数据处理完成之时。也就是说在数据处理完之前、发送数据之前清除最合适。

这样就不会因为处理其他事情而导致清除操作过晚而丢失下一帧数据了,因为此时主机还没收到从机上传的数据,也就不会马上开始下一帧数据的传输了。

数据帧检查

你是否会对接收的数据进行检查?如果不进行检查会发生什么?

我们知道一帧数据中,每个部分都有各自的含义,甚至有些部分可能在某些数据帧中不存在,比如数据域部分,我们需要根据长度信息来判断数据域部分是否存在。

但是你能保证你所接收的数据都是准确的吗?你能确保在工作环境下不会因为各种干扰导致数据长度信息由 0x05 变成 0x85(最高位翻转)吗?,如果出现了会导致什么后果?

假设你采用 RXNE 中断方式来一个、一个字节的接收数据,分析如下:

因为是单字节接收数据,所以你需要把所有接收的数据当成数据流,根据帧头信息来确定帧的开始,一旦确定帧头信息之后,你就可以根据接下来的一系列数据保证一帧数据的结束,同时开始新帧的接收……

初看这个接收流程没有问题,但是真的如此吗?

但是就像前面所说,你能保证你的数据没有问题吗?如果说你接收到一个长度信息,本来是 0x05,但是最终接收的数据是 0x85,这就意味着你接下来的数据域的长度是 0x85,根据你的接收流程,你需要再接收 0x85 个字节之后,才能判断校验字节是否正确。

可能你会说,虽然你的长度信息由 0x05 变成了 0x85,之后接收校验过程肯定是失败的,那么这帧数据就会被接收程序丢弃,从而导致接收程序进入重新寻找帧头的流程,这个过程不是挺正常的吗?按理说上述异常情况是能被接收流程处理掉的。

那么首先确认一点,上述异常能被接收流程处理吗?

答案是能!

既然上述异常是能被接收状态机处理的,那么还会有什么问题?

问题就出在这个错误数据本身!

因为你是根据错误数据来决定接下来需要接收多少数据,而一般来说,接收缓存大小设置为最大帧的长度,那么就出现一个问题,你的缓存够你接收 0x85 个字节吗?

如果说你开辟的接收缓存空间很大,足够接收这么多数据,那么就算遇上以上情况,也是没有任何问题,但是万一你比较节省资源,缓存不够大会出现什么情况?

这就涉及到内存分配问题:

你的串口缓存一般在 Data 区域,一旦你接收的数据超出了你开辟的空间,那么必然导致缓存空间溢出!

那么缓存空间溢出会导致什么危害?

我们通过上图可以知道,一旦缓存溢出,必然导致该缓存周围的数据出现异常(数据被篡改),如果你的其它代码刚好需要这个数据作为重要参考,而你在使用的时候又没有对这个数据的有效性进行检查,那么可能导致另一个灾难性后果,而这个后果又导致了其他后果,从而导致雪崩效应。

而你修复这个 bug 时,你以为修复了,但你只修复了表面,真正内在 bug 还存在!

所以,千万别太相信内存中的数据,每一个数据的输入都要进行严格检查,这个数据可以错误,但是不能导致程序崩溃!

所以千万别写能篡改别人数据的代码,这是很危险的事情,也是很难解决的 bug,因为你不知道它会在什么时候篡改哪里的数据!

再假如你的接收缓存放在栈中了呢(稍微有 C 语言常识的程序员都不会把串口接收缓存放在栈中,但鱼鹰偏偏遇到过这种代码,而为了解决这个 bug 整整花了一星期,这还是在 bug 复现率高的情况下)?

根据前面的图可知,栈一般存放在高地址,并且一般栈生长方向为向低地址生长。如果出现上述情况(接收的数据大于开辟的栈缓存空间)会发生什么?

栈帧被破坏!

灰色部分因为接收的数据太多,导致原本存在的栈数据被串口的接收的数据修改了(注意篡改的数据可能不是连续的,因为每一次进入时,开辟的那部分栈空间可能都不在同一个地址),假如这个数据是保存返回寄存器 LR 的,那么必然导致返回错误,极可能触发 HardFault 中断!

那么有什么办法解决栈被破坏的问题?

最有效的方式鱼鹰觉得是使用 ITM,如果无法在线调试,可以尝试 DMA 循环传输 PC 指针值(但是如何得到这个值?毕竟这个寄存器本身是没有地址概念的)到一块内存中,这样就可以得到最后正常执行的代码地址,从而定位错误代码的位置。

如果单片机不支持这些功能呢?鱼鹰现有的知识体系好像无法解决,只能佛系调 bug 了(看和 bug 之间的缘分),囧。

前面说了由于外部工作环境导致数据长度信息错误从而出现数组溢出这种情况,如果说你保证工作环境非常好,不可能出现这种干扰,是否还会出现问题?

当然会!

前面分析了外在原因,现在分析内在原因,你的接收程序能保证及时接收发送端发送过来的数据吗?如果不及时接收数据会出现什么问题?

我们知道,一个系统一般都有很多中断需要处理,如果说你的接收程序的中断优先级不是最高的,那么很可能出现接收程序无法及时接收的情况,即 RXNE 中断来临时,因更高优先级中断需要处理,而且处理时间较长,那么就会出现当前接收的字节因为没有接收完成而被后续的数据冲掉,即出现 ORE(溢出错误)。

这样会导致什么问题?

数据域信息(也可能是校验值等数据)当成了长度信息(为什么只讨论长度,而不讨论功能字之类的数据,难道他们不会出现 ORE 的情况吗?),这样一来,如果这个数据很大,接收程序就会以为接下来还需要接收很多数据才能完成一帧的接收,导致后果和前面分析的数据干扰一样严重。

那么采用 RXNE 接收方式时该怎么解决这种问题?

检查长度信息的合理性,只要长度信息不会导致缓存溢出即可。

但是上面的解决方案会导致什么问题?

因为你的程序设计问题(采用 RXNE 接收导致不能及时处理),使得原本能接收的数据无法及时处理(DMA 可以及时处理),最终使得当前这一帧数据无法正常接收(如果错误的长度信息够大的话,还有可能接下来很多帧数据都无法接收),这你能接受吗?

但是采用 DMA 为什么就不会有上述问题,除了 DMA 能自动接收数据提高效率之外,还有一点就是它不根据接收的数据来判断接下来还需要接收多少数据,而是根据设定的接收数据长度来接收的(如果加入 IDLE 中断,可以提前结束接收工作),这就避免了上述的缓存溢出和接收不及时问题。

最后再分析上述接收的另一个问题,那就是一帧数据中可能出现没有数据域的情况,这种情况该怎么处理?

只要根据长度信息分开处理即可。如果不对没有数据域的情况分开处理,那么你接收的下一个数据直接就是校验值,而你的接收流程却认为这是数据域的数据,必然导致校验失败。

现在总结使用 RXNE 方式接收的几个问题:

1、缓存溢出。

缓存溢出有两种可能,第一种就是环境干扰导致长度信息出错,从而出现缓存溢出情况;第二种情况就是因为接收不及时,导致数据错位,如果刚好是长度信息错误,并且这个长度信息太大,而你的代码未对长度进行检查,那么也会出现缓存溢出 bug,而这种 bug 一旦出现,很难发现。所以在代码中对数据的合理性检查是非常有必要的一件事。

2、中断及时处理。

如果中断不及时处理,会导致数据错位,轻则丢失至少一帧数据,重则缓存溢出!

3、状态机是否需要接收数据部分。

由于数据帧有可能没有数据域的情况,所以必须区别处理,保证代码接收的准确性,否则有可能把校验值当成数据了,这样必然无法通过校验,这一帧数据必然会丢失!

串口空闲

前面一直提到串口空闲,也大概明白串口的作用,但是一些细节问题还是需要好好说一下的。

第一个问题,如何清除串口空闲中断标志位?

很多人会使用 USART_ClearFlag 标准库函数进行清除,但是当你跳转到该函数原型时,你会看到如下说明:

你会看到很多标志位是无法通过该函数清除的。

那么该如何清除 IDLE 标志呢?其实上面的注释已经进行了说明。

PE、FE、NE、ORE、IDLE 标志位的清除是通过一个软件序列进行清除的:首先通过 USART_GetFlagStatus 读取 USART_SR 寄存器的值,然后通过 USART_ReceiveData 函数读取 USART_DR 的值即可。

那么这里就有一个问题,是否这些标志问题的清除都要单独编写清除序列呢?

答案是否定的。

因为这些标志位都是由同一种序列进行清除的,所以只要一个清除序列就会把所有的标志位都进行清除了(同样一旦执行了这个序列,也就意味着你无法再通过 USART_SR 寄存器获得标志位了)。

为了保证获取标志位,我们可以在清除序列之前把 USART_SR 寄存器的值保存到副本中,然后再读取 USART_DR 寄存器的值保存到副本来实现清除功能,注意该序列应该无条件执行(不在某个判断语句中)。这样后续我们就可以使用这个 USART_SR 的副本判断哪一个标志置位了,同样也可以使用 USART_DR 的副本获取串口数据,而为了实现以上效果,USART_GetFlagStatus 这个函数就不合适了,只能直接操作寄存器去实现。

第二个问题,在线调试时对空闲中断会有影响吗?

我们知道,KEIL 能够将一个结构体的数据全部读取出来,而库函数将串口模块的所有寄存器都封装在一个结构体中,这样就会出现一个问题,如果你的窗口是实时刷新的,当你使用 KEIL 读取串口模块寄存器的时候(不管是使用 peripheral 窗口还是 Watch 窗口),就会出现先读取 SR 再读取 DR 的情况, 这样就有可能出现 KEIL 和单片机 CPU 读取这两个寄存器冲突的情况。

如果全速运行时,KEIL 先执行了这个序列(通过调试器读取这两个寄存器的值),单片机 CPU 再读取 SR 寄存值,必然是无法读取到正确标志位的,因为这些标志位已经被 KEIL 的读取序列清除了(这个情况鱼鹰确实碰到过,当时明明下发了数据,但是单片机无法获取标志位),所以在调试串口时,注意不要让 KEIL 去读取这些寄存器(即关闭这些窗口,只有在必须的情况下才开启),防止出现莫名其妙的情况。

第三个问题,空闲中断能准确触发吗?

如果从接收端考虑的话,如果触发了空闲中断,那么必然满足了条件才触发的,而不是意外触发的(嗯,我们要相信 STM32),但从发送端考虑的话,有可能出现一帧数据断续发送,导致一帧数据触发多次空闲中断,所以如果是简单的 DMA+空闲中断方式接收是很有问题的(空闲出现就认为一帧结束了,就会把一帧数据当成两帧处理,这样肯定无法通过数据检查的)。

那么先来分析为什么会出现一帧数据多次触发空闲中断情况。我们知道 linux、windows 系统并不是实时系统,当应用程序需要发送一帧数据时,可能并没有连续发送,而是发送完一个字节后去处理其他事情后才发送下一个字节,这样一来,如果耽误的时间够长,就会触发串口的空闲中断,从而一帧数据当成两帧处理了。

那有什么方法可以解决呢?鱼鹰提供两种解决思路。

第一种,使用两个缓存空间,一个缓存空间专门用于接收串口数据,将接收到的数据存放到另一个缓存,这个缓存采用字节队列的方式进行管理,应用程序从缓存队列中一个字节一个字节的取出数据进行处理(注意检查数据有效性),这样就能保证及时处理。但是因为空闲中断不再可靠,所以空闲中断不再作为判断一帧数据结束的依据(根据长度信息判断),而是只在空闲中断中将已接收数据复制到字节队列缓存中,这样就可以处理意外的空闲中断。

第二种,还是一个缓存空间,还是 DMA+空闲方式处理,但是需要增加额外的条件。就是当进入空闲中断后,不再直接处理,而是获取当前接收时刻,然后在处理数据的时候根据这个时刻来判断是否达到足够的空闲时间,只有在进入空闲中断后并达到一定延时之后才认为一帧数据结束了,这样可以避免一些非常短的空闲时间。

以上问题是就是鱼鹰以前使用空闲中断从未考虑的问题,鱼鹰并不知道使用空闲中断还可能出现误触发的情况,但是既然知道了,就要想办法解决。但是为什么以前使用空闲中断时没有出现通信问题呢?

事实上不是没有问题,而是有可能把分散的一帧数据的两部分直接丢弃了而已,因为有重发机制,所以即使丢弃一帧数据,也能通信正常,而且这种一帧数据分散成两部分的概率还是挺低的,ubuntu(linux 系统)下大概千分之三左右的样子。

第四个问题,如果单片机没有空闲中断又该如何做?

当我们使用 RXNE 的同时其实我们也可以使用空闲中断,这样也能确定一帧数据的结束(但是要注意前面的误触发问题)。但是如果有些低端单片机(如 51 )没有空闲中断又该怎么办?

其实我们可以从 stm32 的空闲中断得到相应的启发。

所谓空闲中断,就是当串口接收到数据后,在应该接收数据的时刻,发送方并没有发送数据,所以串口模块置位空闲标志位,从而引起空闲中断。

那么我们是否可以软件模拟串口模块的这个功能,从而确定一帧数据的结束呢?

答案是肯定的(前提是每一帧数据之间有空闲时间)。

我们可以使用一个定时器,定时器向上计数。当接收到一个字节数据后,初始化计数器并启动定时器,这样一旦有一段时间没有接收到串口数据(也就不再初始化计数器),那么定时器溢出,进入溢出中断,而这个溢出中断就类似于串口的空闲中断(在溢出中断中关闭定时器以达到清除空闲中断标志的作用),这样就达到了串口空闲中断的效果(和前面问题的第二种解决方案类似)。

通信吞吐量

在以上分析过程中,都是采用主机发送,从机接收后再回复主机的方式进行通信,虽然通信正常,但实际上效率比较低下,单位时间传输的数据量较少,如下图所示:

红色部分就是必要的空闲时间,可以看到左右两张图的通信频率是有差异的,右图中从机必须等待前一帧数据发送完毕才能处理数据,而左图可以在接收当前帧时处理上一帧数据,类似 CPU 的指令执行流水线。

(图片来源于《权威指南》)

我们也可以将串口接收分为二级流水线:接收、处理,如此一来,我们最少需要两个缓存空间,当一个缓存在接收时,另一个缓存就进行数据处理。发送端可能不等接收端发送完应答数据,它就已经开始发送下一帧数据了,只要相邻两帧数据保证一定发送间隔,就能正常触发中断。

同理,因为接收端也不再慢悠悠的等待接收数据,而是可能有好几帧数据等着它处理,所以为了确保发送端能正常触发空闲中断,也需要控制发送间隔。

为了最大程度利用串口,我们可以使用队列管理很多缓存空间(当只有两个缓存时,可以直接使用异或运算进行缓存切换),比如 uCOS II 中我们可以利用系统的内存管理服务和队列服务实现有效管理,并且当有非常紧急的通信任务时,还可以插入到队头优先处理。

但是增大吞吐量时,比如对重发机制和从机数据的确认有一定影响,需要考虑清楚。

如果要用一句话总结本篇笔记内容,那就是使用 空闲中断+DMA+队列+内存管理+定时控制 方式接收串口数据会是不错的选择。

本文转载自: 鱼鹰谈单片机
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。

51单片机采用中断进行串口通信

所谓中断方式,就是串口收/发标志位出发中断后,在中断中执行既定操作,可通过函数调用来实现。

接收数据时: 等待中断->然后在中断中接收数据

发送数据时: 发送数据->等待中断->然后在中断中发送数据

具体步骤如下:

确定 T1 的工作方式(编程 TMOD 寄存器);
计算 T1 的初值,装载 TH1、TL1;
启动 T1(编程 TCON 中的 TR1 位);
确定串行口控制(编程 SCON 寄存器);
串行口在中断方式工作时,要进行中断设置(编程 IE、IP 寄存器)。
注:SCON 是一个特殊功能寄存器,用以设定串行口的工作方式、接收/发送控制以及设置状态标志:

有关波特率的计算方法

在串行通信中,收发双方对发送或接收数据的速率要有约定。通过软件可对单片机串行口编程为四种工作方式,其中方式 0 和方式 2 的波特率是固定的,而方式 1 和方式 3 的波特率是可变的,由定时器 T1 的溢出率来决定。

串行口的四种工作方式对应三种波特率。由于输入的移位时钟的来源不同,所以,各种方式的波特率计算公式也不相同。

1
2
3
4
方式0的波特率 = fosc/12
方式2的波特率 =(2SMOD/64)· fosc
方式1的波特率 =(2SMOD/32)·(T1溢出率)
方式3的波特率 =(2SMOD/32)·(T1溢出率)

当 T1 作为波特率发生器时,最典型的用法是使 T1 工作在自动再装入的 8 位定时器方式(即方式 2,且 TCON 的 TR1=1,以启动定时器)。这时溢出率取决于 TH1 中的计数值。

T1 溢出率 = fosc /{12×[256 -(TH1)]}

注:PCON 中只有一位 SMOD 与串行口工作有关, SMOD(PCON.7) 波特率倍增位。在串行口方式 1、2 和 3 时,波特率与 SMOD 有关,当 SMOD=1 时,波特率提高一倍。复位时,SMOD=0。
在单片机的应用中,常用的晶振频率为:12MHz 和 11.0592MHz。所以,选用的波特率也相对固定。常用的串行口波特率以及各参数的关系如表所示。

80C51 串行口的工作方式

方式 1 是 10 位数据的异步通信口。TXD 为数据发送引脚,RXD 为数据接收引脚,传送一帧数据的格式如图所示。其中 1 位起始位,8 位数据位,1 位停止位。

(1) 方式 1 输出
(2) 方式 1 输入

用软件置 REN 为 1 时,接收器以所选择波特率的 16 倍速率采样 RXD 引脚电平,检测到 RXD 引脚输入电平发生负跳变时,则说明起始位有效,将其移入输入移位寄存器,并开始接收这一帧信息的其余位。接收过程中,数据从输入移位寄存器右边移入,起始位移至输入移位寄存器最左边时,控制电路进行最后一次移位。当 RI=0,且 SM2=0(或接收到的停止位为 1)时,将接收到的 9 位数据的前 8 位数据装入接收 SBUF,第 9 位(停止位)进入 RB8,并置 RI=1,向 CPU 请求中断。

定时/计数器的结构与原理

定时/计数器的实质是加 1 计数器(16 位),由高 8 位和低 8 位两个寄存器组成。TMOD 是定时/计数器的工作方式寄存器,确定工作方式和功能;TCON 是控制寄存器,控制 T0、T1 的启动和停止及设置溢出标志。

加 1 计数器输入的计数脉冲有两个来源,一个是由系统的时钟振荡器输出脉冲经 12 分频后送来;一个是 T0 或 T1 引脚输入的外部脉冲源。每来一个脉冲计数器加 1,当加到计数器为全 1 时,再输入一个脉冲就使计数器回零,且计数器的溢出使 TCON 中 TF0 或 TF1 置 1,向 CPU 发出中断请求(定时/计数器中断允许时)。如果定时/计数器工作于定时模式,则表示定时时间已到;如果工作于计数模式,则表示计数值已满。

可见,由溢出时计数器的值减去计数初值加 1 才是计数器的计数值。

设置为定时器模式时,加 1 计数器是对内部机器周期计数(1 个机器周期等于 12 个振荡周期,振荡周期也叫时钟周期,时钟周期即晶振的单位时间发出的脉冲数,如 12MHZ=12×10 的 6 次方,即每秒发出 12000000 个脉冲信号,那么发出一个脉冲的时间就是时钟周期,即 1/12 微秒;如 11.0592MHZ=11.0592×10 的 6 次方,即每秒发出 11059200 个脉冲信号,那么发出一个脉冲的时间就是时钟周期,即 1/11.0592 微秒)。计数值 N 乘以机器周期 Tcy 就是定时时间 t 。

定时/计数器的控制

80C51 单片机定时/计数器的工作由两个特殊功能寄存器控制。TMOD 用于设置其工作方式;TCON 用于控制其启动和中断申请。

工作方式寄存器 TMOD

工作方式寄存器 TMOD 用于设置定时/计数器的工作方式,低四位用于 T0,高四位用于 T1。其格式如下:

M1M0:工作方式设置位。定时/计数器有四种工作方式,由 M1M0 进行设置:

控制寄存器 TCON

TCON 的高 4 位用于控制定时/计数器的启动和中断申请。其格式如下:

  1. TF1(TCON.7):T1 溢出中断请求标志位。T1 计数溢出时由硬件自动置 TF1 为 1。CPU 响应中断后 TF1 由硬件自动清 0。T1 工作时,CPU 可随时查询 TF1 的状态。所以,TF1 可用作查询测试的标志。TF1 也可以用软件置 1 或清 0,同硬件置 1 或清 0 的效果一样。
  2. TR1(TCON.6):T1 运行控制位。TR1 置 1 时,T1 开始工作;TR1 置 0 时,T1 停止工作。TR1 由软件置 1 或清 0。所以,用软件可控制定时/计数器的启动与停止。
  3. TF0(TCON.5):T0 溢出中断请求标志位,其功能与 TF1 类同。
  4. TR0(TCON.4):T0 运行控制位,其功能与 TR1 类同。

定时器 1 的工作方式 2

方式 2 为自动重装初值的 8 位计数方式。

计数个数与计数初值的关系为:X = 2^8 - N

其中:X 为要装的初值 N 为要定时/记数的次数。

注:工作方式 2 特别适合于用作较精确的脉冲信号发生器。所以在进行串口通信时一般选用定时器 1 工作在方式 2 这种经典模式。

程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76

/*51单片机采用中断方式的串口通信程序分析:
接收数据时 等待中断->然后在中断中接收数据
发送数据时 发送数据->等待中断->然后在中断中发送数据
具体步骤如下:
确定T1的工作方式(编程TMOD寄存器);
计算T1的初值,装载TH1、TL1;
启动T1(编程TCON中的TR1位);
确定串行口控制(编程SCON寄存器);
串行口在中断方式工作时,要进行中断设置(编程IE、IP寄存器)。
*/

#include <reg52.h>
#define uchar unsigned char

uchar Temp,RIflag,TIflag;

//串口初始化函数

void serialportinit() {

TMOD=0x20;//设置定时器1为工作方式2 8位自动重装载 作用是产生波特率
TH1=0xfd;//设置波特率位9600bps
TL1=0xfd;
TR1=1;//开启定时器1

//设置串口工作在方式1
//方式1: 8位异步收发 波特率可变(由定时器控制) 收发一帧的数据为10位 一个起始位(0)8个数据位 1个停止位(1)
//先发送或接收最低位
SCON=0x50;//等价于 SM0=0; SM1=1; REN=1;

//SM0=0;
//SM1=1;
//REN=1;//允许串行接收位 允许串行口接收数据
PCON=0x00;//SMOD=0 波特率不加倍

EA=1;//开总中断
ES=1;//开串口中断 注意:如果使用查询方式进行串口通信时,要把串口中断ES关闭、
}



//定义数据发送函数
void sentTemp() {

SBUF=Temp;//把Temp接收的数据再发送到发送缓冲器SBUF中

//注意:51单片机内部有两个物理上独立的接收、发送缓冲器SBUF(属于特殊功能寄存器) 两个缓冲器共用一个特殊功能寄存器 字节地址(99H?
while(!TI);//等待从机向主机发送数据完成
TI=0;//若发送完成 把发送中断标志位软件清0 因为TI必须由软件清零
}

void main() {
serialportinit();

while(1) {

if(RIflag==1) {
ES=0;//关串口中断
RIflag=0;//接收标志位清0
sentTemp();//调用数据发送函数
ES=1;//开串口中断
}
}
}

//串口中断服务函数
void serialportint() interrupt 4 { //串口中断函数

if(RI) {
RI=0;//接收中断标志位RI必须由软件清0
Temp=SBUF;//把接收缓冲器中收到的数据赋值给led
P1=Temp;//通过开发板监测是否接收到主机发送的数据
RIflag=1;//接收标志位置1 表示从机接收收据完成
}
}

嵌入式基本概念

  1. 中断
    CPU 在处理某一事件 A 时,发生的另外某一事件 B 请求 CPU 去处理(产生了中断),随后 CPU 暂时中断当前正在执行的任务,去对事件 B 进行处理,CPU 处理完事件 B 后再返回之前中断的位置继续执行原来的事件 A,这一过程总称为中断。

  2. 单片机中断源
    单片机一共支持 5 个中断源,其中 2 个外部中断源,3 个内部中断源
    (1)外部中断 0,由 INT0(P3.2 引脚)输入。

(2)外部中断 1,由 INT1(P3.3 引脚)输入。

(3)定时/计数器 0 溢出中断(T0)请求。

(4)定时/计数器 0 溢出中断(T1)请求。

(5)串行口发送/接收中断请求。

  1. 中断服务函数格式
1
2
void 函数名(void)interrupt 中断号
void Int0(void) interrupt 0
  1. 单片机片内定时计数器的主要区别
    1)模式控制寄存器 TMOD 的 D2 或是 D6 位来控制。D2 或 D6 位为 0 时,选择定时工作方式;为 1 时选择计数工作方式。

2)在定时方式时,计数脉冲输入信号由内部时钟提供;计数方式时计数脉冲来自于相应的外部输入引脚。

3)定时器是对单片机的机器周期数进行计数;计数器对外部输入脉冲信号计数,当信号产生由 0 到 1 的跳变时计数器的值加一。

模式控制寄存器的 D1D0 或是 D5D4 位用来选择定时/计数器的四种工作模式,00 表示模式 0,01 表示模式 1,10 表示模式 2,11 表示模式 3。这 4 中操作模式各有特点及优势,可以根据需要选择合适的操作模式。

树莓派可以做什么

作为一个伪Geek,我去年下半年看到树莓派4出来之后,小手一抖买了两个。一些朋友问我:树莓派可以用来干啥?本篇就来总结一些树莓派的部分用途,从不同的领域来分析可能的应用场景,主要从四个方向介绍(本文略长):

作为个人电脑的使用场景
作为服务器的使用场景
作为多媒体终端的使用场景
作为物联网设备的使用场景
树莓派是什么
树莓派是一系列为编程教育而设计的只有信用卡大小的微型电脑(单板计算机),拥有丰富的硬件接口,能够安装任何ARM平台的操作系统,比如Linux的一些发行版、Android、Windows IoT等。下图是当下的一款RaspberryPi 4 Model B(图片来自http://www.yahboom.com):

一台这么多接口的单板计算机,实际只有掌心大小(RaspberryPi Zero/ZeroW系列只比大拇指大一点),树莓派3/4系列的四核Cortex A53/A72 CPU功耗低,算力不弱。最重要的是竟然还这么便宜,加上配件也只要3位数的¥!

作为一台个人电脑,树莓派可以…
树莓派4B出来的时候,某些标题党自媒体宣传“替代PC”,但Linux系统的PC市场份额很低,桌面软件生态不完善,作为PC用途而言,注定只是一个小众选择。我其中一台树莓派4B来当PC用,总体体验不错,下面从一个软件开发者的使用角度一一分析。

办公用途

阿特伍德定律告诉我们:一切可以用Javascript写的东西最终都会用Javascript写,推导一下就是:一切以前在客户端做的事情,最终都将可以在浏览器上做。

各种软件的Web化,让浏览器成为操作系统之上的“操作系统”,树莓派3B+就可以比较流畅地运行Chromium,树莓派4B表示打开数十个Web页面毫无压力。所以,能跑浏览器就可以满足大多数普通办公场景,树莓派CPU和GPU的算力也完全足以应对简单的办公场景了。

那么办公用途安装什么系统合适?对于普通终端用户,安卓系统的软件生态则更加繁荣,装Android可以覆盖更多办公娱乐场景(毕竟已经破产锤子TNT工作站已经验证了安卓PC的可行性 );专业领域用户装Raspbian则更合适,比如我这样的程序猿。

教育用途

树莓派基金会是一个慈善组织,让更多人可以接受到编程教育是树莓派的初衷。买一个老少皆宜的树莓派给孩子玩,比去什么少儿编程强多了。个人认为孩子参加少儿编程课程是没有必要的。计算机科学是建立在基础学科根基上的,而编程语言又是建立在计算机科学上的,连数学物理基础都没有的低年级孩子学积木式编程,如建空中楼阁。让孩子玩的开心、锻炼逻辑思维的方式很多,除非孩子对计算机有极大的兴趣,否则我不建议低年级孩子刻意参与编程教育课程。

但在树莓派上参与编程和计算机相关教育活动,是有别于现在市面上的课程的,因为玩树莓派学计算机更具有灵活性,Raspbian系统自带各种真正的编程教育软件和多种语言的初级IDE软件,有兴趣可以自由地深入探索,无兴趣不如多去读读书或者参加户外活动。下图是经典的编程教育软件Scratch的截图(Scratch当前版本也已经Web化,甚至无需安装客户端也可以玩,不一定要在树莓派上运行,图片来自Scratch网站 https://scratch.mit.edu/explore/projects/)

游戏用途

树莓派可以玩Minecraft,也可以运行复古游戏机模拟器,比如RetroPie (https://retropie.org.uk)。能在树莓派上玩的游戏有限,可能更适合小众动手能力很强的Geek的需求。

编程开发用途

Python开发:非常棒,现在Python不仅在机器学习领域是头把交椅,在嵌入式开发也是动态脚本语言第一(感谢
@limyel
提出,Python是强类型而不是弱类型的语言,前一版搞混了)。树莓派上使用Python的优势在于:
树莓派上Python的硬件开发库非常完善,甚至很多库都兼容Arduino上跑micropython
Python本身的跨平台特性,脚本写好放在哪都能跑,比如在树莓派上跑Tensorflow甚至是PyTorch都可以!不过在机器学习方面,树莓派的算力就捉襟见肘了。
Golang开发:差强人意。能写能跑,但调试不方便,因为Go的Debug工具dlv对ARM的支持2020年才初步解决(https://github.com/go-delve/delve/issues/118 )。另一个问题是Go的主场不在嵌入式开发,硬件相关库兼容性和稳定性不足。我尝试过Golang的硬件驱动库https://github.com/google/periph,比Python的庞大完善的生态还差很多。
JS/TS开发:还不错。VS Code作为JS/TS,甚至是C# .Net Core开发的最佳工具,既可以本机直接运行VS Code也可以用VS Code Remote模式。大部分常用的VSCode插件在树莓派上都能正常运行,具体来说:
前端开发体验完美,虽然Webpack构建时间比Intel i7的机子大概慢一倍多,但Hot Reload仍然是秒级,丝毫不影响开发
使用JS/TS开发NodeJS后端体验完美,常用的带C++ Binding的npm库也都能在ARM下正常编译
嵌入式开发不够好,生态不完善,一些硬件模块在npm找不到合适的库,目前rpi-gpio库在树莓派4上有问题,毕竟ECMAScript的主场在大前端领域,不是Python的对手。
小结
通过上面几个例子可以看出,作为开发者是可以尝试使用树莓派来替代PC做日常开发的,主流编程语言及其工具链、三方库大多保持着对ARM平台的兼容性。中小型项目在开发阶段对计算资源要求并不高,树莓派接上屏幕配个键盘,或是VS Code Remote进行远程开发,都挺好的。当然其局限性也不少,比如:ARM Linux下很难做传统C/C++的客户端软件开发;算力有限,大型项目和一些细分领域的专用软件只能用x86平台的中高端PC。

作为一台服务器,树莓派可以…
树莓派更多的用途是在服务器端。可以只在局域网使用,也可以搭配DDNS技术把树莓派挂到公网上(一般的企业甚至电信家庭宽带都可以申请到公网IP,或者直接买一台乞丐配置的云服务器也可以拥有公网IP),不到5W的功耗打造个人云服务器,一年只要20块钱电费,性价比极高。下面来细数一下具体用途。

注:实践这些应用场景,容器化技术是最佳选项,比如先装上Docker或者Containerd,再安装各种Docker镜像就可以满足大部分需求。对于多个树莓派组建集群的场景,推荐Rancher的K3S或者用Docker Swarm,K3S和Kubernetes在使用上几乎感觉不到区别,更轻量但一样强大。

Web服务器

树莓派的算力部署普通的Web服务绰绰有余了,制约并发能力和延时的的可能会是家庭宽带的上行带宽(家庭网络上行带宽远低于下行带宽,300Mbps的下行带宽或许只有30Mbps的上行带宽)。非高并发和高带宽要求的服务,完全可以用树莓派搭建服务端,比如:个人网站,App或小程序后台等等(前提是有固定公网IP,或者使用DDNS把非固定公网IP的树莓派挂到公网)。树莓派的官方网站据说就是18个树莓派服务器集群组成的。

因为各种主流的高级语言和开源组件几乎都支持ARM平台,所以选择熟悉的语言和技术来开发就行了,和在Intel/AMD CPU的Linux上运行差别不大。精简指令集的ARM低功耗优势在服务端非常明显,这几年ARM服务器的出现也印证了这一点。

网格计算

网格计算是分布式计算的一种形式,举个例子,我们可以用树莓派参与SETI@home项目,帮科学家们搜索外星人!全世界的志愿者都可以用算力闲置的设备安装BOINC客户端参与项目,分析射电望远镜的数据寻找可疑的电磁波。BOINC全称是伯克利开放式网络计算平台,现在已经有数十个项目可以参与,如果你不相信有外星人,也可以用BOINC客户端来参与其他更实际的项目,比如我比较喜欢这几个:

Rosetta@home 预测蛋白质结构和蛋白质设计
lhc@home 帮助CERN的大型强子对撞机模拟粒子的运行

注:一些项目在Windows上运行时会有酷炫的屏保(如上图),动态展现当前计算的状态,Linux下命令行运行不会有屏保图片。

我个人服务器上一直用50%的CPU在运行这些项目分摊下来的计算任务,树莓派也可以运行,但相对来说慢一些。我小时候的梦想是从事基础科学的研究,最终却因为种种原因变成了一只程序猿,贡献一些算力也算圆了一丢丢科学梦吧。

同属于分布式计算领域的区块链最近比较火,区块链技术其中一个应用就是虚拟货币,俗称挖矿。树莓派也可以挖矿,只是效率比较低。除了网格计算,还有两个云计算衍生的概念:边缘计算、雾计算,乍一听云里雾里的,大概意思就是:局域网有一堆传感器和硬件设备数据,找个算力还行的设备先汇总计算一下,做一些在内网就能做的事情。具体到应用,比如作为智能家居的中枢、影音娱乐中心等等,具体细节到下面讲物联网方面再展开。

私有云

云存储、云办公等公有云的服务对于大部分人来说已经很方便了,但存在隐私和数据安全方面的担忧,高质量的云服务必然是有代价的,最典型的例子就是某度云的网盘限速逼着用户充会员。

私有云大多在企业中应用,但其实每一个人都可以有一朵属于自己的云,既能像云盘一样随时随地存取和备份文件,又可以覆盖日常办公大多数场景,如文档/表格/PPT/思维导图/UML图编辑、笔记/便笺、任务规划、日历日程和联系人管理等等,所有的数据既掌握在自己手里,又可以任何时间地点访问和同步数据。这么多强大的功能,竟然在树莓派上部署一个NextCloud就全有了!部署这样一整套私有云,对熟悉Linux的人来说,完全是30分钟以内就能搞定的事情。

NextCloud是我看到过的最酷的PHP项目。上几个图来感受一下这开箱即用,功能齐全的私有云系统吧!

文件存储是核心功能,手机上安装NextCloud的App实时同步相册,备份联系人,上传下载文件,是非常实用的功能。导航栏还安装了一些主流的官方和社区的插件,“更多”里面还有:管理个人密码,手机位置追踪等我觉得很不错的插件。另外Office办公套件和http://Draw.io画图插件也非常棒,社区还有很多优秀的插件来满足各种需求。

其中手机追踪插件(PhoneTrack),需要手机安装PhoneTrack的App上传位置信息,然后就可以画出自己每天的轨迹,甚至包括海拔信息。

非IT从业者,或者没玩过LNMP技术栈的话,下载NextCloud Pi系统镜像写入SD卡,树莓派直接启动就可以快速体验NextCloud。关于树莓派支持安装的操作系统,我也整理了一下,用NextCloud的脑图插件,画了个思维导图,这个脑图也是在树莓派上画的。

私有云的用途写的比较多,因为这是我觉得这是老少皆宜最实用的防树莓派吃灰的用途!NextCloud除了强大的插件体系扩展了这个私有云系统的能力,其衍生产品NextCloud Talk甚至还可以用来聊天和语言视频通话。如果不用NextCloud系列搭建私有云,也有其他类似的软件可以尝试。

有人可能会觉得小小的树莓派,怎么可以胜任云存储?几千块钱的群晖NAS固然高端大气,但大部分人只是想同步一下手机相册,备份一下数据,偶尔记记笔记画画图,真的需要磁盘冗余阵列,万兆存取速度吗?用树莓派的USB3.0接个移动硬盘,插上网线,局域网1000Mbps的速率也足够了,5W功耗吊打专业NAS,写个爬虫脚本放上去运行又能作为一台资源下载机。至于公网访问,用DDNS + 免费TLS证书这种方案弄到公网,访问速度也远高于不充会员的百度云盘。

服务端的应用场景就介绍到这里吧,上述的用途还没有用到树莓派大部分硬件接口,下面细说一些跟硬件相关的用途。

作为一个多媒体终端,树莓派可以…
从上面的思维导图看,树莓派有不少多媒体硬件接口。音视频的输入输出也可以带来很多应用场景,下面一一介绍。

家庭安防监控

树莓派的CSI接口接上摄像头,写个录像脚本就变成了最简易的家庭监控摄像头,进阶一点可以搭建一个RTMP Server做实时视频流,想当个主播,把视频流转到直播平台就可以了。

只是作为监控摄像头效果肯定不如海康,360,小米等公司的成熟产品,但树莓派的优势在可扩展性,发挥想象力能够DIY出无限的可能性。我感触很深的一个例子是有一个大佬颇有创新的树莓派项目——共享鱼缸,用树莓派给自家水族馆直播,观众可以互动投食。详细信息参考:https://shumeipai.nxez.com/2017/09/27/nature-aquarium-for-sharing.html

家庭影音娱乐中心

树莓派加上一根HDMI线连到电视上,就是一个机顶盒,搭配OpenELEC/Kodi系统,当一个家庭多媒体中心也是不错的选择。但现在版权控制非常严格,即使OpenELEC虽然很优秀,又有丰富的插件,但新的影视资源并不好找,各家网络视频公司和运营商也是封闭式的发展各自的App和会员系统,他们拿到版权并不想把影视资源付费开放出去。如果有资源的话,把这些影视资源用装了OpenELEC系统的树莓派管理起来,是能够代替家里的机顶盒的,无需忍受各种广告。

音视频通信系统

拇指大的树莓派ZERO就足够扩展成一个小电话了,之前也看到过有Geek把树莓派电话做成了一个真正的产品。我尝试过编译BareSIP(一个C语言写的SIP终端软件)把树莓派变成一个视频会议设备,开Zoom会议或者其他支持SIP/H.323协议接入视频会议是没有问题的,会议期间CPU使用率仅有25%,音视频也很流畅,缺点是裸露的3.5mm耳机接口杂音比较大。对于开发者来说可以二次开发来解决问题,甚至变成真正的商用产品,也有人这么做了。

对于个人用户来说,Raspbian客户端软件的生态不齐全,比如没有ARM Linux版本的Zoom客户端,只能间接通过SIP终端接入(前提是需要买Zoom的Conference Room Connector的服务),如果是装安卓系统来实现音视频通信可能更方便一些。

注:上图为树莓派上BareSIP拨号后的输出,可以配置GPU对视频流硬件编码,帧率还不错。

作为物联网设备,树莓派可以…
上面的用途,基本都不会用到那40Pin引脚,而树莓派的40Pin引脚,是它变身物联网设备和边缘计算中枢的精髓所在。利用树莓派的通用输入输出(GPIO)能力,可能会找到是最富有创新点和最具有Geek范儿的用途,做无人机、做机器人、做智能音箱,还有无数的可能性等待发掘和探索。

从基础的电子积木开始——硬件模块控制中心
树莓派GPIO引脚直连传感器或者其他硬件模块,或是用UART等方式连上其他的MCU。树莓派收集到数据,做一些处理和控制逻辑,再做一些云端同步和消息推送之类的,就可以告诉别人实现“雾运算”了。

简单的搭积木式地组合一些硬件,是入门嵌入式开发的一个方法,如果具体到一些的实际场景,可以衍生出各种应用,比如门禁系统、3D打印机监控和控制组件、无人机图传组件等等,都可以基于树莓派去做。

智能小车

智能小车很多电子电气专业的同学在学校都做过,基础的功能有避障、测距、循迹、遥控等等。51系列单片机的例子比较多,而树莓派加个底盘、轮胎,连上传感器、马达、电源,也能摇身一变智能小车,网上可以找到很多树莓派小车的案例。

简单的智能小车显然实用价值不高,发散思考一下,放大一些可以做智能儿童玩具车,装上刷子可以做扫地机器人,配上室内定位系统可以变成一个智能仓储的运输车。不过工程化研发和量产是有很多挑战的,实际上真正的电动玩具车和扫地机器人也不可能用树莓派作为MCU,但这并不妨碍我们拿树莓派去学习、探索和尝试。

机器人、无人机

这两个产业算是IT技术的集大成者,涉及多个学科的前沿科技,技术含量和门槛都很高,但也有一些大佬用树莓派做成了简单的机器人、无人机的。这里说的机器人并不是简单遥控的“伪机器人”,而是说具有感知能力甚至人工智能的产品。比如斯坦福机器人俱乐部的四足机器人项目:

DogGo:斯坦福大牛们创造的开源四足机器人,使用的MCU是Teensy系列的板子 https://github.com/Nate711/StanfordDoggoProject
Pupper/Woofer:斯坦福机器人俱乐部的二代目四足机器人,使用的MCU正是树莓派ZEROhttps://github.com/stanfordroboticsclub/StanfordQuadruped
关于无人机,我一直有个梦想是组装一个自己的四轴无人机(目前还没有提上日程)。无人机最关键的部分是飞行控制系统,简称飞控,Geek们用的大多是当前最流行的开源飞控平台APM。APM发源于Arduino,目前也支持Linux运行,因此树莓派也是可以运行APM飞控的的。另一个开源飞控PX4,适用于Pixhawk的硬件,直接跑在树莓派Linux系统似乎不太方便。

直接拿树莓派跑飞控做无人机难度还是比较高的,简单的途径是用树莓派做图传,飞控用专用模块,积木式拼接来组装一个无人机。当然,要真正创造它们不仅需要软硬件开发能力和很强的动手能力,深究下去还需要数学、物理以及衍生学科的知识作为理论基础。

更进一步,更高级的感知和预测等AI能力离不开机器学习,在这方面也有一些可以在树莓派上跑的开源机器学习平台,比如:

Tensorflow Lite:Tensorflow的轻量化版本,适用于边缘计算,可以用来在树莓派上训练模型,实现简单的人工智能应用
Pytorch:Facebook的开源机器学习库,最近比较火,也能在树莓派上跑起来
最直接的例子是有Geek拿树莓派玩自动驾驶,有一些是基于Donkey Car项目开发的(Donkey Car是一个基于Tensorflow的开源自动驾驶项目,训练过程需要在高性能GPU上进行,树莓派算力不够)https://github.com/autorope/donkeycar

智能音箱

2014年亚马逊Echo发布之后,智能音箱产业蓬勃发展,2020年已是一片红海,占据国内大部分市场的只剩小米的小爱音箱,天猫精灵,百度小度这几个终端产品了。智能音箱这么火,我们是不是可以打造一个自己的智能音箱呢?其实智能音箱的最核心的几项技术:语音唤醒、语音识别、自然语言处理/语义理解、语言播报,各大厂开发者平台都提供了API,要自己打造一个“没有核心技术”的智能音箱,做API的集成就可以了,毕竟只有技术和数据方面的大厂才有可能拥有智能语音和NLP领域最好的模型。

如果用树莓派做智能音箱的话,软件方面可以选择开源项目Wukong Robot(https://github.com/wzpan/wukong-robot),代码清晰,并且已经实现了很多服务API的集成;硬件方面最好用麦克风阵列获取音频输入,推荐ReSpeaker 4麦克风阵列扩展板,远场识别效果还是不错的。

我拿其中一个树莓派4 + ReSpeaker 做了个“智障”音箱,另外装了DLNA的服务器和客户端(minidlna和gmediarender),手机可以连上去放音乐,但感觉相比专业的智能音箱差别还是太大了:

一个是唤醒灵敏度和准确性的问题,这个问题我家小爱音箱也比较严重,经常误唤醒
另一个是延迟和内容质量问题,集成第三方API比大厂音箱自己原生支持要差了一个级别,无论是响应时间,还是准确度和回答内容的质量,都比不上真正的智能音箱,而且个人无法得到优质内容的版权也是个问题。
当然,专业和业余的区别就在这里,虽然质量比不上,但DIY的优势在于灵活性,想集成什么只要开发代码就可以了。

智能家居中枢

要在自己家里部署智能家居,除了购买成熟的解决方案,比如小米系智能家居产品或者天猫精灵系的产品以外,还有个办法就是DIY智能家居。DIY有什么好处呢?

买带有智能功能的家电时,不用管这个品牌是站队了小米系还是站队了天猫系,或是自成一体要下载单独App的,开源社区有方案就用,没方案逆向工程搞起
实现不同品牌产品互通互联,自己定制非常灵活的自动化方案,无需受限于米家等App的功能
便宜,可以选用性价比更高的产品,小米生态链的智能家居产品性价比参差不齐,并不都是高性价比的产品
有捣腾的乐趣,可以扩展自己想要的功能,比如易微联的智能开关甚至可以拆开给ESP模块烧录自己写的固件
那么DIY智能家居具体要做什么呢?

除了选购或改造电子电器产品外,还需要有一个智能家居中枢负责所有硬件的控制和状态同步,以及自动化的实现。这个智能家居中枢控制系统的角色,树莓派完全可以胜任。

如果想多写一些代码,可以考虑基于NodeRed做
不想写太多代码的话,Home Assistant是不二之选
Home Assistant是一个Python的开源智能家居项目,功能非常强大,扩展性也很不错,社区活跃程度极高,已经实现了很多主流家居产品的集成。用Home Assistant能实现打通各种主流家电品牌的智能或非智能产品(非智能的传统产品通过红外控制等手段,也可以转变成为智能产品)。试想一下让iOS的Siri助理开一个十年前的电视机,或是让天猫精灵控制米家的风扇,是不是很激(ji)动(lei)?

要在树莓派上运行Home Assistant(或者其他地方,比如NAS上),用http://Hass.io部署更方便。http://Hass.io是包括了Home Assistant以及周边组件的一套东西的合集,另外,树莓派直接刷Hassbian系统也可以达到相同的效果。

运行成功之后需要将所有智能产品作为实体,通过Yaml的方式配置到系统中,再配置UI、添加一些自动化规则。

Home Assistant有一定的学习成本,我也只尝试了一些插件,学习了简单的流程,折腾过ESPHome并且添加了ESP8266系列的开关模块,但没有真正把家里改造一番。下图分别是一个卡片布局的Hass系统UI,以及一个配置了Floorplan的UI,平板装到墙上统一可视化控制,非常酷炫(图片来自网络,没找到真正的原创是谁)

其他类似产品

除了树莓派系列,Geek们常玩的还有一些AVR(Arduino系列)、ESP(ESP8266系列)、STC系列(89C51,89C52等)单片机。但这些板子的算力远低于树莓派这样的单板计算机,而下面介绍的是一些算力与树莓派相近的产品。

Nano Pi系列:也是一系列小巧的单板计算机,性价比也很高,不同配置的型号很多,有兴趣可以试一试
BananaPi/OrangePi:俗称香蕉派/香橙派,兼(shan)容(zhai)树莓派的开源硬件产品(树莓派硬件并不开源),据说稳定性不如树莓派,目前4代树莓派4核Cortex-A72 CPU的性能成倍提升,感觉这些板子几乎没有性价比优势了
Rock Pi及基于瑞芯微RK系列芯片的开发板:主打高性能和音视频处理能力,国内大多数机顶盒和智能电视都是RK系列芯片,RK3288、RK3399等等ARM芯片,相关的单板计算机产品性能是超过树莓派的,而且主板大多早已支持双摄、双4K视频输出,搭配安卓系统在生态上非常成熟。当然缺点就是价格和配件也稍贵一些。
Intel Edison/Galileo/Joule/Curie:英特尔的x86嵌入式板子,我有一块曾经在英特尔的好朋友赠送的Edison板子,做工精致,400MHz的双核Quark处理器,虽然算力有限,但是能直接跑x86的二进制程序,硬件接口兼容Arduino,是好东西,但价格不菲,而且前几代已经停产。
其他小众板子,大多数跑Linux的都ARM架构的,比如华硕ThinkBoard、荔枝派等等,也有例外,比如某宝上还有卖国产RISC-V架构的平头哥芯片开发板。
树莓派的博通SoC中的CPU是ARM架构的,其他同算力级别的单板计算机,CPU也大多是基于ARM的,少数是基于x86架构的芯片。

精简指令集(RISC)的ARM架构芯片,通常比复杂指令集(CISC)的x86架构芯片功耗低很多,这对物联网和移动端,乃至服务端都至关重要,除非以后电池技术出现革命性发展。这也是我们日常生活中各种电子产品,ARM芯片的身影越来越多的原因。最近苹果甚至宣布了一个新闻:以后Mac系列的PC和笔记本电脑也要用ARM芯片了!

结语

以上列举的所有树莓派的使用场景,大部分是从网络上搜集到的,都是有人已经做过的,我结合了一些个人经验和理解整理介绍了出来。这些也只是无数极客们探索尝试过的一小部分。上面的介绍涉及了很多领域,但树莓派一定还有很多未被发现的用途。

树莓派作为一台Linux计算机,在软件开发方面就有很多应用场景;而硬件和嵌入式开发方面,即使不了解数电、模电等这些电子专业的知识,我们也可以用树莓派加上现成的模块,搭一搭简单的电子积木。站在巨人肩膀上,让半导体中电子的转移按照我们想要的方式,变成声音、电磁波、热量、动能,或是感知这个世界,岂不是一件极其有趣的事情?

mac远程连接raspberry

当身边没有显示屏或通过HDMI连接笔记本屏幕不成功时,本教程教你通过最少工具(无屏幕,鼠标,键盘),连接raspberry开发板。

准备工具

  • mac电脑
  • type-c电源线
  • sd卡
  • 读卡器
  • raspberry imager
  • raspberry OS
  • raspberry 开发板
  • tightvncserver
  • vnc viewer

安装Pi OS系统

根据如下视频,安装OS到SD卡

注意:

  1. 参考视频,烧录前,正确设置ssh和wifi的密码,方便通过mac直接远程ssh连接PI

启动系统

当SD卡烧录完成,插入开发版,并启动系统。

ssh连接开发板

查看开发板ip

参考《查看mac共享热点所有链接设备

连接开发板

通过mac,打开终端,输入:

1
ssh pi@开发板ip

安装tightvncserver

终端连接成功后,输入

1
sudo apt-get install tightvncserver

配置tightvncserver

终端输入

1
tightvncserver

此时会提示设置一个8位的登陆密码,以及是否view-only的选项,选no即可。
设置完成后,终端输出如下信息,表示成功

1
2
3
4
5
New 'X' desktop is hzz:1

Creating default startup script /home/pi/.vnc/xstartup
Starting applications specified in /home/pi/.vnc/xstartup
Log file is /home/pi/.vnc/hzz:1.log

mac客户端vnc viewer连接

通过连接下载vnc viewer
安装完成后,打开vnc viewer,输入:开发板ip:端口,如上述:hzz:1,其中1表示连接端口

vnc连接成功后,如下图:

mac烧录raspberry pi镜像

此篇博文用于记录在MacOS系统上为TF卡烧录树莓派操作系统。

下载镜像

网址:https://www.raspberrypi.org/downloads/

我试验下来,上图中的Raspberry Pi Imager for macOS并不好用。因此,我们点左下角的Raspbian图片下载镜像。

这边目前有三种版本:

系统+桌面+推荐软件
系统+桌面
系统
我选择了最简洁的Raspbian Buster Lite,下载.zip并解压出其中的.img文件。

TF卡格式化

首先需要下载格式化工具,我这边使用的是SD Memory Card Formatter,这个软件在windows和macos上都可以用。这里是macos下的下载链接:https://www.sdcard.org/downloads/formatter/eula_mac/index.html

格式化

  • 插入TF卡
  • 打开SD Memory Card Formatter

注意不要格式化错了卡,假如你插入了多个TF卡。上图中的Volume label是格式化后磁盘的命名。

开始烧录

查看驱动器列表

在控制台输入命令:diskutil list

这里,我们获取到TF卡的磁盘路径为/dev/disk2

取消TF卡的挂载

在控制台输入命令:diskutil unmountDisk + SD卡设备路径

烧录

在控制台输入命令:sudo dd if=镜像路径 of=SD卡设备的路径 bs=1m;sync,并输入管理员密码。

注意:文件路径不要出现中文。可以将bs=1m改为bs=4m加快烧录的速度。

这个时间有点长,需要耐心等待,400M的镜像大概耗时2分钟左右。

编写树莓派的ssh配置与wifi配置文件
新建两个.txt文件,分别命名为:

1
2
ssh
wpa_supplicant.conf

注意,取消.txt后缀。

ssh文件为空即可,wpa_supplicant.conf文件中写入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
country=CN
ctrl_interface=DIR=/var/run/wpa_supplicant Group=netdev
update_config=1

network={
ssid="Wifi1的名字"
psk="密码"
priority=优先级,越大越优先
}

network={
ssid="Wifi2的名字"
psk="密码"
priority=优先级
}

然后将这两个文件移动至烧录系统镜像后TF卡的根目录。

这里有两个坑点:

  • SSID名字中不能有符号.
  • 优先级范围为1-10

推出TF卡

diskutil eject SD卡设备路径

参考
MacOS下树莓派烧录img/iso文件到SD卡
mac下烧写树莓派系统
树莓派raspbian系统自动连接WIFI开启ssh