Timer/Counter trên AVR/Arduino

Timer/Counter là module hoạt động độc lập và không thể thiếu của bất kỳ Microcontroller nào. Chức năng của Timer/Counter gồm: định thời, đếm sự kiện, tạo xung PWM,....

Như các bạn đã biết, Arduino là một nền tảng hướng tới sự đơn giản, giúp cho việc hiện thực hóa các ý tưởng dễ dàng hơn rất nhiều, nhưng cũng vì thế mà chúng ta sẽ không thể khai thác hết được sức mạnh của vi điều khiển nằm trên board Arduino. Điều mà mình cảm thấy tiếc nhất là sự thiếu sót của các Interrupt Vector trong môi trường Arduino (Arduino hiện chỉ có built-in function hỗ trợ External Interrupts).

Trong bài viết này, mình sẽ giới thiệu với các bạn cách sử dụng Timer/Counter trên Arduino và một số Interrupt của các Timer/Counter này.

1. Chuẩn bị

  1. 1x board Arduino (mình dùng Arduino UNO R3 với chip ATmega328p).
  2. Một vài con LED và điện trở 220 → 560 Ohm.
  3. ATmega328p Datasheet.

2. Giới thiệu

Trên chip Atmega328p của Arduino có 3 bộ Timer/Counter là: Timer/Counter0 (8bit), Timer/Counter1 (16 bit), Timer/Counter2 (8 bit).

Để không làm ảnh hưởng đến hàm delay() và millis() của Arduino, mình sẽ không đề cập đến Timer/Counter0.

Như mình đã giới thiệu, Timer/Count có chức năng: Đếm sự kiện, Định thời và tạo xung PWM, để giữ mọi thứ đơn giản, mình chỉ giới thiệu chức năng cơ bản của Timer/Counter khi lập trình trên Arduino là "Định thời" (Arduino đã hỗ trợ hàm built-in analogWrite để tạo xung PWM nên chúng ta cũng không đề cập đến nữa).

  • Timer/Counter1: là 1 bộ Timer/Counter đa năng 16 bit, gồm 5 chế độ hoạt động.
  • Timer/Counter2: là 1 bộ Timer/Counter 8 bit, gồm 4 chế độ.

Trong pham vi bài viết mình sẽ giới thiệu Normal Mode và Clear Timer on Compare Match (CTC) mode trên Timer/Counter1 và Timer/Counter2.

À, để thuận tiện, mình sẽ viết tắt Timer/Counter thành T/C.

Trước khi bắt đầu, có 1 số định nghĩa quan trong chúng ta cần rõ:

  • BOTTOM: là giá trị thấp nhất mà 1 T/C đạt được, tất nhiên BOTTOM luôn bằng 0.
  • MAX: là giá trị lớn nhất mà 1 T/C có thể đạt được, ở thanh ghi 8 bit giá trị MAX = 2^8 -1 = 255, ở thanh ghi 16 bit giá trị MAX = 2^16 - 1 = 65535. Và tất nhiên giá trị MAX cũng là cố định với từng T/C.
  • TOP: là giá trị đỉnh mà tại có T/C thay đổi trạng thái, giá trị TOP không nhất thiết phải bằng MAX mà có thể thay đổi bằng các thanh ghi. Chúng ta sẽ tìm hiểu sau.
  • Interrupt: (còn gọi là Ngắt) là 1 chương trình có độ ưu tiên cao nhất, được thực hiện ngay lập tức khi có tín hiệu Interrupt. Để hiểu thêm, các bạn hỏi google nhé!

Bảng 1: Interrupt Vectors của Timer/Counter trên ATmega328

3. Timer/Counter 1

3.1 Giới thiệu các thanh ghi

3.1.1 Thanh ghi TCNT1 (Timer/Counter 1 Register)

Là thanh ghi 16 bit, lưu giữ giá trị của Timer/Counter1, cho phép đọc-ghi trực tiếp, do đó, chúng ta có thể thực hiện các phép gán hoặc thay đổi giá trị của TCNT1.

3.1.2 Thanh ghi TCCR1B (Timer/Counter 1 Control Register B)

Là 1 trong 2 thanh ghi điều khiển hoạt đông của Timer/Counter1 (cùng với TCCR1A, nhưng với những mục đích đơn giản, chúng ta chỉ cần thanh ghi TCCR1B).

Bảng 2: Thanh ghi TCCR1B

Trong thanh ghi TCCR1B chúng ta chỉ cần sử dụng 3 bit CS10, CS11, CS12 để lựa chọn xung nhịp cho T/C1. Chúng ta sẽ tham khảo bảng này:

Bảng 3: Mô tả Clock Select Bit trên thanh ghi TCCR1B

Theo mặc định, chip Atmega328p trên Arduino chạy ở 16MHz, prescaler = 64. Điều này có nghĩa là: theo mặc định, các bộ T/C trên Arduino sẽ có tần số hoạt động là 16MHz/64 = 250kHz. 

3.1.3 Thanh ghiTIMSK1 (Timer/Counter1 Interrupt Mask Register)

Là thanh ghi lưu giữ các Interrupt Mask của T/C1. Đây là thanh ghi giúp chúng ta thực hiện các Timer Interrupt. Trên thanh ghi TIMSK1 chúng ta cần chú ý các bit sau:

Bảng 4: Thanh ghi TIMSK1 (Timer/Counter1)

  • bit 5 - ICIE1: Input Capture Interrupt Enable - Cho phép ngắt khi dùng Input Capture.
  • bit 2 - OCIE1B: Output Compare Interrupt Enable 1 channel B -  Cho phép ngắt khi dùng Output Compare ở channel B.
  • bit 1 - OCIE1A: Output Compare Interrupt Enable 1 channel A -  Cho phép ngắt khi dùng Output Compare ở channel A.
  • bit 0 - TOIE1: Overflow Interrupt Enable 1 - Cho phép ngắt khi xảy ra tràn trên T/C.

(Các bạn cứ bình tĩnh, những cái như Output Compare, Input Capture, Overflow mình sẽ giới thiệu ở bên dưới).

3.1.4 Thanh ghi OCR1A và OCR1B (Output Compare Register channel A và channel B)

Lưu giữ giá trị so sánh ở kênh A và kênh B: khi T/C1 hoạt động, giá trị TCNT1 được tăng dần, giá trị này liên tục được so sánh với các giá trị trong thanh ghi OCR1A và OCR1B, việc so sánh này chính là "Output Compare", khi giá trị của TCNT1 bằng giá trị của OCR1A (hoặc OCR1B) thì "Match" xảy ra, lúc này sẽ có 1 Interrupt được thực hiện ( nếu đã được Enable ở thanh ghi TIMSK1).

3.1.5 Thanh ghi ICR1 (Input Capture Register 1)

Giá trị của thanh ghi ICR1 sẽ được cập nhật theo thanh ghi TCNT1 mỗi lần có sự kiện xảy ra trên chân ICP1 (tương ứng là chân digital 8 của Arduino). Chức năng này mình sẽ giới thiệu trong 1 bài viết khác.

3.2 Các chế độ của Timer/Counter 1

Bảng 5: Waveform Generation Mode Bit (Timer/Counter1)

Mình sẽ giới thiệu 2 mode cơ bản nhất của T/C1 là: Normal ModeCTC Mode.

3.2.1 Normal Mode

Đây là chế độ hoạt động đơn giản nhất của T/C1 (mode 0), giá trị của thanh ghi TCNT1 sẽ tăng từ 0 (BOTTOM) đến 65535 (MAX) và quay về 0. Nếu chúng ta gán trước cho TCNT1 một giá trị nào đó thì TCNT1 sẽ bắt đầu đếm từ giá trị này. 

Ví dụ: Bạn muốn viết 1 chương trình để đọc dữ liệu từ cảm biến nhiệt mỗi 0.1s, nhưng trong thân chương trình lại có vài hàm delay(), do đó sẽ không đảm bảo là bạn cập nhật được giá trị nhiệt độ mỗi 0.1s nếu chỉ dùng hàm if và hàm millis(). Phương án ở đây là chúng ta sẽ dùng Interrupt của Timer/Counter.

Theo mặc định, chip Atmega328p trên Arduino chạy ở 16MHz, prescaler = 64, vì vậy thời gian để TCNT1 tăng lên 1 đơn vị  là 64/16MHz = 4us, thời gian để T/C1 đếm từ 0 đến 65535 là 4us*65536 = 0.262144s, mà thời gian chúng ta cần tạo là 0.1s (thỏa mãn vì 0.1 < 0.262144), do đó ta cần 0.1s/4us = 25000 lần đếm. Giá trị ban đầu của TCNT1 = 65536 - 25000 = 40536.

3.2.1.1 Lập trình
#include <avr/interrupt.h>
#define sensor A0

volatile int temp;

void setup()
{
    Serial.begin(9600);
    cli();                                  // tắt ngắt toàn cục
    
    /* Reset Timer/Counter1 */
    TCCR1A = 0;
    TCCR1B = 0;
    TIMSK1 = 0;
    
    /* Setup Timer/Counter1 */
    TCCR1B |= (1 << CS11) | (1 << CS10);    // prescale = 64
    TCNT1 = 40536;
    TIMSK1 = (1 << TOIE1);                  // Overflow interrupt enable 
    sei();                                  // cho phép ngắt toàn cục
}

void loop()
{
    /* add main program code here */
}


ISR (TIMER1_OVF_vect) 
{
    TCNT1 = 40536;
    temp = analogRead(sensor);
    Serial.print(F("Temp:"));
    Serial.println(temp);
}
3.2.1.2 Giải thích
  • #include <avr/interrupt.h> là thư viện Interrupt của AVR.
  • biến temp cần được khai báo volatile vì nó được sử dụng cả ở chương trình chính và ở chương trình ngắt.
  • cli() dùng để tắt ngắt toàn cục.
  • tham khảo bảng Waveform Generation Mode Bit, chúng ta thấy rằng để cài đặt T/C1 ở mode 0, các bit cần được set như sau: WGM13 = 0, WGM12 = 0, WGM11 = 0, WGM10 = 0, vì mặc định các bit này là 0 nên chúng ta không cần quan tâm đến nó ở thanh ghi TCCR1B nữa.
  • TCCR1B |= (1 << CS11) | (1 << CS10) được dùng để cài đặt prescaler = 64. (tham khảo bảng Clock Select Bit).   
  • sei() dùng để bật ngắt toàn cục.
  • các biểu thức  như (1 << CS11) được dùng để set bit CS11 lên 1.
  • ISR (Vector_name) là các trình phục vụ ngắt, trong đó ISR là keyword, Vector_name ở chương trình này là TIMER1_OVF_vect, có nghĩa là "Ngắt tràn trên Timer/Counter1".
  • Ở trong trình phục vụ ngắt, chúng ta cần gán lại giá trị ban đầu cho TCNT1 = 40536 vì lúc này T/C1 đã đếm tràn qua 65535 và về lại 0. Nếu không gán lại TCNT1 = 40536, chúng ta sẽ không tạo được 0.1s như mong muốn.

3.2.2 Clear Timer on Compare Match (CTC) mode

Có 2 CTC mode trên T/C1 là mode 4mode 12

Mình sẽ giới thiệu mode 4 trước. Để chọn mode 4, chúng ta cần set các bit như sau: WGM13 = 0, WGM12 = 1, WGM11 = 0, WGM10 = 0.

CTC mode hoạt động như sau: thanh ghi OCR1A lưu giữ giá trị TOP, thanh ghi TCNT1 bắt đầu đếm từ 0, khi giá trị TCNT1 = OCR1A thì "Compare Match", lúc này ngắt Compare Match có thể xảy ra nếu bit OCIE1A đã được set ở thanh ghi TIMSK1.

Các bạn chú ý là chỉ có thanh ghi OCR1A được sử dụng để lưu giá trị COMPARE trong CTC mode thôi nhé!

Trở lại VD ở Normal Mode, mình sẽ thực hiện với CTC Mode như sau:

3.2.2.1 Lập trình
#include <avr/interrupt.h>
#define sensor A0

volatile int temp;

void setup()
{
    Serial.begin(9600);
    cli();              // tắt ngắt toàn cục
    
    /* Reset Timer/Counter1 */
    TCCR1A = 0;
    TCCR1B = 0;
    TIMSK1 = 0;
    
    /* Setup Timer/Counter1 */
    TCCR1B |= (1 << WGM12) | (1 << CS11) | (1 << CS10); // prescale = 64 and CTC mode 4
    OCR1A = 24999;              // initialize OCR1A
    TIMSK1 = (1 << OCIE1A);     // Output Compare Interrupt Enable Timer/Counter1 channel A
    sei();                      // cho phép ngắt toàn cục
}

void loop()
{
    /* add main program code here */
}
    

ISR (TIMER1_COMPA_vect) 
{
    temp = analogRead(sensor);
    Serial.print(F("Temp:"));
    Serial.println(temp);
}
3.2.2.2 Giải thích
  • Để chọn mode 4, trong phần cài đặt cho thanh ghi TCCR1B ta cần set bit WGM12 lên 1, tức là (1 << WMG12). 
  • Để tạo được 0.1s (ở 16MHz, prescaler = 64) ta cần T/C1 đếm 25000 lần, do đó giá trị TOP = OCR1A = 24999.
  • Để enable Compare Match Interrupt Timer/Counter 1 channel A, chúng ta cần set bit OCIE1A của thanh ghi TIMSK1 lên 1.
  • Ở phần hàm ngắt, ta thay đổi thành ISR (TIMER1_COMPA_vect) cho phù hợp với Compare Match Interrupt T/C1. (Các bạn tham khảo bảng Interrupt Vectors ở phía trên nhé).

Để sử dụng Mode 12, chúng ta chỉ cần thay đổi 1 chút như sau: 

// prescale = 64 and CTC mode 12
TCCR1B |= (1 << WGM12) | (1 << WGM13) | (1 << CS11) | (1 << CS10);

// initialize ICR1
ICR1 = 24999;                                                       

// Input Capture Interrupt Enable Timer/Counter1 channel A
TIMSK1 = (1 << ICIE1);                                             

// Input Capture Interrupt Vector
ISR (TIMER1_CAPT_vect) {...}                                        
3.2.2.3 Ứng dụng chế độ CTC trong đơn giản hóa việc đếm sự kiện ngoài

VD: Mình sẽ dùng cảm biến chuyển động PIR HC-SR501 để đếm số người bước vào phòng, cứ 10 người bước vào thì đèn LED sẽ nháy 1 lần. (Tất nhiên đây chỉ là ví dụ minh họa cho CTC mode, mình không dám chắc hiệu quả trong thực tế của nó)

Chúng ta sẽ nối chân OUT của cảm biến với chân digital 5 (chân T1 của ATmega328) của Arduino.

Lập trình
#include <avr/interrupt.h>
#define LED	10

void setup()
{
    pinMode(LED, OUTPUT);
    cli();
    TCCR1A = 0;
    TCCR1B = 0;
    TIMSK1 = 0;

    /* CTC mode 4, External Clock source from pin T1, clock on falling edge.*/
    TCCR1B |= (1 << WGM12) | (1 << CS12) | (1 << CS11); 
    OCR1A = 9;
    TIMSK1 |= (1 << OCIE1A);
    sei();
}

void loop()
{
    // nothing here  
}

ISR (TIMER1_COMPA_vect)
{
    digitalWrite(LED, 1);
    delay(200);
    digitalWrite(LED, 0);
}
Giải thích

Mỗi khi phát hiện được chuyển động, cảm biến sẽ trả về HIGH của chân OUT, do đó, ta sẽ lấy tín hiệu này làm source clock cho T/C1 bằng cách set các bit CS12 và CS11  của thanh ghi TCCR1B lên 1:

TCCR1B |= (1 << WGM12) | (1 << CS12) | (1 << CS11);

Theo yêu cầu của VD, cứ 10 người đi qua cảm biến thì LED nháy 1 lần, do đó giá trị COMPARE trong thanh ghi OCR1A = 9.

Phần còn lại của đoạn code trên tương tự như VD đầu tiên, các bạn tham khảo lại bảng Waveform Generation Mode Bit (Timer/Counter1) và Clock select bit để hiểu rõ hơn cách set các thanh ghi nhé!

4. Timer/Counter 2

4.1 Các thanh ghi

Trên T/C2 cũng có những thanh ghi tương tự T/C1

4.1.1 Thanh ghi TCNT2 (Timer/Counter 2 Register)

Là thanh ghi 8 bit, lưu giữ giá trị của Timer/Counter2.

4.1.2 Thanh ghi TCCR2A và TCCR2B (Timer/Counter 2 Control Register A và B)

Là 2 thanh ghi điều khiển hoạt động của Timer/Counter2.

Bảng 6: Thanh ghi TCCR2A và TCCR2B (Timer/Counter 2)

Bảng 7: Mô tả Clock Select Bit

4.1.3 Thanh ghi TIMSK2 (Timer/Counter2 Interrupt Mask Register)

Là thanh ghi lưu giữ các Interrupt Mask của T/C2.

Bảng 8: Thanh ghi TIMSK2 (Timer/Counter 2)

4.1.4 Thanh ghi OCR2A và OCR2B (Output Compare Register channel A và channel B)

Lưu giữ giá trị so sánh ở kênh A và kênh B khi T/C2 hoạt động.

Bảng 9: Lưu giữ giá trị so sánh ở kênh A và kênh B khi T/C2 hoạt động.

4.2 Các chế độ hoạt động

Theo bảng Waveform Generation Mode bit, T/C2 có Normal Mode 0CTC mode 2.

Để không trùng lặp nội dụng với T/C1, mình chỉ giới thiệu cách set thanh ghi trong T/C2.

  • Ở Normal Mode, các bạn chỉ cần set các bit CS20, CS21, CS22 trong thanh ghi TCCR2B để chọn prescaler.
  • Ở CTC Mode: ngoài set các bit trong thanh ghi TCCR2B để chọn prescaler, các bạn cần set bit WGM21 lên 1 bằng dòng: TCCR2A |= (1 << WGM21);
  • Cách set thanh ghi TIMSK2 tương tự như TIMSK1: OCIE2A (Output Compare Interrupt Enable 2 Channel A), OCIE2B, TOIE2 (Timer Overflow Interrupt 2 Enable).

5. Kết luận

Hy vọng bài viết này sẽ giúp các bạn có thể lập trình Arduino hiệu quả hơn. Trong bài viết có thể còn sai sót, các bạn cứ còm men khi có thắc mắc nhé!^^

Chúc các bạn thành công và vui cùng Arduino!

6. Tham khảo

Bài 4 - Timer - Counter: http://www.hocavr.com/index.php/en/lectures/timer-counter

lên
32 thành viên đã đánh giá bài viết này hữu ích.
Các dự án được truyền cảm hứng

Select any filter and click on Apply to see results

Các bài viết cùng tác giả

Giao tiếp I2C và sử dụng module Realtime clock DS1307 (module RTC)

Xin chào các bạn, bài viết này của mình sẽ giới thiệu về giao tiếp I2C trên Arduino và sử dụng module Realtime clock DS1307.

  • Giới thiệu về chuẩn giao tiếp I2C.
  • Giao tiếp I2C trên Arduino.
  • Cách sử dụng module Realtime Clock DS1307.
lên
37 thành viên đã đánh giá bài viết này hữu ích.

Debounce cho nút nhấn bằng tụ điện

Xin chào, bài đăng của mình sẽ nói về kỹ thuật DEBOUNCE cho nút bấm. Đây là kỹ thuật rất đơn giản và hiệu quả.

lên
19 thành viên đã đánh giá bài viết này hữu ích.