最近向一个项目中移植了FreeRTOS,发现这个os没有很好地支持CPU统计功能,既然它没有,我自己就加一个。
原理
我要实现的是仅是获取整个CPU的使用率,各个线程的使用率就不统计了。一半系统上运行的肯定不止一个线程,但不需要每个线程都要计算占用率,只需要计算空闲线程的占用率就可以。
这个方法需要一个寄存器,能读出实时的时间,我在STM32F411上用TIM5->CNT
来作为这个寄存器。为什么使用TIM5
? 我是看重它是一个32位的寄存器,自增频率可以提到很高。
自增频率是有限制的,因为寄存器字长有限,每隔一段时间寄存器会溢出,两次读取之间最多只能容许一次溢出,如果超过一次,计算的时间就会偏小(真实值小了
(溢出次数-1)*溢出周期
),所以溢出频率不能太高,要频率小于读取频率。
然后构造一个函数,负责读取这个寄存器,对于我这个例子就是TIM5->CNT
了。每次调用这个函数,这个函数就会返回一个值,代表上次调用距本次调用过了多少个时间间隔。
然后在线程调度的地方加入一个函数调用,调用的函数里要获取当前线程句柄,然后判断是否为空闲线程,并且调用前面那个获取时间的函数,就可以得到这个线程运行了多长时间,然后进行一些处理,得到占用率。
实现
TIM5
的计数器是32位,我设置它自增频率为1MHz,每一个多小时溢出一次,肯定是满足要求的。(没有什么线程会连续运行1小时不调度吧)
定时器初始化代码,使用MXCube生成,CPU主频96MHz
不要忘记在启动线程调度之前使用 HAL_TIM_Base_Start(&htim5);
void MX_TIM5_Init(void)
{
/* USER CODE BEGIN TIM5_Init 0 */
/* USER CODE END TIM5_Init 0 */
TIM_ClockConfigTypeDef sClockSourceConfig = {0};
TIM_MasterConfigTypeDef sMasterConfig = {0};
/* USER CODE BEGIN TIM5_Init 1 */
/* USER CODE END TIM5_Init 1 */
htim5.Instance = TIM5;
htim5.Init.Prescaler = 95;
htim5.Init.CounterMode = TIM_COUNTERMODE_UP;
htim5.Init.Period = 0xffffffff;
htim5.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim5.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
if (HAL_TIM_Base_Init(&htim5) != HAL_OK)
{
Error_Handler();
}
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
if (HAL_TIM_ConfigClockSource(&htim5, &sClockSourceConfig) != HAL_OK)
{
Error_Handler();
}
sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
if (HAL_TIMEx_MasterConfigSynchronization(&htim5, &sMasterConfig) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN TIM5_Init 2 */
/* USER CODE END TIM5_Init 2 */
}
这个函数就是前面说的那个函数,用于获取经过的时间
//获取距上次调用这个函数过了多少个时间间隔
TickType_t H_TS_GetDT(){
static TickType_t lastT=0;
TickType_t nowT;
TickType_t r;
nowT=(TickType_t)TIM5->CNT;
r=nowT-lastT;
lastT=nowT;
return r;
}
下面的代码提供了获取占用率的方法,
还有供FreeRTOS调用的void H_TS_GetCPULoadCall()
,在这个方法里完成时间统计
#define vH_TS_CPULoad_T 400000
extern int RTOS_IsIdleThread(void);
static volatile int H_TS_CPU_Load=-1;
static volatile TickType_t H_TS_RunTickCnt=0;
static volatile TickType_t H_TS_IdleTickCnt=0;
void H_TS_GetCPULoadCall(){
TickType_t dt;
TickType_t RunTickCnt;
TickType_t IdleTickCnt;
dt=H_TS_GetDT();
RunTickCnt=H_TS_RunTickCnt;
IdleTickCnt=H_TS_IdleTickCnt;
RunTickCnt+=dt;
if(RTOS_IsIdleThread()!=0){
IdleTickCnt+=dt;
}
if(RunTickCnt>vH_TS_CPULoad_T){
H_TS_CPU_Load=1000-(1000*IdleTickCnt/RunTickCnt);
H_TS_RunTickCnt=0;
H_TS_IdleTickCnt=0;
}else{
H_TS_RunTickCnt=RunTickCnt;
H_TS_IdleTickCnt=IdleTickCnt;
}
}
/**
* @brief 获取CPU使用率
* @return CPU使用率 单位0.1%
*/
int H_TS_GetCPULoad(void){
return H_TS_CPU_Load;
}
还有一个函数,用于判断当前线程是否为空闲线程,
这个函数被实现在FreeRTOS源码的tasks.c
文件的最末尾。
(xIdleTaskHandle
被定义为了static, 只能把这个函数定义在这个文件里)
int RTOS_IsIdleThread(void){
if(pxCurrentTCB==xIdleTaskHandle){
return -1;
}
return 0;
}
最后,需要FreeRTOS去调用void H_TS_GetCPULoadCall()
,
在port.c
中找到__asm void xPortPendSVHandler( void )
,修改为下面的代码。
主要修改两个地方
- 开头的声明中添加
extern H_TS_GetCPULoadCall;
- 插入
bl H_TS_GetCPULoadCall
到bl vTaskSwitchContext
之前
这样FreeRTOS调度时就可以调用到统计相关的方法了
__asm void xPortPendSVHandler( void )
{
extern uxCriticalNesting;
extern pxCurrentTCB;
extern vTaskSwitchContext;
extern H_TS_GetCPULoadCall;
PRESERVE8
mrs r0, psp
//isb
/* Get the location of the current TCB. */
ldr r3, =pxCurrentTCB
ldr r2, [r3]
/* Is the task using the FPU context? If so, push high vfp registers. */
tst r14, #0x10
it eq
vstmdbeq r0!, {s16-s31}
/* Save the core registers. */
stmdb r0!, {r4-r11, r14}
/* Save the new top of stack into the first member of the TCB. */
str r0, [r2]
stmdb sp!, {r0, r3}
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0
//dsb
//isb
bl H_TS_GetCPULoadCall
bl vTaskSwitchContext
mov r0, #0
msr basepri, r0
ldmia sp!, {r0, r3}
/* The first item in pxCurrentTCB is the task top of stack. */
ldr r1, [r3]
ldr r0, [r1]
/* Pop the core registers. */
ldmia r0!, {r4-r11, r14}
/* Is the task using the FPU context? If so, pop the high vfp registers
too. */
tst r14, #0x10
it eq
vldmiaeq r0!, {s16-s31}
msr psp, r0
//isb
#ifdef WORKAROUND_PMU_CM001 /* XMC4000 specific errata */
#if WORKAROUND_PMU_CM001 == 1
push { r14 }
pop { pc }
nop
#endif
#endif
bx r14
}
这样,就可以在其他地方调用int H_TS_GetCPULoad(void)
来获取CPU使用率。
附加测试
代码添加完了,得测试一下。我的项目正好可以测试。
最大优化开启
屏幕的左上角为CPU占用率,右上角第一行为OLED显存刷新频率(底层SPI发送的帧速率),第二行为FFT渲染的帧频率
之前写代码都是畏首畏尾,生怕一不小心写出单片机跑不动的代码,但用了CPU统计功能之后,我发现我的担心是多余的,即使OLED有好几个帧缓存相互复制,显示算法不字节对齐(OLED 一个字节表示8个像素,造成了如果y不为8的倍数的情况下显示算法变得复杂),仅刷屏CPU的占用率为8%,发现这个单片机的性能比我想象得还要强大。在处理数据时,CPU的占用率为30%多,这个单片机还能干更多事情啊(邪魅一笑)。
不过帧率似乎不太理想,帧率我是人为限制帧率的,第一行的F:
应该在166上下浮动才对,但实际上才130~140,而且居然还会波动,最低时与FFT帧速率差不多,这很明显有问题。
刷屏相关代码
由于这个工程原本使用的是我自己写的os ,替换为FreeRTOS后为了方便把相关API用宏定义定向到了FreeRTOS中
相关宏定义
渲染线程
刷屏线程
刷屏线程优先级比渲染线程高
消息队列Screen_Display_Param.RecvQueue
用来传递帧缓存指针
信号量Screen_Display_Param.RecvDone
用于刷屏线程向渲染线程报告自己已经接收到帧缓存
渲染线程应该是每隔6ms生成一个帧缓存发送到刷屏线程中,然后刷屏线程通过底层SPI刷新屏幕,这个循环与刷屏线程为两个不同的线程。通过消息队列与信号量进行同步。刷屏线程在等待DMA传输的同时渲染线程渲染帧缓存。
按道理应该为166帧,但实际上并没有。并且在我原来的工程中(使用我自己写的os),是能达到166帧的,并且很稳定,即使有没有处理数据都没有大的变化(165~167之间变化),但在FreeRTOS中,仿佛延时时间变长了。感觉cpu被FreeRTOS占用了,但计算的CPU占用率与我的系统的占用率可以说是几乎一致,最后我干脆把这个循环的延时去掉,把6ms执行一次的限制将到2ms,但结果得到的情况居然是没有变化!!,按道理应该是帧率变高,但实际上并没有变化。这个情况非常奇怪。
刚开始猜测是FreeRTOS线程与线程之间消息传递有延迟,但我感觉不太可能,这个延迟是很致命的问题,对于我这个项目来说还没什么,但是其他项目就不一定了。或许是FreeRTOS配置头文件某些配置有问题,当是配置项有很多,一时半会找不到哪里出问题了。
后来又想到是不是cpu统计的问题,但去掉相关代码后问题依旧。(实际上这个统计压根占不了多少性能)。
2021年11月20日18点02分->这个帧速率的问题已得到解决:
https://blog.csdn.net/qq_42907191/article/details/121439789.