Arduino 进阶 – digitalWrite 速度有点慢?
当在更新Arduino_DY_Daikin功能过程中,一些相容的Arduino板子无法使用PWM来产生38kHz,必需要使用软体产生红外线所需的38kHz波形,波形的产生是以方波来完成,波形的高、低算一个周期,而38khz代表的是一秒有3800次的波形高、低。,所以利用输出的HIGH
、LOW
及加上延迟就能完成方波的模拟,模拟的方式类似下面程式(范例产生50kHz波形):
void setup() { pinMode(4,OUTPUT); digitalWrite(4,LOW); } void loop() { digitalWrite(4,HIGH); delayMicroseconds(10); digitalWrite(4,LOW); delayMicroseconds(10); }
从LOOP程式中能理解当输出HIGH
时会延时10微秒后再LOW
延时10微秒,产生的波形可能会如此:
HIGH
、LOW
各维持10微秒(uS),所产生的是50kHz,但实际上的波形是如下:
没错,实际上却是如此,整个误差非常大!其实除了delayMicroseconds
本身的延迟外,digitalWrite
的延迟才是重要问题!
digitalWrite 延迟
Arduino为了保持各式各样平台的共通性所产生出来的一组函数,此函数可以指定pin脚输出HIGH
、LOW
状态,所以Arduino相容板也必需要实做所有相关的函数,所有函数列表参照官方网站。以UNO
设计此函数是透过几个对照表对照后再经过对照表产生的值来比较、运算完成输出的动作,其实较耗时的则是对照表,对照表是写入程式记忆体
后再经过读取的方式取得,并非是从记忆体中直接取得,接下来看看digitalWrite
的程式码,它是位于wiring_digital.c
void digitalWrite(uint8_t pin, uint8_t val) { uint8_t timer = digitalPinToTimer(pin); uint8_t bit = digitalPinToBitMask(pin); uint8_t port = digitalPinToPort(pin); volatile uint8_t *out; if (port == NOT_A_PIN) return; // If the pin that support PWM output, we need to turn it off // before doing a digital write. if (timer != NOT_ON_TIMER) turnOffPWM(timer); out = portOutputRegister(port); uint8_t oldSREG = SREG; cli(); if (val == LOW) { *out &= ~bit; } else { *out |= bit; } SREG = oldSREG; }
程式中能解理,利用digitalPinToPort
取得PORT索引,digitalPinToBitMask
取得遮罩位元,因每个PIN对照至1bit,而UNO
是8bit MCU,每组为8bit,需要用遮罩位元取得该bit状态,portOutputRegister
则是利用取得的PORT名称,将名称带入后取得输出PORT的真正位址,因UNO
是属Atmel的AVR系列,输出、输入是有各自的暂存器,例如:PORTD是指输出PORT的D,PIND是指输入的PORT的D,必需要使用portOutputRegister
取得输出PORT的暂存器。那么应该会好奇是如何参照的?那来看首digitalPinToPort
怎么宣告的,开启Arduino.h,程式约177行看到宣告:
#define digitalPinToPort(P) ( pgm_read_byte( digital_pin_to_port_PGM + (P) ) ) #define digitalPinToBitMask(P) ( pgm_read_byte( digital_pin_to_bit_mask_PGM + (P) ) ) #define digitalPinToTimer(P) ( pgm_read_byte( digital_pin_to_timer_PGM + (P) ) ) #define analogInPinToBit(P) (P) #define portOutputRegister(P) ( (volatile uint8_t *)( pgm_read_word( port_to_output_PGM + (P))) ) #define portInputRegister(P) ( (volatile uint8_t *)( pgm_read_word( port_to_input_PGM + (P))) ) #define portModeRegister(P) ( (volatile uint8_t *)( pgm_read_word( port_to_mode_PGM + (P))) )
digitalPinToPort
经过pgm_read_byte
读取表格digital_pin_to_port_PGM
:
const uint8_t PROGMEM digital_pin_to_port_PGM[] = { PD, /* 0 */ PD, PD, PD, PD, PD, PD, PD, PB, /* 8 */ PB, PB, PB, PB, PB, PC, /* 14 */ PC, PC, PC, PC, PC, };
因表格宣告包含PROGMEM
,表格会存入程式记忆体中,再利用pgm_read_byte
取得其值,例如:D0代表的是索引0
,其中对照的是PD,而PD的值是4:
#define PA 1 #define PB 2 #define PC 3 #define PD 4 #define PE 5 #define PF 6 #define PG 7 #define PH 8 #define PJ 10 #define PK 11 #define PL 12
再利过portOutputRegister
带入PD值4当索引,从表格port_to_output_PGM
得到的是PORTD
:
const uint16_t PROGMEM port_to_output_PGM[] = { NOT_A_PORT, NOT_A_PORT, (uint16_t) &PORTB, (uint16_t) &PORTC, (uint16_t) &PORTD, };
在这所看到的port_to_output_PGM
会依照每个板子的不同于pins_arduino.h进行不同的宣告,例子是以UNO
为主,UNO
属于standard
类型,所以pins_arduino.h
位于variants/standard
目录中,如果是Leonardo
则定义于variants/leonardo
。
digitalWrite
函数中out = portOutputRegister(port);
取得的就是PORTD
的指标位址(定址位址),再将取得的值当做out
指标的位址,运用指标写入*out
将状态输出,如此一来就完成改变输出状态的动作,整体下来整个函数要经过很多的对照后才能改变输出的态状,看似覆杂,但也提供了极大的相容性及节省记忆体空间,当然代价就是耗费较长的时间完成。
加快digitalWrite
前面说明能理解整digitalWrite
的运作方式,那要如何加快digitalWrite
呢?其中最直接的方式是直接自行操作输出状态,以UNO
为例,D0
~ D7
使用的输出是PORTD
,pins_arduino.h
的注解中能得到讯息:
以D4当输出为例,D4位于PORTD中的第4个bit,也就是第5个位置(这里都是由0当起始),
当你要将D4输出LOW(0)时:
PORTD &= B11101111;
D4输出HIGH(1)
PORTD |= B00010000;
最终将程式改变为:
void setup() { pinMode(4,OUTPUT); digitalWrite(4,LOW); } void loop() { PORTD &= B11101111; delayMicroseconds(10); PORTD |= B00010000; delayMicroseconds(10); }
再来量测一下结果:
原先的结果:
从图中能明确的知道一点,将digitalWrite
取代后整体的反应就非常的理想,其中的误差就在于delayMicroseconds
是否够精确,当然也有其他方式可以取代delayMicroseconds
的,但因此篇主要是改进digitalWrite
的方法,暂时不讨论。
结论
digitalWrite所造成的输出延迟经过简化digitalWrite后的确可以改善输出延迟,但所伴随而来的是相容性问题,每个Arduino的D0所对应到实体MCU中的接脚并非相同,此时实作的仅相容于使用MCU的Atmega 8
、Atmega 168/328
,也就是Arduino UNO/NANO/MINI
,其他的像Arduino Mega
、Arduino Leonardo`…..等,就必需要另外看它对应到的实际接脚再做改变才行。
如果您本身对于8051或是其他MCU的操作都有相当解理后,对于本篇提供的方法应该不觉的有什么特别的,很多环境所带来的便利所引发出来的就是您对于此环境所产生的应用方式的来源是否够清楚明白,如你能清楚明白时,当你发生Library有问题时或是想将程式再精简化时,这些问题都不足难道你的!