##はじめに
Arduinoで直接ポートを操作する為の覚書です。ポートを直接使うというのはどういう事か? 通常、ArduinoでGPIOを使って入出力を行う場合には、
pinMode(11, INPUT);
a = digitalRead(11);
pinMode(12, OUTPUT);
digitalWrite(12, HIGH);
などとして、指定したピンに入力、もしくはHIGH/LOWのステートを出力します。Arduinoの流儀に従えば、これは各ピン、1ピンずつこの設定を行うことになります。一般的にArduinoでプログラムする場合にはこの流儀に従ってください。(プログラムの見やすさや、互換性などを考えるとArduinoの流儀に従う方がメリットが大きいです)
しかし、__このプログラミングはスピードを犠牲にしています。どうしてもスピードが必要だったり、複数のI/Oを同時に変化させたい場合には直接ポートを操作__して、入出力を行う方法があります。
##公開動画
以下の動画をあげていますのでご興味のある方はご覧ください。
注)私の勘違いにより動画内のPORTCのアサインが違っています。
PC6にはA6ではなくRESETがPC7にはA7ではなく、何もアサインされていません。
下の方にピンアサインが載せてあるのでそれを参照してください。
##PORTに付けられた名前
PORTを直接操作するにはポートが割り当てられたレジスタ・アドレスから直接読み出し、書き込みを行います。しかしアドレスの即値をプログラマーが知る必要はありません。名前が定義されているので、あたかも変数に値を代入するかの様に使えます。使える名前は以下のものになります。
DDRB :ポートBの方向レジスタ。入出力方向(INPUT/OUTPUTを設定する)
DDRC :ポートCの方向レジスタ。入出力方向(INPUT/OUTPUTを設定する)
DDRD :ポートDの方向レジスタ。入出力方向(INPUT/OUTPUTを設定する)
PORTB :ポートBの出力レジスタ。出力するデータを書き込む
PORTC :ポートCの出力レジスタ。出力するデータを書き込む
PORTD :ポートDの出力レジスタ。出力するデータを書き込む
PINB :ポートBの入力レジスタ、入力したデータが保存されている
PINC :ポートCの入力レジスタ、入力したデータが保存されている
PIND :ポートDの入力レジスタ、入力したデータが保存されている
各ポートのレジスタには1つで最大8個のGPIOがアサインされています。
##ポートにどの様に設定するか
上記で示しているレジスタは8ビットのレジスタです。従って、8bitをまとめて設定することになります。これはArduinoが8bit CPUであることに由来します。8bit CPUは「8bitのデータをまとめて取り扱える」という能力を有したCPUです。1bitづつのデータを取り扱う事はできますが、それなりの処理が必要になります。
なので8bitまとめて設定する場合は簡単です。
PORTB = 0b10101010;
ポートBにアサインされている8個のI/Oにまとめて出力するステートを設定します。このやり方の場合、8bitまとめて設定されます。これで各bitにアサインされているGPIOのステートをまとめて変更できます。特定のbitだけを変更したい場合は後述します。
入出力方向はDDR(Data Direction Register)で設定します。
DDRB = 0b10101010;
これで0に設定したbitはINPUT、1に設定したbitはOUTPUTに設定されます。
0:INPUT
1:OUTPUT
です。
入力はPINレジスタを読み出す事で実現できます。
uint8_t input_data = PINB;
でPORTBにアサインされているGPIO全てが読み出されて、8bitのデータinput_dataに代入されます。
##ポートにはどの様にGPIOがアサインされているか?
ArduinoNanoで見ると各ポートは以下の様に対応します。
また、各ポートに対してbitのアサインは以下の様になります。
ただしPC6をI/Oピンとして使用する場合にはFuseを書き換える必要があるようです。
例えばD13であれば、PORTBのbit5が対応します。
#プログラミング
D13-11をOUTPUTにD10-D8をINPUTに設定し、D13=High, D12=Low, D11=Highに設定する場合。
DDRB = 0b111000;
PORTB = 0b101000;
となります。
1bitづつ設定したいのであれば、bitシフトと、bit演算を用いて
PORTB = (PORTB & 0b011111) | (0b1 << 5); // bit5だけHighにする
とします。
入力は
uint8_t d10_8 = PINB & 0x000111;
でD10-8のデータがbit2-0に入ります。
1bitづつ入力したいのであれば出力の時と同様、bitシフトと、bit演算で
uint8_t d10 = (PINB >> 2) & 0b1; // bit2だけを読み出す
uint8_t d9 = (PINB >> 1) & 0b1; // bit1だけを読み出す
uint8_t d8 = (PINB >> 0) & 0b1; // bit0だけを読み出す
とします。
1bitづつデータを取り出す場合には少し不便ですね。
8bitのデータまとめて取り出す場合には、例えばPORTDのD0-D7を8bitのデータとして扱いたい場合Arduinoの流儀に従うと、条件演算子を使ってうまく書いたとしても
#define LED_OUTPUT_PIN_0 0
#define LED_OUTPUT_PIN_1 1
#define LED_OUTPUT_PIN_2 2
#define LED_OUTPUT_PIN_3 3
#define LED_OUTPUT_PIN_4 4
#define LED_OUTPUT_PIN_5 5
#define LED_OUTPUT_PIN_6 6
#define LED_OUTPUT_PIN_7 7
void setup() {
pinMode(LED_OUTPUT_PIN_0, OUTPUT);
pinMode(LED_OUTPUT_PIN_1, OUTPUT);
pinMode(LED_OUTPUT_PIN_2, OUTPUT);
pinMode(LED_OUTPUT_PIN_3, OUTPUT);
pinMode(LED_OUTPUT_PIN_4, OUTPUT);
pinMode(LED_OUTPUT_PIN_5, OUTPUT);
pinMode(LED_OUTPUT_PIN_6, OUTPUT);
pinMode(LED_OUTPUT_PIN_7, OUTPUT);
}
void binary_counter2(uint8_t number){
digitalWrite(LED_OUTPUT_PIN_0, ((number & 0b00000001)==0) ? LOW : HIGH);
digitalWrite(LED_OUTPUT_PIN_1, ((number & 0b00000010)==0) ? LOW : HIGH);
digitalWrite(LED_OUTPUT_PIN_2, ((number & 0b00000100)==0) ? LOW : HIGH);
digitalWrite(LED_OUTPUT_PIN_3, ((number & 0b00001000)==0) ? LOW : HIGH);
digitalWrite(LED_OUTPUT_PIN_4, ((number & 0b00010000)==0) ? LOW : HIGH);
digitalWrite(LED_OUTPUT_PIN_5, ((number & 0b00100000)==0) ? LOW : HIGH);
digitalWrite(LED_OUTPUT_PIN_6, ((number & 0b01000000)==0) ? LOW : HIGH);
digitalWrite(LED_OUTPUT_PIN_7, ((number & 0b10000000)==0) ? LOW : HIGH);
}
この様な長いコードになり、かなり面倒です。しかしPORTを使うと
void setup() {
DDRB = 0b11111111;
}
void binary_counter2(uint8_t number){
PORTB = number;
}
これだけで済みます。
##shiftOut関数をポートを直接操作した場合の速度向上
Arduino標準のshiftOut関数を用いた場合、転送にかかる時間は105us程度、
(オシロの波形は、黄色HC595 A、シアンHC595 Latch CLK、マゼンタHC595 Shift CLK)
ポートを直接操作すると転送にかかる時間は12.2usと約8.6倍まで速度が向上します。
波形を確認した時のプログラム
// 高速版ShiftOut出力波形確認用
// ShiftOutで使うピン
#define HC595_A 2 // シリアル入力
#define HC595_LATCH_CLOCK 4 // ラッチ・シグナル
#define HC595_SHIFT_CLOCK 5 // シフト・レジスタ・クロック
void shiftOutFast(uint8_t byte_data);
void latch_toggle(void);
/*
* 初期化
*/
void setup() {
DDRD = 0b00110100; // D2,D4,D5(ShiftOutで使うピン) = OUTPUT
PORTD = 0b00000000;
}
/*
* ループ
*/
void loop() {
shiftOutFast(0x55);
latch_toggle();
delay(10);
}
/*
* 高速ShiftOutルーチン
*/
void shiftOutFast(uint8_t byte_data){
static uint8_t data_a_bit;
static uint8_t no_effective_bit = PORTD & 0b11001011;
static uint8_t bit_shift[8] = {1,2,4,8,16,32,64,128};
data_a_bit = no_effective_bit;
for(uint8_t i=0; i<8; i++){
if((byte_data & bit_shift[i]) == 0){
PORTD = data_a_bit; // HC595_SHIFT_CLOCKをLowにする
PORTD = data_a_bit = 0b00000000 | no_effective_bit; // HC595_Aを0にする
PORTD = 0b00100000 | no_effective_bit; // HC595_SHIFT_CLOCKをHighにする
}
else{
PORTD = data_a_bit; // HC595_SHIFT_CLOCKをLowにする
PORTD = data_a_bit = 0b00000100 | no_effective_bit; // HC595_Aを1にする
PORTD = 0b00100100 | no_effective_bit; // HC595_SHIFT_CLOCKをHighにする
}
}
PORTD = no_effective_bit;
}
/*
* Latch Clock Toggle
*/
void latch_toggle(void){
static uint8_t no_effective_bit = PORTD & 0b11001011;
volatile uint8_t a;
PORTD = 0b00010000 | no_effective_bit;
a=0;
PORTD = no_effective_bit;
}
##履歴
2022.1.27 プログラムに色つけました。
2022.1.13 Initial release
2022.4.12 PORTのアサイン・テーブルを変更しました。PC6:A6→RESET, PC7:A7→NC