返回列表 发新帖我要提问本帖赏金: 80.00元(功能说明)

[APM32F4] APM32F407+FreeRTOS实现AT模块的命令解析器

[复制链接]
5639|9
 楼主| luobeihai 发表于 2024-12-5 00:28 | 显示全部楼层 |阅读模式
本帖最后由 luobeihai 于 2024-12-5 00:31 编辑


#申请原创# @21小跑堂


1. AT命令基本介绍

1.1 什么是AT命令

AT 命令(AT Commands)最早是由发明拨号调制解调器(MODEM)的贺氏公司(Hayes)为了控制 MODEM 而发明的控制协议。后来随着网络带宽的升级,速度很低的拨号 MODEM 基本退出一般使用市场,但是 AT 命令保留下来。

在嵌入式开发中,经常是使用AT命令去控制各种通讯模块,比如ESP8266 WIFI模块、4G模块、GPRS模块等等。一般就是主芯片通过硬件接口(比如串口、SPI)发送AT命令给通讯模块,模块接收到数据之后回应响应的数据。

在这个通讯过程中其实就是涉及AT命令客户端AT命令服务器,AT命令集就是他们互相约定好的协议接口。大概的通讯结构如下:

image-20220818095426124.png

其中,AT客户端通常是主芯片作为客户端,AT服务器一般就是各种通讯模块。

1.2 AT命令的组成和通讯过程

  • AT命令由三个部分组成,分别是前缀、主体和结束符。其中前缀由字符 AT 构成;主体由命令、参数和可能用到的数据组成;结束符一般为 <CR><LF> ("\r\n")。
    比如 AT+CWMODE=3\r\n 这条命令,AT就是前缀,中间就是主体部分,\r\n 就是结束符。
  • AT命令通讯过程的实现,需要AT Client 和 AT Server 两部分共同完成。
  • AT客户端和AT服务器之间硬件通讯接口,一般最常用的是串口,也有SPI接口等。
  • AT Client主要作用是主动发送AT命令,然后等待AT Server的响应数据,并对响应数据或者AT Server主动发送的数据(即URC数据)进行解析。
  • AT Server 返回给 AT Client 的数据有两种。命令响应数据和 URC 数据(unsolicited result code)。
    命令响应数据:AT Client 发送命令后 AT Server 回应的响应状态和信息。
    URC数据:AT Server 主动发送给 AT Client 的数据。比如 AT Server接收到网络的数据后,会主动把这些数据发送给 AT Client ,又或者 WIFI 断开连接等,也会主动发数据告知 AT Client。  

2. 基于ESP8266模块的AT命令使用

使用AT命令进行通信的模块,就叫AT命令模块。对于AT命令模块,有非常多,最常见的就是ESP8266模块,还有比如4G模块等都是使用AT命令进行通信模块。

2.1 ESP8266的AT命令

关于ESP8266模块有哪些AT命令,大家去查看该模块的用户手册即可,这里不水文字了。

2.2 ESP8266模块AT命令收发实验

ESP8266模块是使用串口对外作为通讯接口的,我们可以通过串口发送各种AT命令配置模块、收发数据等等。

我们使用电脑上运行的串口助手作为 AT Client 发送命令和接收模块的响应数据、URC数据,ESP8266模块作为AT Server。测试步骤如下:

1、USB转串口连接ESP86266模块

首先,使用USB转串口,接线到ESP8266模块。我们只要连接模块的 VCC、GND、RX、TX,这四个引脚即可。如下图:

image-20220818134058596.png

连接好之后,USB转串口插到电脑上。然后使用串口助手发送上面介绍的常用AT命令。

2、PC串口助手发送AT命令
PC端运行串口助手,通过串口助手发送AT命令给ESP8266模块,如下,记录了AT发送命令和命令响应的数据。

  1. [13:46:03.634]发→◇AT+GMR
  2. [13:46:03.639]收←◆AT+GMR
  3. AT version:1.2.0.0(Jul  1 2016 20:04:45)
  4. SDK version:1.5.4.1(39cb9a32)
  5. v1.0.0
  6. Mar 11 2018 18:27:31
  7. OK


  8. [13:46:10.170]发→◇AT+CIFSR
  9. [13:46:10.174]收←◆AT+CIFSR
  10. +CIFSR:APIP,"192.168.4.1"
  11. +CIFSR:APMAC,"4a:55:19:c7:ed:ad"
  12. +CIFSR:STAIP,"192.168.0.100"
  13. +CIFSR:STAMAC,"48:55:19:c7:ed:ad"

  14. OK


  15. [13:46:25.723]发→◇AT+CIPSTART="TCP","192.168.0.103",8080
  16. [13:46:25.729]收←◆AT+CIPSTART="TCP","192.168.0.103",8080

  17. [13:46:25.782]收←◆CONNECT

  18. OK

  19. [13:51:50.705]发→◇AT+CIPSEND=4
  20. [13:51:50.710]收←◆AT+CIPSEND=4

  21. OK
  22. >
  23. [13:51:56.161]发→◇abcd
  24. [13:51:56.166]收←◆
  25. busy s...

  26. Recv 4 bytes

  27. [13:51:56.222]收←◆
  28. SEND OK

  29. [13:52:01.535]收←◆
  30. +IPD,10:1234567890

下图记录了收发过程:

image-20220818134920493.png

如果要连接TCP Server,那么还有在电脑上开启一个TCP Server,这样ESP8266要连接TCP Server是才能连接成功。下面就开启了一个TCP服务器。

image-20220818135237712.png

3. AT命令解析器的代码框架

第一二章节已经介绍了AT命令的组成和通信过程,而且也演示了如何使用AT命令对ESP8266模块进行通信控制。

下面我们就编写代码实现对AT命令的解析过程,包括AT命令的发送,以及响应数据、URC数据的解析

代码框架主要是有两个线程。一个线程负责命令发送,并阻塞等待命令响应结果和响应数据;还有一个是数据解析线程,主要是解析AT命令的响应数据已经URC数据,解析的结果和数据会传递给命令发送线程,然后唤醒命令发送线程。大致流程如下:

image-20241204235019061.png

数据解析线程会调用一个读取一行数据的函数,这个函数会去读取串口的数据,如果读取不到串口数据,那么这个线程就会一直阻塞,直到等到有串口数据的时候,才会发送信号量唤醒这个线程。

然后下面的代码把接收到的数据进行解析。解析完成之后,会发送信号量唤醒AT命令的发送线程。

4. AT命令解析器实现过程

AT命令解析器的核心代码就是这两个线程,以及两个线程所调用的函数。下面记录AT命令解析器的实现过程。

4.1 AT命令响应数据解析线程

该线程是AT模块接收到命令之后,AT模块响应的数据返回给主控的数据,该线程就是对响应数据进行解析。

  1. void at_recv_parser(void *parameter)
  2. {
  3.         char recv_line_buff[128] = {0};
  4.         int read_len = 0;
  5.         at_resp_t tmp_resp = {{0}, 0, 0};
  6.         const at_urc_t *urc = NULL;
  7.         
  8.         while (1)
  9.         {
  10.                 read_len = at_recv_readln(recv_line_buff, sizeof(recv_line_buff));
  11.                 if ( read_len > 0)
  12.                 {                                                                        
  13.                         if ((urc = at_get_urc(recv_line_buff, read_len)) != NULL)
  14.                         {
  15.                                 /* URC数据处理 */
  16.                                 if (urc->func != NULL)
  17.                                 {
  18.                                         urc->func(recv_line_buff, read_len);
  19.                                 }
  20.                         }
  21.                         else
  22.                         {
  23.                                 /* 命令响应数据处理 */
  24.                                 if (tmp_resp.buf_len < AT_MAX_RESP_LEN)
  25.                                 {
  26.                                         recv_line_buff[++read_len] = '\0';
  27.                                         memcpy(tmp_resp.buf + tmp_resp.buf_len, recv_line_buff, read_len);
  28.                                         tmp_resp.buf_len += read_len;
  29.                                         tmp_resp.line_counts++;
  30.                                 }
  31.                                 else
  32.                                 {        
  33.                                         at_set_resp_status(AT_RESP_BUFF_FULL);
  34.                                 }
  35.                                 
  36.                                 if (strstr(recv_line_buff, "OK"))
  37.                                 {
  38.                                         memset(&gs_resp, 0, sizeof(gs_resp));
  39.                                         memcpy(&gs_resp, &tmp_resp, sizeof(gs_resp));
  40.                                         at_set_resp_status(AT_RESP_OK);
  41.                                 }
  42.                                 else if (strstr(recv_line_buff, "ERROR"))
  43.                                 {
  44.                                         memset(&gs_resp, 0, sizeof(gs_resp));
  45.                                         memcpy(&gs_resp, &tmp_resp, sizeof(gs_resp));
  46.                                         at_set_resp_status(AT_RESP_ERROR);
  47.                                 }
  48.                                 else
  49.                                 {
  50.                                         continue;
  51.                                 }
  52.                                 
  53.                                 memset(&tmp_resp, 0, sizeof(tmp_resp));
  54.                                 xSemaphoreGive(at_resp_sem);
  55.                         }                        
  56.                 }
  57.         }
  58. }

1、在这个线程函数中,有一个 at_recv_readln 函数,会一直去读取串口环形缓冲区的数据,如果没有数据那么就会阻塞等待,直到串口接收数据中断释放的信号量去唤醒它。这个函数实现如下,这个函数读取到一行数据或者有匹配的URC数据,那么就返回给数据解析线程

  1. static int at_recv_readln(char *buff, unsigned int buff_len)
  2. {
  3.         char ch = 0, last_ch = 0;
  4.         unsigned int read_len = 0;

  5.         memset(buff, 0, buff_len);
  6.         
  7.         while (1)
  8.         {
  9.                 at_client_getchar(&ch, portMAX_DELAY);
  10.                 if (read_len < buff_len)
  11.                 {
  12.                         buff[read_len++] = ch;
  13.                 }
  14.                 else
  15.                 {/* buff溢出错误 */
  16.                         memset(buff, 0x00, buff_len);
  17.                         return -1;
  18.                 }

  19.                 /* 读到一行数据,或者接收到URC数据 */
  20.                 if ((ch == '\n' && last_ch == '\r') || at_get_urc(buff, read_len))
  21.                 {
  22.                         break;
  23.                 }
  24.                 last_ch = ch;         /* 暂存前一个字符 */
  25.         }
  26.         
  27.         return read_len;
  28. }

2、数据解析线程主要分为两个部分的情况进行解析。一个是URC数据解析,一个是命令响应数据的解析

其中URC数据处理,我定义了一个URC的数据结构体

  1. typedef struct _at_urc_t
  2. {
  3.     const char *cmd_prefix;                /* 前缀 */
  4.     const char *cmd_suffix;                /* 后缀 */
  5.     void (*func)(const char *data, unsigned int size);        /* 对应执行函数 */
  6. } at_urc_t;

然后定义一个URC数据表格

  1. static at_urc_t esp8266_urc_table[] =
  2. {
  3.         {"SEND OK",          "\r\n",           urc_send_func},
  4.     {"SEND FAIL",        "\r\n",           urc_send_func},
  5.     {"+IPD",             ":",              urc_recv_func},
  6. };

我这里暂时只实现数据收发的URC数据的处理函数。在这个 at_recv_parser 函数中,会调用 at_get_urc 函数去匹配判断是否有接收到和我们表格中定义的URC数据,如果匹配上了,就会调用对应的函数去处理,处理完成之后,就会释放信号量唤醒AT命令发送线程。

at_get_urc 函数实现如下:

  1. static const at_urc_t *at_get_urc(const char *recv_line_buf, unsigned int recv_line_len)
  2. {
  3.         unsigned char prefix_len = 0, suffix_len = 0;

  4.         for (int i = 0; i < sizeof(esp8266_urc_table) / sizeof(esp8266_urc_table[0]); i++)
  5.         {
  6.                 prefix_len = strlen(esp8266_urc_table[i].cmd_prefix);
  7.                 suffix_len = strlen(esp8266_urc_table[i].cmd_suffix);
  8.                
  9.                 if ((prefix_len ? !strncmp(recv_line_buf, esp8266_urc_table[i].cmd_prefix, prefix_len) : 1)
  10.                 && (suffix_len ? !strncmp(recv_line_buf + recv_line_len - suffix_len, esp8266_urc_table[i].cmd_suffix, suffix_len) : 1))
  11.         {
  12.             return &esp8266_urc_table[i];               
  13.         }
  14.         }
  15.         
  16.         return NULL;
  17. }

3、命令响应数据解析

对于命令响应数据,因为都会有响应的状态回复,要么回复 “OK” ,要么是 “ERROR” ,所以我们只需要判断匹配这两个字符串即可。判断完成之后,把响应的数据和状态通过全局变量传递给AT命令发送线程,然后再发送信号量去唤醒这个线程。

4.2 AT命令发送线程

主要是实现了 at_exce_cmd 这个发送AT命令的函数,并返回命令响应的状态和响应数据。

  1. /**
  2. * 发送命令给AT服务器,和等待回应
  3. *
  4. * @param resp : 输出AT服务器回应的数据
  5. *
  6. * [url=home.php?mod=space&uid=266161]@return[/url] 0 : success
  7. *        -1 : response status error
  8. *        -2 : wait timeout
  9. */
  10. int at_exce_cmd(const char *cmd, at_resp_t *resp, unsigned int timeout)
  11. {
  12.         /* 发送命令给AT服务器 */
  13.         at_client_sendcmd(cmd);

  14.         /* 获取解析AT服务器回应数据的任务释放的信号量 */
  15.         if (xSemaphoreTake(at_resp_sem, pdMS_TO_TICKS(timeout)) == pdTRUE)
  16.         {
  17.                 if (resp != NULL)
  18.                 {
  19.                         memcpy(resp, &gs_resp, sizeof(at_resp_t));
  20.                 }
  21.                 return at_get_resp_status();
  22.         }
  23.         else
  24.         {
  25.                 return AT_RESP_TIMEOUT;
  26.         }
  27. }

其中调用的 at_client_sendcmd 函数,就是通过串口发送数据给AT Server(即ESP8266模块)的。发送命令之后,就阻塞等待数据解析线程的回应,解析成功,就会唤醒这个线程。

以上就是AT命令解析器的实现过程介绍。该AT命令解析器不仅适用于ESP8266模块,对于与ESP8266模块具有相同的AT命令格式的其他所有模块都适用。



打赏榜单

21小跑堂 打赏了 80.00 元 2024-12-11
理由:恭喜通过原创审核!期待您更多的原创作品~~

评论

APM32F407与FreeRTOS的结合,设计一款通用的AT命令解析器,程序框架合理,效率较高。  发表于 2024-12-11 10:55
呐咯密密 发表于 2024-12-8 11:30 | 显示全部楼层
读取串口用阻塞的方式不是很友好啊
 楼主| luobeihai 发表于 2024-12-8 17:59 | 显示全部楼层
呐咯密密 发表于 2024-12-8 11:30
读取串口用阻塞的方式不是很友好啊

是阻塞读取串口环形缓冲区的数据,而缓冲区的数据是在串口中断中写入的,只要串口有数据,就会触发串口中断,然后把数据写入环形缓冲区。而当环形缓冲区有数据可读,就不会阻塞,否则阻塞等待。这种设计机制我觉得很合理,因为CPU需要留出时间去执行其他的任务,而不能一直执行一个任务,所以才需要阻塞等待。
呐咯密密 发表于 2024-12-10 15:30 | 显示全部楼层
luobeihai 发表于 2024-12-8 17:59
是阻塞读取串口环形缓冲区的数据,而缓冲区的数据是在串口中断中写入的,只要串口有数据,就会触发串口中 ...

之前看的不仔细,再看一遍觉得是合理的,我过于武断了。
海洋无限 发表于 2024-12-11 12:12 | 显示全部楼层
ringbuffer在APM32F407串口数据接收中的应用  有源码,这个没有源码
 楼主| luobeihai 发表于 2024-12-11 17:07 | 显示全部楼层
海洋无限 发表于 2024-12-11 12:12
ringbuffer在APM32F407串口数据接收中的应用  有源码,这个没有源码

哈哈 主要是这个AT命令解析器的工程源码不想全部公开。
海洋无限 发表于 2024-12-12 11:58 | 显示全部楼层
luobeihai 发表于 2024-12-11 17:07
哈哈 主要是这个AT命令解析器的工程源码不想全部公开。

嗯   其实想参考的也就是这部分
dql2015 发表于 2024-12-17 11:06 | 显示全部楼层
跟腾讯的rtos里面的那个挺像的
 楼主| luobeihai 发表于 2024-12-17 12:20 | 显示全部楼层
dql2015 发表于 2024-12-17 11:06
跟腾讯的rtos里面的那个挺像的

所有RTOS内核提供的接口都差不多
您需要登录后才可以回帖 登录 | 注册

本版积分规则

23

主题

101

帖子

4

粉丝
快速回复 在线客服 返回列表 返回顶部