ข้ามไปยังเนื้อหา

UART

เมื่อจบบทเรียนนี้ คุณจะ

  1. ถอดรหัสเฟรม UART หนึ่งเฟรมจากภาพสัญญาณได้ครบ start bit, data, parity และ stop bit
  2. อธิบายว่าทำไม UART หนึ่งพอร์ตควรมีเจ้าของเพียงงานเดียว และส่งต่อข้อมูลผ่านบัฟเฟอร์
  3. ตั้งค่า logic analyzer ให้ถอดรหัส UART ที่ baud rate ที่กำหนดได้

ใช้เวลาประมาณ 70 นาที (แนวคิด 15 · ฝึก 25 · แล็บ 25 · เช็ก 5) แล็บต้องมี logic analyzer ที่รับสัญญาณ 3.3 V ได้ และโปรแกรม PulseView หรือโปรแกรมของผู้ผลิตเครื่องที่ถอดรหัส UART ได้

ทวนจากบทก่อนหน้าสองข้อ

  1. debug UART ของบอร์ดนี้ใช้สัญญาณนาฬิกา 100 MHz ตัวหาร 86 และ oversample 10 ได้ baud จริงเท่าไร และคลาดจาก 115200 กี่เปอร์เซ็นต์ (บทเรียน 4.2)
  2. ring buffer ในบทเรียน 1.3 ให้ผู้เขียนแก้อะไร และผู้อ่านแก้อะไร

เปิด examples/11_uart_frame.c โปรแกรมนี้วาดรูปคลื่นของหนึ่งไบต์บนสาย UART ทายก่อนรัน ว่าไบต์ 0xA5 บิตแรกหลัง start bit เป็น 1 หรือ 0

Terminal window
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 ยาวสิบบิต ไบต์เดียวกันนี้คือไบต์แรกที่คุณจะจับได้จากขาของบอร์ดในแล็บ

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)

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)

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

ลองแก้แล้วทายก่อนรัน

  1. เปลี่ยน baud เป็น 9600 ความยาวของเฟรมเป็นเท่าไร และถ้าต้องส่ง log 200 ไบต์ต่อวินาที 9600 พอไหม
  2. เพิ่ม PARITY_ODD ให้ 0xA5 บิต parity เป็นอะไร
  3. วาด 0xB4 แล้วเทียบกับรูปคลื่นที่คุณจะจับได้ในแล็บ

เปิด practice/11_uart_decode.c ตัวถอดรหัสที่ทำงานแบบเดียวกับ logic analyzer มีช่องให้เติม 3 จุด เป็นบทแรกของโมดูล 5 จึงเว้นว่างน้อย

  1. อ่านบิตข้อมูลที่กลางบิต LSB ก่อน
  2. ตรวจ parity แบบ even และ odd
  3. ตรวจ stop bit แล้วคืน framing error เมื่อผิด
Terminal window
gcc -std=c11 -Wall -Wextra -o uart_decode practice/11_uart_decode.c && ./uart_decode

test มีกรณี 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

  1. เปิดตัวอย่าง QWA309 — Header I/O Test บน Developer Hub แล้ว flash เฟิร์มแวร์สำเร็จรูปของตัวอย่างลงบอร์ด (ตัวอย่างนี้ใช้ master template ของ Developer Hub ไม่ใช่แม่แบบของ SDK ภาพรวมอยู่ใน TESAIoT Firmware Stack บทเรียน 1.1)
  2. ต่อ logic analyzer: ช่องหนึ่งที่ P15.1 (TX ของ UART บน header) และกราวด์ ดูตำแหน่งขาจากแผ่นวงจรหรือเอกสารของบอร์ด ตั้งค่าตามตารางในแนวคิดข้อ 3
  3. เริ่มจับ แล้วกดปุ่ม UART Echo บนจอ จอจะพิมพ์บรรทัด TX: A5 11 00 B4 (เลขตัวที่สามเพิ่มทุกครั้งที่กด) เมื่อไม่มีบอร์ดคู่ทดสอบต่ออยู่ ผลบนจอจะเป็น FAIL เพราะไม่มีใครตอบ แต่แพ็กเก็ตยังถูกส่งออกจากขา TX ทุกครั้งที่ลองใหม่ (ตัวอย่างส่ง Cy_SCB_UART_PutArrayBlocking() ก่อนรอคำตอบ และลองซ้ำได้ถึงสี่ครั้ง)
  4. ถอดรหัสด้วยตาก่อน ขยายรูปคลื่นของไบต์แรก ระบุ start bit บิตข้อมูลทั้งแปด และ stop bit แล้วแปลงเป็นเลขฐานสิบหก ต้องได้ A5 วัดความกว้างของหนึ่งบิตด้วยเคอร์เซอร์ของโปรแกรม เทียบกับ 8.68 ไมโครวินาที
  5. เปิด decoder แล้วเทียบกับที่ถอดด้วยตา และกับบรรทัด TX: บนจอ
  6. ทดลองตั้ง 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 ของคนอื่น และคุณจะออกแบบให้มันรับข้อมูลจากเจ้าของแทนได้อย่างไร

ลองของจริงบน 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

คำถามทบทวน

ลองตอบเองก่อน แล้วค่อยเปิดดูเฉลย

  1. เฟรม 8N1 หนึ่งเฟรมมีระดับตามลำดับเวลาเป็น 0 1 1 0 0 0 0 0 0 1 (บิตแรกคือ start บิตสุดท้ายคือ stop) ไบต์ข้อมูลคืออะไร (เป้าหมายข้อ 1)

    1. 0x60
    2. 0x03
    3. 0xC0
    4. 0x06
    ดูเฉลย

    คำตอบ: B. 0x03

    ตัด start กับ stop ออก เหลือ 1 1 0 0 0 0 0 0 ซึ่งเป็น D0 ถึง D7 เพราะ UART ส่ง LSB ก่อน D0 = 1 และ D1 = 1 จึงได้ 0x03 ถ้าอ่านแบบ MSB ก่อนจะได้ 0xC0 ซึ่งผิด

  2. decoder ขึ้น framing error เกือบทุกเฟรม สาเหตุที่น่าสงสัยก่อนสิ่งอื่นคืออะไร (เป้าหมายข้อ 1)

    1. parity ผิด
    2. baud rate ของผู้ส่งกับผู้รับ (หรือ decoder) ไม่ตรงกัน ทำให้ตำแหน่งของ stop bit คลาด
    3. ข้อมูลมีค่า 0xFF มากเกินไป
    4. สายยาวเกินไปเพียงอย่างเดียว
    ดูเฉลย

    คำตอบ: B. baud rate ของผู้ส่งกับผู้รับ (หรือ decoder) ไม่ตรงกัน ทำให้ตำแหน่งของ stop bit คลาด

    framing error คือผู้รับเห็น 0 ตรงตำแหน่งที่ควรเป็น stop bit ซึ่งเกิดบ่อยที่สุดเมื่อ baud ไม่ตรงกัน parity ผิดจะขึ้น parity error ต่างหาก และ 0xFF เป็นข้อมูลปกติ

  3. task A กับ task B ต่างเรียกฟังก์ชันอ่าน UART พอร์ตเดียวกันวนไปเรื่อย ๆ จะเกิดอะไร (เป้าหมายข้อ 2)

    1. ทั้งสองได้ข้อมูลครบเหมือนกัน
    2. ไบต์ถูกแบ่งไปคนละ task ไม่มีใครได้ข้อความครบ เช่น magic byte ไปที่ A แต่ command byte ไปที่ B
    3. UART จะส่งข้อมูลซ้ำให้ task ที่สอง
    4. ปลอดภัยถ้าใช้ volatile
    ดูเฉลย

    คำตอบ: B. ไบต์ถูกแบ่งไปคนละ task ไม่มีใครได้ข้อความครบ เช่น magic byte ไปที่ A แต่ command byte ไปที่ B

    แต่ละไบต์ถูกอ่านได้ครั้งเดียว ตัวอย่าง TACP ของ SDK จึงตั้งกติกา ONE OWNER. EXACTLY ONE. แล้วส่งต่อข้อมูลให้งานอื่นผ่าน ring buffer

  4. ทำไมตัวรันตัวอย่างของ SDK ที่ใช้ printf จึงตั้งไว้ที่ priority ต่ำที่สุดเหนือ idle (เป้าหมายข้อ 2)

    1. เพราะ printf ช้า
    2. เพราะ printf ถือ mutex ที่ไม่มี priority inheritance task ที่ต่ำที่สุดจึงเป็นได้แค่ฝ่ายรอ ไม่มีวันถือ mutex ขวาง task ที่สำคัญกว่า
    3. เพราะ UART ทำงานได้เฉพาะ priority 1
    4. เพราะต้องการให้ log ออกช้าที่สุด
    ดูเฉลย

    คำตอบ: B. เพราะ printf ถือ mutex ที่ไม่มี priority inheritance task ที่ต่ำที่สุดจึงเป็นได้แค่ฝ่ายรอ ไม่มีวันถือ mutex ขวาง task ที่สำคัญกว่า

    ผู้ส่งบนพอร์ตเดียวกันก็ต้องมีวินัย คอมเมนต์ใน sdk_examples_cm33.c อธิบายว่า mutex ของ retarget-io ไม่มี priority inheritance จึงเสี่ยง priority inversion ถ้า task ต่ำถือมันไว้

  5. ต้องถอดรหัส UART บนขา P15.1 ของ header ด้วย logic analyzer ค่าตั้งใดถูกต้อง (เลือกได้หลายข้อ) (เป้าหมายข้อ 3)

    1. baud 115200, data 8, parity none, stop 1
    2. bit order LSB first
    3. ต่อกราวด์ของ analyzer เข้ากับกราวด์ของบอร์ด
    4. อัตราสุ่มเท่ากับ baud พอดี คือ 115200 ครั้งต่อวินาที
    5. ตั้ง 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 ฉบับเต็ม

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0

เนื้อหาเผยแพร่ภายใต้ CC BY-NC 4.0 นำไปใช้ต่อในงานที่ไม่ใช่เพื่อการค้าได้ โปรดอ้างอิงสมาคมสมองกลฝังตัวไทย (TESA) ทุกครั้ง · วิธีอ้างอิง TESA