串口通讯

STM32串口通讯有3种形式:轮询(阻塞式)、中断、DMA。我不知道中断方式的串口通讯有什么适合的应用场景:每接收/发送一个字节,就要发生一次中断,这对CPU反而是一种浪费。使用Cube HAL,轮询式的串口通讯最简单了,发送和接收数据分别有一个函数:

HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);

Timeout 参数给 HAL_MAX_DELAY 常量,则无限超时,函数一直阻塞,直到发出/接收数据完成。

STM32F303RE 有3个 USART 和 2个 UART,而 USART 也可以工作在异步模式,相当于一个 UART。Nucleo 的 ST-LINK 部分也同时实现了 USB-TTL 转换,与 MCU 的 USART2 连接。因此,将 C 的标准输入输出(<stdio.h>)重定向到 USART2 似乎是一个挺不错的调试选项。

ARM GCC 使用的是 newlib-nano 标准 C 库。要使用 newlib-nano,并将 stdio 重定向到串口,gcc 选项和重定向的实现可参考 ARM GCC 附带的 retarget 示例,其路径为share\gcc-arm-none-eabi\samples\src\retarget。概括起来:

  • 指定 C 编译器选项: -fno-builtin
  • 指定linker(ld.exe)选项: --specs=nano.specs、--specs=nosys.specs
  • 实现函数 _read()、_write() ,其原型如下。_read() 函数从串口读取数据,_write() 将数据写到串口。stdio 函数如 printf()、getchar() 等将最终调用这两个函数实现输入输出
int _read (int fd, char *ptr, int len);
int _write (int fd, char *ptr, int len);

在 main() 函数中,首先打印一个字符串,然后进入主循环,将接收到的字符原样返回。如下:

...
    puts("Good judgments come from experiences. ");

    uint8_t buf[32];

    while (1) { 
        HAL_UART_Receive(&huart2, buf, 1, HAL_MAX_DELAY);
        putchar(buf[0]);
        fflush(stdout);
    }

我发现,在写出数据(字符/字符串)时,直到 \n 字符时,数据才真正写到串口。这应该是 stdio 的缓冲机制。puts() 自动在字符串末尾增加了一个 \n,因此都回立即写出到串口。而 putchar() 则需要显式 flush 一下。

_write() 重定向实现很简单,直接调用HAL_UART_Transmit():

#include "main.h"
#include "stm32f3xx_hal.h"
#include "usart.h"

int _write (int fd, char *ptr, int len)
{
  /* Write "len" of char from "ptr" to file id "fd"
   * Return number of char written.
   * Need implementing with UART here. */

    HAL_UART_Transmit(&huart2, (uint8_t *) ptr, len, HAL_MAX_DELAY);

  return len;
}