魔法书1:利用底层代码释放Arduino空间

我是潘,曾经是个工程师。这是为 Ardui.Co 制作的 “Arduino 魔法书” 系列的专栏。从现在开始,我将尝试通过各种方式,去挖掘Arduino的潜能极限。本篇将探讨如何利用底层代码,让程序变得更小、让 Arduino 变得更高效,进而让产品更可靠。“魔法书”不是教程,而是实验性的栏目,仅供参考。

任何机器只能认1和0,二进制是最基础的机器语言,但从人类的角度来看,几乎不可能用二进制代码来编程,效率太低。所以需要更通俗易懂的“高级”语言,C语言就是其中一种,而Arduino的编程语言就是通过C语言的简化、演变过来。

当编写好程序后,编译器(内置在Arduino IDE中)就可以把高级语言,翻译成机器能懂的二进制代码。但这个过程存在一个弊端,编译器总是无法翻译出最优的代码,要么体积臃肿,要么各种资源浪费。如果我们希望程序变得更高效,就必须往底层深入。

但别误会,我们不会直接写二进制代码。其实,C语言已非常接近底层,只要通过 Arduino 兼容C语言的特性,即可让程序得到不错的优化,让Arduino 的资源有效利用,大幅提升执行效率。

Arduino UNO 的 ROM 空间只有 32K 极为有限,如果你编写程序时,发现空间不够,别急着买更高级的板子,而是先看看你的代码,是否还有压缩和改进的空间。现在我们尝试一下,将最简单的示例程序 Blink (LED闪烁)优化,看看能压缩多少空间出来。

编译后,Arduino IDE 会给出二进制代码的大小,因此我们可以清晰看到改进的空间:

示例Blink 程序编译后,二进制代码的大小是928字节,占用了Arduino UNO 总空间的2%。

压缩这个程序前,先看看它是怎样运作的:

1、配置数字引脚 D13 为输出;
2、改变这个输出引脚的状态,LED亮或灭;
3、暂停一下让人感知亮/灭时间,然后回到第2步。

这个程序为了初学者明白易懂,所以没有做任何优化,每一步都清晰简单:点亮LED,停留一段时间,关闭LED,停留一段时间,再重复。不过,点亮和关闭也可以用一个抽象概念来代替——翻转LED的状态。明白这点后,这程序起码可以压缩一半。

 必须 pinMode() 吗?

我们先拿pinMode()函数来开刀,如果把它注释掉,程序从928字节,减少到864字节,也就是说pinMode() 占用了64字节的空间。

但没有pinMode() 引脚就会一直处于输入状态。此时,引脚抗阻极高,即使其电平可以被控制,但也无法驱动LED。用什么底层代码来实现 pinMode() 的效果呢?首先,我们先得了解pinMode() 的工作原理。

Arduino 基于 Atmel AVR 单片机简化而来。不同的 AVR 单片机具有不同数量的端口,每个端口有一定数量的引脚。同时,每个端口也关联着一个数据方向寄存器(data direction register,即DDRx)。

DDRx 决定了端口上不同引脚的输入输出状态。例如,端口 A 有 6 个引脚,给寄存器赋值 000001 的话,前面4个引脚,都是输入状态,最后一个引脚则为输出状态。pinMode() 简化将指定端口、寄存器赋值两个过程完全省略了。我们只要告诉它,某个引脚的状态,它就会设定设定好。

现在用 bitSet(DDRB, 5) 来代替它。LED的引脚D13 对应 AVR 的是端口B,第5个引脚,因此,我们设置端口B的寄存器 DDRB的第5位为1,即可以设置D13为输出状态。

此时D13为输出状态了,但程序空间只增加了2字节。不过,bitSet() 为何那么神奇呢?因为他不是一个函数,而只是一个宏定义。他被包含在编译器的 wiring.h 头文件中,它的定义和Arduino 的代码一起被打包。

这个函数需要两个参数:值(value)和位(bit)。bitSet(DDRB, 5),在Arduino 编译时,会直接替换成C语言: DDRB |= 1<<5,其含义是,将DDRB的第5位置位(赋值1)。“I=” 是一个复合赋值运算符,表示按“位”运算,“<<”是左移运算符,这里用了5表示要操作DDRB寄存器的第5位,即对应I/O端口B的第5个引脚:PB5(关于端口对应的的列表,可以参考官方MAP)。另外,也可以用二进制0b00100000、八进制040、十进制32、16进制0x20数值来赋值,不过可读性就差很多。

简单的语句替换,就把笨重的pinMode()占用的空间节省出来了。 不过,前提是你很熟悉 Arduino 的端口、引脚和C语言的表达。

digitalWrite() 太费力

digitalWrite()虽然很方便,但是代价却相当大,如果把它注释掉,足足释放了226个字节的空间:

Blink 程序的原理是点亮LED,停留一段时间,再熄灭,停留一段时间,再点亮。这个亮-灭交替的过程可以看作是输出引脚在翻转。

对于 ATmage168 或者以上型号的芯片,AVR的翻转方式很简单,只要向输入寄存器的地址写入一个1即可,但对于老的型号ATmega8/16/32则无效。

如果将digitalWrite()函数都换成bitSet(PINB,5);那么空间可以大幅缩少至:644个字节,只用了4个字节就完成 digitalWrite() 的功能。

程序足够精简了吗?不,loop() 本身就是一个循环,每次循环 LED 都可以翻转,即亮灭替换,因此loop() 中可以进一步精简,去掉一个 bitSet(PINB, 5)和delay(1000),效果完全是一样的,但节约了44字节。

delay() 纯粹为了浪费时间,但是非常浪费系统空间,注释掉之后系统减少了152个字节,堪比digitalWrite()。

最有有效浪费时间的方法,就是利用内部定时器。这里引入time0(Arduino 三个定时器之一),定时会不断递增一个变量timer0_millis ,即“毫秒递增变量”,我们要引用这个变量就需要在setup() 和 loop()之外,加入 extern volatile unsigned long timer0_millis; 来声明这个变量。现在引入一个while()循环来替代delay():

while(timer0_millis < 1000); // 如果 timer0_millis每一毫秒加1,如果不到1000,就一直执行循环;
timer0_millis = 0; // 将 timer0_millis 复位置0,下一个loop() 时重新计数;

这个计数方法使得millis()、delay() 函数完全失效了,因为这些函数都是基于timer0来实现的。

经这一番压缩,最后Blink 从 928字节压缩到490字节,差不多只有原来一半大小,现在你还急着购买空间更大、价格更贵的芯片吗?

Leave a Reply

Your email address will not be published. Required fields are marked *