1. 问题现象与背景分析
最近在调试GD32F103C8T6的UART中断发送功能时,遇到了一个奇怪的问题:当使用UART中断发送数据后调用Systick的delay_1ms函数,程序会卡死在延时函数中。这个现象让我百思不得其解,经过一番排查才发现是FreeRTOS与裸机延时函数的冲突导致的。
具体现象表现为:
- 程序在while(1)循环中交替调用usart_send_string_it()发送字符串和delay_1ms(1000)进行延时
- 执行到delay_1ms时,程序会永远卡在while循环中无法退出
- 通过Keil调试器观察,发现Systick的VAL寄存器在递减,说明硬件计时器工作正常
- 在delay_decrement()函数中设置的GPIO引脚电平变化从未发生,说明该函数从未被执行
2. 关键代码解析
2.1 主程序结构
主程序的逻辑非常简单明了:
c复制int main(void)
{
SystemInit();
systick_config();
UART0_Init();
while(1){
usart_send_string_it("hello\r\n");
usart_send_string_it("abc\r\n");
delay_1ms(1000);
}
}
这段代码初始化系统时钟、Systick和UART0后,进入一个无限循环,不断发送两个字符串然后延时1秒。
2.2 中断发送实现
中断发送的核心逻辑分为两部分:
- 发送启动函数:
c复制void usart_send_string_it(char *str)
{
while(usart0_tx_len > 0); // 等待上一次发送完成
strcpy((char*)usart0_tx_buf, str);
usart0_tx_len = strlen(str);
tx_index = 0;
usart_interrupt_enable(Usart_Periph1,USART_INT_TBE);
}
- 中断服务函数中的发送处理:
c复制if(usart_interrupt_flag_get(Usart_Periph1,USART_INT_FLAG_TBE) != RESET){
if(tx_index < usart0_tx_len)
{
usart_data_transmit(USART0, usart0_tx_buf[tx_index++]);
}
else
{
usart_interrupt_disable(Usart_Periph1, USART_INT_TBE);
usart0_tx_len = 0;
tx_index = 0;
memset(usart0_tx_buf,0,sizeof(usart0_tx_buf));
}
}
这是一个典型的中断发送实现,通过使能发送缓冲区空中断(TBE)来触发中断发送。
2.3 延时函数实现
延时函数基于Systick实现,关键部分包括:
c复制void delay_decrement(void)
{
if (0U != delay){
delay--;
}
gpio_bit_set(GPIOA,GPIO_PIN_11);
}
这个函数本应在Systick中断中定期调用,用于递减延时计数器。
3. 问题排查过程
3.1 初步现象分析
当发现程序卡死在delay_1ms函数中时,我首先进行了以下检查:
- 确认Systick初始化正确:systick_config()函数被正确调用
- 通过Keil调试器观察Systick寄存器:
- VAL寄存器在递减,说明计数器在工作
- 但LOAD和CTRL寄存器值异常
- 使用万用表测量PA11引脚电平,始终为低,说明delay_decrement()从未执行
3.2 中断优先级测试
考虑到可能是中断优先级冲突,我尝试调整中断优先级:
- 将UART中断优先级设为0(最高)
- 将Systick中断优先级设为较低值
但问题依旧存在。
3.3 关键发现
经过仔细检查,终于发现问题所在:
c复制void SysTick_Handler(void)
{
xPortSysTickHandler(); // FreeRTOS的系统时钟处理函数
// delay_decrement(); // 被注释掉的裸机延时处理
}
FreeRTOS已经接管了Systick中断用于任务调度,导致我们自定义的delay_decrement()函数无法被执行。
4. 问题根源与解决方案
4.1 根本原因
问题的本质在于资源冲突:
- FreeRTOS需要使用Systick作为系统时钟源
- 裸机延时函数也需要使用Systick
- 两者同时存在时,FreeRTOS会完全接管Systick中断
4.2 解决方案
根据实际需求,有两种解决方案:
方案一:完全使用FreeRTOS的延时
如果项目基于FreeRTOS开发,应该使用FreeRTOS提供的延时函数:
c复制#include "FreeRTOS.h"
#include "task.h"
vTaskDelay(pdMS_TO_TICKS(1000)); // 替代delay_1ms(1000)
方案二:裸机开发不使用FreeRTOS
如果不需要FreeRTOS,应该:
- 移除FreeRTOS相关代码
- 确保SysTick_Handler中调用delay_decrement()
- 恢复裸机延时函数的使用
5. 经验总结与注意事项
5.1 调试技巧分享
-
寄存器级调试:当遇到硬件相关问题时,直接查看外设寄存器是最有效的方法。在本次调试中,观察Systick的VAL寄存器确认了硬件计时器在工作。
-
信号量检测法:通过在中断函数中设置GPIO电平,可以直观判断中断是否被触发。这是一种简单有效的调试手段。
-
优先级检查:中断冲突问题往往与优先级设置有关,需要仔细检查NVIC配置。
5.2 常见陷阱
-
RTOS与裸机代码混用:特别注意RTOS会接管哪些硬件资源,避免冲突。除了Systick,还可能包括PendSV、SVC等异常。
-
中断使能顺序:确保外设初始化完成后再使能中断,避免意外触发。
-
资源竞争:如UART发送中的while(usart0_tx_len > 0)忙等待,在高优先级中断中可能导致死锁。
5.3 最佳实践建议
-
延时函数选择:
- 裸机开发:使用Systick或通用定时器实现
- RTOS环境:使用RTOS提供的延时函数
-
中断设计原则:
- 保持中断处理尽可能简短
- 避免在中断中调用可能阻塞的函数
- 合理设置中断优先级
-
调试准备:
- 提前规划好调试用GPIO
- 准备必要的调试工具(逻辑分析仪、万用表等)
- 编写可复现的测试用例
6. 扩展思考
6.1 替代延时方案
如果必须同时使用FreeRTOS和裸机延时,可以考虑:
- 使用通用定时器实现延时
- 基于FreeRTOS的时钟节拍实现二次封装
- 使用DWT周期计数器(Cortex-M3/M4特有)
6.2 GD32与STM32的差异
虽然GD32与STM32高度兼容,但在使用中仍需注意:
- 时钟树配置可能不同
- 部分外设行为有细微差异
- 中断优先级分组默认设置可能不同
6.3 更健壮的UART发送设计
为避免潜在问题,可以考虑改进UART发送设计:
- 使用环形缓冲区管理待发送数据
- 实现DMA传输提高效率
- 添加超时机制避免死锁
这次调试经历让我深刻体会到,在嵌入式开发中,对系统底层机制的清晰理解至关重要。特别是在混合使用不同层级代码时,必须清楚每一部分代码对硬件资源的占用情况。一个小小的疏忽就可能导致难以排查的问题。记录下这个案例,希望能帮助遇到类似问题的开发者少走弯路。