最近一个项目需要生成一个方波信号,要求信号的频率,脉宽和初相都要可以在运行时调节。于是掏出吃灰已久的Arduino UNO R3来做了一个。经过调优,这台单片机最高可以输出55KHz的方波信号(误差在0.5KHz左右;最高能达到77.5KHz但是那时候的误差就有5KHz左右了)。程序可以在GitHub Gist: Jamesits/high-frequency-square-wave-generator.ino下载到。做的过程中遇到了不少坑,写本文来纪念一下。
精确计时
精确计时千万不能用延时。(别把祖传的 delay() 掏出来丢人现眼了谢谢。)正确的做法是开一个定时器(Arduino默认就开着一个定时器),算出某个过程应该发生的时间,然后用一个循环比较定时器数值和预定发生时间,一旦到达或者超出就立即执行。
millis() 函数很好用,但是要注意它的返回值类型是 unsigned long 而且会溢出(不仅是70分钟一次的计时器溢出,还有做减法的时候的溢出)。尤其是在做数学运算和比较的时候一定要注意。Arduino IDE用的语言是C++,可以参考C++的文档。(Arduino真的有文档?)
高速切换引脚电平
Arduino自己带的 digitalWrite() 非常非常慢,所以这里我们直接写寄存器。 DDRB 是控制引脚状态(输入或者输出)的, PORTB 控制引脚的输出电平。关于寄存器的详细解释参见第一篇参考文章。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
void setup() { // ... DDRB = B00100000; // ... } inline void setHigh() { PORTB = B00100000; PORTB = B00100000; } inline void setLow() { PORTB = B00000000; } |
这边设置高电平的函数把同一句写了两遍,目的是让两个函数消耗大致相同的CPU Cycle。这个大致相同是我用示波器测量出来的,不同的Arduino型号可能不一样,如果你使用不同型号的话可能需要自己测定。测定的方法很简单,写如下程序:
1 2 3 4 5 6 7 |
void setup() { DDRB = B00100000; while(1) { PORTB = B00100000; PORTB = B00000000; } } |
然后把示波器接到PIN13上看一下低电平和高电平的时间大约相差多少就好了。(你需要一个采样精度达到2MHz并且不是平均采样的示波器。)
邪恶的 loop()
如果你要做高精度的循环,那么尽量不要使用 loop() ,而应该选择在 setup() 的末尾写个 while(1) ,然后在里面写你的循环程序。每次 loop() 被执行之前(或者之后)Arduino都会做一些奇怪的事情,包括把引脚重置成悬空状态。于是我的输出波形就变成了这副样子(大块黄色区域是悬空状态):
而正常的波形是这样的:
一定要用 loop() 的话,每次循环都要重新设定一下引脚状态。(但是这样会导致循环的速度降低,所以为什么要用它呢?)
串口是个很慢很慢的东西
即使你在调试,也不要打开串口,更不要想从串口输出什么东西。打开串口会破坏整个程序的timing。
注意引脚对PWM的支持
支持PWM的引脚上面自带一个定时器,它的响应特性和不带PWM的引脚是完全不同的。我测试的时候用了不带PWM的引脚。
==========
如果你需要更高频率的方波发生器,可以考虑使用性能更高的ESP8266实现。
参考