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

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


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

บทความในตอนที่ 4 สาธิตตัวอย่างการเขียนโค้ดภาษา C เพื่อใช้งานวงจรที่เรียกว่า USART ภายในชิป ATmega328P มีการใช้คำสั่งหรือฟังก์ชันของไลบรารี avr-libc (Online User Manual) และใช้ Wokwi Simulator ในการจำลองการทำงานของโค้ดเพื่อตรวจสอบความถูกต้องในเบื้องต้น

คำแนะนำ: ถ้าจะลองโค้ดตัวอย่างโดยใช้ Arduino IDE เพื่อคอมไพล์และอัปโหลดไปยังบอร์ดทดลอง Arduino Uno หรือ Nano ให้สร้าง Arduino Sketch และทำให้ไฟล์ .ino ไม่มีโค้ดใด ๆ (Empty Sketch) และให้สร้างไฟล์ main.c เพื่อเขียนโค้ด

 


วงจรสื่อสารข้อมูลแบบบิตอนุกรม: USART#

วงจร "USART" (ย่อมาจากคำว่า Universal Synchronous / Asynchronous Serial Receiver & Transmitter) ซึ่งมีอยู่ภายในชิป ATmega328P และมีเพียงวงจรเดียว (ใช้ชื่ออ้างอิงว่า USART0) แต่ถ้าเป็นชิปรุ่นอื่น ก็อาจมีมากกว่าหนึ่งวงจรให้ใช้งานได้ เช่น ชิป ATmega2560 มีวงจรประเภทนี้ (หรือเรียกว่า Hardware Serial) ให้ใช้งานได้พร้อมกันสูงสุดถึง 4 ชุด

วงจร USART รองรับการทำงานได้ 2 โหมดคือ

  • Asynchronous: ไม่ใช้สัญญาณ Clock ("อะซิงโครนัส") เพื่อกำหนดจังหวะการทำงานสำหรับการรับส่งข้อมูล และใช้ขาสัญญาณ 2 ขา ได้แก่ TxD0 และ RxD0
  • Synchronous: มีการสร้างและใช้สัญญาณ Clock ("ซิงโครนัส") เพื่อกำหนดจังหวะการรับส่งข้อมูลแต่ละบิต (เหมือนกรณีที่มีการสื่อสารด้วยบัส SPI) และใช้ขาสัญญาณ 3 ขา ได้แก่ TxD, RxD และ XCK

โดยทั่วไปแล้ว USART มักจะนิยมใช้งานในรูปแบบ Asynchronous (ดังนั้นจึงเรียกว่า UART หรือ Asynchronous Serial) การส่งข้อมูลบิตของ UART เป็นแบบ Full Duplex รับและส่งข้อมูลบิตได้สองทิศทางพร้อมกัน โดยใช้ขาข้อมูลสำหรับส่งและรับ (Tx และ Rx) อุปกรณ์ทั้งสองฝ่ายที่จะสื่อสารกันด้วย UART จะต้องตกลงใช้ค่า Baud หรืออัตราการรับส่งข้อมูลที่เท่ากัน (มีหน่วยเป็นบิตต่อวินาที) เช่น 9600 หรือ 115200

การส่งข้อมูลหนึ่งเฟรมสำหรับ UART ประกอบด้วยบิตต่าง ๆ ตามลำดับดังนี้ และมีด้วยระยะเวลาในการสื่อสารข้อมูลแต่ละบิตเท่ากัน

  • เริ่มต้นด้วยบิตแรกที่ถูกส่งออกไป เรียกว่า Start Bit (มีค่าบิตเป็น 0 หรือ LOW เสมอ)
  • บิตข้อมูลตามจำนวนที่เลือกใช้ (เช่น มี 8 บิต) เริ่มที่บิต LSB (Least Significant Bit) จะถูกส่งออกไปก่อน ขนาดของเฟรมข้อมูลมีให้เลือกใช้ได้ เช่น 5,6,7,8 หรือ 9 บิต (โดยปรกติก็ใช้ 8 บิต)
  • บิตสำหรับการตรวจสอบข้อมูลด้วยวิธี Bit-level XOR (Exclusive OR) และผลลัพธ์ที่ได้จะถูกนำไปใช้เป็นค่าบิตที่เรียกว่า Parity Bit แบ่งเป็นสองกรณี คือ Even Parity Bit และ Odd Parity Bit หรือไม่มีก็ได้ (No Parity Bit)
  • ปิดท้ายด้วย Stop Bit ซึ่งมี 1 หรือ 2 บิต และแต่ละบิตมีค่าเป็น 1 หรือ HIGH เสมอ (โดยปรกติก็ใช้เพียงหนึ่งบิต)

การตั้งค่าการทำงานของ USART0 ในโหมด Asynchronous (UART) มีดังนี้

  • ให้ตั้งค่าสำหรับ Baud Rate Generator ของวงจร USART0 เพื่อกำหนดอัตราการรับส่งข้อมูล โดยเขียนค่าลงในรีจิสเตอร์ UBRR0 (UART Baud Rate Generator) ให้ถูกต้อง ซึ่งมีขนาด 16 บิต (UBRR0H:UBRR0L) โดยใช้สัญญาณ Clock ภายในระบบ () เช่น ความถี่ 16MHz เป็นต้น
  • เลือกใช้โหมดความเร็ว ซึ่งมีอยู่ 2 โหมด (ขึ้นอยู่กับค่าบิต U20X0 ใน UCSR0A) คือ
    • Normal Speed: บิต U20X0 เป็น 0 และ
    • Double Speed: บิต U20X0 เป็น 1 และ
  • เขียนค่าลงในรีจิสเตอร์ UCSR0C เพื่อกำหนดขนาดฟรมข้อมูล (Frame Format) และบิตที่ตามมา เช่น Parity Bit และ Stop Bit
  • เฟรมข้อมูลที่มีขนาดไม่เกิน 8 บิต จะถูกเขียนหรืออ่านจะอยู่ในรีจิสเตอร์สำหรับข้อมูล (UDR0)
    • ถ้าเขียนข้อมูล หมายถึง TXB (Transmit Data Buffer Register)
    • ถ้าอ่านข้อมูล หมายถึง RXB (Receive Data Buffer Register)
    • รีจิสเตอร์ TXB และ RXB ใช้แอดเดรสของรีจิสเตอร์ร่วมกัน และเข้าถึงโดยใช้ชื่อ UDR0
  • แต่ถ้ามีจำนวนข้อมูลเท่ากับ 9 บิต จะต้องใช้บิต TXB8 ในรีจิสเตอร์ UCSR0B ร่วมด้วย (สำหรับการส่งข้อมูล) และบิต RXB80 ในรีจิสเตอร์ UCSR0B (สำหรับการรับข้อมูล)
  • ในบางกรณีอาจมีการเปิดใช้งานอินเทอร์รัพท์สำหรับ Tx และ Rx ของวงจร USART0 ด้วย
  • เปิดใช้งานตัวส่งและตัวรับ (Tx และ Rx)
    • Enable Tx: เขียนค่าบิต TXEN0 ให้เป็น 1 ในรีจิสเตอร์ UCSR0B
    • Enable Rx: เขียนค่าบิต RXEN0 ให้เป็น 1 ในรีจิสเตอร์ UCSR0B

รายละเอียดต่าง ๆ เกี่ยวกับรีจิสเตอร์และบิตที่เกี่ยวข้องกับการทำงานของ USART0 แนะนำให้ศึกษาจากไฟล์เอกสารของผู้ผลิต

 


โค้ดตัวอย่างที่ 1: ส่งข้อมูล ASCII#

โค้ดตัวอย่างแรกนี้สาธิตการตั้งค่าใช้งาน USART0 เพื่อส่งข้อมูลแบบ Serial ออกทางขา Tx ของชิป ATmega328P และเลือกใช้ค่า Baud เท่ากับ 9600 ข้อมูลไบต์ที่ถูกส่งออกมา คือ 'A' ถึง 'Z' ตามลำดับ แล้ววนซ้ำใหม่

ถ้าใช้บอร์ด Arduino Uno หรือ Nano ขา D1/TX (Out) และ D0/Rx (In) ของชิป ATmega328P ก็ได้เชื่อมต่อกับวงจร USB-to-Serial ไว้แล้ว ดังนั้นจึงสามารถรับและส่งข้อมูลผ่านพอร์ต USB ของคอมพิวเตอร์สำหรับผู้ใช้ได้

ฟังก์ชัน init_uart(...) ทำหน้าที่ตั้งค่าการใช้งาน ให้วงจร USART ทำงานในโหมด Asynchronous มีค่า Baud เท่ากับ 9600 และมีขนาดเฟรมของข้อมูลเท่ากับ 8 บิต มี Stop Bit จำนวน 1 บิต และไม่มี Parity Bit

ฟังก์ชัน uart_write(...) ใช้สำหรับการส่งข้อมูลทีละไบต์ และมีการตรวจสอบก่อนด้วยว่า จะเขียนข้อมูลไบต์ถัดไปลงในรีจิสเตอร์ UDR0 ได้หรือไม่ ถ้ายังไม่ว่าง ก็ให้รอก่อน (ให้วนซ้ำเพื่อตรวจสอบและรอแบบมีระยะเวลาจำกัด หรือ Timeout)

ถ้าเลือกอัตราการรับส่งข้อมูลเท่ากับ 9600 ก็จะคำนวณค่าสำหรับรีจิสเตอร์ UBRR ได้เท่ากับ 103 สำหรับ Normal Speed Mode ดังนี้

แต่ถ้านำตัวเลข 103 (มีการปัดเศษทิ้งไป) ไปคำนวณเพื่อหาค่า Baud จะได้ประมาณ 9615

// Date: 2023-01-18
#include <avr/io.h>         // for AVR macros
#include <util/delay.h>     // for _delay_ms()

// Macros / Constants
#define BAUD (9600)         // UART baud rate
#define CHECK_TIMEOUT (10)  // Timeout value in msec

// Function prototypes
void init_uart(uint32_t);
int uart_write(char);

int main() {
  uint8_t c = 'A';
  // Initialize UART
  init_uart(BAUD);
  // Send 'A' - 'Z' repeatedly
  while (1) {
    uart_write(c);
    if (c == 'Z') {
      c = 'A';
      uart_write('\n'); // Send a newline
      _delay_ms(100);
    } else {
      c = c+1;
    }
    _delay_ms(10);
  }
}

// Initialize UART
void init_uart(uint32_t baud) {
  uint16_t value;
  DDRD &= ~(1 << DD0); // set RXD (PD0) as input
  DDRD |= (1 << DD1);  // set TXD (PD1) as output
  if (baud >= 115200) {
    UCSR0A |= _BV(U2X0);
    value = F_CPU / 8 / baud - 1;
    UBRR0H = (uint8_t)(value >> 8);
    UBRR0L = (uint8_t)(value);
  } else {
    UCSR0A &= ~(_BV(U2X0));
    value = F_CPU / 16 / baud - 1;
    UBRR0H = (uint8_t)(value >> 8);
    UBRR0L = (uint8_t)(value);
  }
  // Enable both Tx and Rx of USART
  UCSR0B = (1 << RXEN0) | (1 << TXEN0);
  // Set frame format: 8 data bits, 1 stop bit
  UCSR0C = (1 << UCSZ01) | (1 << UCSZ00);
}

int uart_write(char c) {
  // Wait for empty Tx buffer
  uint8_t timeout = CHECK_TIMEOUT;
  while (!(UCSR0A & (1 << UDRE0))) {
    if (timeout == 0) {
      return 1;  // timeout occurred
    }
    timeout--;
    _delay_ms(1);
  }
  // Write the data byte into data-buffer register of the UART
  UDR0 = c;
  return 0;
}

รูป: ตัวอย่างผลการจำลองการทำงานของโค้ดตัวอย่างสำหรับบอร์ด Uno ด้วย Wokwi Simulator

รูป: ตัวอย่างการแสดงสัญญาณ RX0 และ TX0 ที่วัดได้โดยใช้ Virtual Logic Analyzer เมื่อจำลองการทำงานด้วย Wokwi Simulator

รูป: ตัวอย่างการขยายดูสัญญาณดิจิทัล และวัดความกว้างหรือระยะเวลาสำหรับข้อมูลหนึ่งบิต ซึ่งจะได้ประมาณ 104 ไมโครวินาที หรือคิดเป็นอัตรา Baud ได้เท่ากับ บิตต่อวินาที

รูป: ตัวอย่างการวัดและวิเคราะห์สัญญาณด้วย USB-based Logic Analyzer และแสดงผลด้วยซอฟต์แวร์ Sigrok-PulseView (เปิดใช้ UART Protocol Decoder เพื่อแปลงสัญญาณให้เป็นข้อมูลตามรหัส ASCII)

รูป: การวัดความกว้างของบิต (หรือ Bit Timing Measurement) เมื่อมีการส่งข้อมูลแบบ Serial/USART และมีการตั้งค่า Baud เท่ากับ 9600 (โดยประมาณ)

 


โค้ดตัวอย่างที่ 2: ส่งรับและส่งข้อมูลในลักษณะ Serial Loopback#

ถ้าต้องการรอรับข้อมูลจาก UART-Rx ครั้งละหนึ่งไบต์ และส่งกลับทาง UART-Tx ก็มีแนวทางดังนี้ โดยแก้ไขโค้ดจากตัวอย่างที่ 1 เฉพาะในส่วนของฟังก์ชัน main()

#include <avr/io.h>      // for AVR macros
#include <util/delay.h>  // for _delay_ms()

// Macros / Constants
#define BAUD (9600)         // UART baud rate
#define CHECK_TIMEOUT (10)  // Timeout value in msec

// Function prototypes
void init_uart(uint32_t);
int uart_write(char);

int main() {
  // Initialize UART
  init_uart(BAUD);
  while (1) {
    // Check if a byte has been received
    if (UCSR0A & (1 << RXC0)) {
      uint8_t c = UDR0;  // Read the received data
      uart_write(c);     // Send the received data back
    }
  }
}

// Initialize UART
void init_uart(uint32_t baud) {
  uint16_t value;
  DDRD &= ~(1 << DD0); // set RXD (PD0) as input
  DDRD |= (1 << DD1);  // set TXD (PD1) as output
  if (baud >= 115200) {
    UCSR0A |= _BV(U2X0);
    value = F_CPU / 8 / baud - 1;
    UBRR0H = (uint8_t)(value >> 8);
    UBRR0L = (uint8_t)(value);
  } else {
    UCSR0A &= ~(_BV(U2X0));
    value = F_CPU / 16 / baud - 1;
    UBRR0H = (uint8_t)(value >> 8);
    UBRR0L = (uint8_t)(value);
  }
  // Enable both Tx and Rx of USART
  UCSR0B = (1 << RXEN0) | (1 << TXEN0);
  // Set frame format: 8 data bits, 1 stop bit
  UCSR0C = (1 << UCSZ01) | (1 << UCSZ00);
}

int uart_write(char c) {
  // Wait for empty Tx buffer
  uint8_t timeout = CHECK_TIMEOUT;
  while (!(UCSR0A & (1 << UDRE0))) {
    if (timeout == 0) {
      return 1;  // timeout occurred
    }
    timeout--;
    _delay_ms(1);
  }
  // Write the data byte into data-buffer register of the UART
  UDR0 = c;
  return 0;
}

รูป: การบันทึกสัญญาณดิจิทัลที่ขา Rx (D0) และ Tx (D1) ของบอร์ด Arduino Nano ด้วยอุปกรณ์ USB Logic Analyzer และแสดงผลด้วยซอฟต์แวร์ PulseView

ถ้าต้องการจะเปลี่ยนมาใช้วิธีเปิดใช้งานอินเทอร์รัพท์ USART_RX_vect เมื่อได้รับข้อมูลไบต์ในแต่ละครั้ง (RX Complete Interrupt) ก็สามารถเขียนโค้ดได้ตามตัวอย่างดังนี้

#include <avr/io.h>      // for AVR macros
#include <util/delay.h>  // for _delay_ms()

// Macros / Constants
#define BAUD (9600)         // UART baud rate
#define CHECK_TIMEOUT (10)  // Timeout value in msec

// Function prototypes
void init_uart(uint32_t);
int uart_write(char);

volatile uint8_t data;
volatile uint8_t data_valid = 0;

ISR(USART_RX_vect) { // ISR for UART-RX 
  data = UDR0;       // Read data from UDR register
  data_valid = 1;    // Set the data-valid flag
}

int main() {
  // Initialize UART
  init_uart(BAUD);
  // Enable global interrupts
  sei();
  while (1) {
    // Check if the next data byte is received 
    if (data_valid) { 
      data_valid = 0;   // Clear flag
      uart_write(data); // Write the received data byte
    }
  }
}

// Initialize UART
void init_uart(uint32_t baud) {
  uint16_t value;
  DDRD &= ~(1 << DD0); // set RXD (PD0) as input
  DDRD |= (1 << DD1);  // set TXD (PD1) as output
  if (baud >= 115200) {
    UCSR0A |= _BV(U2X0);
    value = F_CPU / 8 / baud - 1;
    UBRR0H = (uint8_t)(value >> 8);
    UBRR0L = (uint8_t)(value);
  } else {
    UCSR0A &= ~(_BV(U2X0));
    value = F_CPU / 16 / baud - 1;
    UBRR0H = (uint8_t)(value >> 8);
    UBRR0L = (uint8_t)(value);
  }
  // Enable both Tx and Rx of USART
  UCSR0B = (1 << RXEN0) | (1 << TXEN0);
  // Enable RX interrupt
  UCSR0B |= (1 << RXCIE0);
  // Set frame format: 8 data bits, 1 stop bit
  UCSR0C = (1 << UCSZ01) | (1 << UCSZ00);
}

int uart_write(char c) {
  // Wait for empty Tx buffer
  uint8_t timeout = CHECK_TIMEOUT;
  while (!(UCSR0A & (1 << UDRE0))) {
    if (timeout == 0) {
      return 1;  // timeout occurred
    }
    timeout--;
    _delay_ms(1);
  }
  // Write the data byte into data-buffer register of the UART
  UDR0 = c;
  return 0;
}

ถ้าต้องการจะเก็บบันทึกข้อมูลไบต์ที่ได้รับจาก RX ลงในบัฟเฟอร์ตามรูปแบบของ FIFO เมื่อเกิดอินเทอร์รัพท์ USART_RX_vect และให้อ่านข้อมูลออกจาก FIFO แล้วส่งกลับไปทาง TX ในลักษณะ Serial Loopback ก็มีตัวอย่างดังนี้

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

#define BAUD (115200)
#define UART_BUFSIZE (64)

volatile uint8_t rx_buf[UART_BUFSIZE];
volatile uint8_t rx_buf_tail = 0, rx_buf_head = 0;

void init_uart(unsigned long baud) {
  uint16_t value;
  DDRD &= ~(1 << DD0); // set RXD (PD0) as input
  DDRD |= (1 << DD1);  // set TXD (PD1) as output
  if (baud >= 115200) {
    UCSR0A |= _BV(U2X0);
    value = F_CPU / 8 / baud - 1;
    UBRR0H = (uint8_t)(value >> 8);
    UBRR0L = (uint8_t)(value);
  } else {
    UCSR0A &= ~(_BV(U2X0));
    value = F_CPU / 16 / baud - 1;
    UBRR0H = (uint8_t)(value >> 8);
    UBRR0L = (uint8_t)(value);
  }
  // Enable transmitter, receiver and RX interrupt
  UCSR0B = (1 << TXEN0) | (1 << RXEN0) | (1 << RXCIE0);
  // Set frame format: 8 data bits, 1 stop bit
  UCSR0C = (1 << UCSZ01) | (1 << UCSZ00);
}

ISR(USART_RX_vect) { // ISR for UART-RX
  // Read the received data byte
  uint8_t data = UDR0; 
  // Increment the head pointer
  uint8_t next_head = (rx_buf_head + 1) % UART_BUFSIZE;
  // If the buffer is not full, save the data byte into the buffer.
  if (next_head != rx_buf_tail) {
    rx_buf[rx_buf_head] = data;
    rx_buf_head = next_head;
  }
}

uint8_t is_uart_buffer_empty() {
  return (rx_buf_head == rx_buf_tail);
}

uint8_t uart_read(void) {
  if (is_uart_buffer_empty()) {
    return 0; 
  }
  uint8_t data = rx_buf[rx_buf_tail];
  rx_buf_tail = (rx_buf_tail + 1) % UART_BUFSIZE;
  return data;
}

void uart_write(uint8_t data) {
  while (!(UCSR0A & (1 << UDRE0))) {}
  UDR0 = data;
}

int main(void) {
  init_uart(BAUD); // Initialize USART
  sei();           // Enable global interrupts
  uart_write( '\r' );
  while (1) {
    // Check if the RX buffer is not empty
    if (!is_uart_buffer_empty()) {
      // Read one data byte from buffer and write it to UART
      uart_write( uart_read() );
    }
    _delay_ms(1);
  }
  return 0;
}

 


โค้ดตัวอย่างที่ 3: การใช้คำสั่ง printf()#

ถ้าต้องการใช้ฟังก์ชัน printf() จาก <stdio.h> ซึ่งเป็น Standard C Library ในการเขียนโค้ดเพื่อส่งข้อความออกทาง UART ก็จะต้องสร้างฟังก์ชันตามรูปแบบต่อไปนี้

int uart_putchar(char c, FILE *stream);

เพื่อทำหน้าที่ส่งข้อมูลทีละไบต์ และทำคำสั่งต่อไปนี้ เพื่อเปลี่ยนช่องทางการส่งข้อมูลจาก C Standard Output ไปยัง UART

fdevopen(&uart_putchar, NULL);

ตัวอย่างโค้ดมีดังนี้ เพื่อสาธิตการใช้คำสั่ง printf() เพื่อส่งข้อความออกทาง UART

#include <avr/io.h>         // for AVR macros
#include <util/delay.h>     // for _delay_ms()
#include <stdio.h>          // for printf()

// Macros / Constants
#define BAUD (115200)       // UART baud rate
#define CHECK_TIMEOUT (10)  // Timeout value in msec

int main() {
  uint32_t count = 0;
  // Iniitalize UART
  init_uart(BAUD);
  // Redirect stdout to UART
  fdevopen(&uart_putchar, NULL);
  while (1) {
    // Send a string to UART every 1 second
    printf("Count: %lu\n", ++count);
    _delay_ms(1000);
  }
}

// Initialize UART
void init_uart(uint32_t baud) {
  uint16_t value;
  if (baud >= 115200) {
    UCSR0A |= _BV(U2X0);
    value = F_CPU / 8 / baud - 1;
    UBRR0H = (uint8_t)(value >> 8);
    UBRR0L = (uint8_t)(value);
  } else {
    UCSR0A &= ~(_BV(U2X0));
    value = F_CPU / 16 / baud - 1;
    UBRR0H = (uint8_t)(value >> 8);
    UBRR0L = (uint8_t)(value);
  }
  // Enable transmit and receive
  UCSR0B = (1 << RXEN0) | (1 << TXEN0);
  // Set frame format: 8 data bits, 1 stop bit
  UCSR0C = (1 << UCSZ01) | (1 << UCSZ00);
}

int uart_putchar(char c, FILE *stream) {
  // Wait for empty transmit buffer
  uint8_t timeout = CHECK_TIMEOUT;
  while (!(UCSR0A & (1 << UDRE0))) {
    if (timeout == 0) {
      return 1;  // timeout occurred
    }
    timeout--;
    _delay_ms(1);
  }
  // Write the data byte into data-buffer register of the UART
  UDR0 = c;
  return 0;
}

 


กล่าวสรุป#

บทความนี้นำเสนอการใช้งานวงจร USART เพื่อการสื่อสารข้อมูลบิตอนุกรมแบบอะซิงโครนัส โดยวิธีการจำลองการทำงานด้วย Wokwi Simulator และทดลองกับบอร์ดไมโครคอนโทรลเลอร์ โดยใช้อุปกรณ์จริง เช่น Arduino Nano

 


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

Created: 2023-01-18 | Last Updated: 2023-01-19