STM32从零到量产开发:四路继电器工业控制模块开发-RS232 调试通信驱动模块(bsp_res232)设计说明

1. 模块概述

bsp_rs232 模块是基于 STM32 HAL 库封装的 RS232 串口通信驱动,底层使用 UART4 外设(TX: PC10, RX: PC11)。该模块主要用于与上位机或串口助手进行通信,支持调试输出、参数查询及固件升级等功能。

硬件注意:该接口为 TTL 电平,外部必须连接 RS232 电平转换芯片(如 MAX3232)。

核心特性

  • 发送机制:采用阻塞式发送(HAL_UART_Transmit),无需流控或方向控制。
  • 接收机制:采用中断驱动的单字节接收,结合环形缓冲区(Ring Buffer)实现非阻塞读取。
  • 协议解析:内置行缓冲读取功能,自动兼容 Windows (\r\n)、Linux (\n) 及老设备 (\r) 的换行格式,支持命令行交互。

2. 关键数据结构与宏定义

2.1 缓冲区配置宏

#define RS232_RX_BUF_SIZE     256u  // 接收环形缓冲区存储上限
#define RS232_TX_BUF_SIZE     256u  // 预留发送缓冲区宏(当前阻塞发送暂未启用)

2.2 静态环形缓冲区结构

接收端采用经典的环形缓冲区设计,通过头尾指针解耦中断写入与主循环读取:

static uint8_t  s_rx_buf[RS232_RX_BUF_SIZE];    // 接收缓存数组
static volatile uint16_t s_rx_head = 0u;        // 写指针(中断上下文写入)
static volatile uint16_t s_rx_tail = 0u;        // 读指针(主循环上下文读取)
static uint8_t  s_rx_byte = 0u;                 // 中断单字节临时接收变量

3. API 接口说明

3.1 初始化与中断回调

接口函数 说明
void BSP_RS232_Init(void) 初始化模块,清空环形缓存读写指针,并启动 UART4 单字节中断接收。必须在硬件初始化后调用。
void BSP_RS232_OnRxByte(void) 单字节接收完成回调。将数据写入环形缓存,若缓存满则丢弃当前字节,并重新启动下一次接收。仅限中断文件调用
void BSP_RS232_OnError(UART_HandleTypeDef *huart) 串口错误回调(溢出/帧/奇偶/噪声错误)。触发时清空脏缓存并重启接收。仅限中断文件调用

3.2 数据发送接口

接口函数 说明
uint8_t BSP_RS232_Send(const uint8_t *data, uint16_t len) 二进制阻塞发送。超时时间按 len * 20ms + 50ms 估算。返回 0 成功,非 0 失败。
void BSP_RS232_SendString(const char *str) 字符串便捷发送。内部自动计算长度并调用底层发送接口,包含空指针保护。

3.3 数据接收接口

接口函数 说明
uint16_t BSP_RS232_Available(void) 查询接收缓冲区中可读的字节数。内部使用关中断保护计算,兼容缓存绕圈场景。
int16_t BSP_RS232_ReadByte(void) 非阻塞读取单字节。读到数据返回 ASCII 值,缓冲区为空返回 -1。内部使用关中断保护。
uint8_t BSP_RS232_ReadLine(uint8_t *buf, uint16_t bufsize, uint16_t *len) 尝试读取一行数据。返回 1 表示读到完整行,返回 0 表示仅有半包数据。自动过滤空回车,并在末尾追加 \0

4. 核心机制与注意事项

4.1 中断链路

数据接收遵循以下调用链路:
UART4_IRQHandlerHAL_UART_IRQHandlerHAL_UART_RxCpltCallbackBSP_RS232_OnRxByte

4.2 并发安全保护

在主循环读取数据(ReadByteAvailable)时,代码使用了 __disable_irq()__enable_irq() 进行临界区保护。这是为了防止在读取或更新 head/tail 指针时被 UART 接收中断打断,从而避免数据错乱或指针越界。

4.3 错误恢复机制

当发生 ORE(溢出错误)、FE(帧错误)等硬件异常时,BSP_RS232_OnError 会被触发。该函数会强制复位环形缓冲区的读写指针,并重新调用 HAL_UART_Receive_IT 启动接收,确保串口在异常后能够自动恢复工作。

4.4 使用限制

  • BSP_RS232_OnRxByteBSP_RS232_OnError 属于底层中断回调,业务代码严禁直接调用,否则会破坏环形缓存的指针状态。
  • 当前发送为阻塞模式,若在发送过程中有高频中断或长耗时任务,可能会导致发送超时,需根据实际波特率合理评估超时时间。
/**
  ******************************************************************************
  * @file    bsp_rs232.h
  * @brief   RS232 调试通信驱动 - UART4 (PC10_TX / PC11_RX)
  *
  * 用途: 与上位机/电脑串口助手通信, 用于调试输出、参数查询、固件升级等。
  *       注意: 此接口为 TTL 电平, 外部需接 RS232 电平转换芯片 (如 MAX3232)。
  *
  * 功能:
  *   - 阻塞发送 (printf 重定向基础)
  *   - 中断接收 (环形缓冲区)
  *   - 行缓冲读取 (支持 \r\n 结尾的命令行交互)
  *
  * 宏说明:
  *   RS232_RX_BUF_SIZE:接收环形缓冲区存储上限
  *   RS232_TX_BUF_SIZE:预留发送缓冲区宏,当前底层发送为阻塞模式暂未启用,后续扩展TX FIFO使用
  ******************************************************************************
  */
#ifndef __BSP_RS232_H
#define __BSP_RS232_H
#ifdef __cplusplus
extern "C" {
#endif
#include "main.h"
#include "usart.h"

/* RS232 接收环形缓冲区大小 */
#define RS232_RX_BUF_SIZE     256u
/* RS232 发送缓冲区大小,预留宏,当前阻塞发送逻辑未使用 */
#define RS232_TX_BUF_SIZE     256u

/**
  * @brief  初始化 RS232 (UART4), 启动中断接收
  */
void BSP_RS232_Init(void);

/**
  * @brief  发送数据 (阻塞式, 无方向控制需求)
  * @param  data: 数据指针
  * @param  len:  长度
  * @retval 0=成功, 非0=失败
  */
uint8_t BSP_RS232_Send(const uint8_t *data, uint16_t len);

/**
  * @brief  发送字符串 (阻塞式, 自动计算长度)
  * @param  str: 以 '\0' 结尾的字符串
  */
void BSP_RS232_SendString(const char *str);

/**
  * @brief  查询接收缓冲区可读字节数
  * @note  搭配 ReadByte / ReadLine 使用,优先判断缓存有数据再读取,减少无效调用
  */
uint16_t BSP_RS232_Available(void);

/**
  * @brief  读取一个字节 (非阻塞, 无数据返回 -1)
  * @retval >=0 读到字节ASCII值,-1 缓冲区为空
  */
int16_t BSP_RS232_ReadByte(void);

/**
  * @brief  尝试读取一行 (以 \r 或 \n 结尾)
  * @param  buf:   输出缓冲区
  * @param  bufsize: 缓冲区大小,内部自动预留1字节存放字符串结束符'\0'
  * @param  len:   输出实际读取长度 (不含结尾换行符)
  * @retval 1=读到完整行, 0=无完整行,仅半包数据
  * @note  自动兼容 Windows(\r\n) / Linux(\n) / 老设备(\r) 三种换行格式,自动过滤空回车
  */
uint8_t BSP_RS232_ReadLine(uint8_t *buf, uint16_t bufsize, uint16_t *len);

/**
  * @brief  UART 接收完成单字节回调 (由 HAL_UART_RxCpltCallback 调用)
  * @note   仅 stm32f1xx_it.c 中断文件调用,业务代码禁止直接调用本函数,会破坏环形缓存指针
  */
void BSP_RS232_OnRxByte(void);

/**
  * @brief  UART 错误回调 (溢出/帧/奇偶/噪声错误恢复)
  * @param  huart: 当前报错串口句柄
  * @note   仅 stm32f1xx_it.c 错误中断分支调用,业务代码禁止直接调用
  */
void BSP_RS232_OnError(UART_HandleTypeDef *huart);

#ifdef __cplusplus
}
#endif
#endif /* __BSP_RS232_H */

/**
  ******************************************************************************
  * @file    bsp_rs232.c
  * @brief   RS232 调试通信驱动实现 (基于 UART4)
  * @details 接收: 环形缓冲区 + 中断逐字节无阻塞接收
  *          发送: 阻塞式 HAL_UART_Transmit (RS232 全双工, 无方向控制)
  *          中断链路:
  *            UART4_IRQHandler(芯片底层中断入口)
  *         -> HAL_UART_IRQHandler(HAL 库统一中断处理函数)
  *         -> HAL_UART_RxCpltCallback(HAL 库弱回调函数)
  *         -> BSP_RS232_OnRxByte(业务接收函数)
  *         链路错误恢复:UART4_IRQHandler -> HAL_UART_IRQHandler
  *         -> HAL_UART_ErrorCallback -> BSP_RS232_OnError
  ******************************************************************************
  */
#include "bsp_rs232.h"
#include <string.h>

/* ===================== 模块静态环形缓存与单字节变量 ===================== */
/* 接收环形缓冲区 */
static uint8_t  s_rx_buf[RS232_RX_BUF_SIZE];    /* 接收环形缓存数组 */
static volatile uint16_t s_rx_head = 0u;        /* 写指针(中断写入) */
static volatile uint16_t s_rx_tail = 0u;        /* 读指针(主循环读取) */
static uint8_t  s_rx_byte = 0u;                 /* 中断单字节临时缓存 */

/* ===================== 对外公共接口(按数据流:初始化 → 中断回调 → 读取 → 发送) ===================== */
/**
  * @brief  RS232模块初始化,重置缓存并开启单字节接收中断
  * @note   【关联指引】必须在 UART4 硬件初始化 MX_UART4_Init 之后调用;
  *         环形缓存读写指针清零,HAL_UART_Receive_IT 启动中断逐字节接收
  */
void BSP_RS232_Init(void)
{
    /* 清空环形缓存读写指针 */
    s_rx_head = 0u;
    s_rx_tail = 0u;
    /* 启动UART4单字节中断接收 */
    /* HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size) */
    HAL_UART_Receive_IT(&huart4, &s_rx_byte, 1u);
}

/**
  * @brief  UART4串口错误回调
  * @param  huart: 报错串口句柄
  * @note   出现溢出/帧/奇偶/噪声错误时清空脏缓存,重启接收;
  *         仅在 stm32f1xx_it.c 错误中断分支调用,业务层禁止直接调用
  */
void BSP_RS232_OnError(UART_HandleTypeDef *huart)
{
    s_rx_head = 0u;
    s_rx_tail = 0u;
    HAL_UART_Receive_IT(huart, &s_rx_byte, 1u);
}

/**
  * @brief  单字节接收中断回调,写入环形缓存
  * @note   缓存满则丢弃当前字节,持续开启下一次接收;
  *         仅 stm32f1xx_it.c 的 HAL_UART_RxCpltCallback 调用
  */
void BSP_RS232_OnRxByte(void)
{
    uint16_t next = (s_rx_head + 1u) % RS232_RX_BUF_SIZE;
    if (next != s_rx_tail)
    {
        s_rx_buf[s_rx_head] = s_rx_byte;
        s_rx_head = next;
    }
    HAL_UART_Receive_IT(&huart4, &s_rx_byte, 1u);
}

/**
  * @brief  非阻塞读取单个字节
  * @retval >=0读到字节,-1无数据
  * @note   关中断保护读写指针,读取后立即恢复中断,避免数据错乱
  */
int16_t BSP_RS232_ReadByte(void)
{
    __disable_irq();    /* 关闭全部可屏蔽中断,防止读取中途被接收中断改写 head/tail */
    int16_t ret = -1;
    if (s_rx_tail != s_rx_head)
    {
        ret = (int16_t)s_rx_buf[s_rx_tail];
        s_rx_tail = (s_rx_tail + 1u) % RS232_RX_BUF_SIZE;
    }
    __enable_irq();     /* 打开全部可屏蔽中断,恢复串口接收 */
    return ret;
}

/**
  * @brief  读取一整行(\r/\n结尾)
  * @param  buf:     输出缓存
  * @param  bufsize: 缓存最大长度
  * @param  len:     输出有效字符长度
  * @retval 1读到完整行,0仅半包数据
  * @note   自动兼容CRLF、单独\r、单独\n,过滤空回车;
  *         1. Windows:\r 紧接着\n(两个字符代表一行结束)
  *         2. Linux/Mac:只有\n
  *         3. 部分老设备:只有\r
  */
uint8_t BSP_RS232_ReadLine(uint8_t *buf, uint16_t bufsize, uint16_t *len)
{
    uint16_t i = 0u;    /* i = 已经存了多少个字符到buf */
    int16_t ch;         /* 存每次读到的1个字节,-1代表没数据 */
    *len = 0u;          /* 先把输出长度清零 */

    while ((ch = BSP_RS232_ReadByte()) >= 0)
    {
        /* 判断:读到回车\r 或者 换行\n,代表一行结束 */
        if (ch == '\r' || ch == '\n')
        {
            /* 如果是 Windows 格式 \r\n,多读后面的 \n 扔掉 */
            if (ch == '\r')
            {
                int16_t nxt = BSP_RS232_ReadByte();    /* nxt 为读取字符的ASCII数值 */
                if (nxt == '\n')
                {
                    /* 读到配对\n,直接丢弃,不存进字符串 */
                }
            }
            /* 过滤空行:只敲回车、没有任何文字,跳过不返回 */
            if (i == 0u)
            {
                continue;
            }
            buf[i] = '\0';    /* 字符串末尾加结束符,方便打印 */
            *len = i;         /* 把有效字符数量传给上层 */
            return 1u;        /* 返回1:成功读到完整一行 */
        }
        /* 不是换行符,是普通文字,存进buf */
        /* bufsize-1 留1个位置给最后的'\0',防止溢出 */
        if (i < bufsize - 1u)
        {
            buf[i++] = (uint8_t)ch;
        }
        /* 超出缓存长度的字符直接丢掉,避免数组越界 */
    }
    /* 循环走完,缓冲区读完了也没碰到换行,只有半段不完整文字 */
    *len = i;
    return 0u;        /* 返回0:没有完整一行,只有半包数据 */
}

/**
  * @brief  查询缓存有效未读字节数量
  * @retval 当前可读取字节数
  * @note   关中断保护计算,避免 head/tail 中途被改写;兼容缓存绕圈场景
  */
uint16_t BSP_RS232_Available(void)
{
    __disable_irq();    /* 关闭全部可屏蔽中断,防止计算中途串口中断改写head/tail,数据错乱 */
    uint16_t count;
    /* 写指针head在读指针tail后面,缓存没绕圈 */
    if (s_rx_head >= s_rx_tail)
    {
        count = s_rx_head - s_rx_tail;
    }
    /* head绕到缓存开头,超过tail,数据分段存在数组头尾 */
    else
    {
        count = RS232_RX_BUF_SIZE - s_rx_tail + s_rx_head;
    }
    __enable_irq();     /* 计算完成,重新打开中断,恢复串口接收 */
    return count;       /* 返回剩余未读字节总数 */
}

/**
  * @brief  二进制阻塞发送底层接口
  * @param  data: 待发送数据指针
  * @param  len:  数据长度
  * @retval 0成功 / 1失败(空指针/超时)
  * @note   超时按每字节 20ms 估算 +50ms 兜底;RS232 全双工无需方向控制
  */
uint8_t BSP_RS232_Send(const uint8_t *data, uint16_t len)
{
    if (len == 0u || data == NULL)
    {
        return 1u;
    }
    uint32_t timeout = (uint32_t)len * 20u + 50u;  /* 每个字节20ms, +50总时长兜底 */
    /* HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size, uint32_t Timeout) */
    HAL_StatusTypeDef st = HAL_UART_Transmit(&huart4, data, len, timeout);
    return (st == HAL_OK) ? 0u : 1u;
}

/**
  * @brief  便捷字符串发送封装,打印输出工具,简化调用,无需返回值
  * @param  str: 以'\0'结尾字符串
  * @note   空指针内部保护;将 char* 字符串强转为底层无符号字节数组接口
  */
void BSP_RS232_SendString(const char *str)
{
    if (str == NULL)
    {
        return;
    }
    /* 将char*字符串强转为底层无符号字节数组接口 */
    BSP_RS232_Send((const uint8_t *)str, (uint16_t)strlen(str));
}

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐