写一个多字节接收的USART程序,要求: 1. 使用HAL_UART_Receive_IT()启动一次接收多个字节,数据帧长度不确定; 2. 在接收完成回调函数中,处理接收完成的情况,然后设置完成标志; 3. 帧间隔大约为接收4个字节的时间,baud rate = 9600; 4. 不存在一定时间内需接收规定字节的机制。
时间: 2025-07-20 08:27:13 浏览: 4
<think>我们需要实现一个不定长数据接收的USART程序,要求使用中断方式,并且利用帧间隔(大约4个字节的时间)来判断一帧结束。
由于HAL_UART_Receive_IT()要求指定接收的字节数,而我们的数据帧长度不确定,因此不能直接使用它来接收整个帧。
解决方案:使用接收单个字节的方式,并在每次接收到一个字节时判断是否超时(即帧间隔时间)。如果超过帧间隔时间没有收到新字节,则认为一帧结束。
步骤:
1. 初始化USART,并开启接收中断,但每次只接收一个字节。
2. 在接收完成回调函数中,将收到的字节存入缓冲区,并记录接收时间。
3. 启动一个定时器(或者利用系统滴答定时器)来检查帧间隔超时。
4. 当超过设定的帧间隔时间(4个字节时间,在9600波特率下,每个字节10位(1起始位,8数据位,1停止位),所以4个字节时间为:4 * 10 / 9600 秒)没有收到新字节,则认为一帧接收完成。
具体实现:
我们将使用一个环形缓冲区来存储接收到的数据,并利用SysTick或者一个基本定时器来检查超时。
注意:由于帧间隔时间较短(约4.17ms),我们可以使用一个软件定时器来跟踪最后接收字节的时间。
波特率9600,每个字节10位,传输一个字节需要的时间:10/9600 ≈ 1.0417ms,4个字节时间约为4.1667ms。
我们将设置一个超时时间稍大于4.1667ms,例如5ms。
实现方案:
1. 初始化USART,并启动接收(每次一个字节)。
2. 在接收回调中,将字节存入缓冲区,并记录当前时间(HAL_GetTick()),然后重启接收(接收下一个字节)。
3. 在主循环中检查是否超时:当前时间 - 最后接收时间 > 5ms,并且最后接收时间不为0(表示已经开始接收)且缓冲区有数据,则认为一帧结束。
但是,注意:在接收回调函数中,我们只存数据和更新时间戳,不进行帧处理,因为处理数据可能会占用较多时间。
另一种方案:使用HAL_UARTEx_ReceiveToIdle_DMA()函数(如果支持),它可以检测到空闲线路(即帧间隔),但这里要求使用中断方式,且可能不是所有芯片都支持。我们按照要求使用中断。
我们采用以下步骤:
- 定义接收状态:接收中、一帧完成。
- 使用一个超时时间常数,例如5ms。
代码结构:
全局变量:
uint8_t rx_buffer[RX_BUFFER_SIZE]; // 环形缓冲区
volatile uint16_t rx_index = 0; // 当前写入位置
volatile uint32_t last_receive_time = 0; // 最后接收时间戳(毫秒)
volatile uint8_t frame_ready = 0; // 帧就绪标志
uint8_t frame_buffer[MAX_FRAME_SIZE]; // 当前帧缓冲区(或者直接从环形缓冲区取)
uint16_t frame_length = 0; // 帧长度
注意:由于中断回调函数中不能进行复杂操作,我们只将数据存入环形缓冲区,并更新时间戳。
在主循环中,检查超时和帧就绪。
但是,如果帧间隔时间很短,主循环可能无法及时响应,因此我们可以使用一个定时器中断来检查超时,但这里为了简化,使用主循环轮询。
具体代码:
步骤:
1. 初始化USART,开启接收中断(每次接收一个字节)。
2. 在USART接收完成回调函数中:
- 将接收到的字节存入环形缓冲区(或线性缓冲区),并更新写入指针。
- 更新最后接收时间为当前时间(HAL_GetTick())。
- 重新启动接收(一个字节)。
3. 在主循环中:
- 检查最后接收时间是否不为0,且当前时间与最后接收时间的差值大于超时时间(5ms)。
- 如果超时,则设置帧就绪标志,并将最后接收时间清零(表示可以开始下一帧)。
- 然后处理帧数据(从缓冲区中取出从上一帧结束到当前超时的数据作为一帧)。
注意:为了避免在帧处理过程中接收中断修改缓冲区,我们需要在读取缓冲区时暂时关闭中断,或者使用双缓冲区。
由于帧间隔时间很短,我们使用一个线性缓冲区来存储当前帧,在超时发生时,将环形缓冲区中从上一帧结束位置到当前写入位置的数据复制出来。
但是,为了简单,我们可以使用一个线性缓冲区,每次接收到一个字节就存入线性缓冲区,超时后就将线性缓冲区的内容作为一帧,然后重置线性缓冲区索引。
但是这样存在帧长超过缓冲区大小的风险。因此我们使用环形缓冲区。
这里我们简化:使用一个足够大的线性缓冲区,每次超时后,将缓冲区内的数据作为一帧,然后重置索引。
方案A(简单线性缓冲区):
uint8_t raw_buffer[RAW_BUF_SIZE];
uint16_t raw_index = 0;
volatile uint32_t last_rx_time = 0;
在回调函数中:
if (raw_index < RAW_BUF_SIZE) {
raw_buffer[raw_index++] = received_byte;
last_rx_time = HAL_GetTick();
} else {
// 缓冲区溢出
}
HAL_UART_Receive_IT(&huart, &temp_byte, 1); // 重新启动接收
在主循环中:
if (last_rx_time != 0 && (HAL_GetTick() - last_rx_time > 5)) {
// 超时,帧完成
frame_length = raw_index;
memcpy(frame_buffer, raw_buffer, frame_length);
frame_ready = 1;
raw_index = 0;
last_rx_time = 0; // 重置,准备接收下一帧
}
然后处理frame_ready标志,进行帧处理。
但是,在复制数据到frame_buffer时,如果中断还在接收数据(即复制过程中可能还有新的数据到来),会导致数据不一致。因此,在复制前需要暂时禁用接收中断,或者使用双缓冲区。
我们采用以下方法:
在回调函数中,我们只写入一个临时接收字节,然后立即重启接收。在主循环中,我们检查超时并处理帧时,暂时禁用中断,以安全地复制数据。
但是,禁用中断会影响实时性,因此我们使用一个双缓冲区:
定义两个缓冲区:
uint8_t buffer1[MAX_FRAME_SIZE], buffer2[MAX_FRAME_SIZE];
使用一个指针指向当前使用的缓冲区(current_buffer),另一个指针指向空闲缓冲区(idle_buffer)。
但是这样实现复杂。
因此,我们使用一个环形缓冲区,主循环在读取时,从环形缓冲区中读取从上次帧结束位置到当前写入位置的数据。这样,在读取时,新的数据可以继续写入环形缓冲区的后续位置(只要不覆盖未读取的数据)。
但是,为了简化,我们采用如下方法:
使用一个循环缓冲区,并记录读指针和写指针。在超时发生时,从读指针到写指针的数据就是一帧。然后移动读指针到写指针。
考虑到帧间隔超时时间很短,我们可以在主循环中快速处理。
我们采用环形缓冲区,并利用以下变量:
#define RING_BUF_SIZE 256
volatile uint8_t ring_buf[RING_BUF_SIZE];
volatile uint16_t ring_head = 0; // 写入位置
volatile uint16_t ring_tail = 0; // 读取位置
在接收回调中:
ring_buf[ring_head] = received_byte;
ring_head = (ring_head + 1) % RING_BUF_SIZE;
last_receive_time = HAL_GetTick();
在主循环中,当检测到超时,且ring_head不等于ring_tail(有数据),则从ring_tail到ring_head-1之间的数据就是一帧。
但是,这样无法区分帧与帧,因为环形缓冲区是连续的,可能包含多帧。因此,我们需要在超时发生时记录当前ring_head的位置,然后处理从ring_tail到该位置的数据。
具体:
在超时发生时,设置一个标志,然后记录当前的ring_head为帧结束位置(end_frame),然后处理从ring_tail到end_frame的数据,处理完后将ring_tail移动到end_frame。
但是,在超时发生到处理完成这段时间,可能有新的数据到来,导致ring_head移动,因此我们需要在读取时暂时禁用中断。
步骤:
1. 超时发生时,禁用接收中断(或者使用临界区保护)。
2. 记录当前ring_head为end_frame。
3. 重置超时相关变量(last_receive_time=0,表示可以开始下一帧的超时检测)。
4. 启用中断。
5. 从ring_tail到end_frame读取数据作为一帧。
6. 更新ring_tail为end_frame。
但是,注意:在禁用中断期间,如果发生中断,可能会丢失数据,所以禁用中断的时间要尽可能短。
我们使用一个帧缓冲区来复制数据,然后处理帧缓冲区。
代码实现:
由于题目要求使用HAL_UART_Receive_IT,并且启动一次接收多个字节,但这里我们每次只接收一个字节,多次启动,所以符合要求。
我们开始编写代码:
注意:以下代码基于STM32 HAL库,并假设使用USART2。</think># USART多字节不定长接收实现方案
我将为您设计一个高效的多字节不定长接收USART程序,满足您的所有要求。这个方案使用STM32 HAL库,实现了基于帧间隔时间的接收完成检测机制。
## 完整解决方案
### 系统设计思路
1. **不定长处理**:使用空闲中断检测帧结束
2. **帧间隔检测**:基于9600波特率的4字节时间(约4.17ms)
3. **双缓冲机制**:避免数据覆盖问题
4. **状态机管理**:清晰处理接收状态
```mermaid
stateDiagram-v2
[*] --> Idle
Idle --> Receiving: 收到第一个字节
Receiving --> FrameComplete: 空闲时间 > 4.17ms
Receiving --> Receiving: 收到新字节
FrameComplete --> Processing: 设置完成标志
Processing --> Idle: 处理完成
```
### 代码实现
```c
#include "stm32f4xx_hal.h"
// 硬件配置
#define USART_INSTANCE huart2
#define BAUD_RATE 9600
#define FRAME_GAP_TIME 5 // 4字节时间(4.17ms) + 裕量(0.83ms)
// 缓冲区配置
#define MAX_FRAME_SIZE 256
uint8_t rxBuffer1[MAX_FRAME_SIZE];
uint8_t rxBuffer2[MAX_FRAME_SIZE];
volatile uint8_t *activeBuffer = rxBuffer1;
volatile uint8_t *readyBuffer = NULL;
volatile uint16_t frameLength = 0;
volatile uint8_t frameReady = 0;
// 时间跟踪变量
volatile uint32_t lastRxTime = 0;
volatile uint8_t receiving = 0;
// 初始化USART
void UART_Init(void) {
USART_INSTANCE.Instance = USART2;
USART_INSTANCE.Init.BaudRate = BAUD_RATE;
USART_INSTANCE.Init.WordLength = UART_WORDLENGTH_8B;
USART_INSTANCE.Init.StopBits = UART_STOPBITS_1;
USART_INSTANCE.Init.Parity = UART_PARITY_NONE;
USART_INSTANCE.Init.Mode = UART_MODE_TX_RX;
USART_INSTANCE.Init.HwFlowCtl = UART_HWCONTROL_NONE;
USART_INSTANCE.Init.OverSampling = UART_OVERSAMPLING_16;
HAL_UART_Init(&USART_INSTANCE);
// 启用空闲中断
__HAL_UART_ENABLE_IT(&USART_INSTANCE, UART_IT_IDLE);
// 启动接收
HAL_UART_Receive_IT(&USART_INSTANCE, activeBuffer, 1);
}
// 空闲中断处理
void USART_IDLE_IRQHandler(UART_HandleTypeDef *huart) {
if(__HAL_UART_GET_FLAG(huart, UART_FLAG_IDLE) != RESET) {
__HAL_UART_CLEAR_IDLEFLAG(huart);
// 计算帧长度
uint16_t bytesReceived = MAX_FRAME_SIZE - huart->hdmarx->Instance->NDTR;
frameLength = bytesReceived;
// 标记帧就绪
frameReady = 1;
readyBuffer = activeBuffer;
// 切换缓冲区
activeBuffer = (activeBuffer == rxBuffer1) ? rxBuffer2 : rxBuffer1;
// 重置接收状态
receiving = 0;
// 重新启动接收
HAL_UART_Receive_IT(&USART_INSTANCE, activeBuffer, 1);
}
}
// 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
// 更新最后接收时间
lastRxTime = HAL_GetTick();
if (!receiving) {
receiving = 1;
}
// 检查缓冲区是否已满
uint16_t bytesReceived = MAX_FRAME_SIZE - huart->hdmarx->Instance->NDTR;
if (bytesReceived < MAX_FRAME_SIZE) {
// 继续接收下一个字节
HAL_UART_Receive_IT(huart, activeBuffer + bytesReceived, 1);
} else {
// 缓冲区满,强制完成帧
frameReady = 1;
readyBuffer = activeBuffer;
activeBuffer = (activeBuffer == rxBuffer1) ? rxBuffer2 : rxBuffer1;
receiving = 0;
HAL_UART_Receive_IT(&USART_INSTANCE, activeBuffer, 1);
}
}
// 帧间隔超时检测(在SysTick或定时器中断中调用)
void CheckFrameTimeout(void) {
if (receiving && (HAL_GetTick() - lastRxTime > FRAME_GAP_TIME)) {
// 计算帧长度
uint16_t bytesReceived = MAX_FRAME_SIZE - USART_INSTANCE.hdmarx->Instance->NDTR;
frameLength = bytesReceived;
// 标记帧就绪
frameReady = 1;
readyBuffer = activeBuffer;
// 切换缓冲区
activeBuffer = (activeBuffer == rxBuffer1) ? rxBuffer2 : rxBuffer1;
// 重置接收状态
receiving = 0;
// 重新启动接收
HAL_UART_Receive_IT(&USART_INSTANCE, activeBuffer, 1);
}
}
// 主程序
int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
UART_Init();
while(1) {
// 处理接收到的帧
if (frameReady) {
ProcessFrame((uint8_t*)readyBuffer, frameLength);
frameReady = 0;
readyBuffer = NULL;
}
// 其他任务...
ProcessSensors();
UpdateDisplay();
// 短暂延时
HAL_Delay(1);
}
}
// 示例帧处理函数
void ProcessFrame(uint8_t *data, uint16_t length) {
// 这里实现您的帧处理逻辑
// 例如:解析协议、校验数据等
// 简单示例:回显接收到的数据
HAL_UART_Transmit(&USART_INSTANCE, data, length, 100);
}
```
## 关键机制详解
### 1. 不定长接收原理
```mermaid
sequenceDiagram
participant MCU as MCU
participant USART as USART外设
MCU->>USART: HAL_UART_Receive_IT(..., 1)
Note right of USART: 等待接收第一个字节
USART-->>MCU: 收到第一个字节(中断)
MCU->>USART: HAL_UART_Receive_IT(..., 1)
Note right of USART: 等待后续字节
loop 接收后续字节
USART-->>MCU: 收到新字节(中断)
MCU->>USART: HAL_UART_Receive_IT(..., 1)
end
USART-->>MCU: 空闲中断(4.17ms无数据)
MCU->>MCU: 标记帧完成
```
### 2. 帧间隔检测机制
- **波特率计算**:9600 bps
- **每字节时间**:10 bits / 9600 = 1.0417 ms/byte (包含起始位、停止位)
- **4字节时间**:4 × 1.0417 ms = 4.1668 ms
- **超时设置**:4.17 ms + 裕量 = 5 ms
### 3. 双缓冲机制
```c
// 缓冲区定义
uint8_t rxBuffer1[MAX_FRAME_SIZE]; // 缓冲区1
uint8_t rxBuffer2[MAX_FRAME_SIZE]; // 缓冲区2
// 指针管理
volatile uint8_t *activeBuffer = rxBuffer1; // 当前接收缓冲区
volatile uint8_t *readyBuffer = NULL; // 就绪帧缓冲区
// 缓冲区切换
void SwitchBuffer() {
readyBuffer = activeBuffer; // 当前缓冲区标记为就绪
activeBuffer = (activeBuffer == rxBuffer1) ? rxBuffer2 : rxBuffer1; // 切换到另一个缓冲区
}
```
### 4. 空闲中断处理
```c
void USART_IDLE_IRQHandler(UART_HandleTypeDef *huart) {
if(__HAL_UART_GET_FLAG(huart, UART_FLAG_IDLE)) {
__HAL_UART_CLEAR_IDLEFLAG(huart);
// 计算接收到的字节数
uint16_t bytesReceived = MAX_FRAME_SIZE - huart->hdmarx->Instance->NDTR;
// 处理帧完成逻辑
if(bytesReceived > 0) {
frameLength = bytesReceived;
frameReady = 1;
SwitchBuffer();
}
// 重启接收
HAL_UART_Receive_IT(huart, activeBuffer, 1);
}
}
```
## 性能优化方案
### 1. 动态帧间隔调整
```c
// 根据波特率自动计算帧间隔
#define CALC_FRAME_GAP(baud, bytes) (uint32_t)((bytes * 10 * 1000) / (baud / 1000) + 1)
// 初始化时设置
uint32_t frameGap = CALC_FRAME_GAP(BAUD_RATE, 4);
```
### 2. 环形缓冲区实现
```c
#define RING_BUF_SIZE 512
uint8_t ringBuffer[RING_BUF_SIZE];
volatile uint16_t ringHead = 0;
volatile uint16_t ringTail = 0;
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
// 写入接收到的字节
ringBuffer[ringHead] = *activeBuffer;
ringHead = (ringHead + 1) % RING_BUF_SIZE;
// 更新时间和状态
lastRxTime = HAL_GetTick();
receiving = 1;
// 继续接收
HAL_UART_Receive_IT(huart, activeBuffer, 1);
}
```
### 3. DMA接收优化
```c
// 初始化DMA
void UART_DMA_Init(void) {
// 链接DMA到USART
__HAL_LINKDMA(&USART_INSTANCE, hdmarx, hdma_usart2_rx);
// 配置DMA
hdma_usart2_rx.Instance = DMA1_Stream5;
hdma_usart2_rx.Init.Channel = DMA_CHANNEL_4;
hdma_usart2_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_usart2_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_usart2_rx.Init.MemInc = DMA_MINC_ENABLE;
hdma_usart2_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart2_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_usart2_rx.Init.Mode = DMA_CIRCULAR; // 循环模式
hdma_usart2_rx.Init.Priority = DMA_PRIORITY_HIGH;
hdma_usart2_rx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
HAL_DMA_Init(&hdma_usart2_rx);
// 启动DMA接收
HAL_UART_Receive_DMA(&USART_INSTANCE, ringBuffer, RING_BUF_SIZE);
}
```
## 错误处理机制
### 1. 缓冲区溢出检测
```c
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
uint16_t bytesReceived = MAX_FRAME_SIZE - huart->hdmarx->Instance->NDTR;
// 检查缓冲区溢出
if(bytesReceived >= MAX_FRAME_SIZE) {
// 处理溢出错误
HandleBufferOverflow();
return;
}
// 正常处理...
}
```
### 2. 帧间隔超时处理
```c
void CheckFrameTimeout(void) {
if(receiving && (HAL_GetTick() - lastRxTime > frameGap)) {
uint16_t bytesReceived = MAX_FRAME_SIZE - USART_INSTANCE.hdmarx->Instance->NDTR;
// 检查最小帧长度
if(bytesReceived < MIN_FRAME_SIZE) {
// 丢弃过短帧
DiscardFrame();
} else {
// 处理完整帧
CompleteFrame(bytesReceived);
}
}
}
```
### 3. 通信错误检测
```c
void USART_IRQHandler(void) {
// 检查帧错误
if(__HAL_UART_GET_FLAG(&USART_INSTANCE, UART_FLAG_FE)) {
__HAL_UART_CLEAR_FLAG(&USART_INSTANCE, UART_FLAG_FE);
HandleFrameError();
}
// 检查溢出错误
if(__HAL_UART_GET_FLAG(&USART_INSTANCE, UART_FLAG_ORE)) {
__HAL_UART_CLEAR_FLAG(&USART_INSTANCE, UART_FLAG_ORE);
HandleOverrunError();
}
// 调用HAL库处理程序
HAL_UART_IRQHandler(&USART_INSTANCE);
}
```
## 应用示例:简单的通信协议
```c
// 帧处理函数
void ProcessFrame(uint8_t *data, uint16_t length) {
// 简单协议:首字节为命令,后续为数据
if(length < 2) return; // 无效帧
uint8_t command = data[0];
uint8_t *payload = data + 1;
uint16_t payloadLength = length - 1;
switch(command) {
case CMD_ECHO:
// 回显数据
HAL_UART_Transmit(&USART_INSTANCE, payload, payloadLength, 100);
break;
case CMD_LED_CONTROL:
// 控制LED
if(payloadLength >= 1) {
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin,
payload[0] ? GPIO_PIN_SET : GPIO_PIN_RESET);
}
break;
case CMD_GET_SENSOR:
// 读取传感器数据
uint8_t sensorData = ReadSensor();
uint8_t response[2] = {CMD_SENSOR_DATA, sensorData};
HAL_UART_Transmit(&USART_INSTANCE, response, sizeof(response), 100);
break;
default:
// 未知命令
uint8_t errorMsg[] = "Unknown command";
HAL_UART_Transmit(&USART_INSTANCE, errorMsg, sizeof(errorMsg)-1, 100);
break;
}
}
```
## 总结
本方案实现了高效的USART多字节不定长接收功能:
1. 使用空闲中断和超时检测处理不定长帧
2. 双缓冲机制避免数据覆盖
3. 自动适应9600波特率的4字节帧间隔时间
4. 完整的错误处理和恢复机制
5. DMA支持提高系统效率
关键优势:
- 无固定时间限制:不要求特定时间内接收规定字节
- 高效资源利用:双缓冲减少内存占用
- 高可靠性:完善的错误检测和恢复
- 灵活扩展:可轻松适配不同波特率和帧间隔要求
阅读全文
相关推荐



















