struct, pointer และบัฟเฟอร์วงแหวน
เป้าหมาย
หัวข้อที่มีชื่อว่า “เป้าหมาย”เมื่อจบบทเรียนนี้ คุณจะ
- ออกแบบ struct สำหรับข้อมูลเซนเซอร์หนึ่งชุด และส่งให้ฟังก์ชันด้วย pointer แบบ const เมื่อไม่ต้องแก้
- เขียนบัฟเฟอร์วงแหวนขนาดคงที่ที่อ่านและเขียนได้โดยไม่ทับข้อมูลที่ยังไม่ถูกอ่าน
- อธิบายความเสี่ยงเมื่อ ISR กับ task ใช้บัฟเฟอร์เดียวกัน และวิธีป้องกัน
ใช้เวลาประมาณ 70 นาที (แนวคิด 15 · ฝึก 25 · แล็บ 25 · เช็ก 5)
ก่อนเริ่ม
หัวข้อที่มีชื่อว่า “ก่อนเริ่ม”ทวนจากบทเรียน 1.2 สองข้อ
- บัฟเฟอร์ 512 ไบต์ที่ประกาศเป็นตัวแปร local ใน task ที่มี stack 256 word จะเกิดอะไรขึ้น แล้วควรประกาศแบบไหนแทน
- ทำไม
head - tailของตัวนับuint32_tจึงยังถูกต้องแม้headจะวนกลับผ่านศูนย์ไปแล้ว (คำใบ้อยู่ในบทเรียน 1.1)
ดูของจริงก่อน
หัวข้อที่มีชื่อว่า “ดูของจริงก่อน”เปิด examples/03_sensor_sample.c ทายก่อนรัน ว่า sizeof(imu_loose_t) เท่ากับเท่าไร
สมาชิกของมันคือ uint8_t หนึ่งตัว uint32_t หนึ่งตัว int16_t สามตัว และ uint8_t อีกหนึ่งตัว รวมกันได้ 12 ไบต์ แล้วรัน
gcc -std=c11 -Wall -Wextra -o sensor_sample examples/03_sensor_sample.c./sensor_sampleบนคอมพิวเตอร์ทั่วไปและบน Cortex-M จะได้ 16 ไม่ใช่ 12 เพราะคอมไพเลอร์เติมช่องว่าง (padding) ให้ uint32_t เริ่มที่ขอบ 4 ไบต์
พอเรียงสมาชิกใหม่จากใหญ่ไปเล็ก struct แบบเดียวกันเหลือ 12 ไบต์ ข้อมูลเซนเซอร์ที่เก็บเป็นพันชุดในบัฟเฟอร์ ต่างกัน 4 ไบต์ต่อชุดคือหลายกิโลไบต์
1. struct หนึ่งก้อนคือข้อมูลหนึ่งชุด และ const pointer คือสัญญา
หัวข้อที่มีชื่อว่า “1. struct หนึ่งก้อนคือข้อมูลหนึ่งชุด และ const pointer คือสัญญา”ค่าที่เกิดพร้อมกันควรอยู่ด้วยกัน ความเร่งสามแกนที่อ่านจากการแปลงครั้งเดียวกัน เวลาที่อ่าน และธงว่าข้อมูลใช้ได้ไหม
ถ้ากระจายเป็นตัวแปรหลายตัว ฟังก์ชันจะรับพารามิเตอร์ยาวเหยียด และง่ายที่จะหยิบแกน X จากรอบนี้ไปคู่กับแกน Y จากรอบก่อน
SDK ใช้ struct แบบนี้ทั่วไป เช่น health_t ใน 07_engine_health.c
เก็บตัวนับหกตัวที่อ่านในจังหวะเดียวกัน แล้วให้ sample(health_t *h) เติมผ่าน pointer
กติกาการส่ง struct ให้ฟังก์ชัน
| ฟังก์ชันจะ | ส่งแบบ | ตัวอย่าง |
|---|---|---|
| อ่านอย่างเดียว | const T * |
int32_t magnitude_sq(const imu_sample_t *s) |
| เติมหรือแก้ค่าให้ผู้เรียก | T * (เป็น “ช่องส่งผลลัพธ์”) |
bool radar_dsp_snapshot(ipc_radar_range_t *out) ของ SDK |
| ต้องการสำเนาของตัวเอง และ struct เล็ก | by value | ได้สำเนา แก้สำเนาไม่กระทบตัวจริง แต่ต้องคัดลอกทุกไบต์ลง stack |
const ไม่ได้ทำให้โค้ดเร็วขึ้น แต่ทำให้คอมไพเลอร์ช่วยจับเมื่อเราเผลอแก้ของที่สัญญาว่าจะไม่แก้ และบอกผู้อ่านโค้ดทันทีว่าฟังก์ชันนี้ปลอดภัย
ตัวอย่าง 04_gpio_led_button.c
ของ SDK ใช้ตาราง static const led_def_t s_leds[] และหยิบแถวหนึ่งมาใช้ด้วย const led_def_t *mirror (บรรทัด 203) คือ pointer ไปยังข้อมูลที่ห้ามแก้
2. บัฟเฟอร์วงแหวน (ring buffer)
หัวข้อที่มีชื่อว่า “2. บัฟเฟอร์วงแหวน (ring buffer)”เมื่อข้อมูลไหลเข้ามาเป็นจังหวะ เช่นไบต์จาก UART หรือค่าจากเซนเซอร์ และผู้ใช้ข้อมูลทำงานคนละจังหวะกับผู้ผลิต
เราต้องมีที่พักข้อมูลขนาดคงที่ที่ไม่ต้อง malloc บัฟเฟอร์วงแหวนคืออาร์เรย์ที่ใช้ซ้ำเป็นวง ผู้เขียนเลื่อน head ผู้อ่านเลื่อน tail
data: [ . | . | A | B | C | . | . | . ] A B C ยังไม่ถูกอ่าน ^tail ^head count = head - tail = 3การออกแบบในแบบฝึกของบทนี้ใช้สามเทคนิค
- ขนาดเป็นกำลังของสอง หาตำแหน่งด้วย
counter & MASKแทน%ซึ่งช้ากว่าบน CPU บางรุ่น (บัฟเฟอร์ของเรดาร์ใน SDK ขนาด0x4000ก็ใช้วิธีนี้) - head และ tail เป็นตัวนับที่เดินไปเรื่อย ๆ จำนวนข้อมูลคือ
head - tailเสมอ แยกกรณี “ว่าง” (เท่ากัน) กับ “เต็ม” (ต่างกันเท่าความจุ) ได้โดยไม่ต้องเสียช่องไปหนึ่งช่อง - เต็มแล้วปฏิเสธ
pushคืนfalseผู้เรียกตัดสินเองว่าจะนับว่าข้อมูลหาย หรือจะรอ
นโยบายตอนเต็มเป็นการตัดสินใจ ไม่ใช่รายละเอียด task เรดาร์ของ SDK เลือกตรงข้ามกับเรา: เมื่อเต็มมัน “keep newest samples, drop oldest two”
(radar_task.c บรรทัด 466-480)
เพราะสำหรับการตรวจจับคน ข้อมูลล่าสุดมีค่ากว่าข้อมูลเก่า แต่สำหรับไบต์ของโปรโตคอล การทิ้งไบต์เก่าทำให้เฟรมพังทั้งเฟรม
อีกเรื่องที่ต้องตัดสินคือค่าคืนตอน “ว่าง” TACP ของ SDK (variant mtb-mpy) คืน -1 เมื่อว่างและ 0 ถึง 255 เมื่อมีข้อมูล
และเตือนว่าต้องเก็บใส่ int แล้วตรวจ -1 ก่อนแปลงเป็น uint8_t เพราะ “a byte of 0xFF is real data and is not the empty marker”
(08_tacp_host_protocol.c บรรทัด 46-48)
แบบฝึกของเราเลี่ยงปัญหานี้ด้วยการแยกค่าคืน bool ออกจากข้อมูลที่ส่งกลับผ่าน pointer
3. เมื่อ ISR กับ task ใช้บัฟเฟอร์เดียวกัน
หัวข้อที่มีชื่อว่า “3. เมื่อ ISR กับ task ใช้บัฟเฟอร์เดียวกัน”ISR แทรก task ได้ทุกเมื่อ แม้กลางบรรทัด rb->head = rb->head + 1; ซึ่งเป็นอ่าน แก้ เขียน ถ้าสองฝ่ายแก้ตัวแปรเดียวกัน ค่าหายได้
ถ้าผู้เขียนเลื่อน head ก่อนเขียนข้อมูลลงช่อง ผู้อ่านอาจหยิบช่องที่ยังว่างไปใช้ และถ้า task อ่านข้อมูลหลายไบต์ที่ ISR กำลังเขียนทับ
จะได้ข้อมูลครึ่งเก่าครึ่งใหม่ (torn read) วิธีป้องกันที่ SDK ใช้จริงมีสี่แบบ
| วิธี | ใช้เมื่อ | ที่ใช้ใน SDK |
|---|---|---|
| ring buffer แบบผู้เขียนหนึ่ง ผู้อ่านหนึ่ง: แต่ละฝ่ายแก้ตัวนับของตัวเองเท่านั้น เขียนข้อมูลก่อนแล้วจึงเลื่อนตัวนับ มี barrier คั่น | ไบต์หรือค่าเล็ก ๆ ไหลต่อเนื่อง | ring แบบ lock-free ของ TACP (variant mtb-mpy) ที่ tacp_request_delete_main_from_isr() ใส่ไบต์ลงได้จากบริบท ISR |
ให้ ISR ส่งสำเนาเข้าคิวของ RTOS ด้วย xQueueSendFromISR() แล้วให้ task รับไปทำ |
ข้อความเป็นก้อน ต้องการให้ task ตื่นทันที | sensor_auto_task.c บรรทัด 274-299 |
| seqlock: ผู้เขียนทำเลขลำดับเป็นเลขคี่ระหว่างเขียน ผู้อ่านคัดลอกแล้วตรวจว่าเลขลำดับไม่เปลี่ยนและเป็นเลขคู่ ถ้าไม่ใช่ก็อ่านใหม่ | snapshot ที่อ่านบ่อยและทนอ่านซ้ำได้ | radar_dsp.c บรรทัด 284-320 |
| critical section: ปิด interrupt สั้นที่สุดระหว่างอ่านหรือเขียน | ข้อมูลหลายตัวแปรที่ต้องเปลี่ยนพร้อมกัน | taskENTER_CRITICAL() ของ FreeRTOS |
ข้อที่ห้ามทำคือเรียก malloc หรือ printf ใน ISR (บทเรียน 1.2 และโมดูล 4) และคิดว่า volatile อย่างเดียวพอ
volatile บังคับให้อ่านเขียนจริง แต่ไม่ได้ทำให้การอ่าน แก้ เขียน กลายเป็นหนึ่งจังหวะ
ตัวอย่างสมบูรณ์
หัวข้อที่มีชื่อว่า “ตัวอย่างสมบูรณ์”examples/03_sensor_sample.c ทำงานเป็นสามท่า
- ท่าที่ 1 เทียบ struct สองแบบที่มีสมาชิกเหมือนกันแต่เรียงต่างกัน พิมพ์
sizeofและoffsetofให้เห็นว่า padding อยู่ตรงไหน - ท่าที่ 2
sample_fill()รับimu_sample_t *เพื่อเติมค่า ส่วนmagnitude_sq()รับconst imu_sample_t *เพราะอ่านอย่างเดียว - ท่าที่ 3 struct อยู่ใน stack ของ
mainและส่งที่อยู่ต่อ ส่วนtry_to_reset()รับแบบ by value จึงแก้ได้แค่สำเนา
ลองแก้ทีละอย่าง ทายก่อนรันทุกครั้ง
- เปิดบรรทัด
s->ax = 0;ในmagnitude_sq()คอมไพเลอร์บอกอะไร - สลับ
validกับseqไปไว้หน้าสุดของimu_sample_tขนาดเปลี่ยนไหม เพราะอะไร - เปลี่ยน
try_to_reset()ให้รับimu_sample_t *แล้วแก้ผ่าน pointer ผลบรรทัดสุดท้ายเปลี่ยนเป็นอะไร
อ่านต่อจากของจริงใน SDK: ฝั่งผู้อ่านของ seqlock ใน radar_dsp.c คัดลอก struct ทั้งก้อนผ่าน pointer out แล้ววนอ่านใหม่จนกว่าจะได้ชุดที่ไม่ขาดกลาง
bool radar_dsp_snapshot(ipc_radar_range_t *out){ if (out == NULL) { return false; } if (!s_ready) { memset(out, 0, sizeof(*out)); out->initialized = s_inited ? 1 : 0; return false; } uint32_t s1, s2; do { s1 = s_snap_seq; memcpy(out, (const void *)&s_snap, sizeof(*out)); s2 = s_snap_seq; } while ((s1 != s2) || (s1 & 1u)); /* retry on torn/odd */ return true;}ที่มา: radar_dsp.c บรรทัด 303-320
(Apache-2.0, tesaiot-pse84-devkit-sdk) ฝั่งผู้เขียน (บรรทัด 284-297) เพิ่มเลขลำดับเป็นเลขคี่ เรียก __DMB() เขียนทุกช่อง เรียก __DMB() อีกครั้ง
แล้วจึงเพิ่มเลขลำดับเป็นเลขคู่ ให้สังเกตว่าฟังก์ชันคืน false เมื่อยังไม่มีเฟรมแรก ซึ่งต่างจาก “ไม่มีเป้าหมาย” ผลลัพธ์ที่บอกความจริงเป็นหัวข้อของบทเรียน 3.2
ฝึกเติม
หัวข้อที่มีชื่อว่า “ฝึกเติม”เปิด practice/03_ring_buffer.c มีช่องให้เติม 5 จุด มากกว่าสองบทก่อน เพราะคุณมีเครื่องมือครบแล้ว
rb_count() rb_is_empty() rb_is_full() rb_push() และ rb_pop() test ครอบคลุมสามกรณี
- กรณีปกติ เข้าก่อนออกก่อน และอ่านจากบัฟเฟอร์ว่างต้องได้
false - กรณีขอบ ใส่ครบ 16 ไบต์แล้วไบต์ที่ 17 ต้องถูกปฏิเสธ และไบต์แรกต้องยังเป็นค่าเดิม
- กรณีขอบ ตัวนับเริ่มที่
UINT32_MAX - 3แล้ววิ่งผ่านศูนย์ระหว่างเขียนอ่าน 1000 รอบ
gcc -std=c11 -Wall -Wextra -o ring practice/03_ring_buffer.c && ./ringลองเองก่อนอย่างน้อย 15 นาที แล้วเปิด solution/03_ring_buffer.c
สิ่งที่เฉลยเพิ่มจากโค้ดที่ test ต้องการคือ atomic_thread_fence() ของ C11 คั่นระหว่าง “เขียนข้อมูล” กับ “เลื่อน head”
และระหว่าง “อ่านข้อมูล” กับ “เลื่อน tail” บนคอมพิวเตอร์ test ผ่านได้แม้ไม่มีบรรทัดนี้ แต่เมื่อผู้เขียนเป็น ISR หรือเป็นอีกคอร์
มันคือสิ่งที่กันไม่ให้คอมไพเลอร์หรือ CPU สลับลำดับ บน Cortex-M ใน SDK ใช้ __DMB() ของ CMSIS ทำหน้าที่นี้
นี่คือตัวอย่างของสิ่งที่ test บนเครื่องโฮสต์พิสูจน์ไม่ได้ ซึ่งเราจะกลับมาคุยในบทเรียน 6.2
เช็กความเข้าใจ
หัวข้อที่มีชื่อว่า “เช็กความเข้าใจ”ตอบคำถาม 5 ข้อใน quiz.yaml (บนเว็บไซต์อยู่ท้ายหน้านี้) ครอบคลุมเป้าหมายทั้งสามข้อ ตอบถูกตั้งแต่ 4 ข้อขึ้นไปถือว่าจบบทเรียน
งาน: ดู struct ที่ส่งผ่าน pointer และ seqlock ทำงานจริงบนบอร์ด แล้วอ่านโค้ดเพื่อหาว่าใครเป็นเจ้าของข้อมูล
- build แม่แบบของ SDK ด้วย
make build -j ENABLE_PAGE_EXAMPLES=1แล้วmake programถอดสาย USB ให้สุดแล้วเสียบใหม่ (ขั้นตอนเต็มอยู่ใน บทเรียน 2.1) - บนจอ แตะ SDK Examples แล้วรัน
cm55/sensors/02_radar_presenceตัวอย่างนี้อ่านradar_dsp_snapshot()ทุก 200 ms สังเกตบรรทัดบนสุด เมื่อไม่มีเป้าหมายจะแสดงno target seq ... bin width ... mmและเลขseqจะเพิ่มขึ้นเรื่อย ๆ ยื่นมือเข้าหาบอร์ดช้า ๆ แล้วดูบรรทัดเปลี่ยนเป็นtarget ... mm bin ... - จดค่า
seqสองครั้งห่างกันประมาณสิบวินาที แล้วประมาณว่าเรดาร์ประกาศ snapshot ใหม่กี่ครั้งต่อวินาที - อ่าน ipc_tesaiot_handler.c บรรทัด 330-363
ฟังก์ชันนี้ทำงานในบริบท ISR คัดลอกข้อความลงตัวแปร
s_pendingช่องเดียว แล้วปลุก task ด้วย semaphore ตอบในบันทึกว่า ถ้าข้อความที่สองมาถึงก่อน task จะอ่านข้อความแรกเสร็จ จะเกิดอะไรกับs_pendingแล้วค้นในไฟล์เดียวกันหรือไฟล์ของฝั่ง CM55 ว่ามีอะไรรับประกันว่าเหตุการณ์นั้นเกิดไม่ได้หรือไม่ (ถ้าหาไม่เจอ ให้เขียนว่าหาไม่เจอ ไม่ต้องเดา) - เทียบกับ sensor_auto_task.c บรรทัด 274-299 ซึ่งส่งสำเนา struct เข้าคิว วิธีไหนรับข้อความที่มาติดกันได้มากกว่า และแลกกับอะไร
หลักฐานที่เก็บไว้ใน portfolio: ภาพหน้าจอผลของตัวอย่างเรดาร์ทั้งสองสถานะ ค่า seq ที่จดและการคำนวณ คำตอบข้อ 4 และ 5
- ขยายแบบฝึกให้บัฟเฟอร์เก็บ
imu_sample_tทั้งก้อนแทนไบต์ แล้วคิดว่าต้องเปลี่ยนอะไรบ้าง (คำใบ้: ขนาดของช่อง และการคัดลอกทีละก้อน) - โจทย์ท้าทาย: เพิ่มตัวนับ
droppedที่rb_push()เพิ่มทุกครั้งที่ปฏิเสธ แล้วถามตัวเองว่าใครเป็นเจ้าของตัวนับนี้ ผู้เขียนหรือผู้อ่าน
บทถัดไปเข้าสู่โมดูล 2: บทเรียน 2.1 ชุดเครื่องมือและการ build ครั้งแรก
สะท้อนคิด
หัวข้อที่มีชื่อว่า “สะท้อนคิด”- ในงานที่คุณเคยทำ มีข้อมูลตัวไหนที่ควรทิ้งของเก่าเมื่อเต็ม และตัวไหนที่ห้ามทิ้งเลย
- ถ้า test ผ่านบนคอมพิวเตอร์ทุกครั้ง แต่บนบอร์ดข้อมูลเพี้ยนนาน ๆ ครั้ง คุณจะสงสัยอะไรก่อน
แหล่งอ้างอิง
หัวข้อที่มีชื่อว่า “แหล่งอ้างอิง”- SDK: cm33/connectivity/08_tacp_host_protocol.c (ring buffer และฟังก์ชัน _from_isr)
- SDK: tesaiot-radar/radar_dsp.c (seqlock ของ snapshot)
- SDK: cm55/sensors/02_radar_presence.c (อ่าน snapshot จากฝั่งจอ)
- B3 — The IPC backbone: setup, deferred binding, snapshots (เอกสาร SDK สร้างจาก commit ef72c1b)
- FreeRTOS documentation
คำถามทบทวน
ลองตอบเองก่อน แล้วค่อยเปิดดูเฉลย
-
ฟังก์ชัน print_sample() แค่อ่านค่าจาก imu_sample_t แล้วพิมพ์ ควรประกาศพารามิเตอร์แบบใด (เป้าหมายข้อ 1)
- void print_sample(imu_sample_t s)
- void print_sample(imu_sample_t *s)
- void print_sample(const imu_sample_t *s)
- void print_sample(volatile imu_sample_t *s)
ดูเฉลย
คำตอบ: C. void print_sample(const imu_sample_t *s)
const pointer ไม่คัดลอกทั้งก้อนลง stack และคอมไพเลอร์จะไม่ยอมถ้าฟังก์ชันเผลอแก้ค่า by value ใช้ได้แต่เสียการคัดลอก pointer ธรรมดาไม่บอกว่าฟังก์ชันจะไม่แก้ ส่วน volatile ไม่เกี่ยวกับเรื่องนี้
-
struct { uint8_t a; uint32_t b; uint8_t c; } บน Cortex-M ด้วย GCC มักมีขนาดเท่าไร และจะลดได้อย่างไร (เป้าหมายข้อ 1)
- 6 ไบต์ ลดไม่ได้
- 12 ไบต์ เรียงใหม่เป็น b, a, c จะเหลือ 8 ไบต์
- 8 ไบต์ เรียงใหม่แล้วเหลือ 6 ไบต์
- 12 ไบต์ เรียงแบบไหนก็ได้ 12 ไบต์
ดูเฉลย
คำตอบ: B. 12 ไบต์ เรียงใหม่เป็น b, a, c จะเหลือ 8 ไบต์
b ต้องเริ่มที่ขอบ 4 ไบต์ จึงมีช่องว่าง 3 ไบต์หลัง a และ struct ทั้งก้อนต้องยาวเป็นพหุคูณของ 4 จึงได้ 12 เรียงตัวใหญ่ก่อนเป็น b, a, c ได้ 4 + 1 + 1 แล้วเติมท้ายเป็น 8
-
บัฟเฟอร์วงแหวนความจุ 16 ใช้ตัวนับ uint32_t แบบเดินไปเรื่อย ๆ ตอนนี้ head = 5 และ tail = 0xFFFFFFF9 มีข้อมูลที่ยังไม่อ่านกี่ตัว (เป้าหมายข้อ 2)
- ติดลบ แปลว่าบัฟเฟอร์เสีย
- 12
- 16 เพราะเต็ม
- 4294967284
ดูเฉลย
คำตอบ: B. 12
5 - 0xFFFFFFF9 ใน uint32_t วนกลับได้ 12 (0xFFFFFFF9 คือ -7 ถ้ามองแบบมีเครื่องหมาย 5 - (-7) = 12) การลบของ unsigned เป็น modulo 2^32 ตัวนับจึงวนผ่านศูนย์ได้โดย count ยังถูก
-
เรียงขั้นของ rb_push() ที่ไม่ทับข้อมูลเก่าและปลอดภัยเมื่อผู้อ่านเป็นอีกบริบท (เป้าหมายข้อ 2)
- rb->head = h + 1u; (ประกาศว่ามีข้อมูลใหม่)
- if (rb_is_full(rb)) return false;
- atomic_thread_fence(memory_order_release); (หรือ __DMB() บน Cortex-M)
- rb->data[h & RB_MASK] = byte;
ดูเฉลย
ลำดับที่ถูก: B. if (rb_is_full(rb)) return false; → D. rb->data[h & RB_MASK] = byte; → C. atomic_thread_fence(memory_order_release); (หรือ __DMB() บน Cortex-M) → A. rb->head = h + 1u; (ประกาศว่ามีข้อมูลใหม่)
ตรวจเต็มก่อนเพื่อไม่ทับข้อมูลที่ยังไม่ถูกอ่าน เขียนข้อมูลลงช่อง ใส่ barrier ให้ข้อมูลถึงหน่วยความจำ แล้วจึงเลื่อน head ถ้าเลื่อน head ก่อน ผู้อ่านอาจหยิบช่องที่ยังว่างไป
-
ISR ได้รับข้อความจากอีกคอร์และต้องส่งให้ task ทำงานต่อ วิธีใดเหมาะสม (เลือกได้หลายข้อ) (เป้าหมายข้อ 3)
- คัดลอกข้อความเข้าคิวด้วย xQueueSendFromISR() แล้วเรียก portYIELD_FROM_ISR()
- เรียก malloc() สร้างก้อนใหม่ใน ISR แล้วส่ง pointer ให้ task
- ใส่ลง ring buffer ที่ ISR แก้เฉพาะ head และ task แก้เฉพาะ tail โดยเขียนข้อมูลก่อนเลื่อน head
- ประกาศตัวแปรร่วมเป็น volatile แล้วให้ทั้ง ISR และ task ทำ count++ กับมันได้เลย
ดูเฉลย
คำตอบ: A. คัดลอกข้อความเข้าคิวด้วย xQueueSendFromISR() แล้วเรียก portYIELD_FROM_ISR() · C. ใส่ลง ring buffer ที่ ISR แก้เฉพาะ head และ task แก้เฉพาะ tail โดยเขียนข้อมูลก่อนเลื่อน head
คิวของ RTOS คัดลอกข้อมูลให้และปลุก task ส่วน ring แบบผู้เขียนหนึ่ง ผู้อ่านหนึ่งให้แต่ละฝ่ายแก้เฉพาะตัวนับของตัวเอง malloc ใน ISR ผิดกติกาของบทเรียน 1.2 และ volatile ไม่ได้ทำให้ count++ ที่สองฝ่ายแก้พร้อมกันปลอดภัย
อ้างอิงบทเรียนนี้
ถ้านำบทเรียนนี้ไปสอน ทำสไลด์ หรือทำเอกสารต่อ ให้อ้างอิงด้วยข้อความนี้ ถ้าดัดแปลงเนื้อหา ให้เติม (ดัดแปลง)ต่อท้ายชื่อบทเรียน
"struct, pointer และบัฟเฟอร์วงแหวน" จาก TESA Open Knowledge โดยสมาคมสมองกลฝังตัวไทย (Thai Embedded Systems Association: TESA) https://github.com/tesaiot/tesa-qualification-program สัญญาอนุญาต CC BY-NC 4.0
ข้อความอ้างอิงภาษาอังกฤษ: "Structs, pointers and ring buffers" from TESA Open Knowledge by the Thai Embedded Systems Association (TESA), https://github.com/tesaiot/tesa-qualification-program, licensed under CC BY-NC 4.0
บทเรียนนี้ดัดแปลงจากต้นฉบับด้านล่าง เมื่ออ้างอิงให้คงเครดิตต้นฉบับไว้ด้วย
https://github.com/tesaiot/tesaiot-pse84-devkit-sdk/tree/ef72c1b658178eee8c38b1e47d28b006f80a59b5 · SDK examples and docs are linked at this commit, not copied into this course. Lessons quote short excerpts (at most 25 lines) with a link to the file at this commit and the credit (Apache-2.0, tesaiot-pse84-devkit-sdk).
TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0
เนื้อหาเผยแพร่ภายใต้ CC BY-NC 4.0 นำไปใช้ต่อในงานที่ไม่ใช่เพื่อการค้าได้ โปรดอ้างอิงสมาคมสมองกลฝังตัวไทย (TESA) ทุกครั้ง · วิธีอ้างอิง TESA