如上图整个智能家居程序总体框架图,还剩下网络子系统没有实现,以及最终的业务子系统没有实现。
如上图所示是乐鑫的多种网卡芯片,本喵使用的是其中的ESP8266
,具体性能参数可以参照上图。
如上图所示,该芯片只用连接四个引脚,除开供电的正负外,剩下的两个引脚分别是串口的发送端和接收端,和MCU相连。
通过串口发送指令给esp8266
芯片让其做相应的操作,但是这些指令并不是随意发送的,而是要按照一定的规定。
如上图所示,我们通过串口给esp8266
芯片发送AT指令,芯片会解析指令并做出响应,指令正确会返回OK
,错误返回ERROR
,也是通过串口返回给MCU。
- AT指令:AT command set,叫做AT指令集或AT命令集,一般称其为AT指令。
如上图所示是ESP8266
AT指令的参考手册,需要的小伙伴自取(文章末尾会放资源链接)。
如上图,在手册中给了一些演示示例,模仿这些实例中AT指令的用法就可以去操作esp8266
。
本喵这里将提前写好的代码烧录到板子里来演示一下esp8266
的简单使用,这部分测试代码本喵就不讲解了,可以自取(文章末尾会放资源链接),只是一些简单的串口基本配置,后面本喵会直接使用这些配置。
- 用串口助手发送内容给MCU(UART1),MCU就将对应的内容发送给
esp8266
(UART3)。esp8266
的反馈给MCU什么内容,MCU就将对应的内容通过串口助手发给我们。
将开发板和PC端连好并烧好程序后,打开串口调试助手:
如上图,通过串口助手发送指令AT
用来测试esp8266的通信是否正常,当接收到OK
时,说明此时和MCU的通信是正常的。
- 注意:
AT
指令必须以回车换行结束,所以在串口调试助手中必须勾选发送新行。
接下来就是配置esp8266
的网络连接了:
发送指令AT+CWMODE=3
,将esp8266
配置成softAP+station
模式,此时该模块自己就是一个网络热点,而且也可以连接其他热点,受到反馈OK
以后说明设置成功。
如上图是可以配置的模式,其中1表示Station
模式,表示该模块可以接入网络,2表示SoftAP
表示该模块可以作为一个热点(就像我们的WIFI路由器),3表示两种模式都有,具体用法自行去查阅手册。
发送指令AT+CWJAP="WIFI名称","密码"
让esp8266
模块连接WIFI,第一个""
中的字符串是WIFI名称,该模块不支持5G,所以不要连接后缀有5G的网络,第一个""
中的字符串就是WIFI密码。
指令发出后,得到绿色框中的反馈,最终返回OK
说明WIFI连接成功,此时该模块就连网了。
发送指令AT+CIFSR
查询该模块的IP地址和MAC地址,如上图中绿色框中的就是该模块的IP地址和MAC地址。
这里有两个IP地址和MAC地址,APIP,"192.168.4.1"
表示该模块作为热点的IP地址是192.168.4.1
。
STAIP,"192.168.2.15"
表示该模块接入到网络中的IP地址是192.168.2.15
,同一个局域网中的其他网络设备可以通过这个IP地址找到该模块,所以我们使用的也是这个IP地址。
至于MAC地址这里我们并不会用到。
发送指令AT+CIPMUX=0
将该模块设置成单连接模式,得到反馈OK
说明设置成功。
- 单连接模式就是和该模块进行网络通信的设备只有一个,多连接则是有多个。
sscom
软件,但是PC端必须和esp8266
处于同一个局域网,在左下角设置本地端口号8080,远端IP地址(模块IP地址)以及远端端口号。
- 本地IP地址是由路由器分配的,打开软件后自动就有,端口号由我们自己决定。
再通过串口助手发送AT+CIPSTART="UDP","192.168.119.1",8080,1112,2
指令,让esp8266
模块和PC端的sscom
建立UDP连接,其中192.168.119.1
是远端sscom
的IP地址,8080是远端的端口号,1112是自己(模块)的端口号。
最后一个参数设置为2时表示UDP通信的远端可变,而不是只能固定一个远端和esp8266
模块通信。
发送指令AT+CIPSEND=7
后得到反馈OK
和一个>
,表示可以发送数据了,该指令等号后面的7表示要发送7个字节(包括回车换行)。
发送123456
后,得到了模块的反馈Rev 7 bytes
表示受到了7个字节数据,以及SEND OK
表示发送成功。
如上图所示,此时在远端的sscom
中就可以看到esp8266
发送来的数据。
如上图,用远端的sscom
向esp8266
发送数据abcdefg
。
此时esp8266
模块就受到了远端发送来的数据,并通过串口调试助手给我们显示出来。
发送指令AT+CIPCLOSE
得到反馈OK
后,说明该模块和远端断开UDP连接,此时模块就接收不到远端发送的数据了,并且也不能无法向远端发送数据,除非重新建立UDP连接。
现在已经认识了esp8266
模块,接下来就该实现我们的网络子系统了。
如上图所示是网络子系统的层次示意图,只看网络子系统,它是从网卡设备管理层开始的,上面两层是最后要将网卡设备作为一个输入设备接入到输入子系统中,现在先只从网卡设备管理层开始往下看。
如上图所示,网卡设备有很多,比如本喵使用的esp8266
,esp32
自带的网卡,或者Linux网卡等等,为了方便维护和扩展,将所有的网卡设备放在管理层中管理起来,首先要做的就是抽象描述网卡设备的结构体:
如上图所示是网卡设备NetDevice
结构体的定义和注册网卡设备以及获取网卡设备的函数声明。
在网卡设备结构体中,由于可以将实例化不同类型的网卡设备对象,所以要有一个名字name
来标识不同的网卡。
在前面介绍esp8266
模块时,模块和远端进行通信时,必须有模块IP地址,模块端口号,远端IP地址,远端端口号,所以描述网卡设备时,要有char ip[20]
数组用来存放模块的IP地址,char mac[6]
来存放模块的MAC地址(虽然当前用不到)。
每一个网卡设备在使用之前和其他设备一样,都需要进行初始化,所以要有初始化的方法Init
函数指针。
网卡设备要和连接局域网的WIFI,所以提供一个Connect
方法来连接网络,在参数中传入WIFI名称和WIFI密码。
网卡设备在连接上WIFI后,局域网中的路由器会给网卡分配一个IP地址,为了方便获取网卡的IP地址以及其它信息,需要一个GetInfo
接口。
网卡设备要和远端建立通信,提供了一个CreateTransfer
方法,在创建连接时,需要指定连接类型Type
,以及网卡的端口号,至于远端的IP地址,本喵内嵌到了程序中,因为和我们进行通信的远端并不会改变。
网络通信结束后,需要和远端端口连接,同样也提供了一个CloseTransfer
方法用来端口连接。
网卡设备是肯定要和远端进行通信的,所以必须有发送数据Send
和接收数据Recv
的方法,这个项目中网卡设备并不会给远端发送数据,这里只是为了完善才写上的。
在接收数据时,接收到是数据保存到Data
中,还有要接收到的数据长度,这两个参数都是输出型参数,最后还要有超时时间,不能一直处于接收状态,超出这个时间就退出接收。
如上图源文件中代码所示,创建了一个全局的pNetDevice
链表用来管理网卡设备,并且实现了注册网卡设备和获取网卡设备函数。
此时管理网卡设备的方法已经有了,接下来就是实现网卡设备的管理系统了net_system
:
如上图,网卡管理系统中,只有添加所有网络设备以及获取指定网络设备的方法,在添加所有网络设备函数的实现中,由于目前只有ESP8266
,所以只有一个AddNetDeviceESP8266
函数,有其他网络设备只需要再增加相应的添加函数即可。
管理层和系统层等上层已经实现了,接下来就是实现ESP8266了:
如上图头文件代码所示,仅有一个添加ESP8266
网卡设备的函数声明,供上层net_system
添加所有设备时调用。由于NetDevice
结构体中所有函数的实现都是static
类型的,所以都不用在这里声明。
如上图源文件中代码,是NetDevice
结构体中那些方法的实现,初始化的时候,先设置esp8266
芯片的复位引脚(本喵使用的这一款不用设置),接下来就是初始化MEC和esp8266
芯片进行通信的接口,然后就是配置WIFI模式,以及设置成单连接状态。
- 对于
ESP8266
芯片的配置是按照前面演示过程中的方式配置的,可以对照这里和前面演示部分的标号来看。
在给ESP8266
发送指令时,使用ATCommandSend
函数(暂时还没有实现)来发送指令AT指令,也可以使用OtherCommandSend
函数来发送其他类型的指令,处理发送指令本身,还需要设定超时时间,即使没有设置成功在一定时间后也要返回。
- AT指令的发送函数在AT层实现,这样同样是为了实现分层,将
ESP8266
模块和指令类型解耦。
在发送指令时,先要将不同的形参拼接成一个完整的AT字符串指令,使用sprintf
库函数即可。
对于接收数据的函数ESP8266Recv
的实现,在还是内部调用了ATDataRecv
,这是针对AT指令的接收数据方式,其他指令模式对应的方式不同,所以这个函数也是在AT层实现的。
对于获取ESP8266
IP地址及网卡信息的函数本喵要单独讲解一下:
如上图代码所示,当发送指令AT+CIFSR
后,MCU会受到esp8266
芯片发送来的设备信息,包括两个IP地址和两个MAC地址,以及一个OK
反馈。
我们要从这一整个字符串中解析出我们需要的IP地址,所以使用strstr
C库函数查找子串"+CIFSR:STAIP,\""
,该字串后面的内容就是我们需要的IP地址,然后将IP地址按字节放入ESP8266
网卡设备结构体变量的ip成员数组中。
- 字符串中要注意转义字符。
由于调用ESP8266GetInfo
函数时,传入一个输出型参数,所以在函数调用结束之前,将获取到的IP地址使用strcpy
C库函数拷贝给输出型参数。
如上图代码所示,创建一个ESP8266
的全局结构体变量,并且初始化结构体中的成员,网卡设备名字就是esp8266
,IP地址和MAC地址给一个初始值,再用前面实现的那些函数来初始化函数指针。
添加网卡设备的函数中,调用net_device
设备管理层中的注册函数。
现在管理层已经实现,接下来就是实现AT命令层了。
如上图代码所示,在AT
层中包含接口初始化,发送指令的函数,发送指令并接收数据的函数,以及接收数据的函数。
在源文件中,创建了一个全局的接口设备变量,在当前层是不用关心该结构是UART
还是I2C
的,初始化中,需要先获得接口设备,然后调用它的初始化方法。
如上图所示是发送AT指令的函数实现,指令内容是在设备层调用该函数时传入的,在发送指令时,调用接口设备的Write
方法,将指令写入后,再写回车换行。
- 为了防止干扰,在每次发送指令
cmd
之前,都调用InvalidRecvBuf
方法清空环形缓冲区。
指令通过结构设备发送给ESP8266
模块以后,要等待该模块的反馈,在等待之前获取一下当前的时间pre
,然后再等待反馈。
等待反馈的过程中,使用接口设备的ReadByte
方法一个字节一个字节读取数据,读取到的数据按顺序放入buf
中,当读取到’\n’字符时,判断前一个是否是\r
,如果是,说明得到完整的一行数据。
再判断该行数据中是否有OK
或者ERROR
,如果有则得到反馈,如果没有则再接收下一行数据,将前面的抛弃。
如上图所示代码是判断OK
和ERROR
的函数。
每接收一个字节数据并判断完成后,还需进行超时判断,再获取当前程序所在位置的时间now
,如果当前时间比pre
前一刻时间大,说明程序在指向并耗费时间,所以超时时间iTimeoutMS
减一,并且更新前一刻时间pre
,当超时时间变为0后说明已经超时了但仍然没有得到想要的,则超时返回。
为了方便调试,在config.h
中增加了答应调试信息的功能:
如上图所示,如果需要打印调试信息,则在这里定义DEBUG
,之后就可以使用debug_printf
打印调试信息到串口了(UART1
)。
如上图所示是发送AT指令并且接收ESP8266
的全部反馈信息的,同样在通过接口写指令之前要将缓冲区清空,然后等待模块的返回信息,等待裸机和ATCommandSend
一样,只是这里要将所有返回的数据都保存到传入的Data
数组中。
如上图所示代码是用来接收ESP8266
通过接口发送给MCU的数据的,就像在认识esp8266
模块的时候,用远端sscom
发送数据到模块,此时就调用该函数来接收这些数据。
接收到的数据格式是+IPD,8:1234
样子的,其中前缀+IPD
表示这是接收到的网络数据,,
逗号后面的数字8表示有效数据的大小是8个字节,:
冒号之后的是有效数据。
采用状态机模型,将接收网络数据分为三个阶段,分别是起始阶段INIT_STATUS
,解析有效数据长度阶段LEN_STATUS
,接收有效数据阶段DATA_STATUS
。
最开始处于INIT_STATUS
阶段,通过接口设备中的ReadByte
方法接收数据,在读取到的数据中查找是否存在+IPD
子串,如果存在说明这是网络数据,状态变为LEN_STATUS
,并且将接收到的前缀丢弃掉,因为后面不用再用到了。
再继续接收数据,如果接收到的数据中有冒号:
,说明此时已经读取到了长度,冒号前面的数据就是有效数据的长度,解析出有效数据的长度,将状态变为DATA_STATUS
,并且丢掉前面的数据,也是因为以后不用了。
此时接收到的数据就是有效数据,是真正需要的数据,每接收一个字节的数据就放入到传入的数组中,直到接收的数据长度和前面解析出的长度相符才停止接收。
同样也有超时处理,就不多解释了。
此时就可以继续实现串口设备层了。
前面的AT层一直都在使用接口设备来收发数据,但是接口设备并没有实现,只是空头支票,接下来就是实现接口设备了。AT指令一般有UART
和I2C
两种传输方式,由于本喵使用的是UART
方式,所以这里也只实现这一种方式:
如上图所示,定义了描述串口设备的结构体,包含串口设备的名称,初始化方法,清空缓冲区的方法,以及通过串口读写数据的方法,还有一个过去串口设备环形缓冲区的函数声明。
使用typedef
给UARTDevice
重命名为ATInerFaceDevice
,将pUARTDevice
重命名为pATInterFaceDevice
,此时在就实现了封装,在AT层之间使用重命名后的类型即可。
如上图所示代码,创建一个全局的环形缓冲区,供串口设备读写数据,并且创建了串口设备的全局变量并进行了初始化,实现了串口设备中的各种方法。
接下来就是实现老生常谈的部分了,内核抽象层和芯片抽象层。
在操作串口设备的时候,不同类型的操作系统操作方式不同,和之前一样,要抽象出一层内核抽象层和芯片抽象层,分层原因和依据本喵就不再说了,前面已经说过好多次了。
如上图代码所示,内核抽象层中,只有对串口的写函数,并没有实现读等其他操作,因为给esp8266
模块写数据采用直接写的方式,而读数据采用中断的方式,所以读函数会在中断部分实现。
如上图代码所示,在芯片抽象层中写数据函数会调用一个USART_SendBytes
,这是对于HAL库方式的,其他方式则调用相应的函数即可,本喵这里并没有列举。
最后就是硬件操作,我们整个网络子系统的实现完整了。
硬件操作就是具体操作本喵使用的STM32F103ZET6
芯片的UART3
了:
如上图所示头文件,包含串口3使能,发送多个字节数据,以及注册输入处理回调函数的函数声明。
如上图源文件代码所示,实现了串口3的中断使能函数以及发生多个字节数据的函数,发生多个字节时,通过检测寄存器中SR
位来判断一个字节是发送完成,如果完成则发送下一个,直到所有字节都发送完,这里并没有使用中断发送方式。
- 串口3中断使能函数中也只使能了接收中断。
定义了一个接收中断函数中的回调函数,该函数是一个全局的函数指针,在中断函数中会回调该函数来用指定的方式处理接收到的数据。
在中断函数中,先定义了一个静态的环形缓冲区,这是串口的标配缓冲区结构,给用串口3的环形缓冲区来初始化它,当数据到来时,将接收到的数据按字节放入该缓冲区中一份。
如果回调函数指针不为空,则再调用回调函数来处理这个数据,现在不考虑回调函数,只有Recv
方法来接收网络数据。
接下来就是测试我们的代码能否正常执行,接收到网络数据:
如上图代码所示,首先就是添加网卡设备并初始化等一系列基本操作。一开始要先延时2s,让ESP8266
模块启动完成,然后连接到WIFI,连接过程放在一个循环中,如果连接成功则跳出死循环,否则就一直连,因为如果没有脸上WIFI我们的测试就没有意义。
连接成功以后就是创建网络连接,指定连接类型(本喵仅实现了UDP连接)和模块的端口号,创建成功则继续,否则直接返回。
最后在一个死循环中不断获取网络数据,使用网卡设备中的Recv
方法,如果接收到则通过URAT1
打印出来。
如上图所示,将代码烧录到开发板中,并且上电以后,通过串口调试助手可以看到上面信息,准备工作全部完成后,开始循环检测网络数据的到来。
如上图所示,在远端发送数据,ESP8266
就将接收到的数据通过串口调试助手打印出来,此时说明我们的网卡设备可以正常工作。
本文所用到的所有工具,手册,以及源码,都放在这里传送门,需要的小伙伴自取。