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有問題時或是想將程式再精簡化時,這些問題都不足難道你的!