1. 程式人生 > >Arduino程式碼機制-引腳讀寫

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