การเขียนโปรแกรมภาษา C สำหรับ AVR (ATmega328P): ตอนที่ 4#
Keywords: Atmel AVR MCU, ATmega328P, Bare-metal C Programming, AVR-GCC, avr-libc
- การเขียนโปรแกรมภาษา C แบบ Bare-Metal และการใช้ไลบรารี avr-libc
- วงจรสื่อสารข้อมูลแบบบิตอนุกรม: USART
- โค้ดตัวอย่างที่ 1: ส่งข้อมูล ASCII
- โค้ดตัวอย่างที่ 2: ส่งรับและส่งข้อมูลในลักษณะ Serial Loopback
- โค้ดตัวอย่างที่ 3: การใช้คำสั่ง printf()
▷ การเขียนโปรแกรมภาษา 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 และ
- Normal Speed: บิต
- เขียนค่าลงในรีจิสเตอร์
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
- Enable Tx: เขียนค่าบิต
รายละเอียดต่าง ๆ เกี่ยวกับรีจิสเตอร์และบิตที่เกี่ยวข้องกับการทำงานของ 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