在嵌入式开发中,串口通信是最基础也最常用的外设之一。但当你需要处理像MODBUS这样没有固定帧长度的协议时,如何准确判断一帧数据的结束就成了一个棘手的问题。今天我们就来深入探讨如何利用STM32的通用定时器实现可靠的帧超时判断机制。
串口通信中,我们通常会遇到两种数据帧格式:固定长度和可变长度。对于固定长度的帧,处理起来相对简单,只需要计数接收到的字节数即可。但像MODBUS这样的工业协议,帧长度会根据功能码和数据内容而变化,这就带来了几个关键挑战:
传统的解决方案主要有三种:
轮询方式:不断检查串口接收缓冲区
中断+DMA:利用DMA自动搬运数据
中断+定时器:每次收到数据重置定时器
c复制// 三种方式的简单对比
typedef enum {
UART_RX_MODE_POLLING, // 轮询
UART_RX_MODE_DMA, // DMA
UART_RX_MODE_TIMER // 定时器
} UartRxMode;
定时器作为"看门狗"的实现思路其实非常直观:每次收到一个字节就重置定时器,如果定时器溢出就认为一帧结束。这种机制完美契合了MODBUS协议中"帧间隔"的概念。
关键参数计算:
对于常见的MODBUS RTU模式,协议规定帧间隔至少为3.5个字符时间。以9600bps为例:
因此,我们需要将定时器溢出时间设置为略大于3.65ms(通常取4ms)。
定时器配置要点:
c复制// 定时器初始化示例(以TIM7为例)
void MX_TIM7_Init(void)
{
htim7.Instance = TIM7;
htim7.Init.Prescaler = 84-1; // 84MHz/84 = 1MHz
htim7.Init.CounterMode = TIM_COUNTERMODE_UP;
htim7.Init.Period = 4000-1; // 1MHz下4000计数=4ms
htim7.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
if (HAL_TIM_Base_Init(&htim7) != HAL_OK) {
Error_Handler();
}
}
让我们通过一个完整的示例来理解这套机制如何工作。我们将使用STM32CubeMX生成基础代码,然后添加关键功能。
首先需要设计一个合理的数据结构来管理接收过程:
c复制typedef struct {
uint8_t *rx_buf; // 接收缓冲区指针
uint16_t rx_buf_cnt; // 当前接收计数
uint16_t rx_size; // 完整帧长度
uint8_t rx_flag; // 帧接收完成标志
uint8_t *tx_buf; // 发送缓冲区指针
uint16_t tx_buf_cnt; // 发送计数
uint16_t tx_size; // 待发送数据长度
} UART_BUF;
#define UART_RX_BUF_SIZE 256
#define UART_TX_BUF_SIZE 256
UART_BUF uart_buf;
uint8_t uart_rx_buffer[UART_RX_BUF_SIZE];
uint8_t uart_tx_buffer[UART_TX_BUF_SIZE];
uint8_t rx_byte; // 单字节接收缓存
在HAL库中,当开启串口接收中断后,每收到一个字节都会触发HAL_UART_RxCpltCallback回调:
c复制void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1) {
// 检查缓冲区是否溢出
if(uart_buf.rx_buf_cnt >= UART_RX_BUF_SIZE-1) {
uart_buf.rx_buf_cnt = 0;
memset(uart_buf.rx_buf, 0, UART_RX_BUF_SIZE);
HAL_UART_Transmit(huart, (uint8_t *)"ERROR: Buffer overflow!\r\n", 25, 100);
} else {
// 存储接收到的字节
uart_buf.rx_buf[uart_buf.rx_buf_cnt++] = rx_byte;
// 重置定时器
HAL_TIM_Base_Stop_IT(&htim7);
__HAL_TIM_SET_COUNTER(&htim7, 0);
HAL_TIM_Base_Start_IT(&htim7);
}
// 重新开启单字节接收中断
HAL_UART_Receive_IT(huart, &rx_byte, 1);
}
}
当超过设定的时间没有收到新数据时,定时器溢出中断会被触发:
c复制void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM7) {
// 清除中断标志
__HAL_TIM_CLEAR_FLAG(htim, TIM_FLAG_UPDATE);
// 停止定时器
HAL_TIM_Base_Stop_IT(htim);
// 设置帧接收完成标志
uart_buf.rx_size = uart_buf.rx_buf_cnt;
uart_buf.rx_buf_cnt = 0;
uart_buf.rx_flag = 1;
}
}
在主循环中,我们检查帧接收标志并进行相应处理:
c复制int main(void)
{
// HAL初始化...
// 用户初始化
uart_buf.rx_buf = uart_rx_buffer;
uart_buf.tx_buf = uart_tx_buffer;
uart_buf.rx_buf_cnt = 0;
uart_buf.rx_flag = 0;
// 开启串口接收中断
HAL_UART_Receive_IT(&huart1, &rx_byte, 1);
while (1) {
if(uart_buf.rx_flag) {
// 帧处理逻辑
ProcessFrame(uart_buf.rx_buf, uart_buf.rx_size);
// 清除标志
uart_buf.rx_flag = 0;
uart_buf.rx_size = 0;
}
// 其他任务...
HAL_Delay(1);
}
}
在实际项目中,你可能会遇到以下典型问题:
串口中断和定时器中断的优先级设置不当会导致问题:
c复制// 正确的中断优先级设置示例
HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);
HAL_NVIC_SetPriority(TIM7_IRQn, 1, 0);
定时器的超时时间需要根据波特率精确计算:
| 波特率(bps) | 字符时间(ms) | 3.5字符时间(ms) | 推荐超时(ms) |
|---|---|---|---|
| 9600 | 1.04 | 3.65 | 4.0 |
| 19200 | 0.52 | 1.82 | 2.0 |
| 38400 | 0.26 | 0.91 | 1.0 |
| 115200 | 0.087 | 0.30 | 0.5 |
对于高负载场景,可以考虑以下优化:
c复制// 双缓冲实现示例
typedef struct {
uint8_t buffer[2][UART_RX_BUF_SIZE];
uint8_t active_buf;
uint16_t length[2];
} DoubleBuffer;
DoubleBuffer rx_double_buf;
// 在中断中切换缓冲区
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
// ...其他逻辑
// 存储数据到当前活跃缓冲区
uint8_t buf_idx = rx_double_buf.active_buf;
rx_double_buf.buffer[buf_idx][rx_double_buf.length[buf_idx]++] = rx_byte;
// 检查是否需要切换缓冲区
if(rx_double_buf.length[buf_idx] >= UART_RX_BUF_SIZE) {
rx_double_buf.active_buf ^= 1; // 切换缓冲区
rx_double_buf.length[buf_idx] = 0;
// 通知主循环处理满的缓冲区...
}
}
健壮的通信程序需要完善的错误处理:
c复制// MODBUS CRC16计算示例
uint16_t CalculateCRC16(uint8_t *data, uint16_t length)
{
uint16_t crc = 0xFFFF;
for(uint16_t i = 0; i < length; i++) {
crc ^= data[i];
for(uint8_t j = 0; j < 8; j++) {
if(crc & 0x0001) {
crc >>= 1;
crc ^= 0xA001;
} else {
crc >>= 1;
}
}
}
return crc;
}
在工业现场应用中,我们发现几个值得注意的细节:
电磁干扰问题:在恶劣环境中,串口线容易引入干扰,导致误触发。解决方法包括:
多设备通信:当总线上有多个MODBUS设备时:
性能优化:对于高波特率(115200以上):
c复制// 高效的中断处理示例
__attribute__((optimize("O3")))
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
// 最小化中断处理时间
static uint32_t last_tick = 0;
uint32_t current_tick = HAL_GetTick();
if(current_tick - last_tick < 1) {
// 短时间内多次中断,可能是噪声
return;
}
last_tick = current_tick;
// ...其余处理逻辑
}
通过这个项目,我们发现定时器超时判断的方法在资源占用和可靠性之间取得了很好的平衡。它不需要像DMA那样占用大量内存,又能比轮询方式更高效地利用CPU资源。对于大多数MODBUS应用场景,这无疑是一个理想的解决方案。