jklincn


Stm32F103 + NuttX(六):连接 Wi-Fi(ESP8266+USART)


很多事情要有了网络才有趣~

手头的wifi模块是:正点原子 ATK-MW8266D,应该是正点原子自己封装了 ESP8266 然后弄了一个模块。

本章代码:https://github.com/jklincn/nuttxspace/tree/series/ch06,这章代码改动较多(包括对官方 apps 的修改),强烈建议搭配 github 源码查看。

模块连接

连接其实很简单,按照《模块使用说明》插入到开发板上的 ATK MODULE 接口即可。

但这里还是想写一下对应的接口,因为感觉正点原子的文档容易让人误解。

先看模块用户手册:

可以看到模块 TXD 引脚期望连接到单片机的 RXD

看硬件参考手册:

可以看到是 GBC TX —— PB11 —— TXD,给人一种“单片机 TX 连接到模块 TX ”的感觉。

但看模块使用说明,确实又是这样连接

所以感觉文档有些误解性,这里补充一个官方的引脚定义就可以了

可以看到 PB10USART3_TXPB11USART3_RX,这样就说得通了。

NuttX 支持

首先这个 wifi 模块是完整的一套板卡,MCU 是 ESP8266,因此这里使用的话实际在 STM32 方面就只是通过串口来进行通信,不需要在软件上开启网络相关的配置。

但看 NuttX 的介绍中有这样一段话

Networking

  • Support for networking modules (e.g., ESP8266).

它支持 ESP8266 网络模块,注意这里用的也是 modules 的概念,说明并不是直接集成到运行 NuttX 的 MCU 中的。

可以找到对应的代码,源代码位于 apps/netutils/esp8266/esp8266.c,头文件在 apps/include/netutils/esp8266.h(其在 apps 目录下,也符合我们上面的推断)

 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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#ifndef __APPS_INCLUDE_NETUTILS_ESP8266_H
#define __APPS_INCLUDE_NETUTILS_ESP8266_H

/****************************************************************************
 * Included Files
 ****************************************************************************/

#include "nuttx/config.h"

#include <netinet/in.h>

#ifdef CONFIG_NETUTILS_ESP8266

/****************************************************************************
 * Pre-processor Definitions
 ****************************************************************************/

#define LESP_SSID_SIZE 32 /* Number of character max of SSID (null char not included) */
#define LESP_BSSID_SIZE 6

#define lespIP(x1,x2,x3,x4) ((x1) << 24 | (x2) << 16 | (x3) << 8 | (x4) << 0)

/****************************************************************************
 * Public Types
 ****************************************************************************/

typedef enum
{
  LESP_MODE_AP       = 0,
  LESP_MODE_STATION  = 1,
  LESP_MODE_BOTH     = 2
} lesp_mode_t;

typedef enum
{
  LESP_SECURITY_NONE = 0,
  LESP_SECURITY_WEP,
  LESP_SECURITY_WPA_PSK,
  LESP_SECURITY_WPA2_PSK,
  LESP_SECURITY_WPA_WPA2_PSK,
  LESP_SECURITY_NBR
} lesp_security_t;

typedef struct
{
  lesp_security_t security;
  char ssid[LESP_SSID_SIZE + 1];    /* +1 for null char */
  uint8_t bssid[LESP_BSSID_SIZE];
  int rssi;
  int channel;
} lesp_ap_t;

/****************************************************************************
 * Public Function Prototypes
 ****************************************************************************/

int lesp_initialize(void);
int lesp_finalize(void);
int lesp_soft_reset(void);

const char *lesp_security_to_str(lesp_security_t security);

int lesp_ap_connect(const char *ssid_name,
                    const char *ap_key, int timeout_s);
int lesp_ap_get(lesp_ap_t *ap);

int lesp_ap_is_connected(void);

int lesp_set_dhcp(lesp_mode_t mode, bool enable);
int lesp_get_dhcp(bool *ap_enable, bool *sta_enable);
int lesp_set_net(lesp_mode_t mode, in_addr_t ip, in_addr_t mask,
                 in_addr_t gateway);
int lesp_get_net(lesp_mode_t mode, in_addr_t *ip, in_addr_t *mask,
                 in_addr_t *gw);

typedef void (*lesp_cb_t)(lesp_ap_t *wlan);

int lesp_list_access_points(lesp_cb_t cb);

int lesp_socket(int domain, int type, int protocol);
int lesp_closesocket(int sockfd);
int lesp_bind(int sockfd,
              FAR const struct sockaddr *addr, socklen_t addrlen);
int lesp_connect(int sockfd,
                 FAR const struct sockaddr *addr, socklen_t addrlen);
int lesp_listen(int sockfd, int backlog);
int lesp_accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
ssize_t lesp_send(int sockfd, FAR const uint8_t *buf, size_t len, int flags);
ssize_t lesp_recv(int sockfd, FAR uint8_t *buf, size_t len, int flags);
int lesp_setsockopt(int sockfd, int level, int option,
                    FAR const void *value, socklen_t value_len);
FAR struct hostent *lesp_gethostbyname(FAR const char *hostname);

#endif /* CONFIG_NETUTILS_ESP8266 */
#endif /* __APPS_INCLUDE_NETUTILS_ESP8266_H */

可以看到提供了很多便利的接口,这样我们就不用自己手搓 AT 指令来完成各种操作了,直接调用这些函数即可。

我们可以直接先看初始化函数

 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
int lesp_initialize(void)
{
  int ret = 0;

  pthread_mutex_lock(&g_lesp_state.mutex);

  if (g_lesp_state.is_initialized)
    {
      pthread_mutex_unlock(&g_lesp_state.mutex);
      ninfo("Esp8266 already initialized\n");
      return 0;
    }

  pthread_mutex_lock(&g_lesp_state.worker.mutex);

  ninfo("Initializing Esp8266...\n");

  memset(g_lesp_state.sockets, 0, SOCKET_NBR * sizeof(lesp_socket_t));

  if (sem_init(&g_lesp_state.worker.sem, 0, 0) < 0)
    {
      ninfo("Cannot create semaphore\n");
      ret = -1;
    }

  if (ret >= 0 && g_lesp_state.fd < 0)
    {
      g_lesp_state.fd = open(CONFIG_NETUTILS_ESP8266_DEV_PATH, O_RDWR);
    }

  if (ret >= 0 && g_lesp_state.fd < 0)
    {
      nerr("ERROR: Cannot open %s\n", CONFIG_NETUTILS_ESP8266_DEV_PATH);
      ret = -1;
    }

#ifdef CONFIG_SERIAL_TERMIOS
  if (ret >= 0 && lesp_set_baudrate(CONFIG_NETUTILS_ESP8266_BAUDRATE) < 0)
    {
      nerr("ERROR: Cannot set baud rate %d\n",
            CONFIG_NETUTILS_ESP8266_BAUDRATE);
      ret = -1;
    }
#endif

  if ((ret >= 0) && (g_lesp_state.worker.running == false))
    {
      ret = lesp_create_worker(CONFIG_NETUTILS_ESP8266_THREADPRIO);
    }

  if (ret < 0)
    {
      ninfo("Esp8266 initialisation failed!\n");
      ret = -1;
    }
  else
    {
      g_lesp_state.is_initialized = true;
      ninfo("Esp8266 initialized\n");
    }

  pthread_mutex_unlock(&g_lesp_state.worker.mutex);
  pthread_mutex_unlock(&g_lesp_state.mutex);

  return 0;
}

可以看到其中有

1
g_lesp_state.fd = open(CONFIG_NETUTILS_ESP8266_DEV_PATH, O_RDWR);

这里就是模块代码和系统内核的交互方式了:通过字符设备文件。在 Kconfig 中,NETUTILS_ESP8266_DEV_PATH 的默认值是 /dev/ttyS1,这一般是 USART2,所以等下在写代码的时候需要注意一下。

config NETUTILS_ESP8266_DEV_PATH
	string "Serial device path"
	default "/dev/ttyS1"

梳理思路

从代码实现角度来说比较简单:

  • 板级代码:由于 NuttX 中包含了 ESP8266 支持,因此我们只需要配置好 USART,将其注册到 /dev 文件系统下,然后让 esp8266.c 成功使用它就可以了。
  • 应用层代码:调用 esp8266.h 中定义的接口就完事了。

代码实现

配置 USART

nuttx/arch/arm/src/stm32/stm32_start.c 中的 __start 开始重新过一遍初始化流程,找一下 USART 是在哪里初始化的。

  • stm32_lowsetup 对所有 USART 做了引脚映射,并且为串口控制台做了完整初始化
  • arm_earlyserialinit 对所有 USART 做了早期低级初始化,主要目的还是为了串口控制台可以打印启动阶段的 debug 信息。
  • 关键是 up_initialize 中调用的 arm_serialinit
 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
77
78
79
80
81
82
83
84
85
86
87
88
89
void arm_serialinit(void)
{
#ifdef HAVE_SERIALDRIVER
  char devname[16];
  unsigned i;
  unsigned minor = 0;
#ifdef CONFIG_PM
  int ret;
#endif

  // 注册电源管理
#ifdef CONFIG_PM
  ret = pm_register(&g_serialcb);
  DEBUGASSERT(ret == OK);
  UNUSED(ret);
#endif

  // 注册控制台
#if CONSOLE_UART > 0
  struct uart_dev_s *dev = &g_uart_devs[CONSOLE_UART - 1]->dev;
#elif CONSOLE_LPUART > 0
  struct uart_dev_s *dev = &g_lpuart_devs[CONSOLE_LPUART - 1]->dev;
#endif

#if CONSOLE_UART > 0 || CONSOLE_LPUART > 0
  uart_register("/dev/console", dev);

#ifndef CONFIG_STM32_SERIAL_DISABLE_REORDERING
  // 没有禁用重排序,则将控制台映射为 /dev/ttyS0,并在后续的初始化中排除它
  uart_register("/dev/ttyS0", dev);
  minor = 1;
#endif

  // 如果配置了控制台使用 DMA,这里会进行额外的 DMA 初始化
#if defined(SERIAL_HAVE_CONSOLE_RXDMA) || defined(SERIAL_HAVE_CONSOLE_TXDMA)
  up_dma_setup(dev);
#endif
#endif /* CONSOLE_UART > 0 || CONSOLE_LPUART > 0 */

  // 注册剩余的 USARTs

  strlcpy(devname, "/dev/ttySx", sizeof(devname));

  for (i = 0; i < STM32_NUSART; i++)
    {
      // 跳过未配置的端口

      if (g_uart_devs[i] == 0)
        {
          continue;
        }

#ifndef CONFIG_STM32_SERIAL_DISABLE_REORDERING
      // 如果没有禁用重排序,则跳过控制台

      if (g_uart_devs[i]->dev.isconsole)
        {
          continue;
        }
#endif

      // 注册设备并增加次设备号

      devname[9] = '0' + minor++;
      uart_register(devname, &g_uart_devs[i]->dev);
    }

  // 注册剩余的 LPUART 设备,和逻辑相同
  for (i = 0; i < STM32_NLPUART; i++)
    {

      if (g_lpuart_devs[i] == 0)
        {
          continue;
        }

#ifndef CONFIG_STM32_SERIAL_DISABLE_REORDERING
      if (g_lpuart_devs[i]->dev.isconsole)
        {
          continue;
        }
#endif

      devname[9] = '0' + minor++;
      uart_register(devname, &g_lpuart_devs[i]->dev);
    }

#endif
}

可以看到这里是取出 g_uart_devs 数组中的 dev 进行注册。

 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
static struct up_dev_s * const g_uart_devs[STM32_NUSART] =
{
#ifdef CONFIG_STM32_USART1_SERIALDRIVER
  [0] = &g_usart1priv,
#endif
#ifdef CONFIG_STM32_USART2_SERIALDRIVER
  [1] = &g_usart2priv,
#endif
#ifdef CONFIG_STM32_USART3_SERIALDRIVER
  [2] = &g_usart3priv,
#endif
#ifdef CONFIG_STM32_UART4_SERIALDRIVER
  [3] = &g_uart4priv,
#endif
#ifdef CONFIG_STM32_UART5_SERIALDRIVER
  [4] = &g_uart5priv,
#endif
#ifdef CONFIG_STM32_USART6_SERIALDRIVER
  [5] = &g_usart6priv,
#endif
#ifdef CONFIG_STM32_UART7_SERIALDRIVER
  [6] = &g_uart7priv,
#endif
#ifdef CONFIG_STM32_UART8_SERIALDRIVER
  [7] = &g_uart8priv,
#endif
};

可以查看 g_usart3priv

 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
static struct up_dev_s g_usart3priv =
{
  .dev =
    {
#  if CONSOLE_UART == 3
      .isconsole = true,
#  endif
      .recv      =
      {
        .size    = CONFIG_USART3_RXBUFSIZE,
        .buffer  = g_usart3rxbuffer,
      },
      .xmit      =
      {
        .size    = CONFIG_USART3_TXBUFSIZE,
        .buffer  = g_usart3txbuffer,
      },
#  if defined(CONFIG_USART3_RXDMA) && defined(CONFIG_USART3_TXDMA)
      .ops       = &g_uart_rxtxdma_ops,
#  elif defined(CONFIG_USART3_RXDMA) && !defined(CONFIG_USART3_TXDMA)
      .ops       = &g_uart_rxdma_ops,
#  elif !defined(CONFIG_USART3_RXDMA) && defined(CONFIG_USART3_TXDMA)
      .ops       = &g_uart_txdma_ops,
#  else
      .ops       = &g_uart_ops,
#  endif
      .priv      = &g_usart3priv,
    },

  .islpuart      = false,
  .irq           = STM32_IRQ_USART3,
  .parity        = CONFIG_USART3_PARITY,
  .bits          = CONFIG_USART3_BITS,
  .stopbits2     = CONFIG_USART3_2STOP,
  .baud          = CONFIG_USART3_BAUD,
  .apbclock      = STM32_PCLK1_FREQUENCY,
  .usartbase     = STM32_USART3_BASE,
  .tx_gpio       = GPIO_USART3_TX,
  .rx_gpio       = GPIO_USART3_RX,
#  if defined(CONFIG_SERIAL_OFLOWCONTROL) && defined(CONFIG_USART3_OFLOWCONTROL)
  .oflow         = true,
  .cts_gpio      = GPIO_USART3_CTS,
#  endif
#  if defined(CONFIG_SERIAL_IFLOWCONTROL) && defined(CONFIG_USART3_IFLOWCONTROL)
  .iflow         = true,
  .rts_gpio      = GPIO_USART3_RTS,
#  endif
#  ifdef CONFIG_USART3_TXDMA
  .txdma_channel = DMAMAP_USART3_TX,
#  endif
#  ifdef CONFIG_USART3_RXDMA
  .rxdma_channel = DMAMAP_USART3_RX,
  .rxfifo        = g_usart3rxfifo,
#  endif

#  ifdef CONFIG_USART3_RS485
  .rs485_dir_gpio = GPIO_USART3_RS485_DIR,
#    if (CONFIG_USART3_RS485_DIR_POLARITY == 0)
  .rs485_dir_polarity = false,
#    else
  .rs485_dir_polarity = true,
#    endif
#  endif
  .lock = SP_UNLOCKED,
};

可以看到这里已经有 TX 和 RX 的引脚定义,分别是 GPIO_USART3_TXGPIO_USART3_RX

下面以 nsh 的配置为基础配置。

在配置菜单中打开 USART3,并同时采取新版的引脚定义(即需要我们自己附加速度上去)

  • System Type -> STM32 Peripheral Support -> USART3(选中)
  • System Type -> Use the legacy pinmap with GPIO_SPEED_xxx included. (取消选中)

在 board.h 中加入 USART1 和 USART3 的引脚定义

1
2
3
4
5
#define GPIO_USART1_TX          (GPIO_ADJUST_MODE(GPIO_USART1_TX_0, GPIO_MODE_50MHz))
#define GPIO_USART1_RX          GPIO_USART1_RX_0

#define GPIO_USART3_TX          (GPIO_ADJUST_MODE(GPIO_USART3_TX_0, GPIO_MODE_50MHz))
#define GPIO_USART3_RX          GPIO_USART3_RX_0

上板后查看 /dev 目录

nsh> ls /dev
/dev:
 console
 ttyS0
 ttyS1

这里 USART3 成功注册为 /dev/ttyS1(这是由于启用了重排序,因此当 USART2 未开启时,USART3 就使用了 1 编号,这样设备节点中间不会有断层)

编写应用代码

开启 ESP8266 配置:

  • Application Configuration -> Network Utilities -> ESP8266(选中)

这里关于 myapps 的应用配置说两点:

  1. 更完善的选择:其实可以在 MY_APPS_WIFI 的配置下用 select 语句顺带选择 ESP8266,这样依赖关系更清晰
  2. 每次新增 Kconfig 文件后好像都需要重新进行配置,这样前面配过的 USART3 又要重新选一次了…

这里开始是比较痛苦的过程了,因为调试发现 esp8266.c 代码有点旧,这是它的参考接口文档:ESP8266 - AT Command Reference · room-15,可以看到日期是 2015 年的。

最新的 ESP8266 AT 版本应该是 v2.3.0.0,官方接口文档链接:ESP-AT 用户指南

所以目前的 esp8266.c 使用起来会有大量的报错,主要是各类对 AT 命令返回值的解析函数和目前的固件对不上。

这里对 esp8266.c 和 esp8266.h 都做了一些修改,修改后的代码可以看仓库,这里只是想快速连上网,就不展开了。

这里的 AT 命令收发也蛮有意思的,当前代码有非常多可以优化改进的地方,后续有空会考虑重写一下然后提个 PR 吧。

应用代码 wifi.c 这里也不展示了,见仓库即可,主要设计了 3 个命令:

  • scan:扫描附近的 wifi

    nsh> wifi scan
    [wifi] Initializing Driver...
    [wifi] Performing Soft Reset before scan...
    [wifi] Starting Scan...
       SSID: <Hidden>                  | RSSI: -51 | Sec: WPA2_PSK
       SSID: 1504                      | RSSI: -51 | Sec: WPA2_PSK
       SSID: <Hidden>                  | RSSI: -53 | Sec: WPA2_PSK
       SSID: New                       | RSSI: -69 | Sec: WPA2_PSK
       SSID: <Hidden>                  | RSSI: -70 | Sec: WPA_WPA2_PSK
       SSID: CU_CUNF                   | RSSI: -72 | Sec: WPA2_PSK
       SSID: midea_ea_0885             | RSSI: -73 | Sec: OPEN
       SSID: <Hidden>                  | RSSI: -76 | Sec: WPA2_PSK
       SSID: CU_QI5S                   | RSSI: -76 | Sec: WPA2_WPA3_PSK
       SSID: 1504                      | RSSI: -77 | Sec: WPA2_PSK
       SSID: 曲                       | RSSI: -79 | Sec: WPA_WPA2_PSK
       SSID: CU_Ab35                   | RSSI: -86 | Sec: WPA2_PSK
       SSID: CU_saEX                   | RSSI: -86 | Sec: WPA2_PSK
       SSID: WangRQC_SMALL_2.4G        | RSSI: -88 | Sec: WPA2_PSK
       SSID: ChinaNet-UmyN             | RSSI: -88 | Sec: WPA_WPA2_PSK
       SSID: ChinaNet-24gU             | RSSI: -91 | Sec: WPA_WPA2_PSK
       SSID: CU_t9RS                   | RSSI: -91 | Sec: WPA_WPA2_PSK
       SSID: CMCC-6Eu2                 | RSSI: -91 | Sec: WPA_WPA2_PSK
       SSID: Tenda_DE2990              | RSSI: -92 | Sec: WPA_WPA2_PSK
       SSID: 11-1-1503                 | RSSI: -92 | Sec: WPA_WPA2_PSK
    [wifi] Scan complete. Found 20 APs.
    

    如果扫满不到自己的 wifi,大概率是 wifi 设置问题(ESP8266 不支持太新的wifi),这里推荐一个配置:

  • 频段:2.4 GHz
  • 安全模式:WPA2-PSK
  • 无线模式:b/g/n
  • connect:连接 wifi

    nsh> wifi connect [SSID] [PASSWORD]
    [wifi] Initializing Driver...
    [wifi] Resetting module...
    [wifi] Connecting to SSID: [SSID]...
    [wifi] Joined. Waiting for DHCP (Max 15s)...
    [wifi] DHCP Success!
    [wifi] IP Addr: 192.168.5.52
    [wifi] Gateway: 192.168.5.1
    
  • test:测试

    nsh> wifi test
    [wifi] Initializing Driver...
    [wifi] Current IP: 192.168.5.55
    [wifi] Resolving DNS for example.com ...
    [wifi] Target IP: 104.18.27.120
    [wifi] Creating SSL Socket...
    [wifi] Connecting to Server...
    [wifi] Sending Request:
    GET / HTTP/1.1
    Host: example.com
    User-Agent: NuttX-ESP8266
    Accept: */*
    Connection: close
    
    [wifi] Waiting for response...
    
    ----- REMOTE RESPONSE -----
    HTTP/1.1 200 OK
    Date: Tue, 20 Jan 2026 07:58:02 GMT
    Content-Type: text/html
    Transfer-Encoding: chunked
    Connection: close
    CF-RAY: 9c0d06419e9ac277-LAX
    last-modified: Mon, 19 Jan 2026 19:33:01 GMT
    allow: GET, HEAD
    Age: 4
    cf-cache-status: HIT
    Accept-Ranges: bytes
    Server: cloudflare
    
    201
    <!doctype html><html lang="en"><head><title>Example Domain</title><meta name="viewport" content="width=device-width, initial-scale=1"><style>body{background:#eee;width:60vw;margin:15vh auto;font-family:system-ui,sans-serif}h1{font-size:1.5em}div{opacity:0.8}a:link,a:visited{color:#348}</style><body><div><h1>Example Domain</h1><p>This domain is for use in documentation examples without needing permission. Avoid use in operations.<p><a href="https://iana.org/domains/example">Learn more</a></div></body></html>
    
    
    
    
    --------------------------------------------
    [wifi] Total received: 819 bytes
    

    原本选用 baidu.com,但返回的内容太多了(Content-Length: 29506)导致缓冲区溢出,两个根本原因:

    • 数据处理太慢(涉及驱动优化)
    • 缓冲区大小不够(SRAM 有限)

    因此换成了 example.com 。

补充:这里编译会有一个报错:未找到板级特定的 arm_netinitialize 函数,因此这里加了一个 stm32_network.c 文件,其中对这个函数进行了空实现。

提升网速

这节主要是想讨论一下怎么提升数据收发的速度。因为都实现起来内容太多,所以就探讨一个方案的可行性。

数据收发全过程梳理

以上内容只是代码上实现,对于原理来说掌握得一知半解,因此还是要回过头把底层过程整理明白。

USART 驱动层

重新看回这个 USART 驱动结构体

 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
static struct up_dev_s g_usart3priv =
{
  .dev =
    {
#  if CONSOLE_UART == 3
      .isconsole = true,
#  endif
      .recv      =
      {
        .size    = CONFIG_USART3_RXBUFSIZE,
        .buffer  = g_usart3rxbuffer,
      },
      .xmit      =
      {
        .size    = CONFIG_USART3_TXBUFSIZE,
        .buffer  = g_usart3txbuffer,
      },
#  if defined(CONFIG_USART3_RXDMA) && defined(CONFIG_USART3_TXDMA)
      .ops       = &g_uart_rxtxdma_ops,
#  elif defined(CONFIG_USART3_RXDMA) && !defined(CONFIG_USART3_TXDMA)
      .ops       = &g_uart_rxdma_ops,
#  elif !defined(CONFIG_USART3_RXDMA) && defined(CONFIG_USART3_TXDMA)
      .ops       = &g_uart_txdma_ops,
#  else
      .ops       = &g_uart_ops,
#  endif
      .priv      = &g_usart3priv,
    },

  .islpuart      = false,
  .irq           = STM32_IRQ_USART3,
  .parity        = CONFIG_USART3_PARITY,
  .bits          = CONFIG_USART3_BITS,
  .stopbits2     = CONFIG_USART3_2STOP,
  .baud          = CONFIG_USART3_BAUD,
  .apbclock      = STM32_PCLK1_FREQUENCY,
  .usartbase     = STM32_USART3_BASE,
  .tx_gpio       = GPIO_USART3_TX,
  .rx_gpio       = GPIO_USART3_RX,
#  if defined(CONFIG_SERIAL_OFLOWCONTROL) && defined(CONFIG_USART3_OFLOWCONTROL)
  .oflow         = true,
  .cts_gpio      = GPIO_USART3_CTS,
#  endif
#  if defined(CONFIG_SERIAL_IFLOWCONTROL) && defined(CONFIG_USART3_IFLOWCONTROL)
  .iflow         = true,
  .rts_gpio      = GPIO_USART3_RTS,
#  endif
#  ifdef CONFIG_USART3_TXDMA
  .txdma_channel = DMAMAP_USART3_TX,
#  endif
#  ifdef CONFIG_USART3_RXDMA
  .rxdma_channel = DMAMAP_USART3_RX,
  .rxfifo        = g_usart3rxfifo,
#  endif

#  ifdef CONFIG_USART3_RS485
  .rs485_dir_gpio = GPIO_USART3_RS485_DIR,
#    if (CONFIG_USART3_RS485_DIR_POLARITY == 0)
  .rs485_dir_polarity = false,
#    else
  .rs485_dir_polarity = true,
#    endif
#  endif
  .lock = SP_UNLOCKED,
};

这里定义了中断(irq)、数据接收缓冲区(g_usart3rxbuffer)和数据发送缓冲区(g_usart3txbuffer)。

USART 接收字节的过程是:

  • 外设把串口线上来的 bit 采样、组装成 1 个字节
  • 字节放进数据寄存器 DR
  • 同时置位状态位
  • CPU 必须尽快读 DR,把字节拿走

这里 USART 通知 CPU 读取的方法就是中断,其注册是在 open 这个字符设备文件时完成的

  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
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
static const struct file_operations g_serialops =
{
  uart_open,    /* open */
  uart_close,   /* close */
  NULL,         /* read */
  NULL,         /* write */
  NULL,         /* seek */
  uart_ioctl,   /* ioctl */
  NULL,         /* mmap */
  NULL,         /* truncate */
  uart_poll,    /* poll */
  uart_readv,   /* readv */
  uart_writev   /* writev */
#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS
  , uart_unlink /* unlink */
#endif
};

static int uart_open(FAR struct file *filep)
{
  FAR struct inode *inode = filep->f_inode;
  FAR uart_dev_t   *dev   = inode->i_private;
  uint8_t           tmp;
  int               ret;

  /* If the port is the middle of closing, wait until the close is finished.
   * If a signal is received while we are waiting, then return EINTR.
   */

  ret = nxmutex_lock(&dev->closelock);
  if (ret < 0)
    {
      /* A signal received while waiting for the last close operation. */

      return ret;
    }

#ifdef CONFIG_SERIAL_REMOVABLE
  /* If the removable device is no longer connected, refuse to open the
   * device.  We check this after obtaining the close semaphore because
   * we might have been waiting when the device was disconnected.
   */

  if (dev->disconnected)
    {
      ret = -ENOTCONN;
      goto errout_with_lock;
    }
#endif

  /* Start up serial port */

  /* Increment the count of references to the device. */

  tmp = dev->open_count + 1;
  if (tmp == 0)
    {
      /* More than 255 opens; uint8_t overflows to zero */

      ret = -EMFILE;
      goto errout_with_lock;
    }

  /* Check if this is the first time that the driver has been opened. */

  if (tmp == 1)
    {
      irqstate_t flags = enter_critical_section();

      /* If this is the console, then the UART has already been
       * initialized.
       */

      if (!dev->isconsole)
        {
          /* Perform one time hardware initialization */

          ret = uart_setup(dev);
          if (ret < 0)
            {
              leave_critical_section(flags);
              goto errout_with_lock;
            }
        }

      /* In any event, we do have to configure for interrupt driven mode of
       * operation.  Attach the hardware IRQ(s). Hmm.. should shutdown() the
       * the device in the rare case that uart_attach() fails, tmp==1, and
       * this is not the console.
       */

      ret = uart_attach(dev);
      if (ret < 0)
        {
          if (!dev->isconsole)
            {
              uart_shutdown(dev);
            }

          leave_critical_section(flags);
          goto errout_with_lock;
        }

#ifdef CONFIG_SERIAL_RXDMA
      /* Notify DMA that there is free space in the RX buffer */

      uart_dmarxfree(dev);
#endif

      /* Enable the RX interrupt */

      uart_enablerxint(dev);
      leave_critical_section(flags);
    }

  /* Save the new open count on success */

  dev->open_count = tmp;

errout_with_lock:
  nxmutex_unlock(&dev->closelock);
  return ret;
}

可以看到在第一次打开时,会分别进行 uart_setupuart_attachuart_enablerxint

up_setup 主要的作用是

  • 开时钟
  • 配 GPIO
  • 配 USART 寄存器(CR1/CR2/CR3/BRR 等)
  • 使能 USART 收发
  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
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
static int up_setup(struct uart_dev_s *dev)
{
  struct up_dev_s *priv = (struct up_dev_s *)dev->priv;

#ifndef CONFIG_SUPPRESS_UART_CONFIG
  uint32_t regval;

  /* Note: The logic here depends on the fact that that the USART module
   * was enabled in stm32_lowsetup().
   */

  /* Enable USART APB1/2 clock */

  up_set_apb_clock(dev, true);

  /* Configure pins for USART use */

  stm32_configgpio(priv->tx_gpio);
  stm32_configgpio(priv->rx_gpio);

#ifdef CONFIG_SERIAL_OFLOWCONTROL
  if (priv->cts_gpio != 0)
    {
      stm32_configgpio(priv->cts_gpio);
    }
#endif

#ifdef CONFIG_SERIAL_IFLOWCONTROL
  if (priv->rts_gpio != 0)
    {
      uint32_t config = priv->rts_gpio;

#ifdef CONFIG_STM32_FLOWCONTROL_BROKEN
      /* Instead of letting hw manage this pin, we will bitbang */

      config = (config & ~GPIO_MODE_MASK) | GPIO_OUTPUT;
#endif
      stm32_configgpio(config);
    }
#endif

#ifdef HAVE_RS485
  if (priv->rs485_dir_gpio != 0)
    {
      stm32_configgpio(priv->rs485_dir_gpio);
      stm32_gpiowrite(priv->rs485_dir_gpio, !priv->rs485_dir_polarity);
    }
#endif

  /* Configure CR2
   * Clear STOP, CLKEN, CPOL, CPHA, LBCL, and interrupt enable bits
   */

  regval  = up_serialin(priv, STM32_USART_CR2_OFFSET);
  if (priv->islpuart == true)
    {
      regval &= ~(USART_CR2_STOP_MASK | USART_CR2_CLKEN);
    }
  else
    {
      regval &= ~(USART_CR2_STOP_MASK | USART_CR2_CLKEN | USART_CR2_CPOL |
                  USART_CR2_CPHA | USART_CR2_LBCL | USART_CR2_LBDIE);
    }

  /* Configure STOP bits */

  if (priv->stopbits2)
    {
      regval |= USART_CR2_STOP2;
    }

  up_serialout(priv, STM32_USART_CR2_OFFSET, regval);

  /* Configure CR1
   * Clear TE, REm and all interrupt enable bits
   */

  regval  = up_serialin(priv, STM32_USART_CR1_OFFSET);

#ifdef CONFIG_STM32_LPUART1
  if (priv->islpuart == true)
    {
      regval &= ~(USART_CR1_TE | USART_CR1_RE | LPUART_CR1_ALLINTS);
    }
  else
#endif
    {
      regval &= ~(USART_CR1_TE | USART_CR1_RE | USART_CR1_ALLINTS);
    }

  up_serialout(priv, STM32_USART_CR1_OFFSET, regval);

  /* Configure CR3
   * Clear CTSE, RTSE, and all interrupt enable bits
   */

  regval  = up_serialin(priv, STM32_USART_CR3_OFFSET);
  regval &= ~(USART_CR3_CTSIE | USART_CR3_CTSE | USART_CR3_RTSE |
              USART_CR3_EIE);

  up_serialout(priv, STM32_USART_CR3_OFFSET, regval);

  /* Configure the USART line format and speed. */

  up_set_format(dev);

  /* Enable Rx, Tx, and the USART */

  regval      = up_serialin(priv, STM32_USART_CR1_OFFSET);
  regval     |= (USART_CR1_UE | USART_CR1_TE | USART_CR1_RE);
  up_serialout(priv, STM32_USART_CR1_OFFSET, regval);

#endif /* CONFIG_SUPPRESS_UART_CONFIG */

  /* Set up the cached interrupt enables value */

  priv->ie    = 0;

  /* Mark device as initialized. */

  priv->initialized = true;

  return OK;
}

up_attach 是配置 CPU 侧的中断:注册中断服务例程(ISR),并启用中断,在 NVIC 层面允许这个 IRQ 进来

此处 IRQ 是 STM32_IRQ_USART3,值为 55。ISR 是函数 up_interrupt

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
static int up_attach(struct uart_dev_s *dev)
{
  struct up_dev_s *priv = (struct up_dev_s *)dev->priv;
  int ret;

  /* Attach and enable the IRQ */

  ret = irq_attach(priv->irq, up_interrupt, priv);
  if (ret == OK)
    {
      /* Enable the interrupt (RX and TX interrupts are still disabled
       * in the USART
       */

      up_enable_irq(priv->irq);
    }

  return ret;
}

up_rxint 是配置外设侧的中断:在 USART 外设里设置 RXNEIE 等位,在外设层面允许“接收事件”去触发 IRQ

 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
static void up_rxint(struct uart_dev_s *dev, bool enable)
{
  struct up_dev_s *priv = (struct up_dev_s *)dev->priv;
  irqstate_t flags;
  uint16_t ie;

  /* USART receive interrupts:
   *
   * Enable             Status          Meaning                   Usage
   * ------------------ --------------- ------------------------- ----------
   * USART_CR1_IDLEIE   USART_SR_IDLE   Idle Line Detected        (not used)
   * USART_CR1_RXNEIE   USART_SR_RXNE   Rx Data Ready to be Read
   * "              "   USART_SR_ORE    Overrun Error Detected
   * USART_CR1_PEIE     USART_SR_PE     Parity Error
   *
   * USART_CR2_LBDIE    USART_SR_LBD    Break Flag
   * USART_CR3_EIE      USART_SR_FE     Framing Error
   * "           "      USART_SR_NE     Noise Error
   * "           "      USART_SR_ORE    Overrun Error Detected
   */

  flags = enter_critical_section();
  ie = priv->ie;
  if (enable)
    {
      /* Receive an interrupt when their is anything in the Rx data register
       * (or an Rx timeout occurs).
       */

#ifndef CONFIG_SUPPRESS_SERIAL_INTS
#ifdef CONFIG_USART_ERRINTS
      ie |= (USART_CR1_RXNEIE | USART_CR1_PEIE | USART_CR3_EIE);
#else
      ie |= USART_CR1_RXNEIE;
#endif
#endif
    }
  else
    {
      ie &= ~(USART_CR1_RXNEIE | USART_CR1_PEIE | USART_CR3_EIE);
    }

  /* Then set the new interrupt state */

  up_restoreusartint(priv, ie);
  leave_critical_section(flags);
}

这样在 open 后我们就设置好了中断,当 USART 外设接收到来自 ESP8266 发送来的数据时,就会触发中断,然后 CPU 跳转至 up_interrupt 进行处理。

  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
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
static int up_interrupt(int irq, void *context, void *arg)
{
  struct up_dev_s *priv = (struct up_dev_s *)arg;
  int  passes;
  bool handled;

  DEBUGASSERT(priv != NULL);

  /* Report serial activity to the power management logic */

#if defined(CONFIG_PM) && CONFIG_STM32_PM_SERIAL_ACTIVITY > 0
  pm_activity(PM_IDLE_DOMAIN, CONFIG_STM32_PM_SERIAL_ACTIVITY);
#endif

  /* Loop until there are no characters to be transferred or,
   * until we have been looping for a long time.
   */

  handled = true;
  for (passes = 0; passes < 256 && handled; passes++)
    {
      handled = false;

      /* Get the masked USART status word. */

      priv->sr = up_serialin(priv, STM32_USART_SR_OFFSET);

      /* USART interrupts:
       *
       * Enable             Status          Meaning                Usage
       * ------------------ --------------- ---------------------- ----------
       * USART_CR1_IDLEIE   USART_SR_IDLE   Idle Line Detected     (not used)
       * USART_CR1_RXNEIE   USART_SR_RXNE   Rx Data Ready
       * "              "   USART_SR_ORE    Overrun Error Detected
       * USART_CR1_TCIE     USART_SR_TC     Tx Complete            (RS-485)
       * USART_CR1_TXEIE    USART_SR_TXE    Tx Data Register Empty
       * USART_CR1_PEIE     USART_SR_PE     Parity Error
       *
       * USART_CR2_LBDIE    USART_SR_LBD    Break Flag
       * USART_CR3_EIE      USART_SR_FE     Framing Error
       * "           "      USART_SR_NE     Noise Error
       * "           "      USART_SR_ORE    Overrun Error Detected
       * USART_CR3_CTSIE    USART_SR_CTS    CTS flag               (not used)
       *
       * NOTE: Some of these status bits must be cleared by explicitly
       * writing zero to the SR register: USART_SR_CTS, USART_SR_LBD. Note of
       * those are currently being used.
       */

      /* Error report */

#ifdef CONFIG_SERIAL_TIOCGICOUNT
      if (priv->sr & USART_SR_FE)
        {
          priv->icount.frame++;
        }

      if (priv->sr & USART_SR_ORE)
        {
          priv->icount.overrun++;
        }

      if (priv->sr & USART_SR_PE)
        {
          priv->icount.parity++;
        }

      if (priv->sr & USART_SR_LBD)
        {
          priv->icount.brk++;
        }
#endif

#ifdef HAVE_RS485
      /* Transmission of whole buffer is over - TC is set, TXEIE is cleared.
       * Note - this should be first, to have the most recent TC bit value
       * from SR register - sending data affects TC, but without refresh we
       * will not know that...
       */

      if (((priv->sr & USART_SR_TC) != 0) &&
          ((priv->ie & USART_CR1_TCIE) != 0) &&
          ((priv->ie & USART_CR1_TXEIE) == 0))
        {
          stm32_gpiowrite(priv->rs485_dir_gpio, !priv->rs485_dir_polarity);
          up_restoreusartint(priv, priv->ie & ~USART_CR1_TCIE);
        }
#endif

      /* Handle incoming, receive bytes. */

      if (((priv->sr & USART_SR_RXNE) != 0) &&
          ((priv->ie & USART_CR1_RXNEIE) != 0))
        {
          /* Received data ready... process incoming bytes.  NOTE the check
           * for RXNEIE:  We cannot call uart_recvchards of RX interrupts are
           * disabled.
           */

          uart_recvchars(&priv->dev);
          handled = true;
        }

      /* We may still have to read from the DR register to clear any pending
       * error conditions.
       */

      else if ((priv->sr & (USART_SR_ORE | USART_SR_NE | USART_SR_FE |
                            USART_SR_LBD)) != 0)
        {
#if defined(CONFIG_STM32_STM32F30XX) || defined(CONFIG_STM32_STM32F33XX) || \
    defined(CONFIG_STM32_STM32F37XX) || defined(CONFIG_STM32_STM32G4XXX)
          /* These errors are cleared by writing the corresponding bit to the
           * interrupt clear register (ICR).
           */

          up_serialout(priv, STM32_USART_ICR_OFFSET,
                      (USART_ICR_NCF | USART_ICR_ORECF | USART_ICR_FECF |
                       USART_ICR_LBDCF));
#else
          /* If an error occurs, read from DR to clear the error (data has
           * been lost).  If ORE is set along with RXNE then it tells you
           * that the byte *after* the one in the data register has been
           * lost, but the data register value is correct.  That case will
           * be handled above if interrupts are enabled. Otherwise, that
           * good byte will be lost.
           */

          up_serialin(priv, STM32_USART_RDR_OFFSET);
#endif
        }

      /* Handle outgoing, transmit bytes */

      if (((priv->sr & USART_SR_TXE) != 0) &&
          ((priv->ie & USART_CR1_TXEIE) != 0))
        {
          /* Transmit data register empty ... process outgoing bytes */

          uart_xmitchars(&priv->dev);
          handled = true;
        }
    }

  return OK;
}

这里进行最多 256 次的一个循环,每次循环中:

  • 先复制当前的状态保存到 sr 中,
  • 然后判断是否是“接收中断”(((priv->sr & USART_SR_RXNE) != 0) && ((priv->ie & USART_CR1_RXNEIE) != 0)):
    • 如果是 RXNE,则调用 uart_recvchars 去读取字符到接收缓冲区中
    • 如果不是 RXNE,并发现 sr 有 ORE/NE/FE/LBD 等错误位,则进行错误清理,否则中断可能会反复触发
  • 最后检查发送数据寄存器 DR 是否为空,如果是空的,则调用 uart_xmitchars 处理字符发送。

只要在一次循环中做了数据读取或数据发送,则再重复进行一次,否则退出循环。这样的好处是在一次中断中尽可能多处理接收和发送事件,避免频繁进中断。

我们这里关注 uart_recvchars 函数

  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
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
void uart_recvchars(FAR uart_dev_t *dev)
{
  FAR struct uart_buffer_s *rxbuf = &dev->recv;
#ifdef CONFIG_SERIAL_IFLOWCONTROL_WATERMARKS
  /* Pre-calculate the watermark level that we will need to test against. */

  unsigned int watermark =
    (CONFIG_SERIAL_IFLOWCONTROL_UPPER_WATERMARK * rxbuf->size) / 100;
#endif
#if defined(CONFIG_TTY_SIGINT) || defined(CONFIG_TTY_SIGTSTP) || \
    defined(CONFIG_TTY_FORCE_PANIC) || defined(CONFIG_TTY_LAUNCH)
  int signo = 0;
#endif
  uint16_t nbytes = 0;

  /* Loop putting characters into the receive buffer until there are no
   * further characters to available.
   */

  while (uart_rxavailable(dev))
    {
      int nexthead = rxbuf->head + 1 < rxbuf->size ? rxbuf->head + 1 : 0;
      bool is_full = (nexthead == rxbuf->tail);
      FAR char *pbuf;
      char ch;

#ifdef CONFIG_SERIAL_IFLOWCONTROL_WATERMARKS
      unsigned int nbuffered;

      /* How many bytes are buffered */

      if (rxbuf->head >= rxbuf->tail)
        {
          nbuffered = rxbuf->head - rxbuf->tail;
        }
      else
        {
          nbuffered = rxbuf->size - rxbuf->tail + rxbuf->head;
        }

      /* Is the level now above the watermark level that we need to report? */

      if (nbuffered >= watermark)
        {
          /* Let the lower level driver know that the watermark level has
           * been crossed.  It will probably activate RX flow control.
           */

          if (uart_rxflowcontrol(dev, nbuffered, true))
            {
              /* Low-level driver activated RX flow control, exit loop now. */

              break;
            }
        }
#elif defined(CONFIG_SERIAL_IFLOWCONTROL)
      /* Check if RX buffer is full and allow serial low-level driver to
       * pause processing. This allows proper utilization of hardware flow
       * control.
       */

      if (is_full)
        {
          if (uart_rxflowcontrol(dev, rxbuf->size, true))
            {
              /* Low-level driver activated RX flow control, exit loop now. */

              break;
            }
        }
#endif

      /* Get this next character from the hardware */

      if (!is_full && dev->ops->recvbuf)
        {
          ssize_t ret;

          if (rxbuf->tail > rxbuf->head)
            {
              nbytes = rxbuf->tail - rxbuf->head - 1;
            }
          else if (rxbuf->tail)
            {
              nbytes = rxbuf->size - rxbuf->head;
            }
          else
            {
              nbytes = rxbuf->size - rxbuf->head - 1;
            }

          pbuf = &rxbuf->buffer[rxbuf->head];
          ret = uart_recvbuf(dev, pbuf, nbytes);
          if (ret <= 0)
            {
              continue;
            }

          nbytes = ret;
          rxbuf->head += nbytes;
          if (rxbuf->head >= rxbuf->size)
            {
              rxbuf->head = 0;
            }
        }
      else
        {
          unsigned int status;

          ch = uart_receive(dev, &status);
          pbuf = &ch;
          nbytes = 1;

          /* If the RX buffer becomes full, then the serial data is
           * discarded. This is necessary because on most serial hardware,
           * you must read the data in order to clear the RX interrupt.
           * An option on some hardware might be to simply disable RX
           * interrupts until the RX buffer becomes non-FULL. However, that
           * would probably just cause the overrun to occur in hardware
           * (unless it has some large internal buffering).
           */

          if (!is_full)
            {
              /* Add the character to the buffer */

              rxbuf->buffer[rxbuf->head] = ch;

              /* Increment the head index */

              rxbuf->head = nexthead;
            }
        }

#if defined(CONFIG_TTY_SIGINT) || defined(CONFIG_TTY_SIGTSTP) || \
    defined(CONFIG_TTY_FORCE_PANIC) || defined(CONFIG_TTY_LAUNCH)
      signo = uart_check_special(dev, pbuf, nbytes);
#endif
    }

  /* If any bytes were added to the buffer, inform any waiters there is new
   * incoming data available.
   */

  if (rxbuf->head >= rxbuf->tail)
    {
      nbytes = rxbuf->head - rxbuf->tail;
    }
  else
    {
      nbytes = rxbuf->size - rxbuf->tail + rxbuf->head;
    }

#ifdef CONFIG_SERIAL_TERMIOS
  if (nbytes >= dev->minrecv)
#else
  if (nbytes)
#endif
    {
      uart_datareceived(dev);
    }

#if defined(CONFIG_TTY_SIGINT) || defined(CONFIG_TTY_SIGTSTP) || \
    defined(CONFIG_TTY_FORCE_PANIC) || defined(CONFIG_TTY_LAUNCH)
  /* Send the signal if necessary */

  if (signo != 0)
    {
      nxsig_tgkill(-1, dev->pid, signo);
    }
#endif
}

这里也有个 while 循环,while (uart_rxavailable(dev)) 表示只要硬件还有数据,就一直取。在循环中会计算环形缓冲区 rxbuf 的写入位置,然后调用 uart_receive(dev, &status) 来做架构特定的实际 receive 操作,最后当有写入数据时,调用 uart_datareceived(dev) 来唤醒睡眠的线程(这里就是 esp8266 的 worker)。 这里的 uart_receive 最终会到 up_receive,它会使用 up_serialin 做实际的数据寄存器读取。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static int up_receive(struct uart_dev_s *dev, unsigned int *status)
{
  struct up_dev_s *priv = (struct up_dev_s *)dev->priv;
  uint32_t rdr;

  /* Get the Rx byte */

  rdr      = up_serialin(priv, STM32_USART_RDR_OFFSET);

  /* Get the Rx byte plux error information.  Return those in status */

  *status  = priv->sr << 16 | rdr;
  priv->sr = 0;

  /* Then return the actual received byte */

  return rdr & 0xff;
}

static inline uint32_t up_serialin(struct up_dev_s *priv, int offset)
{
  return getreg32(priv->usartbase + offset);
}

然后看 uart_datareceived,它会把 poll() 那边挂着的等待者唤醒,并且也会唤醒阻塞在 read() 的线程。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
void uart_datareceived(FAR uart_dev_t *dev)
{
  /* Notify all poll/select waiters that they can read from the recv buffer */

  uart_poll_notify(dev, 0, CONFIG_SERIAL_NPOLLWAITERS, POLLIN);

  /* Is there a thread waiting for read data?  */

  uart_wakeup(&dev->recvsem);

#if defined(CONFIG_PM) && defined(CONFIG_SERIAL_CONSOLE)
  /* Call pm_activity when characters are received on the console device */

  if (dev->isconsole)
    {
#  if CONFIG_SERIAL_PM_ACTIVITY_PRIORITY > 0
      pm_activity(CONFIG_SERIAL_PM_ACTIVITY_DOMAIN,
                  CONFIG_SERIAL_PM_ACTIVITY_PRIORITY);
#  endif
    }
#endif
}

到此 USART 驱动层就结束了它的使命,当接收到硬件中断后,它把字符数据拷贝到缓冲区中,然后通知上层线程进行处理。

ESP8266 worker

我们在 ESP8266 初始化时,把打开的 USART 字符设备文件保存到了 g_lesp_state.fd 中,然后创建了 worker 线程。这里仔细看一下 worker 线程的代码。

  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
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
static void *lesp_worker(void *args)
{
  int ret = 0;
  int rxlen = 0;

  uint8_t tmp_buf[256]; 
  int i;

  lesp_worker_t *worker = &g_lesp_state.worker;

  UNUSED(args);

  ninfo("worker Started\n");

  while (worker->running)
    {
      // 原代码:ret = lesp_low_level_read(&c, 1);
      // 逐字符读取效率太低,因此这里改用缓冲区进行批量读取
      ret = lesp_low_level_read(tmp_buf, sizeof(tmp_buf));

      if (ret < 0)
        {
          if (errno == EINTR)
            {
              continue; 
            }
          nerr("ERROR: worker read data Error %d\n", ret);
          usleep(1000); 
        }
      else if (ret > 0)
        {
          pthread_mutex_lock(&(worker->mutex));

          for (i = 0; i < ret; i++)
            {
              uint8_t c = tmp_buf[i];

              // 遇到 '\n' 就结算一行
              if (c == '\n')
                {
                  // 处理 CRLF
                  if (rxlen > 0 && worker->rxbuf[rxlen - 1] == '\r')
                    {
                      rxlen--;
                    }

                  DEBUGASSERT(rxlen >= 0);
                  DEBUGASSERT(rxlen < BUF_WORKER_LEN);

                  worker->rxbuf[rxlen] = '\0';

                  if (rxlen != 0)
                    {
                      // 特判几种“控制行”
                      if (strcmp(worker->rxbuf, "OK") == 0)
                        {
                          worker->and = LESP_OK;
                        }
                      else if ((strcmp(worker->rxbuf, "FAIL") == 0) ||
                               (strcmp(worker->rxbuf, "ERROR") == 0)
                              )
                        {
                          worker->and = LESP_ERR;
                        }
                      else if ((rxlen == 8) &&
                                (memcmp(worker->rxbuf + 1, ",CLOSED", 7) == 0))
                        {
                          unsigned int sockid = worker->rxbuf[0] - '0';
                          if (sockid < SOCKET_NBR)
                            {
                              mark_sock_remote_closed(sockid);
                            }
                        }
                      else
                        {
                          // 一个简单的流控策略,如果上层还没取走数据,则让出CPU等待
                          if (worker->buf[0] != '\0')
                            {
                              pthread_mutex_unlock(&(worker->mutex));
                              usleep(100); /* leave time for application to read buffer */
                              pthread_mutex_lock(&(worker->mutex));
                            }

                          /* ninfo("Worker Read data:%s\n", worker->rxbuf); */

                          // 这里 rxbuf 是正在构建中的行缓冲区,buf 是要交付给上层的缓冲区
                          // 本质行为是这一行已经组装完毕了(遇到 '\n'),要交付给上层
                          if (rxlen + 1 <= BUF_ANS_LEN)
                            {
                              memcpy(worker->buf, worker->rxbuf, rxlen + 1);
                            }
                          else
                            {
                              nerr("Worker and line is too long:%s\n",
                                   worker->rxbuf);
                            }
                        }

                      // 通知上层应用处理该行
                      sem_post(&worker->sem);

                      // 该行结束,复用 rxbuf
                      worker->rxbuf[0] = '\0';
                      rxlen = 0;
                    }
                }
              else if (rxlen < BUF_WORKER_LEN - 1)
                {
                  // 把字符 c 写入到缓冲区中,本质是在缓冲区中拼装一个完整的行,rxlen 是当前行的长度
                  worker->rxbuf[rxlen++] = c;
                  
                  // +IPD 逻辑
                  // 一旦看到 :,并且缓存开头是 +IPD,,就认为 header 完整了
                  if ((c == ':') && (memcmp(worker->rxbuf, "+IPD,", 5) == 0))
                    {
                      int sockfd;
                      int len;
                      char *ptr = worker->rxbuf + 5;

                      sockfd = lesp_str_to_unsigned(&ptr, ',');
                      if (sockfd >= 0)
                        {
                          len = lesp_str_to_unsigned(&ptr, ':');
                          if (len >= 0)
                            {
                              // 读取 IPD
                              // 这里有一个风险:lesp_read_ipd() 会直接从 UART 再读剩余 payload
                              // 但此时 tmp_buf 里可能还有“已经读出来但还没处理的字节”
                              // 这里两个消费流可能会导致 payload 数据错位/丢字节/解析混乱问题,待改进
                              lesp_read_ipd(sockfd, len);
                            }
                        }

                      rxlen = 0;
                    }
                }
              else
                {
                  // 缓冲区溢出保护
                  if (rxlen < BUF_WORKER_LEN) 
                    {
                       // Keep the last character null to prevent OOB
                       worker->rxbuf[BUF_WORKER_LEN - 1] = '\0'; 
                    }
                  nerr("Read char overflow:%c\n", c);
                }
            } /* end for loop */

          pthread_mutex_unlock(&(worker->mutex));
        }
    }

  return NULL;
}

总结来说 worker 就是一个把 USART 字节流变成“按行事件 + IPD 数据事件”的解析线程。如果看的不是很懂,接下去看 lesp_read 就会明白了。

上层应用消费数据

之前提到最后拼凑的数据会拷贝到 worker->buf 中:

1
memcpy(worker->buf, worker->rxbuf, rxlen + 1);

但这个 buffer 并不是最终的存储位置,我们看应用的读取函数 lesp_read

 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
static int lesp_read(int timeout_ms)
{
  int ret = 0;

  struct timespec ts;

  // 检查初始化
  if (! g_lesp_state.is_initialized)
    {
      ninfo("Esp8266 not initialized; can't list access points\n");
      return -1;
    }

  // 计算超时时间点
  // 取当前时间
  if (clock_gettime(CLOCK_REALTIME, &ts) < 0)
    {
      return -1;
    }

  ts.tv_nsec += (timeout_ms % 1000) * 1000000;
  if (ts.tv_nsec >= 1000000000)
    {
      ts.tv_nsec -= 1000000000;
      ts.tv_sec  += 1;
    }

  ts.tv_sec  += (timeout_ms / 1000);

  // 现在 ts 就是会超时的那个时刻

  do
    {
      // 阻塞等待 worker 发来事件,对应 worker 中的 sem_post(&worker->sem)
      // 超时则直接返回 -1
      if (sem_timedwait(&g_lesp_state.worker.sem, &ts) < 0)
        {
          return -1;
        }

      // 保护共享数据
      // worker 线程和调用 lesp_read() 的线程会同时访问:
      // - worker.and
      // - worker.buf
      pthread_mutex_lock(&g_lesp_state.worker.mutex);

      // 状态转移,把 worker 的 and 转移到全局状态中
      if (g_lesp_state.worker.and != LESP_NONE)
        {
          g_lesp_state.and = g_lesp_state.worker.and;
          g_lesp_state.worker.and = LESP_NONE;
        }

      ret = strlen(g_lesp_state.worker.buf);
      if (ret > 0)
        {
          /* +1 to copy null */
		  // 搬运 worker 交付的数据到全局状态中
          memcpy(g_lesp_state.bufans, g_lesp_state.worker.buf, ret + 1);
        }

      // 设置 '\0' 表示数据已经读取了,worker.buf 可以再次被覆盖
      g_lesp_state.worker.buf[0] = '\0';
      pthread_mutex_unlock(&g_lesp_state.worker.mutex);
    } // 这里只要满足其中一个,就结束循环:ret > 0(拿到了一行有效字符串)、g_lesp_state.and != LESP_NONE(拿到了 OK/ERR),否则继续等下一个 sem_post
  while ((ret <= 0) && (g_lesp_state.and == LESP_NONE));

  ninfo("lesp_read %d=>%s and and = %d\n", ret, g_lesp_state.bufans,
        g_lesp_state.and);

  return ret;
}

这个函数总结来说就是:我最多等 timeout_ms 毫秒。每当 worker 告诉我“有新结果了”(sem_post),我就去拿:如果拿到一行文本就把它拷到 bufans;如果拿到 OK/ERROR 就更新 and;拿完就把 worker 的邮箱(worker.buf)清空。只要我拿到了一行或拿到 O`K/ERROR,我就返回;否则继续等,直到超时。

总结

到这整个数据路径应该就已经清楚了:

  1. 上层 API 函数(例如 lesp_ap_get)发送 AT 指令,开始带超时和阻塞的等待结果。
  2. ESP8266 根据指令工作,通过 USART 返回数据。
  3. USART 会触发中断通知 CPU 读取,CPU 会将数据读取到驱动的缓冲区中,然后唤醒调用 poll 的 ESP8266 worker 线程。
  4. worker 线程接收数据进行拼接和识别,当有一个完整的行时就交付给上层 API 函数。
  5. 上层 API 函数被 worker 唤醒,拿到最终数据进行处理并返回上层应用。

DMA

stm32_serial.c 文件中还可以看到有 up_dma_setup 函数,只不过我们之前一直没启用

 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
#if defined(SERIAL_HAVE_RXDMA) || defined(SERIAL_HAVE_TXDMA)
static int up_dma_setup(struct uart_dev_s *dev)
{
  struct up_dev_s *priv = (struct up_dev_s *)dev->priv;
  int result;

  /* Do the basic UART setup first, unless we are the console */

  if (!dev->isconsole)
    {
      result = up_setup(dev);
      if (result != OK)
        {
          return result;
        }
    }

#if defined(SERIAL_HAVE_TXDMA)
  /* Acquire the Tx DMA channel.  This should always succeed. */

  if (priv->txdma_channel != INVALID_SERIAL_DMA_CHANNEL)
    {
      priv->txdma = stm32_dmachannel(priv->txdma_channel);

      /* Enable receive Tx DMA for the UART */

      modifyreg32(priv->usartbase + STM32_USART_CR3_OFFSET,
                  0, USART_CR3_DMAT);
    }
#endif

#if defined(SERIAL_HAVE_RXDMA)
  /* Acquire the DMA channel.  This should always succeed. */

  if (priv->rxdma_channel != INVALID_SERIAL_DMA_CHANNEL)
    {
      priv->rxdma = stm32_dmachannel(priv->rxdma_channel);

      /* Configure for circular DMA reception into the RX fifo */

      stm32_dmasetup(priv->rxdma,
                     priv->usartbase + STM32_USART_RDR_OFFSET,
                     (uint32_t)priv->rxfifo,
                     RXDMA_BUFFER_SIZE,
                     SERIAL_RXDMA_CONTROL_WORD);

      /* Reset our DMA shadow pointer to match the address just
       * programmed above.
       */

      priv->rxdmanext = 0;

      /* Enable receive Rx DMA for the UART */

      modifyreg32(priv->usartbase + STM32_USART_CR3_OFFSET,
                  0, USART_CR3_DMAR);

      /* Start the DMA channel, and arrange for callbacks at the half and
       * full points in the FIFO.  This ensures that we have half a FIFO
       * worth of time to claim bytes before they are overwritten.
       */

      stm32_dmastart(priv->rxdma, up_dma_rxcallback, (void *)priv, true);
    }
#endif

  return OK;
}
#endif

有计算机基础的肯定知道书上讲 I/O 控制方式时,有中断驱动和 DMA 驱动。我们现在的代码用的是中断驱动,USART 外设每次接收到数据时,都会触发中断让 CPU 介入处理一下,尽管在中断服务例程中已经多次循环来减少中断次数,但这里还是会有很大的性能开销。

改用 DMA 的方式后,我们的数据路径就会发生变化:

  • 原来是 USART 每接收到一个字节,就会触发一次 RXNE 中断,让 CPU 进入中断服务例程开始搬数据。
  • 现在是 USART 接收到字节后,产生一个 DMA 请求信号,让 DMA 硬件自动搬数据(整个硬件执行过程包括自动读取 RDR 寄存器,并把数据写入到内存中)。当 DMA 的 FIFO 缓冲区满了之后(nuttx 中的默认配置是 32 字节),由 DMA 触发 CPU 中断,让 CPU 一次性处理这些接收到的字节。

这样优化下来,CPU 进入中断的次数就会大幅降低,避免了反复进入中断的性能开销,让 CPU 可以多处理用户应用程序,变相地提升了 CPU 对数据的处理能力。

驱动代码

这在前面讲 worker 时有提到,原来的 lesp_worker 接收数据是逐字节读取,这会导致频繁的系统调用

1
2
3
    uint8_t c;

    ret = lesp_low_level_read(&c, 1);

这里引入了缓冲区让它进行批量读取,避免了频繁进行系统调用的性能开销。但这目前带来了两个数据消费流的问题,还需要后续改进。

1
2
	uint8_t tmp_buf[256]; 
	ret = lesp_low_level_read(tmp_buf, sizeof(tmp_buf));

这里只是举一个例子,这份驱动代码中能优化的地方还有很多,比如频繁的加锁解锁也会带来庞大的性能开销,可以从架构上思考如何进行优化,变成一个无锁驱动。

USART 波特率

这算是物理链路的带宽限制,目前波特率默认是 115200,理论传输速度是 11.5 KB/s。如果 ESP8266 模块和开发板硬件支持,可以调的更高,比如 460800,理论传输速度就是 45 KB/s,速度提升了 3 倍。

config NETUTILS_ESP8266_BAUDRATE
	int "Serial BAUD rate"
	default 115200

最后

本来是应该写一个类似测速代码,先测出我们优化前的网速,然后应用以上的优化方法,再分别测试优化后的网速做一个性能比较。但由于时间问题和目前我实际也不需要那么极限性能的网络传输,因此这部分就算是“留有遗憾”吧,感兴趣的同学可以自己根据上面的思路去试试“提升网速”。


本站不记录浏览量,但如果您觉得本内容有帮助,请点个小红心,让我知道您的喜欢。