- bz80: A ez80 emulator in TI-BASIC
- 06 Jun 2020 04:36:41 pm
- Last edited by commandblockguy on 02 Jan 2021 06:13:46 pm; edited 3 times in total
So, I've been writing an ez80 emulator called bz80 entirely in TI-BASIC. A bit of eye candy first: here it is computing the Fibonacci sequence:
I'm emulating a regular ez80 here, not a calculator. As a result, the entire address space is treated like RAM, and I don't attempt to emulate any form of IO. So, you won't be playing any games on it any time soon.
Why?
When TI decided to remove native code from the CE series, I decided that writing an emulator in TI-BASIC would be the pettiest possible response.
Limitations
Currently, the two biggest limitations are memory and speed. Memory is limited to 999 bytes, as it's stored in L₁. I could expand this a few times by storing multiple bytes per list element or using multiple lists, at the cost of speed.
I originally intended to store memory in Str1, but then I found out that sub( doesn't work past 9999 elements, and also that due to multi-byte tokens, accessing a token is O(n).
I then decided to store the entire address space in lists named BZAAA through BZZZZ, until I realized that, while you can get a list element without knowing its name using eval(), you cannot store to a list without hardcoding its name.
So, for now, I'm just using one element of L₁ per byte of memory.
The other big limitation here is speed. As you can see from the screenshot, this is probably running at 1-2 Hz when printing the processor state to the screen constantly. It runs a bit faster than that when not displaying anything (about 4.3 Hz for all no-ops, and 2.1 ld hl,1337's per second for the equivalent of 8.4 Hz). In any case, this is never going to do anything at a useful speed.
Here's the GitHub repo, if you want to check it out for yourself: https://github.com/commandblockguy/bz80
Hold up, are those C preprocessor macros?
Yes.
After carefully evaluating the design principles behind TI-BASIC, the ez80, and the CE as a whole, I decided that my build process must include a similar amount of jankiness. I further concluded that the best way to do so would be to use the C preprocessor, since it was obviously not intended for anything like this. I ran into an issue where the preprocessor would insert extra spaces into macros, which I fixed by running the tokenized program through a binary regex to remove spaces. This broke the expr( command, which I fixed with more regexes. Since I was using regexes anyway, I added a few more to automatically remove unnecessary ")]}, which was slightly faster than writing a plugin for my text editor to not freak out when it encounters unclosed parentheses.
I also added a subroutine framework using the preprocessor. It allows you to type "test_routine(0)", and it will add 0 to an argument list, store the ID of test_routine, then recursively call the program. If the ID is set, it will call test_routine instead of the main program. It's pretty bad, and the definition of a subroutine involves a ton of boilerplate. There's really not a better way to do it in the C preprocessor, though.
The subroutines are probably relatively slow - if anyone has any suggestions on how to improve them, that would be great.
Building
You'll need the tivars_lib_cpp CLI, make, and python3.
Clone the repo from the link above, or download a zip, then run make.
I may put a .8xp release on the repo at some point.
Using
Store your program in L₁, with one element per byte. The program will be run from address 0, so existing 8xp programs can't be run.
For small programs, you can probably just ask RunerBot to assemble them for you - just do "@ez80 (assembly code)" to assemble into hex, then convert each hex byte into decimal - this site handles that well.
Alternatively, on Linux you can "od -An -vtu1 -w1 (binary file)", then manually type the result into the list.
I'm working on a script to automatically convert from binary to 8xl.
To-do
z80 mode - everything runs in ez80 mode right now.
CB and ED-prefixed opcodes
Interrupts (not that there are any interrupt sources)
IO (not that there's any hardware to interface with)
Shadow registers
DAA
The H flag
Probably a bunch of other stuff I'm forgetting
Already implemented
(though not necessarily well-tested)
Code:
Here's the repo link again, since I buried it in the middle of the post: https://github.com/commandblockguy/bz80
I'm emulating a regular ez80 here, not a calculator. As a result, the entire address space is treated like RAM, and I don't attempt to emulate any form of IO. So, you won't be playing any games on it any time soon.
Why?
When TI decided to remove native code from the CE series, I decided that writing an emulator in TI-BASIC would be the pettiest possible response.
Limitations
Currently, the two biggest limitations are memory and speed. Memory is limited to 999 bytes, as it's stored in L₁. I could expand this a few times by storing multiple bytes per list element or using multiple lists, at the cost of speed.
I originally intended to store memory in Str1, but then I found out that sub( doesn't work past 9999 elements, and also that due to multi-byte tokens, accessing a token is O(n).
I then decided to store the entire address space in lists named BZAAA through BZZZZ, until I realized that, while you can get a list element without knowing its name using eval(), you cannot store to a list without hardcoding its name.
So, for now, I'm just using one element of L₁ per byte of memory.
The other big limitation here is speed. As you can see from the screenshot, this is probably running at 1-2 Hz when printing the processor state to the screen constantly. It runs a bit faster than that when not displaying anything (about 4.3 Hz for all no-ops, and 2.1 ld hl,1337's per second for the equivalent of 8.4 Hz). In any case, this is never going to do anything at a useful speed.
Here's the GitHub repo, if you want to check it out for yourself: https://github.com/commandblockguy/bz80
Hold up, are those C preprocessor macros?
Yes.
After carefully evaluating the design principles behind TI-BASIC, the ez80, and the CE as a whole, I decided that my build process must include a similar amount of jankiness. I further concluded that the best way to do so would be to use the C preprocessor, since it was obviously not intended for anything like this. I ran into an issue where the preprocessor would insert extra spaces into macros, which I fixed by running the tokenized program through a binary regex to remove spaces. This broke the expr( command, which I fixed with more regexes. Since I was using regexes anyway, I added a few more to automatically remove unnecessary ")]}, which was slightly faster than writing a plugin for my text editor to not freak out when it encounters unclosed parentheses.
I also added a subroutine framework using the preprocessor. It allows you to type "test_routine(0)", and it will add 0 to an argument list, store the ID of test_routine, then recursively call the program. If the ID is set, it will call test_routine instead of the main program. It's pretty bad, and the definition of a subroutine involves a ton of boilerplate. There's really not a better way to do it in the C preprocessor, though.
The subroutines are probably relatively slow - if anyone has any suggestions on how to improve them, that would be great.
Building
You'll need the tivars_lib_cpp CLI, make, and python3.
Clone the repo from the link above, or download a zip, then run make.
I may put a .8xp release on the repo at some point.
Using
Store your program in L₁, with one element per byte. The program will be run from address 0, so existing 8xp programs can't be run.
For small programs, you can probably just ask RunerBot to assemble them for you - just do "@ez80 (assembly code)" to assemble into hex, then convert each hex byte into decimal - this site handles that well.
Alternatively, on Linux you can "od -An -vtu1 -w1 (binary file)", then manually type the result into the list.
I'm working on a script to automatically convert from binary to 8xl.
To-do
z80 mode - everything runs in ez80 mode right now.
CB and ED-prefixed opcodes
Interrupts (not that there are any interrupt sources)
IO (not that there's any hardware to interface with)
Shadow registers
DAA
The H flag
Probably a bunch of other stuff I'm forgetting
Already implemented
(though not necessarily well-tested)
Code:
NOP
DJNZ
JR d
JR cc',d
LD r,n
ADD HL, rr
LD (BC),A
LD (DE),A
LD (Mmn), HL
LD (Mmn), A
LD A, (BC)
LD A, (DE)
LD HL, (Mmn)
LD A, (Mmn)
INC rr
DEC rr
INC r
DEC r
LD r,n
RLCA
RRCA
RLA
RRA
CPL
SCF
CCF
HALT
LD r,r'
ADD A,r
ADC A,r
SUB A,r
SBC A,r
AND A,r
XOR A,r
OR A,r
CP A,r
RET cc
POP rr
RET
JP HL
LD SP,HL
JP Mmn
EX (SP),HL
EX DE,HL
CALL cc, Mmn
PUSH rr
CALL Mmn
ADD A,n
ADC A,n
SUB A,n
SBC A,n
AND A,n
XOR A,n
OR A,n
CP A,n
RST n
IX and IY prefixes
Here's the repo link again, since I buried it in the middle of the post: https://github.com/commandblockguy/bz80