Capture pipeline¶
Every serial_printf and UART_printfLine call the student firmware
makes is intercepted at the call site in src/ti_stubs.cpp and
recorded into a global vector of PrintfCall records. Checkers then
match those records against the spec's expected format string and
cadence.
This is the single most valuable piece of grader infrastructure: the
most common student bug is using %d for an int32_t (works on
the grader's 32-bit-int Linux host, breaks silently on the 16-bit-int
TI C2000 target). Rendered-output inspection cannot catch it; format
parsing can.
flowchart LR
Student["Student temp_main / ISR"]
SerialP["serial_printf<br>(int port_no, char *fmt, …)"]
UartLcd["UART_printfLine<br>(line, fmt, …)"]
Stubs["src/ti_stubs.cpp<br>classify port, vsnprintf,<br>parse format string"]
Capture["g_printfCalls<br>std::vector<PrintfCall>"]
Clock["synthetic_clock_now_us()"]
Expect["expect_format /<br>expect_print_cadence /<br>expect_arg_types"]
Checker["hw#123;N#125;.cpp / lab#123;N#125;.cpp"]
Student --> SerialP
Student --> UartLcd
SerialP --> Stubs
UartLcd --> Stubs
Clock -.timestamp.-> Stubs
Stubs --> Capture
Capture --> Expect
Expect --> Checker
classDef src fill:#dbeafe,stroke:#2563eb
classDef stub fill:#fef3c7,stroke:#d97706
classDef chk fill:#dcfce7,stroke:#16a34a
class Student src
class SerialP,UartLcd,Stubs,Capture,Clock stub
class Expect,Checker chk
What gets recorded¶
PrintfCall is defined in
include/checks/printf_capture.h:
struct PrintfCall {
SerialPort port = SerialPort::Unknown;
std::string fmt; // raw format string
ParsedFormat parsed; // parsed specifier sequence
std::string rendered; // post-vsnprintf output
uint64_t synthetic_timestamp_us; // not wall clock
std::size_t sequence_index; // global ordering
uint16_t lcd_line; // UART_LCD only
};
extern std::vector<grader::PrintfCall> g_printfCalls;
SerialPort enumerates SCIA / SCIB / SCIC / SCID / UART_LCD plus an
Unknown fallback for unclassified ports.
Synthetic clock — why no wall time?¶
synthetic_clock_now_us() returns a tick counter that the driver
advances by period_us on every ISR call. Cadence assertions compare
"how many SCIA prints in 1 s of synthetic time" — there is no wall
clock involved, so the assertion is deterministic regardless of
host load or build mode.
| API | Purpose |
|---|---|
grader::synthetic_clock_now_us() |
Current synthetic time. |
grader::synthetic_clock_reset() |
Zero the clock. Called by resetPrintfCapture(). |
grader::synthetic_clock_advance(us) |
Tick forward. Called internally by the driver and run_isr_for_us. |
grader::run_isr_for_us(isr, period, total_us) |
Convenience: advance + invoke total_us / period_us times. |
How to use it in a check¶
// 1. Reset the capture vector and synthetic clock.
grader::resetPrintfCapture();
// 2. Drive 1 s of synthetic time, interleaving one main-loop iteration
// per ISR call. Single-threaded; the main-body branch that gates a
// print on a counter will fire deterministically.
const uint64_t total_ticks = 1'000'000ull / period_us;
grader::drive_isr_with_main_pump(cpu_timer2_isr, period_us, total_ticks);
// 3. Cadence: expect 4 prints (every 250 ms) in that 1 s window, ±10%.
const bool cad_ok = grader::expect_print_cadence(
grader::SerialPort::SCIA, 4, 0.10, "check_print_cadence");
// 4. Format: pull the latest SCIA call and match its format string.
const grader::PrintfCall *latest =
grader::latestPrintfCall(grader::SerialPort::SCIA);
const bool fmt_ok = grader::expect_format(
latest,
"Timeint = %ld, Time = %.2f sec, Input = %.3f, SatOut = %.2f\r\n",
"check_print_format");
This is verbatim from src/checks/hw1.cpp:461-493 — the canonical
print-cadence + format check.
Accessor API¶
From printf_capture.h:
| API | Returns |
|---|---|
grader::resetPrintfCapture() |
Clears the vector and resets the synthetic clock. Call at the top of every Phase 3 / Phase 4 block. |
grader::getPrintfCalls() |
Const ref to the underlying vector. |
grader::getPrintfCallsForPort(port) |
Vector of pointers to calls on that port. |
grader::latestPrintfCall(port) |
Pointer to most recent call on that port, or nullptr. |
grader::printfCallCount(port) |
Count for that port. |
grader::dumpPrintfCalls() |
Debug helper — logs every call to spdlog. |
Format-string parser¶
include/checks/format_parser.h exposes a portable C printf parser:
grader::parse_format(fmt)→ParsedFormat{ specs, errors }.grader::FormatSpec{conversion, length_modifier, precision, width, flags}.grader::ArgType{Int16, Int32, UInt16, UInt32, Float, Double, String, Pointer, Char}.grader::format_specs_equivalent(a, b)— tolerant compare: whitespace and static text differences ignored, but specifier-vs-required-type mismatches flagged.
The parser is what makes expect_format actually catch the
%d-for-int32_t bug instead of silently accepting whatever the host
renders.
What's deferred (slice 1 scope)¶
These exist in the roadmap but are not yet implemented:
- SCI RX simulation (
inject_serial_rx) — checks that consume student-handled input. - SPI TX-side word log (
spibTxLog) — what the student sent. - SWI post events.
- Camera-blob / OptiTrack injection.
- EPWM-triggered ADC SOC plumbing (separate from
inject_adc_result).
Do not assume any of these are present when designing a new check.