我和同事一起在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。




 

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐