程式人雜誌 -- 2014 年 6 月號 (開放公益出版品)

從 Arduino 到 AVR 晶片(1) -- AVR 晶片與 IO ports 範例 (作者:Cooper Maa)

前言

單晶片微電腦 (Single Chip Microcomputer),或稱微控制器 (Microcontroller,縮寫為 µC 或 MCU),是一個將 CPU、記憶體、I/O Port 等周邊電路全部整合為一體的晶片。不像微處理器需要外部電路連接周邊,微控制器的應用只要少許的電路就可以運作,因為所有必要的周邊它都內建了。微控制器主要用在嵌入式系統,例如汽車電子、工業控制、機械控制等領域。

著名的 Arduino 板子上也有一顆微控制器,它是 AVR 的晶片,例如 Arduino UNO, Duemilanove 用的是 ATmega328,Arduino Diecimila 的是 ATmega168,而早期的 Arduino USB 則是使用 ATmega8。

Arduino 在歐美非常流行,因為它超簡單,很快就可以上手,我認為從 Arduino 到 AVR 是一條進入單晶片韌體開發的捷徑,因此著手撰寫這一系列的教學文件,希望這可以幫助想學單晶片韌體開發的新手,也希望能夠拋磚引玉,藉此引出高手發表佳文,同時也希望路過的先進不吝指教。

教學目標

我假設讀者你是單晶片新手,我不打算一次把所有與單晶片有關的東西全塞到你腦袋裏,因為這麼做其實是揠苗助長,適得其反,因此這系列的教學將維持 Arduino 一貫的簡約風格,點到為止。這系列教學的目標為:

授課對象

你必須具備下列基礎:

上課器材

你需要一張 Arduino 板子,一條 USB 傳輸線,以及 Arduino IDE 軟體開發環境:

▲ Arduino UNO 與 USB 傳輸線

▲ Arduino UNO 與 USB 傳輸線

▲ Arduino IDE

▲ Arduino IDE

1. AVR 晶片簡介

AVR 是 ATmel 這家公司設計的 8 位元晶片,晶片架構來自於 Alf-Egil Bogen 和 Vegard Wollan 的構想。AVR 是 Alf (Egil Bogen) and Vegard (Wollan) 's Risc processor 的縮寫。

AVR 目前大概可分為下列幾個家族:

8-bit tinyAVR 系列 8-bit megaAVR 系列 8/16-bit XMEGA 系列 32-bit AVRs 系列 排愈後面的等級愈高,主要是記憶體較大、速度較快、腳位和周邊也比較多。

如果沒有特別聲明,這系列教學中提到的 AVR 指的都是 8 位元的晶片。

Arduino 與 AVR 晶片的關係

前面說過,Arduino 用的就是 AVR 的晶片,你可以在 Arduino 板子上找到 AVR 晶片,以 Arduino UNO 為例,晶片所在的位置如下圖所示:

▲ Arduino UNO

▲ Arduino UNO

在 arduino.cc 這個頁面 中,條列了 Arduino 各種版本的板子、硬體規格、所用的 MCU 等資訊。下列是幾款 Arduino 跟它們所用的 MCU 對照表:

Arduino UNO 和 Duemilanove 用的是同一顆 ATmega328 晶片,下表是 ATmega328 的晶片特性摘要:

特性 說明
Operating Voltage 1.8V - 5V
Flash Memory 32 KB
SRAM 2 KB
EEPROM 1 KB
Clock Speed 16 MHz
External Interrupt 2
Timer Two 8-bit Timer/Counters with Prescaler and Compare Mode. One 16-bit Timer/Counter with Prescaler, Compare and Capture Mode
PWM Channel 6 Channels
ADC Channel 8 Channels 10-bit ADC in TQFP package. 6 Channels 10-bit ADC in PDIP package
USART 1
SPI 1
TWI Phlilips I2C compatible

如果你是第一次接觸單晶片,表中很多名詞你可能不認識。不用擔心,這張表你現在只要大概瀏覽一下即可,這些周邊之後會一個個介紹。

ATmega328 的晶片封裝(IC package)

一般來說,晶片的封裝分成 PDIP 和 TQFP 兩種封裝。

註:

以 Arduino UNO 所用的 ATmega328 為例,它屬於 PDIP 封裝,其 I/O 腳位配置圖 (Pinout) 如下:

而 ATmega328 TQFP 封裝的 I/O 腳位配置圖 (Pinout) 如下:

2. I/O Ports

Arduino 板子所用的晶片 (以 ATmega8 和 ATmega168/328 為例) 有三個 8-bit 的 PORTs :

Arduino 與 AVR 晶片腳位對應表

底下這張圖顯示 Arduino 跟 ATmega8 之間的腳位對應關係:

▲ 圖片來源: arduino.cc

▲ 圖片來源: arduino.cc

例如,Arduino 的 pin 13 對應的腳位為 PB5。

而底下是 Arduino 跟 ATmega168 的腳位對應關係圖:

▲ 圖片來源: arduino.cc

▲ 圖片來源: arduino.cc

Arduino UNO 和 Arduino Duemilanove 用的晶片都是 ATmega328,Arduino Diecimila 用的是 ATmega168,而早期的 Arduino USB 則是使用 ATmega8。

註: ATmega328 跟 ATmega168 的腳位配置 (Pinout) 是一模一樣的。

I/O Ports 暫存器簡介

AVR 晶片每個 port 都受三個暫存器控制,分別是 (x 代表 B, C, D):

實驗目的

讓一顆燈號閃爍,每隔一秒切換一次燈號。

材料

接線

把 LED 接到 Arduino 板子上,LED 長腳 (陽極) 接到 pin13,短腳 (陰極) 接到 GND,如下圖:

程式碼

先來看 Arduino 版本的 Blink 程式:

/*
 * Blink.pde: 讓一顆燈號閃爍,每隔一秒切換一次燈號
 */

const int ledPin =  13;         // LED pin

void setup() {                
  pinMode(ledPin, OUTPUT);      // 把 ledPin 設置成 output pin 
}

void loop() {
  digitalWrite(ledPin, HIGH);   // 打開 LED 燈號 
  delay(1000);                  // 延遲一秒鐘
  digitalWrite(ledPin, LOW);    // 關閉 LED 燈號 
  delay(1000);                  // 延遲一秒鐘
}

這是 Arduino 的入門程式,相信你應該很熟悉。

從 Arduino 與 AVR 腳位對應關係圖可知,PB5 就是 pin 13,所以現在我們可以改用 I/O Ports 暫存器重新改寫程式:

/*
 * Blink.pde:  讓一顆燈號閃爍,每隔一秒切換一次燈號
 * schematic:
 *   Connect a LED on PB5 (Arduino pin 13)
 */

void setup()
{
  DDRB |= (1 << 5);      // 把 PB5 設置成 output pin 
}

void loop()
{
  PORTB |= (1 << 5);     // 打開 LED 燈號
  delay(1000); 
  PORTB &= ~(1 << 5);    // 關閉 LED 燈號 
  delay(1000);
}

PB5 是 PortB 的 bit 5,所以我們用 (1 << 5) 當作位元遮罩 (bit mask)。 要特別注意的是,在設定 DDRx 暫存器的時候,1 是代表 OUTPUT,而 0 是代表 INPUT。

前一篇的程式,如果用 _BV() 巨集改寫的話,會變得比較清晰易讀:


/*
 * BlinkWithBV.pde:  讓一顆燈號閃爍,每隔一秒切換一次燈號,使用 _BV() 巨集
 * schematic:
 *   Connect a LED on PB5 (Arduino pin 13)
 */

/* 在 avr-libc 中的 sfr_defs.h 有這樣的定義:
#define _BV(bit) (1 << (bit))
*/

void setup()
{
  DDRB |= _BV(5);      // 把 PB5 設置成 output pin
}

void loop()
{
  PORTB |= _BV(5);     // 打開 LED 燈號
  delay(1000); 
  PORTB &= ~_BV(5);    // 關閉 LED 燈號
  delay(1000);
}

BV 是 Bit Value 的縮寫。_BV() 巨集的定義為:

#define _BV(x) (1 << x)

所以 _BV(5) 就是 bit 5,由此我們馬上可以聯想到,DDRB |= _BV(5) 這行代表的是「把 PB5 這支腳位設置設 OUTPUT」(註: 1 是 OUTPUT,0 是 INPUT)。

位元遮罩

我們可以把前面的程式稍微改良一下:

/*
 * BlinkWithbitMask.pde: 讓一顆燈號閃爍&#65292;每隔一秒切換一次燈號&#65292;使用 _BV() 巨集
 * schematic:
 *   Connect a LED on PB5 (Arduino pin 13)
 */

/* 在 avr-libc 中的 sfr_defs.h 有這樣的定義:
#define _BV(bit) (1 << (bit))
*/

#define bitMask _BV(5)  // bit mask of PB5

void setup()
{
  DDRB |= bitMask;      // 把 PB5 設置成 output pin
}

void loop()
{
  PORTB |= bitMask;     // 打開 LED 燈號
  delay(1000); 
  PORTB &= ~bitMask;    // 關閉 LED 燈號
  delay(1000);
}

利用 bit mask (位元遮罩) 的概念,把 _BV(5) 定義成 bitMask 巨集,這麼一來,當 LED 接到別支腳位,不再是 PB5 時,程式只需要調整 bitMask 巨集,其它地方都不用修改。

Arduino 腳位的位元遮罩對照表

利用位元遮罩的概念,我們可以進一步這樣做:


/*
 * digitalPin_to_bitmask.pde:  
 *  讓一顆燈號閃爍&#65292;每隔一秒切換一次燈號&#65292;使用 _BV() 巨集與位元遮罩 
 * schematic:
 *   Connect a LED on PB5 (Arduino pin 13)
 */

// bit masks of Arduino digital pins
const byte digital_pin_to_bit_mask[] = {
    _BV(0), /* 0, port D */
    _BV(1),
    _BV(2),
    _BV(3),
    _BV(4),
    _BV(5),
    _BV(6),
    _BV(7),
    _BV(0), /* 8, port B */
    _BV(1),
    _BV(2),
    _BV(3),
    _BV(4),
    _BV(5),
    _BV(0), /* 14, port C */
    _BV(1),
    _BV(2),
    _BV(3),
    _BV(4),
    _BV(5),
};

const int ledPin = 13;   // PB5
const byte bitMask = digital_pin_to_bit_mask[ledPin]; // will get _BV(5)

void setup()
{
  DDRB |= bitMask;      // 把 PB5 設置成 output pin
}

void loop()
{
  PORTB |= bitMask;     // 打開 LED 燈號
  delay(1000); 
  PORTB &= ~bitMask;    // 關閉 LED 燈號
  delay(1000);
}

在這個範例中,我們建了一個 Arduino digital pin 的位元遮罩對照表 digital_pin_to_bit_mask。我們知道, pin 13 就是 PB5,有了這張對照表,就可以很容易算出 pin 13 的位元遮罩,像這樣:

const int ledPin = 13;   // PB5
const byte bitMask = digital_pin_to_bit_mask[ledPin]; // will get _BV(5)

這會得到 _BV(5),也就是 PB5 的位元遮罩,亦即 pin 13 的位元遮罩。

動動腦

雖然上面的程式可以算出 Arduino digital pin 的位元遮罩,但是如果進一步思考會發現一個問題:因為 setup() 和 loop() 已經固定使用 PORTB,所以即便算出其它腳位的位元遮罩,比如座落在 PD2 的 pin 2 (位元遮罩是 _BV(2) ),到時真正受影響的卻還是 PB2。因此,如果你想控制 pin 2,除了改 ledPin 外,你還得修改 setup() 和 loop() 把其中的 DDRB 換成 DDRD,而 PORTB 換成 PORTD,這樣才行。

想想看,這個問題要怎麼解決才好呢?

2.3 Button

實驗目的

使用按鍵 (PushButton) 控制 LED,按鍵被按下時打開 LED,按鍵放開時關掉 LED。 材料

接線

把 LED 接到 pin 13,長腳 (陽極) 接到 pin 13,短腳 (陰極) 接到 GND 把 pushbutton 一支腳接到 +5V,另一支腳接到 pin 2 同時接一顆 10K 電阻連到 GND

程式碼

先來看 Arduino 版本的 Button 程式:


/*
 * Button.pde: 使用按鍵 (PushButton)控制 LED 燈號的開關
 */

const int buttonPin = 2;                 // 按鈕(pushbutton)
const int ledPin = 13;                   // LED

int buttonState;                         // 用來儲存按鈕狀態

void setup() {
  pinMode(ledPin, OUTPUT);               // 把 ledPin 設置成 OUTPUT
  pinMode(buttonPin, INPUT);             // 把 buttonPin 設置成 INPUT
}

void loop(){
  // 讀取按鈕的狀態
  buttonState = digitalRead(buttonPin);

  // 檢查按鈕是否被按下
  // 是的話&#65292;buttonState 會是 HIGH
  if (buttonState == HIGH) {     
    digitalWrite(ledPin, HIGH);          // 打開 LED
  } 
  else {
    digitalWrite(ledPin, LOW);           // 關閉 LED
  }
}

從腳位對應關係圖可知,PB5 就是 pin 13,而 PD2 就是 pin 2,所以現在我們可以改用 I/O Ports 暫存器重新改寫程式:


/*
 * Button.pde: 使用按鍵 (PushButton)控制 LED 燈號的開關
 * 
 * Pin map:
 *  Arduino pin 13 = PB5
 *  Arduino pin  2 = PD2 
 */

// 底下兩個常數程式沒有用到,只是當作參考 
const int buttonPin = 2;           // 按鈕(pushbutton)
const int ledPin = 13;             // LED

#define buttonPinBitMask  _BV(2)   // pin 2  = PD2
#define ledPinBitMask     _BV(5)   // pin 13 = PB5

int buttonState;                   // 用來儲存按鈕狀態

void setup()
{
  DDRB |= ledPinBitMask;           // 把 ledPin 設置成 OUTPUT
  DDRD &= ~buttonPinBitMask;       // 把 buttonPin 設置成 INPUT
}

void loop()
{    
  // 讀取按鈕的狀態
  if (PIND & buttonPinBitMask) {
    buttonState = HIGH;
  } else {
    buttonState = LOW;     
  }

  // 檢查按鈕是否被按下
  // 是的話&#65292;buttonState 會是 HIGH  
  if (buttonState == HIGH) {
    PORTB |= ledPinBitMask;         // 打開 LED
  } else {
    PORTB &= ~ledPinBitMask;        // 關閉 LED
  }
}

程式為 PD2 和 PB5 兩支腳位分別定義了 buttonPinBitMask 和 ledPinBitMask 兩個位元遮罩,利用這兩個位元遮罩進行位元運算,透過 DDRx 暫存器決定腳位是 INPUT 或 OUTPUT 模式,用 PINx 暫存器讀取按鈕的腳位狀態,然後用 PORTx 暫存器控制 led 腳位的輸出訊號。 _BV() 巨集我們在前面已經介紹過。

到此相信你對 AVR 晶片的 I/O Ports 應該已經有足夠的了解了。

延伸閱讀

【本文作者為馬萬圳,原文網址為: http://coopermaa2nd.blogspot.tw/2011/07/from-arduino-to-avr.html , http://coopermaa2nd.blogspot.tw/2011/07/1-avr.html , http://coopermaa2nd.blogspot.tw/2011/07/2-io-ports.html , http://coopermaa2nd.blogspot.tw/2011/04/21-blink-part-1.html , http://coopermaa2nd.blogspot.tw/2011/07/21-blink-part2.html , http://coopermaa2nd.blogspot.tw/2011/07/22-button.html ,由陳鍾誠編輯後納入本雜誌】