魔法书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字节,差不多只有原来一半大小,现在你还急着购买空间更大、价格更贵的芯片吗?

第34课 ENC28J60联网 网络控制 LED

我是潘,曾经是个工程师。这是为 Ardui.Co 制作的 “Arduino 公开课” 系列的入门教程。上节课介绍 让 Arduino 成为 Web 服务器,现在为其加入交互控制的功能,形象来说,即通过浏览器控制板载 LED。同时介绍如何主动抓取命令,以突破无法访问内网的限制。有任何疑问请在评论区提出,我会逐一回答。

通过 Web 界面控制控制 LED 可能是最简单的交互形式了。如果你具有 HTML 的编程基础,立马会想到实现控制的形式就是表单反馈:

检查一下源代码:

Web 服务器执行,按下按钮(“<input>” 是一个按钮)时, “LED Status: ” 的显示状态就会改变:

/?status=ON,打开LED,
/?status=OFF,关闭LED。

“value” 是按钮的值,按下后,以“GET”方式(不是很安全,实际应用中建议用 “POST”,后话),发送到目标网页上(这里是href=”/”,即当前页面;”?status=XXX”是这个网页后缀),然后被服务器当作变量读取。按下按钮后,完整的路径如下:

但注意,这里改变的是网页显示的内容(ON/OFF),而不是 LED 的实际状态。为此,程序设计的思路是,通过检索网页的变化,去调整 LED 的状态

完整的程序:

跟之前的程序相比,这个程序很有点意思。

我们定义了4个指针变量,前两个赋值“ON”和“OFF”。但为什么用指针变量呢?因为可以减少值之间的传递,提高程序的效率。

将 LED 和它的状态初始化为 OFF。

核心在这:strstr(str1, str2) 方法。Arduino 能够直接调用大部分的 C 的方法,strstr() 的含义是在 str1 上,寻找 str2,一旦找到返回 str2 开始的地址(指针),如果找不到反馈 “0”。如果找到 “/?status=ON” 就让 LED 亮起来,反之亦然。

同时,通过改变 statusLabel、buttonLabel 的值,调整网页的显示状态。

$S 是占位符,一共有3个,按顺序由 statusLabel,buttonLabel, buttonLabel 来替换。

为了方便展示,我选择了在手机浏览器上操作:

理解网页操作的整个过程很重要:

用户输入网址 ——> 浏览器向服务器发起请求 ——> 服务器响应并返回初始状态数据 ——> 浏览器显示网页 ——> 用户点击操作 ——> 浏览器向服务器发送数据  ——>  服务器执行 if(pos) 判断 ——> 向浏览器返回数据 ——> 浏览器刷新页面

第33课 ENC28J60联网 Web 服务器

我是潘,曾经是个工程师。这是为 Ardui.Co 制作的 “Arduino 公开课” 系列的入门教程。上节课介绍了如何访问服务器,反过来,本课要让 Arduino 作为一个可以随时随地访问 Web 服务器,在交互控制中更有现实意义。有任何疑问请在评论区提出,我会逐一回答。

Arduino 更适合作为控制节点,而不适合当服务器,在复杂的中央控制系统上,Raspberry Pi 是我的首选。不过,在需要快速、简单、可靠的应用环境中,将一块 Arduino 作为节点的网络界面也未尝不可。但是区区的 32KB ROM 十分考验我们对 HTML 代码的优化能力。

其实,这节课的意义在于理解 Arduino 是如何相应来自网络的请求,作为系统中的节点,这点相当重要。

HTTP 可能是世界上使用最广的通信协议了。访问一个网站,服务器和浏览器之间就是通过 HTTP 协议来互通信息。HTTP 协议的应用层是由 HTML 语言构成的,由于 HTML 属于文本类型的解析性语言,所以即使是主频只有 16MHz 的 Arduino 单片机,也可以轻松驾驭它。

利用 HTTP 通信,不仅通过一般的 PC 浏览器去访问、控制 Arduino,而且可以让它利用局域网,构成一个更大的节点网络。相比 I2C、SPI 的通信连接方式,HTTP 理论上,只要找到路由,是可以无线拓展的。当然,HTTP 更适合人机、节点之间、或者中央与节点间的长距离通信,毕竟在硬件通信层面,串口、I2C、SPI 等协议的延时要低得多。 (HTTP 的总体延时在毫秒级别,而 I2C 在高速模式下 4MHz,延时在微秒级)

Web 服务器完整代码如下:

设置MAC、IP地址等都属于基础部分,核心内容在 loop() 中:

首先,我们将 packetLoop() 的返回值保存在一个变量里 pos。

缓存中,pos 是浏览器请求开始的位置,如果 pos 有效(大于0),则把请求打印出来(方便学习理解,但不是必须的)。

难点在这里,BufferFiller 是一个构造函数(Constructor,参考官方文档),它的对象 bfill,用于存储服务器对浏览器的响应内容。tcpOffset() 指针,指向响应内容的位置。

用 emit_p() 方法将响应内容保存在缓冲区,并用PSTR() 将网页内容(字符串存)储在 Flash 空间中,以节省 SRAM。

position() 方法以获得 bfill 包含的内容长度,然后,httpServerReply() 获取缓冲区的内容,响应浏览器的请求。

为何一次访问会产生两次请求?因为浏览器除了网页内容外,还会请求 favicon.ico 网站的角标: