- Published at
EMBED - สรุปแนวข้อสอบ Embedded Systems (AVR ATmega328P) แบบละเอียดขั้นสุดท้าย
เอกสารสรุปสำหรับการสอบวิชา Embedded Systems ครอบคลุมการเขียนโค้ดแบบดั้งเดิม (Polling/Blocking) และแบบขั้นสูง (Interrupt/Non-blocking) พร้อมระบบ SOT และ Custom Delay
Table of Contents
- 📘 Master Cheat Sheet: AVR ATmega328P (The Ultimate Final Edition)
- 📋 เช็คลิสต์ก่อนสอบ (Hardware Pin Map & Rules)
- 🏗️ ส่วนที่ 1: SOT (Single Source of Truth)
- 🛠️ ส่วนที่ 3: ไดรเวอร์แบบดั้งเดิม (Polling / Standard Modules)
- 3.1 LCD & GPIO (พื้นฐาน)
- 3.2 I2C (RTC) & UART (Serial) & SPI
- 3.3 Matrix Keypad & PWM
- ⚡ ส่วนที่ 4: ไดรเวอร์แบบ Interrupt (Non-blocking Handlers)
- 4.1 Button Handle (External Interrupt - ทันทีทันใด)
- 4.2 Ultrasonic Handle (Input Capture - แม่นยำสูงสุด)
- 4.3 1-Second Tick Handle (ทำงานทุก 1 วินาทีเป๊ะๆ)
- 4.4 ADC Auto Handle (อ่านเซนเซอร์โดยไม่เสียเวลา CPU)
- 🧩 ส่วนที่ 5: การประกอบร่างระบบ (Integration & Flags)
- 🚨 ส่วนที่ 6: คำเตือน & Tips (ฟันคะแนนเต็ม)
📘 Master Cheat Sheet: AVR ATmega328P (The Ultimate Final Edition)
เอกสารสรุปสำหรับการสอบวิชา Embedded Systems ครอบคลุมการเขียนโค้ดแบบดั้งเดิม (Polling/Blocking) และแบบขั้นสูง (Interrupt/Non-blocking) พร้อมระบบ SOT และ Custom Delay
📋 เช็คลิสต์ก่อนสอบ (Hardware Pin Map & Rules)
ชิปมีขาน้อย หากโจทย์ให้อุปกรณ์มาเยอะ “ห้ามต่อขาชนกันเด็ดขาด”
- UART (Serial): บังคับ
PD0 (RX),PD1 (TX) - I2C (RTC / จอ): บังคับ
PC4 (SDA),PC5 (SCL) - SPI (External ADC): บังคับ
PB3 (MOSI),PB4 (MISO),PB5 (SCK) - PWM (คุมมอเตอร์): บังคับ
PD6 (OC0A)(Timer0) - Ultrasonic (Echo): บังคับ
PB0 (ICP1)(Timer1 Input Capture)
🏗️ ส่วนที่ 1: SOT (Single Source of Truth)
วางไว้บนสุดของไฟล์เสมอ โค้ดจะปรับความเร็วและคำนวณค่าต่างๆ ให้อัตโนมัติ
/* ========================================================
* ⏱️ CLOCK SPEED SELECTION (เลือกความเร็วของชิป)
* ======================================================== */
#define F_CPU 16000000UL // สำหรับ 16MHz (Arduino)
// #define F_CPU 8000000UL // สำหรับ 8MHz
#include <avr/io.h>
#include <avr/interrupt.h>
#include <stdio.h>
/* ========================================================
* 🌟 SOT: CONFIGURATION (ตั้งค่าขาพอร์ต)
* ======================================================== */
// --- 1. GPIO (LED, Relay, ปุ่มกด) ---
#define OUT_PORT PORTD
#define OUT_DDR DDRD
#define OUT_PIN PD2
#define BTN_PORT PORTD
#define BTN_IN PIND
#define BTN_DDR DDRD
#define BTN_PIN PD3
// --- 2. จอ LCD 16x2 (โหมด 4-bit) ---
#define LCD_DATA_PORT PORTC // ขา D4-D7 ต่อ PC0-PC3
#define LCD_DATA_DDR DDRC
#define LCD_CTRL_PORT PORTD
#define LCD_CTRL_DDR DDRD
#define LCD_RS PD4
#define LCD_EN PD7
// --- 3. Matrix Keypad 4x4 (Split-Port) ---
#define KP_COL_PORT PORTC // คอลัมน์ PC0-PC3 (Output)
#define KP_COL_DDR DDRC
#define KP_ROW_PORT PORTB // แถว PB0-PB3 (Input)
#define KP_ROW_PIN PINB
#define KP_ROW_DDR DDRB
// --- 4. ADC (Analog) ---
#define ADC_CH 4 // PC4
// --- 5. I2C (RTC DS1307) ---
#define RTC_ADDR_W 0xD0
#define RTC_ADDR_R 0xD1
// --- 6. SPI (MCP3201) ---
#define SPI_CS_PORT PORTB
#define SPI_CS_DDR DDRB
#define SPI_CS_PIN PB2
// --- 7. Ultrasonic ---
#define US_TRIG_PORT PORTD
#define US_TRIG_DDR DDRD
#define US_TRIG_PIN PD5
#define US_ECHO_PIN PB0 // บังคับ PB0
## ⏱️ ส่วนที่ 2: ระบบหน่วงเวลา (Custom Delay Engine)
ห้ามใช้ <util/delay.h> ให้ใช้ Timer2 สร้างการหน่วงเวลาแบบบล็อคโค้ด (Blocking) แทน
```c
void setup_Timer2_Delay() {
TCCR2A = (1 << WGM21); // CTC Mode
TCCR2B = (1 << CS22); // Prescaler 64
OCR2A = (F_CPU / 64 / 1000) - 1; // ออโต้: 16M=249, 8M=124
}
void delay_ms(uint16_t ms) {
for (uint16_t i = 0; i < ms; i++) {
TCNT2 = 0; TIFR2 |= (1 << OCF2A);
while (!(TIFR2 & (1 << OCF2A)));
}
}
void delay_us(uint16_t us) {
for (volatile uint16_t i = 0; i < (us * (F_CPU / 4000000UL)); i++);
}
🛠️ ส่วนที่ 3: ไดรเวอร์แบบดั้งเดิม (Polling / Standard Modules)
เลือกก๊อปปี้เฉพาะอุปกรณ์ที่โจทย์ระบุ
3.1 LCD & GPIO (พื้นฐาน)
void setup_GPIO() {
OUT_DDR |= (1 << OUT_PIN);
BTN_DDR &= ~(1 << BTN_PIN); BTN_PORT |= (1 << BTN_PIN);
}
void lcd_pulse() { LCD_CTRL_PORT |= (1<<LCD_EN); delay_us(10); LCD_CTRL_PORT &= ~(1<<LCD_EN); delay_us(10); }
void lcd_send(uint8_t val, uint8_t is_data) {
if (is_data) LCD_CTRL_PORT |= (1<<LCD_RS); else LCD_CTRL_PORT &= ~(1<<LCD_RS);
LCD_DATA_PORT = (LCD_DATA_PORT & 0xF0) | (val >> 4); lcd_pulse();
LCD_DATA_PORT = (LCD_DATA_PORT & 0xF0) | (val & 0x0F); lcd_pulse(); delay_ms(2);
}
void sendLCDCommand(uint8_t cmd) { lcd_send(cmd, 0); }
void sendLCDMessage(char *s) { while(*s) lcd_send(*s++, 1); }
void setup_LCD() {
LCD_DATA_DDR |= 0x0F; LCD_CTRL_DDR |= (1<<LCD_RS)|(1<<LCD_EN); delay_ms(50);
sendLCDCommand(0x33); sendLCDCommand(0x32); sendLCDCommand(0x28);
sendLCDCommand(0x0C); sendLCDCommand(0x01);
}
3.2 I2C (RTC) & UART (Serial) & SPI
// --- I2C ---
void setup_I2C() { TWSR = 0; TWBR = ((F_CPU / 100000UL) - 16) / 2; TWCR = (1 << TWEN); }
void i2c_start() { TWCR = (1<<TWINT)|(1<<TWSTA)|(1<<TWEN); while(!(TWCR & (1<<TWINT))); }
void i2c_write(uint8_t d) { TWDR = d; TWCR = (1<<TWINT)|(1<<TWEN); while(!(TWCR & (1<<TWINT))); }
uint8_t i2c_read(uint8_t ack) { TWCR = (1<<TWINT)|(1<<TWEN)|(ack?(1<<TWEA):0); while(!(TWCR & (1<<TWINT))); return TWDR; }
void i2c_stop() { TWCR = (1<<TWINT)|(1<<TWEN)|(1<<TWSTO); }
uint8_t bcd2dec(uint8_t val) { return ((val >> 4) * 10) + (val & 0x0F); }
// --- UART ---
#define BAUD 9600
#define MYUBRR (F_CPU/16/BAUD-1)
void setup_UART() {
UBRR0H = (unsigned char)(MYUBRR >> 8); UBRR0L = (unsigned char)(MYUBRR);
UCSR0B = (1 << RXEN0) | (1 << TXEN0); UCSR0C = (3 << UCSZ00);
}
void uart_print(char *s) { while (*s) { while (!(UCSR0A & (1 << UDRE0))); UDR0 = *s++; } }
// --- SPI ---
void setup_SPI() {
SPI_CS_DDR |= (1 << SPI_CS_PIN); SPI_CS_PORT |= (1 << SPI_CS_PIN);
DDRB |= (1 << PB3) | (1 << PB5); DDRB &= ~(1 << PB4);
SPCR = (1 << SPE) | (1 << MSTR) | (1 << SPR0);
}
3.3 Matrix Keypad & PWM
char scan_Keypad() {
KP_COL_DDR |= 0x0F; KP_ROW_DDR &= ~0x0F; KP_ROW_PORT |= 0x0F; // Col=Out, Row=In(Pull-up)
char keys[4][4] = {
{'1','2','3','A'},
{'4','5','6','B'},
{'7','8','9','C'},
{'*','0','#','D'}
};
for (int col = 0; col < 4; col++) {
KP_COL_PORT = (KP_COL_PORT & 0xF0) | (~(1 << col) & 0x0F); delay_us(10);
for (int row = 0; row < 4; row++) {
if (!(KP_ROW_PIN & (1 << row))) {
while (!(KP_ROW_PIN & (1 << row))); delay_ms(20); return keys[row][col];
}
}
} return '\0';
}
void setup_PWM() {
DDRD |= (1 << PD6);
TCCR0A = (1 << WGM01) | (1 << WGM00) | (1 << COM0A1); TCCR0B = (1 << CS01) | (1 << CS00);
}
void set_PWM(uint8_t val) { OCR0A = val; }
⚡ ส่วนที่ 4: ไดรเวอร์แบบ Interrupt (Non-blocking Handlers)
ใช้ท่าเหล่านี้เพื่อไม่ให้โปรแกรมค้าง และทำงานพร้อมกันได้หลายอย่าง (Multi-tasking)
4.1 Button Handle (External Interrupt - ทันทีทันใด)
volatile uint8_t btn_flag = 0;
void setup_INT_Button() {
BTN_DDR &= ~(1 << BTN_PIN); BTN_PORT |= (1 << BTN_PIN);
PCICR |= (1 << PCIE2); // เปิดพอร์ต D (PCINT16-23)
PCMSK2 |= (1 << PCINT19); // ดักจับที่ PD3 (PCINT19)
}
ISR(PCINT2_vect) {
for(volatile int i=0; i<3000; i++); // Debounce ในฮาร์ดแวร์
if (!(BTN_IN & (1 << BTN_PIN))) { btn_flag = 1; }
}
4.2 Ultrasonic Handle (Input Capture - แม่นยำสูงสุด)
volatile uint16_t us_start, us_end; volatile uint8_t us_ready = 0;
void setup_INT_Ultrasonic() {
US_TRIG_DDR |= (1 << US_TRIG_PIN);
DDRB &= ~(1 << US_ECHO_PIN); PORTB |= (1 << US_ECHO_PIN);
TCCR1B = (1 << CS11) | (1 << ICES1); // Prescaler 8, ขาขึ้น
TIMSK1 |= (1 << ICIE1);
}
void trigger_US() { us_ready = 0; US_TRIG_PORT |= (1 << US_TRIG_PIN); delay_us(15); US_TRIG_PORT &= ~(1 << US_TRIG_PIN); }
ISR(TIMER1_CAPT_vect) {
if (TCCR1B & (1 << ICES1)) { us_start = ICR1; TCCR1B &= ~(1 << ICES1); }
else { us_end = ICR1; TCCR1B |= (1 << ICES1); us_ready = 1; }
}
4.3 1-Second Tick Handle (ทำงานทุก 1 วินาทีเป๊ะๆ)
volatile uint8_t tick_1s_flag = 0;
void setup_Timer1_1Sec() {
TCCR1A = 0; TCCR1B = (1 << WGM12) | (1 << CS12) | (1 << CS10); // CTC, Prescaler 1024
OCR1A = ((F_CPU / 1024) * 1) - 1; // คำนวณออโต้สำหรับ 1 วินาที
TIMSK1 |= (1 << OCIE1A);
}
ISR(TIMER1_COMPA_vect) { tick_1s_flag = 1; }
4.4 ADC Auto Handle (อ่านเซนเซอร์โดยไม่เสียเวลา CPU)
volatile uint16_t adc_val = 0; volatile uint8_t adc_ready = 0;
void setup_INT_ADC() {
ADMUX = (1 << REFS0) | (ADC_CH & 0x0F);
ADCSRA = (1 << ADEN) | (1 << ADIE) | (7 << ADPS0); // เปิด ADIE
}
void trigger_ADC() { adc_ready = 0; ADCSRA |= (1 << ADSC); }
ISR(ADC_vect) { adc_val = ADC; adc_ready = 1; }
🧩 ส่วนที่ 5: การประกอบร่างระบบ (Integration & Flags)
ตัวอย่างโครงสร้าง main() แบบผสมผสาน (Hybrid) ที่ดีที่สุด
int main(void) {
// 1. SETUP
setup_Timer2_Delay(); // บังคับอันแรก
setup_GPIO();
setup_LCD();
setup_PWM();
// Setup พวก Interrupt
setup_INT_Button();
setup_INT_Ultrasonic();
setup_Timer1_1Sec();
sei(); // 🚨 เปิดสวิตช์ Interrupt (ห้ามลืมเด็ดขาด)
char buf[16];
int state = 0;
// เริ่มอ่านรอบแรก
trigger_US();
while(1) {
// --- 1. จัดการปุ่มกด (Interrupt Flag) ---
if (btn_flag) {
btn_flag = 0; // เคลียร์ธง
state = !state;
if(state) OUT_PORT |= (1 << OUT_PIN); else OUT_PORT &= ~(1 << OUT_PIN);
}
// --- 2. จัดการ Ultrasonic (Interrupt Flag) ---
uint16_t dist = 0;
if (us_ready) {
us_ready = 0; // เคลียร์ธง
dist = (us_end - us_start) / (F_CPU == 16000000UL ? 116 : 58);
// สั่งยิงคลื่นรอบต่อไป
trigger_US();
}
// --- 3. จัดการเวลา 1 วินาที (อัปเดตจอทุก 1 วิ ไม่กระพริบ) ---
if (tick_1s_flag) {
tick_1s_flag = 0; // เคลียร์ธง
// ปรับ PWM ตามระยะทาง
if(dist < 20) set_PWM(255); else set_PWM(0);
sendLCDCommand(0x80);
sprintf(buf, "Dist: %03d cm ", dist);
sendLCDMessage(buf);
sendLCDCommand(0xC0);
sprintf(buf, "State: %d ", state);
sendLCDMessage(buf);
}
// หมายเหตุ: โค้ดใน Loop วิ่งเร็วมาก ไม่โดน delay_ms บล็อคเลย!
}
}
🚨 ส่วนที่ 6: คำเตือน & Tips (ฟันคะแนนเต็ม)
sei();สำคัญระดับชาติ: ถ้าเขียนโค้ด Interrupt แต่ลืมใส่sei();ในmain()โปรแกรมจะไม่ค้าง แต่ฟังก์ชันในISR()จะไม่มีวันถูกเรียกใช้งานเลย!- ตัวแปร
volatile: ตัวแปรใดที่ถูกดัดแปลงค่าในฟังก์ชันISR()และนำมาอ่านค่าในmain()จะต้องมีคำนำหน้าว่าvolatileเสมอ (เพื่อกันไม่ให้ Compiler แอบลบมันทิ้งตอน Optimize) - การชนกันของ Timer:
- Timer0 = PWM (PD6)
- Timer1 = Ultrasonic, Timer1_1Sec
- Timer2 = delay_ms
- Tip: คุณไม่สามารถใช้ Ultrasonic ควบคู่กับ Timer1_1Sec ได้ เพราะมันใช้ Timer1 เหมือนกัน! ต้องเลือกใช้อย่างใดอย่างหนึ่ง
- เทคนิคเคาะ Spacebar ล้างจอ: ใช้ sprintf(buf, “Val: %d ”, val); ดีกว่าใช้ sendLCDCommand(0x01) ในลูป เพราะ 0x01 กินเวลา 2ms และทำให้จอกระพริบน่าเกลียด
- ถอดสายก่อนอัปโหลด: หากมีปัญหายิงโค้ดไม่เข้าบอร์ด ให้ถอดสายที่ต่อขา PB3, PB4, PB5 ออกก่อน เพราะมันเป็นขาของเครื่องโปรแกรม (ISP)