STM32F4 UART HAL驱动程序

20

我正在尝试学习如何使用这个新的HAL驱动程序。我想要使用HAL_UART_Receive_IT()来接收数据,该函数设置了当接收到数据时运行中断函数。

问题是在触发中断之前必须指定要读取的数据长度。我打算发送不同长度的类似控制台命令,因此不能有固定长度。我假设唯一的方法就是一个接一个字符地读取并构建一个单独的字符串。

HAL驱动程序似乎存在一个问题,如果您将HAL_UART_Receive_IT()设置为接收x个字符,然后尝试发送超过x个字符,将会出现错误。

目前我不知道我是否正确进行,有什么想法吗?

6个回答

18

我决定使用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> ");
        }
    }
}

这段代码基本上是接收字符并构建一个字符串(字符数组),以显示用户输入的内容。如果用户按下退格或删除键,则覆盖数组中的最后一个字符;如果他们按下回车键,则将该数组发送到另一个函数并作为命令处理。
要查看命令解析和传输代码的工作原理,请参见我的项目这里
感谢@Flip和@Dormen提出的建议!

8
接收数据时,如果数据寄存器(DR)已满,则会导致溢出错误。问题在于函数UART_Receive_IT(UART_HandleTypeDef*)一旦接收到足够的数据,就会停止读取DR寄存器。任何新数据都会导致溢出错误。
我的做法是使用循环DMA接收结构。然后可以使用currentPosInBuffer - uart->hdmarx->Instance->NDTR来确定你尚未处理的接收到的数据量。
这有点复杂,因为虽然DMA自己执行循环缓冲,但如果超过缓冲区末尾,你必须手动实现回路到开头。
我还发现一个小故障,控制器表示已传输数据(即NDTR已减少),但数据尚未在缓冲区中。可能是一些DMA /总线访问争用问题,但很烦人。

故障可能是由于处理器中数据缓存而引起的。应在MPU中将缓冲区设置为非缓存,或在读取缓冲区之前使用缓存刷新指令。 - Flip

3
STM32 UART驱动程序有些不稳定。它们只有在您知道要接收的确切字符数时才能开箱即用。如果您想接收未指定数量的字符,则有几个解决方案可以尝试:
  1. 将要接收的字符数设置为1并构建一个单独的字符串。这种方法有效,但当快速接收数据时会出现问题,因为每次驱动程序读取rxBuffer时都会禁用中断,因此可能会丢失一些字符。
  2. 将要接收的字符数设置为最大可能的消息大小,并实现超时,在超时后读取整个消息。
  3. 编写自己的UART_Receive_IT函数,直接写入循环缓冲区。这需要更多的工作,但最终我发现这是最好的方法。但您需要更改一些hal驱动程序,因此代码不太可移植。
另一种方法是像@Flip建议的那样使用DMA。

另一个想法:在首次接收1个字节时使用“协议”,其中包含下一批数据的大小。等待1个字节->接收值“5”,等待5个字节->接收这5个字节, 等待1个字节->接收值“28”,等待28个字节->接收这28个字节,...,等待1个字节->接收值“0”,结束 - ofaurax
1
@ofaurax 是的,但这仅在您控制通信的两端时才有效。 - Domen Kern

1
我在我的项目中也遇到了同样的问题。 我所做的是,在外设初始化后,使用HAL_USART_Receive_IT()开始读取1个字节。
然后,我编写了一个传输完成回调函数,将字节放入缓冲区,如果命令完成,则设置一个标志,然后再次调用HAL_USART_Receive_IT()以获取另一个字节。
对我来说,这似乎很好用,因为我通过USART接收命令,其第一个字节告诉我命令还需要多少个字节。 也许这对你也有用!

这是我的第一种方法,对于低传输速率效果很好。但每次初始化USART驱动程序只为一个字符会有巨大的时间损失。对于更快的速率和更少的机器负载,中断驱动的环形缓冲区解决方案效果很好。 - peets
现在我也使用LL驱动程序(与项目中的HAL结合使用),来处理UART中断数据接收。 - LoriB

0

在编程方面,有一个不同的方法来修补例如“void USART2_IRQHandler(void)”在文件“stm32l0xx_it.c”(或根据需要使用l4xx)。每次接收到字符时,都会调用此中断。可以插入用户代码的空间,在使用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 并使用 IT 的 USARTS。 使用循环模式下的 1 字节 DMA 在使用 CubeMX 生成器和 HAL 库时更短且更易实现。

编辑2:
由于最近 HAL 库的更改,这种方法不能逐行工作。原则仍然快速有效,但必须适应这些“方言”。抱歉,但一直更改它是无底洞。


0
通常我会编写自己的UART循环缓冲区实现。如前所述,STM32 HAL库的UART中断函数有点奇怪。 你可以使用UART中断标志仅使用2个数组和指针编写自己的循环缓冲区。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接