本作品以“智能宠物生活助手”为设计目标,设计以HT32F52352为主控芯片,搭载ESP32实现联网...
創建於:2025年3月12日
創建於:2025年3月12日
本作品以“智能宠物生活助手”为设计目标,设计以HT32F52352为主控芯片,搭载ESP32实现联网通信,配备称重传感器、温湿度传感器、水质传感器等多种模块,通过整合喂食、饮水、监控、语音及数据记录等功能。
现在为了能更好地运行HT32F52352的程序,我在HT中使用了freertos,HT32F52352要实现的功能有:1.读取称重传感器HX711的值。2.读取温度传感器DS18B20的值。3.读取液位传感器的值。4.读取人体传感器的值。5.通过接收串口来控制语音模块(JQ8900),来播放语音。6.通过称重传感器HX711的值用RGB灯光(普通IO口控制)显示食物余量状态。7.根据人体传感器的值(有人),就通过PWM来控制水泵。8.接收串口来控制电机转动,实现放猫粮。9.通过2.0寸LCD屏幕来显示称重传感器HX711的值、温度传感器DS18B20的值、液位传感器的值、人体传感器的值、简单表情等等。
现在我想知道在HT的freertos中,我应该创建几个任务,每个任务包含什么内容,来完成上述HT需要实现的功能呢?
在使用 FreeRTOS 进行多任务设计时,最核心的思路是将系统需要完成的功能进行合理地拆分,既要避免单个任务过度臃肿,又要注意任务之间的数据交互和优先级管理。下面给出一个较为常见、清晰的划分思路,供参考:
主要内容:
实现要点:
vTaskDelay()
或者定时器中断触发等方式来控制采集周期,例如 100~500 ms。主要内容:
实现要点:
如果工程比较小,也可以将“控制逻辑”和“通信处理”放到同一个任务。但当功能逐渐增多或者需要逻辑更清晰时,分开会更好。
主要内容:
实现要点:
PlaySound(uint8_t track_num)
之类。主要内容:
实现要点:
DrawWeight()
显示重量,DrawTemperature()
显示温度等。vTaskDelay(500 ~ 1000ms)
周期刷新一次即可。方案 A(五个任务):
方案 B(四个任务):
如果整个系统规模较小,可以合并部分任务,例如将“控制”和“传感器”合并到一个任务中,再将“通信”和“显示”合并到另一个任务中。但随着功能增多,不建议过度合并。
以上方案可以帮助你更好地在 HT32F52352 + FreeRTOS 环境下完成多功能智能宠物生活助手的设计与开发。祝项目顺利完成!
电机和水泵我还没做,这是我现在的代码,请问我现在的代码有什么改进的地方吗?
“/* Includes ------------------------------------------------------------------------------------------------*/
#include "ht32.h"
#include "ht32_board.h"
#include "ht32_board_config.h"
#include "delay.h"
#include "ControlMotor.h"
#include "CKCU.H"
#include "freertos.h"
#include "task.h"
#include "headfild.h"
static StackType_t g_pucStackOfSensor_Task[128];
static StackType_t g_pucStackOfControl_Task[128];
static StackType_t g_pucStackOfSerial_Control_Task[128];
static StackType_t g_pucStackOfLCD_Show_Task[128];
static StaticTask_t g_TCBofSensor_Task;
static StaticTask_t g_TCBofControl_Task;
static StaticTask_t g_TCBofSerial_Control_Task;
static StaticTask_t g_TCBofLCD_Show_Task;
//static TaskHandle_t xLightTaskHandle;
//static TaskHandle_t xLightTaskHandle2;
//u16 bode_flag;
void Sensor_Task(void argument)
{
while (1)
{
// TickType_t delay = pdMS_TO_TICKS(1000); / How many OS-Ticks need to wait for 1000ms. */
// RGB_3_LED_TEST();
textwater_work(); HX711_work(); DS18B20_work(); vTaskDelay(1000); }
}
void Control_Task(void *argument)
{
// const char msg[] = "Hello World\r\n";
while (1)
{
// USART_SendData(HT_USART1,value);
//// USART_Tx(msg, strlen(msg));
// printf("水位: %d\r\n",value);
////// LED_On();
//// vTaskDelay(1000);
//// LED_Off();
textbody_hw();//人体感应 亮灯、水泵 }
}
void Serial_Control_Task(void *argument)
{
while (1)
{
if(Serial_Control_flag == 1)
{
JQ8x00_Command_Data(SetVolume,30); //设置音量,最大为30
JQ8x00_Command_Data(AppointTrack,1); //播放曲目1
}
else if(Serial_Control_flag == 2)
{
JQ8x00_Command_Data(SetVolume,30); //设置音量,最大为30
JQ8x00_Command_Data(AppointTrack,2); //播放曲目1
}
else if(Serial_Control_flag == 3)
{
RGB_3_LED_GREEN();
}
else
{
text} }
}
void LCD_Show_Task(void *argument)
{
while (1)
{
textLCD_ShowChinese(0,0,"中景园电子",RED,WHITE,32,0); LCD_ShowString(0,40,"LCD_W:",RED,WHITE,16,0); LCD_ShowIntNum(48,40,LCD_W,3,RED,WHITE,16); LCD_ShowString(80,40,"LCD_H:",RED,WHITE,16,0); LCD_ShowIntNum(128,40,LCD_H,3,RED,WHITE,16); LCD_ShowString(80,40,"LCD_H:",RED,WHITE,16,0); LCD_ShowString(0,70,"T:",RED,WHITE,16,0); LCD_ShowFloatNum1(128,70,temperature,5,RED,WHITE,16); LCD_ShowString(0,90,"water:",RED,WHITE,16,0); LCD_ShowIntNum(128,90,value,4,RED,WHITE,16); LCD_ShowString(0,130,"weight:",RED,WHITE,16,0); LCD_ShowIntNum(128,130,weight,5,RED,WHITE,16); if(bode_flag == 1) { LCD_ShowString(0,110,"body_hw:",RED,WHITE,16,0); LCD_ShowIntNum(128,110,bode_flag,4,RED,WHITE,16); } else { LCD_ShowString(0,110,"body_hw:",RED,WHITE,16,0); LCD_ShowIntNum(128,110,bode_flag,4,RED,WHITE,16); } }
}
int main(void)
{
// float t=0;
CKCU_init();
// delay_init();
// RETARGET_Configuration();
// Motor_GPIO_init();
textRGB_3_LED_Init(); WATER_Init(); USART_Configuration(); USART0_Configuration(); BODY_HW_Init(); JQ8x00_Init();//语音模块初始化 while(DS18B20_Init()) //DS18B20初始化 { printf("DS18B20 Error"); delay_us2(2000); } LCD_Init(); LCD_Fill(0,0,LCD_W,LCD_H,WHITE); HX711_GPIO_Init(); //xTaskCreate(Led_Test2,"LightTask2", 128,NULL,24,&g_TCBofLightTask); xTaskCreateStatic(Sensor_Task, "Sensor_Task", 128, NULL, tskIDLE_PRIORITY, g_pucStackOfSensor_Task, &g_TCBofSensor_Task); xTaskCreateStatic(Control_Task, "Control_Task", 128, NULL, tskIDLE_PRIORITY, g_pucStackOfControl_Task, &g_TCBofControl_Task); xTaskCreateStatic(Serial_Control_Task, "Serial_Control_Task", 128, NULL, tskIDLE_PRIORITY, g_pucStackOfSerial_Control_Task, &g_TCBofSerial_Control_Task); xTaskCreateStatic(LCD_Show_Task, "LCD_Show_Task", 128, NULL, tskIDLE_PRIORITY, g_pucStackOfLCD_Show_Task, &g_TCBofLCD_Show_Task); vTaskStartScheduler(); //开启任务调度
// vTaskDelay(500); //等待模块稳定
// JQ8x00_Command_Data(SetVolume,30); //设置音量,最大为30
textwhile(1) { }
}
void delay_us2(u32 nus)
{
u32 ticks;
u32 told,tnow,reload,tcnt=0;
if((0x0001&(SysTick->CTRL)) ==0) //定时器未工作
vPortSetupTimerInterrupt(); //初始化定时器
textreload = SysTick->LOAD; //获取重装载寄存器值 ticks = nus * (SystemCoreClock / 1000000); //计数时间值 vTaskSuspendAll();//阻止OS调度,防止打断us延时 told=SysTick->VAL; //获取当前数值寄存器值(开始时数值) while(1) { tnow=SysTick->VAL; //获取当前数值寄存器值 if(tnow!=told) //当前值不等于开始值说明已在计数 { if(tnow<told) //当前值小于开始数值,说明未计到0 tcnt+=told-tnow; //计数值=开始值-当前值 else //当前值大于开始数值,说明已计到0并重新计数 tcnt+=reload-tnow+told; //计数值=重装载值-当前值+开始值 ( //已从开始值计到0) told=tnow; //更新开始值 if(tcnt>=ticks)break; //时间超过/等于要延迟的时间,则退出. } } xTaskResumeAll(); //恢复OS调度
}
//SystemCoreClock为系统时钟(system_stmf4xx.c中),通常选择该时钟作为
//systick定时器时钟,根据具体情况更改
void delay_ms2(u32 ms)
{
u16 i;
for(i = 0;i<=ms ;i++)
{
delay_us2(1000);//1ms
}
}
void vApplicationMallocFailedHook(void)
{
/* vApplicationMallocFailedHook() will only be called if
configUSE_MALLOC_FAILED_HOOK is set to 1 in FreeRTOSConfig.h. It is a hook
function that will get called if a call to pvPortMalloc() fails.
pvPortMalloc() is called internally by the kernel whenever a task, queue,
timer or semaphore is created. It is also called by various parts of the
demo application. If heap_1.c or heap_2.c are used, then the size of the
heap available to pvPortMalloc() is defined by configTOTAL_HEAP_SIZE in
FreeRTOSConfig.h, and the xPortGetFreeHeapSize() API function can be used
to query the size of free heap space that remains (although it does not
provide information on how the remaining heap might be fragmented). */
taskDISABLE_INTERRUPTS();
for (;; );
}
void vApplicationStackOverflowHook(TaskHandle_t pxTask, char pcTaskName)
{
/ Run time stack overflow checking is performed if
configCHECK_FOR_STACK_OVERFLOW is defined to 1 or 2. This hook
function is called if a stack overflow is detected. */
taskDISABLE_INTERRUPTS();
for (;; );
}
#if (HT32_LIB_DEBUG == 1)
/*******************************************************************************************************//
while (1)
{
}
}
#endif
”
下面结合你目前的代码,从 FreeRTOS 任务划分、任务之间的配合以及一些常见的编程习惯出发,给出一些改进建议,供你参考。
vTaskDelay
问题现象
目前 Control_Task
、Serial_Control_Task
、LCD_Show_Task
这三个任务在 while(1)
循环里都没有任何形式的延时或阻塞调用。这样会导致它们在优先级相同的情况下争抢 CPU 时间,且因为无限循环占用 CPU,Idle 任务几乎得不到运行机会。
改进建议
vTaskDelay()
vTaskDelay(pdMS_TO_TICKS(500));
或者 vTaskDelay(100/portTICK_RATE_MS);
等延时,这样可以让出 CPU 给其他任务。
Control_Task
可以每 100~500 ms 检查一次传感器状态或更新逻辑。Serial_Control_Task
如果没有新的串口指令要处理,也可以让出 CPU。LCD_Show_Task
不需要高刷新率,可以每隔 500 ms ~ 1000 ms 刷新一次屏幕。问题现象
你在创建任务时都使用了 tskIDLE_PRIORITY
(在 FreeRTOS 中一般是 0 级)。这意味着如果这些任务都没有阻塞,可能会完全占用系统资源,导致 Idle 任务完全无法执行,进而一些 FreeRTOS 的钩子函数(如空闲任务中进行的清理等)也无法正常工作。
改进建议
tskIDLE_PRIORITY
,例如简单的 LED 指示或一些低优先级显示。一个常见的做法是:
Sensor_Task
(传感器采集):优先级 2Control_Task
(控制逻辑):优先级 2Serial_Control_Task
(串口收发/语音):优先级 1LCD_Show_Task
(显示):优先级 1目前的 4 个任务基本逻辑清晰:
这样的划分思路可以继续沿用,只是需要注意它们之间的数据同步和共享方式。
目前看你在不同任务间使用了全局变量(如 weight
, temperature
, value
, bode_flag
, Serial_Control_flag
等)。如果数据量少、且对实时性要求不高,可以先这样做。但在多任务环境下,需要保证共享变量的读写安全。
128
(即 128 * 4 字节)作为每个任务的栈深度,这在很多情况下可能比较紧张,尤其当函数中需要开辟较大局部变量、或者调用一些库函数时。uxTaskGetStackHighWaterMark()
或者启用 FreeRTOS 的堆栈检测功能,观察实际剩余多少栈空间。如果过小,可能引发栈溢出。pvPortMalloc()
或者静态分配在全局,而不是在任务栈中直接开大的局部数组。Sensor_Task
)cvoid Sensor_Task(void *argument) { while (1) { water_work(); // 读取或更新水位值 HX711_work(); // 读取重量 DS18B20_work();// 读取温度 // 建议:如果要保留1秒周期,可以把 vTaskDelay 放这里 vTaskDelay(pdMS_TO_TICKS(1000)); } }
vTaskDelay(1000)
,或者写成 vTaskDelay(pdMS_TO_TICKS(1000));
,这样表达更直观。Control_Task
)cvoid Control_Task(void *argument) { while (1) { body_hw(); // 人体感应 -> 亮灯、水泵 // 建议这里至少做一个短暂的延时,如: vTaskDelay(pdMS_TO_TICKS(200)); } }
body_hw()
函数本身就包含了较长的阻塞,不再需要多余的延时;但如果它只是一个很快的 GPIO 读取,那么最好给 vTaskDelay()
一个延时,避免死循环占用 CPU。Serial_Control_Task
)cvoid Serial_Control_Task(void *argument) { while (1) { if(Serial_Control_flag == 1) { JQ8x00_Command_Data(SetVolume,30); JQ8x00_Command_Data(AppointTrack,1); } else if(Serial_Control_flag == 2) { JQ8x00_Command_Data(SetVolume,30); JQ8x00_Command_Data(AppointTrack,2); } else if(Serial_Control_flag == 3) { RGB_3_LED_GREEN(); } else { // ... } // 建议每次处理完后延时一下 vTaskDelay(pdMS_TO_TICKS(100)); } }
Serial_Control_flag
是在其它地方通过中断或者其它任务修改的,而这里只是轮询,它可能会出现“反应不够及时”的问题。如果需要更及时的处理,可以用队列或事件来通知这个任务,而不是简单轮询。LCD_Show_Task
)cvoid LCD_Show_Task(void *argument) { while (1) { LCD_ShowChinese(0, 0, "中景园电子", RED, WHITE, 32, 0); LCD_ShowString(0, 40, "LCD_W:", RED, WHITE, 16, 0); LCD_ShowIntNum(48, 40, LCD_W, 3, RED, WHITE, 16); ... // 省略部分重复显示逻辑 // ... vTaskDelay(pdMS_TO_TICKS(500)); // 每500ms刷新一次屏幕 } }
while
内代码过于冗长。printf
的地方要注意,某些芯片的库或打印函数可能对堆栈有较大消耗,也要小心对实时性的影响。Sensor_Init()
, Motor_Init()
, Pump_Init()
,在 main()
中调用,结构更清晰。Control_Task
或者单独做成“电机控制任务”,看需要来拆分。tskIDLE_PRIORITY
,会导致调度竞争、CPU 占用过高等问题。vTaskDelay()
或者事件触发的阻塞,避免空转。以上就是针对你目前代码的一些改进建议,希望对你后续完善项目有所帮助。祝项目开发顺利!
void UART0_IRQHandler(void)
{
// Tx, move data from buffer to UART FIFO
if ((HT_UART0->SR) & USART_FLAG_TXC)
{
if (!txd_comm0.cnt)
{
USART_IntConfig(HT_UART0, USART_INT_TXC, DISABLE);
}
else
{
u16 i;
for (i = 0; i < UART0_FIFO_LEN; i++) //have FIFO?
{
USART_SendData(HT_UART0, txd_comm0.buffer[txd_comm0.read_pt]);
txd_comm0.read_pt = (txd_comm0.read_pt + 1) % UART0_BUF_SIZE;
txd_comm0.cnt--;
if (!txd_comm0.cnt)
break;
}
}
}
// Rx, move data from UART FIFO to buffer
if ((HT_UART0->SR) & USART_FLAG_RXDR )
{
rxd_comm0.buffer[rxd_comm0.write_pt] = USART_ReceiveData(HT_UART0);
textrxd_comm0.write_pt = (rxd_comm0.write_pt + 1) % UART0_BUF_SIZE; rxd_comm0.cnt++;
}
}
解释代码
下面对 UART0_IRQHandler()
中的主要逻辑进行逐行解释,帮助理解该中断服务函数的工作流程。总体而言,这是一个UART0 的收发中断服务,利用**环形缓冲区(ring buffer)**来管理发送和接收的数据。
cvoid UART0_IRQHandler(void) { // ... }
c// Tx, move data from buffer to UART FIFO if ((HT_UART0->SR) & USART_FLAG_TXC) { if (!txd_comm0.cnt) { // ... } else { // ... } }
if ((HT_UART0->SR) & USART_FLAG_TXC)
:SR
是否含有 USART_FLAG_TXC
标志,表示当前发送 FIFO 可以继续发送新的数据或上一帧发送完成,需要填写新数据到 FIFO 中。cif (!txd_comm0.cnt) { USART_IntConfig(HT_UART0, USART_INT_TXC, DISABLE); }
txd_comm0.cnt
:代表发送缓冲区中剩余待发送字节数。txd_comm0.cnt == 0
,说明发送缓冲区已经没有数据需要发送了,于是就关闭发送完成中断,防止频繁地进入中断浪费资源。celse { u16 i; for (i = 0; i < UART0_FIFO_LEN; i++) //have FIFO? { USART_SendData(HT_UART0, txd_comm0.buffer[txd_comm0.read_pt]); txd_comm0.read_pt = (txd_comm0.read_pt + 1) % UART0_BUF_SIZE; txd_comm0.cnt--; if (!txd_comm0.cnt) break; } }
for (i = 0; i < UART0_FIFO_LEN; i++)
:一次最多往硬件 FIFO 里填 UART0_FIFO_LEN
个字节(这是对 UART0 硬件 FIFO 的深度或一次想写入的字节数做限制)。USART_SendData(HT_UART0, txd_comm0.buffer[txd_comm0.read_pt]);
:从软件发送缓冲区读出一个字节并发送给 UART0。txd_comm0.read_pt = (txd_comm0.read_pt + 1) % UART0_BUF_SIZE;
:更新发送缓冲区的读指针(环形缓冲区典型做法:读指针到末尾后从 0 重新开始)。txd_comm0.cnt--;
:当前发送缓冲区剩余待发送字节数减少 1。if (!txd_comm0.cnt) break;
:如果已经没有剩余数据可以发送,就立即跳出循环,后面不再填数据。c// Rx, move data from UART FIFO to buffer if ((HT_UART0->SR) & USART_FLAG_RXDR ) { rxd_comm0.buffer[rxd_comm0.write_pt] = USART_ReceiveData(HT_UART0); rxd_comm0.write_pt = (rxd_comm0.write_pt + 1) % UART0_BUF_SIZE; rxd_comm0.cnt++; }
if ((HT_UART0->SR) & USART_FLAG_RXDR )
:SR
是否含有 USART_FLAG_RXDR
标志,表示硬件 UART FIFO 中有新的接收数据可读取。rxd_comm0.buffer[rxd_comm0.write_pt] = USART_ReceiveData(HT_UART0);
USART_ReceiveData(HT_UART0)
),放入软件接收缓冲区的写指针位置。rxd_comm0.write_pt = (rxd_comm0.write_pt + 1) % UART0_BUF_SIZE;
rxd_comm0.cnt++;
UART0_IRQHandler()
后,先查看发送完成中断标志 (TXC):
整体上,这是一个典型的 UART中断收发环形缓冲实现方式:
- 发送:将要发送的数据先存到发送缓冲,开中断后由 ISR 每次填硬件 FIFO,减少了主循环里阻塞等待的时间。
- 接收:中断里一旦有数据到来,就及时保存到接收缓冲区,以免丢失数据。主循环或其他任务可随时从缓冲区读取。
UART0_BUF_SIZE
)以及 FIFO 大小(UART0_FIFO_LEN
),以免溢出或阻塞。这就是 UART0_IRQHandler()
的工作原理和各个关键步骤的解释。
如果我在HT中使用了freertos,那么原先在HT中的串口中断还能正常使用吗?
一般来说,在移植或使用 FreeRTOS 后,原先的硬件中断(包括串口中断)依然可以正常使用,但需要注意以下几点:
中断优先级配置
configMAX_SYSCALL_INTERRUPT_PRIORITY
。xQueueSendFromISR()
、xSemaphoreGiveFromISR()
等),那么对中断优先级的限制较少,可以按之前的方法配置。如果你需要在中断中与任务通信,则需要用 FromISR
系列的 FreeRTOS API,且中断优先级要符合 FreeRTOS 要求。中断函数的命名和向量表
UARTx_IRQHandler
函数按照原有命名来编写。UARTx_IRQHandler
的声明)。ISR 中尽量避免复杂操作
vTaskDelay()
, vTaskSuspend()
),也不要做大量计算。xQueueSendFromISR()
或者 xSemaphoreGiveFromISR()
来通知任务。任务里再进行更复杂的处理。与 FreeRTOS 任务的配合
注意堆栈和优先级
configMAX_SYSCALL_INTERRUPT_PRIORITY
),并遵守 “FromISR” 的调用规范。xxxFromISR()
),并配置好中断优先级;这样既能保留原有硬件中断的优点,又能与 FreeRTOS 的任务调度有机结合。
现在在freertos中,我把“data = USART_ReceiveData(COM1_PORT);//读取接收到的数据
USART_SendData(HT_USART1,data);
textif(data == 1) { Serial_Control_flag = 1; data = 0; } else if(data == 2) { Serial_Control_flag = 2; data = 0; } else if(data == 3) { Serial_Control_flag = 3; data = 0; } else { Serial_Control_flag = 0; data = 0; } vTaskDelay(200); }”放在一个线程里,现在我通过上位机给HT只发了一次“1”后,它会反复data =1;如何才能我发一次,他就只收到一次
当你把 data = USART_ReceiveData(COM1_PORT);
直接放在任务里循环调用时,如果没有先检查“是否有新的接收数据可读”,就会导致重复读到上一次的数据。也就是说,如果硬件并没有准备新的字节,而你却一口气调用 USART_ReceiveData()
,大多数情况下会返回上一次读过的内容或无效数据,造成“我只发了一次 ‘1’,却在循环中不停检测到 data == 1”。
在非中断方式下做串口轮询读取时,需要先判断硬件寄存器里是否有新数据,再去 USART_ReceiveData()
。如果没有新的数据,则跳过读取,避免重复拿到旧值。
下面给出几种常见的解决方法:
如果你想用轮询方式(在任务里不断查询硬件寄存器),示例代码可以改成:
cvoid SerialPollTask(void *argument) { uint16_t data; for(;;) { // 1) 判断是否有新数据到达 if (USART_GetFlagStatus(COM1_PORT, USART_FLAG_RXDR) != RESET) { // 2) 确定有数据以后再读取 data = USART_ReceiveData(COM1_PORT); USART_SendData(HT_USART1, data); // 简单逻辑判断 if (data == 1) { Serial_Control_flag = 1; } else if (data == 2) { Serial_Control_flag = 2; } else if (data == 3) { Serial_Control_flag = 3; } else { Serial_Control_flag = 0; } } // 给任务一个延时,避免一直空转占满CPU vTaskDelay(pdMS_TO_TICKS(200)); } }
USART_GetFlagStatus(COM1_PORT, USART_FLAG_RXDR)
用来检查“接收缓冲区是否准备好新数据”;USART_ReceiveData()
,这样不会一直拿到旧值。100~200ms
的轮询完全可以满足简单控制需求。如果你的项目后期还会处理更多串口数据,或者对接收及时性要求高,强烈推荐用中断方式将数据保存到环形缓冲区(或用 FreeRTOS 队列),再在任务中非阻塞地读取,这样效率更好,也不会漏数据。
大体流程是:
UARTx_IRQHandler()
中检测到新数据到来 (USART_FLAG_RXDR
),就 ReceiveData()
,存到一个全局环形缓冲数组或 FreeRTOS 队列里。vTaskDelay
一段时间,或者阻塞等待队列信号。示例(简略伪代码):
c// ------------ 中断ISR ------------ void UARTx_IRQHandler(void) { if (USART_GetFlagStatus(UARTx, USART_FLAG_RXDR) != RESET) { uint8_t rxd = USART_ReceiveData(UARTx); // 放进环形缓冲,或用 xQueueSendFromISR() ringBufferPut(&rxRing, rxd); } } // ------------ 任务中 ------------ void SerialTask(void *argument) { uint8_t data; for (;;) { // 如果环形缓冲中有数据,就取出来 if (ringBufferAvailable(&rxRing) > 0) { data = ringBufferGet(&rxRing); // ... 进行你的判断逻辑 ... } vTaskDelay(pdMS_TO_TICKS(100)); } }
USART_ReceiveData()
。Serial_Control_flag = 1
。简而言之:你在读一次后没有确认是否真的有新数据就再次读,结果读到了重复值。
if (USART_GetFlagStatus(...) != RESET)
判断。使用轮询方式,为什么每当我换数字时,会重复上一次的数字,第二次才是本数字。比如我先发1,他收到1.我再发2,它还是收到1,再发一次2,它才收到2.请解决。中断用不了
在 FreeRTOS 下使用轮询方式读取串口时,如果没有先确认硬件里确实有新数据就直接 USART_ReceiveData()
,往往会出现“读取到旧数据”或“要发送两次才能真正收到新值”的现象。这也是你所描述的“我先发 1,它能收到 1;再发 2,它还在读到 1;只有第二次发 2 才读到 2”的根本原因。
以下给出原因分析和常见解决方案,帮助你一次发送数据就能正确读取。
没有判断“是否有新数据”就读
USART_ReceiveData()
而不先检查 USART_FLAG_RXDR
(接收缓冲区非空标志),很多厂商的驱动会返回上一次残留的数据,导致反复读到旧值。只有在再次发送时才触发新的数据
USART_ReceiveData()
才读到更新后的值。这就导致你需要发送两次才能“追上”真正想要读的新字符。可能存在硬件 FIFO 或者多字节一次性到达
如果确实无法使用中断,那么最简便的解决方法就是:** 只有在检测到硬件寄存器里有数据时,才调用 USART_ReceiveData()
**,并且如果一次可能有多个字节,就在 while
里连续读。示例:
cvoid SerialPollTask(void *argument) { uint16_t data; for (;;) { // 连续读到 FIFO 为空为止 while (USART_GetFlagStatus(COM1_PORT, USART_FLAG_RXDR) != RESET) { // 1) 读取一个字节 data = USART_ReceiveData(COM1_PORT); // 2) 此处可做相应处理,比如打印回显 USART_SendData(HT_USART1, data); // 3) 根据 data 做逻辑判断 if (data == 1) { Serial_Control_flag = 1; } else if (data == 2) { Serial_Control_flag = 2; } else if (data == 3) { Serial_Control_flag = 3; } else { Serial_Control_flag = 0; } } // 让出 CPU,避免空转 vTaskDelay(pdMS_TO_TICKS(100)); } }
while (USART_GetFlagStatus(...) != RESET)
读走所有当前 FIFO 中的字节。如果只写 if(...)
那么若有多字节进来,下一次轮询才读到剩余字节;如果一次发送 2 个字节,就需要 2 次循环才能完整读取。如果硬件或软件条件允许(虽然你说“中断用不了”,但还是列一下更优方法),最好用UART 中断把数据丢到环形缓冲区,再在任务中做非阻塞读取。比如:
中断服务函数 (ISR)
cvoid UART0_IRQHandler(void) { // 判断是否有新数据 if (USART_GetFlagStatus(UART0, USART_FLAG_RXDR) != RESET) { uint8_t rxd = USART_ReceiveData(UART0); // 存入环形缓冲区 ringBufferPut(&uart0RxRing, rxd); } }
任务中读取
cvoid SerialTask(void *argument) { for (;;) { // 当环形缓冲有数据,则读出来并处理 while (ringBufferAvailable(&uart0RxRing) > 0) { uint8_t data = ringBufferGet(&uart0RxRing); // ...做你的逻辑判断... } vTaskDelay(pdMS_TO_TICKS(50)); } }
\r\n
,就会多出两个字符 0x0D、0x0A。要留意你的判断逻辑中如何处理这些字符。USART_ReceiveData()
RXDR
标志就读,会拿到旧值或无效值。while (USART_GetFlagStatus(...) != RESET)
再读数据。USART_ReceiveData()
,导致读到上次残留数据或硬件 FIFO 中的上一字节。while
判断 USART_FLAG_RXDR
是否置位,再执行 USART_ReceiveData()
。同时,每次读完都要写入变量、更新逻辑,并且循环读取直到 FIFO 空。这样能够一次性取走所有新字节,不会产生“要发两次才能读到”的现象。只要你按照上述方法改写轮询代码,就可以解决“换数字时会先重复上一次数字”的问题,做到“发一次就立即读到新值”。
轮询还是有一样的问题,而且第一次发没反应,第二次发才有反应,请解决。不能用中断
在纯轮询方式下出现“第一次发送无响应、第二次发送才收到上一次的数据”这一典型现象,常见原因与解决思路如下。最核心的问题通常是:没有正确、完整地从硬件中读出新数据(或者读到了残留的旧数据),再加上ASCII 与数字值的混淆、**没有读走多余字节(如换行、回车)**等,也会造成“要发送两次才能正确收到”的假象。
下面从最常见的几个角度来分析,并给出解决办法。
1
并发送时,绝大多数情况下实际发送的是 ASCII 字符 '1'
,其十进制值是 49 (0x31)。if (data == 1)
,那就意味着你想检测到一个字节 0x01(控制字符),并不是 '1'
。所以就会“没反应”或“要发两次”都对不上。可以临时加一行打印代码:
cprintf("Received data = %d (0x%02X)\r\n", data, data);
看一下你发 1
时到底收到什么数值。如果你看到的是 49 (0x31)
, 那就说明你应该在代码里写:
cif (data == '1') // 或 data == 0x31 { // ... }
同理,对于 2
就是 '2'
= 0x32 (十进制 50)。如果你想用纯数字 1、2、3,就需要发送真正的 0x01、0x02、0x03(一般终端工具不这么做)。
\r
(0x0D) 或 \n
(0x0A)。'1'
+ '\r'
+ '\n'
(具体看终端配置)。如果你没有一次性读走,下一次轮询时就会读到上一次残留的 '\r'
或 '\n'
。解决:在你轮询接收时,把所有字节都读完(见下文的 “循环读走 FIFO”),并且如果遇到 '\r'
/'\n'
,你可以选择忽略它们或用它们做分隔符。
如果你无法使用中断,就需要在任务里每隔一段时间轮询查看硬件 UART 是否有新的字节可以读。最安全的做法是:
'1'
, '2'
, '3'
…);vTaskDelay()
让出 CPU。cvoid SerialPollTask(void *argument) { uint16_t data; for (;;) { // 连续读,直到FIFO为空 while (USART_GetFlagStatus(COM1_PORT, USART_FLAG_RXDR) != RESET) { // 读取当前字节 data = USART_ReceiveData(COM1_PORT); // (可选)打印看看自己到底收到了什么 // printf("Got: %d (0x%02X)\r\n", data, data); // 如果你在终端发送的是ASCII字符,则这里要写成 data == '1' / '2' / '3' if (data == '1') { Serial_Control_flag = 1; } else if (data == '2') { Serial_Control_flag = 2; } else if (data == '3') { Serial_Control_flag = 3; } else { Serial_Control_flag = 0; } } // 适度延时 vTaskDelay(pdMS_TO_TICKS(50)); } }
while (USART_GetFlagStatus(...RXDR) != RESET)
:
保证只要 FIFO 中还有字节,就继续 ReceiveData()
读走。否则留在FIFO,下次才读,容易造成 “旧数据混着下一次新数据”。
ASCII 判断:
如果你在终端敲的是 '1'
、'2'
、'3'
,就要用 if (data == '1')
,而不是 if (data == 1)
。
调试打印(可以先加上看看实际收到了哪些字节):
cprintf("Got data = %d (0x%02X)\r\n", data, data);
如果看到 49 (0x31)
, 那就是 '1'
;如果看到 10 (0x0A)
, 那就是换行。这样能搞清楚为什么会出现“第一次没反应,第二次才看到数据”的情况。
如果你已经做了**“循环读 FIFO + ASCII 判断”**,却依旧出现要发两次才行,通常还有以下可能:
USART_GetFlagStatus()
用错了标志位。
USART_FLAG_RXDR
表示“接收数据就绪”,或者是具体厂商命名如 USART_FLAG_RXNE
。if (data == 1)
。
if (data == 1)
分支,必须重复发送多次时才偶然出现数据残留、让你误以为要发两次才能匹配。RxCmd(ENABLE)
或者波特率没对上,也可能导致第一个字节丢失,第二次才同步上。if (data == 1)
写成检测 ASCII '1'
。实际终端发出的 '1'
是 0x31(十进制49),不等于 1(0x01)。因此第一次“没反应”。\r
, \n
,会额外读到 0x0D, 0x0A。一定要在轮询里多次读走,或者忽略它们。只要你按照上面示例,循环读取 + 正确判断 ASCII 字符,并确保硬件和波特率都正常,基本就能做到“发一次就能立刻正确接收”。
能否学习一下“void uart1_rev_str(void)
{
while(1)
{
while(!(USART1->SR&(1<< 5)))
{
;
}
Uart_rev.rev_buff[Uart_rev.rev_len++] = USART1->DR ;//读数据
text//判断是否字符的结束字符* if( Uart_rev.rev_buff[Uart_rev.rev_len-1] == '*') { Uart_rev.rev_buff[Uart_rev.rev_len-1] ='\0'; //构成一个字符串 Uart_rev.rev_len = 0; //接受长度清空方便下一次接受数据 break;//数据传输完毕,退出 } }
}”来完善上述的串口问题
你提供的函数 uart1_rev_str()
展示了一种阻塞式读取串口直到遇到结束符 '*'
才返回的示例。大概流程是:
等待数据就绪:
cwhile(!(USART1->SR & (1 << 5))) { }
这一句会一直“卡”在这里,直到串口硬件表示有新的数据可读(即 RXNE
/ RXDR
标志位置位)。
读一个字节:
cUart_rev.rev_buff[Uart_rev.rev_len++] = USART1->DR;
直接从 DR
里获取数据,存到 rev_buff
缓冲区里。
判断是否为结束符 '*'
:
'*'
就把它替换为 '\0'
,使之成为标准 C 字符串,并将 rev_len
置 0,最后 break
跳出读取循环。'*'
,就继续循环等待下一个字节。这是一个非常线性的阻塞式读取:** 函数不会返回**,一直堵在内部,直到收到结束符
'*'
。在无操作系统的小项目中,这种做法简洁直观。不过在 FreeRTOS 或多任务环境下,如果你这样写,就会独占 CPU,别的任务无法执行,系统的多任务特性也难以发挥。
'*'
才拼成一帧”的逻辑你可以把这套逻辑搬到 FreeRTOS 任务中,但不能再使用 while (!(USART1->SR & (1<<5))) { }
这种硬等待。
伪代码示例:
cvoid UartReceiveTask(void *argument) { for (;;) { // 1) 如果硬件里有数据,就一直读,直到为空 while (USART_GetFlagStatus(UART1, USART_FLAG_RXDR) != RESET) { char c = (char)USART_ReceiveData(UART1); // 2) 存入缓冲区 Uart_rev.rev_buff[Uart_rev.rev_len++] = c; // 如果超过最大长度,要注意处理越界 if (Uart_rev.rev_len >= REV_BUFF_MAX_SIZE) { Uart_rev.rev_len = 0; // 也可以丢弃或清空缓冲,看你需求 } // 3) 检查是否是结束符 '*' if (c == '*') { // 覆盖 '*', 变成一个标准字符串结尾 Uart_rev.rev_buff[Uart_rev.rev_len - 1] = '\0'; // TODO: 这里可以把这条完整指令交给其他任务处理,或者直接解析 // 比如 xQueueSend(...) 发送给解析队列 // 清空重置,为接收下一帧做准备 Uart_rev.rev_len = 0; } } // 4) 让出CPU时间,避免空转 vTaskDelay(pdMS_TO_TICKS(50)); } }
while (RXDR != RESET)
不停 USART_ReceiveData(...)
。这样如果一次性到达多个字节,你都能一口气读完,避免“一次只能读一个字节,下次再轮询时才读到后续字节”。'*'
,就认为一帧消息结束,把 '*'
替换成 '\0'
。rev_len = 0
重置缓冲。vTaskDelay(50)
或者更小的延时,给其他任务运行机会。Uart_rev.rev_len
不能超过 rev_buff
的最大容量,否则会造成数组越界。需要加判断或自动环回。这样就实现了**“非阻塞、带结束符”** 的串口接收。在此过程中,你的任务并不会在某行硬等待下去,而是**“不停检查 + 延时 + 再检查”**,如果真的需要更高的实时性,就把 vTaskDelay(50)
改得小一点,比如 5ms、10ms。
uart1_rev_str()
是一个阻塞式函数while (!(USART1->SR & (1<<5)))
,必须等下一字节到达才能继续。vTaskDelay
;'*'
再返回” 的同步函数有些场景下,你可能想保留“调用一个函数,直到获取到整条指令为止才返回”的写法。例如,在单线程命令行解析场景里,想在同一个函数里拿到完整指令就地解析。这种场景可以用:
vTaskDelay(1)
)示例(不一定是最佳实践,但能兼顾“同步逻辑”和“RTOS多任务”):
c// 在FreeRTOS的环境下实现一个“阻塞等待直到遇到*”或超时返回”的函数 BaseType_t uart1_recv_until_star(char *outBuf, size_t bufSize, TickType_t timeout) { size_t idx = 0; TickType_t startTick = xTaskGetTickCount(); // 轮询到超时为止 while ((xTaskGetTickCount() - startTick) < timeout) { // 尝试读完当前FIFO while (USART_GetFlagStatus(UART1, USART_FLAG_RXDR) != RESET) { char c = (char)USART_ReceiveData(UART1); if (idx < (bufSize - 1)) // -1 留一个位置给 \0 { outBuf[idx++] = c; } if (c == '*') { // 用 '\0' 替换 if (idx > 0) idx--; outBuf[idx] = '\0'; return pdPASS; // 成功 } } // 每次空转后让出CPU vTaskDelay(1); } // 超时仍未遇到 '*' outBuf[idx] = '\0'; return pdFAIL; }
逻辑:
timeout
,超过这个 Tick 计数还没读到 '*'
就返回失败;'*'
就返回;vTaskDelay(1)
让出 CPU 给其它任务,避免长时间阻塞;'*'
就返回,要么超时就返回。优点:
if (uart1_recv_until_star(..., 2000) == pdPASS) {...}
;vTaskDelay(1)
,让出CPU给别的任务。缺点:
timeout
长度的时间。uart1_rev_str()
方法是典型的阻塞式读取,在无 RTOS 或简单场景下很直观,但在多任务环境下会占用 CPU。vTaskDelay(...)
让出 CPU;'*'
才返回”的同步函数,可以在内部通过小步轮询 + vTaskDelay(1)
来避免死等,这样至少不会卡死别的任务。这样就能将“遇到结束符号 '*'
才结束组包”的思路灵活运用到 FreeRTOS 多任务场景中,并避免“第一次要发两次才能读到”的问题。
为什么串口接收第一个数据在第二次接收时出现,第二个数据在第三次出现
在串口轮询或简单阻塞读取的场景里,“第一个数据要到第二次读取时才出现,第二个数据要到第三次读取时才出现” 这种**“数据滞后一帧”的现象非常常见,一般是由于读取逻辑和硬件接收时序不同步**导致的。下面列出几种最常见的原因,以及对应的排查与解决思路。
举一个伪代码的例子来说明这个典型错误写法:
cuint8_t read_data, last_data; // 在循环或函数中: while (1) { // 步骤1:read_data = last_data; // 先把“上一次保留的值”取出来当本次结果 // 步骤2:last_data = USART_ReceiveData(); // 这里才真正去读当前硬件寄存器 // 这样做会导致 read_data 总是“滞后”一次 // ... Delay(...); }
解决:直接 read_data = USART_ReceiveData()
并立即使用当前读到的 read_data
。不要在“下一次循环”才去取出“上一次存下的值”。
如果芯片的 UART 有接收 FIFO(即硬件寄存器可以缓存多字节),或者你一次发送了多字节,而你的代码每次循环只读一次,则会产生数据被分多次循环读走的延迟。有时甚至会出现“上次循环没读完,还剩几个字节等待下次循环才被读出”。
典型表现:
'1'
,留着 '2'
还在 FIFO 里;直到下次循环才读 '2'
,导致看似要“发两次”才能取到数据。正确做法:在每次轮询时,写一个**while (有新数据)
就 USART_ReceiveData()
**的循环,把当前硬件 FIFO 里所有字节都读出来。比如:
cwhile (USART_GetFlagStatus(UARTn, USART_FLAG_RXDR) != RESET) { uint8_t data = USART_ReceiveData(UARTn); // 立即处理或存进缓冲 }
这样就不会留到下一次循环再读。
data == 1
)这是一个相当普遍的新手陷阱:在终端里发送字符 '1'
,它的 ASCII 码是 0x31(十进制 49),而你在 C 代码里判断 if (data == 1)
自然永远不成立。你可能又在下一次发送 '2'
时,恰好才读到 '1'
的残留……
排查方法:
printf("data = %d (0x%02X)\n", data, data);
看看到底读到多少。49 (0x31)
就应该写 if (data == '1')
或 if (data == 0x31)
。1
, 2
, 3
表示,就需要真正发送二进制 0x01, 0x02, 0x03,这在大多数串口调试工具里并不常见。\r
、\n
,没有处理导致后续滞后有些串口助手在你发送“1”时,会自动再发送回车/换行 (\r\n
)。如果你的代码只是只读一次就不继续读,那么下次循环才把 '\r'
/ '\n'
读出来,干扰了本来要读的下一条数据。于是看起来“数据不对应”或“数据滞后一帧”。
解决:同样是循环读空所有可能发送的字节,并且如果遇到 '\r'
/'\n'
,决定是忽略还是另外处理,都要代码里明确。
USART_ReceiveData()
就马上对当前字节作判断或存到环形缓冲。while (USART_GetFlagStatus(...) != RESET) { ... }
循环,把当前可读的字节全部取完。'1'
,实际是 ASCII 0x31,代码里要用 if (data == '1')
。\r
或 \n
'1'
后,可能还有回车、换行符进来,也要读并丢弃或另外处理。之类的方式,也要确保顺序正确,不能先取一次 data,却直到下一次再赋值 data2。cdata = USART_ReceiveData(); while(!(USART1->SR & (1<<5))); // 等下一字节 data2 = USART_ReceiveData();
“第一个数据在第二次读取时才出现,第二个数据在第三次读取时才出现”典型原因是你的代码在第一次接收循环中并没有实际使用新数据,而是到下一个循环才把那数据读出来或判断。
对策:立即读、立即存/处理,循环把 FIFO 清空,并根据实际接收的字节(ASCII/二进制)正确比对,就不会出现一帧滞后的现象了。
我发现HT32的(我用了freertos)串口中断,只要我发了数据,进入到中断函数中,就会卡死,为什么
当你在 FreeRTOS 环境下使用 HT32 的串口中断,发现一旦进入中断就卡死,通常可能由以下几个原因引起。你可以逐一排查,找到并修正问题即可让中断在 FreeRTOS 下正常工作。
在采用 Cortex-M 内核(如 M0/M0+/M3/M4)的 HT32 系列时,FreeRTOS 对中断优先级有严格要求,尤其当你在中断里想要调用 FromISR
系列的 FreeRTOS API(例如 xQueueSendFromISR()
、xSemaphoreGiveFromISR()
等)时,中断优先级必须设置为低于(数值大于) configMAX_SYSCALL_INTERRUPT_PRIORITY
。
configMAX_SYSCALL_INTERRUPT_PRIORITY
(也就是数值更小),那么在该中断中调用任何 FreeRTOS 内核函数都有可能导致系统进入不可恢复的死锁。configPRIO_BITS
、configMAX_SYSCALL_INTERRUPT_PRIORITY
)匹配。排查方法:
FreeRTOSConfig.h
中 configMAX_SYSCALL_INTERRUPT_PRIORITY
的定义,以及启动文件或 NVIC_SetPriority(UARTx_IRQn, ...)
中断优先级的设置。(实际中断优先级数值) >= (configMAX_SYSCALL_INTERRUPT_PRIORITY)
。如果你在串口 ISR 内直接调用了像 vTaskDelay()
、xQueueSend()
、xSemaphoreGive()
这类常规(非 *FromISR)API,或者使用了 printf()
内部带阻塞功能,会导致中断不能正确退出或调度器发生混乱。进而出现“卡死”现象。
xxxFromISR()
系列
xQueueSendFromISR()
、xSemaphoreGiveFromISR()
、vTaskNotifyGiveFromISR()
等 API,并传入合适的 pxHigherPriorityTaskWoken
指针。vTaskDelay()
, vTaskSuspend()
, vTaskDelete()
, 以及可能会阻塞的 IO 函数。如果在 ISR 内调用,会导致系统挂起。有时因为HAL 库或驱动的用法不同,你需要手动清除某些中断标志。如果没有正确清除,硬件会反复触发相同中断,导致系统频繁进入中断无法退出,看起来就像“卡死”了。
while(1)
在 ISR 中
如果你在 ISR 中做了 printf
或类似操作,有时也会导致卡死,原因包括:
printf
可能会调用阻塞式的写缓冲或等待互斥锁,与 FreeRTOS 冲突;printf
又调用了非线程安全函数或使用了某些需要信号量的环节,也可能导致死锁;vTaskDelay()
或者轮询阻塞操作。解决:
printf
;如果必须输出调试信息,可以简单地往某个字符串缓冲里写,然后在普通任务里再进行输出。main()
中或相应的硬件初始化函数里先设置 NVIC,再启动调度器。__main_stack_size
或相应链接脚本中对中断栈的大小分配。vTaskDelay
、普通 xQueueSend
)在 ISR 中;若需要与任务通信,改用 xxxFromISR()
。FreeRTOSConfig.h
里看 configMAX_SYSCALL_INTERRUPT_PRIORITY
。NVIC_SetPriority()
调用里要满足 (irqPriority >= configMAX_SYSCALL_INTERRUPT_PRIORITY)
, 注意优先级数值是“越大越低”。while(1)
;printf
、sprintf
等调试语句,先注释掉试试。configCHECK_FOR_STACK_OVERFLOW
开启,或者使用断点调试看是否溢出。只要针对这些常见点一一排查,几乎都能找到导致“进入中断就卡死”的根本原因并加以解决。
void USART_Configuration(void)
{
USART_InitTypeDef USART_InitStructure;
textCKCU_PeripClockConfig_TypeDef CKCUClock= {{0}}; CKCUClock.Bit.AFIO = 1; COM1_CLK(CKCUClock) = 1; //开启时钟 CKCU_PeripClockConfig(CKCUClock, ENABLE);
AFIO_GPxConfig(USART_GPIO_GROUP, USART_TX_PIN, AFIO_FUN_USART_UART);
AFIO_GPxConfig(USART_GPIO_GROUP, USART_RX_PIN, AFIO_FUN_USART_UART);
/*
波特率: 115200
长度: 8bits
停止位: 1位
校验位: 无
模式: 正常模式
*/
USART_InitStructure.USART_BaudRate = 115200;
USART_InitStructure.USART_WordLength = USART_WORDLENGTH_8B;
USART_InitStructure.USART_StopBits = USART_STOPBITS_1;
USART_InitStructure.USART_Parity = USART_PARITY_NO;
USART_InitStructure.USART_Mode = USART_MODE_NORMAL;
USART_Init(COM1_PORT, &USART_InitStructure);
// 使能 COM1_PORT 发送和接收
USART_TxCmd(COM1_PORT, ENABLE);
USART_RxCmd(COM1_PORT, ENABLE);
textUSART_IntConfig(COM1_PORT, USART_INT_RXDR , ENABLE); //中断设置
NVIC_EnableIRQ(COM1_IRQn);
// USART_IntConfig(COM1_PORT, USART_INT_RXDR , ENABLE);
// USART_IntConfig(COM1_PORT, USART_INT_TXDE , ENABLE);
USART_IntConfig(COM1_PORT, USART_FLAG_TXDE , ENABLE);
USART_IntConfig(COM1_PORT, USART_FLAG_RXDR , ENABLE);
// USART_RXTLConfig(COM1_PORT, USART_RXTL_04);
// USART_TXTLConfig(COM1_PORT, USART_TXTL_02);
/* 设置FIFO接收等级 */
USART_RXTLConfig(COM1_PORT, USART_RXTL_04);
}void UART1_IRQHandler(void)
{
u8 data;
textif( USART_GetFlagStatus(COM1_PORT, USART_FLAG_RXDR ) ) //接收中断 { data = USART_ReceiveData(COM1_PORT);//读取接收到的数据
// printf("%c\n",data); //把收到的数据发送回电脑
USART_SendData(COM1_PORT,data);
textUSART_ClearFlag(COM1_PORT, USART_FLAG_RXDR); }
}
这是初始化函数和中断,哪里有问题
从你贴出的初始化和中断服务函数来看,最大的可疑之处在于你把 USART_FLAG_TXDE
、USART_FLAG_RXDR
当作参数传给 USART_IntConfig()
。在 Holtek HT32 的标准库里,USART_IntConfig()
函数期望的通常是类似 USART_INT_TXDE
、USART_INT_RXDR
这样的“中断枚举”,而不是 USART_FLAG_
开头的“标志位枚举”。二者是不同的宏定义,混用会导致初始化不正确,从而产生卡死或无效中断等问题。
下面结合你的代码,逐点分析并给出如何修改。
USART_INT_
与 USART_FLAG_
宏cUSART_IntConfig(COM1_PORT, USART_INT_RXDR , ENABLE); ... USART_IntConfig(COM1_PORT, USART_FLAG_TXDE , ENABLE); USART_IntConfig(COM1_PORT, USART_FLAG_RXDR , ENABLE);
其中:
USART_INT_RXDR
是正确用法(表示“接收中断”)USART_FLAG_TXDE
, USART_FLAG_RXDR
则是标志位,和上面的“中断源”宏并不相同。在 HT32 的官方库或示例中,常常可以看到类似:
cUSART_IntConfig(UARTx, USART_INT_TXDE, ENABLE); // 启用发送缓冲空中断 USART_IntConfig(UARTx, USART_INT_RXDR, ENABLE); // 启用接收数据就绪中断
而清除中断标志时用:
cUSART_ClearFlag(UARTx, USART_FLAG_TXDE); USART_ClearFlag(UARTx, USART_FLAG_RXDR);
可以看出 INT
系列与 FLAG
系列是两种常量,不可混用。
而不是cUSART_IntConfig(COM1_PORT, USART_INT_TXDE, ENABLE);
USART_IntConfig(COM1_PORT, USART_FLAG_TXDE, ENABLE);
cUSART_IntConfig(COM1_PORT, USART_INT_RXDR, ENABLE);
这才是 “cUSART_ClearFlag(COM1_PORT, USART_FLAG_TXDE); USART_ClearFlag(COM1_PORT, USART_FLAG_RXDR);
FLAG
” 宏的用途。你先后写了:
cUSART_IntConfig(COM1_PORT, USART_INT_RXDR , ENABLE); NVIC_EnableIRQ(COM1_IRQn); // 紧接着又 USART_IntConfig(COM1_PORT, USART_FLAG_TXDE , ENABLE); USART_IntConfig(COM1_PORT, USART_FLAG_RXDR , ENABLE); // 还配置了一些 FIFO 相关 USART_RXTLConfig(COM1_PORT, USART_RXTL_04);
建议改成下面这样更清晰、更规范的写法:
cvoid USART_Configuration(void) { USART_InitTypeDef USART_InitStructure; CKCU_PeripClockConfig_TypeDef CKCUClock= {{0}}; CKCUClock.Bit.AFIO = 1; COM1_CLK(CKCUClock) = 1; CKCU_PeripClockConfig(CKCUClock, ENABLE); AFIO_GPxConfig(USART_GPIO_GROUP, USART_TX_PIN, AFIO_FUN_USART_UART); AFIO_GPxConfig(USART_GPIO_GROUP, USART_RX_PIN, AFIO_FUN_USART_UART); // 配置基本参数:波特率115200、8N1 USART_InitStructure.USART_BaudRate = 115200; USART_InitStructure.USART_WordLength = USART_WORDLENGTH_8B; USART_InitStructure.USART_StopBits = USART_STOPBITS_1; USART_InitStructure.USART_Parity = USART_PARITY_NO; USART_InitStructure.USART_Mode = USART_MODE_NORMAL; USART_Init(COM1_PORT, &USART_InitStructure); // 使能收发 USART_TxCmd(COM1_PORT, ENABLE); USART_RxCmd(COM1_PORT, ENABLE); // 配置 FIFO 接收阈值 USART_RXTLConfig(COM1_PORT, USART_RXTL_04); // 如果想要发送中断,就加 USART_INT_TXDE USART_IntConfig(COM1_PORT, USART_INT_RXDR, ENABLE); // USART_IntConfig(COM1_PORT, USART_INT_TXDE, ENABLE); // 是否需要发送中断? // 使能中断向量 NVIC_EnableIRQ(COM1_IRQn); }
USART_INT_TXDE
)。你的中断函数:
cvoid UART1_IRQHandler(void) { u8 data; if (USART_GetFlagStatus(COM1_PORT, USART_FLAG_RXDR )) { data = USART_ReceiveData(COM1_PORT); USART_SendData(COM1_PORT, data); USART_ClearFlag(COM1_PORT, USART_FLAG_RXDR); } }
USART_INT_RXDR
的情况下没有问题。if(USART_GetFlagStatus(COM1_PORT, USART_FLAG_TXDE)) {...}
判断,然后清除它。否则你可能会在 TX 中断触发后跑到 else 分支、没做处理,导致重复进入中断。比如可扩展成:
cvoid UART1_IRQHandler(void) { // RX中断 if (USART_GetIntStatus(COM1_PORT, USART_INT_RXDR) != RESET) { uint8_t data = USART_ReceiveData(COM1_PORT); USART_SendData(COM1_PORT, data); // 简单回显 USART_ClearFlag(COM1_PORT, USART_FLAG_RXDR); } // TX中断 if (USART_GetIntStatus(COM1_PORT, USART_INT_TXDE) != RESET) { // 如果需要给发送FIFO补充数据,就在这里完成 // ... // 最后清除中断标志 USART_ClearFlag(COM1_PORT, USART_FLAG_TXDE); } }
注意看检测中断时常见做法是 USART_GetIntStatus()
,而清标志则是 USART_ClearFlag()
。
如果你只是做简单的寄存器读写/写 FIFO(如 USART_SendData()
),而不调用任何 FreeRTOS API(例如 xQueueSendFromISR()
、xSemaphoreGiveFromISR()
等),那么可以不用过分纠结中断优先级是否低于 configMAX_SYSCALL_INTERRUPT_PRIORITY
。因为你并没有跟 RTOS 直接交互。
但若以后要在 ISR 里调用 FreeRTOS 提供的 FromISR API,就必须:
FreeRTOSConfig.h
里设置好 configMAX_SYSCALL_INTERRUPT_PRIORITY
;NVIC_SetPriority(UART1_IRQn, x)
设定中断优先级大于等于此阈值(在 Cortex-M 架构中“数值越大、优先级越低”)。printf
在 ISR 内用 printf
,如果底层实现是阻塞式或依赖信号量,会造成 RTOS 死锁。对于简单的调试场景,最好只做极简的 register
读写和状态标记,实际的输出打印放在任务中完成。
USART_FLAG_
和 USART_INT_
来配置中断;正确用法要用 USART_IntConfig(COM1_PORT, USART_INT_RXDR, ENABLE)
等 “INT_
” 常量,不是 “FLAG_
”。USART_GetFlagStatus()
或 USART_GetIntStatus()
来判断是哪个中断源触发,然后用 USART_ClearFlag()
清除标志。只要把这几点修正好,通常就可以避免“进入中断就卡死”或“中断不起作用”等问题。