The design discussion with Claude that produced the documents here, kept because the reasons for a decision matter as much as the decision. Tool calls, file edits and command output are left out; what remains is what was asked and what was answered. Session 5642a97f-3659-4076-aaf6-3155d3f75806.
I need help building a CPU. I want to use a Raspberry Pi Pico 2 for the RAM and clock. The clock speed should be variable down to 1Hz or less, preferably 0.01Hz. It will use discrete components, no ICs. There will be a LED to show the state of nearly all transistors. It's educational.
I'll start by checking what's already in the project directory, then lay out a design proposal.
There's a substantial existing project here. Let me read the notes and the pico, emu, and kicad directories before proposing anything.
there is a lot od old work here.
Yes, there's a lot of history. I read only enough to anchor the design: the ISA in the emulator, the LED-loaded memory cell, and the NOR chain timings. Here is a proposal built on those.
Keep the existing ISA. The 16-bit instruction format in the emulator (op or condition, two sources with indirect bits, one destination with an indirect bit, registers P A B C and constants 0 1 2 3) already has the Forth system and assembler behind it. The hardware is a register file, two operand latches, an ALU, an output latch and a phase sequencer. Only memory and clock move to the Pico.
Pico interface: one 16-bit wired-AND bus plus four control lines. The Pico 2 has 26 GPIO, so separate address and data buses won't fit. Instead every bus driver on the CPU side is an open-collector pull-down with a pull-up to 3.3V, and the Pico drives a line low by switching the pin to output-low and releases it by switching to input. The protocol is two phases per memory access. In the address phase the CPU puts a register on the bus and the Pico samples it. In the data phase the CPU releases the bus and the Pico drives the word, or for a write the CPU drives the result and the Pico stores it. Pin budget:
| Signal | Direction | Pins |
|---|---|---|
| BUS0..15 | both | 16 |
| CLK, RESET | Pico to CPU | 2 |
| MEMREQ, WE | CPU to Pico | 2 |
| USB serial to host | none | 0 |
Clock and sequencing. The Pico outputs a single clock. A ring counter of about 13 flip-flops on the CPU sequences the phases, and the Pico mirrors the phase count since it generated every edge. That keeps the Pico's fan-out to one line and makes the sequencer itself a row of LEDs that walks along at any speed. The phases per instruction are: fetch address, fetch data, P plus one via the ALU in two phases, operand 0 address and data, operand 1 address and data, execute and latch flags, writeback address and data, and a conditional second P plus one when the instruction read through P. At 0.01 Hz one instruction takes about 22 minutes, which only makes sense for walking through phases one at a time, and the firmware should also offer single-step by phase and by instruction.
Everything must be static. The 2T1C DRAM idea in your notes is dead at these speeds. The cross-coupled cell with the LED as collector load is the right building block. Speed-up capacitors are fine since they only help edges.
Two electrical points I'd change from the old 3.3V work. Run the CPU logic at 5V, not 3.3V. Your January 2017 note that LEDs in the NOR chain sat too close to the switching point is the LED forward drop eating the headroom; at 5V a collector with an LED load still reaches about 2.5V, enough for the next base through 10k. Keep only the bus pull-ups at 3.3V, so the Pico never sees 5V and the open-collector drivers don't care. Second, budget the power: around 2000 LEDs at even 2 mA is 4 A, so plan a supply and choose high-efficiency LEDs with larger series resistors.
Rough transistor count. Register file about 500, instruction register and operand and output latches about 250, ALU about 400, sequencer and decode about 250. Roughly 1500 transistors and as many LEDs.
Firmware. C with the Pico SDK rather than MicroPython. A loop toggles the clock from a timer, samples or drives the bus per phase, holds the 64K-word image in SRAM, and handles a write to 0xFFFF as the OS call over USB serial. The host side can load binaries from your existing assembler and txt2bin flow.
Three decisions change the design materially, so they're yours:
Once you pick, I can produce the phase-by-phase control table, the per-bit cell schematics for the register file and ALU, and the Pico firmware.
1 maybe both. we need two layers of clock. to explain how an instruction executes, including incrementing P, every part of that must be stepped through at slow speed - so that's single stepping at every ring counter increment. Then there is single stepping at the instruction level.
2 we need to see how the transitor count comes out - which in turn depends on the SRAM/DRAM cell design. if we can get it down ot about 500 then use the ALU, if it remains 1500 then a separate increment.
3 you have registers P A B C and constants 0 1 2 3 - that's the latest ISA, ignore earlier ones. I need a carry bit, or some way to do operations on more than 16 bits. Conditional instructions can change, branch and jump are possible, I once thought that having all instructions conditional would simplify the design. Simplicity is important, transistor count is important, execution speed is not very important.
transistor count is highly dependent on the memory cell. 2T1C doesn't really have a capacitor, it uses the internal capacitance of a FET. The measured storage times were several seconds, but this was a long time ago. 10s is perhaps acceptable.
Good answers. Before I size the ISA options I want real numbers on how the Forth system uses the instruction fields, since that decides whether a 2-bit condition field on every instruction fits in 16 bits. Let me run your existing toolchain and count.
The Forth system assembles to 1550 instructions. Field usage:
| Field | Usage |
|---|---|
| ops | ADD 1074, SUB 293, ADC 83, LSR 40, AND 30, SBC 25, XOR 3, OR 2 |
| conditions | always 1458, lt0 42, ne0 25, eq0 25 |
| both sources indirect | 160 |
| destination 0..3 as scratch memory | 224 |
| destination discarded, compare only | 102 |
Memory cell and transistor count
The 2T1C cell is out for this build, and it's the clock speed that kills it rather than the count. At 0.01 Hz one ring-counter phase lasts 100 s and one instruction about 22 minutes. A student can also pause at any phase for as long as they like, so every stored bit must survive an unbounded pause. Ten seconds of retention cannot meet that, and only the P A B C registers could be refreshed from the Pico over the bus anyway. The instruction register, operand latches, flags and ring counter have no refresh path without adding the transistors you were trying to save. The one DRAM variant that could work is your 100 nF measurement from 2019, which you noted as roughly a day. That saves about 110 transistors across the machine and costs 112 capacitors, a temperature dependence, and a write path through a reverse-biased BJT. I don't think it's worth it.
The static cell I propose is 4 transistors per register bit, not the usual 5 or 6:
Latches that never drive the bus need 3 per bit. The count comes out like this:
| Block | Transistors |
|---|---|
| P A B C, 4 per bit plus 3 line drivers each | 268 |
| IR, IN0, IN1 at 3 per bit plus drivers | 150 |
| flags, 13-stage ring counter, clock buffers | 70 |
| ALU: adder, B invert, AND, shift, result drivers | 130 to 224 |
| decode, exec gating, MEMREQ | 60 |
| bus indicator LEDs, one transistor per line | 16 |
| Total | 700 to 790 |
The ALU range depends on the adder. A resistor-threshold adder of the kind your analogMajority.py explores is about 4 transistors per bit, a plain DTL adder about 8. Both are in play.
P increment. At roughly 750 the count sits between your two thresholds. I recommend the ALU path. An incrementer with its own write path is about 100 transistors, 13 percent more hardware, to save two phases per instruction you've said you don't care about. Routing P through the ALU also teaches that P is just a register.
ISA. All-conditional doesn't fit. Two condition bits plus three op bits plus the twelve operand bits is 17, and the counts above show every operand bit is used heavily. The hardware cost of conditional execution is about six transistors either way, so the current encoding loses nothing. What I'd change: drop XOR and OR, keep ADC and SBC for your carry, and put ROR through carry in a spare slot so multi-word shifts work too. ROR is wiring plus two transistors. The emulator and assembler change by a few lines.
Two layers of stepping. The Pico owns all timing with four outputs: a clock that advances the ring counter, a clear strobe, a set strobe, and reset. It reads back MEMREQ and the ring's phase-0 line. One phase is: pulse clock, drive or sample the bus, pulse clear, pulse set. Step-phase runs one of those and stops. Step-instruction runs until phase 0 comes back. Continuous mode stretches the gap between phases to anything from microseconds to minutes. The ring counter stays in transistors, and nothing inside it needs to generate its own edges, which is what makes it safe at any speed.
Two things I want from you before I write the per-bit schematics and the control table. Confirm 5V for the logic with 3.3V bus pull-ups, as I argued last time. And confirm the parts base is NPN only, since that's what sets the gate style.
the name changed from nanocpu to blinkingcomputer a long time ago. http://blinkingcomputer.org/ exists, you can write it at /net/www/etc/apache2/sites-available/blinkingcomputer.org.conf and /net/www/var/lib/dokuwiki/farm/blinkingcomputer.org/
Let me look at the existing site so the design matches what's published there.
The wiki pages are readable with sudo. The home page describes an 8-bit, register-less design that differs from the emulator's ISA, so I need to read all three pages in full before going further.
Noted, Blinking Computer it is. I read all three wiki pages with sudo, since the data directory is www-data only. The DokuWiki change log shows the home page was last edited in March 2023, before the August 2023 Forth and emulator work in the repo.
That matters because the wiki describes a different machine from the one we've been designing:
cnd Mi = Mj OP Mk with 6-bit addresses.I'm treating that page as superseded by what you told me today: 16-bit, registers P A B C plus constants 0 to 3, Pico as RAM and clock. The Pico at a few pounds removes the cost objection the wiki raised, so going back to external memory is consistent. Two things from the 2023 plan still look worth keeping in the new design. Control by address is already there as the write to 0xFFFF, and the Pico can grow that into OUT, FREQ and HCF cheaply. The twelve-phase visible clock is essentially the ring counter we discussed.
Three things I need from you:
most of the wiki is wrong.
./utest.sh is useful, it shows that something sitll works
The two earlier questions still stand: 5V logic with 3.3V bus pull-ups, That's perfectly acceptable. 12V or larger is also possible, there's a design somewhere for a very fast adder that just uses diodes, that needed higher voltage.
NPN-only as the parts base. PNP is possible if that would help. But TRL is the main design, not TTL as TTL doubles the transistor count.
Understood. I'll stop referring to the wiki and proceed on today's statements. I have enough to write the design down properly, so I'm doing that now rather than asking more questions. Assumptions I'm building on, flag any that are wrong: 5V logic with 3.3V bus pull-ups, NPN only, and nothing gets published to the wiki until you've reviewed it.
Thanks, both noted. Let me look at utest.sh and run it to get a baseline before any emulator changes.
The design is written up at /srv/nvme/blinking/design/blinking-computer-2026.txt in DokuWiki markup, ready to publish once you've been through it. It is a draft for you to tear apart, not a finished spec.
What it commits to:
Transistor budget comes to about 780, with about 740 LEDs, so around 2 A at 5V worst case.
The firmware section includes a feature I think you'll want: the Pico runs your emulator alongside the hardware and compares every address and data word it sees, reporting the first mismatch with the phase number. That makes the Pico the logic analyser for the whole build.
Open items are listed at the end of the document: whether the blue LED stock can be used anywhere, whether to go to 12V, and the physical form.
Next steps once you've reviewed it: I can make the emulator and assembler change and prove it with utest.sh, and write the Pico firmware against the phase table so it's ready for the first breadboard register bit.
Confirm the wiki plan is dead Confirmed. the old wiki pages are now at oldWiki so you can write - but you have to perserve ownership www-data or dockuwiki breaks
we can also abandon dokuwiki
Let me look at what the wiki pages directory looks like now.
I'd take you up on abandoning DokuWiki. Your other sites on this box are already plain HTML under the Apache web root, the DokuWiki farm needs sudo and www-data ownership for every edit, and the design document is easier to keep in the repo next to the emulator and firmware. I've prepared the static route without touching the live site:
/srv/nvme/blinking/design/blinking-computer-2026.md. The DokuWiki version is gone so there is one source./srv/nvme/blinking/design/build.py renders every Markdown file in that directory to standalone HTML with a minimal stylesheet, light and dark. It uses the python-markdown module already installed here./tmp/claude-1000/-srv-nvme-blinking/5642a97f-3659-4076-aaf6-3155d3f75806/scratchpad/site/blinking-computer-2026.html. Tables, headings and the numbered build order all come through.Going live would be three steps, which I'll do on your word since they change the public site:
/net/www/var/www/html/blinkingcomputer.org/ and build into it, with an index page that links the design and says the older plan is retired./wiki alias so the old DokuWiki pages stay reachable.One thing to flag: the pages directory still only holds home, dippy and weird_memory_cells, so I don't see the oldWiki move yet. If you'd rather keep DokuWiki after all, I can publish the Markdown there instead, converted back and written with www-data ownership.
It's educational, so as many instructions as we can fit in provided they are cheap on the transistors.
We also need a high level language - that's complelte open as of now. the cut-down forth seems to work well as does mixing forth and assember. 3rd is supposed to represetn a cut down forth.
Two things to fold in: an ISA that fills all 16 opcode slots where the transistors are cheap, and the language question. Let me check what the SIMPL compiler currently does before I recommend anything on the language.
Create /net/www/var/www/html/blinkingcomputer.org/ and build into it, with an index page that links the design and says the older plan is retired. Sure - but no need to represent the past, it's changed over 40-50 years, it only needs to document the present really
Approved, so I'll go live with the static site. First a check on where Apache actually runs, since /net/www looks like a mount of another host.
The static site is in place and the vhost is switched, but Apache has to be reloaded on the web server host and I can't do that from here. There's no ssh key for www from this machine. Run this once and the new site goes live:
ssh www sudo systemctl reload apache2
What's done on the site:
/net/www/var/www/html/blinkingcomputer.org/, root-owned and world-readable like your other static sites. The index describes the present machine only and links the design document. No history./srv/nvme/blinking/design/, with build.py to render it. Deploying is build, copy, reload.On the instruction set, I've rewritten that section of the design to your rule of "as many as fit, if cheap". All 16 slots are now used:
| Slot | Instruction | Extra transistors |
|---|---|---|
| 0000 to 0101 | AND, LSR, ADC, SBC, XOR, OR as now | about 17 each for the logic ops |
| 0110 | ROR, rotate right through carry | 3 |
| 0111 | ASR, arithmetic shift right | 2 |
| 1ccc | conditional ADD and SUB as now |
AND, OR and XOR each cost one open-collector driver per bit because the complements they need already fall out of the latches and the adder. The three right shifts share one set of drivers and differ only in what feeds bit 15. All right shifts now write bit 0 into the carry, so multi-word shifts and low-bit tests are free. The ALU goes to about 200 transistors and the machine to about 800. Bitwise NOT would cost 17 and is the first candidate if a slot is ever freed; until then it's two instructions.
On the language, my recommendation is to keep two layers and make both visible to students. The Forth layer stays as the system language, since it already runs with 247 tests and shows threaded code moving through the machine on the LEDs. SIMPL becomes the teaching language on top, compiling to Forth words, because its Python-like syntax is what a school class will actually write. Your simpl.py already does most of this, but it needs the lark module, which isn't installed here, so I couldn't run it. Before I go further on the language I'd like to know whether SIMPL is the direction you want, or whether you'd rather I look at something else entirely.
Once Apache is reloaded, the next thing I'd do is the emulator and assembler change for ROR, ASR and the carry on shifts, proven with utest.sh.
That makes the Pico the logic analyser for the whole build. yes. it's educational, it's supposed to be free for schools to build. I will make mistakes, having the inbuild analysis and stop on error will help debugging considerably.
Good, I'll make that a first-class feature rather than a footnote. I'll spec it properly in the design document, check whether the site is live yet, and then make the emulator and assembler change so the ISA in the document is the ISA the tools run.
The live site still shows the DokuWiki title, so Apache hasn't been reloaded yet. Now the toolchain change, with dated backups in your usual style.
All green. The emulator and assembler now implement the 16-slot instruction set from the design document, and nothing in the Forth system broke.
emu/emu.c and the two opcode lines in emu/asm.py. The previous versions are saved beside them with a 13Nov26 suffix, following your naming. HCF0 and HCF1 are gone; halting is a write to 0xFFFF as before.The design document now has a full section on the Pico as logic analyser: a table of what is on the bus in each of the 13 phases, what the emulator value to compare against is, and which block is suspect when they differ. It also covers MEMREQ and ring checks, the stop-and-report behaviour with the LEDs frozen at the failing phase, and self-tests that need no program. It's rebuilt and copied to the web directory.
The live site still serves DokuWiki until Apache is reloaded on the web host:
ssh www sudo systemctl reload apache2
Still open from my side: whether SIMPL is the language direction you want on top of the Forth layer. Everything else in the design is now consistent between the document, the emulator and the assembler, so the next piece of real work is the Pico firmware, starting with the bus protocol and the single-register test so it's ready when the first breadboard bit exists.
github.com/drtonyr/pis-third-asm-emu is an older instruction set, quite similar. If I ever build this then it will all go to github.com/drtonyr/blinking or blinkingcomputer
I'll take the old repository link off the index page, since it points at a different instruction set and the new repository doesn't exist yet.
The link is gone. The index now says the language, assembler and emulator exist and will be published alongside the hardware, with no repository named. And the site is live: blinkingcomputer.org now returns the new index, so Apache has been reloaded on the web host.
Both pages are served from /net/www/var/www/html/blinkingcomputer.org/, built from the Markdown in /srv/nvme/blinking/design/. The DokuWiki install is untouched but no longer reachable through the vhost.
When you do create the new repository, the natural contents are the design directory with its build script, the emu directory with the modified emulator and assembler, and the Pico firmware once it exists. The firmware is the next real piece of work, and I'd start it with the bus protocol and the single-register test so it's ready for the first breadboard bit.
Before I go further on the language I’d like to know whether SIMPL is the direction you want, or whether you’d rather I look at something else entirely. Ideally something else entirely. Something standard that can be compiled to the forth/asm level and then run. It's acceptable for the CPU to make 'system' calls, to write to the screen, etc, so some libraries can be hidden and run on the Pico. C would be ideal, it being the first 'portable assembler' and having no large standard library (unlike, say, Python).
Ease of use is important, compile speed isn't, programs can never get very big.
Completely missing is input/output. There are some VGA drivers for Pico2, even HDMI, thta would be nice.
Not documetned is the fact that I'd like this to fit into a 1m by 1m flat picture, so that it can hang on a wall or be easily transported to schools to give demos. Also I want to give https://www.computinghistory.org.uk/ one of them.
Three new requirements: a standard language, I/O, and the physical form. Let me verify two hardware facts before I recommend anything on I/O, since the Pico's pin budget is nearly spent on the CPU interface.
The design document now has sections on the software stack, input and output, and the physical form, and the site is redeployed. The substance:
Language: C, by porting chibicc. It's about nine thousand lines under MIT, covers most of C11, and was written as a teaching compiler with one feature per commit, which fits the project's purpose. Its code generator already evaluates expressions on a stack, so swapping the x86-64 back end for one that emits your Forth words is the natural port, and the front end stays untouched. The machine is word addressed, so char is 16 bits, which C permits. Int and pointers are 16, long is 32 using the U4 and S4 words you already have. Function calls use the Forth call stack. The C library lives mostly on the Pico behind the 0xFFFF system call, so putchar, printf formatting and string functions cost no machine code unless a lesson is about them. I didn't suggest TinyCC even though it's in the repo. Its back ends are far harder to add than chibicc's.
I/O: the Pico 2 can't do it alone. HSTX video on the RP2350 needs GPIO 12 to 19, output only, and the Pico 2 has 26 GPIO of which the CPU interface takes 23. Two options are in the document:
Physical form. One metre square as two hinged halves, so it folds for a car and opens flat on a wall. The layout follows the architecture, with the ring counter across the top, the bus and the four registers as rows of sixteen, then IR, IN0, IN1 and the ALU, with decode and the Pico at the edge. Sixteen identical bit-slice boards of about fifty transistors each, so a school can build one slice at a time and a museum can replace one. I've noted the Centre for Computing History unit as a requirement for replaceable boards and repair-grade documentation.
Two decisions are yours before I act on them: chibicc as the compiler base, and the RP2350B board in place of the Pico 2. With those settled, the firmware and the compiler back end can both start.
Sources: RP2350 HSTX interface overview, Adafruit PiCowBell HSTX DVI, Waveshare RP2350-PiZero wiki, RP2350-PiZero announcement, WeAct RP2350B core board
One RP2350B board instead of the Pico 2 Could be good. Here in the UK it's £15 on Amazon.co.uk - so that is 15% of budget but if it gives more pins out (one of these do), and/or HDMI then that makes it stand-alone, and that's a far more compelling 1950s style demo than one that connects with USB and prints out on another computer.
Agreed, stand-alone is the stronger demo and the extra pins remove the two-Pico complication. I'll record the board choice and add a budget section so the £15 sits in context.
Recorded as a decision in the design, with the reasoning in your words, and the site is updated.
I also added a budget section so the £15 is seen against the whole. Rough UK numbers for one machine:
| Item | About |
|---|---|
| transistors, LEDs, resistors, diodes | £23 |
| RP2350B board with HDMI | £15 |
| 5 V 3 A supply | £8 |
| 17 printed circuit boards | £35 to £50 |
| wire, headers, hinges | £10 |
| total | £90 to £105 |
The boards decide whether it fits under £100, not the Pico. Sixteen identical bit slices keep the price down, and a school with a laminator could build a slice on laminated sheet for almost nothing at the cost of more wiring.
Which RP2350B board to specify is still worth a check against the real pin map before the firmware fixes pin numbers. The Waveshare RP2350-PiZero exposes 28 GPIO on its header, and we need 23 CPU-facing pins plus none for video since HDMI is on the board, so it fits with room to spare. If the £15 board you found is a different one, tell me which and I'll check its header.
The one decision still open on my side is chibicc as the compiler base. Once you're happy with that, the two streams of work are the firmware against the phase table and the Forth-emitting back end.
chibicc looks great
Good. I'll record that, then start the port, since the compiler can be built and proven on the emulator without any hardware.
I wasn't expecting to get a real PCB in budget, but if close then that's great. My soldering is very bad.
it's not real forth, it's minimal, just enough to add some 'instructions' to the assembler.
Now I have what I need to design the back end. Before writing it I need five details from the existing code that affect it, all independent reads:
The compiler segfaults in the cc1 stage. Let me get a backtrace to find where.
The parser wraps every plain declaration in an empty expression statement with no type. I'll treat a missing type as void, rebuild normally and run both tests.
The runaway emulator is stopped and the patches are in. Running both test suites again.
mul2 already exists in the runtime with the same meaning, so I'll drop my duplicate and rerun.
The C compiler is working end to end. A C program now compiles to your threaded-code layer, runs on the emulator, and eight test programs pass, along with all the runtime word tests and your original 247 Forth tests.
What exists now
chibicc/ is a clone of chibicc with a new code generator. Everything in the front end stays except the type sizes. The changes are listed in chibicc/BLINKING.md.emu/cc.sh prog.c compiles, links against sys.4th and the new clib.4th, and runs on the emulator. The intermediate .4th file is readable: a + b compiles to the word +, a C function is one colon definition, and a class can read it.emu/ctest.sh runs every program in emu/ctest/ against an expected-output line in the source. emu/clibtest.sh runs the tests embedded in the runtime words, same scheme as your utest.sh.What the compiler handles so far: int, char, short, unsigned, long as two words, pointers, arrays, structs, globals with initialisers, strings, all control flow including switch and goto, recursion, function pointers, and printf with a literal format expanded at compile time into your output words. Not yet: floats, variadics beyond printf, structs by value, bitfields.
Conventions that matter if you read the output. Everything is evaluated on the data stack. A function's arguments and locals form a frame on that stack reached through a frame pointer cell, with new words enter, leave, fload, fstore and faddr. Names defined in C get a c_ prefix so a program can define add without colliding with your word of that name. A C declaration with no definition names a runtime word directly, which is how putc and putd are called from C.
Three things I learned about the machine along the way, now written into the design and the runtime comments:
_divmod and divmodU2 words give wrong answers when the top bit is set. The C runtime carries its own 16-bit and 32-bit unsigned division. Your Forth code is unaffected since it never hits those cases, but you may want to know.On your two notes: a real PCB is the thing that makes bad soldering survivable, since every part has a labelled hole and nothing is point to point, so the slice boards are worth it even if they push the budget slightly. And yes, I've treated your Forth as what it is, a set of instructions bolted onto the assembler. The C compiler just emits more of them.
The design document and the site are updated with the software state. Next on my list would be the Pico firmware, unless you'd rather I push the compiler further first.
Please push on the compiler
I'll push on four fronts: readable output through peephole simplifications and C source comments in the generated code, a small C library with standard names, an input path, and a wider test suite. Let me check two runtime words I want to build on first.
The python heredoc for the header was missing its terminator, so nothing in that call applied and a few files landed in the wrong directory. Cleaning up and redoing it in separate steps.
So I'm keen to use the character set from the BBC micro. This was eight pixels by eight pixels... I think you can find it online. Also thinking about how a Hdmi - Display is driven, given that we can't use all of the memory for the output. One way of doing this which I quite like is for the display to be 16 bit values each of which just points to a rewritable character. So if we only use the seven bits out of 16, we would have a asky style display. But we could also write custom ones for for any shape you like. Does this fit with the Waveshare board and in general, how does the waveshare board take the memory and map it out to the Hdmi?
Everything is green again: 12 C test programs, the runtime word tests and your 247 Forth tests.
Compiler progress this round
i++ is now fload(1) 1 + fstore(1), total = 0 is ` 0 fstore(0), a global read is load(c_x). The output of a for loop is something a class can follow line by line.puts, strlen, strcpy, strcat, strcmp, memcpy, memset, abs, atoi are written in C in emu/libc.c and compiled with every program, and putchar, getchar, rand and srand map onto your words. getd reads a decimal number from the input, and the test runner can feed a program input. I fixed the emulator's getc, which returned the character in the wrong memory cell.extern is a runtime word and keeps its name; everything else written in C gets a c_ prefix. Labels carry the source file name, so separately compiled files assemble together.The display, in answer to your question
Yes, your model fits the board well, and I've written it into the design. How the board gets from memory to HDMI: the RP2350's HSTX peripheral takes a stream from the Pico's SRAM by DMA, does the TMDS encoding in hardware, and the board routes the eight HSTX pins through series resistors to the mini HDMI socket. It's a DVI signal, which any HDMI monitor accepts, with no audio. The SDK example streams a 640 by 480 frame buffer that way. Nothing forces a frame buffer, though. The Pico's second core can build each scan line just in time from a character map and a glyph table, 80 characters by 8 pixels per line, which is a few hundred lookups per line and easy at 150 MHz. The Adafruit HSTX library already does a text mode on the same principle.
The part I'd add to your idea is that because the Pico is the memory, the character map and the glyph table can be inside the CPU's 64K address space. The design puts the 80 by 30 character map at 0xF000 and 256 glyphs of 8 words each at 0xE800. A C program writes a word and it appears on screen; it writes eight words and it has redefined a character. Low 8 bits select the glyph, so 7-bit ASCII gives text and the upper 128 glyphs are free, with the high bits for inverse video and colour. The system calls keep a cursor so plain printf still works.
On the BBC font: it's the 8 by 8 Acorn MOS font at &C000 in the OS 1.20 ROM, 8 bytes per glyph with the left pixel in the high bit. The data is reproduced in several open projects, including the bbcelite disassembly, but its copyright still belongs to Acorn's successors and I found no licence allowing redistribution. For something schools build and the museum displays, I'd ship a redrawn 8 by 8 font in the same spirit as the default and let a builder load the real BBC glyphs themselves.
Sources: bbcelite on the MOS font, extracting the font from the ROM, bbcfont BDF generator, RP2350 HSTX overview, Adafruit DVI HSTX library with text mode
Next on the compiler I'd add floats using your F3 format, since that's the one remaining gap a school program is likely to hit, unless you'd rather I start the Pico firmware now that the display model is settled.
This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation.
Summary: 1. Primary Request and Intent: - Build an educational CPU ("The Blinking Computer", formerly nanoCPU) from discrete components (no ICs), with a Raspberry Pi Pico 2 (now decided: an RP2350B board with HDMI, e.g. Waveshare RP2350-PiZero, ~£15) providing RAM and clock; clock variable down to 0.01 Hz; an LED on nearly every transistor; two layers of single-stepping (every ring-counter phase, and per instruction); RTL ("TRL"), not TTL; NPN base (PNP allowed); 5 V logic with 3.3 V bus pull-ups accepted (12 V possible); simplicity and transistor count matter, speed does not; everything static (0.01 Hz rules out DRAM). - ISA: the emulator's 16-bit ISA (registers P A B C, constants 0 1 2 3) is the latest; "as many instructions as we can fit provided they are cheap on the transistors"; needs a carry / multi-word ops. - Pico as built-in logic analyser with stop-on-error ("I will make mistakes"). - Website: blinkingcomputer.org; DokuWiki abandoned; static HTML built from Markdown in /srv/nvme/blinking/design/; "only needs to document the present", no history; don't link github.com/drtonyr/pis-third-asm-emu (older ISA); future repo github.com/drtonyr/blinking or blinkingcomputer. - High-level language: "C would be ideal" (standard, portable assembler, no big library), compiled to the forth/asm level, system calls to the Pico allowed; user approved porting chibicc ("chibicc looks great"); then "Please push on the compiler". - I/O: missing; wants HDMI output; physical form 1 m × 1 m flat wall panel transportable to schools; one unit for https://www.computinghistory.org.uk/; £100 budget target; user's soldering is bad (PCBs welcome). - Latest: wants the BBC Micro 8×8 character set; a display of 16-bit cells each pointing to a rewritable glyph (7 bits = ASCII, custom glyphs otherwise); asks whether this fits the Waveshare board and how that board maps memory to HDMI.
name(n) inline-arg words; ` 42 push; 2-word values high word first (low on top).c_ prefix for C symbols, extern = runtime word, labels L_<unit>_..., compile-time printf expansion, source-line comments in output.Tooling: forth2asm.py → asm.py → txt2bin → emu; utest.sh (247 Forth tests), clibtest.sh, ctest.sh; python-markdown site build; sudo -n for /net/www (NFS from host "www" x.x.x.x; Apache reload must be done by user).
Files and Code Sections:
./build.py OUTDIR; sudo -n cp OUTDIR/*.html /net/www/var/www/html/blinkingcomputer.org/; sudo -n chmod 644 ....case OSgetc: m[0] = fgetc(stdin); (backup emu.c-13Nov26). asm.py: ops dict with ROR/ASR, opsex (AND|LSR|ADD|SUB|ADC|SBC|XOR|ROR|ASR|OR) (backup asm.py-13Nov26).a b - quot rem); : divu _udivmod swap-drop ; : divs over <0 over <0 xor toCall swap abs swap abs _udivmod swap-drop fromCall if(neg) ; : divu2 _udivmod2 drop2 ; : modu2 _udivmod2 swap2 drop2 ; : putu 10 _udivmod dup if([ putu ] [ drop ]) add(48) putc ;putstr via OSputc loop. Tests embedded as# in/# out` lines (don't start comment lines with "# in ").../chibicc/chibicc -o libc.4th libc.c; ... -o $base.4th $src; ./forth2asm.py sys.4th clib.4th libc.4th $base.4th post.4th | ./asm.py | ./txt2bin > $base.bin; ./emu "$@" $base.bin), ctest.sh (compares // out line and // in stdin, exit status, timeout 20), clibtest.sh (timeout 20), ctest/t0.c..t10.c (12 programs, all pass).:value words, relocations :label+n, skip unused L_ anon globals, extra text strings), emit_text() (: c_name enter(nargs nlocals) … ret_c_name: leave(k) ;, plus : main c_main exit ;), include/bc.h (extern runtime words putc getc putstr putd putu putx putdS2 putdU2 putxU2 rnd rndSeed exit; macros putchar/getchar/rand/srand/EOF/NULL; printf; libc prototypes), BLINKING.md (port notes).Memory files in /home/tonyr/.claude/projects/-srv-nvme-blinking/memory/: MEMORY.md, nanocpu-pico2-phase.md, blinkingcomputer-site.md, design-constraints.md, requirements-2026.md, c-compiler.md (all updated).
Errors and fixes:
strlen(op)==tok->len && memcmp) and NULL type from ND_NULL_EXPR (wsize returns 0 for NULL ty).timeout 20 because broken binaries loop forever.main unpairable → removed (covered by ctest).mul2 duplicate (exists in sys.4th) → removed mine.add/puts vs runtime) → c_ prefix; across units the "definition" rule failed → extern declarations = runtime words.__func__ strings emitted per function → is_used flag.Auto-mode classifier sometimes rejects Bash calls after cwd changes; re-issue exactly.
Problem Solving:
i++ → fload(1) 1 + fstore(1), C source lines as comments).Web search results to cite in the reply: bbcelite drawing_text (https://elite.bbcelite.com/deep_dives/drawing_text.html), rjw57 font-from-bbc-rom (https://notebook.community/rjw57/richwareham.com-static/downloads/2015-02-03-font-from-bbc-rom), hfmanson/bbcfont (https://github.com/hfmanson/bbcfont), ivop/8x8-fonts (https://github.com/ivop/8x8-fonts), cnx-software HSTX (https://www.cnx-software.com/2024/08/15/raspberry-pi-rp2350-hstx-high-speed-serial-transmit-interface/), Adafruit DVI HSTX docs (https://learn.adafruit.com/adafruit-picowbell-hstx-dvi-output/arduino).
All user messages:
"So I'm keen to use the character set from the BBC micro. This was eight pixels by eight pixels... I think you can find it online. Also thinking about how a Hdmi - Display is driven, given that we can't use all of the memory for the output. One way of doing this which I quite like is for the display to be 16 bit values each of which just points to a rewritable character. So if we only use the seven bits out of 16, we would have a asky style display. But we could also write custom ones for for any shape you like. Does this fit with the Waveshare board and in general, how does the waveshare board take the memory and map it out to the Hdmi?"
Pending Tasks:
Later: Pico firmware (bus protocol, logic analyser, display renderer), BBC font data, hardware schematics.
Current Work: Immediately before this summary: all compiler fixes applied and verified (ctest 12/12 ok; utest and clibtest silent = pass); the "### The display" section was added to blinking-computer-2026.md and deployed; memory files (c-compiler.md, requirements-2026.md) and chibicc/BLINKING.md updated. The final user-facing reply covering the compiler push and the BBC-Micro/HDMI display question had not yet been written.
Optional Next Step: Write the reply to the user: report the compiler state ("Please push on the compiler") and answer the display question ("Does this fit with the Waveshare board and in general, how does the waveshare board take the memory and map it out to the Hdmi?") using the design-doc content (HSTX fed by DMA from Pico SRAM, TMDS encoded in hardware, 8 pins to mini HDMI; core 1 builds each scan line from the 80×30 character map at 0xF000 and 256-glyph table at 0xE800; 16-bit cell = glyph index + attributes; BBC MOS font at &C000 available in bbcelite/other projects but no licence found), with the Sources list. Then continue with the next compiler gap (floats or struct-by-value) only as part of the ongoing "push on the compiler" request.
If you need specific details from before compaction (like exact code snippets, error messages, or content you generated), read the full transcript at: /home/tonyr/.claude/projects/-srv-nvme-blinking/5642a97f-3659-4076-aaf6-3155d3f75806.jsonl Continue the conversation from where it left off without asking the user any further questions. Resume directly — do not acknowledge the summary, do not recap what was happening, do not preface with "I'll continue" or similar. Pick up the last task as if the break never happened.
The messages after this summary are the most recent messages from before compaction, kept verbatim. The summary was written without seeing them, so something it says has not happened yet may already have happened in them.
my 16 bits per glyph was for one display mode, but I hear you saying that we can do a standard 640 by 480 frame buffer, which is pretty good for 8 bit days, and do a separate 8 bits per glyph mode. We can come back to this later, what matters is that we have lots of options.
I'm aware that the BBC rom is still under copyright, I also have experiance of these things and it's impossible to get permission when nobody cares any more. So let's explore two routes, one is that I take full responsibility for the copyright breach, including any schools not wanting to use this because of the breach, and the other is a something similar to the BBC font. We could even offer the option on download, make it very clear that the BBC font is an add on pack htat is completely optional. Are there any free fonts that fit into 8x8 that have hte same style as the BBC?
I'll search for freely licensed 8×8 fonts in the Acorn/BBC style.
Agreed on keeping both modes open, and the design doc now says so. The font question turned up something better than a lookalike.
The BBC font may already be free to copy in the UK. Section 55 of the Copyright, Designs and Patents Act 1988 covers a typeface design as an artistic work. Once 25 years have passed from the end of the year in which the first articles for producing text in that typeface were sold, anyone may copy the design to make further such articles, and do anything with the articles so made. The BBC Micro went on sale in 1981, so the period ended in 2006. This is exactly the argument Ben Harris makes for Bedstead, his public domain font drawn from the Mode 7 teletext chip, and the same reasoning covers the Mode 0 to 6 font in the MOS ROM. It is UK law only, neither of us is a lawyer, and a glyph table is arguably a copy of the ROM bytes rather than just the typeface. But it is a solid basis for your "I take responsibility" route, and much stronger than "nobody cares any more". I've put this reasoning in the design doc so it travels with the download.
Free fonts in the BBC style. Nothing I found is an original design that merely resembles the Acorn font. What exists is either the Acorn font redistributed under a free licence, or unrelated 8 by 8 fonts.
What I'd do. Ship a default drawn by us or taken from the Unlicense collection, and offer the Acorn glyphs as a separate optional pack on the download page with the section 55 note. A school that worries can leave the pack off and lose nothing, and the museum unit can run the real font. I can write the glyph tables in the 8 bytes per character VDU 23 format so a pack is just a 768-word file loaded into 0xE800.
The design doc is updated and deployed, covering the two display modes and the font position.
Sources: Bedstead and the UK copyright argument, CDPA 1988 section 55, pixel-fonts-8x8-bbc-micro, Damien Guard BBC Micro font, ZX Origins, font8x8, ivop/8x8-fonts
what would ours look like?
I'll draw one, then render it so you can judge it on screen.
These are the Artifact tool's own rules for the file you publish; the design guidance below builds on them.
Format: Always author the page as .html. Publish a .md file only when a loaded skill explicitly instructs it. When the user shares a markdown document or asks to turn one into an artifact, author an HTML page based on its content — preserve its substance, and design the page as you would any other artifact rather than transcribing the markdown one-to-one.
Skeleton: The file is wrapped in a <!doctype html>…<head>…</head><body> skeleton at publish time, so write the page content directly — no <!DOCTYPE>, <html>, <head>, or <body> tags of your own. Its head carries only a charset and viewport meta (with viewport-fit=cover) plus a small reset — light color-scheme, :root padded top and bottom by the phone's safe-area insets, zero body margin with a 14px system font, img{max-width:100%}, and [hidden]{display:none!important} (toggle visibility with el.hidden, not style.display) — so put your own <title> and <style> at the top of the file. Keep the :root padding. Put <meta name="claude:viewport-fit-sides" content="cover"> at the top of the file beside your <title>: it promises that the side gutter uses the two max() paddings given under Responsive, so the tag goes in only together with both paddings, never alone. An edit of an older page that has neither leaves both out, unless the ask redoes the page's layout. With it the page also runs under a system bar at the phone's side, where backgrounds reach that edge while the gutter keeps text and controls clear. Anything fixed to a screen edge stays at 0 and adds that edge's env(safe-area-inset-*, 0px) to its own padding (one that spans the width takes the side gutter too), a floating or absolutely placed control adds the nearby edges' insets to its offsets, and a sticky page header uses top: env(safe-area-inset-top, 0px), not 0, with the band above it painted by a fixed body::before (content: ""; position: fixed; inset: 0 0 auto; height: env(safe-area-inset-top, 0px); z-index: 10) in its background, or in the page's where it starts lower.
Title: Put a <title> at the top of the HTML; only the first 8KB of the file is scanned for it. It is the artifact's name in the browser tab and the gallery, so write a name, not a summary: a short noun phrase, typically two to four words, specific enough to pick this page out among many, the way an app or a document is named. When the user already has a specific name for the thing, use that name for the title rather than coining a new one. Never use a generic category label alone, and never append an explainer after a dash or colon. If you shorten a title that pairs the name with a generic word, keep the name, not the generic word. A multi-word title that already reads as one specific name is finished; do not shorten it further. The explanation goes in the one-sentence description parameter, which becomes the gallery card's subtitle. The title parameter fills in only when an HTML file has no <title> tag (Markdown pages keep their filename). Keep the title stable across redeploys.
External resources — CDN allowlist (CSP-enforced): external scripts load ONLY from https://cdnjs.cloudflare.com (preferred), https://cdn.jsdelivr.net/npm/, https://unpkg.com, https://cdn.tailwindcss.com (Tailwind's play-CDN script) and https://code.jquery.com; external stylesheets ONLY from https://fonts.googleapis.com, with the font files they pull from https://fonts.gstatic.com (give every face a real fallback stack). Everything else is blocked, with no visible error: every other host (esm.sh included) and, even on those CDNs, anything but a script — stylesheets, images, media, fetch/XHR/WebSocket, a library's runtime fetches. So inline all other CSS and JS and embed assets as data: URIs. How to load a library: <script src="https://cdnjs.cloudflare.com/ajax/libs/<lib>/<exact version>/<file>"> — pick the UMD build, which defines a global (e.g. react/18.3.1/umd/react.production.min.js, then react-dom) — placed BEFORE any inline <script> that uses it. Pin every library URL to an exact version ([email] on jsDelivr or unpkg, react/18.3.1/… on cdnjs, cdn.tailwindcss.com/3.4.17), never a bare name, a range or a tag (react, react@18, @latest). Choose a version at least two weeks old. Any version you knew before this conversation is old enough; if you look versions up, skip any published in the last two weeks. The viewer's sandbox also blocks any download the page starts itself — <a download> links (data:/blob: hrefs included) and script-driven saves are inert for viewers — so never offer a file through a plain link. Links to other websites (https://…) open in a new tab, but email, phone and app links (mailto:, tel:, sms:, other custom schemes) are unreliable inside an artifact: for many viewers (for example anyone outside the user's organization, or anyone viewing through a public link) following one, by link or by script, often does not work, and the page cannot tell whether it did. So show the address or number itself as selectable text (a copy button helps), treat such a link as a convenience that may do nothing, and never tell the viewer a message was sent or a call placed because the viewer tapped one. Artifacts render mermaid diagrams natively — markdown via ``mermaid fences, HTML via
blocks — no library needed, don't load one. The viewer never showsalert(),confirm()orprompt()dialogs —confirm()returns false andprompt()` returns null immediately — so build any confirmation step into the page itself.What the viewer's frame allows: The page runs in a locked-down frame; what it refuses below, it refuses for every viewer (anonymous, signed-in, embedded, desktop and mobile apps), so build around these limits instead of detecting them. The page cannot open the print dialog —
window.print()does nothing — so never offer a Print or "Save as PDF" button. Forms work as page UI (inputs, validation, submit events), but a real submission has nowhere to go: handlesubmitin script withpreventDefault()and never pointactionat another site or a mailto: address. Copy buttons work whennavigator.clipboard.writeTextis called inside the click handler — catch its rejection (older desktop apps and some app views refuse it) and fall back to selecting the text; reading the clipboard never works, though the viewer's own Paste (thepasteevent) does. Camera, microphone, screen capture, location, motion, Web Share and similar device APIs are refused without a prompt — build on one only when this user has a runtime capability for that device (theartifact-capabilitiesskill lists them) and the page declares it; otherwise take photos, audio and data as uploaded files instead (file inputs, drag-and-drop of files andFileReaderwork in browsers). A screen wake lock may be granted while the page is visible: request it and tolerate rejection. Fullscreen and pointer lock work from a click in desktop browsers; treat both as optional (handle the rejection) since phones and some app views lack them. Sound plays only after the viewer interacts (muted autoplay is fine), so start audio from a button. Other sites cannot be embedded — no YouTube, map or form iframes, and no<object>/<embed>; link out instead: links open outside the artifact, normally in a new tab, whilewindow.openworks only for some signed-in viewers in the artifact's own organization and returns null for everyone else, so use real<a href>links. Web Workers work from your own files orblob:URLs; service workers and WebRTC do not.fetch()of files published alongside the page works with relative URLs; images from your own files,data:orblob:URLs draw to canvas and export cleanly. Only a plain#namepasses between the artifact's link andlocation.hash, never#/path,#key=value, apushStatepath or a query string. The address bar on the artifact's own page and Share's Copy link follow the page's hash; beside a chat, in a project or in an app the bar may not. Only where a page has several views worth bookmarking or sending, route on the hash: name each in ASCII letters, digits and hyphens (neversettings,sidebar,customize,upgrade,downloadsorcomment-*, which claude.ai reserves) and show the one inlocation.hash, or the start view if none, on load and onhashchange. Keep other state in the page, and keys, tokens or private values out of the hash: it shows in history and copied links.Browser storage:
localStorage(alsosessionStorageand IndexedDB) works, but each artifact has its own origin and the data lives only in that viewer's browser — it survives republishes to the same URL and never reaches other viewers, other devices, or Claude. It can come back empty or the accessor can throw (a private window, cleared or blocked site data, previews or thumbnail capture), so wrap every read and write in try/catch and render the page correctly without it. Use it only for per-viewer conveniences (a remembered tab or filter, a collapsed section, an unsent draft), never for state that must persist reliably, be shared between viewers, or be read back by Claude — state like that belongs in a runtime capability when this user has one: load theartifact-capabilitiesskill before writing the page.Size: The rendered page must be 16MB or smaller, and embedded data: URIs count toward that.
Responsive: The page must also work at phone width (about 400px), and the page body must never scroll horizontally. Keep a side gutter of at least 16px at every width, written
padding-left: max(16px, env(safe-area-inset-left, 0px))andpadding-right: max(16px, env(safe-area-inset-right, 0px))so that it also clears a system bar at the phone's side: set it once onbodyor one outer wrapper (where colored bands span the width, on each band's inner wrapper), and give that element its vertical padding withpadding-block, never apaddingshorthand that zeroes the sides. Use relative units. Let flex and grid rows wrap or stack to one column when narrow, and give any flex or grid child that holds running text, code or a tablemin-width: 0, so long content wraps or scrolls inside it instead of pushing the page wider. Putmax-width: 100%on images and on anyaspect-ratiobox, and give nothing amin-widthwider than the screen. Only tables, diagrams and code blocks may be wider, each inside its ownoverflow-x: autocontainer.Theme-aware: The page renders in the viewer's theme, which has three states: an explicit choice sets
data-theme="dark"ordata-theme="light"on the root element, and the default "system" setting sets nothing, so for most viewers onlyprefers-color-schemetells light from dark. Define every color as a token, in this shape (token names and count are the design's own)::root { --bg: …; --fg: …; --accent: … } /* every token, light values */ @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --bg: …; --fg: …; --accent: …; color-scheme: dark } } :root[data-theme="dark"] { --bg: …; --fg: …; --accent: …; color-scheme: dark } /* same dark values, so the toggle wins both ways */ body { background: var(--bg); color: var(--fg) }Every token gets its first definition on bare
:root; the two dark blocks only redefine tokens, and theircolor-scheme: darkmakes form controls and scrollbars follow. No color has its only definition inside a media or[data-theme]block, and no component rule uses a literal color that reads in one theme only.bodykeeps that explicit token background: the viewer paints its own ground behind the page, so a transparent body shows the host's theme instead. A dark-first design mirrors the whole shape, selectors included: dark values andcolor-scheme: darkon bare:root(the skeleton pinslightthere), light values andcolor-scheme: lightunder(prefers-color-scheme: light)guarded:root:not([data-theme="dark"])and again under:root[data-theme="light"]. A design that deliberately commits to a single look may drop the two dark blocks but still sets the background and every color explicitly, pluscolor-scheme: darkon:rootif that look is dark.Icon (on every first publish): Pass one short generic word as
icon(e.g."chart","calendar","recipe") for the artifact's browser-tab icon — a plain signifier for what the page is, never a product or brand name, and never an emoji or markup. It stays the same for the life of an artifact, so on a redeploy (the same file path this session, orurl) omiticonand the artifact keeps the one it has; pass a different one only when the user asks.Work the way the design lead at a small, versatile studio would: give each client a visual identity at the level of treatment the task calls for. Make deliberate choices about palette, typography, and layout that are specific to this subject, and avoid templated designs.
Read the request first
Decide the treatment; designing is a given. A doc gets the same craft as a landing page; only the treatment differs. Format is a separate matter: author HTML, and publish Markdown only when a loaded skill explicitly instructs it. A Markdown publish keeps its filename as its title, uses almost none of the craft below, and is never a way to save time.
Many requests call for a more utilitarian treatment: a plan, a memo, a demo. Make it polished, with real typographic hierarchy, considered spacing, and a proper palette, but avoid over-designing. Most pages don't need a flashy, gigantic hero. Keep flourishes tasteful and limited.
Some requests call for an editorial treatment: a landing page, a game, an app or tool they'll keep or share.
If unsure: a well-composed page is always acceptable; an over-designed visual identity sometimes isn't.
Fundamentals below apply to everything. Follow the editorial process after them only when that reading calls for it.
Fundamentals for every artifact
Respect what already exists. Look for an existing design system first: CLAUDE.md, a tokens or theme file, existing component styles. When one exists, apply it; everything below fills gaps and never overrides. Precedence is always the user's own words, then the project's existing system, then your choices.
Ground it in the subject. If the subject isn't already clear, define it: one concrete subject, its audience, and the page's single job. Distinctive choices come from the subject's own world: its materials, instruments, and vernacular. Whatever the treatment, include at least one detail only this subject would have (its real units and scales, its document conventions, its terms of art) as content rather than ornament; it costs nothing even on a plain page. Use real content throughout and never lorem ipsum.
Pair typefaces. Typography determines how the page reads even when the page isn't about typography. Inter for text and Space Grotesk for headings are good defaults when nothing in the request or the subject calls for other type. Google Fonts is the only font host the Artifact CSP allows; link it directly (
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=...&display=swap">). A face from anywhere else must be inlined as a @font-face data URI, or the browser silently uses a fallback. In both cases, declare a real fallback stack. Keep running text near 65 characters wide. Set a type scale and keep to it. Give headingstext-wrap: balance, give body text comfortable spacing, and give uppercase labels a little letter-spacing.Load libraries instead of inlining them. When the page really needs a library (React, a charting or highlighting package), load its UMD build from cdnjs with one
<script src="https://cdnjs.cloudflare.com/ajax/libs/...">, pinned to an exact version at least two weeks old and placed before the inline script that uses its global; don't inline the library's source or hand-write a substitute. Only the script loads this way; a library's stylesheet still has to be inlined, and the page contract above lists the few other script hosts the CSP admits. The page's own CSS and JS, its images, and its data ship with the page. Most pages don't need any library; use one only when it does substantial work for the page.Choose neutrals deliberately. A pure mid-grey looks unconsidered; a grey with a slight hue bias toward the page's accent looks intentional. Pure white and near-black are fine backgrounds when they suit the subject, as long as you chose the neutral deliberately instead of inheriting a default.
Design both themes. The page renders in the viewer's theme, and the viewer has three states: an explicit choice sets
data-theme="dark"ordata-theme="light"on the root element, and the default "system" setting sets nothing. Most viewers see the document with nodata-themeattribute, where onlyprefers-color-schemedistinguishes light from dark. Structure the CSS at the token level for all three. The bare:rootblock defines the complete light palette (for a deliberately dark-first design, swap light and dark consistently through this whole pattern);@media (prefers-color-scheme: dark)redefines only the tokens, guarded as:root:not([data-theme="light"])so an explicit light choice overrides a dark OS setting;:root[data-theme="dark"]redefines them again so the toggle also overrides in the other direction; wherever the dark palette applies - both dark blocks, or bare:rootin a dark-first or single-dark design - also setcolor-scheme: dark(the skeleton pinslighton:root), so native form controls and scrollbars follow the palette. Style components through the tokens, never directly inside a media or[data-theme]block: a color defined only inside[data-theme]never applies when nodata-themeattribute is set, and the page then shows one theme's text on the other theme's background. Two more rules keep each theme consistent. First, the artifact is composited over a background the viewer paints in its own theme, sobodymust set an explicitbackgroundfrom a token; a transparent body shows the host's background with no warning. Second, every element that sets a color takes it from the same token set as the surface behind it, never from a literal that works in only one theme. Declare every token in the bare:rootblock before any media or[data-theme]block redefines it; a color that exists only inside one of those blocks is the classic unreadable-artifact bug. Give the second theme the same care as the first: don't simply invert it; keep contrast legible and keep the accent working on both backgrounds. A design that deliberately commits to one visual world (a neon arcade screen, a letterpress invitation) may stay single-theme: then omit the media query and[data-theme]blocks entirely but still set the background and every color explicitly, so the page looks right on either host background; do this by choice, never by omission.Use layout for spacing. Lay out sibling groups with flex or grid and
gapinstead of per-element margins, which collapse or double without warning. Keep a side gutter of at least 16px at every width, writtenpadding-left: max(16px, env(safe-area-inset-left, 0px))andpadding-right: max(16px, env(safe-area-inset-right, 0px))so that it also clears a system bar at the phone's side: set it once onbodyor one outer wrapper (where colored bands span the width, on each band's inner wrapper), whose vertical padding usespadding-blockand never apaddingshorthand that zeroes the sides. Let rows wrap or stack to one column at phone width (about 400px). Give images and anyaspect-ratioboxmax-width: 100%, and don't give anything amin-widthwider than the screen. Only wide tables, code, and diagrams may exceed it; give eachoverflow-x: autoon its own container so the page body never scrolls sideways. The publish skeleton pads:roottop and bottom by the phone's safe-area insets (zero everywhere except a phone app) so the page runs edge to edge while its content stays clear of the system bars; keep that padding. Put<meta name="claude:viewport-fit-sides" content="cover">at the top of the file beside your<title>: it promises that the side gutter uses those twomax()paddings, so the tag goes in only together with both paddings, never alone. An edit of an older page that has neither leaves both out, unless the ask redoes the page's layout. With it the page also runs under a system bar at the phone's side. Anything fixed to a screen edge stays at0and adds that edge's inset to its own padding (one that spans the width takes the side gutter too); a floating or absolutely placed control adds the nearby edges' insets to its offsets. A sticky page header usestop: env(safe-area-inset-top, 0px)and never0. So that nothing scrolls into view above it, under the host's controls, paint that band withbody::before { content: ""; position: fixed; inset: 0 0 auto; height: env(safe-area-inset-top, 0px); z-index: 10; background: ... }, in the header's own background where the header opens the page and in the page's where the sticky bar starts lower. Size a one-screen app withheight: 100%onhtmlandbodyinstead of100vh, so it fits inside the:rootpadding. A page that includes its own viewport meta gets the:rootpadding only when that meta declaresviewport-fit=cover. Usefont-variant-numeric: tabular-numswherever digits line up in columns.Make repeated elements consistent. For cards in a row, label/value pairs down a list, or badges on sibling items, use the same edges, baselines, and inner padding on each, and put any recurring element in the same place on each. Let content set a container's height and pick a column count the items fill, so nothing stretches over empty space or sits alone in a row. Make text that can outgrow its track wrap or scroll in its own container; clipped text is a bug.
Use card styling selectively. Border, fill, radius, and shadow each mark an element as a separate object. Apply them by role, to set off the one element that needs it; applying the same radius and shadow to every block flattens the hierarchy. Open with big-number tiles only when those figures are the point of the page.
Draw charts to scale. Place marks, ticks, and labels with one scale, and make every label name a value the chart actually reaches. Color chart text from the theme tokens so it is readable in both themes. Keep marks, labels, and edges clear of one another and inside the drawing's bounds; in SVG, leave room in the viewBox for the outermost labels and give every drawn shape an explicit fill.
Make the page complete at rest. Everything meant to be read is visible once the page has loaded, with no scrolling to trigger it; that first still frame is what a thumbnail, a shared link, and a skimming reader all see. A section may animate in, but from a visible resting state, never left at
opacity: 0waiting for an observer. Size a hero to its content instead of to the viewport; a100vhopener pushes the rest of the page out of that first frame. A tool or app opens in a realistic working state: the user's real data where it exists, otherwise example rows, a loaded sample, or a plausibly filled form, clearly marked as examples and never presented as the user's own figures. The first view shows what the tool does; an empty shell waiting for input shows nothing. When those records live in thedbcapability, keep them out of the page source. Its first frame renders before the store answers, so make that frame a designed empty state that names what will appear and how to add the first entry, then fill it from the store. Seed the store through theArtifactDatatool (what type instructions callwrite_db) after the first publish: the user's real data, or marked examples for a tool only the user will use. For a store other people will fill with their own entries, seed only what the user gave you or asked for, never invented people or records.Avoid AI-generated design. AI-generated design currently clusters around a few looks: warm cream (#F4F1EA) with a serif display and terracotta accent; near-black with a lone acid-green or vermilion pop; broadsheet hairline rules with dense columns; a purple-to-blue gradient hero on white; emoji as section markers; everything centered;
rounded-lgeverywhere; accent bar/rail on rounded cards. When the user specifies a visual direction, follow it exactly; their words always win, including when they ask for one of these looks. When nothing is specified, don't use that freedom on one of these defaults.Build cleanly. Watch for overlapping elements, cascade collisions, and silent font fallbacks. Close every non-void element, double-quote attributes, give keyboard focus a visible state, and respect
prefers-reduced-motion. Give every form control a stableid(the platform preserves form values, focus, and scroll position across a republish). For generative or decorative graphics, use Canvas or WebGL instead of hand-writing long SVG path data.CSS rules. When writing the CSS, watch your selector specificity. It is easy to generate classes that cancel each other out, e.g. a type-based selector like
.sectionand an element-based one like.ctaboth setting padding and margins between sections. Structure the cascade so it doesn't undo your spacing unnoticed.Writing the copy. Treat words as design material and never as decoration. Write from the user's side of the screen: name things by what people recognize instead of how the system is built (a person manages notifications; they don't manage webhook config). Use active voice; a control states exactly what happens ("Publish", then a toast that says "Published"). Errors explain what went wrong and how to fix it, without apologies or vagueness. Prefer specific to clever. Write plainly, the way a knowledgeable person would talk. Avoid mannered devices: asides set off by em-dashes, "not X, but Y" framing, colon-then-reveal sentences, scare quotes around invented labels, and stock phrases such as "worth noting" or "honest caveat". Prefer short, direct sentences over compressed or clever phrasing.
Name the page like a product; don't caption it. The
<title>is the artifact's name in the gallery and the browser tab, and it gives the reader a first impression of the care taken. Give the page a real name: a short noun phrase, typically two to four words, specific to the subject; or, for a page that exists to answer one question, that question itself, which then is the page's name. When the user already has a specific name for the thing, use that name for the title rather than coining a new one. Stop at the name; a title that adds its own explanation after a dash or colon reads as generated filler. The name must also identify the page among many: in the gallery it appears beside dozens of other artifacts, and a generic category label that could apply to any of them fails as a name just as an appended explanation does. When a candidate title combines the name with a generic word (a greeting, a category, a page-type label), keep the name; a trim that drops the identifying part and keeps the generic word produces a title that could apply to any page. This rule removes explanations and doesn't require brevity: a multi-word title that already reads as one specific name is finished, and shortening it further only makes it generic. Put the explanation in the one-sentence publishdescription; the gallery shows it directly under the title.Structure is information. Structural devices (numbering, eyebrows, dividers, labels) should encode something true about the content instead of decorating it. Many generic designs use numbered markers (01 / 02 / 03), but those fit only if the content actually is a sequence, such as a real process or a typed timeline where the order is information the reader needs. Before adding numbered markers or similar devices, check that they actually make sense.
When the page is a UI (dashboard, tool). It is scanned and operated instead of read top to bottom, so the craft shifts from typography to information design. Put the summary before the detail. Encode state in form as well as in numbers (a pill, a chip, a severity stripe) so that whatever needs attention is visible at a glance. Semantic color (good / warning / critical) is separate from the accent hue and doesn't count as your accent. Give sparklines and charts the same care as type: an area fill, a faint grid, an emphasized endpoint. Interactive elements should look interactive.
Process
Start from what the viewer should be able to do on the page, in addition to what they will read. If the page should take input, keep what people change for whoever opens it next, show live data, or ask Claude something, load the
artifact-capabilitiesskill now and design around what it makes available to this user. A page that is only read doesn't need that.Before writing the page, settle a short design plan (a compact token system) and write it into the file itself, as the
:rootblock at the top of the page's<style>, rather than into your reply: - Color: the palette as 4-6 named color tokens. - Type: font tokens for 2+ roles: a characterful display face used with restraint, a complementary body face, and a utility face for captions or data if needed. - Layout: the layout concept as a one-line comment above the tokens.Then build the rest of the page from those tokens, deriving every color and type decision from them. The plan is working material, not part of the answer: unless the user asks about the design, one plain sentence on the direction is the most to say about it, with no hex values or font names.
Write, check once, publish. Before publishing you may look at the rendered page once, where this session offers a way: one
ArtifactCheckpreview (or the Artifact tool's ownaction: "preview"where there is no separateArtifactChecktool), or else one screenshot of the local file; if the session offers none of these, skip the look. The preview renders desktop and phone widths in light and dark and lists overflow, colors that ignore the theme, blocked loads and console errors, including a script that fails to parse, so nothing it covers needs a check of your own. The look is optional: if you take it, make one pass of edits for what it shows, without a second look; then publish. For a page that charts real numbers, take the look rather than skip it, and spend it on the chart. A page whose point is logic (dates, money, scoring, parsing) may get one more check before publishing: one run of a pure function on a sample input, or, where no preview ran, one syntax check of its script; nothing more. A page that declarescapabilitiestakes the look rather than skipping it, then one functional pass after the first publish, because the preview cannot run that code: read back once what the page stores or serves (one read of its stored data or one read-only route call; where that read alone would prove nothing, as on an empty store, you may first write one probe document yourself and delete it after the read, passing theversionthe write returned asif_version; no other test writes unless the user wants one), tell the user in one line what you exercised and what you could not, and stop. None of this becomes a loop, because the user is waiting for a link: no second screenshot, no scripts that probe the DOM, no re-running a check that passed. Review happens on the live page, and further polish is for the user to request; if the user reports something visibly broken (a clipped column, unreadable text, a control that does nothing), fix that, take at most one more look if the session offers a way, and republish once. These checks are for code you wrote into this page, not for content you fill into an Artifact made from an Artifact type (a Slides deck, a Design canvas): there the type's own instructions say whether to check, and if they say nothing, don't.Open viewers. You don't need to do anything for viewers who already have the page open: published changes are delivered to them automatically at their next quiet moment, with state preserved where possible. If your page has state a viewer would miss (a game, a long form), register
window.claude?.hot?.snapshot(...)and boot throughwindow.claude?.hot?.ready ? window.claude.hot.ready(start) : start(window.claude?.hot?.data ?? {});hotis the viewer's own update hook, not a capability behindclaude.use(), and is absent outside a viewer, hence the?..When the request is editorial
The stance changes here: the client has already rejected proposals that felt templated, and is paying for a distinctive point of view. Make opinionated decisions, and take one real aesthetic risk where it serves the work.
Review the design plan against the subject before building: if any part of it reads like the generic default you would produce for any similar page, revise that part in the plan itself, without narrating the revision to the user. Write the code only after the plan is specific to this subject, following the revised plan exactly.
Principles
The "35 #" header line is being skipped by the comment filter. I'll only treat lines as comments if they start with "# " or are exactly "#".
I drew a complete 96-character font and put it on a page so you can see it rather than imagine it:
https://claude.ai/artifact/De36exdVjBvgdLVVaL71QE
The page shows the project name set in the font, a grid of every glyph from ASCII 32 to 127, and an 80-column screen you can type into, with white, green, amber or red phosphor and a scale control.
The design rules I used, chosen to feel like a 1981 home computer without copying one:
Where it lives. The source is a plain text file, design/font/blink8x8.txt, one block per character with a dot for dark and a hash for lit, so anyone can redraw a glyph in an editor. The converter design/font/font2tab.py turns it into a C array, a JSON array, or the 768 words that load straight into the glyph table at 0xE800.
Nothing on this page is from the Acorn ROM. Where it looks similar it is because an 8 by 8 grid with single-pixel strokes allows few ways to draw an A. Tell me which glyphs look wrong to you and I'll redraw them; the lower case m, r and t are the ones I'm least sure of.
can I see it side by side with Acorn font, it's clear that ours is light on the diagonals, as in 'x', maybe that is unavpidable. sometimes we move from one to two pixels width, like in middle of '3', also 'H', and 'I' and 'P'. there isn't much gap between lines, perhaps that is also true of all 1980's 8 bit displays.
The uneven stroke widths you're seeing are most likely my preview, not the font: the canvases are scaled by CSS and by your monitor's pixel ratio, so some font pixels land on 1 screen pixel and some on 2. I'll fix the rendering to whole device pixels, add a line-spacing control, and fetch the Acorn glyph data for a side-by-side.
Now the page: three fonts side by side, integer device-pixel rendering, and a line-spacing control.
Side by side is up, same link: https://claude.ai/artifact/De36exdVjBvgdLVVaL71QE
Why ours looked light. I pulled the MOS 1.20 font bytes and every stroke in the Acorn face is two pixels wide, using columns 1 to 6. Mine was a one-pixel face. That is the whole difference, not the diagonals as such. So I drew a second font, Blink bold, with two-pixel strokes and Acorn's proportions, and kept the one-pixel face as Blink light. Each cell in the grid now shows Acorn, bold, light for that character, and the screen has a font selector. In the bold face the diagonals of x, V and Z are as heavy as the verticals.
The one-to-two pixel steps were my preview, not the glyphs. The canvases were being scaled to the page width and to your monitor's pixel ratio, so some font pixels covered one screen pixel and some covered two. Every canvas is now drawn at a whole number of device pixels per font pixel. If H or I still look uneven at scale 1 or 2, tell me which, because at that point it is real.
Line spacing. You're right that the BBC Micro stacked text lines 8 rows apart with no gap, and so did the Spectrum and the C64. We don't have to. The Pico builds each scan line itself, so inserting blank rows between text lines is free. The page has a rows-per-line control, 8, 9, 10 or 12, so you can judge what looks best. At 640 by 480 that gives 60, 53, 48 or 40 text lines, or half those if we double each row for a chunkier 1980s look.
A few bold letters converged on Acorn's because six columns of two-pixel strokes leave one way to draw H, E, L, T and o. Where there was room I went my own way: pointed A, flat-topped 3, a diagonal through 0, different M, W, &, @ and Q. Tell me which ones you'd like changed.
rows per line is very nice. It allows for the authentic 80's crowded look as well as expanding it later to be readable in a classroom. What's the maximum resoltion we can run at with the waveshare. BTW, I have ordered a waveshare.
Good news on the board. I'll check what resolutions the RP2350's HSTX output can reach on the Waveshare, with and without overclocking.
Short answer: 640 by 480 at 60 Hz is what the board does at its rated clock, 800 by 600 needs a mild overclock, and anything higher is a stunt. The arithmetic is simple and worth having in your head.
How the limit arises. HSTX puts out two bits per clock on each pin, and DVI needs ten bits per pixel per colour lane. So the pixel clock is a fifth of the HSTX clock, and the HSTX clock is rated at 150 MHz.
| Mode | Pixel clock | HSTX clock needed | Verdict |
|---|---|---|---|
| 640×480 @ 60 | 25.2 MHz | 126 MHz | stock, Waveshare's own demo |
| 800×600 @ 60 | 40 MHz | 200 MHz | mild overclock, common on RP2350 |
| 1024×768 @ 60 | 65 MHz | 325 MHz | heavy overclock, voltage boost |
| 1280×720 @ 60 | 74.25 MHz | 371 MHz | demonstrated, not for a school build |
One project runs a 1080p DVI signal from an RP2350 at 372 MHz with no frame buffer, generating each scan line on the fly. By my arithmetic that bandwidth is 720p at 60 Hz or 1080p at 30 Hz, so I'd treat 720p as the practical ceiling even for enthusiasts.
What I'd design for. 640 by 480 at stock clock, and let the monitor or projector do the enlarging. Resolution is not what makes text readable in a classroom; glyph size is. At 640 by 480, 80 columns on a 32-inch monitor gives letters about 8 mm high, and your rows-per-line control plus pixel doubling gets you anything from 60 cramped lines to 15 huge ones. Memory also agrees: a 640 by 480 frame buffer at 16 colours is 150 KB, which fits beside the CPU's 128 KB in the Pico's 520 KB. At 800 by 600 that buffer no longer fits with 8-bit colour, and nothing is gained for an educational machine.
On the board you've ordered. The RP2350-PiZero carries HSTX to the mini HDMI socket on GPIO 12 to 19, which leaves 28 GPIO on the 40-pin header, enough for our 23 bus and strobe pins with a few spare. Its hello_dvi example runs 640 by 480 at 60 Hz, and that is the first thing to flash when it arrives. The signal is DVI timing over an HDMI plug with no audio, and most monitors accept it, but some TVs want real HDMI info frames and show nothing. If your monitor shows the test picture, the whole display path is proven before we write a line of our own.
I've added the resolution table and the board details to the display section of the design doc and deployed it.
Sources: Hackaday on the HSTX peripheral, RP2350 1080p DVI at 372 MHz, Waveshare RP2350-PiZero wiki, CNX Software on the RP2350-PiZero, Adafruit DVI HSTX library
Please keep all of these references - and indeed please stash this whole conversation so that nothing gets lost even when compaction happens. Anyone who wants to copy the design will be very interested in the 'why' of the design, and will enjob following the references and reading why and why not.
also, the pi zero is within budget and has HDMI. On the hardware side, do we need the pico PIO? On the software side, can we poll fast enough and reliably enough to use a Pi zero?
Two jobs: first I'll stash the conversation and its references into the project so compaction can't lose them, then answer the Pi Zero question.
Stash done and deployed. Now the Pi Zero answer.
What's stashed. A script, design/tools/stash.py, exports every Claude session on this project into design/conversations/ as readable Markdown, with the typed messages and my replies in order and the tool traffic dropped. It also rebuilds design/references.md from every link I cited, 19 so far, each grouped under the conversation it came from. Both are now pages on blinkingcomputer.org, linked from the home page, and the raw transcript is copied alongside but kept out of any future git repo because it carries account identifiers. I've recorded that the script is to be re-run at the end of every session, so a compaction can no longer lose anything. This reply will be captured on the next run.
Do we need PIO? No, on either board. The machine runs between 0.01 Hz and a few kHz, so every phase is a handful of ordinary GPIO writes from software with a pause after each strobe. PIO, or the second core, would only matter if we wanted the discrete CPU running at megahertz, which we don't. The Pico's second core is useful for the display, but the Pi Zero has its own GPU for that.
Can a Pi Zero poll fast and reliably enough? Yes, and the reason is a property of your design rather than of the Pi. Linux is not real time and will pause a user program for milliseconds now and then. But every flip-flop in the machine is static and the Pi makes every timing edge, so a pause just makes the machine wait. It lengthens a strobe, never shortens one. Nothing in the hardware times out, so there is nothing for jitter to break. Speed is not a problem either: memory-mapped GPIO toggles in tens of nanoseconds, and even the slow portable path through libgpiod costs about a microsecond a call, so an instruction of 13 phases still runs tens of thousands of times a second.
Pins and electrics. The header gives 26 GPIO plus two on the ID EEPROM pins, against 23 needed. Like the RP2350 the Pi is 3.3 V and not 5 V tolerant, so nothing changes on the bus. One detail to watch: 16 bus lines pulled up by 1 kΩ sink 53 mA when all low, which is more than the Pi's GPIO block likes, so I'd move to 2.2 kΩ pull-ups.
The trade. The Pi Zero 2 W gives you real HDMI at any resolution with no encoder work, a USB keyboard, Wi-Fi, and enough machine to run chibicc on the panel itself, so a class edits, compiles and runs C on the discrete CPU with no other computer. The logic analyser view goes on the same screen. Against that: a 30-second boot, an SD card that can corrupt when the power is pulled at the wall, which a read-only root filesystem largely fixes, and a Linux system to look after. The Pico boots in milliseconds and has nothing to corrupt, which I'd still want for the museum unit.
I've written this up as a new section in the design doc with both options kept, since the 23 bus wires are identical and the choice can be made per unit. My own leaning is a Pi Zero for the self-contained classroom machine and the Waveshare for the museum, but the firmware should be one C program that compiles for both.
The Pi zero is also useful as I can give you full control without plugging anything into ml0. And from an education point of vuew, it can be full screen or in a window. The design was Pi Zero before the pico came out. There is also a Pi Zero 3W rumoured https://hackaday.com/2026/11/10/devicetree-for-hackers-intro/ - Raspberri Pi are very good at keeping selling the old designs though.