UART
เป้าหมาย
หัวข้อที่มีชื่อว่า “เป้าหมาย”เมื่อจบบทเรียนนี้ คุณจะ
- ถอดรหัสเฟรม UART หนึ่งเฟรมจากภาพสัญญาณได้ครบ start bit, data, parity และ stop bit
- อธิบายว่าทำไม UART หนึ่งพอร์ตควรมีเจ้าของเพียงงานเดียว และส่งต่อข้อมูลผ่านบัฟเฟอร์
- ตั้งค่า logic analyzer ให้ถอดรหัส UART ที่ baud rate ที่กำหนดได้
ใช้เวลาประมาณ 70 นาที (แนวคิด 15 · ฝึก 25 · แล็บ 25 · เช็ก 5) แล็บต้องมี logic analyzer ที่รับสัญญาณ 3.3 V ได้ และโปรแกรม PulseView หรือโปรแกรมของผู้ผลิตเครื่องที่ถอดรหัส UART ได้
ก่อนเริ่ม
หัวข้อที่มีชื่อว่า “ก่อนเริ่ม”ทวนจากบทก่อนหน้าสองข้อ
- debug UART ของบอร์ดนี้ใช้สัญญาณนาฬิกา 100 MHz ตัวหาร 86 และ oversample 10 ได้ baud จริงเท่าไร และคลาดจาก 115200 กี่เปอร์เซ็นต์ (บทเรียน 4.2)
- ring buffer ในบทเรียน 1.3 ให้ผู้เขียนแก้อะไร และผู้อ่านแก้อะไร
ดูของจริงก่อน
หัวข้อที่มีชื่อว่า “ดูของจริงก่อน”เปิด examples/11_uart_frame.c โปรแกรมนี้วาดรูปคลื่นของหนึ่งไบต์บนสาย UART ทายก่อนรัน ว่าไบต์ 0xA5
บิตแรกหลัง start bit เป็น 1 หรือ 0
gcc -std=c11 -Wall -Wextra -o uart_frame examples/11_uart_frame.c./uart_frameบิตแรกเป็น 1 เพราะ UART ส่ง บิตต่ำสุด (LSB) ก่อน 0xA5 คือ 1010 0101 บนสายจึงเรียงเป็น 1 0 1 0 0 1 0 1 อ่านจากซ้ายไปขวา
ที่ 115200 baud หนึ่งบิตยาว 8.68 ไมโครวินาที และหนึ่งเฟรมแบบ 8N1 ยาวสิบบิต ไบต์เดียวกันนี้คือไบต์แรกที่คุณจะจับได้จากขาของบอร์ดในแล็บ
1. หนึ่งเฟรมของ UART
หัวข้อที่มีชื่อว่า “1. หนึ่งเฟรมของ UART”UART ไม่มีสายสัญญาณนาฬิกา ทั้งสองฝั่งตกลงความเร็ว (baud rate) กันไว้ก่อน แล้วใช้ขอบของ start bit เป็นจุดตั้งเวลา
idle start D0 D1 D2 D3 D4 D5 D6 D7 [parity] stop idle ‾‾‾‾‾\_____/‾‾‾ ... ข้อมูล 8 บิต LSB ก่อน ... [P] ‾‾‾‾ ‾‾‾‾| ส่วน | ระดับ | หน้าที่ |
|---|---|---|
| idle | 1 | สายว่างถูกดึงไว้ที่ระดับสูง |
| start | 0 | ขอบขาลงบอกผู้รับว่าเฟรมเริ่ม ผู้รับนับเวลาจากขอบนี้ |
| data | ตามข้อมูล | 5 ถึง 9 บิต ส่วนใหญ่ 8 บิต LSB ก่อน |
| parity (ถ้ามี) | ตามกติกา | even หรือ odd ให้จำนวน 1 รวมเป็นคู่หรือคี่ จับบิตผิดได้เป็นจำนวนคี่เท่านั้น |
| stop | 1 | 1 หรือ 2 บิต ถ้าผู้รับเห็น 0 ตรงนี้คือ framing error ซึ่งมักแปลว่า baud ไม่ตรงกัน |
ค่าตั้งของ debug UART ใน BSP ของ SDK ตรงกับ 8N1 ทุกข้อ dataWidth = 8UL, parity = CY_SCB_UART_PARITY_NONE,
stopBits = CY_SCB_UART_STOP_BITS_1, enableMsbFirst = false และ oversample = 10
(cycfg_peripherals.c บรรทัด 584-608)
oversample คือจำนวนจังหวะนาฬิกาต่อหนึ่งบิต ผู้รับใช้มันหากลางบิต จุดที่ไกลจากขอบที่สุด จึงทนความคลาดของ baud ได้ระดับหนึ่ง
แม่แบบเปิดพอร์ตนี้ใน init_retarget_io() ด้วย Cy_SCB_UART_Init() และ Cy_SCB_UART_Enable() แล้วผูกกับ printf ผ่าน retarget-io
(retarget_io_init.c บรรทัด 59-93)
2. หนึ่งพอร์ต หนึ่งเจ้าของ
หัวข้อที่มีชื่อว่า “2. หนึ่งพอร์ต หนึ่งเจ้าของ”UART คือสายไบต์เส้นเดียว ถ้าสอง task อ่านพอร์ตเดียวกัน ไบต์จะถูกแบ่งไปคนละครึ่งโดยไม่มีใครได้ข้อความครบ
ตัวอย่าง 08_tacp_host_protocol.c
ของ SDK (variant mtb-mpy) มีหัวข้อว่า “ONE OWNER. EXACTLY ONE.” และอธิบายว่า “a split stream is not a protocol — a magic byte lands in one task
and the command byte in the other, and both see garbage” งานอื่นที่อยากได้ข้อมูลจึงรับต่อจากเจ้าของผ่านบัฟเฟอร์ ในไฟล์นั้นคือ ring ที่อ่านด้วย
tacp_ring_buf_read() ซึ่งคืน -1 เมื่อว่าง (บทเรียน 1.3)
ขาส่งก็มีเจ้าของเหมือนกัน printf ของ retarget-io ถือ mutex ระหว่างพิมพ์ ตัวรันตัวอย่างของ SDK จึงตั้ง task ของมันไว้ที่ priority 1
พร้อมเหตุผลว่า mutex นี้ “with no priority inheritance” task ที่ความสำคัญต่ำที่สุดจึง “can only ever be the waiter, never the holder that blocks somebody more important”
(sdk_examples_cm33.c)
และห้าม printf จาก ISR เด็ดขาด ในแม่แบบนี้คอร์ CM33_NS เป็นเจ้าของ console ตัวเดียวของบอร์ด ส่วน CM55 ไม่มี console เลย (บทเรียน 2.1)
3. อ่านสาย UART ด้วย logic analyzer
หัวข้อที่มีชื่อว่า “3. อ่านสาย UART ด้วย logic analyzer”logic analyzer สุ่มระดับของสายเป็น 0 หรือ 1 หลายครั้งต่อบิต แล้วโปรแกรมถอดรหัสทำสิ่งเดียวกับผู้รับ UART หาขอบ start แล้วอ่านกลางบิต ตั้งค่าให้ตรงกับผู้ส่งทุกข้อ
| ตั้งค่า | สำหรับ UART บน header ของบอร์ดนี้ |
|---|---|
| ขาที่วัด | TX ของ header คือ P15.1 (SCB9) และต่อกราวด์ของ analyzer กับกราวด์ของบอร์ดเสมอ |
| ระดับแรงดัน | 3.3 V ตรวจว่า analyzer ของคุณรับได้ |
| อัตราสุ่ม | หลายเท่าของ baud เช่น 1 MHz ขึ้นไปสำหรับ 115200 ยิ่งสูงยิ่งเห็นขอบชัด |
| decoder | UART, baud 115200, data 8 บิต, parity none, stop 1, bit order LSB first, สัญญาณไม่กลับขั้ว |
| trigger | ขอบขาลงบนสาย TX เพื่อจับเฟรมแรก |
ขาและรูปแบบนี้มาจากตาราง “Peripherals at a glance” ของเอกสาร SDK (“QWA309 header UART | P15.0 RX / P15.1 TX | SCB9, 115200 8N1”) ถ้า decoder ขึ้น framing error ทุกเฟรม ให้สงสัย baud ก่อนสิ่งอื่น ถ้าขึ้นไบต์ที่ดูเหมือนกลับบิต ให้สงสัยลำดับบิตหรือขั้วของสัญญาณ
ตัวอย่างสมบูรณ์
หัวข้อที่มีชื่อว่า “ตัวอย่างสมบูรณ์”examples/11_uart_frame.c ทำงานเป็นสามท่า
- ท่าที่ 1
uart_encode()สร้างระดับของ start, data แบบ LSB ก่อน, parity และ stop - ท่าที่ 2
draw()วาดรูปคลื่นแบบข้อความพร้อมป้าย S, D0 ถึง D7, P, T และเวลาของบิตกับเฟรม - ท่าที่ 3 วาด
0xA5แบบ 8N1 และ 8E1 และ0x11ซึ่งเป็นไบต์ที่สองที่ตัวอย่าง Header I/O Test ส่ง แล้วคำนวณ throughput
ลองแก้แล้วทายก่อนรัน
- เปลี่ยน baud เป็น 9600 ความยาวของเฟรมเป็นเท่าไร และถ้าต้องส่ง log 200 ไบต์ต่อวินาที 9600 พอไหม
- เพิ่ม
PARITY_ODDให้0xA5บิต parity เป็นอะไร - วาด
0xB4แล้วเทียบกับรูปคลื่นที่คุณจะจับได้ในแล็บ
ฝึกเติม
หัวข้อที่มีชื่อว่า “ฝึกเติม”เปิด practice/11_uart_decode.c ตัวถอดรหัสที่ทำงานแบบเดียวกับ logic analyzer มีช่องให้เติม 3 จุด เป็นบทแรกของโมดูล 5 จึงเว้นว่างน้อย
- อ่านบิตข้อมูลที่กลางบิต LSB ก่อน
- ตรวจ parity แบบ even และ odd
- ตรวจ stop bit แล้วคืน framing error เมื่อผิด
gcc -std=c11 -Wall -Wextra -o uart_decode practice/11_uart_decode.c && ./uart_decodetest มีกรณี 0xFF ซึ่งเป็นข้อมูลจริงไม่ใช่ “ไม่มีข้อมูล” และกรณี stop bit เป็น 0 ที่จำลองอาการ baud ไม่ตรงกัน
ลองเองก่อนอย่างน้อย 15 นาที แล้วเปิด solution/11_uart_decode.c คอมเมนต์ในเฉลยชี้ข้อจำกัดของ parity ที่คนมักลืม มันจับได้เฉพาะเมื่อบิตผิดเป็นจำนวนคี่ ถ้าผิดสองบิตพร้อมกัน parity ยังถูก ข้อมูลที่สำคัญจึงต้องมี checksum หรือ CRC ในระดับข้อความด้วย ตัวอย่าง Header I/O Test ปิดท้ายทุกแพ็กเก็ตด้วย XOR checksum และ TACP ของ SDK ใช้ CRC-16
เช็กความเข้าใจ
หัวข้อที่มีชื่อว่า “เช็กความเข้าใจ”ตอบคำถาม 5 ข้อใน quiz.yaml (บนเว็บไซต์อยู่ท้ายหน้านี้) ครอบคลุมเป้าหมายทั้งสามข้อ ตอบถูกตั้งแต่ 4 ข้อขึ้นไปถือว่าจบบทเรียน
งาน: จับเฟรม UART จริงจากขาของบอร์ดด้วย logic analyzer ถอดรหัสด้วยตาก่อน แล้วยืนยันด้วย decoder
- เปิดตัวอย่าง QWA309 — Header I/O Test บน Developer Hub แล้ว flash เฟิร์มแวร์สำเร็จรูปของตัวอย่างลงบอร์ด (ตัวอย่างนี้ใช้ master template ของ Developer Hub ไม่ใช่แม่แบบของ SDK ภาพรวมอยู่ใน TESAIoT Firmware Stack บทเรียน 1.1)
- ต่อ logic analyzer: ช่องหนึ่งที่ P15.1 (TX ของ UART บน header) และกราวด์ ดูตำแหน่งขาจากแผ่นวงจรหรือเอกสารของบอร์ด ตั้งค่าตามตารางในแนวคิดข้อ 3
- เริ่มจับ แล้วกดปุ่ม UART Echo บนจอ จอจะพิมพ์บรรทัด
TX: A5 11 00 B4(เลขตัวที่สามเพิ่มทุกครั้งที่กด) เมื่อไม่มีบอร์ดคู่ทดสอบต่ออยู่ ผลบนจอจะเป็น FAIL เพราะไม่มีใครตอบ แต่แพ็กเก็ตยังถูกส่งออกจากขา TX ทุกครั้งที่ลองใหม่ (ตัวอย่างส่งCy_SCB_UART_PutArrayBlocking()ก่อนรอคำตอบ และลองซ้ำได้ถึงสี่ครั้ง) - ถอดรหัสด้วยตาก่อน ขยายรูปคลื่นของไบต์แรก ระบุ start bit บิตข้อมูลทั้งแปด และ stop bit แล้วแปลงเป็นเลขฐานสิบหก ต้องได้
A5วัดความกว้างของหนึ่งบิตด้วยเคอร์เซอร์ของโปรแกรม เทียบกับ 8.68 ไมโครวินาที - เปิด decoder แล้วเทียบกับที่ถอดด้วยตา และกับบรรทัด
TX:บนจอ - ทดลองตั้ง decoder ผิดทีละข้อ: baud 57600, parity even, bit order MSB first จดว่า decoder แสดงอะไรในแต่ละกรณี
หลักฐานที่เก็บไว้ใน portfolio: ภาพรูปคลื่นที่ขีดป้ายบิตด้วยมือ ภาพผลของ decoder ที่ตรงกับบรรทัด TX: บนจอ ค่าความกว้างของบิตที่วัดได้
และตารางอาการเมื่อตั้งค่าผิดทั้งสามแบบ
- ตัวอย่าง
08_tacp_host_protocol.cของ SDK อธิบายว่าtacp_init()ล้าง RX FIFO ของฮาร์ดแวร์ ซึ่งถูกต้องตอนเริ่มระบบแต่ผิดถ้าเรียกทีหลัง อ่านหัวข้อ “WHAT tacp_init() COSTS” แล้วอธิบายว่าทำไม - เอกสาร Universal asynchronous receiver-transmitter (Wikipedia) หัวข้อ framing และ break condition
- โจทย์ท้าทาย: ขยายตัวถอดรหัสในแบบฝึกให้อ่านหลายเฟรมต่อกันจากสัญญาณยาวหนึ่งเส้น แล้วถอดแพ็กเก็ต
A5 11 00 B4ทั้งแพ็กเก็ตพร้อมตรวจ XOR checksum
บทถัดไป: บทเรียน 5.2 I2C
สะท้อนคิด
หัวข้อที่มีชื่อว่า “สะท้อนคิด”- ถ้าคุณเห็นแต่ framing error บนสาย คุณจะตรวจอะไรเป็นสามอย่างแรก และเรียงลำดับอย่างไร
- งานไหนในระบบของคุณที่อยาก “แอบอ่าน” UART ของคนอื่น และคุณจะออกแบบให้มันรับข้อมูลจากเจ้าของแทนได้อย่างไร
แหล่งอ้างอิง
หัวข้อที่มีชื่อว่า “แหล่งอ้างอิง”- SDK: cm33/connectivity/08_tacp_host_protocol.c (UART มีเจ้าของเดียว)
- SDK: cycfg_peripherals.c ของ BSP (ค่าตั้งของ debug UART)
- Peripherals at a glance (เอกสาร SDK สร้างจาก commit ef72c1b)
- sigrok PulseView
- Universal asynchronous receiver-transmitter (Wikipedia)
ตัวอย่างบน TESAIoT Developer Hub
หัวข้อที่มีชื่อว่า “ตัวอย่างบน TESAIoT Developer Hub”ลองของจริงบน TESAIoT Dev Kit: เปิดตัวอย่างบน Developer Hub เพื่ออ่านโค้ด ดาวน์โหลด หรือ flash เฟิร์มแวร์สำเร็จรูป
- QWA309 — Header I/O Test — diagnostic: ทดสอบ Arduino header I/O ครบ (I2C 3V3, UART SCB9, SPI bit-bang, GPIO P13, PWM, ADC net, 4000T EZI2C) พร้อม console UI
คำถามทบทวน
ลองตอบเองก่อน แล้วค่อยเปิดดูเฉลย
-
เฟรม 8N1 หนึ่งเฟรมมีระดับตามลำดับเวลาเป็น 0 1 1 0 0 0 0 0 0 1 (บิตแรกคือ start บิตสุดท้ายคือ stop) ไบต์ข้อมูลคืออะไร (เป้าหมายข้อ 1)
- 0x60
- 0x03
- 0xC0
- 0x06
ดูเฉลย
คำตอบ: B. 0x03
ตัด start กับ stop ออก เหลือ 1 1 0 0 0 0 0 0 ซึ่งเป็น D0 ถึง D7 เพราะ UART ส่ง LSB ก่อน D0 = 1 และ D1 = 1 จึงได้ 0x03 ถ้าอ่านแบบ MSB ก่อนจะได้ 0xC0 ซึ่งผิด
-
decoder ขึ้น framing error เกือบทุกเฟรม สาเหตุที่น่าสงสัยก่อนสิ่งอื่นคืออะไร (เป้าหมายข้อ 1)
- parity ผิด
- baud rate ของผู้ส่งกับผู้รับ (หรือ decoder) ไม่ตรงกัน ทำให้ตำแหน่งของ stop bit คลาด
- ข้อมูลมีค่า 0xFF มากเกินไป
- สายยาวเกินไปเพียงอย่างเดียว
ดูเฉลย
คำตอบ: B. baud rate ของผู้ส่งกับผู้รับ (หรือ decoder) ไม่ตรงกัน ทำให้ตำแหน่งของ stop bit คลาด
framing error คือผู้รับเห็น 0 ตรงตำแหน่งที่ควรเป็น stop bit ซึ่งเกิดบ่อยที่สุดเมื่อ baud ไม่ตรงกัน parity ผิดจะขึ้น parity error ต่างหาก และ 0xFF เป็นข้อมูลปกติ
-
task A กับ task B ต่างเรียกฟังก์ชันอ่าน UART พอร์ตเดียวกันวนไปเรื่อย ๆ จะเกิดอะไร (เป้าหมายข้อ 2)
- ทั้งสองได้ข้อมูลครบเหมือนกัน
- ไบต์ถูกแบ่งไปคนละ task ไม่มีใครได้ข้อความครบ เช่น magic byte ไปที่ A แต่ command byte ไปที่ B
- UART จะส่งข้อมูลซ้ำให้ task ที่สอง
- ปลอดภัยถ้าใช้ volatile
ดูเฉลย
คำตอบ: B. ไบต์ถูกแบ่งไปคนละ task ไม่มีใครได้ข้อความครบ เช่น magic byte ไปที่ A แต่ command byte ไปที่ B
แต่ละไบต์ถูกอ่านได้ครั้งเดียว ตัวอย่าง TACP ของ SDK จึงตั้งกติกา ONE OWNER. EXACTLY ONE. แล้วส่งต่อข้อมูลให้งานอื่นผ่าน ring buffer
-
ทำไมตัวรันตัวอย่างของ SDK ที่ใช้ printf จึงตั้งไว้ที่ priority ต่ำที่สุดเหนือ idle (เป้าหมายข้อ 2)
- เพราะ printf ช้า
- เพราะ printf ถือ mutex ที่ไม่มี priority inheritance task ที่ต่ำที่สุดจึงเป็นได้แค่ฝ่ายรอ ไม่มีวันถือ mutex ขวาง task ที่สำคัญกว่า
- เพราะ UART ทำงานได้เฉพาะ priority 1
- เพราะต้องการให้ log ออกช้าที่สุด
ดูเฉลย
คำตอบ: B. เพราะ printf ถือ mutex ที่ไม่มี priority inheritance task ที่ต่ำที่สุดจึงเป็นได้แค่ฝ่ายรอ ไม่มีวันถือ mutex ขวาง task ที่สำคัญกว่า
ผู้ส่งบนพอร์ตเดียวกันก็ต้องมีวินัย คอมเมนต์ใน sdk_examples_cm33.c อธิบายว่า mutex ของ retarget-io ไม่มี priority inheritance จึงเสี่ยง priority inversion ถ้า task ต่ำถือมันไว้
-
ต้องถอดรหัส UART บนขา P15.1 ของ header ด้วย logic analyzer ค่าตั้งใดถูกต้อง (เลือกได้หลายข้อ) (เป้าหมายข้อ 3)
- baud 115200, data 8, parity none, stop 1
- bit order LSB first
- ต่อกราวด์ของ analyzer เข้ากับกราวด์ของบอร์ด
- อัตราสุ่มเท่ากับ baud พอดี คือ 115200 ครั้งต่อวินาที
- ตั้ง trigger ที่ขอบขาลงของสาย TX
ดูเฉลย
คำตอบ: A. baud 115200, data 8, parity none, stop 1 · B. bit order LSB first · C. ต่อกราวด์ของ analyzer เข้ากับกราวด์ของบอร์ด · E. ตั้ง trigger ที่ขอบขาลงของสาย TX
UART บน header ของบอร์ดนี้เป็น 115200 8N1 ส่ง LSB ก่อน สายว่างอยู่ที่ 1 และ start bit เป็นขอบขาลง อัตราสุ่มต้องสูงกว่า baud หลายเท่าจึงจะหากลางบิตได้ และต้องมีกราวด์ร่วม
อ้างอิงบทเรียนนี้
ถ้านำบทเรียนนี้ไปสอน ทำสไลด์ หรือทำเอกสารต่อ ให้อ้างอิงด้วยข้อความนี้ ถ้าดัดแปลงเนื้อหา ให้เติม (ดัดแปลง)ต่อท้ายชื่อบทเรียน
"UART" จาก TESA Open Knowledge โดยสมาคมสมองกลฝังตัวไทย (Thai Embedded Systems Association: TESA) https://github.com/tesaiot/tesa-qualification-program สัญญาอนุญาต CC BY-NC 4.0
ข้อความอ้างอิงภาษาอังกฤษ: "UART" 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://tesaiot.github.io/tesa-qualification-program/courses/embedded-c-foundations/m05-serial-buses/l01-uart/
บทเรียนนี้ดัดแปลงจากต้นฉบับด้านล่าง เมื่ออ้างอิงให้คงเครดิตต้นฉบับไว้ด้วย
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