PSoC5LP RTOS
Lab 14 — UART Command Interface

FreeRTOS on PSoC 5LP · Lab Series Platform: CY8CKIT-059 (PSoC 5LP, Cortex-M3) · PSoC Creator 4.4 · FreeRTOS Kernel V11 Pipeline: UART RX ISR → Stream Buffer → Reader task → Queue → Handler task Pairs with: Chapter 11 — Stream & Message Buffers & Chapter 8 — Queues


1. Introduction & Objectives

A command line over UART is the embedded "hello world" of interactive firmware — and it is the perfect place to combine three things you already know into one clean pipeline. A tiny RX ISR pushes raw bytes into a stream buffer; a Reader task turns that byte stream into whole lines and parses each into a command; a queue hands each command to a Handler task that acts. Reception and action live in separate tasks, so slow work never drops an incoming byte.

After this lab you can:

  • Use a stream buffer for a variable-length byte stream from an ISR (xStreamBufferSendFromISR, xStreamBufferReceive).
  • Explain the stream buffer trigger level and when the reader unblocks.
  • Choose a stream buffer for bytes vs a queue for discrete parsed commands.
  • Assemble lines, parse a small command set, and dispatch by value through a queue.
  • Keep the RX ISR tiny and on the NVIC 2–7 safe side of the syscall line.
  • (Extension) Use a message buffer when the framing is already known, and read it with xMessageBufferReceive.
  • (Extension) Make a blocking read robust with an overall deadline using vTaskSetTimeOutState / xTaskCheckForTimeOut.

2. Prerequisites

Hardware - CY8CKIT-059 PSoC 5LP Prototyping Kit - RobotSoC Kit: 1× LED; a UART-to-USB path (KitProg UART) and a serial terminal on the PC

Software - PSoC Creator 4.4 with the FreeRTOS V11 BaseProject; add stream_buffer.c to the build

Chapters (sync progress) - Required: Chapter 11 (Stream & Message Buffers) and Chapter 8 (Queues). - Related: Chapter 6/15 (the deferred-interrupt pattern and the NVIC 2–7 safety line, from Lab 12). - Peripheral prerequisite: UART configuration and interrupts are covered in the separate (non-FreeRTOS) Lab 03.

3. Reading Materials / References

  • Chapter 11 slides — Stream & Message Buffers (byte streams, trigger level, single writer/reader).
  • Chapter 8 slides — Queues (by-value items, send/receive, blocking).
  • FreeRTOS API: xStreamBufferCreate, xStreamBufferSendFromISR, xStreamBufferReceive, xQueueSend, xQueueReceive.
  • (Extension) xMessageBufferCreate, xMessageBufferSend, xMessageBufferReceive; vTaskSetTimeOutState, xTaskCheckForTimeOut.

4. Background & Theory of Operation

A stream buffer is for bytes. xStreamBufferCreate(size, triggerLevel) makes a lightweight FIFO optimised for a single writer and a single reader — exactly a UART RX ISR writing and one task reading. Unlike a queue, it carries a variable number of bytes, not fixed-size items, so it fits an arbitrary stream of characters with no framing of its own.

The trigger level. A reader blocked in xStreamBufferReceive(..., blockTime) unblocks when at least triggerLevel bytes are available, or when blockTime expires — whichever comes first. A small trigger level (even 1) gives low latency; a larger one lets you wake only once per burst. The block time ensures a short tail of bytes is still delivered even if it never reaches the trigger.

Why not parse in the ISR? The same rule as Lab 12: the ISR does the minimum — drain the hardware FIFO into the stream buffer and yield. Parsing, line assembly, and the command action all happen in tasks, which may block, validate, and run at chosen priorities. A byte that arrives while the handler is busy simply lands in the stream buffer; nothing is lost.

A queue is for commands. Once the Reader has a complete, validated line, it parses it into a small fixed-size Cmd_t (an op + an argument) and sends that by value through a queue to the Handler. This is the right tool for discrete items: each xQueueReceive delivers exactly one command, and the Handler can be slow (print a report, drive a relay) without ever back-pressuring reception beyond the queue's depth.

Two stages, two tools. Bytes → stream buffer → lines; commands → queue → actions. The shape of the data picks the primitive: a continuous stream on one side, discrete records on the other.

Extension — message buffer: framing for free. A stream buffer carries loose bytes, so the Reader must find the message boundaries itself (scan for '\n'). A message buffer is the stream buffer's sibling that preserves message boundaries: xMessageBufferSend(mb, data, len, …) writes one message, and each xMessageBufferReceive returns exactly one whole message — no scanning, no framing code. It is still single-writer / single-reader, and internally it is a stream buffer that prefixes each message with its length. Use a stream buffer when the data is an unframed character stream you parse yourself (a UART command line); use a message buffer when the producer already knows each message's extent (a fixed record from a sensor task, a packet you assembled elsewhere). For this lab's raw UART the stream buffer is the natural fit; the message buffer earns its place when the sender, not the reader, owns the framing.

Extension — robust reads with a deadline. A single xStreamBufferReceive(…, blockTime) waits up to blockTime per call. But "wait at most 200 ms for a whole line, across however many partial reads it takes" is a different requirement — each partial read must subtract the time already spent. FreeRTOS gives you that with vTaskSetTimeOutState(&state) (snapshot the start) and xTaskCheckForTimeOut(&state, &remaining), which returns pdTRUE once the deadline has passed and otherwise adjusts remaining down for the next blocking call. It is the correct pattern for any "retry a blocking operation but bound the total wait" loop — a line read, a handshake, a sensor that answers in fragments.

5. RTOS Task Architecture

UART COMMAND INTERFACE — BYTES IN, COMMANDS OUT UART RX 'l' 'e' 'd' … RX ISR (NVIC 2–7) read byte(s) from FIFO xStreamBufferSendFromISR() portYIELD_FROM_ISR(w) Stream Buffer raw bytes Reader Task xStreamBufferReceive() assemble line to '\n' parse → Cmd_t Command Queue Cmd_t items Handler Task act: LED, status… byte stream discrete commands Stream buffer vs. Queue — pick by shape Stream buffer: a byte stream, variable length, one writer & one reader. Ideal for raw UART RX. Has a trigger level: the reader wakes when that many bytes are available (or on timeout). Queue: discrete, fixed-size items, by value. Ideal for the parsed Cmd_t handed to the handler. Why two task stages · The ISR stays tiny: it only moves bytes, never parses. · The Reader turns a byte stream into whole lines and validates them — it may block without losing bytes. · The Handler acts on clean commands at its own priority — slow work never stalls reception. Separation = no dropped bytes, testable stages. Build note: add stream_buffer.c and set configSUPPORT_DYNAMIC_ALLOCATION = 1; the RX ISR must sit at NVIC 2–7 to call the *FromISR API.

The RX ISR (NVIC 2–7) only moves bytes into the stream buffer. The Reader task assembles a line up to '\n', parses it into a Cmd_t, and sends it through the command queue. The Handler task receives each command and acts at its own priority. The panels contrast the stream buffer (bytes, variable, trigger level) with the queue (discrete Cmd_t by value), and explain why the two task stages keep reception from ever stalling.

6. The Pipeline Over Time

From keystrokes to action — one line through the pipeline:

From keystrokes to action — "led on\n" through the pipeline The RX ISR only moves bytes; the Reader assembles the line; the Handler acts once, on a clean command RX ISR Stream buf Reader Cmd queue Handler l e d ␣ o n \n receiving + buffering the line … '\n' → line complete Cmd{LED,ON} enqueued LED on + echo "OK" Bytes never wait on the handler: reception and action run in separate tasks, linked by the queue. Conceptual / idealized

The ISR ticks per byte; the Reader buffers until '\n'; then one command is enqueued and the Handler acts once.

The stream-buffer trigger level — the reader wakes only when it is worth it:

Stream-buffer trigger level — the reader wakes only when it is worth it xStreamBufferReceive() unblocks when >= trigger-level bytes are available, or when the block time expires bytes in Reader fill trigger level (e.g. 4 bytes) wake: 4 bytes reached wake: block-time expired (3 bytes) A low trigger level + a block time gives low latency without spinning: wake on enough bytes, or on timeout for a short tail. Conceptual / idealized

The reader unblocks when the fill reaches the trigger level, or when its block time expires for a short tail.

The lesson both diagrams carry: the pipeline decouples rate from work. Bytes arrive at the line's pace into the stream buffer; the Handler runs at its own pace on clean commands; the queue absorbs the difference.

Extension — stream buffer vs message buffer: who draws the frame:

Stream buffer vs. message buffer — who draws the frame Same two messages sent; a stream buffer carries loose bytes, a message buffer preserves each message’s boundary Stream buffer — bytes run together writer buffer reader send "led on\n" then send "pwm 9\n" l e d ␣ o n \n p w m ␣ 9 \n no boundaries stored reader scans byte-by-byte, splits on '\n' itself → "led on" | "pwm 9" Message buffer — each send is one whole message writer [ "led on" ] [ "pwm 9" ] each xMessageBufferReceive() returns one whole message — no scanning. Conceptual / idealized

A stream buffer carries loose bytes and the reader splits on '\n'; a message buffer returns each message whole, so when the sender owns the framing you write no parsing at all.

7. Schematic Circuit & Pin Configuration

  • UART (RX/TX) → the KitProg UART or a USB-UART bridge; RX drives the interrupt (isr_UART_RX).
  • LED → GPIO + 220 Ω → GND, driven by the led on / led off commands.
  • Assign the pins in <project>.cydwr → Pins.

8. PSoC Creator Configuration

  • A UART component with an RX interrupt on "byte received"; a Digital Output Pin for the LED.
  • Set the UART RX interrupt NVIC priority to 3 (within 2–7), or isr_UART_RX_SetPriority(3) at startup (the template does this).
  • FreeRTOSConfig.h (already set): configSUPPORT_DYNAMIC_ALLOCATION = 1, configMAX_SYSCALL_INTERRUPT_PRIORITY = (2<<5), heap_4; add stream_buffer.c to the project.

9. Provided Template

You are given Lab14_Template_main.c:

  • Given: all includes (note #include "FreeRTOS_PSoC.h"), the stream buffer, the command queue, the task handles, a Cmd_t type and a parse_line() sketch, and a complete main() that creates the stream buffer and queue, both tasks, and installs the RX ISR in the safe 2–7 range.
  • You write: UART_RX_ISR() (drain FIFO → stream buffer → yield), vReaderTask() (bytes → line → Cmd_t → queue), and vHandlerTask() (act on each command).
  • Extensions (commented in the template): a xMessageBufferReceive path for Exp04, and a vTaskSetTimeOutState / xTaskCheckForTimeOut deadline loop for Exp05.
  • Do not change the boot sequence in main().
CY_ISR(UART_RX_ISR)
{
    BaseType_t woken = pdFALSE;
    uint8_t b;
    while (UART_GetRxBufferSize() > 0u) {        /* drain the FIFO */
        b = (uint8_t)UART_GetChar();
        xStreamBufferSendFromISR(sbRx, &b, 1u, &woken);
    }
    portYIELD_FROM_ISR(woken);                   /* run the reader now */
}

10. Lab Experiments

Exp01 — Basic: the working command line

Write the ISR, the Reader, and the Handler. From a serial terminal, type led on, led off, status, help. Expected: the LED follows the commands and the Handler echoes a reply per line; characters typed while a reply prints are not lost.

Exp02 — Advanced: trigger level and latency

Set the stream-buffer trigger level to 1, then to a larger value, and observe how the Reader wakes per byte vs per burst. Add a vTaskDelay inside the Handler to simulate slow work and confirm reception continues (bytes queue up, nothing drops) until the queue is full. Expected: you can describe the trigger-level trade-off and show that a slow Handler does not drop incoming bytes.

Exp03 — Challenge: extend the protocol, and back-pressure

Add a command with an argument (e.g. pwm 128) parsed into Cmd_t.arg, and make the Handler reject unknown commands with ?. Then flood commands faster than the Handler drains them and observe the queue filling (xQueueSend returning errQUEUE_FULL if you pass a 0 timeout). Expected: a richer parser, graceful rejection, and a demonstration of queue back-pressure and how you choose the queue depth.

Exp04 — Extension: swap in a message buffer

Replace the raw-byte stream buffer with a message buffer for a path where the sender owns the framing — e.g. have a second task assemble a complete command and xMessageBufferSend it, with the Reader calling xMessageBufferReceive to get each one whole (no '\n' scanning). Compare the Reader code with and without the framing work. Expected: a working message-buffer path and a clear statement of when a message buffer removes parsing that a stream buffer would force on you. (Add stream_buffer.c — it also provides message buffers — and configSUPPORT_DYNAMIC_ALLOCATION = 1.)

Exp05 — Extension: a total deadline across partial reads

Make the Reader wait at most LINE_TIMEOUT_MS for a whole line, not per byte: snapshot with vTaskSetTimeOutState before the first read, and on each xStreamBufferReceive pass the remaining time that xTaskCheckForTimeOut computes; abandon the partial line if the deadline passes. Expected: a line read that cannot hang indefinitely on a half-typed command, and the general pattern for bounding the total wait of a retried blocking call.

11. Testing & Verification

#Pass condition
Exp01 led on/off, status, help work from a terminal; no bytes lost while replying.
Exp02 Trigger-level behaviour described; a slow Handler does not drop received bytes.
Exp03 An argument command parses; unknown → ?; queue-full back-pressure shown and explained.
Exp04 A message-buffer path delivers whole messages with no '\n' scanning; the trade-off is stated.
Exp05 The line read is bounded by a total deadline across partial reads using the timeout-state API.

Submit: your ISR, Reader, and Handler; your trigger-level observation; your extended command + back-pressure note; and (extensions) your message-buffer path and total-deadline read.

12. Q&A & Troubleshooting

  • Nothing received / HardFault on the first byte → the RX ISR is at NVIC 0–1; set it to 2–7 before calling *FromISR.
  • Characters lost at speed → you parsed in the ISR, or the stream buffer is too small; keep the ISR tiny and size SB_SIZE for a full line.
  • Reader never wakes → trigger level higher than the bytes sent with no block-time; lower the trigger or pass a finite block time.
  • Lines never complete → the terminal sends \r only; handle \r, \n, or both as the line end.
  • xQueueSend fails → the queue is full because the Handler is slower than the input; raise CMD_Q_LEN or speed up the Handler.
  • Build error on xStreamBuffer… → stream_buffer.c is not in the project, or configSUPPORT_DYNAMIC_ALLOCATION = 0.

13. Analysis Questions & Lab Report

  1. Why is a stream buffer the right primitive for raw UART RX, and a queue the right one for parsed commands?
  2. What exactly makes xStreamBufferReceive unblock? How do the trigger level and the block time interact?
  3. Why keep line assembly and parsing out of the ISR? What would break if you parsed there?
  4. How does the queue between the Reader and the Handler provide back-pressure, and how do you size its depth?
  5. (Extension) When does a message buffer save you work over a stream buffer, and when is a stream buffer still the right choice?
  6. (Extension) Why can't a single blockTime bound the total wait for a whole line, and how do vTaskSetTimeOutState / xTaskCheckForTimeOut fix it?

Your report should include your three code sections, your trigger-level and back-pressure observations, your extension paths, and answers to the questions.

© 2026 Air Supply Information Center (Air Supply BBS)