Arduino程式碼機制-引腳讀寫
在寫arduino程式碼時,pinMode, digitalWrite, digitalRead這些函式用起來是不是非常順手呢?有了這些函式,我們就不用關心AVR微控制器的那些令人頭疼暫存器了。我們向函式傳入引腳在Arduino開發板上的引腳號,就能對這個引腳進行讀寫和設定操作了。這些函式是如何實現的呢?
以上這三個函式,最終還是要通過設定PORT,PIN和DDR三個暫存器實現,要設定某個引腳,就必須知道這個引腳在哪個埠上,還必須知道在這個埠的哪一位上,這樣就能通過設定暫存器來讀寫和設定引腳了。
巨集 digitalPinToPort(P)
#define digitalPinToPort(P) ( pgm_read_byte( digital_pin_to_port_PGM + (P) ) )
Arduino將埠定義為整形,後面將會領略到這樣定義的妙處
#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
再將每個引腳在哪個埠儲存為陣列,放在Flash中,通過查表得到引腳所在的埠。關於如何將資料放在Flash中這方面內容可以看我的上一篇部落格。
const uint8_t PROGMEM digital_pin_to_port_PGM[] = { // PORTLIST // ------------------------------------------- PE , // PE 0 ** 0 ** USART0_RX PE , // PE 1 ** 1 ** USART0_TX PE , // PE 4 ** 2 ** PWM2 PE , // PE 5 ** 3 ** PWM3 PG , // PG 5 ** 4 ** PWM4 PE , // PE 3 ** 5 ** PWM5 PH , // PH 3 ** 6 ** PWM6 PH , // PH 4 ** 7 ** PWM7 PH , // PH 5 ** 8 ** PWM8 PH , // PH 6 ** 9 ** PWM9 PB , // PB 4 ** 10 ** PWM10 PB , // PB 5 ** 11 ** PWM11 PB , // PB 6 ** 12 ** PWM12 PB , // PB 7 ** 13 ** PWM13 PJ , // PJ 1 ** 14 ** USART3_TX PJ , // PJ 0 ** 15 ** USART3_RX PH , // PH 1 ** 16 ** USART2_TX PH , // PH 0 ** 17 ** USART2_RX PD , // PD 3 ** 18 ** USART1_TX PD , // PD 2 ** 19 ** USART1_RX PD , // PD 1 ** 20 ** I2C_SDA PD , // PD 0 ** 21 ** I2C_SCL PA , // PA 0 ** 22 ** D22 PA , // PA 1 ** 23 ** D23 PA , // PA 2 ** 24 ** D24 PA , // PA 3 ** 25 ** D25 PA , // PA 4 ** 26 ** D26 PA , // PA 5 ** 27 ** D27 PA , // PA 6 ** 28 ** D28 PA , // PA 7 ** 29 ** D29 PC , // PC 7 ** 30 ** D30 PC , // PC 6 ** 31 ** D31 PC , // PC 5 ** 32 ** D32 PC , // PC 4 ** 33 ** D33 PC , // PC 3 ** 34 ** D34 PC , // PC 2 ** 35 ** D35 PC , // PC 1 ** 36 ** D36 PC , // PC 0 ** 37 ** D37 PD , // PD 7 ** 38 ** D38 PG , // PG 2 ** 39 ** D39 PG , // PG 1 ** 40 ** D40 PG , // PG 0 ** 41 ** D41 PL , // PL 7 ** 42 ** D42 PL , // PL 6 ** 43 ** D43 PL , // PL 5 ** 44 ** D44 PL , // PL 4 ** 45 ** D45 PL , // PL 3 ** 46 ** D46 PL , // PL 2 ** 47 ** D47 PL , // PL 1 ** 48 ** D48 PL , // PL 0 ** 49 ** D49 PB , // PB 3 ** 50 ** SPI_MISO PB , // PB 2 ** 51 ** SPI_MOSI PB , // PB 1 ** 52 ** SPI_SCK PB , // PB 0 ** 53 ** SPI_SS PF , // PF 0 ** 54 ** A0 PF , // PF 1 ** 55 ** A1 PF , // PF 2 ** 56 ** A2 PF , // PF 3 ** 57 ** A3 PF , // PF 4 ** 58 ** A4 PF , // PF 5 ** 59 ** A5 PF , // PF 6 ** 60 ** A6 PF , // PF 7 ** 61 ** A7 PK , // PK 0 ** 62 ** A8 PK , // PK 1 ** 63 ** A9 PK , // PK 2 ** 64 ** A10 PK , // PK 3 ** 65 ** A11 PK , // PK 4 ** 66 ** A12 PK , // PK 5 ** 67 ** A13 PK , // PK 6 ** 68 ** A14 PK , // PK 7 ** 69 ** A15 };
這樣,比如說我們想要知道13號引腳在哪個埠上,就可以用這樣的程式碼:
uint8_t pin = 13;
uint8_t port = digitalPinToPort(pin);
獲取埠三個暫存器
同樣的,還是通過查表法實現。
Arduino先將每個暫存器的地址儲存為陣列,放在Flash中,想要知道某個埠的相應暫存器地址,從陣列中讀取即可。Arduino提供了三個巨集來讀取資料:
#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))) )
暫存器地址儲存在陣列存放在Flash中:
const uint16_t PROGMEM port_to_mode_PGM[] = {
NOT_A_PORT,
(uint16_t) &DDRA,
(uint16_t) &DDRB,
(uint16_t) &DDRC,
(uint16_t) &DDRD,
(uint16_t) &DDRE,
(uint16_t) &DDRF,
(uint16_t) &DDRG,
(uint16_t) &DDRH,
NOT_A_PORT,
(uint16_t) &DDRJ,
(uint16_t) &DDRK,
(uint16_t) &DDRL,
};
const uint16_t PROGMEM port_to_output_PGM[] = {
NOT_A_PORT,
(uint16_t) &PORTA,
(uint16_t) &PORTB,
(uint16_t) &PORTC,
(uint16_t) &PORTD,
(uint16_t) &PORTE,
(uint16_t) &PORTF,
(uint16_t) &PORTG,
(uint16_t) &PORTH,
NOT_A_PORT,
(uint16_t) &PORTJ,
(uint16_t) &PORTK,
(uint16_t) &PORTL,
};
const uint16_t PROGMEM port_to_input_PGM[] = {
NOT_A_PIN,
(uint16_t) &PINA,
(uint16_t) &PINB,
(uint16_t) &PINC,
(uint16_t) &PIND,
(uint16_t) &PINE,
(uint16_t) &PINF,
(uint16_t) &PING,
(uint16_t) &PINH,
NOT_A_PIN,
(uint16_t) &PINJ,
(uint16_t) &PINK,
(uint16_t) &PINL,
};
有了這些巨集定義和資料,假如我們想要獲取PA埠的暫存器地址,就可以用下面程式碼:
uint8_t* pina = portInputRegister(PA);
uint8_t* porta = portOutputRegister(PA);
uint8_t* ddra = portModeRegister(PA);
如果仔細看這些巨集定義的話,可能會有個疑問:如下圖,假如讀取output暫存器,再假定port_to_output_PGM為0,由於暫存器地址為16位的,所以PORTA的地址存放在陣列的2,3兩個位元組的位置,由於PA是1,讀取的地址為port_to_output_PGM + (P),使用pgm_read_word巨集時,會不會讀取1,2兩個位元組的資料呢?
是不會的,實際會讀取2,3兩個位元組的資料。因為port_to_output_PGM是一個地址,將地址加1時,首先會檢查指標指向的資料的大小,指標偏移的量是這種資料的大小。儘管對地址加的是1,實際上地址偏移了兩個位元組。進一步的,在C語言中, a[1] ,*(a + 1) , 1[a]都是一樣的。
bitmask
要對引腳的操作還要知道這個引腳在埠的哪一位上,假如要對PORTA的第n位置1,可以用這樣的程式碼:
PORTA |= (1<<n);
若將PORTA的第n為置0,則可以:
PORTA &= ~(1<<n);
後面移位操作結果部分就稱為bitmask,可以看到,如果給出每一個引腳的bitmask, 操作起來將會更簡單。
對於移位操作,Arduino提供了一個巨集:
#define _BV(n) (1<<(n))
Arduino將每個引腳的bitmask儲存在陣列放在Flash中:
const uint8_t PROGMEM digital_pin_to_bit_mask_PGM[] = {
// PIN IN PORT
// -------------------------------------------
_BV( 0 ) , // PE 0 ** 0 ** USART0_RX
_BV( 1 ) , // PE 1 ** 1 ** USART0_TX
_BV( 4 ) , // PE 4 ** 2 ** PWM2
_BV( 5 ) , // PE 5 ** 3 ** PWM3
_BV( 5 ) , // PG 5 ** 4 ** PWM4
_BV( 3 ) , // PE 3 ** 5 ** PWM5
_BV( 3 ) , // PH 3 ** 6 ** PWM6
_BV( 4 ) , // PH 4 ** 7 ** PWM7
_BV( 5 ) , // PH 5 ** 8 ** PWM8
_BV( 6 ) , // PH 6 ** 9 ** PWM9
_BV( 4 ) , // PB 4 ** 10 ** PWM10
_BV( 5 ) , // PB 5 ** 11 ** PWM11
_BV( 6 ) , // PB 6 ** 12 ** PWM12
_BV( 7 ) , // PB 7 ** 13 ** PWM13
_BV( 1 ) , // PJ 1 ** 14 ** USART3_TX
_BV( 0 ) , // PJ 0 ** 15 ** USART3_RX
_BV( 1 ) , // PH 1 ** 16 ** USART2_TX
_BV( 0 ) , // PH 0 ** 17 ** USART2_RX
_BV( 3 ) , // PD 3 ** 18 ** USART1_TX
_BV( 2 ) , // PD 2 ** 19 ** USART1_RX
_BV( 1 ) , // PD 1 ** 20 ** I2C_SDA
_BV( 0 ) , // PD 0 ** 21 ** I2C_SCL
_BV( 0 ) , // PA 0 ** 22 ** D22
_BV( 1 ) , // PA 1 ** 23 ** D23
_BV( 2 ) , // PA 2 ** 24 ** D24
_BV( 3 ) , // PA 3 ** 25 ** D25
_BV( 4 ) , // PA 4 ** 26 ** D26
_BV( 5 ) , // PA 5 ** 27 ** D27
_BV( 6 ) , // PA 6 ** 28 ** D28
_BV( 7 ) , // PA 7 ** 29 ** D29
_BV( 7 ) , // PC 7 ** 30 ** D30
_BV( 6 ) , // PC 6 ** 31 ** D31
_BV( 5 ) , // PC 5 ** 32 ** D32
_BV( 4 ) , // PC 4 ** 33 ** D33
_BV( 3 ) , // PC 3 ** 34 ** D34
_BV( 2 ) , // PC 2 ** 35 ** D35
_BV( 1 ) , // PC 1 ** 36 ** D36
_BV( 0 ) , // PC 0 ** 37 ** D37
_BV( 7 ) , // PD 7 ** 38 ** D38
_BV( 2 ) , // PG 2 ** 39 ** D39
_BV( 1 ) , // PG 1 ** 40 ** D40
_BV( 0 ) , // PG 0 ** 41 ** D41
_BV( 7 ) , // PL 7 ** 42 ** D42
_BV( 6 ) , // PL 6 ** 43 ** D43
_BV( 5 ) , // PL 5 ** 44 ** D44
_BV( 4 ) , // PL 4 ** 45 ** D45
_BV( 3 ) , // PL 3 ** 46 ** D46
_BV( 2 ) , // PL 2 ** 47 ** D47
_BV( 1 ) , // PL 1 ** 48 ** D48
_BV( 0 ) , // PL 0 ** 49 ** D49
_BV( 3 ) , // PB 3 ** 50 ** SPI_MISO
_BV( 2 ) , // PB 2 ** 51 ** SPI_MOSI
_BV( 1 ) , // PB 1 ** 52 ** SPI_SCK
_BV( 0 ) , // PB 0 ** 53 ** SPI_SS
_BV( 0 ) , // PF 0 ** 54 ** A0
_BV( 1 ) , // PF 1 ** 55 ** A1
_BV( 2 ) , // PF 2 ** 56 ** A2
_BV( 3 ) , // PF 3 ** 57 ** A3
_BV( 4 ) , // PF 4 ** 58 ** A4
_BV( 5 ) , // PF 5 ** 59 ** A5
_BV( 6 ) , // PF 6 ** 60 ** A6
_BV( 7 ) , // PF 7 ** 61 ** A7
_BV( 0 ) , // PK 0 ** 62 ** A8
_BV( 1 ) , // PK 1 ** 63 ** A9
_BV( 2 ) , // PK 2 ** 64 ** A10
_BV( 3 ) , // PK 3 ** 65 ** A11
_BV( 4 ) , // PK 4 ** 66 ** A12
_BV( 5 ) , // PK 5 ** 67 ** A13
_BV( 6 ) , // PK 6 ** 68 ** A14
_BV( 7 ) , // PK 7 ** 69 ** A15
};
又定義了巨集來讀取引腳的bitmask:
#define digitalPinToBitMask(P) ( pgm_read_byte( digital_pin_to_bit_mask_PGM + (P) ) )
digitalWrite函式
函式原型為
void digitalWrite(uint8_t pin, uint8_t val);
當對某一引腳賦值是,就要先獲得這個引腳的PORT暫存器地址和bitmask,然後對其賦值,程式碼可以是:
uint8_t port = digitalPinToPort(pin);
uint8_t* reg = portOutputRegister(port);
uint8_t bit = digitalPinToBitMask(pin);
if(val == LOW) {
*reg &= ~bit;
} else {
*reg |= bit;
}
為了安全,在digitalWrite函式中還做了一些其他事情,具體程式碼就不貼出。正因為這樣,digitalWrite的效率並不高。當需要反覆對某一個引腳賦值時,使用digitalWrite就不合適了。使用下面的巨集定義將能提高效率。
#define cbi(reg, bit) *reg &= ~bit;
#define sbi(reg, bit) *reg |= bit;
reg是要賦值的引腳所在的PORT暫存器地址,bit是這個引腳的bitmask。使用這樣的巨集定義時,還需要先用之前給出的巨集定義得到reg和bit。使用這樣的巨集定義,只需要進行一次Flash的讀取得到reg和bit,reg和bit能反覆使用,會大大提高效率。
令外兩個函式能自己理解了吧?
更多程式碼可以參考原始檔:
\hardware\arduino\avr\variants\…\pins_arduino.h
\hardware\arduino\avr\cores\arduino\Arduino.h