UART
Objectives
Section titled “Objectives”By the end of this lesson, you will be able to
- Decode one UART frame from a trace, in full: the start bit, data, parity and stop bit.
- Explain why one UART port should have exactly one owner task, handing data on through a buffer.
- Set up a logic analyzer to decode UART at a given baud rate.
Takes about 70 minutes (concepts 15 · practice 25 · lab 25 · check 5). The lab needs a logic analyzer that can accept 3.3 V signals, and PulseView or a manufacturer’s program that can decode UART.
Before you start
Section titled “Before you start”Two review questions from earlier lessons.
- This board’s debug UART uses a 100 MHz clock, a divider of 86, and 10x oversampling — what is the real baud rate, and how far off is it from 115200, as a percentage (lesson 4.2)?
- In the ring buffer from lesson 1.3, what does the writer change, and what does the reader change?
See it work first
Section titled “See it work first”Open examples/11_uart_frame.c. This program draws the waveform of one byte on a UART wire. Predict before you run it: for the byte 0xA5, is the first bit after the start bit a 1 or a 0?
gcc -std=c11 -Wall -Wextra -o uart_frame examples/11_uart_frame.c./uart_frameThe first bit is 1, because UART sends the least significant bit (LSB) first. 0xA5 is 1010 0101, so on the wire it appears in the order 1 0 1 0 0 1 0 1, reading left to right. At 115200 baud, one bit lasts 8.68 microseconds, and one 8N1 frame is ten bits long. This same byte is the first one you’ll capture from the board’s pins in the lab.
Concepts
Section titled “Concepts”1. One UART frame
Section titled “1. One UART frame”UART has no clock wire. Both sides agree on a speed (the baud rate) beforehand, and use the start bit’s edge as the timing reference point.
idle start D0 D1 D2 D3 D4 D5 D6 D7 [parity] stop idle ‾‾‾‾‾\_____/‾‾‾ ... 8 bits of data, LSB first ... [P] ‾‾‾‾ ‾‾‾‾| Part | Level | Purpose |
|---|---|---|
| idle | 1 | An idle wire is held high |
| start | 0 | A falling edge tells the receiver a frame is starting; the receiver counts time from this edge |
| data | follows the data | 5 to 9 bits, most commonly 8, LSB first |
| parity (if present) | follows the rule | even or odd, making the total count of 1s even or odd; catches only an odd number of flipped bits |
| stop | 1 | 1 or 2 bits; if the receiver sees a 0 here, that’s a framing error, usually meaning the baud rates don’t match |
The debug UART’s settings in the SDK’s BSP match 8N1 in every respect: dataWidth = 8UL, parity = CY_SCB_UART_PARITY_NONE, stopBits = CY_SCB_UART_STOP_BITS_1, enableMsbFirst = false, and oversample = 10 (cycfg_peripherals.c lines 584-608). Oversampling is how many clock beats fall within one bit; the receiver uses it to find the middle of the bit, the point furthest from either edge, which gives it some tolerance for baud rate mismatch. The template opens this port in init_retarget_io() with Cy_SCB_UART_Init() and Cy_SCB_UART_Enable(), then wires it to printf through retarget-io (retarget_io_init.c lines 59-93).
2. One port, one owner
Section titled “2. One port, one owner”UART is a single stream of bytes. If two tasks read the same port, the bytes get split between them, and neither one gets a complete message. The SDK’s 08_tacp_host_protocol.c example (mtb-mpy variant) has a section titled “ONE OWNER. EXACTLY ONE.”, explaining that “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.” Any other task that wants the data receives it from the owner instead, through a buffer — in that file, a ring buffer read with tacp_ring_buf_read(), which returns -1 when empty (lesson 1.3).
The transmit side has an owner too. Retarget-io’s printf holds a mutex while printing, so the SDK’s example runner sets its own task to priority 1, with the reasoning that this mutex has “no priority inheritance” — the lowest-priority task “can only ever be the waiter, never the holder that blocks somebody more important” (sdk_examples_cm33.c) — and printf from an ISR is strictly forbidden. In this template, CM33_NS owns the board’s one console; CM55 has no console at all (lesson 2.1).
3. Reading a UART wire with a logic analyzer
Section titled “3. Reading a UART wire with a logic analyzer”A logic analyzer samples the wire’s level as 0 or 1 many times per bit, and decoding software does the same thing a UART receiver does: find the start edge, then read the middle of each bit. Every setting must match the sender.
| Setting | For the UART on this board’s header |
|---|---|
| Pin to probe | The header’s TX, which is P15.1 (SCB9); always connect the analyzer’s ground to the board’s ground too |
| Voltage level | 3.3 V — check your analyzer can accept this |
| Sample rate | Several times the baud rate, e.g. 1 MHz or higher for 115200; higher gives sharper edges |
| Decoder | UART, baud 115200, 8 data bits, no parity, 1 stop bit, LSB first, not inverted |
| Trigger | A falling edge on TX, to capture the first frame |
This pin and format come from the SDK documentation’s “Peripherals at a glance” table (“QWA309 header UART | P15.0 RX / P15.1 TX | SCB9, 115200 8N1”). If the decoder shows a framing error on every frame, suspect the baud rate first. If it shows bytes that look bit-reversed, suspect the bit order or signal polarity.
Worked example
Section titled “Worked example”examples/11_uart_frame.c runs in three parts.
- Part 1:
uart_encode()produces the levels for the start bit, LSB-first data, parity, and stop. - Part 2:
draw()draws a text waveform labelled S, D0 through D7, P, T, with the timing of each bit and the whole frame. - Part 3: draws
0xA5in 8N1 and 8E1, and0x11(the second byte the Header I/O Test example sends), then calculates throughput.
Try changing things and predicting the result before you run it.
- Change the baud to 9600. How long does a frame become, and if you need to send 200 bytes per second of logging, is 9600 enough?
- Add
PARITY_ODD. What does the parity bit for0xA5become? - Draw
0xB4and compare it with the waveform you’ll capture in the lab.
Practice
Section titled “Practice”Open practice/11_uart_decode.c, a decoder that works the same way a logic analyzer does. There are 3 gaps to fill in — this is module 5’s first lesson, so there are fewer than usual.
- Read the data bits at the middle of each bit, LSB first.
- Check parity, both even and odd.
- Check the stop bit, and return a framing error when it’s wrong.
gcc -std=c11 -Wall -Wextra -o uart_decode practice/11_uart_decode.c && ./uart_decodeThere’s a test case for 0xFF, which is real data, not “no data,” and a case with the stop bit set to 0, simulating a baud mismatch.
Solution
Section titled “Solution”Try it yourself for at least 15 minutes first, then open solution/11_uart_decode.c. The comments in the solution point out a limitation of parity that’s often forgotten: it only catches an odd number of flipped bits. If two bits flip at once, parity still checks out. Data that matters therefore needs a checksum or CRC at the message level too — the Header I/O Test example ends every packet with an XOR checksum, and the SDK’s TACP uses CRC-16.
Check your understanding
Section titled “Check your understanding”Answer the 5 questions in quiz.yaml (shown at the bottom of this page on the website), covering all three objectives. Getting 4 or more right counts as finishing the lesson.
Task: capture a real UART frame from the board’s pins with a logic analyzer, decode it by eye first, then confirm with a decoder.
- Open the QWA309 — Header I/O Test example on the Developer Hub, and flash the example’s prebuilt firmware onto the board. (This example uses the Developer Hub’s master template, not the SDK’s template — an overview is in TESAIoT Firmware Stack, lesson 1.1.)
- Connect the logic analyzer: one channel to P15.1 (the header UART’s TX) and ground. Check the pin location from the board’s silkscreen or documentation. Set it up following the table in concept 3.
- Start capturing, then press UART Echo on the screen. The screen will print a line
TX: A5 11 00 B4(the third number increases every time you press it). With no paired test board connected, the screen shows FAIL because nothing answers — but the packet is still sent out the TX pin every time it retries. (The example callsCy_SCB_UART_PutArrayBlocking()before waiting for a reply, and retries up to four times.) - Decode it by eye first. Zoom into the first byte’s waveform, identify the start bit, all eight data bits, and the stop bit, then convert to hex. You should get
A5. Measure the width of one bit with the program’s cursors, and compare it against 8.68 microseconds. - Turn on the decoder and compare it against what you decoded by eye, and against the
TX:line on the screen. - Try setting the decoder wrong, one setting at a time: baud 57600, parity even, bit order MSB first. Note what the decoder shows in each case.
Evidence to keep in your portfolio: a picture of the waveform with bits labelled by hand, a picture of the decoder’s result matching the TX: line on the screen, the bit width you measured, and a table of symptoms for each of the three wrong settings.
Going further
Section titled “Going further”- The SDK’s
08_tacp_host_protocol.cexample explains thattacp_init()clears the hardware’s RX FIFO, which is correct at system start-up but wrong if called later. Read the “WHAT tacp_init() COSTS” section and explain why. - Universal asynchronous receiver-transmitter (Wikipedia), the framing and break condition sections.
- Challenge: extend the exercise’s decoder to read several frames back to back from one long signal, and decode the whole
A5 11 00 B4packet, including checking the XOR checksum.
Next lesson: lesson 5.2, I2C
Reflect
Section titled “Reflect”- If all you saw on a wire were framing errors, what three things would you check first, and in what order?
- Which job in your system wants to “secretly listen in” on someone else’s UART, and how would you design it to receive data from the owner instead?
References
Section titled “References”- SDK: cm33/connectivity/08_tacp_host_protocol.c (UART has a single owner)
- SDK: the BSP’s cycfg_peripherals.c (the debug UART’s settings)
- Peripherals at a glance (SDK docs built from commit ef72c1b)
- sigrok PulseView
- Universal asynchronous receiver-transmitter (Wikipedia)
Examples on the TESAIoT Developer Hub
Section titled “Examples on the TESAIoT Developer Hub”Try the real thing on the TESAIoT Dev Kit: open an example on the Developer Hub to read the code, download it, or flash a prebuilt firmware image.
- QWA309 — Header I/O Test — diagnostic: tests all Arduino header I/O (I2C 3V3, UART SCB9, bit-banged SPI, GPIO P13, PWM, an ADC net, and 4000T EZI2C), with a console UI.
Review questions
Answer on your own first, then open the answer.
-
An 8N1 frame reads, in time order, 0 1 1 0 0 0 0 0 0 1 (first is start, last is stop). What is the data byte? (Objective 1)
- 0x60
- 0x03
- 0xC0
- 0x06
Show answer
Answer: B. 0x03
ตัด start กับ stop ออก เหลือ 1 1 0 0 0 0 0 0 ซึ่งเป็น D0 ถึง D7 เพราะ UART ส่ง LSB ก่อน D0 = 1 และ D1 = 1 จึงได้ 0x03 ถ้าอ่านแบบ MSB ก่อนจะได้ 0xC0 ซึ่งผิด
-
The decoder reports a framing error on almost every frame. What should you suspect first? (Objective 1)
- parity ผิด
- baud rate ของผู้ส่งกับผู้รับ (หรือ decoder) ไม่ตรงกัน ทำให้ตำแหน่งของ stop bit คลาด
- ข้อมูลมีค่า 0xFF มากเกินไป
- สายยาวเกินไปเพียงอย่างเดียว
Show answer
Answer: B. baud rate ของผู้ส่งกับผู้รับ (หรือ decoder) ไม่ตรงกัน ทำให้ตำแหน่งของ stop bit คลาด
framing error คือผู้รับเห็น 0 ตรงตำแหน่งที่ควรเป็น stop bit ซึ่งเกิดบ่อยที่สุดเมื่อ baud ไม่ตรงกัน parity ผิดจะขึ้น parity error ต่างหาก และ 0xFF เป็นข้อมูลปกติ
-
Tasks A and B both keep calling the read function of the same UART port. What happens? (Objective 2)
- ทั้งสองได้ข้อมูลครบเหมือนกัน
- ไบต์ถูกแบ่งไปคนละ task ไม่มีใครได้ข้อความครบ เช่น magic byte ไปที่ A แต่ command byte ไปที่ B
- UART จะส่งข้อมูลซ้ำให้ task ที่สอง
- ปลอดภัยถ้าใช้ volatile
Show answer
Answer: B. ไบต์ถูกแบ่งไปคนละ task ไม่มีใครได้ข้อความครบ เช่น magic byte ไปที่ A แต่ command byte ไปที่ B
แต่ละไบต์ถูกอ่านได้ครั้งเดียว ตัวอย่าง TACP ของ SDK จึงตั้งกติกา ONE OWNER. EXACTLY ONE. แล้วส่งต่อข้อมูลให้งานอื่นผ่าน ring buffer
-
Why does the SDK example runner, which uses printf, run at the lowest priority above idle? (Objective 2)
- เพราะ printf ช้า
- เพราะ printf ถือ mutex ที่ไม่มี priority inheritance task ที่ต่ำที่สุดจึงเป็นได้แค่ฝ่ายรอ ไม่มีวันถือ mutex ขวาง task ที่สำคัญกว่า
- เพราะ UART ทำงานได้เฉพาะ priority 1
- เพราะต้องการให้ log ออกช้าที่สุด
Show answer
Answer: B. เพราะ printf ถือ mutex ที่ไม่มี priority inheritance task ที่ต่ำที่สุดจึงเป็นได้แค่ฝ่ายรอ ไม่มีวันถือ mutex ขวาง task ที่สำคัญกว่า
ผู้ส่งบนพอร์ตเดียวกันก็ต้องมีวินัย คอมเมนต์ใน sdk_examples_cm33.c อธิบายว่า mutex ของ retarget-io ไม่มี priority inheritance จึงเสี่ยง priority inversion ถ้า task ต่ำถือมันไว้
-
Decoding the header UART on P15.1 with a logic analyzer — which settings are right? (choose all that apply) (Objective 3)
- baud 115200, data 8, parity none, stop 1
- bit order LSB first
- ต่อกราวด์ของ analyzer เข้ากับกราวด์ของบอร์ด
- อัตราสุ่มเท่ากับ baud พอดี คือ 115200 ครั้งต่อวินาที
- ตั้ง trigger ที่ขอบขาลงของสาย TX
Show answer
Answer: 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 หลายเท่าจึงจะหากลางบิตได้ และต้องมีกราวด์ร่วม
Cite this lesson
If you teach from this lesson or reuse it in slides or documents, credit it with the text below. If you changed it, add (adapted) after the title.
"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
Thai attribution: "UART" จาก TESA Open Knowledge โดยสมาคมสมองกลฝังตัวไทย (Thai Embedded Systems Association: TESA) https://github.com/tesaiot/tesa-qualification-program สัญญาอนุญาต CC BY-NC 4.0
Lesson link: https://tesaiot.github.io/tesa-qualification-program/en/courses/embedded-c-foundations/m05-serial-buses/l01-uart/
This lesson adapts the source below; keep its credit too.
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
Content is licensed CC BY-NC 4.0. Reuse it non-commercially and credit the Thai Embedded Systems Association (TESA) every time. · How to cite TESA