STM32的USART组件支持异步、同步、单线半双工、多处理器、IrDA、LIN、SmartCard等模式,本文介绍的是异步即UART模式。html
总线通讯有三种模型:轮询、中断和DMA。DMA对我来讲是陌生的内容,之后单独开篇细讲。设计模式
HAL把寄存器组组织成组件,组件包含外设的各个寄存器。在USART这里,寄存器不足以描述外设的全部状态,HAL用handle来包装组件。一个handle包含指向组件的指针、初始化参数、状态、与其余组件的连接(如DMA)和内部状态等。数组
图源ST官方MOOC,打开以前注意调低音量。异步
USART的初始化除了USART自己的寄存器之外,还要设置GPIO的复用功能,这两项任务分别在stm32f4xx_hal_uart.c
中的HAL_UART_Init
和stm32f4xx_hal_msp.c
的HAL_UART_MspInit
中完成(MSP意为“MCU Specific Package”)。stm32f4xx_hal_uart.c
中也定义了HAL_UART_MspInit
,添加了weak
属性(提供实现,容许被覆写)。函数
轮询是与中断相对的。对于发送,轮询是指写一个字节(或一个packet),等待它发送完,再写下一个字节,直到全部数据被发送完才返回;对于接受,轮询是指等待直到接收到必定长度的数据。轮询相对简单,可是效率很低。工具
#include "main.h" #include <string.h> UART_HandleTypeDef huart1; void SystemClock_Config(void); static void MX_GPIO_Init(void); static void MX_USART1_UART_Init(void); void uart_transmit(const char* string); int main(void) { char buffer[2] = {0}; HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); uart_transmit("hello\n"); while (1) { HAL_StatusTypeDef status = HAL_UART_Receive(&huart1, buffer, 1, 1000); if (status == HAL_OK) { uart_transmit("received: "); uart_transmit(buffer); uart_transmit("\n"); } else uart_transmit("timeout\n"); } } void uart_transmit(const char* string) { HAL_UART_Transmit(&huart1, string, strlen(string), 1000); } static void MX_USART1_UART_Init(void) { huart1.Instance = USART1; huart1.Init.BaudRate = 115200; huart1.Init.WordLength = UART_WORDLENGTH_8B; huart1.Init.StopBits = UART_STOPBITS_1; huart1.Init.Parity = UART_PARITY_NONE; huart1.Init.Mode = UART_MODE_TX_RX; huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE; huart1.Init.OverSampling = UART_OVERSAMPLING_16; if (HAL_UART_Init(&huart1) != HAL_OK) { Error_Handler(); } } // ...
HAL的UART接收只能指定数据长度而不能指定终止符。在轮询模式下,能够设置数据长度为1,即每次读取一个字节,判断它是否为终止符。ui
在中断模式下,函数当即返回,数据在中断中发送或接收。在发送或接收完成后,相应的回调函数会被调用。debug
#include "main.h" #include <stdbool.h> UART_HandleTypeDef huart1; volatile bool finished = false; char buffer[3] = {0}; void SystemClock_Config(void); static void MX_GPIO_Init(void); static void MX_USART1_UART_Init(void); void uart_transmit(const char* string); void uart_transmit_it(const char* string); int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); uart_transmit_it("hello\n"); const char* info = finished ? "already finished\n" : "still transmitting\n"; while (!finished) ; finished = false; uart_transmit_it(info); uart_transmit_it(info); while (!finished) ; while (1) { finished = false; HAL_UART_Receive_IT(&huart1, buffer, 2); while (!finished) ; uart_transmit("received: "); uart_transmit(buffer); uart_transmit("\n"); } } void uart_transmit(const char* string) { HAL_UART_Transmit(&huart1, string, strlen(string), 1000); } void uart_transmit_it(const char* string) { HAL_UART_Transmit_IT(&huart1, string, strlen(string)); } void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if (huart == &huart1) { finished = true; } } void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart == &huart1) { finished = true; } } // ...
串口输出still transmitting
,说明HAL_UART_Transmit_IT
确实是发送完成前就返回的;still transmitting
只出现一次,由于第二次调用时第一次的发送还没结束。设计
读了HAL的源码,我发现中断发送的数据是拷贝指针的,也就是浅拷贝的,须要保证发送期间该地址上的数据有效。好比,若是一个函数把局部变量数组做为参数传给HAL_UART_Transmit_IT
,未等待发送完成便返回,那么发送的数据将会是错误的,甚至致使程序行为未定义。指针
若是给单片机发送了多余所需量的数据,程序会崩溃,我没有debug出问题在哪。
这样的接收连差强人意都算不上,个人终极目标是实现scanf
那样的接收函数。中断发送只能缓冲一次和浅拷贝等问题也至关愚蠢,我想顺便把发送也改形成printf
。改造的工具是用循环队列实现的缓冲区,这个我在AVR单片机教程中还煞有其事地写过,正好能够做为如今的练习。
queue.h
:
#ifndef QUEUE_H #define QUEUE_H #include <stdint.h> #include <stdbool.h> #include <stdlib.h> #ifdef __cplusplus extern "C" { #endif typedef struct { uint16_t mask; uint16_t head; uint16_t tail; queue_element_t data[0]; } queue_t; static inline queue_t* queue_create(uint16_t _size) { if (_size & (_size - 1)) _size = 256; queue_t* q = malloc(sizeof(queue_t) + _size * sizeof(queue_element_t)); if (q) { q->mask = _size - 1; q->head = q->tail = 0; } return q; } static inline bool queue_empty(const volatile queue_t* _queue) { return _queue->head == _queue->tail; } static inline bool queue_full(const volatile queue_t* _queue) { return ((_queue->tail + 1) & _queue->mask) == _queue->head; } static inline uint16_t queue_size(const volatile queue_t* _queue) { return (_queue->tail - _queue->head) & _queue->mask; } static inline uint16_t queue_capacity(const volatile queue_t* _queue) { return _queue->mask; } static inline queue_element_t queue_peek(const volatile queue_t* _queue) { return _queue->data[_queue->head]; } static inline void queue_push(volatile queue_t* _queue, const queue_element_t _ele) { _queue->data[_queue->tail] = _ele; _queue->tail = (_queue->tail + 1) & _queue->mask; } static inline void queue_pop(volatile queue_t* _queue) { _queue->head = (_queue->head + 1) & _queue->mask; } #ifdef __cplusplus } #endif #endif
写inline
遇到了点问题,原来C和C++中的inline
是不同的!改为static inline
就行了。有空再去深究这个问题。
main.c
:
#include "main.h" #include <string.h> #include "cmsis_gcc.h" typedef char queue_element_t; #include "queue.h" UART_HandleTypeDef huart1; queue_t* tx_buffer; queue_t* rx_buffer; void SystemClock_Config(void); static void MX_GPIO_Init(void); static void MX_USART1_UART_Init(void); static void usart1_init_0(); static void usart1_init_2(); static void usart1_transmit(const char* string); static void usart1_receive(char* dest, char delim); int main(void) { char buffer[80]; HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); usart1_transmit("hello\n"); while (1) { usart1_receive(buffer, '\n'); usart1_transmit("received: "); usart1_transmit(buffer); usart1_transmit("\n"); } } void usart1_init_0() { tx_buffer = queue_create(1024); rx_buffer = queue_create(1024); } void usart1_init_2() { USART1->CR1 |= USART_CR1_RXNEIE & UART_IT_MASK; } void usart1_transmit(const char* string) { uint16_t capacity = queue_capacity(tx_buffer); uint16_t size = strlen(string); bool ok = false; while (1) { __disable_irq(); ok = capacity - queue_size(tx_buffer) >= size; if (ok) break; __enable_irq(); __NOP(); } for (uint16_t i = 0; i != size; ++i) queue_push(tx_buffer, string[i]); USART1->CR1 |= USART_CR1_TXEIE & UART_IT_MASK; __enable_irq(); } void usart1_receive(char* dest, char delim) { while (1) { bool ok = false; while (1) { __disable_irq(); ok = !queue_empty(rx_buffer); if (ok) break; __enable_irq(); __NOP(); } char c = queue_peek(rx_buffer); queue_pop(rx_buffer); __enable_irq(); if (c == delim) break; *dest++ = c; } *dest = '\0'; } void usart1_transmit_handler() { USART1->DR = queue_peek(tx_buffer); queue_pop(tx_buffer); if (queue_empty(tx_buffer)) USART1->CR1 &= ~USART_CR1_TXEIE & UART_IT_MASK; } void usart1_receive_handler() { queue_push(rx_buffer, USART1->DR); } void USART1_IRQHandler(void) { uint32_t isrflags = USART1->SR; uint32_t cr1its = USART1->CR1; uint32_t errorflags = 0x00U; errorflags = (isrflags & (uint32_t)(USART_SR_PE | USART_SR_FE | USART_SR_ORE | USART_SR_NE)); if (errorflags == RESET) { if (((isrflags & USART_SR_RXNE) != RESET) && ((cr1its & USART_CR1_RXNEIE) != RESET)) { usart1_receive_handler(); return; } if (((isrflags & USART_SR_TXE) != RESET) && ((cr1its & USART_CR1_TXEIE) != RESET)) { usart1_transmit_handler(); return; } } HAL_UART_IRQHandler(&huart1); } static void MX_USART1_UART_Init(void) { usart1_init_0(); huart1.Instance = USART1; huart1.Init.BaudRate = 115200; huart1.Init.WordLength = UART_WORDLENGTH_8B; huart1.Init.StopBits = UART_STOPBITS_1; huart1.Init.Parity = UART_PARITY_NONE; huart1.Init.Mode = UART_MODE_TX_RX; huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE; huart1.Init.OverSampling = UART_OVERSAMPLING_16; if (HAL_UART_Init(&huart1) != HAL_OK) { Error_Handler(); } usart1_init_2(); }
中断的调用流程是:USART1中断请求调用USART1_IRQHandler
(这个名字在startup_stm32f407vetx.s
中定义),由STM32CubeMX生成的USART1_IRQHandler
调用HAL_UART_IRQHandler
,里面进行各类判断和处理,在合适的时机调用HAL_UART_TxCpltCallback
等。我在USART1_IRQHandler
中插入了一些代码,把TXE
和RXNE
两种中断拦截了下来,其他仍是丢给HAL_UART_IRQHandler
处理(Chain of Responsibility设计模式?)。
queue
上的操做不是原子的,主函数与中断共享须要加锁。__disable_irq
关闭全局中断,__enable_irq
开启全局中断。ARM说在开中断以后Cortex-M3/4还可能执行2条指令才响应中断,而在汇编代码中cpsie
后第二句就是cpsid
,因此我在__enable_irq
后加一句__NOP
空指令,以保证中断请求能被响应。
离printf
和scanf
只有一步之遥了,但我想把它放到下一篇。20pin的ST-LINK/V2已经在路上了。