Four-phase pattern¶
Every per-assignment checker is organised into four phases. The pattern gives every check function the same skeleton, so a contributor can scan a file and immediately know where to look for "the printf check" or "the post-init register state".
flowchart LR
P1["Phase 1<br>Pre-main zero"] --> P2["Phase 2<br>Post-init compare"]
P2 --> P3["Phase 3<br>ISR + stimulus"]
P3 --> P4["Phase 4<br>Print cadence + format"]
Restore["Snapshot/restore<br>before return"] -.applies to.-> P3
Restore -.applies to.-> P4
classDef p fill:#dcfce7,stroke:#16a34a
classDef r fill:#fef3c7,stroke:#d97706
class P1,P2,P3,P4 p
class Restore r
This page walks src/checks/hw1.cpp phase by phase. Open the file
beside this page — line numbers below match it verbatim.
Phase 1 — pre-main zero state¶
int check_initialization(Validator *val) {
HardwareStateValidator validator;
validator.populate_all_zero(); // (1)
int success = 1;
success &= validator.validate(); // (2)
val->start_main_thread(); // (3)
// ... Phase 2 follows in the same check function ...
}
populate_all_zero()registers a zero-check for every TI register struct known to the auto-generated overloads. The registration is driven bytools/hash_map_setter.pyand lives insrc/checks/stat_checker.cpp::HardwareStateValidator::populate_all_zero().validate()returns true only if every register is still zeroed — catches stubs that silently write during static init.start_main_thread()is a thin wrapper aroundgrader::run_student_init()(see Cooperative driver). It runs the student's init code with thewhile(1)body bound to zero iterations.
Tip
Phase 1 is run once in the first check function (almost always
check_initialization). Subsequent check functions don't need to
repeat it — they can rely on the populate-all-zero invariant
holding before init ran.
Phase 2 — post-init register comparisons¶
After start_main_thread() returns, every peripheral the student's
init code touched has its register state set. Build expected structs
and register comparisons.
// Excerpt from src/checks/hw1.cpp:42-80
{
GpioSetup expected[MAX_GPIO];
for (auto &i: expected) i = {};
expected[31] = {GPIO_MUX_CPU1, 0, GPIO_OUTPUT, GPIO_PUSHPULL};
// ...
expected[4] = {GPIO_MUX_CPU1, 0, GPIO_INPUT, GPIO_PULLUP};
validator.register_comparison("GpioSetup", gpiosSetup, expected);
}
{
CPUTIMER_VARS expected = {};
ConfigCpuTimer(&expected, LAUNCHPAD_CPU_FREQUENCY, 10000);
validator.register_comparison("CpuTimer0", CpuTimer0, expected);
// ...
}
success &= validator.validate();
Which register_* variant to use:
| API | When |
|---|---|
register_comparison(name, actual, expected) |
actual won't change before validate() (most common). |
register_comparison_copy(name, actual, expected) |
actual is mutated between registration and validate() (e.g. GpioDataRegs toggling between ISR ticks). Captures by value. |
register_custom(name, actual, expected, predicate) |
You need a custom predicate — IER bitmask comparison, or UARTPrint toggling. |
mark_as_used(name) |
Opt out of populate-all-zero for a register the student is expected to mutate. |
The full surface is in
include/checks/state_checker.h.
Authoring tip — regex for GPIO setup pairs¶
src/ti_stubs.cpp keeps a gpiosSetup[MAX_GPIO] array that mirrors
every GPIO_SetupPinMux / GPIO_SetupPinOptions pair the student
called. To lift those pairs out of a reference solution into your
expected[] array, run this regex in your editor:
Phase 3 — ISR-driven dynamics + stimulus¶
The ISR runs on the actual target; on the grader, the checker invokes
it directly. To exercise an LED-toggle-when-button-held assertion,
press the button (= drive its GPxDAT bit low), drive the timer ISR
for the spec's specified window, assert the toggle bit is set, and
restore.
// Skeleton lifted from src/checks/hw1.cpp:307-458
int check_timer2(Validator *) {
const Hw1Phase3Snapshot baseline = take_snapshot(); // (1)
auto run_window = [&](const char *label, bool press_pb1, bool press_pb4) {
GpioDataRegs = baseline.data;
CpuTimer2.InterruptCount = 0;
UARTPrint = 0;
clear_all_toggle_regs();
grader::release_button(4); // (2) primer
grader::release_button(7);
cpu_timer2_isr();
clear_all_toggle_regs();
CpuTimer2.InterruptCount = 0;
if (press_pb1) grader::press_button(4);
else grader::release_button(4);
if (press_pb4) grader::press_button(7);
else grader::release_button(7);
for (size_t i = 0; i < ticks_per_100ms + 2; ++i) { // (3)
cpu_timer2_isr();
}
};
run_window("PB1-pressed", true, false);
// (4) Assert the spec-required LED toggles.
success &= report(toggle_bit_set(GpioDataRegs.GPBTOGGLE.all, 61 - 32),
"PB1-pressed: GPIO61 (LED12) toggle bit not set",
"spec Ex.9: 'If PB1 (GPIO4) pressed, toggle LED12 (GPIO61)'");
restore_snapshot(baseline); // (5)
return success;
}
- Snapshot — capture every volatile global the check will mutate.
- Primer — fire one ISR with the buttons released. This latches
prev=1in any edge-detector student code, so a subsequent press produces an actual edge. - Drive — invoke the ISR
ticks_per_100ms + 2times. The+ 2ensures the 100 ms boundary is firmly inside the window even ifperiod_usdoesn't divide 100 000 µs evenly. - Report —
report()is a per-check helper that emits a spec-quality log line on failure. The pattern: bool condition, short label, optional spec-quote hint. - Restore — every snapshot field, in reverse. Subsequent checks in the list must see the same pre-check state.
Which driver to use¶
- ISR-internal state (toggle bit between two register snapshots):
call the ISR directly in a
forloop, as above. Or usegrader::run_isr_for_us(isr, period_us, total_us)for a tick-count derived from synthetic time. - Anything observed via a print (cadence or format), or any flag
set in
main'swhile(1)body that gates a print: usegrader::drive_isr_with_main_pump(isr, period_us, total_ticks). This pairs each ISR call with exactly one main-loop iteration via the cooperative driver, so flag-gated prints fire deterministically.
Phase 4 — print cadence + format¶
Reset the capture, drive the ISR/main pump for a known synthetic window, then assert.
// src/checks/hw1.cpp:461-493 (check_print_cadence)
int check_print_cadence(Validator *) {
const Hw1Phase3Snapshot baseline = take_snapshot();
const uint32_t period_us = static_cast<uint32_t>(CpuTimer2.PeriodInUSec);
grader::release_button(4);
grader::release_button(7);
grader::resetPrintfCapture(); // (1)
UARTPrint = 0;
CpuTimer2.InterruptCount = 0;
const uint64_t total_ticks = 1'000'000ull / period_us;
grader::drive_isr_with_main_pump(cpu_timer2_isr, period_us, total_ticks);
// Spec Ex.5: serial_printf must fire every 250 ms — 4 prints in 1 s.
const bool ok = grader::expect_print_cadence(
grader::SerialPort::SCIA, 4, 0.10, "check_print_cadence"); // (2)
restore_snapshot(baseline);
return ok ? 1 : 0;
}
resetPrintfCapture()clearsg_printfCallsand resets the synthetic clock. Call this before every Phase 3/4 driver loop.expect_print_cadence(port, expected_count, tolerance_pct, name)— tolerance ≤ ±10%.
Format check (HW1 Ex.8):
const grader::PrintfCall *latest =
grader::latestPrintfCall(grader::SerialPort::SCIA);
const bool ex8_ok = grader::expect_format(
latest,
"Timeint = %ld, Time = %.2f sec, Input = %.3f, SatOut = %.2f\r\n",
"check_print_format[Ex.8]");
const bool args_ok = grader::expect_arg_types(
latest,
{grader::ArgType::Int32, grader::ArgType::Float,
grader::ArgType::Float, grader::ArgType::Float},
"check_print_format[Ex.8 arg types]");
The format parser tolerates whitespace and static-text differences but
flags any specifier-vs-required-type mismatch. %d for an int32_t
produces an explicit "TI C2000 16-bit int" warning in the error
message — the spec'd format string is the contract you give your
students.
Why this skeleton, and not something else?¶
- Phase 1 → 2 → 3 → 4 matches the order things happen on the device: power-on → init runs once → ISRs fire → printfs go out.
- Snapshot/restore in Phase 3 and 4 is the discipline that lets
multiple checks coexist in the same
checker()list. Without it, check ordering becomes a hidden coupling. - Tolerance ≤ ±10% is enforceable because the cooperative driver removed the race that the old code masked with ±25–30% slop.