How exactly did you "enter it" without a second computer though? Was there a simple test rig or something that you would make as an electronics project, where you would input opcodes and ascii and it would write it to a rom? Only real thing i can think of.
RE "...How exactly did you "enter it" without a second computer though...."
You would use the "monitor program" to poke hex data into memory locations, starting at a nominated RAM memory address. Needed to be done VERY CAREFULLY. one bad hex digit and your program likely crash.
After entering all the program into memory, you would use a monitor program , command to get tell the CPU to jump to a certain address and start executing a program there. OLD computer magazines often had such hex listings printed in them.
Old geek who was one of my nominal bosses when I first started had monitor he wrote that could do break points and single stepping on a Z80.
[−]holgerschurig · 2026-11-15 Sun 00:38 UTC ·
link
Can't tell it for 6502... but for Z80. Because was my first own "computer":
* a single Z80 (which was nicer than 8080, because the latter needed an extra clock IC)
* 256 bytes of static RAM
* 1 8bit D-Latch (Flipflop) with 8 LEDs
* 1 8bit D-Latch with 8 dip switches
* 2 8bit buffers that can go tristrate between the hex-thingies and the address & data bus
* 2 rotating hex thingies: you could rotate them between 0-f for data
* 2 rotating hex ones for address
Now, how did it work? I pulled nBUSRQ down (bus request). This made the Z80 go tri-state. It also made my buffers non-tristate.
Then I used 2 of the hex-thingies to set the address and 2 of them to set the data. Now I set nWR to low and that would write the entered data byte into the SRAM.
When I was done, I turned nBUSRQ back to high, so the Z80 again was the boss of the bus. And then I simply used nRESET to low to make the Z80 execute my program starting at address 0.
I did only tiny programs, like adder, some PWN/sound wave program, an traffic light simulator.
But, since I soldered this all by myself, I had no "monitor program" like the KIM-1 had. So I had to do all of that by hardware.
For later hardware like the TRS-80, the BASIC interpreter had an escape hatch: PEEK(addr), POKE addr, value, and USR1(). Basically you could read and write single bytes to RAM, and there was a magic 2-byte value which was a branch address that calling USR1() would jump into. So you could bootstrap some simple tooling that way. (I'd been writing hex machine code before I got any hardware, just pencil and graph paper, so it wasn't a big step to go from that to stuffing bytes into memory.) Of course there were tools available but actually paying for them was enough of a challenge that a lot of bootstrapping got done entirely anti-economically :-) (Once floppy drives and disk operating systems were a thing, you'd usually have a simple debugger where you could just type hex digits in, and load register values directly - so if you wanted to read raw disk sectors, you'd drop CD 75 46 18 FB somewhere, R DE track/sector, R HL where to read, R BC 0001 and then single step the code and it would read a single sector.)
(Note that while a lot of this was unjustifiable even then - my first real paying job in computers was down a step, working on a Z80-modded IMSAI so while most code was on 8" floppies, doing machine code in binary on toggle switches "just to fix something quick" was a legitimate skill :-)
The Apple II had a thing called the monitor, which was a tiny CLI program in ROM that you could invoke from the BASIC command line (CALL -151). With it you got the ability to disassemble memory (300L would spit out 6502 opcodes starting at address 768), set memory regions to sequences of hex digits (300: A9 21 20 FO FD 60 would write a tiny program on an Apple II Plus that printed an exclamation point, if my memory serves), and execute memory (300G).
With these three commands, and a working memory of the hex that corresponded to the opcodes, you could be reasonably productive writing short 6502 programs.
Fun and extremely niche fact: the typo CALL -161 jumped into the monitor, but only after the weird side effect of unhooking the sniffer that watched for DOS 3.3 commands sent via the ROM's text I/O. Not only did this make text print faster -- useful when you were visually dumping memory to the screen to find something quickly, but it also protected against accidentally destroying your disk if you unwittingly printed a sequence like ^M^DINIT HELLO^M, which would trigger the disk initialization routine.
> 300: A9 21 20 FO FD 60 would write a tiny program on an Apple II Plus that printed an exclamation point, if my memory serves.
Ancient assembly hacker tip: A JSR/RTS sequence can be simplified to a JMP. To continue your example, the bytes you provided decompile as follows (assuming Apple 2 monitor ROM at $F800):
So, you load the A register with #$21, which is the ASCII code for `!` in hexadecimal, and then call COUT1 which will output the character in the A register, printing the exclamation point. When COUT1 is finished, it will return to you using an RTS instruction, and then you'll return to your caller using another RTS instruction.
Since the call to COUT1 is a tail call, you can borrow its RTS instruction to return to your caller and save one instruction. So the following code is equivalent to yours but a little bit smaller and faster:
PRINTBANG:
A9 21 LDA #$21
4C F0 FD JMP COUT1
Notice that the JSR instruction has become a JMP.
Another related trick: When you have a routine that callers often want to call twice—say to print `!!` for extreme exclamations—you may be tempted to create a do-it-twice call:
But we can do even better! We can eliminate the jump and save three more bytes by placing our do-it-twice routine immediately before the routine it is calling:
When PRINTBANG2 is called, it calls PRINTBANG to print an exclamation point, but when PRINTBANG returns, it returns to PRINTBANG2, which promplty falls through to PRINTBANG, calling it again and printing a second exclamation point. This time, however, when PRINTBANG returns, it will return to the original caller.
Back when every byte mattered, these tricks were useful to know.
[−]flopsamjetsam · 2026-11-15 Sun 03:35 UTC ·
link
> CALL -151
I haven't seen that in a long time! Young me was too young to understand 2s complement arithmetic, or that addresses would wrap around. I just thought it was some kind of magic.
There was also a mini-assembler [0] that let you enter assembly code directly into memory. It was dead simple to use, and since it printed an assembly listing after you entered each instruction, it was conducive to learning the hex codes by heart.
The mini-assembler didn't understand symbols (other than the opcodes themselves), but it did understand the addressing modes. This saved you from, for instance, hand calculating branch offsets.
Not all Apple II models included it, but I can't find a reference right now. Ours had it.
Guess what? Apple brought back the mini-assembler when it released the "enhanced" version of the Apple //e in in 1985:
Monitor changes also included a return of the mini-assembler, absent since the days of Integer BASIC. It was activated by entering a “!” command in the Monitor, instead of a jump to a memory location as in the older Apple II. Features also added included the ability to enter ASCII characters directly into memory, and an “S” command to make it possible to search memory for a byte sequence. Interrupt handling was also improved. However, the “L” command to disassemble 6502 code still did not handle the new 65c02 opcodes as did the IIc disassembler.
In case of Wozniak, he had access to a commercial programmer at work.
Some people did burn the code into electrically programmable PROMs completely by hand, using just a power supply, some wires and switches. Anyone could do this at home, but it was extremely tedious even for a tiny 256 byte chip.
You'd hand write most of the code, type it into the assembler and that could then create what was effectively a machine code binary which you'd save to tape.
With small stuff you could run the code and drop back out to the OS. Generally you'd load and run the binary to test it and then back to loading the editor and source code, edit that, reassemble. Repeat ad infinitum.
Much of the ZX81 and Spectrum code I wrote was initially done on pen & paper and developed like that.
When I got my first in-house job in 1984 they were using TRS80s for editing and assembling which also had a custom hardware link to transfer the binary to the Spectrum.
By the time we got onto Atari & Amigas there was much better custom hardware, such as SNASM, which also facilitated remote debugging from a PC.
You would use the "monitor program" to poke hex data into memory locations, starting at a nominated RAM memory address. Needed to be done VERY CAREFULLY. one bad hex digit and your program likely crash. After entering all the program into memory, you would use a monitor program , command to get tell the CPU to jump to a certain address and start executing a program there. OLD computer magazines often had such hex listings printed in them.
* a single Z80 (which was nicer than 8080, because the latter needed an extra clock IC) * 256 bytes of static RAM * 1 8bit D-Latch (Flipflop) with 8 LEDs * 1 8bit D-Latch with 8 dip switches * 2 8bit buffers that can go tristrate between the hex-thingies and the address & data bus * 2 rotating hex thingies: you could rotate them between 0-f for data * 2 rotating hex ones for address
Now, how did it work? I pulled nBUSRQ down (bus request). This made the Z80 go tri-state. It also made my buffers non-tristate.
Then I used 2 of the hex-thingies to set the address and 2 of them to set the data. Now I set nWR to low and that would write the entered data byte into the SRAM.
When I was done, I turned nBUSRQ back to high, so the Z80 again was the boss of the bus. And then I simply used nRESET to low to make the Z80 execute my program starting at address 0.
I did only tiny programs, like adder, some PWN/sound wave program, an traffic light simulator.
But, since I soldered this all by myself, I had no "monitor program" like the KIM-1 had. So I had to do all of that by hardware.
(Note that while a lot of this was unjustifiable even then - my first real paying job in computers was down a step, working on a Z80-modded IMSAI so while most code was on 8" floppies, doing machine code in binary on toggle switches "just to fix something quick" was a legitimate skill :-)
With these three commands, and a working memory of the hex that corresponded to the opcodes, you could be reasonably productive writing short 6502 programs.
Fun and extremely niche fact: the typo CALL -161 jumped into the monitor, but only after the weird side effect of unhooking the sniffer that watched for DOS 3.3 commands sent via the ROM's text I/O. Not only did this make text print faster -- useful when you were visually dumping memory to the screen to find something quickly, but it also protected against accidentally destroying your disk if you unwittingly printed a sequence like ^M^DINIT HELLO^M, which would trigger the disk initialization routine.
Ancient assembly hacker tip: A JSR/RTS sequence can be simplified to a JMP. To continue your example, the bytes you provided decompile as follows (assuming Apple 2 monitor ROM at $F800):
So, you load the A register with #$21, which is the ASCII code for `!` in hexadecimal, and then call COUT1 which will output the character in the A register, printing the exclamation point. When COUT1 is finished, it will return to you using an RTS instruction, and then you'll return to your caller using another RTS instruction.Since the call to COUT1 is a tail call, you can borrow its RTS instruction to return to your caller and save one instruction. So the following code is equivalent to yours but a little bit smaller and faster:
Notice that the JSR instruction has become a JMP.Another related trick: When you have a routine that callers often want to call twice—say to print `!!` for extreme exclamations—you may be tempted to create a do-it-twice call:
But now we know better and code it this way: But we can do even better! We can eliminate the jump and save three more bytes by placing our do-it-twice routine immediately before the routine it is calling: When PRINTBANG2 is called, it calls PRINTBANG to print an exclamation point, but when PRINTBANG returns, it returns to PRINTBANG2, which promplty falls through to PRINTBANG, calling it again and printing a second exclamation point. This time, however, when PRINTBANG returns, it will return to the original caller.Back when every byte mattered, these tricks were useful to know.
I haven't seen that in a long time! Young me was too young to understand 2s complement arithmetic, or that addresses would wrap around. I just thought it was some kind of magic.
The mini-assembler didn't understand symbols (other than the opcodes themselves), but it did understand the addressing modes. This saved you from, for instance, hand calculating branch offsets.
Not all Apple II models included it, but I can't find a reference right now. Ours had it.
[0] https://www.callapple.org/vintage-apple-computers/apple-ii/a... (1978)
Monitor changes also included a return of the mini-assembler, absent since the days of Integer BASIC. It was activated by entering a “!” command in the Monitor, instead of a jump to a memory location as in the older Apple II. Features also added included the ability to enter ASCII characters directly into memory, and an “S” command to make it possible to search memory for a byte sequence. Interrupt handling was also improved. However, the “L” command to disassemble 6502 code still did not handle the new 65c02 opcodes as did the IIc disassembler.
Source: https://www.apple2history.org/history/ah07/#07:~:text=Monito...
You go to a start address, switch to data mode, and just start plugging in hex…
Some people did burn the code into electrically programmable PROMs completely by hand, using just a power supply, some wires and switches. Anyone could do this at home, but it was extremely tedious even for a tiny 256 byte chip.
With small stuff you could run the code and drop back out to the OS. Generally you'd load and run the binary to test it and then back to loading the editor and source code, edit that, reassemble. Repeat ad infinitum.
Much of the ZX81 and Spectrum code I wrote was initially done on pen & paper and developed like that.
When I got my first in-house job in 1984 they were using TRS80s for editing and assembling which also had a custom hardware link to transfer the binary to the Spectrum.
By the time we got onto Atari & Amigas there was much better custom hardware, such as SNASM, which also facilitated remote debugging from a PC.