โมดูล 2/5 · สัปดาห์ 4–6 · 27 ชม.

C/C++ บนระบบฝังตัว

UAT 204 ไมโครคอนโทรลเลอร์และระบบสมองกลฝังตัว

เวลาเรียนประมาณ 80 นาทีร่าง รอตรวจปรับปรุงล่าสุด 27 กันยายน 2569

บทเรียน

เมื่อเรียนจบโมดูลนี้ ผู้เรียนจะสามารถ

  1. ใช้ชนิดข้อมูลขนาดตายตัวและอธิบายผลของการล้นค่า (overflow)
  2. ตั้ง ล้าง สลับ และอ่านบิตของรีจิสเตอร์ด้วยตัวดำเนินการระดับบิต
  3. อธิบายการใช้ volatile กับรีจิสเตอร์และตัวแปรที่ใช้ร่วมกับ interrupt
  4. อธิบายแผนที่หน่วยความจำ stack และ heap และมาตรฐานการเขียนโค้ด MISRA C และ BARR-C

ความรู้พื้นฐานที่ควรมี: UAT 204 โมดูล 1 · UAT 104 (การเขียนโปรแกรม)

ทำไมต้องรู้

ภาษา C และ C++ ยังเป็นภาษาหลักของระบบฝังตัว เพราะควบคุมหน่วยความจำและฮาร์ดแวร์ได้โดยตรงและทำงานเร็ว ทั้ง ArduPilot และ PX4 เขียนด้วย C++ แต่ความสามารถนี้มาพร้อมความเสี่ยง ตัวแปรที่ล้นค่าเงียบ ๆ หรือตัวแปรที่คอมไพเลอร์ตัดทิ้งเพราะไม่รู้ว่าฮาร์ดแวร์เปลี่ยนค่าได้ ทำให้เกิดบั๊กที่หายากที่สุด บทนี้ใช้ Python จำลองพฤติกรรมเหล่านั้นให้เห็นก่อนเขียนบนบอร์ดจริง

ชนิดข้อมูลขนาดตายตัว

ในระบบฝังตัวควรใช้ชนิดข้อมูลที่รู้ขนาดแน่นอนจาก <stdint.h> เช่น uint8_t (0 ถึง 255) int16_t และ uint32_t แทน int ที่ขนาดขึ้นกับชิป เมื่อค่าเกินช่วง จำนวนเต็มไม่มีเครื่องหมายจะ วนกลับ (wrap around) โดยไม่เตือน

ตัวอย่างที่ 1 ตัวนับ 8 บิตล้นค่า

จำลองตัวนับ uint8_t ที่นับจำนวนพัลส์จากเซนเซอร์รอบหมุน

def as_uint8(x):
    return x & 0xFF                       # เก็บเฉพาะ 8 บิตล่าง เหมือน uint8_t


count = 250
for pulse in range(8):
    count = as_uint8(count + 1)
    print(count, end=" ")
print()
print("elapsed pulses between readings 250 and 2:", as_uint8(2 - 250))
251 252 253 254 255 0 1 2
elapsed pulses between readings 250 and 2: 8

ตัวนับกระโดดจาก 255 กลับเป็น 0 ถ้าคำนวณผลต่างด้วยชนิดเดียวกัน (uint8_t) ผลต่างยังถูกต้องแม้ตัวนับวนกลับ เทคนิคนี้ใช้กับตัวนับเวลาของ MCU ที่วนกลับเป็นระยะ

จัดการบิตของรีจิสเตอร์

อุปกรณ์รอบข้างควบคุมผ่าน รีจิสเตอร์ ซึ่งแต่ละบิตมีความหมาย เช่น บิตหนึ่งเปิด LED อีกบิตเลือกโหมด การเปลี่ยนบิตเดียวต้องไม่กระทบบิตอื่น

รีจิสเตอร์ 8 บิตสี่แถว แถวแรกค่าเดิม 00101001 แถวที่สองตั้งบิต 4 ด้วย or กับ 1 เลื่อน 4 ได้ 00111001 แถวที่สามล้างบิต 0 ด้วย and กับนิเสธของ 1 ได้ 00111000 แถวที่สี่สลับบิต 7 ด้วย xor ได้ 10111000 บิตที่เปลี่ยนแสดงสีเหลือง
ภาพที่ 1 ตั้ง ล้าง และสลับบิตของรีจิสเตอร์ 8 บิต

ตัวอย่างที่ 2 ตั้ง ล้าง สลับ และอ่านบิต

reg = 0b00101001
print(f"start        {reg:08b}")
reg |= 1 << 4            # ตั้งบิต 4
print(f"set bit 4    {reg:08b}")
reg &= ~(1 << 0) & 0xFF  # ล้างบิต 0
print(f"clear bit 0  {reg:08b}")
reg ^= 1 << 7            # สลับบิต 7
print(f"toggle bit 7 {reg:08b}")
print("bit 3 is", (reg >> 3) & 1, "| bits 5..4 as a field:", (reg >> 4) & 0b11)
start        00101001
set bit 4    00111001
clear bit 0  00111000
toggle bit 7 10111000
bit 3 is 1 | bits 5..4 as a field: 3

ในภาษา C เขียนแบบเดียวกัน เช่น GPIO_OUT |= (1u << 4); ตัว u ทำให้เป็นจำนวนไม่มีเครื่องหมาย ป้องกันผลแปลก ๆ เมื่อเลื่อนบิตไปถึงบิตบนสุด

volatile

คอมไพเลอร์ปรับโค้ดให้เร็วขึ้นโดยสมมติว่าตัวแปรเปลี่ยนค่าเฉพาะเมื่อโปรแกรมเขียนเท่านั้น แต่รีจิสเตอร์ของฮาร์ดแวร์และตัวแปรที่ interrupt แก้ไขเปลี่ยนค่าได้เองโดยโปรแกรมหลักไม่รู้ คำสั่ง volatile บอกให้คอมไพเลอร์อ่านค่าจากหน่วยความจำทุกครั้ง ตามคู่มือของ Arm ใช้กับรีจิสเตอร์ของอุปกรณ์ ตัวแปรที่ใช้ร่วมกับ interrupt และตัวแปรที่ใช้ร่วมกันระหว่าง thread

volatile uint8_t data_ready = 0;      /* ISR sets it, main loop reads it */

void sensor_isr(void) { data_ready = 1; }

int main(void) {
    for (;;) {
        if (data_ready) {             /* without volatile the compiler may read this only once */
            data_ready = 0;
            read_sensor();
        }
    }
}

หน่วยความจำและมาตรฐานการเขียนโค้ด

แผนที่หน่วยความจำจากที่อยู่สูงลงต่ำ อุปกรณ์รอบข้างและรีจิสเตอร์ด้านบน ถัดมาเป็น SRAM ส่วน stack โตลง ส่วนว่าง heap โตขึ้น และ data bss ด้านล่างสุดคือ Flash เก็บโค้ดและค่าคงที่ กล่องด้านขวาเตือนว่า stack กับ heap ชนกันเป็นบั๊กที่หายาก
ภาพที่ 2 แผนที่หน่วยความจำอย่างง่ายของไมโครคอนโทรลเลอร์

SRAM ของ MCU มีน้อย stack เก็บตัวแปรในฟังก์ชันและโตลงเมื่อเรียกฟังก์ชันซ้อน heap เก็บหน่วยความจำที่ขอระหว่างทำงาน ถ้าทั้งสองโตจนชนกัน โปรแกรมจะพังแบบคาดเดาไม่ได้ ระบบที่ต้องปลอดภัยจึงมักห้ามขอหน่วยความจำแบบไดนามิกหลังเริ่มทำงาน

มาตรฐานการเขียนโค้ดช่วยป้องกันบั๊กเหล่านี้ MISRA C (ฉบับปัจจุบัน MISRA C:2025) ใช้ในงานที่ความปลอดภัยสำคัญ เช่น ยานยนต์และอากาศยาน และ BARR-C:2018 ของ Barr Group เป็นมาตรฐานที่ดาวน์โหลดได้ฟรี เหมาะเป็นจุดเริ่มต้น

ปฏิบัติการประจำโมดูล

ปฏิบัติการ: บิต ตัวนับ และ volatile บนบอร์ด

  1. รันโค้ดตัวอย่างที่ 1 และ 2 เปลี่ยนค่าเริ่มต้นแล้วทำนายผลก่อนรัน
  2. บน Pico 2 เขียนโปรแกรม C ที่ตั้งและล้างบิตควบคุมขา GPIO ด้วยฟังก์ชันของ SDK แล้วเทียบกับการคำนวณบิตด้วยมือ
  3. เขียนโปรแกรมที่ใช้ตัวนับ uint8_t แล้วแสดงการวนกลับทาง serial
  4. ทดลองโปรแกรมที่ใช้ flag จาก interrupt ทั้งแบบมีและไม่มี volatile คอมไพล์แบบเปิด optimization แล้วบันทึกพฤติกรรม
  5. อ่านกฎ BARR-C สามข้อที่เกี่ยวกับชนิดข้อมูลและตัวแปร แล้วตรวจโค้ดของกลุ่มตามกฎนั้น

ข้อผิดพลาดที่พบบ่อย

ระวัง

  • ใช้ int โดยไม่รู้ขนาดบนชิปนั้น
  • ลืมว่าจำนวนเต็มไม่มีเครื่องหมายวนกลับโดยไม่เตือน
  • เขียนทับทั้งรีจิสเตอร์ แทนการเปลี่ยนเฉพาะบิต
  • ลืม volatile กับตัวแปรที่ใช้ร่วมกับ interrupt
  • ขอหน่วยความจำแบบไดนามิกซ้ำ ๆ ใน MCU ที่ SRAM น้อย

สรุป

  • ใช้ชนิดข้อมูลขนาดตายตัว และระวังการวนกลับของจำนวนเต็มไม่มีเครื่องหมาย
  • ตั้งบิตด้วย OR ล้างด้วย AND กับนิเสธ สลับด้วย XOR
  • volatile บังคับให้อ่านค่าจริงทุกครั้ง ใช้กับรีจิสเตอร์และตัวแปรที่ใช้ร่วมกับ interrupt
  • stack และ heap ใช้ SRAM ร่วมกัน และมาตรฐาน MISRA C กับ BARR-C ช่วยลดบั๊ก

แบบฝึกตรวจความเข้าใจ

  1. uint16_t เก็บค่าได้สูงสุดเท่าใด
  2. uint8_t ค่า 200 บวก 100 ได้เท่าใด
  3. รีจิสเตอร์ 0b00001111 ล้างบิต 2 ได้ค่าใด
  4. ทำไมตัวแปรที่ ISR แก้ไขต้องประกาศเป็น volatile
  5. มาตรฐานการเขียนโค้ดภาษา C ใดดาวน์โหลดได้ฟรี
เฉลย
  1. 0b00001011
  2. เพื่อให้คอมไพเลอร์อ่านค่าจากหน่วยความจำทุกครั้ง ไม่ใช้ค่าที่จำไว้
  3. BARR-C:2018

สรุปสูตรสำคัญ

ช่วงของจำนวนเต็มไม่มีเครื่องหมาย N บิต
ตั้ง ล้าง สลับบิต k

แหล่งอ้างอิงหลัก

  1. White, E. (2024). Making embedded systems: Design patterns for great software (2nd ed.). O'Reilly Media.
  2. Arm. Effect of the volatile keyword on compiler optimization. Arm Compiler software development guide. link
  3. MISRA. (2025). MISRA C:2025 — Guidelines for the use of the C language in critical systems. link
  4. Barr Group. (2018). Embedded C coding standard (BARR-C:2018). link

อ่านเพิ่มเติม

ศึกษาหน่วยความรู้ที่กำหนดล่วงหน้า ดูสื่อประกอบ และทำ quiz ประจำโมดูล

ในชั้นเรียน / ภาคสนาม

ปฏิบัติการในห้องแล็บหรือภาคสนามตามใบงาน พร้อม checklist ความปลอดภัย

หลักฐานการเรียนรู้: ใบงานที่ผ่านการตรวจและผล quiz

แบบทดสอบประจำโมดูล

แบบทดสอบนี้ใช้ตรวจความเข้าใจ (formative) ไม่ใช่การสอบเก็บคะแนน

โดเมนความรู้: การเขียนโปรแกรมและเทคโนโลยีดิจิทัล · เซนเซอร์และระบบสมองกลฝังตัว