大花科技
嵌入式开发原创教程嵌入式入门 04

UART 串口实战:中断接收、环形缓冲和命令解析

从串口自发自收到中断接收、环形缓冲区、按行解析 LED 命令,完成一个可复用的调试通道。

UART 串口实战:中断接收、环形缓冲和命令解析

实验目标

实现一个串口命令:

textcode
LED,ON
LED,OFF
STATUS

每行以回车换行结束。串口接收通过中断完成,主循环负责解析,不在中断中执行耗时业务。

一、配置串口

USART2:

  • 115200;
  • 8 数据位;
  • 无校验;
  • 1 停止位;
  • 开启 USART2 global interrupt。

二、环形缓冲区

ccode
/* USER CODE BEGIN PV */
#define UART_RX_SIZE 256U

static uint8_t  rx_byte;
static uint8_t  rx_buffer[UART_RX_SIZE];
static volatile uint16_t rx_head = 0U;
static volatile uint16_t rx_tail = 0U;

static void uart_start_receive(void)
{
    HAL_UART_Receive_IT(&huart2, &rx_byte, 1U);
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART2)
    {
        uint16_t next = (uint16_t)((rx_head + 1U) % UART_RX_SIZE);

        if (next != rx_tail)
        {
            rx_buffer[rx_head] = rx_byte;
            rx_head = next;
        }

        HAL_UART_Receive_IT(&huart2, &rx_byte, 1U);
    }
}

static int uart_pop(uint8_t *value)
{
    if (rx_tail == rx_head)
    {
        return 0;
    }

    *value = rx_buffer[rx_tail];
    rx_tail = (uint16_t)((rx_tail + 1U) % UART_RX_SIZE);
    return 1;
}
/* USER CODE END PV */

在 main 函数初始化后调用:

ccode
uart_start_receive();

三、按行解析命令

ccode
/* USER CODE BEGIN PV */
static char command[64];
static uint8_t command_length = 0U;

static void handle_command(const char *cmd)
{
    if (strcmp(cmd, "LED,ON") == 0)
    {
        HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
        HAL_UART_Transmit(&huart2, (uint8_t *)"OK\r\n", 4, 100);
    }
    else if (strcmp(cmd, "LED,OFF") == 0)
    {
        HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
        HAL_UART_Transmit(&huart2, (uint8_t *)"OK\r\n", 4, 100);
    }
    else if (strcmp(cmd, "STATUS") == 0)
    {
        HAL_UART_Transmit(&huart2, (uint8_t *)"RUNNING\r\n", 9, 100);
    }
    else
    {
        HAL_UART_Transmit(&huart2, (uint8_t *)"ERR\r\n", 5, 100);
    }
}

static void uart_process(void)
{
    uint8_t ch;

    while (uart_pop(&ch))
    {
        if (ch == '\r')
        {
            continue;
        }

        if (ch == '\n')
        {
            command[command_length] = '\0';
            if (command_length > 0U)
            {
                handle_command(command);
            }
            command_length = 0U;
        }
        else if (command_length < (sizeof(command) - 1U))
        {
            command[command_length++] = (char)ch;
        }
        else
        {
            command_length = 0U;
            HAL_UART_Transmit(&huart2, (uint8_t *)"TOO_LONG\r\n", 10, 100);
        }
    }
}
/* USER CODE END PV */

主循环:

ccode
while (1)
{
    uart_process();
    HAL_Delay(5);
}

四、实验步骤

  1. 下载程序;
  2. 打开串口工具;
  3. 选择“发送新行”;
  4. 输入 LED,ON 并发送;
  5. 输入 LED,OFF 并发送;
  6. 输入 STATUS 并观察回复;
  7. 输入未知命令,确认返回 ERR。

五、排错

串口只收到乱码

检查波特率、时钟和串口工具设置。

只收到第一个字符

检查是否在回调末尾重新调用了 HAL_UART_Receive_IT。

命令始终 ERR

确认发送时带换行,确认命令没有多余空格,确认数组长度足够。

六、进阶

  • 把命令处理改成函数表;
  • 加入校验和;
  • 支持二进制帧;
  • 为缓冲区增加高水位告警。