前言
4 X$ m9 ~& v: O5 A/ s这一节,我们来聊聊 STM32 的 FOTA 例程中用到的 wifi 模块:ESP-01。ESP-01 是安信可公司基于 ESP8266 wifi 芯片的WIFI 模块。在 STM32 FOTAdemo 里,用来实现无线通信。下面我们将来认识一下这个模块,并介绍 demo 里相关底层驱动的实现。; m, ?. S6 h4 d1 l" V/ T7 B
! P7 \! d/ F: q) O4 ?$ SESP-01 模块
2 h7 y0 s$ @# U1 J# r( o/ F G; DESP-01 模块集成 esp8266EX WIFI 芯片,支持 802.11b/g/n 协议,支持 UART/GPIO 等接口,内嵌 LwIP 协议栈,支持STA/AP/STA+AP 工作模式,是一款低成本的无线模块。8 o6 M7 K5 v5 X2 p* z, p
" u7 d% E* c8 z/ Z7 x
ESP-01 模块采用 DIP-8 封装。尺寸和引脚定义见下图。提供一个 UART 接口和两个 GPIO 口。
' v% e% C$ c$ D# J& z5 j2 D% s' E% I1 D! y# M
- [2 w# j9 O- c9 E
/ j. c7 h8 L6 X; P2 f6 ^2 q6 GSTM32F769 探索板的 CN2 接口支持对 ESP-01 模块的扩展,可以直接将 ESP-01 模块插在 CN2 接口上。STM32F769 和ESP-01 之间通过串口通信,串口配置为:115200 波特率,8 位数据位,无奇偶校验,1 位停止位。连接如下:
: p5 ]6 u4 K2 ^ `2 e1 `8 C2 ?5 Z
- x9 q) W/ Q& T9 W8 j$ m! f, x0 y: Q" T
* V) b$ E) |" X" c1 u" u4 h; i
. a* _* f+ ^" w/ Q. l. Y& x
AT 指令格式3 D: k/ l4 Z+ b$ F
ESP-01 的 AT 指令集就是 ESP8266 的 AT 指令集。可以细分为四种类型:
7 Z4 Q8 D# F" f% A3 y6 B
. W8 a L% Z7 e
/ |+ [ K5 \; q( ~4 K
: W6 c# g* g7 P5 w要注意的是,% l5 A4 \3 j/ \0 O; w
不是每条 AT 指令都具备上面 4 种类型,具体要去看该条 AT 指令的说明。$ `4 d4 g. f9 h! n/ L
使用双引号表示字符串数据。比如“123”,就是一个字符串,而 123 就是数字。/ A# B8 ^+ @7 B$ x0 o9 C
开头的 AT 指令两个字符必须大写,每条命令以回车换行符结尾“\r\n”2 s* `" h( l+ n2 D) T
3 M6 G$ Q- T! H% m
$ p" A1 x( M$ B) o! bESP8266 的 AT 指令集又分为:基础 AT 命令(对模块的配置,串口设置等),WIFI 功能 AT 命令(设置 wifi 模式,连接 AP等),TCP/IP 功能 AT 命令(建立 TCP 连接,收发数据等)。. a% h3 h% g7 S) D8 m9 L/ G# K- U
, n# b3 @! f9 x |# h7 U
* s* j8 ]# o* D0 Y' F4 D5 [作为 TCP 客户端工作
( [2 f1 S! ~+ g9 y! L, D9 g模块初始化
% @# k9 p% [/ f4 O: \! U程序上电运行后,在开始使用 WIFI 模块前,必须先对其进行初始化。除了 GPIO 口以及串口的初始化外,还需要对 WIFI 模块进行配置,使其工作在我们希望的模式下。在 STM32 FOTA demo 里 ESP8266 应该工作在 STATION 模式和多连接模式下。所以在初始化的时候,需要通过相应的 AT 指令进行配置。下面是初始化的代码以及 AT 指令执行的过程。, j) q5 \; u" ]" b
- ESP8266_StatusTypeDef ESP8266_Init(void)
: b( Y S; J @, q - {
/ u: C( T) @4 \1 p1 j& d, o; J9 [ - ESP8266_StatusTypeDef Ret;
7 k, x, i0 C7 ]6 C - . z: f/ @% s z A$ |
- /* Configuration the IO low layer */* p" G8 q- e- w; V$ G% c( j) ~
- if (ESP8266_IO_Init() < 0)% N0 s' ?6 W2 r# V' s
- {
; V' p! o" M4 a9 m4 s" d - return ESP8266_ERROR;& E! O9 j9 J8 S- H( S! b
- } m; C2 q K' j: K* H
- /* Disable the Echo mode */ <font color="#00ff00"> //关回显功能</font>
& N5 ~7 m7 c* ?1 d: P$ ~1 s - /* Construct the command */
+ w' ^! z! P( N5 w0 u: C - memset (AtCmd, '\0', MAX_AT_CMD_SIZE);
- o% F' P5 }- U" L1 W - sprintf((char *)AtCmd, "ATE0%c%c", '\r', '\n');
" s( k8 Q$ ~% |8 _: ^ - " c- e8 @1 M1 Z8 P5 B
- /* Send the command */
& X" S) D- c7 w: n2 Q1 M! I1 W - Ret = runAtCmd(AtCmd, strlen((char *)AtCmd), (uint8_t*)AT_OK_STRING,NULL);3 I! T3 l2 n0 |6 W7 Q
4 k. i1 p4 i2 @' c; {- /* Exit in case of error */
. X! o: t! F; @; B3 U' [9 d( w - if (Ret != ESP8266_OK)& H! x' S9 G/ ^4 b9 j
- {
; c& S; B( l! R3 O, q3 a: ^1 F: C0 y t - return ESP8266_ERROR;
+ B* m2 n; u% B h) J) t) x9 Y6 `7 U - }
& `' o6 ?3 l: m2 t& P1 L, ?, O/ U - 3 f+ V8 k' |5 d* C! D1 u4 n8 o/ y q
- /* Setup the module in Station Mode*/ <font color="#00ff00"> //设置 wifi 模块工作在 STATION 模式</font>
, {# Z/ _1 h& A2 A3 | - /* Construct the command */
1 n1 y9 W6 j% O1 c - memset (AtCmd, '\0', MAX_AT_CMD_SIZE);
: X" U4 E; D& ^, H% K7 U. c0 v - sprintf((char *)AtCmd, "AT+CWMODE=1%c%c", '\r', '\n');+ s1 F; B# x" B* I! t
- # b+ Q6 Y; I; f! H
- /* Send the command */
) T, i* Z+ n$ D$ i1 K# f+ ?1 t6 x - Ret = runAtCmd(AtCmd, strlen((char *)AtCmd), (uint8_t*)AT_OK_STRING,NULL);4 P; B- |$ s2 @. [$ v* ~
) W: F7 l5 D2 q+ t' R4 R3 H- //colse all socket connection
. n2 t# r4 n9 o - if(ESP8266_CheckMUXStatus()==1) <font color="#00ff00"> //设置多连接模式</font>
' n, J& U7 i# A# V% x - {
0 [& l7 e$ i' M' d: `+ @' [
& M- S. a4 n* i( _/ b- uint8_t i=0;
, g$ @. {2 j: _4 ?/ h) i - for(i=0;i<4;i++)
6 \- s# D9 ?+ v, P w# K/ ? - {% y. @; n' T5 Q$ O! F8 F" h% c
- ESP8266_CloseConnection(i); //don't check and return the returned status
1 g/ W! i, H$ A- v6 H. ] - }7 x& {) G: i! p u3 n. P
- }$ G; T8 W F( W# X# Q: J
- else //this application is mult-connection application,if MUXMODE is 0,it means this is$ n9 t+ X8 \) P% {8 B$ Y
- the first time run4 m5 u3 S3 V6 L2 ]" u, V
- ESP8266_SetMuxMode(1);" o" L; h3 h! D/ `! z L* l* d d
- / m( d# m( o8 H7 p; {
- return Ret;4 }5 l5 T6 B& g7 x
- }
复制代码 : h, y! \: n* a1 t1 x3 S
AT 指令的运行记录:6 ?0 ~- ]5 T; H/ r9 r9 b+ D
# t: b8 }% {5 h, G$ m4 X, s! L
S, m+ w$ }0 p) Q9 G连接到 AP5 e3 b0 J f' ?, \
调用 ESP8266_JoinAccessPoint 函数,输入 AP 的 SSID 和密码,连接到对应的 wifi 热点。0 D$ p8 ?) F" H5 p
- ESP8266_StatusTypeDef ESP8266_JoinAccessPoint(uint8_t* Ssid, uint8_t* Password)
, I3 \3 ^ `% c. c% b - {: v/ Y0 X8 x: G3 Q" X# h z
- ESP8266_StatusTypeDef Ret;
( T/ u3 k( f3 S" O - /* List all the available Access points first1 L1 R6 n- o' a% i( O
- then check whether the specified 'ssid' exists among them or not.*/( z6 p/ r- r' h+ ~: p
- memset(AtCmd, '\0', MAX_AT_CMD_SIZE);
" t0 I& R, Z L$ w) n - sprintf((char *)AtCmd, "AT+CWJAP_DEF="%s","%s"%c%c", Ssid, Password, '\r', '\n');0 m" h0 M \ J$ q) b
- /* Send the command */
! s2 N) C- q2 p) F8 Z+ R8 M7 z( W - Ret = runAtCmd(AtCmd, strlen((char *)AtCmd), (uint8_t*)AT_OK_STRING,NULL);
复制代码 7 I4 E, y8 K) N2 a6 m; _5 n
AT 指令运行记录:
* P4 M/ {# H, K; i
+ e+ J( M2 ^# T6 N+ D- E. q; [
/ ^9 ^* l7 s4 P! \
$ j. y( L( ^& G上图的黄色部分是 WIFI 模块的返回状态。必须要接收到"OK\r\n",才能去读取 IP 地址。连接 WIFI 热点的过程,需要的时长不一定,有时 2、,3 秒,有时 6、,7 秒。所以这里最好把等待的时间 留长一点,否则经常会出现连接 WIFI 热点失败的情况。% h1 N: ~5 }0 Y' r8 n
% D5 c( w7 ^2 r! b) \; X) s5 K
与服务器建立连接2 L$ `. R( E* H% p) M8 J
连接到 WIFI 热点后,就可以开始与服务器建立连接了。ESP8266 支持 5 个并发连接。) _6 U- |3 K5 G, J3 J& }
一般我们知道的不是目标服务器的 IP 地址,而是域名。所以在开始创建连接之前需要先通过 DNS 服务获该取域名对应的 IP地址。ESP8266 也提供了相应的 AT 指令。
1 u0 P& H) ^, ^: }* N下面是代码中的一段和建立连接相关的代码:+ [- G( o8 _4 O# G- v2 N( z
! N9 ?% z4 B: U8 T+ A/ A7 _1 U
- if (WIFI_GetHostAddress((char *)hostname, ip_addr) != WIFI_STATUS_OK) <font color="#00ff00">//域名解析</font>
^ |% D! B* i' y3 V: ?5 d( {3 y - {
b2 J: F5 Q3 X/ k5 X - // TODO: Defect report on WIFI_GetHostAddress() which return code is not
& h; t5 `. g* ~$ t5 u4 i - informative.
/ y- d7 c: \2 A f) u" y2 \/ f - // NB: This blocking call may take several seconds before returning. An, V% {3 G4 x; H: Z7 F
- asynchronous interface should be added.9 u% j3 p9 p* D
- msg_info("The address of %s could not be resolved.\n", hostname);
5 e$ `& J6 p( I: E1 D1 { - rc = NET_ERR;5 j0 q: A1 ~! a) R6 j" {4 ~
- }# \0 o F& h3 U8 c
- else
( ` l: ^, P3 S. h - {
9 w- v, ]2 E; V' T3 H - if( WIFI_STATUS_OK == WIFI_OpenClientConnection( <font color="#00ff00">//根据获得的 IP 地址连接服务器</font>
, U/ C) j/ ~3 D% l% T - (uint32_t) sock->underlying_sock_ctxt, WIFI_TCP_PROTOCOL, "", ip_addr,
$ D/ h3 ?* V% b) v9 T; V0 T* b - dstport, 0) )6 ^6 M) l; b1 t6 S; K) m& g
- {
6 L* s0 Y; b9 P# d7 w1 ^ - rc = NET_OK; ]$ ?9 N1 b" O8 g' u8 D: W1 o
- }
, s& q9 \0 e5 C: }$ Q5 S - else; O9 t9 h8 ` b* C/ g4 _3 n9 H
- { f, c( ]1 L; g: }/ ~& k: O! o
- underlying_socket_busy[(int) sock->underlying_sock_ctxt] = false;7 t1 n" X' r: a/ V5 Q6 K6 t: U
- msg_error("Failed opening the underlying Wifi socket %d.\n", (int) sock-( |2 R/ d4 ^, R; {2 d% [
- >underlying_sock_ctxt);/ O0 D- t5 T1 G+ B0 L; T( R$ Q
- sock->underlying_sock_ctxt = (net_sockhnd_t) -1;9 b5 ?& }& x v% p9 w% g1 O! v) Q
- }9 ~$ k3 K8 K* O1 p# b! M9 M3 g
- }
复制代码 & U0 Z' P% k4 {- h
AT 指令运行记录:9 @0 t7 M9 L9 @* I8 O1 z
9 |& b) ^5 g* H# v% O& i9 }! q
1 b4 ~# c8 J0 G; {7 J! w
1 `% x, u* t" E* a1 J" l8 N2 {发送数据! K- J4 _! u8 s1 c! r
发送数据过程分两步:7 T. ]/ L8 M. \) q7 g( n
1.发送 AT+CIPSEND=<LINK ID>,<LENGTH>命令。说明要往哪个 socket 通道,发送多少字节的数据。1 p7 N2 @/ @& r: y
2.收到 WIFI 模块返回的"OK\r\n>"后,再发送数据。2 b3 ?+ `$ a9 Y. D l) m! Y3 P
下面是例程中,ESP8266 发送数据的代码实现。, t, e& z' j5 w' j
- ESP8266_StatusTypeDef ESP8266_SendData(uint8_t socket,uint8_t* Buffer, uint32_t Length)
, j: ~1 U) }' a - {
/ r$ m* U5 R$ u, R$ [" f+ E9 Q - ESP8266_StatusTypeDef Ret = ESP8266_OK; e Y5 J1 @( l; K1 L) _4 q
2 Y9 d" [2 R2 D; A- if (Buffer != NULL)
+ |: @. }& {9 E: u4 l, a1 Z - { ` R) p' W+ c
- /* Construct the CIPSEND command */ <font color="#00ff00">//准备发送数据,等待 WIFI模块发的“发送数据提示符”</font>
6 i& s2 @% O* ]3 I" J- H - memset(AtCmd, '\0', MAX_AT_CMD_SIZE);
7 H: y3 R" D9 X2 B, H( d% z9 u - sprintf((char *)AtCmd, "AT+CIPSEND=%lu,%lu%c%c", socket,Length, '\r', '\n');; |# w" b [0 p, R; ^
( d: q' `8 W% N5 w- /* The CIPSEND command doesn't have a return command5 `( y) `" i& B+ Z; W3 ]' ~
- until the data is actually sent. Thus we check here whether. k4 Z2 Q, b1 W: Q
- we got the '>' prompt or not. *// w& v+ p* p$ v
- Ret = runAtCmd(AtCmd, strlen((char *)AtCmd), (uint8_t*)AT_SEND_PROMPT_STRING,NULL);
# T6 Z" `* Y! G! i6 Q8 L - 6 X o5 W$ x: d6 m/ K5 t" z
- /* Return Error */
/ R) ?, y4 q# t& k( ? - if (Ret != ESP8266_OK)
0 C2 p. S D7 Y r - {
" P2 J i# w/ {, a8 X - return ESP8266_ERROR;
; S0 j. Q# s$ r! w - }
& s4 t$ O, r. M7 c+ T - . G: y. J" B( \+ v# {( |
- /* Send the data */ <font color="#00ff00"> //开始发送数据</font>& |' T6 ^+ Q' @/ l) E( h
- Ret = runAtCmd(Buffer, Length, (uint8_t*)AT_SEND_OK_STRING,NULL);
) S6 L: A/ S0 U+ S; b$ g - if (Ret != ESP8266_OK)
8 K3 I' K6 M. I0 [2 B' x/ m) u - {5 Z$ |% ?2 D! a+ }
- return ESP8266_ERROR;$ R0 y ?. ^' V( ^: @; C( G: N3 T0 e
- }
" V) r9 C( B& R) m' }% W0 j! W - }+ `! g s6 d3 x$ p& n6 z! Q
- & l. c$ W+ t; ?* R
- return Ret;5 s9 r" Q- I* y8 k c
- }
复制代码 " P$ t1 r4 J! F+ }; y1 J, ^" A+ S
AT 指令运行记录:/ ?# Q& x5 c/ O! o ]4 b
" @+ ~- Y) L# c4 p9 K0 z
! r& s" Q9 Z% Y. a$ {( j
$ H; e" l6 _4 q! w
接收数据的模式; [9 F1 D. u& |4 P* S! P: m" M
在上一节“发送数据”的 AT 指令执行记录中,我们看到最后 WIFI 模块回复了一条以“+IPD”开头的消息。这是从网络上的服务器返回的数据。使用 Esp8266 模块时,接收网络上发来的数据,不需要 MCU 先发“接收”的“AT 指令。只要收到数据,ESP8266 就会通过“+IPD”开头的数据包,将数据传递给 MCU。$ N2 g( a1 ]9 j2 ^+ E: }. Q
demo 里接收数据,我们采用了下面的这种模式:' s3 j1 f, P% |
串口中断程序(UART ISR)负责把从 UART 端口接收到的数据放到 ring buffer 中。' Q5 P! F! s9 ]6 r; ^# g
上层应用程序需要数据的时候,不直接从 ring buffer 中拿,而是从各自的 socket buffer 中拿,每个通道(ESP8266 支持 5 个通道)都有一个对应的 socket buffer。本示例代码里有两个应用,MQTT 和 HTTP,因此使用了 wifi 模块的两个 socket 通道。只有在 socket buffer 中没有有效数据的时候,再到 ring buffer 中去查看是否有收到数据,然后将数据拷贝到 socket buffer 中。除了读取 wifi 传过来的云端数据,上层应用程序还需要就发送的 AT 指令检查响应。这个是在 ring buffer 中去读取。如果发现ring buffer 里面有以“+IPD”开头的来自云端的数据,会“帮忙”拷贝到对应的 socket buffer 里。
0 c/ F* i; g! A. V- E* x0 ]# `1 \! v6 B- U$ ~2 d3 t
这么做虽然多了一次数据的拷贝(集中的 ring buffer 到各自的 socket buffer),但是方便上层应用程序的操作。因为有时候,我们会一下子从网络端收到上百个字节的数据,这些数据都在一个“+PID”开头的数据包中传给了 MCU。但也许上层应用程序,希望一个或几个几个字节的来解析这个数据包。比如,MQTT 中,要先读一个字节的数据判断一下收到的是什么类型的数据包,是 CONNACK 还是 PUBLISH?在这种情况下,增加了 socket buffer 这一层的话,就可以做到,每次从 ring buffer中读数据,都是将整个+IPD 的数据包读出,而上层应用程序,可以想从 socket buffer 中每次读几个字节都可以。将数据从 ring buffer 拷贝到 socket buffer 是由 ExtractDatafromIPDpacket()函数来完成的。9 P7 e9 Z2 Z. `2 Y1 M! F7 P$ c
在 esp8266_io.h 文件中,分别定义了 socket buffer 和 ring buffer 的结构:esp_sock_ctx_t, RingBuffer_t。% g* ~; ^/ C3 F9 G# b
7 U' T% [. o: `+ C; K- X) p. E( u: e r' J* U; {9 P% l
注意:该示例中 MQTT 和 HTTP 应用都在一个 task 里面调用,如果在不同的 task 实现,接收驱动需要做相应修改。
9 O& t# |& [# ~/ S9 o
( @- U6 n; b) Y8 H* q
$ ~; u. _4 B: B7 f/ u; t$ ~ @) x5 ^# F! ?% H
4 }* N8 m& E) t9 y
小结9 T0 H& n- T+ G) h6 I5 T* K
在例程中,和 WIFI 模块相关的驱动由三层组成:7 G/ D# w/ }( _& y! J( E
在 ESP8266_io.c 中,是最底层的跟 STM32 外设打交道的部分。包括初始化引脚,从串口读取和发送数据;Esp8266.c 中是对 AT 指令的实现。8 o+ f- q. w+ e/ n* Q; `6 ?" p9 b
Wifi.c 是 wifi 底层驱动和上层的一个接口。我们可以看到它的函数和 ESP8266.c 中的某些函数名字都很类似。3 d5 T; f; S, W: f6 f
我们前面讲的内容基本都在这三个文件中,对于如果想更换 WIFI 模块来讲,它主要涉及到的代码也就是这三部分。
% {+ [/ ]* l3 ^1 R, @+ o& j- i% P; x' G9 }1 W. H8 v K. {
4 v8 W( E) P! _$ k4 L7 _: M2 e
1 @- ~; G4 X/ z `7 Q% y0 \" N6 w+ `) @
1 [. l- E8 l' M9 W$ \6 [- Y. [/ A6 j. J6 X3 x- I0 u
4 u$ h4 k( y( p# \+ I: q
! [4 l. ]) D. \' S* N. C |