-
Notifications
You must be signed in to change notification settings - Fork 4
Home
In May 2021, David Lovett (that's me!) of the Usagi Electric YouTube channel began restoring a Centurion minicomputer. A typo during testing took on a life of its own and propagated across several YouTube channels related to retrocomputing and homebrewing.
"HELLORLD!" as a test phrase actually makes a lot of sense as it's 7-segment display friendly, really opening up the number of devices that it can run on. If you have "HELLORLD!" running on a unique machine, this Wiki is publicly editable, so feel free to add it!
However, a few rules:
- The program needs to be as low level as possible using just assembly or raw machine code through a memory monitor.
- We're really looking to highlight it running on real hardware that unique or rare.
With that said, scroll on down for links to "Hellorld!" on all sorts of epic stuff!
The full story on how this madness began!
Minicomputers are decidedly not "mini" by today's standards, but I just love seeing large scale systems run "Hellorld!"
Microcomputers changed the world, but now we get to change microcomputers by running "Hellorld!" on them!
Computer trainers taught us how to use smart rocks, and now they're teaching us how to "Hellorld!"
These ones are really special because everything was designed from the ground up! Even the code to run "Hellorld!"
Homebrews showcase some of the greatest ingenuity you'll see, and it's even cooler when they run "Hellorld!"
"Hellorld!" can be run on so, so much - calculators, trainers, multimeters, if it has a display, let's get "Hellorld!" on it!
These are implementations that are almost there. Perhaps you can lend a helping hand!
The very first code written for the Centurion in probably 30 years was a simple "Hello World!" program that phire wrote in pure hexadecimal for me to type directly in using the Test Operating System. Unfortunately, when I was typing in the program, I got the location of my "o" confused, and after I typed in the first "o" from "Hello," I picked up starting after the second "o" in "World!" The result was "Hellorld!"
I think it's simultaneously beautiful and hilarious that the first code written for the Centurion in so long had a bug in it. But, I love it all the same and "Hellorld!" is the new gold standard in my book!
The code is incredibly simple:
\M1000 79 86 23 C8 E5 EC EC EF F2 EC E4 A1 8D 8A 00 71 80 01
\G1000
Let's break it down!
-
0x79 86 23 = JSR 0x8623
- This is a Jump Subroutine direct to the address of 0x8623. That particular location is where the write string subroutine of the Test Operating System lives.
-
0xC8 E5 EC EC EF F2 EC E4 A1 8D 8A 00 = The ASCII for "Hellorld!".
- It is important to note that the ASCII is just 7-bits, and the 8th bit, which would normally be parity, is set to 1. So, each hex byte needs to have 80 subtracted from it to get the real 7-bit ASCII value. For example. C8 - 80 = 48 which is "H" in 7-bit ASCII.
- The final two ASCII characters "8D and 8A" before the "00" are a carriage return and a line feed. The "00" is the null terminator to let the subroutine know it's time to wrap things up.
-
0x71 80 01 = JMP 0x8001
- This is a simple Jump direct to the address of 0x8001, which is the beginning of the of the DIAG board ROM program.
However, we're using a print to screen routine that already exists on the DIAG board. We can do better!
TITLE 'HELLORLD'
ZHELLORLD BEGIN X'1000'
ENTRY JSR PRINTNULL ; Jump relative to PC (within 128, so ok)
DW X'8D8A'
DC 'HELLORLD!'
DB 0
HLT
PRINTNULL LDAB= B'00000010' ; Set mask to check for tx buffer empty
XAYB ; AL -> YL
PNLOOP LDBB+ X+ ; Load the next byte
BZ PNEND ; If 0, we are done
PNWAIT LDAB/ X'F200' ; AL = MUX status byte
ANDB YL,AL ; Check if transmit buffer empty
BZ PNWAIT ; If not empty, loop
STBB/ X'F201' ; Store the character to the MUX data
JMP PNLOOP ; Go to the next character
PNEND RSR
END ENTRY ; Set the entry point
Which breaks down into pure hexadecimal as:
7B 0D 8D 8A C8 C5 CC CC CF D2 CC C4 A1 00 00 80 02 4C C5 41 14 0C 81 F2 00 42 71 14 F9 E1 F2 01 73 F0 09
Let's break it down and figure out what's going on!
TITLE 'HELLORLD'
-
ZHELLORLD BEGIN X'1000'
- This doesn't actually get broken down into machine code, this is just preliminary information for the CPU6 assembler. It tells it what the title of the program (and subsequent executable) will be and where it's going to live in memory.
-
ENTRY JSR PRINTNULL ; Jump relative to PC (within 128, so ok)
- Labels live on the far left and this is the entry point, which hilariously immediately jumps to the
PRINTNULL
subroutine. The space afterJSR
denotes this is a relative jump, and so we can only jump 128 bytes forwards or backwards. The whole program is only 40-ish bytes, so we're good.
- Labels live on the far left and this is the entry point, which hilariously immediately jumps to the
DW X'8D8A'
DC 'HELLORLD!'
-
DB 0
- DW, DC and DB are interesting OpCodes because they aren't actually OpCodes. The assembler just drops those values at that location in memory. So, if our program starts at x'1000', starting at x'1002' we'll have 8D 8A C8, etc. This is an interesting, Centurion trick that we'll use to our advantage a little later.
-
PRINTNULL LDAB= B'00000010' ; Set mask to check for tx buffer empty
- Now we're getting into the meat of the program. This is the subroutine that will print our Hellorld! to the screen. But first, we have to make sure the serial MUX is ready, and the first step to that is creating a mask that we can compare a status bit against. That mask is X'02', which we wrote out in Binary. This value is loaded into the A register byte, and the
=
means it's a literal load.
- Now we're getting into the meat of the program. This is the subroutine that will print our Hellorld! to the screen. But first, we have to make sure the serial MUX is ready, and the first step to that is creating a mask that we can compare a status bit against. That mask is X'02', which we wrote out in Binary. This value is loaded into the A register byte, and the
-
XAYB ; AL -> YL
- There isn't a load Y instruction, but we want our mask byte to live in the Y register. So we load it into A and then use XAYB to transfer the A register byte to the Y register.
-
PNLOOP LDBB+ X+ ; Load the next byte
- We're creating a new label here we can loop back to called PNLOOP. And the first instruction of this loop is to Load the B register Byte with an indirect value. That value is the value currently stored at where the X register points. This is our tricky bit, because when we run a Jump SubRoutine instruction, the return vector is stored in the X register, so it's currently pointing at the bytes we laid down earlier with DW, DC and DB. The plus sign after X says to increment the X register by one after we've read the value in, moving it up to the next byte. '* BZ PNEND ; If 0, we are done`
- If that byte we just read in is a 0, then we've hit our null terminator. BZ is a Branch if Zero instruction, and we'll branch to the label PNEND. If it's not zero, let's get to printing.
-
PNWAIT LDAB/ X'F200' ; AL = MUX status byte
- Another new label, another new loop! This loop is for waiting for the transmit buffer to clear up, and the first step in that is loading up what is stored in the transmit buffer. X'F200' is the MUX card MMIO port for reading the status byte. So, we'll read that byte in and store it in the A register.
-
ANDB YL,AL ; Check if transmit buffer empty
- We then AND the A register byte with the Y register byte and store the result back in the A register. The Y register is still holding our mask from earlier, so this is our first step to checking that the buffer is empty.
-
BZ PNWAIT ; If not empty, loop
- The MUX card status register sets the second bit if it's ready to go, but if that second bit isn't set, we'll AND our two bytes into a big 'ol zero, and if it's zero, we'll branch back up to PNWAIT and loop around again, checking and waiting.
-
STBB/ X'F201' ; Store the character to the MUX data
- If the bit did get set, let's go! The B register is storing our ASCII byte, so we'll store that byte into the MUX MMIO port for receiving a byte, which is X'F201'.
-
JMP PNLOOP ; Go to the next character
- Then, we just do a relative jump back up to PNLOOP to grab the next character and start the whole process again!
-
PNEND RSR
- Finally, here's our PNEND label that we jump to if that next character was a zero, and all it does is return from the subroutine.