动态数组???

不知道你是否听说过 C99 有一个动态数组的特性,也就是说,数组大小可以根据需要动态的变化。

 

我们都知道,在 C89 模式下,数组的声明只能是这样:


但到了 C99,数组的大小可以用变量代替,根据需要变化:

有些人为了尝鲜或者为了使用方便,可能会在程序中写上类似的代码。一般情况下,代码运行很正常,没有一点问题。但在运行时间需要严格控制的情况下,这段代码就不讲武(码)德了。

 

防出去了昂

我们都知道,嵌入式系统的一大好处就是运行时间可控,是实时的系统,所以它可以做一些高实时性的工作,比如控制、采样等。

 

就拿采样来说,一般都会要求采样率,比如 100Hz、10Hz,换算到时间单位,就是需要 10 毫秒、100 毫秒读取一次数据,这个数据可能是内部寄存器(比如 ADC),也可能是外部的器件通过 I2C、SPI 等总线获取,而一般来说,这些总线的通信时间是稳定的、可控的。但是有一天,你发现你的 SPI 驱动程序运行时间变得不再可控,它有时 50us 完成一次数据的采集,有时需要 1 ms 才完成,总之没有规律可循,唯一的规律就是,当系统全面开始工作时,这个时间大部分在 1 ms 以上,只有很少几次是几十微秒就完成了执行。

 

在 10 Hz 采样率下,1 ms 误差也不算太大,但当在 100 Hz 采样时,时间误差就是 10%,不可忽略你仔仔细细的查看了实现代码,发现就是简单的 SPI 通信,基本上都是判断、赋值操作,还有就是使用循环等待标志位(使用硬件 SPI)。(或许你会怀疑循环等等标志代码导致了时间的不确定,但我的第一直觉告诉我不是它,因为 SPI 的通信时间是可控的(只要器件正常,从机一定会返回数据),STM32F1 系列的硬件 SPI 通信鱼鹰也用了五六年,不应该有问题才对)这些我全部防出去了昂(甚至鱼鹰都考虑到线程执行可能受到中断的影响,特地在问题代码执行期间禁止了中断)。没办法,我只能停停,放下源码本身的分析,拿出了杀手锏:《KEIL 下如何准确测量代码执行时间?》开始对问题代码进行时间测量。

 

有备而来

经过几番测量,很快昂,定位到类似下面的代码:


发现竟然在 52 到 57 行之间花费了大量时间。就一些局部变量的定义,唯一和传统写法不同的是使用了动态数组,怎么会花费这么多时间?

 

按照传统写法,这里应该使用固定大小的数组。我大意了啊,没有闪,当时移植这份代码的时候就留意到了这个另类写法,当时还特地看了一下实现,但最终还是栽在了这里。 

 

这段代码是乱打(写)的吗?他可不是乱打(写)的,格式清晰、移植方便、还有各种异常处理,明显有备而来。来、骗,来、偷袭我这经验丰富的老同志。这好吗?这不好,我劝他耗子尾汁。

 

寻根问底

事实上,如果对时间要求不是很高的话,这段代码不会有任何问题,它的基本读取功能是没有任何问题的,只是说它的执行时间很不稳定,有的时候几十微秒就可以执行完毕,有时候可能需要几毫秒时间,还有极端的可能是直接死机(Hardfault)!

 

那么动态数组是如何实现的,或者说它的本质是什么呢?本质就是使用 malloc 函数申请堆空间(在 rt-thread 中又会调用 rt_malloc),然后在离开函数前使用 free 函数释放堆空间。可以查看汇编确认:


看到这里,你也就知道为什么会出现之前的现象了吧。系统未完全运行前,很少有线程申请堆空间,所以执行时间比较稳定,因为它能快速的找到合适的内存块,一旦系统里所有线程正式工作了,涉及到大量的内存申请与释放,有大量的内存碎片,也就不容易找到合适的内存,这样执行时间也就不稳定了,这对于实时要求高的功能是一个灾难。

 

所以,如果你的功能不要求实时性的话,使用动态数组是可以的,一旦你的功能要求实时性,那么使用静态数组才是更好的选择(如果使用静态数组,一定要注意使用范围,最好加上断言机制),如果代码是在中断执行,rt-thread 系统中,则必须使用静态数组,否则 rt_malloc 无法正常执行(断言失败)。你防住了吗?