STM32提高串口发送函数执行速度并避免丢帧方法
目录
前言
提高串口发送函数的执行速度并尽量避免丢帧,涉及优化数据传输的方法、减少阻塞时间以及合理管理缓冲区。以下是几种有效的方法和详细的实现步骤:
一、使用非阻塞模式(中断或DMA)
a、使用中断驱动的传输
原理:利用 UART 中断驱动数据发送,避免在传输过程中阻塞主线程。每当 UART 发送一个字节完成时,触发中断并发送下一个字节。
实现步骤:
1.初始化 UART 并启用发送中断:
// 假设使用的是 STM32 HAL 库
HAL_UART_Transmit_IT(&huart1, buffer, length);
2.实现中断回调函数:
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1) {
// 发送完成后的处理
// 可以开始下一个传输或设置标志位
}
}
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1) {
// 错误处理
}
}
3.管理传输队列:
使用一个环形缓冲区(环形队列)来存储待发送的数据。主线程将数据放入队列,中断处理函数从队列中取出数据并发送。
#define TX_BUFFER_SIZE 256
uint8_t txBuffer[TX_BUFFER_SIZE];
volatile uint16_t txHead = 0;
volatile uint16_t txTail = 0;
void enqueueTxData(uint8_t data) {
uint16_t nextHead = (txHead + 1) % TX_BUFFER_SIZE;
if (nextHead != txTail) { // 检查缓冲区是否满
txBuffer[txHead] = data;
txHead = nextHead;
}
// 处理缓冲区满的情况
}
uint8_t dequeueTxData(void) {
uint8_t data = 0;
if (txTail != txHead) { // 检查缓冲区是否为空
data = txBuffer[txTail];
txTail = (txTail + 1) % TX_BUFFER_SIZE;
}
return data;
}
void startNextTransmission(UART_HandleTypeDef *huart) {
if (txTail != txHead) {
uint8_t data = dequeueTxData();
HAL_UART_Transmit_IT(huart, &data, 1);
}
}
// 在 HAL_UART_TxCpltCallback 中调用
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1) {
startNextTransmission(huart);
}
}
// 主线程发送数据
void sendData(uint8_t *data, uint16_t length) {
for (uint16_t i = 0; i < length; i++) {
enqueueTxData(data[i]);
}
// 如果没有正在发送,启动传输
if (txTail == txHead) {
startNextTransmission(&huart1);
}
}
b. 使用 DMA 驱动的传输
原理:利用 DMA(Direct Memory Access)控制器进行数据传输,减少 CPU 负担,提高传输效率。DMA 可以在后台自动将数据从内存复制到 UART 数据寄存器,无需 CPU 介入。
实现步骤:
1.初始化 UART 并配置 DMA:
// 在 CubeMX 中配置 USART1 使用 DMA 进行 TX 传输
// 生成初始化代码
2.使用 HAL_UART_Transmit_DMA:
HAL_UART_Transmit_DMA(&huart1, buffer, length);
3.实现 DMA 传输完成回调:
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1) {
// DMA 传输完成后的处理
// 可以开始下一个传输或设置标志位
}
}
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1) {
// 错误处理
}
}
4.管理传输队列:
类似于中断驱动的方法,使用环形缓冲区存储待发送数据。当 DMA 传输完成后,从队列中取出下一个数据块并启动新的 DMA 传输。
#define TX_BUFFER_SIZE 1024
uint8_t txBuffer[TX_BUFFER_SIZE];
volatile uint16_t txHead = 0;
volatile uint16_t txTail = 0;
void enqueueTxData(uint8_t *data, uint16_t length) {
for (uint16_t i = 0; i < length; i++) {
uint16_t nextHead = (txHead + 1) % TX_BUFFER_SIZE;
if (nextHead != txTail) { // 检查缓冲区是否满
txBuffer[txHead] = data[i];
txHead = nextHead;
} else {
// 处理缓冲区满的情况,例如丢弃数据或等待
}
}
}
void startNextDMA(UART_HandleTypeDef *huart) {
if (txTail != txHead) {
uint16_t remaining = (txHead >= txTail) ? (txHead - txTail) : (TX_BUFFER_SIZE - txTail);
HAL_UART_Transmit_DMA(huart, &txBuffer[txTail], remaining);
txTail = (txTail + remaining) % TX_BUFFER_SIZE;
}
}
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1) {
startNextDMA(huart);
}
}
void sendData(uint8_t *data, uint16_t length) {
enqueueTxData(data, length);
// 如果没有正在发送,启动传输
if (txTail == txHead) {
startNextDMA(&huart1);
}
}
优点:
提高传输效率:DMA 可以在后台传输数据,无需 CPU 介入,释放 CPU 资源处理其他任务。
减少中断负担:DMA 传输完成后只触发一次中断,而中断驱动模式每发送一个字节都会触发中断。
注意事项:
缓冲区管理:确保缓冲区足够大,避免溢出。
错误处理:处理 DMA 传输错误,例如缓冲区溢出或传输失败。
二、 增大缓冲区和优化数据管理
a. 增大发送缓冲区
原理:增加发送缓冲区的大小,减少频繁启动传输的次数,提高整体传输效率。
实现步骤:
在环形缓冲区或 DMA 缓冲区中使用较大的数据块进行传输,减少中断或 DMA 传输启动的频率。
#define TX_BUFFER_SIZE 2048 // 根据需求调整
uint8_t txBuffer[TX_BUFFER_SIZE];
volatile uint16_t txHead = 0;
volatile uint16_t txTail = 0;
优点:
减少传输启动次数:一次传输大量数据,减少中断或 DMA 启动的频率。
提高传输效率:一次传输多个字节,提高数据吞吐量。
注意事项:
内存占用:确保系统有足够的内存来支持较大的缓冲区。
实时性:在实时性要求较高的应用中,可能需要平衡缓冲区大小与实时响应能力。
b. 优化数据传输逻辑
原理:减少不必要的数据复制和传输次数,优化数据管理逻辑,提高传输效率。
实现步骤:
避免频繁调用传输函数:批量传输数据,减少函数调用次数。
使用指针操作:直接操作数据指针,减少数据复制。
// 例如,批量传输多个数据块
void sendDataBatch(uint8_t *data, uint16_t length) {
while (length > 0) {
uint16_t chunk = (length > MAX_CHUNK_SIZE) ? MAX_CHUNK_SIZE : length;
enqueueTxData(data, chunk);
data += chunk;
length -= chunk;
}
// 启动传输
if (txTail == txHead) {
startNextTransmission(&huart1);
}
}
三、 提高 UART 的配置效率
a. 增加 UART 的波特率
原理:增加 UART 的波特率可以提高数据传输速率,减少传输时间。
实现步骤:
调整波特率配置:
在 CubeMX 中配置 UART 的波特率,例如从 115200 增加到 921600。
确保硬件支持高波特率:
确保你的硬件(微控制器和接收端设备)支持更高的波特率,并且信号完整性良好。
优点:
提高传输速率:更高的波特率意味着更快的数据传输。
减少传输时间:同样数量的数据在更短的时间内传输完成。
注意事项:
信号完整性:高波特率可能导致信号干扰和误码率增加,确保 PCB 设计良好,必要时使用屏蔽和差分信号。
硬件限制:某些 UART 硬件模块对波特率有上限,需查阅具体微控制器的参考手册。
b. 调整 UART 配置参数
原理:优化 UART 的配置参数(如数据位、停止位、奇偶校验)以提高传输效率。
实现步骤:
数据位:使用 8 数据位是常见配置,兼容性最好。
停止位:使用 1 个停止位而不是 2 个,可以稍微提高传输速率。
奇偶校验:如果不需要错误检测,可以禁用奇偶校验,以减少传输时间。
示例:
huart1.Init.BaudRate = 921600; // 提高波特率
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
HAL_UART_Init(&huart1);
优点:
提高传输效率:减少每个数据包的占用时间。
减少协议开销:禁用奇偶校验减少额外的数据位。
注意事项:
兼容性:确保接收端设备支持相同的配置参数。
错误率:禁用奇偶校验可能增加数据错误的风险,需权衡传输效率和数据完整性。
四、 优化缓冲区和内存管理
a. 使用环形缓冲区
原理:使用环形缓冲区(环形队列)来管理发送和接收数据,减少数据复制和提高传输效率。
实现步骤:
定义环形缓冲区:
#define RX_BUFFER_SIZE 512
uint8_t rxBuffer[RX_BUFFER_SIZE];
volatile uint16_t rxHead = 0;
volatile uint16_t rxTail = 0;
#define TX_BUFFER_SIZE 512
uint8_t txBuffer[TX_BUFFER_SIZE];
volatile uint16_t txHead = 0;
volatile uint16_t txTail = 0;
实现缓冲区管理函数:
// Enqueue data
void enqueueTxData(uint8_t data) {
uint16_t nextHead = (txHead + 1) % TX_BUFFER_SIZE;
if (nextHead != txTail) { // 检查缓冲区是否满
txBuffer[txHead] = data;
txHead = nextHead;
}
// 处理缓冲区满的情况
}
// Dequeue data
uint8_t dequeueTxData(void) {
uint8_t data = 0;
if (txTail != txHead) { // 检查缓冲区是否为空
data = txBuffer[txTail];
txTail = (txTail + 1) % TX_BUFFER_SIZE;
}
return data;
}
优点:
高效管理数据:环形缓冲区能够高效地管理连续的数据流。
减少内存碎片:避免动态内存分配导致的碎片问题。
注意事项:
缓冲区大小:根据应用需求选择合适的缓冲区大小,避免溢出。
线程安全:在多线程环境下,确保缓冲区访问的线程安全,可以使用互斥锁或禁用中断。
b. 减少数据复制
原理:尽量减少数据在传输过程中的复制,直接使用指针或引用传输数据,提升效率。
实现步骤:
直接传输数据块:避免逐字节传输,使用批量数据传输。
void sendData(uint8_t *data, uint16_t length) {
// 确保数据长度不超过缓冲区容量
for (uint16_t i = 0; i < length; i++) {
enqueueTxData(data[i]);
}
// 启动传输
if (txTail == txHead) {
startNextTransmission(&huart1);
}
}
使用指针管理缓冲区:
c
复制代码
// 使用指针进行数据传输
void sendData(uint8_t *data, uint16_t length) {
while (length > 0) {
uint16_t space = TX_BUFFER_SIZE - txHead;
uint16_t chunk = (length < space) ? length : space;
memcpy(&txBuffer[txHead], data, chunk);
txHead = (txHead + chunk) % TX_BUFFER_SIZE;
data += chunk;
length -= chunk;
}
// 启动传输
if (txTail == txHead - chunk) {
startNextTransmission(&huart1);
}
}
优点:
提高传输效率:减少数据复制步骤,提高传输速度。
简化代码逻辑:通过指针管理,简化数据传输逻辑。
注意事项:
内存管理:确保数据指针有效,避免悬空指针或越界访问。
数据对齐:确保数据块对齐,避免传输过程中出现对齐错误。
五、使用更高效的传输函数
a. 直接操作寄存器
原理:直接操作 UART 数据寄存器,绕过 HAL 库的封装,减少函数调用开销,提升传输速度。
实现步骤:
实现非阻塞传输函数:
void UART_SendByte(UART_HandleTypeDef *huart, uint8_t byte) {
// 等待发送寄存器为空
while (!(huart->Instance->SR & USART_SR_TXE));
huart->Instance->DR = byte;
}
void UART_SendData(UART_HandleTypeDef *huart, uint8_t *data, uint16_t length) {
for (uint16_t i = 0; i < length; i++) {
UART_SendByte(huart, data[i]);
}
// 等待传输完成
while (!(huart->Instance->SR & USART_SR_TC));
}
使用 DMA 或中断优化:
结合 DMA 或中断驱动的数据发送,进一步提升效率。
优点:
减少开销:直接操作寄存器,避免 HAL 函数的额外开销。
提升速度:更快速的传输响应。
注意事项:
可维护性:直接操作寄存器可能降低代码的可读性和可维护性。
错误处理:需要手动实现错误检测和处理机制。
b. 使用轻量级库或自定义驱动
原理:使用更轻量级的 UART 驱动库,减少不必要的功能和开销,提升传输速度。
实现步骤:
选择轻量级驱动:例如 ChibiOS 或其他开源实时操作系统,提供更高效的 UART 驱动。
自定义优化:根据项目需求自定义优化 UART 驱动,去除不必要的功能,提升传输效率。
优点:
灵活性:可以根据具体需求进行定制和优化。
性能提升:去除不必要的功能,提高驱动效率。
注意事项:
开发时间:需要花费更多时间进行驱动开发和测试。
兼容性:确保自定义驱动与现有系统和硬件兼容。
六、优化中断和任务优先级
a. 提高 UART 中断优先级
原理:提高 UART 中断的优先级,确保数据传输的及时处理,减少中断延迟。
实现步骤:
配置中断优先级:
// 使用 NVIC 设置 USART1 中断优先级
HAL_NVIC_SetPriority(USART1_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(USART1_IRQn);
确保中断处理及时:
确保中断处理函数简洁高效,避免长时间占用 CPU 资源。
优点:
及时处理数据:高优先级中断可以更快地响应数据传输请求。
减少丢帧风险:及时处理中断,减少数据丢失的可能性。
注意事项:
优先级冲突:避免将 UART 中断优先级设置得过高,导致其他关键任务被阻塞。
中断嵌套:小心处理中断嵌套,避免优先级反转和死锁。
b. 合理安排任务优先级(RTOS 环境)
原理:在使用实时操作系统(如 FreeRTOS)的情况下,合理安排任务优先级,确保 UART 传输任务具有足够的优先级。
实现步骤:
定义任务优先级:
#define UART_TASK_PRIORITY (osPriorityAboveNormal)
创建 UART 传输任务:
void UART_Task(void *argument) {
while (1) {
// 处理发送队列
sendDataFromQueue();
osDelay(1); // 适当的延迟
}
}
// 在主函数中创建任务
osThreadDef(UART_Thread, UART_Task, UART_TASK_PRIORITY, 0, 128);
osThreadCreate(osThread(UART_Thread), NULL);
优点:
提高任务响应:高优先级任务能够更快地响应数据传输需求。
减少任务延迟:合理安排任务优先级,确保关键任务不会被低优先级任务阻塞。
注意事项:
优先级分配:确保高优先级任务不会导致低优先级任务长时间被阻塞。
资源管理:避免优先级反转和资源争用,确保任务调度的公平性。
七、实现双缓冲技术
原理
双缓冲技术使用两个缓冲区交替进行数据传输,一个缓冲区用于数据准备,另一个缓冲区用于传输,减少等待时间和提高传输效率。
实现步骤
定义双缓冲区:
#define BUFFER_SIZE 512
uint8_t buffer1[BUFFER_SIZE];
uint8_t buffer2[BUFFER_SIZE];
volatile uint8_t currentBuffer = 0;
实现缓冲区切换:
void switchBuffer(void) {
if (currentBuffer == 0) {
HAL_UART_Transmit_DMA(&huart1, buffer1, BUFFER_SIZE);
currentBuffer = 1;
} else {
HAL_UART_Transmit_DMA(&huart1, buffer2, BUFFER_SIZE);
currentBuffer = 0;
}
}
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1) {
switchBuffer();
}
}
void prepareBuffer(uint8_t *data, uint16_t length) {
if (currentBuffer == 0) {
memcpy(buffer2, data, length); // 准备下一个缓冲区
} else {
memcpy(buffer1, data, length);
}
// 传输已经在中断回调中处理
}
优点:
减少传输等待时间:一个缓冲区传输时,另一个缓冲区可以准备数据,减少传输等待。
提高吞吐量:双缓冲技术能够更高效地利用 DMA 传输,提高整体数据吞吐量。
注意事项:
同步管理:确保缓冲区切换和数据准备的同步,避免数据覆盖或丢失。
内存占用:双缓冲技术需要额外的内存来存储两个缓冲区。
八、 其他优化建议
a. 避免使用阻塞函数
原理:阻塞函数会阻塞当前线程,导致 CPU 无法处理其他任务,影响整体系统性能。
实现步骤:
使用非阻塞传输:使用中断或 DMA 驱动的传输方式,避免阻塞函数的使用。
优化主循环:确保主循环或任务能够高效执行,避免长时间占用 CPU。
优点:
提高系统响应性:非阻塞传输允许系统同时处理多个任务,提高响应速度。
减少丢帧风险:避免因阻塞导致的数据传输延迟或丢帧。
注意事项:
复杂性:非阻塞传输可能增加代码复杂性,需要合理管理状态和回调。
b. 优化系统时钟配置
原理:提高系统时钟频率可以提升整个系统的运行速度,包括 UART 传输。
实现步骤:
调整系统时钟配置:
在 CubeMX 中配置更高的系统时钟频率(例如,从 72 MHz 提高到 168 MHz)。
确保硬件支持:
确保微控制器的时钟配置稳定,电源设计满足更高频率下的需求。
优点:
提高系统性能:更高的系统时钟频率提升 CPU 和外设的运行速度。
加快数据处理:更高的 CPU 频率可以更快地处理数据传输和其他任务。
注意事项:
功耗增加:更高的系统时钟频率会增加功耗,需要权衡性能和功耗。
散热管理:高频率运行可能导致温度升高,确保良好的散热设计。
c. 使用双核或多核微控制器
原理:利用多核微控制器将数据传输任务和其他任务分离,提高整体系统性能和传输效率。
实现步骤:
选择多核微控制器:
选择支持双核或多核架构的微控制器,如 ARM Cortex-M7 与 Cortex-M4 的组合。
任务分离:
将数据传输任务分配到一个核心,其他任务分配到另一个核心,减少任务间的干扰和延迟。
优点:
并行处理:提高系统的并行处理能力,提升传输效率和系统响应速度。
任务隔离:减少任务间的干扰,提高系统的可靠性和稳定性。
注意事项:
复杂性增加:多核系统需要更复杂的任务管理和同步机制。
成本和功耗:多核系统通常成本更高,功耗也更大。
九、示例代码:使用 DMA 和环形缓冲区优化 UART 传输
以下是一个综合示例,展示如何结合 DMA 驱动和环形缓冲区来优化 UART 传输,提升传输速度并减少丢帧。
a. 定义缓冲区和管理变量
#define TX_BUFFER_SIZE 1024
uint8_t txBuffer[TX_BUFFER_SIZE];
volatile uint16_t txHead = 0;
volatile uint16_t txTail = 0;
volatile uint8_t txInProgress = 0;
b. 实现缓冲区管理函数
// Enqueue data into the transmit buffer
void enqueueTxData(uint8_t *data, uint16_t length) {
for (uint16_t i = 0; i < length; i++) {
uint16_t nextHead = (txHead + 1) % TX_BUFFER_SIZE;
if (nextHead != txTail) { // Check if buffer is not full
txBuffer[txHead] = data[i];
txHead = nextHead;
} else {
// Buffer is full, handle overflow (e.g., discard data or wait)
}
}
}
// Start DMA transmission if not already in progress
void startDMA(UART_HandleTypeDef *huart) {
if (!txInProgress && txTail != txHead) {
uint16_t length = (txHead > txTail) ? (txHead - txTail) : (TX_BUFFER_SIZE - txTail);
HAL_UART_Transmit_DMA(huart, &txBuffer[txTail], length);
txInProgress = 1;
}
}
// Callback function when DMA transmission is complete
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1) {
txTail = (txTail + (huart->TxXferSize)) % TX_BUFFER_SIZE;
txInProgress = 0;
startDMA(huart);
}
}
void sendData(uint8_t *data, uint16_t length) {
enqueueTxData(data, length);
startDMA(&huart1);
}
c. 主函数和初始化
int main(void) {
HAL_Init();
SystemClock_Config(); // 由 CubeMX 生成的系统时钟配置函数
MX_USART1_UART_Init(); // 由 CubeMX 生成的 UART 初始化函数
// 主循环
while (1) {
uint8_t dataToSend[] = "Hello, UART DMA!\n";
sendData(dataToSend, sizeof(dataToSend) - 1);
HAL_Delay(1000); // 发送间隔
}
}
解释:
环形缓冲区:使用 txBuffer 存储待发送的数据,通过 txHead 和 txTail 管理缓冲区的头尾位置。
DMA 传输:当缓冲区有数据且没有传输正在进行时,启动 DMA 传输。传输完成后,更新 txTail 并检查是否有更多数据需要传输。
避免丢帧:通过合理管理缓冲区和 DMA 传输,确保数据按顺序传输,减少丢帧风险。
优点:
高效传输:利用 DMA 驱动高效传输大量数据。
低延迟:环形缓冲区确保数据及时传输,减少传输延迟。
灵活管理:可以根据需要调整缓冲区大小和传输逻辑,适应不同的应用场景。
总结
要提高 HAL_UART_Transmit 函数的执行速度并尽量避免丢帧,可以采取以下策略:
使用非阻塞传输:
中断驱动模式。
DMA 驱动模式。
优化缓冲区管理:
使用环形缓冲区。
增大缓冲区容量。
提高 UART 配置效率:
增加波特率。
优化数据位、停止位和奇偶校验设置。
优化系统和任务管理:
提高 UART 中断优先级。
合理安排任务优先级(在 RTOS 环境下)。
采用双缓冲或更高效的传输函数:
实现双缓冲技术。
直接操作寄存器或使用轻量级驱动。
系统时钟优化:
提高系统时钟频率。
确保系统时钟配置稳定。
通过结合上述方法,可以显著提升 UART 传输性能,确保数据高效、稳定地传输,减少丢帧风险。
作者:stone_hefei