单片机里面的CPU使用率到底该怎么计算?
上周提到为什么我们需要关注CPU利用率的问题,总结一句话就是,利用率越低,你的系统效率越高、响应越快,实时性越高。但是并没有具体说该如何计算CPU利用率。
今天,借助国产操作系统RT-Thread,我们开始实操一番。
在实操之前,需要简单了解几个概念。
钩子函数,即以hook命名的那些函数。那么什么是钩子函数呢?说白了,就是一个函数指针 ,只是这个函数比较特殊一点。
特殊在哪?操作系统某些指定位置才会设置钩子函数,比如程序运行到空闲任务了,为了不修改系统源码(没事别修改源码,很危险的事情,除非你是真大佬),系统会提供一个设置钩子函数的函数接口给你,当你需要在空闲任务中执行某些功能时,用这个函数设置你的需要功能函数就可以了,等系统运行到空闲任务,他就会帮你调用这个函数了。
这个功能看着是不是有点眼熟,对的,和所谓的回调函数是一个道理(我也不明白为啥叫钩子函数,可能是因为和系统有关,和通用的回调函数又有点区别,所以就称之为钩子函数吧,不过你不要管名称,只要知道意思就行了)。
除了在空闲任务可以设置钩子函数,还有可能在任务切换、系统启动、任务创建等等关键的地方设置,当然了,这里的每一个钩子函数都是一个单独的函数指针。
前面也说了,设置钩子函数的目的只有一个,那就是可以让你在不修改系统源码的情况下达到私人目的,让系统的扩展性更强,比如今天说的内容(还有下次介绍的线程CPU使用率问题),如果系统没有空闲钩子函数的存在,你只能去修改系统源码才能达到目的啦。
还有文章所说的线程(task)、任务(thread),其实在RTOS中都是一样的。在 uCOS、FreeRTOS 中,叫任务,RT-Thread 叫线程,只是叫的名称不一样,内容都是差不多的。
然后再大概说说怎么计算的问题。也就是在空闲钩子函数里面,我们需要干什么事情才能到达CPU计算的目的。
首先,第一步肯定是设置钩子函数,其次就是钩子函数该怎么写的问题。
这个网上一搜就出现了(鱼鹰也是网上搜的代码),然后就要分析为什么这么写。
前面说过,CPU利用率其实是首先计算一段时间内空闲任务执行时间,然后反推其他任务的执行时间。
这里有两个问题,一段时间是多少?空闲任务的执行时间怎么计算?
先说第二个问题。用定时器时间掐?好像不好,因为你不知道什么时候程序就离开了空闲任务跑去执行其他任务了,而即使你可以知道它什么时候离开空闲任务的,那也会增加计算难度,不是好的方式。
那怎么办?还记得刚学单片机时你是怎么进行软件延时的吗?对,就是用这个方法,软件延时!
只要程序执行到空闲任务了,就用一个变量不停自加。这样就可以根据变量值来大概计算空闲任务的执行时间。
但是这里又存在一个问题:如果这个变量一直自加,肯定会溢出,该怎么解决。
加大变量的大小,比如原先使用一个字节、两个字节的,那么如果溢出,就用四个字节、八个字节。
但32位系统最大能支持的也就8个字节了,如果还是溢出了咋办?再套一个循环,一个循环的数加完了,再加另一数就行了。
但是还有一个问题,如果说自加的时间不做限制,那么再多的变量也不行,而且还会影响CPU计算的实时性,也就不能实时反映CPU利用率了;而如果时间太短,如果刚好有任务的执行时间在这个范围,那么很可能你计算CPU利用率就直接是100%了。
比如说你一个任务需要执行10毫秒,然后你计算CPU的周期也是10毫秒,那么可能刚好开始计算时跳到了那个任务执行,那么你的变量就没有自加了,也就会显示100%利用率了。
这里其实说的是前面的第一个问题,一段时间是多少?
对于这个时间,因为鱼鹰看的书籍比较少,所以也没有理论支撑(如果有道友知道的,不如留言)。
但是肯定既要考虑变量溢出(这个可以通过加循环方式解决),又要考虑实时性,还要考虑其他任务的最大执行时间 ,否则本来系统没有问题的,但是因为你追求实时性,导致CPU利用率80%、90%的,那就很尴尬了。
以上讨论如果没有经验可能比较难理解,所以建议大家在看完后面内容,实操过后,再回头重新看一遍,这样才有更深的理解。
现在再看CPU计算公式:
cpu_usage = (total_count – count)/ total_count × 100 %
cpu_usage: CPU利用率;
total_count:单位时间内全速运行下的变量值;
count:单位时间内空闲任务自加的变量值。
total_count这个值表现了单片机全速运行下,所能达到的最大值。所谓全速运行,即不响应中断,也不去执行其他任务 ,就单纯让它在一个地方持续运行一段时间,这个值可以体现CPU的算力有多大。
比如,51单片机,可能这个值自加10毫秒之后只有100,STM32F1单片机自加能到1000,而STM32F4单片机能到2000,这样就能体现他们之间的算力差别了。
这个值可以是动态的,也可以是静态的。静态有静态的好处,动态有动态的好处。
所谓的静态是指,在系统没有运行任务时,关闭所有的中断,自加这个值。这样,这个值比较准确,但是如果一开始这个值计算错了,那么后面的计算肯定也是有问题的,而且如果系统启动后长时间既不启动任务,也不响应中断,肯定对系统有一定的影响。但是好处是,系统消耗更少,因为他只计算一次。
而动态计算,则是在空闲任务中,当这个值为零时,计算一次,之后只会在空闲任务自加的变量值超过这个数时,才会更新这个值,这样一来,最终还是能准确反映CPU利用率的。好处是,不需要在开机时关闭所有中断,当然坏处是,前期可能不是很准,因为可能由于中断原因导致计算的值较小(中断处理时消耗了算力)。
废话太多了一些,直接开始干吧。新建一个文件,拷贝如下代码:
以上的代码网上找的,首先分析这两个宏,第二个宏就是前面所说的防止变量溢出用的,而第一个值就是CPU计算周期,这个值比较关键,后面再说。
首先在系统启动前设置钩子函数:
然后,就没有然后了。
对的,设置完之后就可以了,但为了让我们能观察到,可以打印出来。
我们可以观察效果如何,开始设置计算周期和任务延时函数一样,10毫秒。
测试结果:
可以看到,因为是动态计算的,所以开始为0,因为系统首先运行其他任务,只有其它任务不运行时,才会开始运行空闲任务,所以CPU利用率为0。
但是即使后面有值了,你也会发现CPU利用率变化很大,0.82%~1.5%。而且你会发现除了开始的0.0%,后面又再次出现了,这又是怎么回事?
通过设置断点分析,发现,这是因为计算值超出了开始的值,重新设置了:
这就是动态计算的一些问题了,它在一开始的一段时间里,因为无法完全表现算力,只能通过后面不停的修正该值才能达到稳定。
现在修改计算周期 20 毫秒:
发现它的表现更差劲,4.3%~11.61%,而且会周期性出现低利用率的情况。
再改,100毫秒:
可以看到这个比较稳定了,13.71%~14.35%。
那么这个测试代码实际情况的CPU利用率是多少呢?
我们可以通过前面的笔记《》大概计算线程执行时间:
1.59毫秒,10毫秒执行周期,如果只有这个任务执行,大概1.59/10=15.9%(准确计算应该是 1.59/(10 + 1.59) =13.7%)。
和前面的100毫秒类似。
我们先不管前面的结果,先理解一下里面的计算方法。
首先,如果total_count开始为0,那么开始第一次计算。这次计算会关闭调度器。
计算过后,就不再进入。
之后就是动态计算过程:
和第一次计算一样,都是在一定时间内自加计数器,不同的是,这次不会关闭调度器,也就是说,如果有高优先级任务就绪,那么是可以执行其他任务的。
并且计时时间使用的是系统函数rt_tick_get(),单位为系统调度时间。测试环境中,系统调度时间为 1 毫秒。
有意思的是,在进行最终的计算时,采用了分步计算,首先计算整数,再计算小数。
为什么要这样做?效率!
这样的计算方法,可以将浮点运算转化成整型运算,这在没有浮点运算单元的单片机中,能大大减少计算时间。
另外,为了防止溢出,还使用了一个循环结构。
理解了以上内容,现在开始进行鱼鹰式深度思考:
1、 上面的分步计算是否存在问题?
2、 关调度器只关闭了任务调度,但还是会响应中断,这能够体现单片机最大算力吗?
3、 使用rt_tick_get() 函数进行计时,精度是多少,会影响最终的计时吗?
4、 有必要使用循环体吗?如果单位时间内不溢出,是否不用循环体会更好?
5、 前面的CPU使用率为什么会跳动,按理说任务的执行时间应该是确定的,也只有一个任务在运行,不应该跳动才对?
6、 10毫秒的计算和100毫秒的计算差别在哪?
7、 终极问题,如何精确计算CPU使用率?
上面的问题,如果只是粗略计算,其实都可以不用考虑,本着对技术的热爱,还是聊一聊好了。
1) 分步计算,不知道你想到了什么BUG?这个问题其实在以往的笔记都提过,这次再说一次。
当你在获取CPU使用率时,如果刚好在更新这两个值,那么可能整数部分是上一次计算的值,而小数部分却是这次计算的值,那么肯定有问题。
这就涉及到数据完整性获取的问题。怎么解决。关调度器、关中断都可以。
但是因为是粗略计算,那么小数部分即使是错误的,也没事。
2) 因为只关调度器,所以对于中断还是会响应,比如说你设定计算周期为100毫秒,那么1毫秒一次的systick中断肯定会执行,那么在100毫秒中,有100次进入中断执行,而这些算力在上述算法中是无法体现的。
3) rt_tick_get() 函数精度问题,因为这个是系统的软件计时器,所以在测试环境中为1毫秒递增一次,也就是说它的精度在1毫秒。因此,在100毫秒的计算周期里面,有1% 的误差存在,在10毫秒的计算周期里面,误差10%!
4) 有没有必要用循环体?在1秒计算一次的情况下,即使不用循环体,也不会导致溢出问题。而且使用了循环体,还会导致精度降低,毕竟样本少了。比如使用循环体最大值为100,不使用时为10000,哪个精度高?
5) CPU使用率跳动问题。因为是测试,所以只有一个任务在运行,而且任务很简单。
这个任务的执行时间应该是固定的才对,但即使是使用了后面的高精度计算方式,CPU使用率还是会跳动,这是为什么?
第一,rt_kprintf函数执行时间是不固定的,不固定在哪,比如要显示的变量开始是1,后面是1000,因此它输出的字符串不一样,并且打印时间也不一样,因为是查询方式打印,所以差别很大!这就是我为什么推荐DMA打印的原因,未使用前是10%,使用后可能就是1%,甚至更低。
第二点,也是非常容易忽视的一点,插入的中断执行时间。
系统每隔1毫秒需要进入systick执行一次(或者其他中断执行时间),如果说任务的执行时间超过1毫秒,那么中间必然会先执行中断,再执行任务,这样一来,因为中断的插入,导致时间不再那么准确了。而当你把打印的时间控制在 1 毫秒以内,那么CPU使用率会变的非常稳定。
第三:延时rt_thread_delay()函数本身的误差,受到系统精度的影响,这个延时时间其实也不是固定的,会有一定的浮动。
6) 10毫秒和100毫秒计算的差别?
如果说你的任务执行时间 小于1毫秒,那么在10毫秒和100毫秒的计算差别不是很大,但是如果说计算周期变成了5毫秒,即使任务执行时间小于1毫秒的情况下,计算值也是会在最大 和最小 之间来回跳动的。而执行时间一旦超过1毫秒,那么10毫秒和100毫秒的计算就有较大的差别。
并且测试的时候,因为系统延时时间是10毫秒,而计算的时候也是10毫秒的周期,所以出现了比较诡异的事情,因为按理说延时10毫秒,任务执行时间2.56毫秒,任务运行周期为12 毫秒(还记得前面所说的延时误差吗),CPU 使用率按理应该是 21.3 左右,实际上却是 6.5% 左右,相差太大了,这就非常奇怪了。而且如果更改执行时间为1.5毫秒时(通过修改代码修改执行时间),发现计算值又正常了;而即使不修改执行时间,修改计算时间为100毫秒,又正常了,这是怎么回事?
通过深入分析发现,刚好在主任务延时10毫秒的时候,切换到了空闲任务进行空闲时间计算,执行了9.4毫秒的时候,又切回到了主任务,所以计算时,得到了6.5%的计算值。
粗略表示如下所示:
通过这个分析,你应该知道,计算CPU的时候,尽量不要使用和任务延时时间一样的计算周期,否则会出现莫名其妙的事情;还有一点就是,任务的执行周期 = 任务执行时间 + 系统延时,而前面所介绍的计算方法只是粗略的表示,严格来说是有问题的。
7) 终极问题,如何提高计算精度?
通过以上分析,我们其实已经知道了计算时的一些问题点。首先,计算周期问题,这个可以根据系统来确定,但是千万要注意前面的提到的问题。如果说500毫秒计算周期可以满足要求的话,就没必要使用50毫秒,不然你会发现计算值跳动很大。
其次,时间精度问题,这个问题老生常谈了,鱼鹰建议是DWT,如果没有,找一个定时器代替也是可以的。
最后是单位时间算力问题,为了保证精确,可以关闭中断进行第一次计算,或者用短一点的时间,比如1毫秒得到一个算力,如果计算周期为100毫秒,那这个算力乘以100就行了。当然如果系统时钟不经常变的话,也可以通过静态方式先得到单位时间的算力,之后就以它为标准就可以了。这样就不会有长时间关中断的情况出现了。
但是计算算力的时候,千万千万要注意一点的是,C语言转化为汇编代码时,可能一样的代码,在不同的地方执行时间是不一样的 (比如前面代码的第一次计算和后面的计算,看似一样,但实际上有较大差别,原因就在于执行效率不一样),这个涉及到寄存器比内存效率更高的问题,所以计算算力时,可以把它封装成一个函数,这样,只要优化等级不变,那么函数的执行时间就可以认为是确定的。
有帮助的话,记得关注哦!
Nucleo-F413ZH 测评(中) 浮点计算,频率计
浮点计算单元(FPU)的测试。先还是用前面那个程序,修改了一下,避免了中间的double/single类型转换:
uint32_t test(int32_t *data, uint32_t size)
{
uint32_t i;
完整代码请点击阅读原文
}
复制代码
在默认编译指令下,上面的代码不产生浮点指令,而是调用gcc的软浮点程序来计算的:
10000440 <test>:
10000440: e92d 41f0 stmdb sp!, {r4, r5, r6, r7, r8, lr}
10000444: 2900 cmp r1, #0
完整代码请点击阅读原文
100004b0: e8bd 81f0 ldmia.w sp!, {r4, r5, r6, r7, r8, pc}
100004b4: 4c0260f4
100004b8: 49742400
复制代码
上面反汇编中没有一条是FPU的指令。形如 __aeabi_fmul() 这样的函数是gcc库函数,实现浮点运算。sinf() 和 cosf() 两个函数是C标准库 libm.a 中的。
如何让GCC生成FPU的指令呢?搜了下,在 http://gcc.gnu.org/onlinedocs/gcc/ARM-Options.html 列出了ARM平台的特定选项,其中 -mfpu=xxx 指定使用FPU,不过还没完,还需要 -mfloat-abi=... 指定浮点参数如何传递。结合 https://en.wikipedia.org/wiki/ARM_architecture 的信息,Cortex-M4F对应的FPU应该是 fpv4-sp-d16,所以在GCC编译选项中增加 -mfpu=fpv4-sp-d16 以及 -mfloat-abi=softfp, 然后可以看到编译代码变化:
10000440 <test>:
10000440: b538 push {r3, r4, r5, lr}
完整代码请点击阅读原文
100004b4: 4c0260f4
100004b8: 49742400
复制代码
除了 sinf() 和 cosf() 两个函数没有直接对应的浮点指令的外,其它运算都直接翻译成FPU的指令了。这两个函数在 libm.a 当中,软件算法也是要使用很多的浮点计算的,那么它用指令还是用模拟实现呢?查看下arm-gcc的目录树,发现 libm.a 这个文件有很多个,在不同的子目录下:
Cortex-M4F 对应的 ARM 版本是 ARM-v7e-m, 故有三个 libm.a 对应,分别是默认的、softfp目录下的和fpu目录下的。softfp 和 hard (fpu)是不同的ABI模式,就是参数传递约定不同,不能通用,所以分开成两个库了。默认的那个数学库,应该是不使用浮点处理器的。好,GCC是如何选择使用哪个库来连接呢?我发现是根据 -mfpu 和 -mfloat-abi 选项的。如果直接调用 ld 程序来连接,就要自己选择库文件了。
比较下几种方式的平均执行周期:数值越小执行越快
一般运算用库函数一般运算用FPU 库函数软件模拟5411535948338666库函数用FPU125044086730899可发现即使数学库函数用FPU进行浮点运算,在代码中直接用浮点指令而不经过函数调用能节省很多机器周期。
从ST网站下载的软件开发包里面,有CMSIS的数学库:arm_math. 而且提供了源程序。不妨把其中的 arm_sin_f32() 和 arm_cos_f32() 两个函数拿出来试试。这两个函数计算三角函数是用查表加插值的方法。我对比发现虽然它快,计算误差也比GCC的函数大了很多,不要求很准确才敢用啊。
在不用FPU的条件下,平均执行周期为 27879682; 用了FPU以后变为 2326043——程序获得超过十倍的加速。
总结:需要单精度浮点运算的时候,使用STM32F413 Cortex-M4F处理器中的FPU是可以极大提高计算能力。没有FPU的时候,浮点计算开销一部分是软件模拟算法的,一部分是运算函数调用产生的。CMSIS的DSP数学库有许多巧妙的算法可以去发掘,牺牲精度,换来更短的执行时间。
STM32F413有两个32-bit的Timer, 最高可以在100MHz频率下计数。虽然100MHz并不是很高,对于一般单片机背景的DIY项目来说,测个频率也差不多够用了。最直接的测量频率的方法是用被测时钟信号作为定时器的时钟输入,然后将定时器打开,等待一个固定的时间段再关闭,看计数值是多少。
虽然这样可以测量很高的频率,比如用F413能测到100MHz,但是缺点是分辨率与频率成正比,测量低频信号的相对分辨率就很低了。例如一秒的测量时间,对1000000Hz和1000001Hz是可分辨的,相对分辨率为1ppm;对50Hz和50.2Hz是不能分辨的,相对分辨率不到1%,因为有效数字位数随频率降低而减少。要在不同测量频率下保持同样的相对分辨率,一个办法是用固定参考频率去对测量信号的跳变沿进行捕捉(定位),测量出信号的N个周期经历的总时间T,用N/T计算频率。在基本的测量时间段内,被测量信号频率降低,周期数减少,但总的时间仍然是约等于测量时间,所以一除之后小数的有效数字就增加,分辨率向低频扩展。
我曾经用Atmel 8位的ATMega48做过一个等精度频率计,MCU运行在10MHz,利用16bit定时器的捕捉功能,做到分辨率6个数字。受到MCU定时器频繁访问、程序指令开销的限制,我用汇编优化了程序,最高也只能测量到300kHz频率。如今有了更强大的STM32F413,我不妨再试下此法能做到什么程度。因为我发现它有几个重要的优点:
(1)STM32F413的Timer 2和Timer 5是32-bit计数的,在以1秒为测量总时间的条件下绝不会溢出。否则必须跟踪每次捕捉的结果,看是否溢出了:16-bit Timer的最大计数范围65535而已。
(2)STM32F413的Timer 2/5 捕捉通道可以使用DMA将每次捕捉的值传送到RAM,比CPU去查询读取快。
(3)STM32F413的Timer 2捕捉通道可以产生一个触发事件,作为Timer 5的计数源——这个功能可以省去软件记录捕捉次数了。
我的程序核心是这段:
Timer 2用来捕捉,clock是最高的100MHz,使用CH1的捕捉功能,并使用DMA1 Stream5进行传输。测量时间是1秒,因此又用了Timer 2的CH4通道比较功能,当记数到100000000时触发中断。因为只关心首次捕捉和最后一次捕捉的结果,而最后一次捕捉结果会保留在CCR1寄存器中,就只需要想办法保存第一次捕捉就行了。我是采用DMA来将捕捉发生时CCR1寄存器的值读取,然后写到SRAM的存储区中,只传输开始的几次捕捉就够了(理论上一次就可以,但频率高了在初始时似乎有不稳定的状态,多存一些后面再判断处理)。
Timer 5工作在从模式,用Timer 2输出的事件进行触发计数。启动顺序是先启动Timer 5, 再启动Timer 2; 关闭则顺序反过来。Timer关闭后整理结果,计算频率,再循环重新开始。
配置PA0为TIM2_CH1功能,测试信号接在这里
测试对象:我几年前做的4060振荡+分频时钟模块
8kHz输出的测量。因为两个晶振都是有误差的,结果不能作为计量标定。由于是等精度频率计,得益于STM32F413的100MHz Timer, 8kHz分辨到0.0001Hz没有难度(若用高精度时钟给MCU,就可以做到高精度)。
测量频率数字的抖动,除了捕捉本身有一两个时钟沿的随机误差外,可能是MCU内部PLL时钟抖动引起的。另外,测试了这个简单的软件频率计测量到20MHz还可用,要测更高频率就得外部加硬件分频器了。
欢迎点击阅读原文与作者交流、沟通。
以上图文内容均是EEWORLD论坛网友:cruelfox 原创,在此感谢。
欢迎微博@EEWORLD
如果你也写过此类原创干货请关注微信公众号:EEWORLD(电子工程世界)回复“投稿”, 也可将你的原创发至: bbs_service@eeworld.com.cn,一经入选,我们将帮你登上头条!
与更多行业内网友进行交流请登陆EEWORLD论坛。
相关问答
51 单片机 能不能计算浮点数,支不支持 浮点运算 ?有人说能,有人说不能?不能直接硬件计算浮点(就是所谓的FPU),只能通过程序库(调用函数)模拟完成,非常耗时!不能直接硬件计算浮点(就是所谓的FPU),只能通过程序库(调用函数)模拟完成...
单片机 中ov位如何判别?在单片机中,OV位是一个溢出标志位,用于表示浮点运算结果是否溢出。当浮点运算结果超出单精度浮点数的表示范围时,就会产生溢出,此时OV位会被置为1。可以通过...
51 单片机 能存多少个 浮点 数?51单片机的存储空间与具体型号和内存大小有关,但一般情况下,它能够存储有限数量的浮点数。由于浮点数在内存中的存储占用空间较大,因此在单片机程序设计中,使...
关于C51 单片机 的小数点四舍五入处理?将你需保留的那一位的低一位加5之后舍掉即可单片机内一般采用整数计算,如果要保留两位小数,需将原数据扩大100倍,小数点该向左移2位18754/100=187·54但单片机...
让小车到达指定位置有哪些PID算法?许多学生不知道PID是什么,因为许多学生不是自动化的。他们需要信息和程序来开口说话。很明显,这种学习方法是错误的。至少,首先,您需要理解PID是什么。首先,.../...
定点数和 浮点数 的计算[最佳回答]统统放大一定的倍数成为整数.结果是有误差,中间过程也可能溢出.在单片机,dsp上常用
stm 单片机 的优点?STM单片机,是一款性价比超高的系列单片机,功能及其强大。优点:专为要求高性能、低成本、低功耗的嵌入式应用专门设计的ARMCortex-M内核,同时具有一流的外设...
世界上有哪些著名的 单片机 公司?1微芯科技(Microchip)+爱特梅尔(Atmel)(后者被前者收购)Atmel主要是单片机:AT89C51、AT89C52、AT89C2051,AT89S51(RC)对于从事单片机开发的人员...
DSP与 单片机 到底有什么区别啊?如果是电力电子领域dsp一般指的是德州的Tms320F28335或者新出的28379系列,前者包含浮点运算单元,定时器,pwm发生器adc,串口,i2c等,后者在此基础上改进成了...
单片机 控制MOS开关管问题?是P沟的好,这样控制可以共地,处理起来方便;IRF5305可以,不过却有20A电流的话,建议两个并一起使用,那样安全多了。使用MOS管来控制恒流,MOS管上为了恒流,...