---
tags: 單晶片
---
# [W10] Timer(E230900)單晶片系統設計與應用
## Arduino講義:Timer 的使用
### ATmega328p 微控制器晶片
ATmega328p是Atmel發展的megaAVR產品線其中一個單晶片產品,也是目前Arduino Uno系列等常使用的8 bits AVR架構微控制器核心。(P代表Picopower,意思是製程更好更省電)
![](https://i.imgur.com/3uZTBIh.png)![](https://i.imgur.com/Ke8xr6m.png)
| Paremeter | Value |
| -------- | -------- |
| MCU | 8-bit AVR |
| Performance | 20 MIPS at 20 MHz|
| Flash memory | 32 KB |
| SRAM | 2KB |
| EEPROM | 1KB |
|Clock speed |16MHz|
|External Interrupt| 2|
|Timer|3|
|Digital I/O|14 (6 for PWM)|
|Analog I/O|6|
> ### ATmega328 微控制器記憶體類型與容量
| 名稱 | 類型 | 容量大小 | 用途|
| -------- | -------- | -------- |---|
|SRAM |揮發性(volatile),資料會在斷電後消失 |2048 byte(2KB) | 資料記憶體、暫存程式運作中所需的資料|
|Flash|非揮發性,資料會在斷電後繼續保存|32468bytes(32KB)|程式記憶體、存放開機啟動程式及使用者自訂的程式碼(開機程式大約佔2KB)|
|EEPROM|非揮發性|1024bytes(1KB)|存放程式的永久性資料|
![](https://i.imgur.com/JqZdYTl.png)
### 更多資訊可以參考ATmega328p的[datasheet](http://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-7810-Automotive-Microcontrollers-ATmega328P_Datasheet.pdf)
### 暫存器(Register)
> ### I/O Ports
> Arduino UNO板所用的晶片ATmega328p有三個 8-bit 的 PORTs :
> B: 對應 Arduino 的 digital pin 8 to 13
> C: 對應 Arduino 的 analog input pin 0 to 5
> D: 對應 Arduino 的 digital pins 0 to 7
>
* [Arduino UNO Schemetic](https://www.arduino.cc/en/uploads/Main/arduino-uno-schematic.pdf)
* ATmega328p微控制器總共有三組數位輸出入埠,分別為PORTB、PORTC、PORTD。每一組輸出入埠都可作為一般的數位輸入(Digital Input)與數位輸出(Digital Output)使用。
* AVR 晶片每個 port 都受三個暫存器控制,分別是 (x 代表 B, C, D):
* **DDRx register(資料方向暫存器)**
Data Direction Register,可讀可寫的暫存器,指定輸入/輸出。
* **Portx register(輸入/輸出暫存器)**
Data Register,透過這類的暫存器設定電壓為高電壓或低電壓。
* **PINx register(讀取腳位暫存器)**
Port Input Pins(Read Only),用來讀取腳位的輸入訊號。
* 根據 datasheet p.72 對應Pin 13 的PB5找到所屬之腳位控制暫存器組
![](https://i.imgur.com/gxIf8Y3.png)
> ### 實作LED Blink
---
* 如下圖所示,在Arduino Uno上Pin 13與ATmega328p的第19腳位相連,並得知其暫存器編號為PB5,且該腳位亦為SPI溝通之SCK腳位。
![](./img/M4lse0a.png)* 以下為LED Blink之原始程式碼:
![](./img/Pk1EaHA.png)* 我們可以藉由Arduino UNO與ATmega328p晶片與暫存器對應之關係來修改程式碼,且會像以函式包裝的原始程式碼一樣工作:
```C++=
void setup() {
DDRB = 0b00100000; //將DDRB中的PB5設定為OUTPUT
}
void loop() {
PORTB = 0b00100000; //將PB5設為HIGH
delay(1000); //delay
PORTB = 0b00000000; //將PB5設為LOW
delay(1000); //delay
}
```
* 我們也可以使用 **位元遮罩(bit mask)** 的方式來描述此段程式碼:(較佳,不會修改其他Bit)
```C++=
void setup(){
DDRB |= (1 << 5); //將PB5設定為OUTPUT
}
void loop(){
PORTB |= (1 << 5); //將PB5設為HIGH
delay(1000);
PORTB &= ~(1 << 5);//將PB5設為LOW
delay(1000);
}
```
**PB5 是 PortB 的 bit 5,所以我們用 (1 << 5) 當作位元遮罩 (bit mask)。**
![](https://i.imgur.com/cmvZApD.png)
要特別注意的是,在設定 DDRx 暫存器的時候,***1 是代表 OUTPUT,而 0 是代表 INPUT。***
---
### 中斷服務常式(Interrupt Service Routine,簡稱ISR)
什麼是 Interrupts?
當你在工作的時候,突然電話鈴聲響起,於是你把手邊工作停下來、接電話、講電話,然後回來繼續做剛剛的工作 -- 這就是所謂的中斷 (Interrupt),而電話便是中斷源。
當中斷腳位的訊號發生改變時,將會觸發執行中斷服務常式。ISR是一個函式程式,由微控制器自動觸發執行的函式。
[Reference:attachInterrupt()](https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/)
![](https://i.imgur.com/PyUV0bw.png)
```c++=
const byte swPin = 2;
const byte ledPin = 13;
volatile boolean state == LOW;
void swISR(){
state = !state;
digitalWrite(ledPin, state);
}
void setup(){
pinMode(ledPin,OUTPUT);
pinMode(swPin, INPUT_PULLUP);
attachInterrupt(0, swISR, CHANGE); // 啟用中斷服務常式
}
void loop(){
}
```
> ### 中斷觸發時機
當擁有外部中斷功能的腳位,其訊號(也就是電壓)改變或處於某訊號時,就會觸發微處理器去執行ISR函式,這個函式我們可以自行定義,再由系統當中斷觸發時自動去執行。
既然觸發的時機是當依據訊號狀態,那麼,就可能會有如下的五種情況:
**1.FALLING(當訊號下降時觸發)**
![](https://i.imgur.com/jvO96pH.png)**2.RISING(當訊號上升時觸發)**
![](https://i.imgur.com/Qf0mm8e.png)**3.CHANGE(當訊號改變時觸發)**
![](https://i.imgur.com/KvcAcHH.png)**4.LOW(當訊號處在低電位時持續觸發)**
![](https://i.imgur.com/2hXLbhl.png)**5.HIGH(當訊號處在高電位時持續觸發)**
![](https://i.imgur.com/4mzdYRW.png)* 第五種「HIGH」只有Leonardo板子有支援
---
**撰寫ISR程式應注意:**
1. 程式本體應簡潔
2. 中斷處理函式沒有接受參數的輸入,也沒有回傳值
3. 在ISR執行期間,所有與時間相關的指令都不會執行,如:millis()、delay()
4. 在ISR執行期間的序列阜通訊程式可能會遺失部分資料,如:Serial.print("Hello"),可能只會傳出hell等
5. 若ISR程式需要改變某變數值,要在宣告變數前加上"volatile"
![](./img/tjXmVse.png)<註>
要使用ISR之前務必要確認:
1.Enable Interrupt
2.對應的Interrupt Mask也有打開(TIMSKx)
---
### volatile 變數宣告
volatile這個變數宣告方式是用於指揮編譯器的指令。
以下面這個程式碼片段為例:
![](./img/ewjpB7H.png)
在編譯器將原始碼轉換成機械碼的過程中,會先掃描整個程式,並且將原始碼最佳化來節省時間(此過程在記憶體中進行)。一般的程式中,最佳化的程式碼不會出現問題;然而,若是在處理包含中斷事件的程式,就會有無法預期的錯誤。
![](https://i.imgur.com/MT29Fn3.png)
![](https://i.imgur.com/nhmrDlG.png)
---
## Timer
* Timer 是一種計時器,可以用來量時間
* 來自石英振盪器脈衝 (pulse) 每一個 clock 會來一次,Timer 的內容會跟著計數遞增。
* ATmega328p內部共有3種timer,分別是:Timer0, Timer1及Timer2
![](https://i.imgur.com/jV40ths.png)* 如果是 8-bit Timer,那麼可以寫入的最大數值是 255 (16-bit 的話是 65535),假如超過了最大數值,Timer 就會自動 reset 為 0,這種情況稱為溢位 (overflow)。Timer overflow 的時候可以引發中斷,如果啟用了 Timer overflow 中斷,那麼你就必須在程式裏提供 ISR 處理中斷。
----
>### Timer暫存器 [datasheet p.108](http://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-7810-Automotive-Microcontrollers-ATmega328P_Datasheet.pdf)
![](https://i.imgur.com/Dab1Yqr.png)
|暫存器|簡寫|用途|
|---|-|-|
|Timer Counter Control Register A|TCCRnA|決定Timer的運作模式|
|Timer Counter Control Register B|TCCRnB|決定Timer的prescale value|
|Timer Counter Register |TCNTn|儲存Timer目前的累積計數(計數器)|
|Output Compare Register A|OCRnA|當TCNTn累積計數到OCRnA的計數時,觸發中斷|
|Output Compare Register B |OCRnB|當TCNTn累積計數到OCRnB的計數時,觸發中斷|
|Timer Counter Interrupt Mask Register|TIMSKn|開啟/結束中斷|
* ATmega328p中具有三個**比對效果**的計時計數器,其中各個Timer可以設定的模式不同。若以Timer1為例,共有三種計時模式:
| 計時模式 | 功能 |
| -------- | -------- |
|TIMER1_COMPA|設定計數參數來觸發|
|TIMER1_OVF|只要overflow就會觸發|
|TIMER1_CAPT|儲存輸入腳位的觸發瞬間時間|
![](https://imgur.com/XoZtVdm.png)
![](https://i.imgur.com/Etxzy9J.png)
---
### 範例
> ### 計算 1 second 的計數器和除頻數
> * 不同的單晶片有不同的MCU時脈,實際上的運用不一定需要這麼高的時脈,這時候便需要進行「除頻」的動作,將MCU的時脈除以除率(prescaler),藉此將Timer的時脈降低。
> * Prescaler (預除器) 是一個用來提供 clock 給 Timer 的電路。MCU clock 頻率通常是 1 MHz, 8 MHz, 16 MHz,而 Precaler 的用途則是除頻。
> * ATmega328中Timer0與Timer1使用的是相同的一組prescaler(1, 8, 64, 256, 1024),而Timer2則是有較多的prescaler選項(1, 8, 32, 64, 128, 256, 1024)。
* 根據datasheet,找出需要的除頻晶振頻率
* ATmega328p內部晶振頻率為16Mhz,1秒暫存器應計數16,000,000次
* 週期為:1 / 16Mhz(s)
* 而暫存器最大長度僅有16bits(65535),因此需要除頻晶振頻率
* 查詢ATmega328p中timer1可用的除頻範圍及暫存器(TCCR1B)設定
* 此處選擇256除頻16MHz時脈,得到16,000,000/256 = ==62500==,代表暫存器可以在256個時脈之後計數一次
* 最後我們可以通過使用中斷模式來使用 1 second 定時器做特定的工作
* 其餘Timer計時的使用方式以此類推
``` c=
void setup()
{
Serial.begin(115200);
DDRB = (1 << 5); // set pin 13 OUTPUT
noInterrupts(); // disable interrupts
TCCR1A = 0; // TCCR1A Reset
TCCR1B = 0; // TCCR1B Reset
TCCR1B |= (1 << CS12); // 256 prescaler
TCNT1 = 3036; // preload timer (65536-62500)
TIMSK1 |= (1 << TOIE1); // enable timer overflow interrupt
interrupts(); // enable interrupts
}
ISR(TIMER1_OVF_vect) // interrupt
{
noInterrupts();
PORTB ^= (1 << 5); // blink
TCNT1 = 3036; // preload timer
interrupts();
}
void loop()
{
Serial.println(TCNT1);
}
```
---
## LAB 1 - 運用Timer1
* 實作目的: 學習閱讀Datasheet,使用Timer1暫存器及I/O暫存器操作。
* 內容要求:使用**Timer1 Register**達成==每兩秒改變LED亮或暗==,藉由==Output Compare A Match Interrupt==中斷模式,並啟用==CTC mode==,**禁止使用delay()**。
* 下圖為500ms計數器的例子,在Datasheet中**p.109 Table15-5 第4個模式** **CTC(Clear Timer on Compare Match Mode)** 模式下,當TCNT1計數器數到一定數值時觸發事件,執行事件為重置為0、反轉LED。
![](https://i.imgur.com/wGcWX3f.png)
``` c=
void setup(){
Serial.begin(115200);
// TODO: set pin 13 as output mode by DDR?
// TODO: disable interrupt
// TODO: reset TIMER1 control register by TCCR1?
// TODO: reset TIMER1 prescaler by TCCR1?
// TODO: set CTC mode by TCCR1?
// TODO: enable Timer1 compare match interrupt by TIMSK?
// TODO: reset Timer1 by TCNT?
// TODO: set compare value by OCR1?
// TODO: enable interrupt
}
ISR(TIMER1_COMPA_vect){
// TODO: change pin 13 potential by PORT?
}
void loop(){
Serial.println(TCNT1);
}
```
---
## LAB 2 - 計時器
使用**Timer1 Register**,完成能夠顯示 **x(sec):y(ms)** 之簡易計時器在LCD螢幕上,**禁止使用delay()**。
---
## LAB 3 - 超聲波測距
使用超聲波模組,**每秒測量一次距離**,並顯示在多工七段顯示器上,請運用Timer之技巧來實現。
### 實驗結果
{%youtube CofhXxQMyMY %}
---
## LAB 4 - Analog Read
使用**ADMUX、ADCSRA及ADCH/L Register**,讀取可變電阻之類比值,並在Serial port顯示出來,**禁止使用analogRead()**。
[](https://garretlab.web.fc2.com/en/arduino/inside/hardware/arduino/avr/cores/arduino/wiring_analog.c/analogRead.html)
![](https://i.imgur.com/KFbYbK0.png =40%x)
- **ADMUX**:p.217
![](https://i.imgur.com/RwcZQUs.png)
- **REFS**:Voltage reference
- **MUX**:Analog channel selection
- **ADCSRA**:p.218
![](https://i.imgur.com/Cr5ZbPx.png)
- **ADEN**:ADC enable
- **ADSC**:ADC Start Conversion
- **ADPS**:ADC prescalar selection
- **ADCL and ADCH**:The ADC Data Register
```C
void setup(){
Serial.begin(115200);
// TODO: set voltage reference as AVCC
// TODO: enable ADC
// TODO: prescale
}
uint16_t ReadADC(byte pin){
// TODO: set channel
// TODO: start conversion
while (bit_is_set(ADCSRA, ADSC)); // TODO: wait until conversion complete
// TODO: return data
}
void loop(){
Serial.print(ReadADC(x)); // x means number of analog in pin
}
```
---
## Lab 5 - PWM
藉由**Timer1 Register**中PWM模式,使用可變電阻調整LED之亮暗,**禁止使用analogRead()及analogWrite()**。
Hint: **Timer1 register**控制**pin9 & 10**之analogWrite()
以下提示為使用Datasheet中**p.109 Table15-5 第14個模式**,歡迎使用不同模式來實作。
```C
void setup(){
Serial.begin(115200);
// TODO: set voltage reference as AVCC
// TODO: enable ADC
// TODO: prescale
// TODO: reset TCCR? and OCR?
// TODO: set to Timer1 to Waveform Generation Mode 14: Fast PWM with TOP set by ICR1
// TODO: set time prescale
// TODO: set TOP by ICR1
// TODO: set clock prescale by TCCR?B
// TODO: enable fast PWM on pin: set OC1B at BOTTOM and clear OC1B on OCR1B compare by TCCR1A
// TODO: set pinMode output
}
uint16_t ReadADC(byte pin){
// TODO: set channel
// TODO: start conversion
while (bit_is_set(ADCSRA, ADSC)); // wait until conversion complete
// TODO: return data
}
void loop(){
// TODO: write analog value by OCR?
}
```
### 實驗結果
{%youtube OGJ2P7nuMLY %}
---
## Bonus 1 - 使用Timer.h
[TIMER函式庫](https://github.com/JChristensen/Timer)
運用timer函式庫,實作出 Lab3超聲波測距
函式庫寫法請參考:[Arduino Timer Library](http://www.doctormonk.com/2012/01/arduino-timer-library.html)
---
:::warning
## Arduino 睡眠模式與看門狗計時器
* Arduino和我們的電腦手機一樣具有睡眠、休眠或是待機的模式。
* 在睡眠模式下系統完全停止運作,僅保留基本偵測功能
* 睡眠模式是開發人員節省電力的一種方法,ATmega328p微控器具有六種睡眠模式,所用的指令須先下載 [**Enerlib**](https://playground.arduino.cc/Code/Enerlib/)。消耗電流指的是ATmega328控制器本身,而非整個Arduino板。
* Arduino內部除了MCU之外,還有記憶體、ADC、序列通訊等模組。在越省電的模式下,在運行的模組就越少。
* 以POWER-DOWN來說,僅剩下**外部中斷**及**看門狗計時器**有在運作。
### 看門狗計時器(Watchdog Timer, WDT)
* 看門狗計時器是微控器內部的「當機」監控器,**若微控器當掉了,它會自動重新啟動微控器**。
* 看門狗是一種專為系統復位而設計的方法。
* 運作原理:在看門狗內部有一個計時器,微處理機必須每隔一段時間就像看門狗發出訊號,來重設計時器的值。倘若看門狗遲遲沒收到微處理機的訊號,計時器仍會持續倒數,直至計數器變成0時,就會認定微處理機已經當機,此時會重新啟動。
* WDT內部有一個除頻器,我們可以設定WDT的值,最長可至8秒,最短則為16ms。
![](./img/bXeOV1a.png)* WDT內部有三種操作模式
* **中斷**- 當WDT超時時,將調用WDT_vect中斷向量。可用於喚醒微睡眠模式,包括最低功耗睡眠模式(SLEEP_MODE_PWR_DOWN),其他定時器不可用。
* **系統復位**-將發生看門狗超時復位,即微控制器將重新啟動。
* **中斷及系統復位**- 首先將調用WDT_vect中斷向量,完成後將發生看門狗超時復位。
---
### 範例程式
* 啟動時,每隔0.5秒點、滅三次位於第13腳的LED。
* LED閃爍完畢後,進入“Power-down(斷電)”睡眠模式。
* 當中斷0(第2腳)的訊號改變時,喚醒Arduino,再次閃爍LED三次,接著再進入睡眠模式。
* PS: 請先把Arduino的數位腳2接高電位(5V或3.3V)
```c=
#include <Enerlib.h>
Energy energy; // 宣告"Energy"程式物件
const byte swPin = 2; // 開關腳位
const byte ledPin = 13; // LED腳位
byte times = 0; // 記錄執行次數
volatile byte state = 0; // 暫存執行狀態
void wakeISR() {
if (energy.WasSleeping()) {
state = 1;
} else {
state = 2;
}
}
void setup() {
Serial.begin(9600);
pinMode(ledPin, OUTPUT);
pinMode(swPin, INPUT);
digitalWrite(swPin, HIGH);
//當中斷0的狀態發生改變時,執行wakeISR
attachInterrupt(0, wakeISR, CHANGE); // 附加中斷服務常式
Serial.println("Running...");
}
void loop()
{
if (state == 1) {
//若中斷發生之前處於休眠狀態
Serial.println("Was sleeping...");
} else if (state == 2) {
//若中斷發生之前處於一般的執行狀態
Serial.println("Was awake...");
}
state = 0;//重設狀態
digitalWrite(ledPin, !digitalRead(ledPin));
delay(500);
times ++;
Serial.println(times);
if (times > 5) { //led亮滅3次,讓Arduino進入休眠模式
times = 0;
Serial.println("Go to sleep...");
energy.PowerDown();//進入最省電的斷電休眠模式
}
}
```
:::info
## 課後習題
1. Arduino上的ATmega328p所採用的是AVR架構;之後課程用到的STM32則是使用ARM架構,比較兩種架構的差別。
2. 分別描述在ATmega328p晶片上的PWM模式:Normal Mode, CTC Mode, Fast PWM Mode,及 Phase Correct PWM Mode。(Hint:參考datasheet 17.4)
:::
:::success
Datasheet中有些暫存器命名方式可以透過所提供的硬體方塊圖來了解其操作原理,當不了解該暫存器的文字說明時,可以利用這些方塊圖來嘗試理解該暫存器運作原理。
:::