diff --git a/CURRICULUM.md b/CURRICULUM.md index f72a3fd..6f6458a 100644 --- a/CURRICULUM.md +++ b/CURRICULUM.md @@ -44,6 +44,11 @@ Flag numbers identify the weak dimension(s): 1=Concept Focus, 2=Starter Calibrat | `sv/counter` | Up-Counter | ✅ | 23/27 ⚠️1,8,9 | `sv/always-ff` | enable/reset counter, cycle-counted stimulus, `@(posedge clk)` in `initial` | | `sv/shift-reg` | Shift Register | 📝 | — | `sv/always-ff` | bit shift, concatenation `{}`, serial-in/serial-out | +### Chapter: Clocking Blocks +| Slug | Title | Status | Score | Prereqs | Teaches | +|---|---|---|---|---|---| +| `sv/clocking-sampler-retention` | Clocking Sampler Retention | ✅ | — | `sv/modules-and-ports` | clocking blocks, input skew, sampled values, explicit `#0` timing | + > Introduces `[*]` bus shift and multi-bit `always_ff`; prerequisite > for pipeline assertions in Part 2. diff --git a/artifacts/tutorial/clocking-sampler-retention/README.md b/artifacts/tutorial/clocking-sampler-retention/README.md index 7f2ccc5..243a395 100644 --- a/artifacts/tutorial/clocking-sampler-retention/README.md +++ b/artifacts/tutorial/clocking-sampler-retention/README.md @@ -6,8 +6,8 @@ Xcelium `refdiff`; the starter's default input skew fails in both engines. ## Native example -The commands use CPUs `0-79`, a 30-second wall guard, and the shared landing -build: +The commands use CPUs `0-79`, a 30-second wall guard, and the current native +Mox build: ```text taskset -c 0-79 timeout --kill-after=3s 30s /var/tmp/thomas-ahle/wt/landing/build-dev-fast/bin/mox-run --single-unit --timescale=1ns/1ns --mode=interpret --max-wall-ms=25000 --top tb src/lessons/sv/clocking-sampler-retention/clocking_sampler.sv @@ -17,17 +17,14 @@ taskset -c 0-79 timeout --kill-after=3s 30s /var/tmp/thomas-ahle/wt/landing/buil ``` The starter prints `FAIL: first sample=aa` in both modes. The solution prints -`PASS: clocking sample retained=bb` in both modes. The available binary reports -Mox `bcbd69b0d63800f2e057a58acf0fc0377db9411f`, which predates the landed -MQ93 test lock but reproduces its behavior. The exact current-main landing -receipt for the upstream MQ93 control is recorded at -`/var/tmp/thomas-ahle/fleet/artifacts/landing2/sched-r5-9529d51aacb/`. +`PASS: clocking sample retained=bb` in both modes. The current native Mox +build used for this receipt is `643f9b3d30259a3fbaae4743cb26903e65fdf110`. The committed source hashes are: ```text -clocking_sampler.sv 75d8287e2293d920c34c1aee73473a7aef293b0b0ddc660bd994e3d8ee06b006 -clocking_sampler.sol.sv e4a21ce79f011dcbd439935ecad93d7cfbcd6bf54dc7acbe7432e923670ec117 +clocking_sampler.sv 25ca50f72941b026d8c96f65890b439a3cf35df69cde8359eed17cc9b9970a4a +clocking_sampler.sol.sv 7231082a4cd4f46b798b65be466590f27927b22a783f50faf67529e068376d96 ``` ## Differential receipts diff --git a/src/lessons/clocking-sampler.test.js b/src/lessons/clocking-sampler.test.js new file mode 100644 index 0000000..cfd6416 --- /dev/null +++ b/src/lessons/clocking-sampler.test.js @@ -0,0 +1,40 @@ +import { describe, expect, it } from 'vitest'; +import { createHash } from 'node:crypto'; +import { readFileSync } from 'node:fs'; +import path from 'node:path'; + +const root = path.resolve(process.cwd()); +const lessonDir = path.join(root, 'src/lessons/sv/clocking-sampler-retention'); +const description = readFileSync(path.join(lessonDir, 'description.html'), 'utf8'); +const starter = readFileSync(path.join(lessonDir, 'clocking_sampler.sv'), 'utf8'); +const solution = readFileSync(path.join(lessonDir, 'clocking_sampler.sol.sv'), 'utf8'); + +describe('clocking sampler retention lesson', () => { + it('teaches standard clocking-block sampling without internal status text', () => { + expect(description).toContain('IEEE 1800-2023 §14.3'); + expect(description).toContain('§14.4'); + expect(description).toContain('§14.13'); + expect(description).toContain('explicit #0'); + expect(description).toContain('sampled values'); + expect(description).not.toMatch(/Mox|WASM|Mox's|commit|receipt|\/var\/tmp|fleet|census/i); + }); + + it('keeps the starter and solution distinct and binds both refdiff receipts', () => { + expect(starter).toContain('input sig;'); + expect(starter).not.toContain('input #0 sig;'); + expect(solution).toContain('input #0 sig;'); + + for (const [file, expectedCategory] of [ + ['starter-refdiff.json', 'both_fail'], + ['solution-refdiff.json', 'both_pass'], + ]) { + const sourceName = file.startsWith('starter') ? 'clocking_sampler.sv' : 'clocking_sampler.sol.sv'; + const source = readFileSync(path.join(lessonDir, sourceName)); + const receipt = JSON.parse( + readFileSync(path.join(root, 'artifacts/tutorial/clocking-sampler-retention', file), 'utf8') + ); + expect(receipt.category).toBe(expectedCategory); + expect(receipt.sha256).toBe(createHash('sha256').update(source).digest('hex')); + } + }); +}); diff --git a/src/lessons/curriculum-integrity.test.js b/src/lessons/curriculum-integrity.test.js index 12fe646..e6fa62a 100644 --- a/src/lessons/curriculum-integrity.test.js +++ b/src/lessons/curriculum-integrity.test.js @@ -10,7 +10,6 @@ const removedStatusLessons = [ 'sv/nested-child-input', 'sv/sequential-udp-init', 'sv/compile-mode-status', - 'sv/clocking-sampler-retention', 'sv/protected-envelope-boundary', 'sv/virtual-provider-closure', 'sv/wide-readmem', diff --git a/src/lessons/index.js b/src/lessons/index.js index 65f1261..2a8d8c8 100644 --- a/src/lessons/index.js +++ b/src/lessons/index.js @@ -78,6 +78,7 @@ export const parts = [ chapters: [ { title: 'Introduction', lessons: [L('sv/welcome'), L('sv/modules-and-ports'), L('sv/data-types'), L('sv/always-comb')] }, { title: 'Sequential Logic', lessons: [L('sv/events'), L('sv/always-ff'), L('sv/counter')] }, + { title: 'Clocking Blocks', lessons: [L('sv/clocking-sampler-retention')] }, { title: 'Data Types', lessons: [L('sv/parameters'), L('sv/packed-structs')] }, { title: 'State Machines', lessons: [L('sv/enums')] }, { title: 'Covergroups', lessons: [L('sv/covergroup-basics'), L('sv/coverpoint-bins'), L('sv/cross-coverage')] }, diff --git a/src/lessons/meta.js b/src/lessons/meta.js index 3e000d4..2987e97 100644 --- a/src/lessons/meta.js +++ b/src/lessons/meta.js @@ -10,6 +10,7 @@ export default { 'sv/events': { title: 'Events', focus: '/src/event_sync.sv', top: 'event_sync' }, 'sv/always-ff': { title: 'Flip-Flops with always_ff', focus: '/src/sram_core.sv' }, 'sv/counter': { title: 'Up-Counter', focus: '/src/counter.sv' }, + 'sv/clocking-sampler-retention': { title: 'Clocking Sampler Retention', focus: '/src/clocking_sampler.sv', top: 'tb' }, 'sv/parameters': { title: 'Parameters', focus: '/src/sram.sv' }, 'sv/packed-structs': { title: 'Packed Structs', focus: '/src/mem_cmd.sv' }, 'sv/interfaces': { title: 'Interfaces', focus: '/src/mem_if.sv' }, diff --git a/src/lessons/receipt-matrix.test.js b/src/lessons/receipt-matrix.test.js index 94d4342..9b58fdd 100644 --- a/src/lessons/receipt-matrix.test.js +++ b/src/lessons/receipt-matrix.test.js @@ -6,6 +6,7 @@ import meta from './meta.js'; const root = path.resolve(process.cwd()); const receiptDir = path.join(root, 'artifacts/tutorial/receipts'); const missingDir = path.join(receiptDir, '20261001-missing'); +const capabilityReceiptSlugs = ['sv/clocking-sampler-retention']; function genericReceiptSlugs() { return readdirSync(receiptDir) @@ -19,6 +20,9 @@ describe('tutorial receipt coverage', () => { const matrix = JSON.parse(readFileSync(path.join(missingDir, 'matrix.json'), 'utf8')); const covered = new Set([ ...genericReceiptSlugs(), + ...capabilityReceiptSlugs.filter((slug) => + existsSync(path.join(root, 'artifacts/tutorial/clocking-sampler-retention/solution-refdiff.json')) + ), ...matrix.entries.map((entry) => entry.slug) ]); expect([...covered].sort()).toEqual(Object.keys(meta).sort()); diff --git a/src/lessons/sv/clocking-sampler-retention/clocking_sampler.sol.sv b/src/lessons/sv/clocking-sampler-retention/clocking_sampler.sol.sv new file mode 100644 index 0000000..4a885b9 --- /dev/null +++ b/src/lessons/sv/clocking-sampler-retention/clocking_sampler.sol.sv @@ -0,0 +1,53 @@ +module tb; + logic clk = 0; + logic [7:0] sig = 8'hAA; + bit reactive_done = 0; + + clocking cb @(posedge clk); + input #0 sig; + endclocking + + always #5 clk = ~clk; + always @(posedge clk) sig = 8'hBB; + reactive_writer writer(.clk(clk)); + + initial begin + @(cb); + if (cb.sig !== 8'hBB) begin + $display("FAIL: first sample=%h", cb.sig); + $finish; + end + wait (reactive_done); + if (cb.sig !== 8'hBB) begin + $display("FAIL: same-slot sample=%h", cb.sig); + $finish; + end + #1; + if (cb.sig !== 8'hBB) begin + $display("FAIL: retained sample=%h", cb.sig); + $finish; + end + #5; + if (cb.sig !== 8'hBB) begin + $display("FAIL: negedge sample=%h", cb.sig); + $finish; + end + @(cb); + if (cb.sig !== 8'hBB) begin + $display("FAIL: resample=%h", cb.sig); + $finish; + end + $display("PASS: clocking sample retained=%h", cb.sig); + $finish; + end +endmodule + +program reactive_writer(input logic clk); + initial begin + @(posedge clk); + tb.sig = 8'hCC; + #0; + tb.reactive_done = 1; + #100; + end +endprogram diff --git a/src/lessons/sv/clocking-sampler-retention/clocking_sampler.sv b/src/lessons/sv/clocking-sampler-retention/clocking_sampler.sv new file mode 100644 index 0000000..b6b0532 --- /dev/null +++ b/src/lessons/sv/clocking-sampler-retention/clocking_sampler.sv @@ -0,0 +1,53 @@ +module tb; + logic clk = 0; + logic [7:0] sig = 8'hAA; + bit reactive_done = 0; + + clocking cb @(posedge clk); + input sig; + endclocking + + always #5 clk = ~clk; + always @(posedge clk) sig = 8'hBB; + reactive_writer writer(.clk(clk)); + + initial begin + @(cb); + if (cb.sig !== 8'hBB) begin + $display("FAIL: first sample=%h", cb.sig); + $finish; + end + wait (reactive_done); + if (cb.sig !== 8'hBB) begin + $display("FAIL: same-slot sample=%h", cb.sig); + $finish; + end + #1; + if (cb.sig !== 8'hBB) begin + $display("FAIL: retained sample=%h", cb.sig); + $finish; + end + #5; + if (cb.sig !== 8'hBB) begin + $display("FAIL: negedge sample=%h", cb.sig); + $finish; + end + @(cb); + if (cb.sig !== 8'hBB) begin + $display("FAIL: resample=%h", cb.sig); + $finish; + end + $display("PASS: clocking sample retained=%h", cb.sig); + $finish; + end +endmodule + +program reactive_writer(input logic clk); + initial begin + @(posedge clk); + tb.sig = 8'hCC; + #0; + tb.reactive_done = 1; + #100; + end +endprogram diff --git a/src/lessons/sv/clocking-sampler-retention/description.html b/src/lessons/sv/clocking-sampler-retention/description.html new file mode 100644 index 0000000..c879ca3 --- /dev/null +++ b/src/lessons/sv/clocking-sampler-retention/description.html @@ -0,0 +1,14 @@ +

A clocking block gives a testbench a named clock event and a sampled view of its input signals. The input skew controls when each signal is sampled relative to that event.

+ +

Exercise

+

Add an explicit #0 input skew to cb.sig. The testbench writes BB in an Active-region process, then writes CC after the clocking event has published its sample. The correct solution observes BB at the first event, keeps that sampled value through the same time slot and the following negedge, and samples BB again at the next positive edge.

+ +
clocking cb @(posedge clk);
+  input #0 sig;
+endclocking
+ +

Without an explicit input skew, the default is 1step, so the input is sampled immediately before the corresponding clock edge. With an explicit #0, the input is sampled in the Observed region. The clocking-block event is triggered after the sampled values are updated, and a clocking input keeps the value from its most recent clocking event.

+ +

IEEE 1800-2023 §14.3 defines the default 1step input skew. §14.4 defines sampling relative to the clock event and specifies that an explicit #0 input skew samples in the Observed region. §14.13 specifies that the updated sampled values are available before the clocking-block event triggers and that a #0 input is not resampled during same-slot Active re-entry unless a new clocking event occurs.

+ +

Open clocking_sampler.sv and add the skew. The passing solution prints PASS: clocking sample retained=bb.