STM32从零到量产开发:四路继电器工业控制模块开发-RS232 调试通信驱动模块(bsp_res232)设计说明
·
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_IRQHandler → HAL_UART_IRQHandler → HAL_UART_RxCpltCallback → BSP_RS232_OnRxByte
4.2 并发安全保护
在主循环读取数据(ReadByte、Available)时,代码使用了 __disable_irq() 和 __enable_irq() 进行临界区保护。这是为了防止在读取或更新 head/tail 指针时被 UART 接收中断打断,从而避免数据错乱或指针越界。
4.3 错误恢复机制
当发生 ORE(溢出错误)、FE(帧错误)等硬件异常时,BSP_RS232_OnError 会被触发。该函数会强制复位环形缓冲区的读写指针,并重新调用 HAL_UART_Receive_IT 启动接收,确保串口在异常后能够自动恢复工作。
4.4 使用限制
BSP_RS232_OnRxByte和BSP_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));
}
更多推荐


所有评论(0)