<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[ASM80 news]]></title><description><![CDATA[ASM80 is a browser-based assembler and emulator for 8-bit processors — Z80, 8080, 6502, and others. This blog is where I write about what's new, what changed, and occasionally how it works under the hood.
If you're looking for a way to write assembly for your KIM-1, ZX Spectrum, or any other retro machine without installing anything, asm80.com is the place.]]></description><link>https://blog.asm80.com</link><image><url>https://cdn.hashnode.com/uploads/logos/69d69909707c1ce7686b950f/19aabe90-47f7-44c2-9eed-da7419ba4d2d.png</url><title>ASM80 news</title><link>https://blog.asm80.com</link></image><generator>RSS for Node</generator><lastBuildDate>Sun, 12 Apr 2026 12:31:47 GMT</lastBuildDate><atom:link href="https://blog.asm80.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[I Like Assembly]]></title><description><![CDATA[I like assembly.
It was the first language I "learned." I was twelve, didn't have a computer, so I learned on paper. And apparently it worked — because a year later, when I finally sat down at a machine, my first programs, carefully written on paper,...]]></description><link>https://blog.asm80.com/i-like-assembly</link><guid isPermaLink="true">https://blog.asm80.com/i-like-assembly</guid><category><![CDATA[asm80]]></category><category><![CDATA[AI-assisted development]]></category><category><![CDATA[Assembly]]></category><category><![CDATA[c-compiler]]></category><category><![CDATA[emulator ]]></category><category><![CDATA[ide]]></category><category><![CDATA[llm]]></category><dc:creator><![CDATA[Martin Malý]]></dc:creator><pubDate>Sun, 12 Apr 2026 11:28:57 GMT</pubDate><content:encoded><![CDATA[<p>I like assembly.</p>
<p>It was the first language I "learned." I was twelve, didn't have a computer, so I learned on paper. And apparently it worked — because a year later, when I finally sat down at a machine, my first programs, carefully written on paper, converted to hex with addresses calculated by hand, actually ran when I typed them into a PMD-85. I learned BASIC afterwards. And even though I now write ten levels of abstraction above all that, I never forgot assembly and I still like it.</p>
<p>I wouldn't write anything big in it, of course. I'm not a fanatic or an orthodox "programmer." But I know it, I can do it, and I remember it.</p>
<hr />
<p>You probably know I've been building an 8-bit online assembler. The first prototype went live sometime around 2014, give or take. It had an editor, a compiler, a debugger, and a handful of emulators for old 8-bit machines. I cobbled it together in spare moments and then maintained it here and there.</p>
<p>Around 2020 I ran out of time for it, and besides, I thought it deserved a rewrite. So I tried rewriting the core in more modern ES6 — at least to get proper modules in there and cover it with tests. It took a while, but it eventually came together and I published it.</p>
<p>Then I thought the IDE itself could use a rewrite. I experimented for a bit, tried integrating Monaco into Svelte, then Svelte changed under my hands and turned into a runic React, so I told myself I'd rewrite it someday — and that "someday" kept receding…</p>
<p>Until this year. There I was, lying in bed, suffering through a gallbladder colic. There's not much you can do in that state. You lie there, grateful to be breathing, and you try to breathe as little as possible so it doesn't hurt more than strictly necessary. After one of those attacks, around half past midnight, I thought I'd take a look at how that celebrated OpenCode with its "coding" models actually performs. I think Minimax M2.5 was just out or something like that, so I had it running. Working was out of the question, but I wanted to try it. My eyes were closing from exhaustion, so I typed something like:</p>
<p><em>Build the base for a web app — an IDE for an online compiler.
Menu, file tree panel, editor panel (Monaco), status bar.
You pick the frontend setup. Probably no Bootstrap. No React, no Vue, no Svelte.
SPA, ES6, async. Set up a dev server and build system too.</em></p>
<p>Then I put the laptop down beside the bed and fell asleep.</p>
<p>In the morning I woke up and went to see where it had gotten stuck. To my surprise, it said: "Done, run it with npm run dev." So I ran it. And what do you think?</p>
<p>I didn't believe it had pulled it off. But it had. There was an IDE with a menu, a working Monaco, working web workers, components via Shoelace — and it looked decent.</p>
<p>Up to that point the maximum I'd gotten out of these tools was "help me with this function" or "write me this kind of module." And even then I had to hover over it, because the models had a tendency to reinvent the wheel, skip available libraries, or slap in a heavyweight Axios where a plain fetch would do.</p>
<p>It was a shot in the dark, but it landed. It did the most annoying part of the work for me. If you've ever integrated bare Monaco into anything, you know what I mean. It encouraged me, so I kept going: connect the filesystem, hook up the existing compiler, make the UI actually make sense, keep telling it "not like that" and nudging it in the right direction.</p>
<p>"Stop! Why this way? Do it simply — do it like this!"</p>
<p>"No, I don't want that. I want it to compile the file!"</p>
<p>"Why did you invent this toggle? You have Shoelace for that!"</p>
<p>Like working with an eager junior.</p>
<p>But then it finished, and it was satisfying enough that I decided this was the way forward.</p>
<hr />
<p>"Add Auth0 login."</p>
<p>"Build a database backend to store the projects."</p>
<p>I just sat there, gave orders, reviewed, and steered. It looks simple from the outside, but there was a lot of other work behind it. "Why are you using this API, it's been deprecated for ages!" Or: "This code is a mess, throw out that middleware and write it yourself. And add JSDoc." Or: "Auth0 doesn't support this method anymore!"</p>
<p>Around that time Claude Code started behaving sensibly on Windows too, so I put it on the main line of development. OpenCode was meanwhile rewriting my BASIC compiler for the 8080 to also support the 6502, and I was using Codex to work on a second version with translation to p-codes.</p>
<p>I gradually improved the workflow — a Ralph Loop here, some skills there — and with that, efficiency grew. I became ambitious, started dreaming up more things to add, had it suggest what else to build, had it research which features would make sense, then planned them and had them developed. It's fun, when you know what you're doing.</p>
<p>A lot of the work was done by subagents. "We have this feature working for one processor. Do it for the others too!" Syntax highlighting for all supported processors, for example. I wouldn't have written that by hand — I really wouldn't. The subagents did, and they seemed happy about it.</p>
<p>Then I had them rewrite the old computer emulators from the original ASM80. They wrestled with it, but they got there. Along the way they came up with small niceties, like showing the cycle count for each instruction right in the editor.</p>
<p>I had it write a manual, modeled on those old 1980s ones. As the assembler develops, a team from paperclip.ai runs in the background, watching commits across all the components and updating and expanding the manual accordingly.</p>
<p>When I was satisfied and didn't know what to add next, I went for a checkup at the hospital, and on the way I was thinking about a slightly mad-sounding idea: what if you could run a piece of code right in the editor and watch — while you're writing — how register values change, what's happening in memory, how many cycles it takes. From the waiting room I worked through it with Claude, figuring out the best way to implement and integrate it, and by the time my name was called, I had a spec written up.</p>
<p>This is a clear example of how detailed preparation pays off, and how useful it is to push the LLM into "ask questions and argue" mode. During the conversation I pulled it back several times when it started "thinking more than was good for it" and had a tendency to overcomplicate things. The most common thing I said was: "That's unnecessary. Just do it like this!"</p>
<p>I handed it the spec that afternoon. By evening it was done. Naturally I sent it back for revisions about ten times, but in the end it iterated and converged toward what I had in mind — even though I hadn't precisely known what I wanted beforehand.</p>
<hr />
<p>From there it was a short step to the "playbook" — something like a Jupyter Notebook, but for assembly. You have text — instructions or a tutorial — and embedded assembly code you can compile and run in the emulator. You can modify it, see what it does, see how a change ripples through. I think it could be useful for teaching.</p>
<hr />
<p>But I still felt I needed something more. I have assembly, I have BASIC, I have live terminals and emulators — but it still needs something, something… Something like a C compiler!</p>
<p>It doesn't have to be GNU C. It won't be cc65 or SDCC — the licensing situation is complicated there, and I'd have to run them server-side, which would be slow, plus all the security concerns and connectors for ASM80. So I took a nice C compiler written in C and had it rewritten in ES6 to run in the browser. Not WASM — an actual rewrite. Plus a small test suite covering all supported constructs. Two days, and it worked.</p>
<p>Then came stripping out the 16-bit x86 foundations, building a codegen for Z80, 6502, and 6809 (phase 1), preparing another round of tests, then E2E tests — which required turning some parts of ASM80 (compiler, linker, archiver, emulator) into CLI utilities. The compiler compiles; I just don't like the output. It's not optimized — a clean compiler translates C constructs pretty mechanically. Sometimes it loads variables into registers it already has them in, sometimes it computes a[0] and adds a pointer to A with zero, sometimes it generates dead code that the processor will never reach. So since yesterday I've been working on code optimizations. All the features are implemented and covered by tests, so it's time to work on the code generator.</p>
<p>As I write this, in the second window I'm watching it wrestle with the problem. I show it a construction that makes no sense, it analyzes why it makes no sense, figures out how it arises, digs around inside the compiler to find how to avoid it, writes a spec, writes a plan, a colleague reviews that plan, they go back and forth three times, then it opens a worktree, launches subagents to execute the plan, test it, commit it — and I expect it to be done in about an hour.</p>
<hr />
<p>It has a moment to itself right now, so I'm thinking about what I can do while the robot is working…</p>
<p>Oh well. I'm looking forward to writing libc. That'll finally be proper hard work. I'm not handing that one over to it. Well — maybe the boring parts.</p>
<p>I'll finish writing and go think of something to keep it busy overnight. :)</p>
]]></content:encoded></item><item><title><![CDATA[Command-line tools for the assembler]]></title><description><![CDATA[ASM80 lives in the browser, and for most people that's fine. But if you want to run the assembler from a Makefile, a CI pipeline, or a script that also compiles C code for a Z80, clicking "Build" in a]]></description><link>https://blog.asm80.com/command-line-tools-for-the-assembler</link><guid isPermaLink="true">https://blog.asm80.com/command-line-tools-for-the-assembler</guid><category><![CDATA[asm80 cli]]></category><category><![CDATA[ic80]]></category><category><![CDATA[assembler]]></category><category><![CDATA[toolchain]]></category><category><![CDATA[z80]]></category><dc:creator><![CDATA[Martin Malý]]></dc:creator><pubDate>Sun, 05 Apr 2026 07:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69d69909707c1ce7686b950f/308bbee9-ae21-4bab-887d-47422eb6de7c.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>ASM80 lives in the browser, and for most people that's fine. But if you want to run the assembler from a Makefile, a CI pipeline, or a script that also compiles C code for a Z80, clicking "Build" in a browser tab isn't going to cut it. That's what <a href="https://www.npmjs.com/package/@asm80/cli"><code>asm80-cli</code></a> is for.</p>
<p>One npm install gives you four commands:</p>
<pre><code class="language-plaintext">npm install -g asm80-cli
</code></pre>
<p>After that, you have <code>asm80</code>, <code>asm80-link</code>, <code>asm80-ar</code>, and <code>asm80-run</code> in your PATH. They share the same assembler core as the browser IDE — same syntax, same directives, same output. No surprises. The code that turns your mnemonics into bytes is literally the same JavaScript module, just invoked from Node instead of a <code>&lt;script&gt;</code> tag.</p>
<h2>asm80</h2>
<p>The assembler. Give it a source file and it produces Intel HEX (or Motorola S-Record for 6800/6809). The CPU is detected from the file extension — <code>.z80</code> for Z80, <code>.a80</code> for 8080, <code>.a65</code> for 6502, and so on — so there's no flag for that. You just write:</p>
<pre><code class="language-plaintext">asm80 hello.z80
</code></pre>
<p>and get <code>hello.hex</code>. If you want a relocatable object module instead (for linking later), pass <code>--module</code>:</p>
<pre><code class="language-plaintext">asm80 --module mylib.a80
</code></pre>
<p>This produces <code>mylib.obj80</code>. Note the extension: it's <code>.obj</code> plus a CPU suffix, i.e. <code>.objz80</code>, <code>.obj80</code>, <code>.obj65</code>, <code>.obj09</code>. The linker needs to know what it's linking. Add <code>--no-lst</code> if you don't care about the listing file. The listing is mostly useful for debugging — it shows you the generated bytes next to each source line, so you can spot when a relative jump is out of range or an operand got encoded differently than you expected.</p>
<h2>asm80-link</h2>
<p>The linker takes a YAML recipe with a <code>.lnk</code> extension in the same format the ASM80 IDE uses and produces a single <code>.hex</code> file from multiple object modules. A typical <code>.lnk</code> looks like this:</p>
<pre><code class="language-yaml">segments:
  CSEG: '0x0000'
  DSEG: '0x8000'
vars:
  BIOS_PRINT: '0x5'
modules:
  - crt0.objz80
  - main.objz80
library:
  - z80-runtime.objz80
entrypoint: __start
</code></pre>
<p>The <code>segments:</code> section tells the linker where to place code and data. <code>CSEG</code> is the code segment, <code>DSEG</code> is for initialized data, the addresses are absolute, so you're laying out your binary exactly as it will sit in memory. If your ROM starts at <code>0x0000</code> and your RAM at <code>0x8000</code>, those are the numbers you put here.</p>
<p>The <code>vars:</code> section injects symbols at link time, which is handy for BIOS entry points or hardware addresses you don't want hardcoded in your source. Your assembly code can reference <code>BIOS_PRINT</code> with an <code>EXTERN</code> directive, and the linker fills in the actual value. No magic numbers scattered across files.</p>
<p>Modules listed under <code>library:</code> only contribute code that's actually referenced, same as a static library in a C toolchain. If your runtime library defines 30 routines and your program calls two of them, only those two end up in the final hex. Modules listed under <code>modules:</code>, on the other hand, are always included in full — that's where your main code and startup routines go.</p>
<p>The <code>entrypoint:</code> field names the symbol where execution begins. The linker makes sure this address ends up as the first thing in the output, so the CPU starts running the right code after reset.</p>
<pre><code class="language-plaintext">asm80-link project.lnk
</code></pre>
<h2>asm80-ar</h2>
<p>If you're building a library from several object modules, <code>asm80-ar</code> packages them together using a <code>.lbr</code> recipe:</p>
<pre><code class="language-yaml">name: mylib
version: 0.1.0
modules:
  - mathops.obj80
  - stringutils.obj80
</code></pre>
<p>The output extension depends on the CPU: <code>.lib80</code> for 8080, <code>.libz80</code> for Z80, <code>.lib65</code> for 6502, <code>.lib09</code> for 6809 etc. This mirrors the <code>.obj</code> convention, everything is tagged with what CPU it belongs to, so you can't accidentally link Z80 code into a 6502 project. (You'd be amazed how often that comes up.)</p>
<pre><code class="language-plaintext">asm80-ar mylib.lbr
</code></pre>
<p>The result is a single file you can reference in any <code>.lnk</code> recipe under the <code>library:</code> key. Build the library once, use it in every project that needs it.</p>
<h2>asm80-run</h2>
<p>It's a command-line emulator. Hand it a source file or a <code>.hex</code> and it assembles (if needed), loads, and runs:</p>
<pre><code class="language-plaintext">asm80-run hello.z80
</code></pre>
<p>Serial output goes to stdout, so you can pipe it, diff it, test it. The <code>-T</code> flag sets a tick limit, which is useful for automated tests where you don't want a runaway loop eating your CI minutes:</p>
<pre><code class="language-plaintext">asm80-run -T 100000 hello.z80
</code></pre>
<p>For hardware configuration (memory layout, serial port addresses, peripherals) it reads <code>.emu</code> files, the same format the IDE uses. If you've already set up your project in the browser, the same config works here. Here's what a real <code>.emu</code> file looks like - this one comes from the IC80 test suite:</p>
<pre><code class="language-yaml">cpu: z80
memory:
  rom:
    from: '0x0000'
    to: '0x7FFF'
  ram:
    from: '0x8000'
    to: '0xFFFF'
serial:
  type: 6850
  mapped: port
  control: '0xDE'
  data: '0xDF'
</code></pre>
<p>The <code>cpu:</code> field tells the emulator which CPU to spin up: <code>z80</code>, <code>8080</code>, <code>6502</code>, <code>6809</code>, ... Everything else follows from that choice: the instruction decoder, the register set, the interrupt model.</p>
<p>The <code>memory:</code> block defines the address space layout. <code>rom:</code> marks the region that gets loaded from the hex file and is read-only at runtime. Writes to ROM addresses are silently ignored, just like on real hardware. <code>ram:</code> marks the read-write region. In this config, the lower 32K is ROM and the upper 32K is RAM, which is a classic split for small Z80 systems. The emulator doesn't care about gaps, if you only define ROM from <code>0x0000</code> to <code>0x3FFF</code> and RAM from <code>0xC000</code> to <code>0xFFFF</code>, everything in between is unmapped and reads back as <code>0xFF</code>.</p>
<p>The <code>serial:</code> section is where it gets useful for testing. <code>type: 6850</code> tells the emulator to simulate a Motorola 6850 ACIA, the most common serial chip in homebrew 8-bit designs. The <code>mapped: port</code> field means the ACIA is accessed through I/O port instructions (<code>IN</code>/<code>OUT</code> on Z80) rather than memory-mapped I/O. The <code>control:</code> and <code>data:</code> fields give the port addresses: <code>0xDE</code> for the status/control register and <code>0xDF</code> for the data register. When your code writes a byte to port <code>0xDF</code>, it shows up on stdout. When it reads the status register at <code>0xDE</code>, the emulator reports whether there's input waiting. This is exactly how real 6850 hardware works — the emulator just replaces the physical UART with a pipe to your terminal.</p>
<p>So when <code>asm80-run</code> executes your program and the code sends characters out through the emulated 6850, those characters land in your shell. That's what makes it possible to write tests that check actual program output: assemble, link, run, capture stdout, compare.</p>
<h2>All four in action: testing a C compiler</h2>
<p>Here's where it gets concrete. IC80 is a C compiler I'm working on that targets 8-bit CPUs. Its test suite (<code>run-test.ps1</code>) exercises the entire pipeline with all four tools:</p>
<ol>
<li><p>IC80 compiles a <code>.c</code> file to Z80 assembly</p>
</li>
<li><p><code>asm80</code> assembles the <code>.z80</code> source and the runtime (<code>crt0.z80</code>, <code>libc.z80</code>) into <code>.objz80</code> modules</p>
</li>
<li><p>The script generates a <code>.lnk</code> recipe and <code>asm80-link</code> links everything into a <code>.hex</code></p>
</li>
<li><p><code>asm80-run</code> loads the hex, runs the emulator with a tick limit, and the script checks the serial output against expected results</p>
</li>
</ol>
<p>The tick limit matters here. Each test case gets 100,000 ticks, enough to run any reasonable C test program to completion, but short enough that an infinite loop bails out in milliseconds instead of hanging the build. If the output doesn't match, the test fails. If the emulator hits the tick limit before the program terminates, the test also fails.</p>
<p>The whole thing runs in a few seconds. No browser, no GUI, no clicking. Just a script that compiles C, assembles Z80, links, runs, and tells you if the output is correct. That's the kind of thing <code>asm80-cli</code> was built for.</p>
]]></content:encoded></item><item><title><![CDATA[Live Coding in ASM80: run your assembly without leaving the editor]]></title><description><![CDATA[Here's a Z80 loop. Open ASM80, paste it in, and just watch.
        ; <LIVESTART maxT=500000>
        ; <SEED A=0x00, B=10, HL=0x4000>
loop:
        add  a, b
        djnz loop  ; <TRACE A, B>
       ]]></description><link>https://blog.asm80.com/live-coding-in-asm80-run-your-assembly-without-leaving-the-editor</link><guid isPermaLink="true">https://blog.asm80.com/live-coding-in-asm80-run-your-assembly-without-leaving-the-editor</guid><category><![CDATA[Assembly]]></category><category><![CDATA[General Programming]]></category><category><![CDATA[webdev]]></category><dc:creator><![CDATA[Martin Malý]]></dc:creator><pubDate>Sat, 28 Mar 2026 08:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69d69909707c1ce7686b950f/0ad99bec-b371-4426-9651-8e7df9aeec3a.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Here's a Z80 loop. Open ASM80, paste it in, and just watch.</p>
<pre><code class="language-z80">        ; &lt;LIVESTART maxT=500000&gt;
        ; &lt;SEED A=0x00, B=10, HL=0x4000&gt;
loop:
        add  a, b
        djnz loop  ; &lt;TRACE A, B&gt;
        ; &lt;LIVESTOP A, B, HL&gt;
</code></pre>
<p>See the text after the arrows? You didn't type that. It showed up on its own (grey, slightly translucent, sitting right next to your code) the moment your fingers stopped moving. That's ghost text, and it's the result of the IDE assembling your source, spinning up a Z80 emulator, and running the whole thing in the background. Change <code>B=10</code> to <code>B=5</code> and the numbers update before you finish reaching for the next key.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d69909707c1ce7686b950f/e1884c76-3891-4770-ae94-1dc6fee7f7aa.png" alt="" style="display:block;margin:0 auto" />

<p>That's Live Coding. Let me walk through what's actually going on.</p>
<h2>The example, line by line</h2>
<p><code>LIVESTART</code> opens a live coding session. The <code>maxT=500000</code> parameter sets a T-state budget; the emulator will stop after half a million T-states regardless of where it is. This prevents runaway loops from locking up the IDE. Default is 1000, which is fine for short snippets but too tight for anything with a real loop.</p>
<p><code>SEED</code> injects values into CPU registers before execution begins. Here I'm setting A to zero, B to 10 (the loop counter for DJNZ), and HL to 0x4000. Every register you don't mention keeps its default (zero on session start, or whatever value it had if you're mid-session). You can use decimal or <code>0x</code> hex notation — <code>B=10</code> and <code>B=0x0A</code> are the same thing.</p>
<p>The loop body is two instructions: <code>ADD A, B</code> adds B into A, and <code>DJNZ loop</code> decrements B and jumps back if B isn't zero. Classic Z80 pattern.</p>
<p><code>TRACE</code> on the DJNZ line captures the CPU state every time the program counter passes through that address. Since this is inside a loop, the ghost text shows aggregated results: <code>10×</code> means the emulator passed through 10 times, <code>avg 44 T/cycle</code> is the average T-state cost per iteration, and <code>A:0A B:00</code> are the final values when the loop exited.</p>
<p><code>LIVESTOP</code> ends the session. The ghost text here shows total T-states for the entire session and the final values of any registers or addresses you listed. The register list is optional — <code>; &lt;LIVESTOP&gt;</code> alone is valid, it just won't show register values in the ghost text.</p>
<h2>How the machine works</h2>
<p>Every time you change a character in your source, the IDE kicks off a pipeline: parse, assemble, load into emulator, run from LIVESTART to LIVESTOP (or until maxT is exhausted, or an ASSERT fails). The results get painted as ghost text next to each annotation. The whole cycle typically finishes in single-digit milliseconds for short programs, so it genuinely feels instant.</p>
<p>If your edit introduces an assembly error — misspelled mnemonic, bad operand, unresolved label — the IDE can't produce a binary, so it can't run the emulator. But it doesn't blank out the ghost text. Instead, the text stays in place and turns grey, meaning "this is stale, from the last successful run." You can still read it while you fix your typo. The moment the source assembles cleanly again, the ghost text goes live with fresh values.</p>
<p>You can have multiple live sessions in a single file. Each LIVESTART/LIVESTOP pair is independent. They can't overlap or nest — that's a hard rule — but otherwise you can scatter them through a file to test different sections of your code in isolation.</p>
<p>A note about LIVESTOP: it's optional. If you omit it, the session runs until it exhausts its T-state budget or hits a failing ASSERT. This is fine for quick experiments, but for anything you want to keep, an explicit LIVESTOP makes the intent clearer.</p>
<h2>SEED, MEMSTATE, and deterministic runs</h2>
<p>Assembly code doesn't exist in a vacuum. Your registers hold values, your memory contains data, your I/O ports have state. If you want Live Coding to show you meaningful results, you need to tell it what state to start from. That's what SEED, MEMSTATE, and PORTSTATE are for.</p>
<p>SEED handles registers:</p>
<pre><code class="language-z80">        ; &lt;SEED A=0xFF, B=10, HL=0x4000, SP=0xFFFF&gt;
</code></pre>
<p>MEMSTATE writes bytes into RAM at a specific address:</p>
<pre><code class="language-z80">        ; &lt;MEMSTATE 0x4000: FF 00 AB CD&gt;
        ; &lt;MEMSTATE *: 21 FF FF 3E&gt;
</code></pre>
<p>The <code>*</code> address means "continue from where the previous MEMSTATE left off." So if the first line wrote four bytes starting at 0x4000, the second line writes four more bytes starting at 0x4004. Handy for setting up longer data blocks without counting addresses by hand.</p>
<p>PORTSTATE writes to the I/O port address space:</p>
<pre><code class="language-z80">        ; &lt;PORTSTATE FE: BF&gt;
</code></pre>
<p>This puts 0xBF at port 0xFE; useful if your code reads from hardware and you want to simulate a particular input.</p>
<p>Placement matters. If SEED or MEMSTATE appears in the header — before the first actual instruction — it sets the initial state for the session. If it appears inline, mid-code, it acts as a one-shot injection: the emulator applies it exactly when the program counter passes through that address. This lets you simulate things like "at this point in execution, new data arrives on port 0xFE."</p>
<p>The reason all of this matters: deterministic runs. If every piece of state is explicitly set, the results are reproducible. Same source, same ghost text, every time. No "it works on my machine" surprises, because there's no ambient machine state to interfere.</p>
<h2>How to insert annotations</h2>
<p>You don't need to memorize any of this syntax. Type <code>; &lt;</code> in the editor and autocomplete shows you every available annotation — TRACE, SEED, EXPECT, MEMSTATE, all of them — with parameter hints. Pick one, fill in the arguments, done.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d69909707c1ce7686b950f/f8e4d246-df06-4244-98f1-701dc504eedb.png" alt="" style="display:block;margin:0 auto" />

<p>Or right-click anywhere in the editor and select "Insert Live Annotation" from the context menu. The IDE writes the <code>; &lt;...&gt;</code> wrapper for you.</p>
<p>The annotations live inside comments, so your assembler ignores them completely. They're metadata for the IDE, not instructions for the CPU. Your assembled binary is identical whether annotations are present or not.</p>
<h2>Ghost diff and the hover panel</h2>
<p>Run that first example again, then change <code>B=10</code> to <code>B=12</code>. The ghost text updates, and here's what you'll notice: the values that changed since the previous run light up in amber. Values that stayed the same are dimmed. At a glance, you can see exactly what your edit affected.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d69909707c1ce7686b950f/4a105a58-1235-4084-a14f-1690f57381b1.png" alt="" style="display:block;margin:0 auto" />

<p>This is surprisingly useful when you're tweaking loop bounds or modifying an algorithm. You don't need to remember what the values were before — the color tells you what moved.</p>
<p>Hover over any ghost text and a panel pops up showing the complete register file at that snapshot: A, F, B, C, D, E, H, L, the shadow registers, IX, IY, SP, PC, flags — everything the CPU holds. You don't need to add every register to your TRACE annotation; just hover when you need the full picture.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d69909707c1ce7686b950f/248d6b6b-72ed-487c-b6c0-60bf0595ffee.png" alt="" style="display:block;margin:0 auto" />

<p>For TRACE inside a loop, the ghost text also includes the iteration count and average T-states per cycle. This is a quick way to check loop performance without manual counting: if you expect 10 iterations at roughly 44 T per cycle and the ghost text says <code>10× avg 44 T/cycle</code>, your mental model matches reality.</p>
<h2>Assertions: EXPECT and ASSERT</h2>
<p>Ghost text shows you what happened. Assertions tell the IDE what <em>should</em> happen.</p>
<pre><code class="language-z80">        ; &lt;LIVESTART maxT=100000&gt;
        ; &lt;SEED HL=0x4000&gt;
        ; &lt;MEMSTATE 0x4000: 01 02 03 04&gt;

        ld   a, (hl)         ; A = 01
        add  a, a            ; A = 02
        ld   (hl), a         ; (0x4000) = 02

        ; &lt;EXPECT A=0x02&gt;                      ● green
        ; &lt;MEMEXPECT 0x4000: 02 02 03 04&gt;      ● green
        ; &lt;ASSERT A=0x02&gt;
        nop ; &lt;LIVESTOP A&gt;        → [18 T]  A:02
</code></pre>
<p>EXPECT is a soft check. If the condition holds, you get a green dot in the gutter. If it doesn't, a red dot and ghost text showing what the actual value was. Either way, the emulator keeps running. ASSERT is the hard version: if the condition fails, emulation halts right there, the line background tints red, and the ghost text shows you the mismatch.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d69909707c1ce7686b950f/dbeb21c9-b48b-4256-923a-7dbf1f96e4ff.png" alt="" style="display:block;margin:0 auto" />

<p>MEMEXPECT and MEMASSERT do the same thing for memory contents. You specify an address and expected bytes (up to 8):</p>
<pre><code class="language-z80">        ; &lt;MEMEXPECT 0x4000: 02 02 03 04&gt;
</code></pre>
<p>If any byte differs, the failing bytes are highlighted in red in the ghost text, giving you a byte-level diff. No guessing which part of your memory block went wrong.</p>
<p>PORTEXPECT and PORTASSERT complete the picture for I/O ports:</p>
<pre><code class="language-z80">        ; &lt;PORTEXPECT 0x10: FF&gt;
        ; &lt;PORTASSERT 0x10: FF&gt;
</code></pre>
<p>The practical difference between EXPECT and ASSERT comes down to this: scatter EXPECTs liberally through your code as sanity checks. Use ASSERT for the one invariant that, if violated, means nothing downstream makes sense. EXPECT says "I'd like this to be true." ASSERT says "if this isn't true, stop wasting cycles."</p>
<h2>ON N: targeting a specific loop pass</h2>
<p>Sometimes you don't care about every pass through a loop. You want the CPU state on exactly the 5th iteration and nothing else. That's <code>ON N</code>:</p>
<pre><code class="language-z80">        ; &lt;LIVESTART maxT=500000&gt;
        ; &lt;SEED B=10&gt;
loop:
        inc  a
        ; &lt;ON 5 TRACE A, B&gt;
        ; &lt;ON 10 EXPECT A=0x0A&gt;
        djnz loop
        ; &lt;LIVESTOP&gt;
</code></pre>
<p><code>ON 5 TRACE A, B</code> fires only on the 5th time the program counter hits that address. Every other pass is ignored. The ghost text shows a single-pass snapshot, not an aggregate.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d69909707c1ce7686b950f/ce25c281-50ea-436f-a966-6c5069ba2ba6.png" alt="" style="display:block;margin:0 auto" />

<p>Combine it with EXPECT and you've got a targeted assertion: "on iteration 10, A must be 0x0A." If your loop logic goes wrong on a specific pass, you'll catch it without wading through all the other iterations.</p>
<p>One edge case: if the loop runs fewer times than your ON target, the ghost text tells you so — something like "loop ran only 7 times" — instead of silently showing nothing. This is helpful when you're debugging early-exit conditions and the loop terminates sooner than expected.</p>
<h2>All supported CPUs</h2>
<p>Everything I've described works identically across every CPU ASM80 supports. The annotation syntax is the same, only the register names change to match the target architecture.</p>
<p>A SEED for a 6502 file uses <code>A</code>, <code>X</code>, <code>Y</code>, <code>SP</code>. A SEED for a 6809 uses <code>A</code>, <code>B</code>, <code>X</code>, <code>Y</code>, <code>U</code>, <code>S</code>, <code>DP</code>. The annotations adapt; your workflow doesn't change.</p>
<h2>What this actually is</h2>
<p>Live Coding isn't a debugger. There's no stepping, no breakpoints, no "run to cursor." It's closer to having unit tests embedded directly in your source code that execute continuously as you type. Each annotation is either an observation ("show me A and B here") or an expectation ("A must be 0x02 here"). The feedback loop is measured in milliseconds, not in "save, switch to terminal, run, squint at hex dump."</p>
<p>The annotations stay in your source. They're comments, so they survive assembly, survive version control, survive being shared with someone else. If another person opens your file in ASM80, they see the same ghost text, the same green dots, the same assertions. It's documentation that verifies itself.</p>
<p>Change a constant and every annotation in the file re-evaluates instantly. You know before you even move to the next line whether your code still does what you think it does. And if it doesn't, the ghost text is right there telling you what it actually did instead.</p>
<p>That's the whole point: write code, state what you expect, see the results. If the ghost text agrees with your head, keep going. If it doesn't, you just learned something, and you learned it before you even left the line.</p>
]]></content:encoded></item><item><title><![CDATA[Running a 1976 Computer in Your Browser]]></title><description><![CDATA[I built a KIM-1 emulator that runs inside ASM80. Here is how, and why, and which parts fought back.
The Machine That Started Everything
In 1975, MOS Technology had a problem. They had designed the 650]]></description><link>https://blog.asm80.com/running-a-1976-computer-in-your-browser</link><guid isPermaLink="true">https://blog.asm80.com/running-a-1976-computer-in-your-browser</guid><category><![CDATA[kim 1]]></category><category><![CDATA[rriot]]></category><category><![CDATA[6502]]></category><category><![CDATA[Emulation]]></category><category><![CDATA[hardware]]></category><category><![CDATA[retro]]></category><dc:creator><![CDATA[Martin Malý]]></dc:creator><pubDate>Sun, 22 Mar 2026 08:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69d69909707c1ce7686b950f/24034db7-5139-43e0-a760-fa5f0514eb89.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I built a KIM-1 emulator that runs inside <a href="https://beta.asm80.com">ASM80</a>. Here is how, and why, and which parts fought back.</p>
<h2>The Machine That Started Everything</h2>
<p>In 1975, MOS Technology had a problem. They had designed the 6502 — a CPU that could do most of what the Motorola 6800 did, at a fraction of the price — but nobody believed them. Chuck Peddle, who led the 6502 design team, understood that a datasheet alone wouldn't sell chips. Engineers needed to touch the thing, to run code on it, to prove to themselves it wasn't vaporware. So MOS built the KIM-1: a single-board computer with a hex keypad, six seven-segment LED displays, and one kilobyte of RAM. They released it in 1976 for $245.</p>
<p>Two hundred forty-five dollars. Adjusted for inflation, that's roughly a thousand dollars today — not cheap in an absolute sense, but in 1976 a comparable development system from Motorola or Intel would run you several thousand. The KIM-1 wasn't marketed as a personal computer. It was a development and evaluation board, a way to learn the 6502, a tool for engineers who wanted to prototype embedded systems without committing to a full product design. But something unplanned happened: hobbyists bought it. Radio clubs bought it. Students bought it. People who had no business buying a single-board computer bought one anyway, because for the first time you could afford to own a real CPU and actually program it.</p>
<p>What could you do with 1K of RAM? More than you'd think, and less than you'd want. The KIM-1's monitor ROM — the firmware that handled the keypad, the display, the cassette tape interface — lived in two 6530 RRIOT chips, outside of that 1K. So your full kilobyte from \(0000 to \)03FF was yours. People wrote games, small control programs, music generators, even a chess program (though that one needed expansion memory). The KIM-1 had an expansion connector, and a small industry grew up around selling additional RAM boards — 4K, 8K, eventually 32K and 64K. With enough expansion, you could run a real operating system. But the stock machine, with its hex keypad and its six LED digits, was where most people started.</p>
<p>Programs were entered by hand, one byte at a time, through the keypad. You'd look at a printed listing — often from a magazine or a user group newsletter — and punch in hex codes. If you were lucky, you had a cassette tape interface and could save your work. If you were very lucky, you had a teletype or a serial terminal and could use the KIM-1's paper tape format to load programs faster. The machine was tedious to use and completely transparent. There was no operating system, no abstraction layer, nothing between you and the hardware. That's exactly what made it a brilliant teaching tool, and exactly what makes it satisfying to emulate fifty years later.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d69909707c1ce7686b950f/5f19b0e9-18a6-48a7-9473-df65f49c8427.jpg" alt="" style="display:block;margin:0 auto" />

<h2>Why Emulate This Now?</h2>
<p>Modern computers are magnificent and opaque. You can spend years programming one without ever understanding what the CPU is actually doing. The KIM-1 is the opposite: every clock cycle is accountable, every memory access is visible, every I/O operation maps to a physical wire. When you press a key on the hex keypad, the monitor ROM scans the keyboard matrix by writing specific bit patterns to a port, then reads another port to see which key is down. When a digit appears on the LED display, it's because the ROM wrote a segment pattern to one port and a digit-select pattern to another, and it's doing this hundreds of times per second, cycling through all six digits fast enough that your eyes see a steady image.</p>
<p>There's no display driver, no keyboard interrupt, no HAL. Just a CPU, two I/O chips, and a clever piece of firmware. You can trace the entire path from keypress to screen update in your head, or in a debugger, without hitting any abstraction boundary you can't cross. For someone learning how computers actually work — not how to use them, but how they work — there is no better starting point than a machine this simple.</p>
<p>That's why I built this emulator. Not for nostalgia (I don't have any — I never owned a KIM-1), but because it's the clearest possible demonstration of what a computer does at the hardware level. And because the ASM80 IDE already has a 6502 assembler, so the toolchain was sitting right there, waiting.</p>
<h2>The ROM Patch Trick</h2>
<p>The KIM-1 communicates with a serial terminal through bit-banging. The 6502 runs at 1 MHz, and the serial routines in the monitor ROM toggle I/O pins in carefully timed loops to produce the right baud rate. This works on real hardware. It is a nightmare to emulate accurately.</p>
<p>The problem isn't the 6502 emulation — ASM80's CPU core handles that fine. The problem is timing. On a real KIM-1, the CPU executes one instruction per microsecond (give or take), and the serial routines count cycles to produce precise bit timing at 110 baud or 300 baud or whatever rate the user has configured. In an emulator, the CPU runs inside a JavaScript event loop, driven by an audio callback (more on that later), and the relationship between emulated cycles and real-world time is approximate at best. Bit-banging serial at the wrong speed produces garbage.</p>
<p>The classic solution is to patch the ROM. The KIM-1 monitor has two key entry points: GETCH at \(1E5A (read a character from the serial port) and OUTCH at \)1EA0 (write a character to the serial port). Each of these is a substantial subroutine that handles start bits, stop bits, timing loops, and error checking. In the emulator, we replace them with something much simpler.</p>
<p>GETCH becomes: <code>LDA \(BFFE; RTS</code>. Four bytes. Instead of bit-banging a serial line, the CPU just reads from a magic address. The emulator watches for reads from \)BFFE and returns whatever character the user typed into the browser-based serial terminal.</p>
<p>OUTCH becomes: <code>STA $BFFF; RTS</code>. Another four bytes. The CPU writes a character to a magic address, the emulator catches it and appends the character to the terminal output.</p>
<p>There's one more piece to this. The original serial routines use two zero-page variables — \(EF and \)F0, called CNTH30 and CNTL30 — to store the baud rate timing constants. If these aren't initialized, the monitor hangs on boot trying to auto-detect the baud rate. So the emulator pre-seeds \(EF with 0x07 and \)F0 with 0x27 before the first reset. The values correspond to 300 baud, but it doesn't matter what they are — the patched GETCH and OUTCH never use them. They just need to be non-zero so the auto-baud code doesn't spin forever.</p>
<p>The elegance of this approach is that it's invisible to user code. Any program that calls GETCH or OUTCH through the standard entry points gets the patched versions. Any program that does its own bit-banging will not work, but such programs are rare, and they wouldn't work in an emulator anyway without cycle-accurate serial timing.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d69909707c1ce7686b950f/04c93f4a-5528-47e3-b711-658993f40a0d.png" alt="" style="display:block;margin:0 auto" />

<h2>The RIOT Chip and the Display/Keyboard Multiplexing</h2>
<p>This was the hardest part of the whole project.</p>
<p>The KIM-1 uses two MCS 6530 RRIOT chips. "RRIOT" stands for RAM, ROM, I/O, Timer — each chip contains 64 bytes of RAM, 1K of ROM, two 8-bit I/O ports, and a programmable timer. The two RRIOTs are called RIOT-002 and RIOT-003, and between them they handle all the I/O: the keyboard, the LED display, the cassette tape interface, and the serial port (before patching).</p>
<p>The seven-segment displays and the keyboard share the same I/O port. This sounds insane, and it kind of is, but it made sense in 1976 when every chip cost money. Here's how it works: Port B on RIOT-002 has a Data Direction Register (DDR) that controls which bits are outputs and which are inputs. The monitor ROM constantly cycles through different DDR values. When the DDR is set to drive a particular digit of the display, the ROM writes the corresponding segment pattern to Port A, and that digit lights up. When the DDR is set to scan a particular row of the keyboard, the ROM reads Port A to see which keys are pressed.</p>
<p>In the emulator, we intercept writes to the Port B DDR and decode which operation the monitor is requesting. The formula is: <code>segment = ((ddrB &gt;&gt; 1) &amp; 0x0F) - 4</code>. The result tells us what the monitor wants.</p>
<p>If segment is 0 through 5, we're refreshing one of the six LED digits. The value written to Port A is the segment pattern, and we update the corresponding display element.</p>
<p>If segment is -1, the monitor is trying to read the cassette tape input. We inject 0xFF into Port A (or the current tape bit, if a tape is loaded).</p>
<p>If segment is -2, -3, or -4, the monitor is scanning one of three keyboard rows. Each row covers a different set of keys. Row 0 (segment -4) handles the hex digits 0 through 6. Row 1 (segment -3) handles 7 through D. Row 2 (segment -2) handles the function keys: PC, GO, +, DA, AD, F, and E.</p>
<p>The keyboard encoding is active-low, which means a pressed key is represented by a 0 bit, not a 1. If the user presses key "5" on the hex keypad, and the monitor is scanning row 0, we need to inject a value where bit 5 is zero and all other bits are one. The formula is <code>(row ^ 0x7F) | tapeBit</code> — we XOR the row value with 0x7F to flip all the key bits to their active-low form, then OR in the tape input bit on bit 7.</p>
<p>Getting this right took more debugging time than everything else combined. The multiplexing happens at high speed — the monitor cycles through all display digits and keyboard rows many times per second — and any mistake in the DDR decoding, the port injection, or the active-low encoding produces symptoms that look like random garbage on the display or phantom keypresses. The kind of bug where you stare at a logic analyzer trace (well, a console.log trace) for an hour before you notice that you're shifting right by one when you should be shifting right by one and then masking.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69d69909707c1ce7686b950f/e861d6b3-c080-4f21-8b39-41a1e430819d.png" alt="" style="display:block;margin:0 auto" />

<h2>Cassette Tape as a List of Numbers</h2>
<p>The real KIM-1 stored programs on audio cassette tape using a variant of the Kansas City Standard. The monitor ROM toggled an output pin at specific frequencies to encode ones and zeros, and read an input pin to detect those frequencies during playback. This is frequency-shift keying (FSK), and accurately emulating it would require modeling audio waveforms, sample rates, and signal detection thresholds. None of which sounded like fun.</p>
<p>Instead, the emulator records cassette data as a list of T-state intervals. When the monitor toggles the tape output bit (PA7 on RIOT-002), the emulator records how many CPU cycles have elapsed since the last toggle. The result is an array of numbers: <code>[412, 413, 207, 206, 412, ...]</code>. Each number represents the time between two successive bit transitions, measured in clock cycles. This captures the timing information that encodes the data without dealing with audio frequencies at all.</p>
<p>Playback works in reverse. The emulator keeps a pointer into the interval array and a cycle counter. Each time the monitor reads the tape input bit, the emulator checks whether enough cycles have elapsed to advance to the next transition. If so, it flips the bit and moves the pointer forward. The monitor's tape-reading routine sees exactly the same timing it would see from a real cassette, because we're replaying the exact cycle counts.</p>
<p>This approach throws away the audio entirely and keeps only the information content. You can't listen to the tape (there's nothing to listen to), but you can save it to a file, load it back, and it works every time. No wobble, no dropout, no "please adjust the volume knob on your tape recorder." The 1970s experience, minus the 1970s frustration.</p>
<h2>AudioContext as a Clock</h2>
<p>An emulator needs a clock. The CPU has to execute instructions at a consistent rate, and the display has to update smoothly, and everything has to stay in sync with real time. In a browser, you have three options: <code>setTimeout</code>, <code>requestAnimationFrame</code>, or the Web Audio API. The first two are unreliable — the browser can throttle them, delay them, or skip them entirely when the tab is in the background. The Web Audio API, specifically <code>ScriptProcessorNode</code>, is the one thing browsers take seriously about timing, because audio glitches are audible and users complain.</p>
<p>The emulator creates an AudioContext at 48 kHz and a ScriptProcessorNode with a buffer size of 2048 samples. Every time the audio system needs a new buffer, it fires a callback. At 48 kHz, 2048 samples is about 42.67 milliseconds — roughly 24 frames per second. Each callback runs the CPU for the appropriate number of cycles (about 42,670 at 1 MHz), then updates the display.</p>
<p>The display update has a subtle trick. The KIM-1's LEDs are multiplexed — the monitor ROM lights one digit at a time, cycling through all six fast enough that they appear steady. In a 42ms frame, the monitor might refresh each digit several times, or it might not get to all of them (if the user's code is doing something time-consuming). The emulator handles this with afterglow: if a digit wasn't refreshed during the current frame, its last value is preserved on screen. If a digit was written with a null pattern (all segments off), it keeps its previous value. This mimics the phosphor-like persistence of real LED displays — not because LEDs have phosphor, but because the human eye retains the image of a briefly-lit digit for a few tens of milliseconds. The effect is the same. Without afterglow, the display flickers horribly. With it, the digits appear steady, exactly as they do on a real KIM-1.</p>
<p>Yes, <code>ScriptProcessorNode</code> is deprecated. Yes, I should migrate to <code>AudioWorklet</code>. It's on the list. The deprecated API works in every browser today, and when it stops working, I'll deal with it. Premature migration is the root of all... well, not evil, but definitely wasted weekends.</p>
<h2>SST Mode</h2>
<p>The KIM-1 has a hardware single-step mode, controlled by a physical toggle switch on the board. When SST is on, the CPU executes one instruction, then immediately takes a Non-Maskable Interrupt (NMI). The NMI vector points into the monitor ROM, which displays the current registers and waits for the user to press GO to execute the next instruction.</p>
<p>On the real hardware, this works because the SST switch is wired to the NMI line through some edge-detection logic. After each instruction completes, if the switch is in the SST position, the hardware triggers an NMI.</p>
<p>In the emulator, the entire mechanism is one line of JavaScript. After each instruction executes, if the SST flag is set, call <code>cpu.nmi()</code>. That's it. The monitor ROM handles everything else — saving registers, displaying them on the LEDs, waiting for GO — exactly as it does on the real machine. The ROM doesn't know or care that the NMI came from a JavaScript boolean instead of a physical switch. An NMI is an NMI.</p>
<p>The toggle switch itself is rendered as an HTML checkbox, injected into the SVG front panel image via a <code>&lt;foreignObject&gt;</code> element. It sits right where the real switch would be. Click it on, and every instruction triggers NMI. Click it off, and the CPU runs free. The checkbox maps to a boolean, the boolean maps to an NMI call, and the original 1976 hardware design just... works, unchanged, fifty years later, in a browser.</p>
<p>This was the easiest part of the whole project. I kept waiting for it to be more complicated. It wasn't.</p>
<h2>MOS Papertape Format</h2>
<p>Before there were hex files (Intel HEX, Motorola S-records), MOS Technology defined their own format for storing programs on paper tape. The KIM-1 uses it for loading and saving programs through the serial port, and ASM80 supports it for uploading code to the emulator.</p>
<p>Each record starts with a semicolon and has the format: <code>;LLAAAADDDD...DDSS</code>. <code>LL</code> is the byte count (how many data bytes follow). <code>AAAA</code> is the starting address, big-endian. Then come the data bytes, each as two hex digits. Finally, <code>SS</code> is a 16-bit checksum, stored big-endian with the high byte first.</p>
<p>The checksum is the sum of the byte count, the address high byte, the address low byte, and all the data bytes, modulo 65536. It's stored in two bytes, high byte first — which is the opposite of the 6502's native little-endian byte order. I assume this made sense to someone at MOS Technology in 1976. Maybe they were Motorola alumni. Maybe they just liked big-endian checksums. I didn't ask.</p>
<p>The end-of-file record is always <code>;0000040001</code>. Zero data bytes, address \(0004, checksum \)0001. Why address \(0004? It's the reset vector for the monitor — pressing GO jumps through \)0004. The checksum is \(0001 because 0x00 + 0x00 + 0x04 = 0x0004... wait, that's \)0004, not \(0001. The end record is actually a special case; the checksum value \)0001 is a convention, not a computed value. The loader recognizes it as the terminator.</p>
<p>In the ASM80 IDE, you can load .pap files directly into the emulator's memory. The parser reads each record, validates the checksum, and writes the data bytes to the specified addresses. Saving works in reverse: select an address range, and the IDE generates .pap records with correct checksums. It's a simple format — no compression, no relocation, no metadata — and that simplicity is exactly why it's still easy to work with half a century later.</p>
<h2>Why It's Satisfying</h2>
<p>The KIM-1 is a machine simple enough to hold in your head. All of it. The CPU has 56 instructions, 13 addressing modes, and three registers that matter (A, X, Y plus the status flags). The address space is 64K, of which only 2K is ROM and 1K is RAM. The I/O is two chips with two ports each. The monitor firmware is about 2K of hand-written 6502 assembly. You can understand the whole thing — not superficially, not "I get the general idea," but actually understand every byte, every pin, every timing constraint.</p>
<p>That's why it was a teaching tool in 1976, and that's why it's still a teaching tool now. When you type a hex value on the keypad and watch it appear on the LED display, you can trace the entire path: the keyboard scan in the monitor ROM, the DDR writes to the RIOT, the port reads, the lookup table for the key code, the segment pattern write, the display multiplex cycle. There's nothing hidden. There's nothing you have to take on faith.</p>
<p>Modern computers are better at everything except being understandable. The KIM-1 is worse at everything except being transparent. And transparency, it turns out, is the one thing you can't get from a faster chip.</p>
<p>The emulator runs at <a href="https://beta.asm80.com">asm80.com</a>. Load a .pap file, flip the SST switch, step through the monitor ROM one instruction at a time, and watch a fifty-year-old design do its work. It's the same machine Chuck Peddle's team built to prove that their cheap little CPU was real. It's still proving it.</p>
]]></content:encoded></item></channel></rss>