การเขียนโปรแกรมภาษา C สำหรับ AVR (ATmega328P): ตอนที่ 8#

Keywords: Atmel AVR MCU, ATmega328P, Bare-metal C Programming, AVR-GCC, avr-libc


การเขียนโปรแกรมภาษา C แบบ Bare-Metal และการใช้ไลบรารี avr-libc#

บทความในตอนที่ 8 สาธิตตัวอย่างการเขียนโค้ดภาษา C เพื่อใช้งานวงจรที่เรียกว่า Timer/Counter1 (วงจรตัวนับหมายเลข 1) ภายในชิป ATmega328P และนำเสนอเนื้อหาต่อเนื่องจากตอนที่แล้ว ดังนั้นแนะนำให้ศึกษาเนื้อหา ตอนที่ 7 ด้วยเช่นกัน

รายละเอียดเกี่ยวกับรีจิสเตอร์ที่เกี่ยวข้องกับการทำงานของ Timer/Counter1 สามารถดูได้เอกสารของผู้ผลิต ATmega328P Datasheet

 


Timer/Counter1: Fast PWM (Single Channel)#

โค้ดนี้สาธิตการทำงานของวงจร Timer/Counter1 ในโหมด Fast PWM โดยจะต้องใช้ขาเอาต์พุตเป็นขา PB1 / OC1A

ในโหมดนี้ จะต้องมีการตั้งค่าให้รีจิสเตอร์ ICR1 เพื่อใช้เป็นค่าสูงสุด TOP เมื่อตัวนับ TCNT1 นับจาก 0 ถึงค่าที่กำหนดไว้ใน ICR1 จะเริ่มต้นนับใหม่ ดังนั้นการกำหนดค่าของ ICR1 เป็นการกำหนดคาบของสัญญาณ PWM

ในการกำหนดความกว้างช่วงที่เป็น HIGH หรือ เรียกว่า PWM Duty Cycle จะต้องตั้งค่าในรีจิสเตอร์ OCR1A โดยที่ค่าของรีจิสเตอร์นี้ต้องไม่มากกว่าค่าในรีจิสเตอร์ ICR1

ถ้าตั้งค่าของ OCR1A มีค่าเป็นครึ่งหนึ่งของ ICR1 ก็จะได้ PWM Duty Cycle เท่ากับ 50% สำหรับสัญญาณที่ขาเอาต์พุต PB1 / OC1A

ในตัวอย่างนี้ตั้งค่า ICR1 ให้ได้ PWM Frequency เท่ากับ 1kHz หรือ มีคาบเท่ากับ 1000 usec และตั้งค่า OCR1A ให้ได้ PWM Duty Cycle 25% (ความกว้างพัลส์เท่ากับ 250 usec)

#include <avr/io.h>

#define PWM_PIN    (PB1)     // Use the OCR1A pin
#define PRESCALER  (64)      // Define prescaler value as 64
#define FREQ_HZ    (1000UL)  // Define the frequency as 1000 Hz
#define TOP_VALUE  (F_CPU/(FREQ_HZ * PRESCALER)) 

void initTimer1() {
  TCCR1A = TCCR1B = 0;
  TCNT1 = 0x000;
  // Set TOP value for the PWM period
  ICR1  = (uint16_t)(TOP_VALUE-1); 
  // Set OCR1A value for 25% duty cycle
  OCR1A = (uint16_t)(TOP_VALUE/4); 
  // Configure Timer1 in Fast PWM mode (mode 14) with prescaler of 64 
  // Set non-inverted output on OC1A
  // Use Fast PWM mode and set prescaler to 64
  TCCR1A |= (1<<COM1A1) | (1<<WGM11); 
  TCCR1B |= (1<<WGM13) | (1<<WGM12) | (1<<CS11) | (1<<CS10); 
}

int main(void) {
  // Set the DDR for PB1 / OC1A pin as output
  DDRB |= (1 << PWM_PIN);
  initTimer1(); // Initialize Timer1
  while (1) {}
  return 0;
}

รูป: การวัดคาบของสัญญาณเอาต์พุต (วัดได้ประมาณ994.67 usec ใกล้เคียง 1000 usec โดยทฤษฎี)

รูป: การวัดความกว้างพัลส์ของสัญญาณเอาต์พุต (วัดได้ประมาณ251.67 usec ใกล้เคียง 250 usec โดยทฤษฎี)

 


Timer/Counter1: Fast PWM (Dual Channels)#

โค้ดตัวอย่างนี้สาธิตการใช้วงจร Timer/Counter1 ในโหมด Fast PWM เพื่อสร้างสัญญาณ PWM จำนวน 2 ช่อง ที่มีความถี่เท่ากัน โดยเลือกตัวหารความถี่เป็น /1 และตั้งค่า ICR1 เท่ากับ 7 ดังนั้นจะได้ความถี่ของสัญญาณ PWM เท่ากับ 16MHz/8 = 2MHz หรือ มีคาบเท่ากับ 0.5 usec (500 nsec)

#include <avr/io.h>

#define PERIOD_CYC (8)

void initTimer1() {
  TCCR1A = TCCR1B = 0;
  TCNT1 = 0x000;
  ICR1  = (PERIOD_CYC-1);
  OCR1A = (2)-1;            // for OC1A (2/8 = 25% duty cycle)
  OCR1B = (PERIOD_CYC/2)-1; // for OC1B (4/8 = 50% duty cycle)
  // Set timer1 in Fast PWM mode (mode 14)
  TCCR1A |= (1<<WGM11) | (1<<COM1A1) | (1<<COM1B1);
  TCCR1B |= (1<<WGM13) | (1<<WGM12) | (1<<CS10);
}

int main() {  
  DDRB |= (1 << PB1); // Set pin PB1 (D9 pin) as output
  DDRB |= (1 << PB2); // Set pin PB2 (D10 pin) as output
  initTimer1();
  while(1) {}
  return 0;
}

รูป: การวัดคาบของสัญญาณเอาต์พุต (วัดได้ประมาณ500 usec)

รูป: การวัดความกว้างของพัลส์ของสัญญาณเอาต์พุต PB1 (วัดได้ประมาณ250 usec)

รูป: การวัดความกว้างของพัลส์ของสัญญาณเอาต์พุต PB2 (วัดได้ประมาณ125 usec)

 


Timer/Counter1: 8-bit Fast-PWM (Mode 5)#

การทำงานในโหมด Fast-PWM ยังมีให้เลือกอีกหลายแบบ เช่น ความละเอียด ตั้งแต่ 8 บิต จนถึง 10 บิต และมีค่าสูงสุดตามจำนวนบิต ถ้าใช้ความละเอียด 8 บิต ค่าสูงสุดของ TCNT1 ที่จะนับได้คือ 0x00FF (255)

โค้ดตัวอย่างนี้สาธิตการทำงานในโหมด 8-bit Fast-PWM และสร้างเอาต์พุตที่ขา PB1 และ PB2 ที่มีความถี่เท่ากัน แต่มีความกว้างพัลส์ต่างกัน (ใช้งาน Non-inverting Output Compare A / B Match)

ในตัวอย่างนี้ได้ตั้งค่าตัวหารความถี่ /64 ดังนั้นความถี่ของสัญญาณ PWM เท่ากับ 16MHz/64/256 = 976.56 Hz หรือ มีคาบเท่ากับ 1024 usec

#include <avr/io.h>

#define PWM_A_PIN  (PB1)
#define PWM_B_PIN  (PB2)

// 16MHz/64/256 = 976.56 Hz or 1.024 msec
void initTimer1() {
  // Set Fast PWM (Mode 10),
  // 8-bit mode and non-inverting OC1A and OC1B
  TCCR1A = (1<<COM1A1) | (1<<COM1B1) | (1<<WGM10); 
  TCCR1B = (1<<WGM12) | (1<<CS11) | (1<<CS10); // Set prescaler /64
  OCR1A = (1<<8)*1/4;  // 25% Duty Cycle 
  OCR1B = (1<<8)*3/4;  // 75% Duty Cycle 
}

int main() {
  DDRB |= (1 << PWM_A_PIN);   // Set PB1/OC1A (pin 9) as output
  DDRB |= (1 << PWM_B_PIN);   // Set PB2/OC1B (pin 10) as output
  initTimer1();
  while (1) {}
  return 0;
}

รูป: การวัดคาบของสัญญาณเอาต์พุต PB1 / PB2 (วัดได้ประมาณ1022.62 usec)

 


Timer/Counter1: PWM Phase-Correct (Mode 10)#

โค้ดตัวอย่างนี้สาธิตการใช้งาน Timer/Counter1 ในโหมด PWM Phase-Correct (Mode 10) และมีค่าบิต WGM1[3:0]="1010" ในโหมดนี้ รีจิสเตอร์ของตัวนับ TCNT1 จะนับขึ้นจนถึงค่าสูงสุด (TOP) แล้วนับลงจนถึงค่าต่ำสุด (0) ดังนั้นจึงเป็นการนับขึ้นและลงในสองทิศทางสำหรับการนับหนึ่งรอบ (Dual-Slope Operation) แตกต่างจากโหมด Fast PWM ซึ่งเป็นการนับขึ้นในทิศทางเดียว (Single-Slope Operation)

การเลือกโหมดเพื่อกำหนดสถานะลอจิกของขาเอาต์พุตที่ขา PB1 / OC1A ได้ตั้งค่าบิต COM1A[1:0]="10" ทำงานแบบ Non-inverting Compare Output A มีลักษณะดังนี้

  • เมื่อตัวนับ TCNT1 นับขึ้นได้เท่ากับ OCR1A จะทำให้เอาต์พุตเปลี่ยนเป็นลอจิก LOW
  • เมื่อนับถึงค่าสูงสุด แล้วนับลงมาจนได้ค่าเท่ากับ OCRA1 จะทำให้เอาต์พุตเปลี่ยนเป็นลอจิก HIGH

ดังนั้นถ้ากำหนดค่าในรีจิสเตอร์ OCR1A น้อยลง ก็จะได้ความกว้างพัลส์ลดลงเช่นกัน

ในตัวอย่างนี้ได้เลือกตัวหารความถี่ให้เป็น /8 ดังนั้นจะนับด้วยความถี่ 16MHz/8 = 2MHz และตั้งค่า ICR1 ซึ่งเป็นค่าสูงสุดในการนับไว้เท่ากับ 20000 (และคูณด้วยสองสำหรับหนึ่งรอบการนับหรือคาบ) และจะได้สัญญาณ PWM ที่มีความถี่ 50Hz หรือ คาบเท่ากับ 20 msec และตั้งค่า OCR1A เพื่อให้ได้ความกว้างพัลส์เท่ากับ 1.5 msec (คิดเป็น 7.5%)

#include <avr/io.h>
#include <avr/interrupt.h>

#define PWM_PIN    (PB1)
#define FREQ_HZ    (50UL)
#define PRESCALER  (8)
#define TOP_VALUE  (F_CPU/(2 * PRESCALER * FREQ_HZ))

void initTimer1() {
  TCCR1A = TCCR1B = 0;
  TCNT1 = 0x0000;
  // Set the ICR1 register to define the TOP value
  ICR1 = (uint16_t)(TOP_VALUE-1);
  // Set the OCR1A register to define the PWM duty cycle
  OCR1A = (uint16_t)(TOP_VALUE*15/200); // 7.5%
  // Set Timer1 in Phase-correct PWM mode (mode 10)
  TCCR1A |= (1 << WGM11);
  TCCR1B |= (1 << WGM13);
  // Set the output compare mode to clear OC1A on compare match 
  // and set on TOP (non-inverting)
  TCCR1A |= (1 << COM1A1);
  // Set the prescaler to 8 (CLK/8)
  TCCR1B |= (1 << CS11);
}

int main(void) {
  // Set the DDR for OC1A pin as output
  DDRB |= (1 << PWM_PIN);
  initTimer1(); // Initialize Timer1
  while (1) {}
  return 0;
}

รูป: การวัดคาบของสัญญาณเอาต์พุต (วัดได้ประมาณ19.97 msec ใกล้เคียงกับ 20 msec โดยทฤษฎี)

รูป: การวัดความกว้างของพัลส์ของสัญญาณเอาต์พุต (วัดได้ประมาณ1.5 ms)

 


กล่าวสรุป#

บทความนี้ได้นำเสนอตัวอย่างการเขียนโค้ดภาษาซีสำหรับ ATmega328P เพื่อใช้งานวงจร Timer/Counter1 และสร้างสัญญาณ PWM เช่น การทำงานในโหมด Fast PWM และ PWM Phase-Correct

 


This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.

Created: 2023-05-05 | Last Updated: 2023-05-05