STM32F4 UART HAL 驱动程序
Posted
技术标签:
【中文标题】STM32F4 UART HAL 驱动程序【英文标题】:STM32F4 UART HAL Driver 【发布时间】:2014-09-12 14:16:00 【问题描述】:我正在尝试弄清楚如何使用这个新的 HAL 驱动程序。我想使用HAL_UART_Receive_IT()
接收数据,它将设备设置为在接收到数据时运行中断功能。
问题是您必须在中断触发之前指定要读取的数据长度。我计划发送控制台之类的不同长度的命令,因此不能有固定的长度。我认为这样做的唯一方法是一次读取单个字符并建立一个单独的字符串。
HAL驱动好像有问题,如果你设置HAL_UART_Receive_IT()
接收x
个字符,然后尝试发送超过x
个字符,就会报错。
目前我不知道我的方法是否正确,有什么想法吗?
【问题讨论】:
【参考方案1】:在数据寄存器 (DR) 已满时接收数据将导致溢出错误。问题是函数UART_Receive_IT(UART_HandleTypeDef*)
将在收到足够的数据后停止读取 DR 寄存器。任何新数据都会导致溢出错误。
我所做的是宁愿使用循环 DMA 接收结构。然后,您可以使用currentPosInBuffer - uart->hdmarx->Instance->NDTR
来确定收到了多少尚未处理的数据。
这有点复杂,因为虽然 DMA 自己执行循环缓冲,但如果超过缓冲区的末尾,则必须手动实现回环到开头。
我还发现了一个故障,控制器说它已传输数据(即NDTR
已减少)但数据尚未在缓冲区中。这可能是一些 DMA/总线访问争用问题,但很烦人。
【讨论】:
故障可能是由于处理器中的数据缓存造成的。应该在 MPU 中将缓冲区设置为非缓存,或者在读取缓冲区之前使用缓存刷新指令。【参考方案2】:STM32 UART 驱动程序有点不稳定。他们开箱即用的唯一方法是如果您知道您将收到的确切字符数。如果您想接收未指定数量的字符,我遇到并尝试了几种解决方案:
将要接收的字符数量设置为 1 并构建一个单独的字符串。这可行,但在接收数据非常快时会出现问题,因为每次驱动程序读取 rxBuffer 时都会禁用中断,因此可能会丢失一些字符。
将要接收的字符数量设置为最大可能的消息大小并实施超时,然后读取整个消息。
编写您自己的 UART_Receive_IT 函数,该函数直接写入循环缓冲区。这是更多的工作,但这是我发现最终效果最好的。不过,您确实必须更改一些 hal 驱动程序,因此代码的可移植性较差。
另一种方法是使用@Flip 建议的 DMA。
【讨论】:
另一个想法:当你第一次收到1个字节时使用一个“协议”,包含下一个数据量的大小。Wait for 1 byte -> receive value "5", Wait for 5 bytes -> receive the 5 bytes, Wait for 1 byte -> receive value "28", Wait for 28 bytes -> receive the 28 bytes, ..., Wait for 1 byte -> receive value "0", END
@ofaurax 是的,但这仅在您可以控制通信的两端时才有效。【参考方案3】:
我决定使用 DMA 来让接收正常工作。我正在使用一个 1 字节的循环缓冲区来处理在发送器的串行终端上键入的数据。这是我的最终代码(只有接收部分,底部有更多关于传输的信息)。
一些定义和变量:
#define BAUDRATE 9600
#define TXPIN GPIO_PIN_6
#define RXPIN GPIO_PIN_7
#define DATAPORT GPIOB
#define UART_PRIORITY 6
#define UART_RX_SUBPRIORITY 0
#define MAXCLISTRING 100 // Biggest string the user will type
uint8_t rxBuffer = '\000'; // where we store that one character that just came in
uint8_t rxString[MAXCLISTRING]; // where we build our string from characters coming in
int rxindex = 0; // index for going though rxString
设置 IO:
__GPIOB_CLK_ENABLE();
__USART1_CLK_ENABLE();
__DMA2_CLK_ENABLE();
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.Pin = TXPIN | RXPIN;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_LOW;
GPIO_InitStruct.Alternate = GPIO_AF7_USART1;
HAL_GPIO_Init(DATAPORT, &GPIO_InitStruct);
设置 UART:
UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_rx;
huart1.Instance = USART1;
huart1.Init.BaudRate = BAUDRATE;
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;
HAL_UART_Init(&huart1);
设置 DMA:
extern DMA_HandleTypeDef hdma_usart1_rx; // assuming this is in a different file
hdma_usart1_rx.Instance = DMA2_Stream2;
hdma_usart1_rx.Init.Channel = DMA_CHANNEL_4;
hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_usart1_rx.Init.MemInc = DMA_MINC_DISABLE;
hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_usart1_rx.Init.Mode = DMA_CIRCULAR;
hdma_usart1_rx.Init.Priority = DMA_PRIORITY_LOW;
hdma_usart1_rx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
HAL_DMA_Init(&hdma_usart1_rx);
__HAL_LINKDMA(huart, hdmarx, hdma_usart1_rx);
HAL_NVIC_SetPriority(DMA2_Stream2_IRQn, UART_PRIORITY, UART_RX_SUBPRIORITY);
HAL_NVIC_EnableIRQ(DMA2_Stream2_IRQn);
设置 DMA 中断:
extern DMA_HandleTypeDef hdma_usart1_rx;
void DMA2_Stream2_IRQHandler(void)
HAL_NVIC_ClearPendingIRQ(DMA2_Stream2_IRQn);
HAL_DMA_IRQHandler(&hdma_usart1_rx);
启动 DMA:
__HAL_UART_FLUSH_DRREGISTER(&huart1);
HAL_UART_Receive_DMA(&huart1, &rxBuffer, 1);
DMA 接收回调:
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
__HAL_UART_FLUSH_DRREGISTER(&huart1); // Clear the buffer to prevent overrun
int i = 0;
print(&rxBuffer); // Echo the character that caused this callback so the user can see what they are typing
if (rxBuffer == 8 || rxBuffer == 127) // If Backspace or del
print(" \b"); // "\b space \b" clears the terminal character. Remember we just echoced a \b so don't need another one here, just space and \b
rxindex--;
if (rxindex < 0) rxindex = 0;
else if (rxBuffer == '\n' || rxBuffer == '\r') // If Enter
executeSerialCommand(rxString);
rxString[rxindex] = 0;
rxindex = 0;
for (i = 0; i < MAXCLISTRING; i++) rxString[i] = 0; // Clear the string buffer
else
rxString[rxindex] = rxBuffer; // Add that character to the string
rxindex++;
if (rxindex > MAXCLISTRING) // User typing too much, we can't have commands that big
rxindex = 0;
for (i = 0; i < MAXCLISTRING; i++) rxString[i] = 0; // Clear the string buffer
print("\r\nConsole> ");
这就是接收字符和构建显示用户输入内容的字符串(字符数组)的所有代码。如果用户点击退格或删除,数组中的最后一个字符将被覆盖,如果他们点击回车,该数组将被发送到另一个函数并作为命令处理。
要查看命令解析和传输代码的工作原理,请参阅我的项目Here
感谢@Flip 和@Dormen 的建议!
【讨论】:
【参考方案4】:我不得不在我的项目中面临同样的问题。
我所做的是在外围初始化后立即使用HAL_USART_Receive_IT()
开始读取 1 个字节。
然后我在传输完成时编写了一个回调,它将字节放入缓冲区,如果命令完成则设置一个标志,然后再次调用 HAL_USART_Receive_IT()
以获取另一个字节。
这对我来说似乎很好用,因为我通过 USART 接收命令,USART 的第一个字节告诉我命令还要长多少字节。 也许它也可以为你工作!
【讨论】:
这是我的第一个方法,在低传输率的情况下效果很好。为每个字符初始化 USART 驱动程序会浪费大量时间。为了更快的速度和更少的机器负载,中断驱动的环形缓冲区(循环缓冲区)解决方案工作得很好。 现在我也使用 LL 驱动程序(在项目中与 HAL 结合使用)来处理 UART 中断数据接收。【参考方案5】:有不同的方法修补,例如文件“stm32l0xx_it.c”(或根据需要的 l4xx)中的“void USART2_IRQHandler(void)”。每次接收到一个字符时,都会调用此中断。使用 CubeMX 代码生成器进行更新时,有插入用户代码的空间。补丁:
void USART2_IRQHandler(void)
/* USER CODE BEGIN USART2_IRQn 0 */
/* USER CODE END USART2_IRQn 0 */
HAL_UART_IRQHandler(&huart2);
/* USER CODE BEGIN USART2_IRQn 1 */
usart_irqHandler_callback( &huart2 ); // patch: call to my function
/* USER CODE END USART2_IRQn 1 */
我提供一个小字符缓冲区并启动接收 IT 功能。高达 115200 波特时,它从未消耗超过 1 个字节,使缓冲区的其余部分未使用。
st = HAL_UART_Receive_IT( &huart2, (uint8_t*)rx2BufIT, RX_BUF_IT_SIZE );
当接收到一个字节时,我捕获它并将其放入我自己的环形缓冲区并设置字符指针和计数器:
// placed in my own source-code module:
void usart_irqHandler_callback( UART_HandleTypeDef* huart )
HAL_UART_StateTypeDef st;
uint8_t c;
if(huart->Instance==USART2)
if( huart->RxXferCount >= RX_BUF_IT_SIZE )
rx2rb.err = 2; // error: IT buffer overflow
else
huart->pRxBuffPtr--; // point back to just received char
c = (uint8_t) *huart->pRxBuffPtr; // newly received char
ringbuf_in( &rx2rb, c ); // put c in rx ring-buffer
huart2.RxXferCount++; // increment xfer-counter avoids end of rx
这种方法被证明是相当快的。使用 IT 或 DMA 仅接收一个字节总是会取消初始化并且需要再次初始化接收过程,结果证明太慢了。上面的代码只是一个框架;我曾经在状态结构中计算换行符,这使我可以随时从环形缓冲区中读取已完成的行。还应检查是否接收到的字符或其他事件导致中断。 编辑: 事实证明,这种方法适用于 DMA 不支持的 USARTS 并改用 IT。 将 CubeMX 生成器与 HAL 库一起使用时,在循环模式下使用 1 字节的 DMA 更短且更易于实现。
EDIT2: 由于最近的 HAL 库的变化,这不能逐行工作。该原理仍然可以快速有效地工作,但必须适应这些“方言”。对不起,但它是无地板桶一直在改变它。
【讨论】:
【参考方案6】:通常我编写自己的 UART 循环缓冲区实现。如前所述,STM32 HAL 库的 UART 中断函数有点奇怪。 您可以使用 UART 中断标志编写自己的循环缓冲区,只需 2 个数组和指针。
【讨论】:
以上是关于STM32F4 UART HAL 驱动程序的主要内容,如果未能解决你的问题,请参考以下文章
带有 HAL 库的 STM32F4-Discovery (STM32F429ZIT6) 上的 RS232 (UART)?