C/C++ บนระบบฝังตัว
UAT 204 ไมโครคอนโทรลเลอร์และระบบสมองกลฝังตัว
บทเรียน
เมื่อเรียนจบโมดูลนี้ ผู้เรียนจะสามารถ
- ใช้ชนิดข้อมูลขนาดตายตัวและอธิบายผลของการล้นค่า (overflow)
- ตั้ง ล้าง สลับ และอ่านบิตของรีจิสเตอร์ด้วยตัวดำเนินการระดับบิต
- อธิบายการใช้ volatile กับรีจิสเตอร์และตัวแปรที่ใช้ร่วมกับ interrupt
- อธิบายแผนที่หน่วยความจำ stack และ heap และมาตรฐานการเขียนโค้ด MISRA C และ BARR-C
ทำไมต้องรู้
ภาษา 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 อีกบิตเลือกโหมด การเปลี่ยนบิตเดียวต้องไม่กระทบบิตอื่น
ตัวอย่างที่ 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 ของ MCU มีน้อย stack เก็บตัวแปรในฟังก์ชันและโตลงเมื่อเรียกฟังก์ชันซ้อน heap เก็บหน่วยความจำที่ขอระหว่างทำงาน ถ้าทั้งสองโตจนชนกัน โปรแกรมจะพังแบบคาดเดาไม่ได้ ระบบที่ต้องปลอดภัยจึงมักห้ามขอหน่วยความจำแบบไดนามิกหลังเริ่มทำงาน
มาตรฐานการเขียนโค้ดช่วยป้องกันบั๊กเหล่านี้ MISRA C (ฉบับปัจจุบัน MISRA C:2025) ใช้ในงานที่ความปลอดภัยสำคัญ เช่น ยานยนต์และอากาศยาน และ BARR-C:2018 ของ Barr Group เป็นมาตรฐานที่ดาวน์โหลดได้ฟรี เหมาะเป็นจุดเริ่มต้น
ปฏิบัติการประจำโมดูล
ปฏิบัติการ: บิต ตัวนับ และ volatile บนบอร์ด
- รันโค้ดตัวอย่างที่ 1 และ 2 เปลี่ยนค่าเริ่มต้นแล้วทำนายผลก่อนรัน
- บน Pico 2 เขียนโปรแกรม C ที่ตั้งและล้างบิตควบคุมขา GPIO ด้วยฟังก์ชันของ SDK แล้วเทียบกับการคำนวณบิตด้วยมือ
- เขียนโปรแกรมที่ใช้ตัวนับ
uint8_tแล้วแสดงการวนกลับทาง serial - ทดลองโปรแกรมที่ใช้ flag จาก interrupt ทั้งแบบมีและไม่มี
volatileคอมไพล์แบบเปิด optimization แล้วบันทึกพฤติกรรม - อ่านกฎ BARR-C สามข้อที่เกี่ยวกับชนิดข้อมูลและตัวแปร แล้วตรวจโค้ดของกลุ่มตามกฎนั้น
ข้อผิดพลาดที่พบบ่อย
ระวัง
- ใช้
intโดยไม่รู้ขนาดบนชิปนั้น - ลืมว่าจำนวนเต็มไม่มีเครื่องหมายวนกลับโดยไม่เตือน
- เขียนทับทั้งรีจิสเตอร์ แทนการเปลี่ยนเฉพาะบิต
- ลืม
volatileกับตัวแปรที่ใช้ร่วมกับ interrupt - ขอหน่วยความจำแบบไดนามิกซ้ำ ๆ ใน MCU ที่ SRAM น้อย
สรุป
- ใช้ชนิดข้อมูลขนาดตายตัว และระวังการวนกลับของจำนวนเต็มไม่มีเครื่องหมาย
- ตั้งบิตด้วย OR ล้างด้วย AND กับนิเสธ สลับด้วย XOR
volatileบังคับให้อ่านค่าจริงทุกครั้ง ใช้กับรีจิสเตอร์และตัวแปรที่ใช้ร่วมกับ interrupt- stack และ heap ใช้ SRAM ร่วมกัน และมาตรฐาน MISRA C กับ BARR-C ช่วยลดบั๊ก
แบบฝึกตรวจความเข้าใจ
uint16_tเก็บค่าได้สูงสุดเท่าใดuint8_tค่า 200 บวก 100 ได้เท่าใด- รีจิสเตอร์ 0b00001111 ล้างบิต 2 ได้ค่าใด
- ทำไมตัวแปรที่ ISR แก้ไขต้องประกาศเป็น
volatile - มาตรฐานการเขียนโค้ดภาษา C ใดดาวน์โหลดได้ฟรี
เฉลย
- 0b00001011
- เพื่อให้คอมไพเลอร์อ่านค่าจากหน่วยความจำทุกครั้ง ไม่ใช้ค่าที่จำไว้
- BARR-C:2018
สรุปสูตรสำคัญ
| ช่วงของจำนวนเต็มไม่มีเครื่องหมาย N บิต | |
| ตั้ง ล้าง สลับบิต k |
แหล่งอ้างอิงหลัก
- White, E. (2024). Making embedded systems: Design patterns for great software (2nd ed.). O'Reilly Media.
- Arm. Effect of the volatile keyword on compiler optimization. Arm Compiler software development guide. link
- MISRA. (2025). MISRA C:2025 — Guidelines for the use of the C language in critical systems. link
- Barr Group. (2018). Embedded C coding standard (BARR-C:2018). link
อ่านเพิ่มเติม
ศึกษาหน่วยความรู้ที่กำหนดล่วงหน้า ดูสื่อประกอบ และทำ quiz ประจำโมดูล
ในชั้นเรียน / ภาคสนาม
ปฏิบัติการในห้องแล็บหรือภาคสนามตามใบงาน พร้อม checklist ความปลอดภัย
หลักฐานการเรียนรู้: ใบงานที่ผ่านการตรวจและผล quiz