STM32f4xx ARM Cortex-M中的IRQ ghost reentry现象
我和同事一起在STM32f4xx上开发代码,我负责bootloader和app的一部分,他负责app的其他部分。他的那部分是可以独立运行的。同事测试发现,在我最终把两个人的代码合在一起,编译烧录进去后,时钟似乎变慢了一半。
为找出问题根源,在AI(豆包)的建议下,我首先计算时钟频率,看从Bootloader到app有无变化。
void Print_Clock_Frequencies(void)
{
uint32_t sysclk = HAL_RCC_GetSysClockFreq();
uint32_t hclk = HAL_RCC_GetHCLKFreq();
uint32_t pclk1 = HAL_RCC_GetPCLK1Freq();
uint32_t pclk2 = HAL_RCC_GetPCLK2Freq();
printf("===== Clock Readout =====\r\n");
printf("SYSCLK = %lu Hz (%.2f MHz)\r\n", sysclk, (float)sysclk / 1000000);
printf("HCLK = %lu Hz (%.2f MHz)\r\n", hclk, (float)hclk / 1000000);
printf("PCLK1 = %lu Hz (%.2f MHz)\r\n", pclk1, (float)pclk1 / 1000000);
printf("PCLK2 = %lu Hz (%.2f MHz)\r\n", pclk2, (float)pclk2 / 1000000);
printf("========================\r\n\r\n");
}
在Bootloader 和 app里在Init之后,在跳转或进入while循环之前调用该函数,得出的是同样的:
===== Clock Readout =====
SYSCLK = 168000000 Hz (168.00 MHz)
HCLK = 168000000 Hz (168.00 MHz)
PCLK1 = 42000000 Hz (42.00 MHz)
PCLK2 = 84000000 Hz (84.00 MHz)
========================
再查看TIM2的频率,
uint32_t Get_TIM2_Real_Clock(void)
{
uint32_t pclk1 = HAL_RCC_GetPCLK1Freq();
uint32_t apb1pre = (RCC->CFGR >> 10) & 0x7;
if(apb1pre >= 4) return pclk1 * 2;
else return pclk1;
}
Bootloader和app都显示84M HZ。
再根据 `htim2.Init.Prescaler = 167; htim2.Init.Period = 1999;` ,可以计算出
F(irq)=F(tim)/(PSC+1)(ARR+1)=84000000/(168*2000)=250HZ,也就是每隔4ms有一次中断。我们程序设计期望的是2ms一次中断(计数), 4ms一次正好是慢一半的节奏。我把这个结果告诉我的同事,他坚称可能芯片内部还有一次转换,时钟频率应该是168MHZ.因为当时我们实验室没有示波器,也没有办法去量。
我当时也假定2ms一次是正常,4ms一次可能是因为芯片的那里处理不及时导致的。当时程序的timer流程是这样的,每次中断响应,都会把一个message(带有执行任务指针)丢到消息队列中。main函数的while循环会不断地去查找这个队列来取消息去执行。那我当时的猜想是main函数来不及处理这些消息,消息队列满了,中断响应只能丢弃一些消息,导致看上去的结果是时钟变慢了。比方说中断响应本来应该放消息1,2,3,4,5,6,7,8,但是因为main函数不能及时处理,只能投放消息1,2,3,4,timer是根据处理的消息来执行任务(包括计时)的,这样时钟就从结果上表现为慢了一半。
于是我第一次的修改方案是,我发现这个消息机制很低效冗余,我就对这个机制进行优化。去掉消息队列机制,就是设一个全局变量,中断响应对这个全局变量每次+1,main函数的循环里只要看到这个全局变量大于0,就去处理任务。当然这里面有些技巧。就是在main里先disable TIM2中断,把这个全局变量拷贝到本地变量,再enable。防止race condition的干扰。这样优化后,我看到一个很稳定的期望频率的输出,也就是似乎是2ms一次的中断产生,但是我并没有去实测。
因为当时的交付任务比较紧,我就先这样解决了。过了几天后,我没有那么忙的时候,我再回头去想这个问题,总觉得没有找到根源。如果是计算机资源不够,处理不过来了,那应该表现为时钟频率的波动,为什么实际的表现是很稳定的慢一半呢?而且我发现,修改程序中的无关代码,有时候只是去掉或加上一行Log,哪怕这个Log没有执行,都会导致时钟“正常”或“变慢”。
我把IRQ的代码发给AI,
void TIM2_IRQHandler(void)
{
if (__HAL_TIM_GET_IT_SOURCE(&htim2,TIM_IT_UPDATE) != RESET)
{
//beginning of some user process
...
//end of some user process
__HAL_TIM_CLEAR_IT(&htim2,TIM_IT_UPDATE);
}
}
AI发现这段代码存在严重缺陷. AI不是一开始马上就指出这个问题,AI带着我走了很多弯路,无论是国内的还是国外的,因为它们都有一个倾向,就是给一个现象强行找理由,哪怕这个理由很荒唐,根本站不住脚,或者这个现象其实也是捏造的。但是最后,AI还是指出了这段代码的问题。
`if (__HAL_TIM_GET_IT_SOURCE(&htim2,TIM_IT_UPDATE) != RESET)` 只检查了source是否是TIM2,这个只要在init代码里enable了这个中断,就一直保持不变,所以永远判断为true。但是我们还需要检查这个中断的flag,`__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE) != RESET`, 硬件在触发这个中断时,会set这个flag,在IRQ里面,需要clear这个flag, `__HAL_TIM_CLEAR_IT(&htim2,TIM_IT_UPDATE);`. 但是clear flag这个操作是由APB总线写入,CPU的时钟内核是168M HZ, 而TIM2挂在APB1上,总线时钟只有42MHZ。中断已经退出,而clear这个操作可能还未完成,这个时候hardware看到flag未清,又会触发IRQ。
标准的IRQ代码应该是:
void TIM2_IRQHandler(void)
{
if(__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE) != RESET
&& __HAL_TIM_GET_IT_SOURCE(&htim2, TIM_IT_UPDATE) != RESET)
{
__HAL_TIM_CLEAR_IT(&htim2, TIM_IT_UPDATE); // 进入后立即清标志
...
我自己做了实验,设立两个local static 变量,一个是在if外,只要进IRQ就+1,一个是在if内,也是+1, 然后每隔1s钟打印这两个变量的值。结果是很稳定的,if外是500,if内是250. 完美地证实了IRQ reentry和时钟变慢一半的问题。而` if(__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE) != RESET ` 之所以能完美避坑,是因为它也需要经过APB总线,上一个clear操作已经先于它发出,所以它读到的是clear后的flag。
这个现象之所以称为ghost reentry是因为它的触发取决于CPU的执行代码效率和APB总线的寄存器写的效率之差,你随便改了其他无关的代码,可能导致系统代码Layout的变化,就可能导致执行完IRQ先于或者后于flag的clear,表现为时钟变快或变慢。而在嵌入式系统中,如果代码固定,执行的任务固定,就会产生固定的re-entry,从而看到上面出现的稳定的2ms发生一次中断事件的现象。
实际上,在上面的例子中,re-entry并没有实际上把中断均匀地分成2ms触发一次,它依然是4ms触发一次,然后在IRQ第一次执行完后又进入,这两次IRQ的间隔只有几十个cycle,ns级别。TIM2能精确产生的最小时间间隔由前面提到的PSC和ARR决定。建议设置使TIM2的最小时间间隔和程序中实际要用到的最小timer间隔一致,在我的程序里是10ms。
更多推荐


所有评论(0)