Published at

EMBED - สรุปแนวข้อสอบ Embedded Systems (AVR ATmega328P) แบบละเอียดขั้นสุดท้าย

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)

เอกสารสรุปสำหรับการสอบวิชา 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 (ฟันคะแนนเต็ม)

  1. sei(); สำคัญระดับชาติ: ถ้าเขียนโค้ด Interrupt แต่ลืมใส่ sei(); ใน main() โปรแกรมจะไม่ค้าง แต่ฟังก์ชันใน ISR() จะไม่มีวันถูกเรียกใช้งานเลย!
  2. ตัวแปร volatile: ตัวแปรใดที่ถูกดัดแปลงค่าในฟังก์ชัน ISR() และนำมาอ่านค่าใน main() จะต้องมีคำนำหน้าว่า volatile เสมอ (เพื่อกันไม่ให้ Compiler แอบลบมันทิ้งตอน Optimize)
  3. การชนกันของ Timer:
  • Timer0 = PWM (PD6)
  • Timer1 = Ultrasonic, Timer1_1Sec
  • Timer2 = delay_ms
    • Tip: คุณไม่สามารถใช้ Ultrasonic ควบคู่กับ Timer1_1Sec ได้ เพราะมันใช้ Timer1 เหมือนกัน! ต้องเลือกใช้อย่างใดอย่างหนึ่ง
  1. เทคนิคเคาะ Spacebar ล้างจอ: ใช้ sprintf(buf, “Val: %d ”, val); ดีกว่าใช้ sendLCDCommand(0x01) ในลูป เพราะ 0x01 กินเวลา 2ms และทำให้จอกระพริบน่าเกลียด
  2. ถอดสายก่อนอัปโหลด: หากมีปัญหายิงโค้ดไม่เข้าบอร์ด ให้ถอดสายที่ต่อขา PB3, PB4, PB5 ออกก่อน เพราะมันเป็นขาของเครื่องโปรแกรม (ISP)
Sharing is caring!