本文主要介绍 STM32 中串口的使用
串口是嵌入式中调试的好工具, 一般来说最先解决的应当是串口,
只有串口调好了, 后面的开发和单元测试写起来才舒服.
处理串口的一个比较经典的做法是采用
DMA + 中断 + 环形缓冲区
的形式,
环形缓冲区有一个读指针和一个写指针.
两个指针保证了缓冲区的数据读写操作是安全的,
即使在读的过程中触发了数据接收中断使程序进入写操作也不会影响到读指针.
下面是一个通用的实现代码示例:
uart_com.c
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 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309
| #include "uart_com.h" #include "lwrb.h"
#define UART_COM_MAX (sizeof(uart_coms) / sizeof(uart_com_dma_t))
#define UART_BIND_STDIO &huart1
typedef struct { lwrb_t rb; uint8_t rbbuf[4096]; uint8_t dma_rxbuf[256]; uint8_t dma_txbuf[4096]; size_t old_pos; UART_HandleTypeDef *huart; uint8_t dma_tx_ready; } uart_com_dma_t;
static uart_com_dma_t uart_coms[] = { {.huart = &huart1} };
static inline uart_com_dma_t *get_uart_com(UART_HandleTypeDef *huart) { for (int i = 0; i < UART_COM_MAX; i++) { if (uart_coms[i].huart == huart) return uart_coms + i; }
return NULL; }
static void uart_rx_check(UART_HandleTypeDef *huart) { uart_com_dma_t *c = get_uart_com(huart); if (c == NULL) return;
size_t pos = sizeof(c->dma_rxbuf) - __HAL_DMA_GET_COUNTER(huart->hdmarx); if (pos > c->old_pos) { lwrb_write(&(c->rb), c->dma_rxbuf + c->old_pos, pos - c->old_pos); c->old_pos = pos; }
else if (pos < c->old_pos) { lwrb_write(&(c->rb), c->dma_rxbuf + c->old_pos, sizeof(c->dma_rxbuf) - c->old_pos); if (pos > 0) lwrb_write(&(c->rb), c->dma_rxbuf, pos); c->old_pos = pos; } }
void uart_com_init(void) { for (int i = 0; i < UART_COM_MAX; i++) { uart_com_dma_t *c = uart_coms + i; c->dma_tx_ready = 1;
lwrb_init(&(c->rb), c->rbbuf, sizeof(c->rbbuf));
__HAL_UART_ENABLE_IT(c->huart, UART_IT_IDLE); __HAL_UART_CLEAR_IDLEFLAG(c->huart);
HAL_UART_Receive_DMA(c->huart, c->dma_rxbuf, sizeof(c->dma_rxbuf)); } }
int uart_write(UART_HandleTypeDef *huart, const void *dat, size_t len, int timeout) { uart_com_dma_t *c = get_uart_com(huart); if (c == NULL || len > sizeof(c->dma_txbuf)) return -1;
uint32_t tick = HAL_GetTick();
while (c->dma_tx_ready == 0 && (HAL_GetTick() - tick) < timeout) ;
if (c->dma_tx_ready) { int min = len > sizeof(c->dma_txbuf) ? sizeof(c->dma_txbuf) : len; memcpy(c->dma_txbuf, dat, min); HAL_UART_Transmit_DMA(c->huart, c->dma_txbuf, min); c->dma_tx_ready = 0; return min; }
return 0; }
int uart_read_from_rb(UART_HandleTypeDef *huart, uint8_t *dat, size_t len) { uart_com_dma_t *c = get_uart_com(huart); if (c == NULL) return -1;
return lwrb_read(&(c->rb), dat, len); }
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { uart_com_dma_t *c = get_uart_com(huart); if (c) c->dma_tx_ready = 1; }
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart) { uart_rx_check(huart); }
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { uart_rx_check(huart); }
void UART_IDLE_Callback(UART_HandleTypeDef *huart) { if (__HAL_UART_GET_FLAG(huart, UART_FLAG_IDLE) != RESET) { uart_rx_check(huart); __HAL_UART_CLEAR_IDLEFLAG(huart); } }
#ifdef UART_BIND_STDIO #include <stdio.h>
int fputc(int ch, FILE *fd) { uint8_t dat = ch; if (fd == stdout) HAL_UART_Transmit(UART_BIND_STDIO, &dat, 1, 50);
return ch; }
int fgetc(FILE *fd) { uint8_t ch; while (fd == stdin && uart_read_from_rb(UART_BIND_STDIO, &ch, 1) <= 0) ;
return ch; }
#ifndef __MICROLIB
#if __ARMCC_VERSION >= 6000000 __asm(".global __use_no_semihosting"); #elif __ARMCC_VERSION >= 5000000 #pragma import(__use_no_semihosting) #else #error "Unsupported compiler" #endif
#include <rt_misc.h> #include <rt_sys.h> #include <time.h>
const char __stdin_name[] = ":tt"; const char __stdout_name[] = ":tt"; const char __stderr_name[] = ":tt";
FILEHANDLE _sys_open(const char *name, int openmode) { return 1; }
int _sys_close(FILEHANDLE fh) { return 0; }
char *_sys_command_string(char *cmd, int len) { return NULL; }
int _sys_write(FILEHANDLE fh, const unsigned char *buf, unsigned len, int mode) { return 0; }
int _sys_read(FILEHANDLE fh, unsigned char *buf, unsigned len, int mode) { return -1; }
void _ttywrch(int ch) { }
int _sys_istty(FILEHANDLE fh) { return 0; }
int _sys_seek(FILEHANDLE fh, long pos) { return -1; }
long _sys_flen(FILEHANDLE fh) { return -1; }
void _sys_exit(int return_code) { while (1) ; }
clock_t clock(void) { clock_t tmp; return tmp; }
void _clock_init(void) { }
time_t time(time_t *timer) { time_t tmp; return tmp; }
int system(const char *string) { return 0; }
char *getenv(const char *name) { return NULL; }
void _getenv_init(void) { } #endif
#endif
|
我们在程序中使能了空闲 (IDLE) 中断,
目的让串口能够在接收到任意字符后立刻做出反应.
因为如果不使用空闲中断, 那么程序将一直等到 DMA 接收半满或全满才响应,
这样的话如果用户只发几个字符, 我们的程序将无法及时处理.
通过将串口绑定到标准输入输出后, 我们可以直接使用 stdio 的 scanf 和
printf 来进行输入输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #include <stdio.h> #include "uart_com.h"
void app_main(void) { uart_com_init();
printf("Hello World\n");
while (1) { int aa; scanf("%d", &aa); printf("Your input is: %d\n", aa); } }
|
裸机情况下, 结合 test_command 也可以实现简单的串口 shell
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
| void test_command_handler(void) { static char input_string[1024]; static int ofs; uint8_t ch;
while (uart_read_from_rb(&huart1, &ch, 1) > 0) { if(ch != '\n') { input_string[ofs++] = ch; if(ofs >= sizeof(input_string) - 1) ofs = 0; continue; }
input_string[ofs] = '\0'; test_command(input_string); ofs = 0; printf("\n#sh "); } }
void app_main(void) { uart_com_init();
printf("\n#sh "); while (1) { test_command_handler();
HAL_Delay(10); } }
|
移植技巧 (Cubemx)
要使用上面的串口驱动, 可在 Cubemx 中选中相应的串口, 比如 USART1
- Mode 选 Asynchronous 异步通信
- DMA Settings 添加 RX 和 TX, 注意 RX 的 DMA Mode 为 Circular,
即循环接收
- NVIC Settings 里将 global interrupt 打勾, 这是给 IDLE 中断用的
- 将 IDLE 中断处理函数放到 USARTx_IRQHandler 中断入口函数中执行,
不要忘了
开 MPU 全局还会影响 DMA 接收问题, 哎调了半天.
其他默认就行了, 但是需要注意的一点是,
在最开始配置串口功能的时候要记得把 DMA 也配置了再输出工程,
因为这里面有一个坑.
如果是使能了串口功能但是不配置 DMA 就输出先输出工程,
到后面才去添加串口 DMA, 输出的初始化函数顺序如下:
1 2
| MX_USART1_UART_Init(); MX_DMA_Init();
|
如果在使能串口功能的同时就把DMA 也配置了, 输出的初始化函数顺序如下:
1 2
| MX_DMA_Init(); MX_USART1_UART_Init();
|
你会发现, 只有第二种情况能工作, 因为 MX_USART1_UART_Init 函数里面依赖
MX_DMA_Init 里面的 DMA handle, 第一种先初始化了串口, 而这时 DMA handle
是不可用的, 因此 DMA 功能失效, 这应该是 Cubemx 的一个 BUG,
我目前使用的版本 V6.6.0 仍有这个 BUG, 不知后面会不会更改.
但是, 如果你真的不小心忘记配置 DMA
就先输出工程也是可以重新设置初始化顺序的, 可在 Project Manager 下的
Advanced Settings 中重新设置顺序.
参考文献
程序中使用到了开源的环形缓冲器的实现 lwrb
另外, 禁用半主机模式的部分参考了 Disable
semihosting with ARM Compiler 5/6
是否使用 MicroLib 的宏 __MICROLIB
参考了 ARM
官方源码里面的 ARM-software/Tool-Solutions
ARM官网提供了更多
ARMCC 的内置宏
串口 MDA 初始化顺序问题参考了 STM32 HAL
UART DMA不通的问题解决及注意事项 以及 Why
does the sequence of init calls matter in STM32CubeIDE?
源文件来自于