It dawned on me yesterday this whole silliness would be a lot easier if I had a debugger. And of course I’ve made everything more complicated by insisting on using Z80 mnemonics, because the stock debugger, DDT, only speaks 8080. I downloaded ZDT, which doesn’t have a manual, works mostly like DDT, and while it uses 8080 mnemonics, it at least recognizes the additional Z80 commands that DDT just misses (like the extended MOV commands).
The T command lets us single-step through the program
Note that MVI C,09 is 8080, in Z80, it’s LD C,09. Also note that the B register listed above is the BC register, so the 09 is seen in the second byte.
Below we’re entering our CHARIN routine. The Call to 0005 is the call to BDOS. After that we end up jumping up to to that routine in high memory, and then it locks up. Dunno if that’s because it’s actually waiting for a character, but the whole thing locks up, and I’ve got to reboot.
Z E A=00 B=0000 D=015B H=0000 X=0000 Y=0000 S=01C0 P=0109 CALL 0145*0145
Z E A=00 B=0000 D=015B H=0000 X=0000 Y=0000 S=01BE P=0145 PUSH B*0146
Z E A=00 B=0000 D=015B H=0000 X=0000 Y=0000 S=01BC P=0146 PUSH D*0147
Z E A=00 B=0000 D=015B H=0000 X=0000 Y=0000 S=01BA P=0147 PUSH H*0148
Z E A=00 B=0000 D=015B H=0000 X=0000 Y=0000 S=01B8 P=0148 MVI C,01*014A
Z E A=00 B=0001 D=015B H=0000 X=0000 Y=0000 S=01B8 P=014A CALL 0005*0005
Z E A=00 B=0001 D=015B H=0000 X=0000 Y=0000 S=01B6 P=0005 JMP BF00*BF00
Z E A=00 B=0001 D=015B H=0000 X=0000 Y=0000 S=01B6 P=BF00 JMP C8F2*C8F2
Z E A=00 B=0001 D=015B H=0000 X=0000 Y=0000 S=01B6 P=C8F2 XTHL *C8F3
Z E A=00 B=0001 D=015B H=014D X=0000 Y=0000 S=01B6 P=C8F3 SHLD D5C8*C8F6
Z E A=00 B=0001 D=015B H=014D X=0000 Y=0000 S=01B6 P=C8F6 XTHL *C8F7
Z E A=00 B=0001 D=015B H=0000 X=0000 Y=0000 S=01B6 P=C8F7 JMP D806
Yesterday we got a simple routine working that set a selected bit on a word using a few registers. Lets see if we can select that bit using the command line.
output a message prompting the user for the bit to set
read the input
convert the ascii character to a number – as simple as subtracting 30H or 48 from the register?
use that number as the bit number (register c), and run set bit
go to 1
WELL… IT DOESN’T WORK. I can output the message, type a single character, then it locks up. not sure where it’s getting gobbed up – I’ve got a single character printing in the setbit routine and it doesn’t even get that far.
Time to fail for another hour at assembly language… Yesterday I think the routines for writing to the console were tripping me up, so I’m going to start a new file with none of that and just write to port 0 for starters.
So that ‘worked’ and is predictable in terms of the # of shifts. Note though that CP/M call 0 ends up wiping what’s on the output (or maybe only port 0 ?)
For now, I’m not going to worry about getting input – I’m just going to hard code some EQU’s that would eventually be values passed to the routine by something else.
HOORAY. it works.
BITNM EQU 5 ;THE BIT WERE GOING TO SET (PRESCALER)
WORD EQU 0 ;EMPTY WORD
TIMER0 EQU 40H ;WHERE TO SEND THE WORD
; REAL SIMPLE NOW - FOR STARTERS, WE PASS THE WORD & THE BIT TO THE
; ROUTINE, AND IT WILL SET THAT SPECIFIED BIT, AND PUT IT ON PORT 0
; REGISTER C WILL BE THE COUNTER
START: LD SP,STACK ;SETUP STACK POINTER
SETBIT: PUSH BC ;SAVE REGISTERS
Lost a day yesterday due to the move and date night with the Wife. I’m doin’ my best here…
So my understanding of how relative & indexed jumps work on the 8080 & Z80 was just flawed. There’s no variable indexing, like on the 6800 or 6502, which is a super bummer.
My original ideas was to have a register be the offset into a subroutine, so like you’d call the subroutine, and what was on the C register would end up selecting which part of the subroutine was run. I thought it was clever… I want to make a general purpose routine that can set or clear a specified bit in a control word.
Lets for get set vs clear for now, and focus on selecting the bit. The SET & RES commands seemed cool at first, but are actually kind of useless, since you can’t select the bit from a register. So what if we go back to using RLCA, and we use C as a counter, to determine the # of rotates we make? Oh right, we tried this two days ago, and couldn’t verify if it worked. LET. TRY. AGAIN ( I’m super hating all of this).
Lets test it on the LEDs on port0 instead of on the console – that should be more conclusive.
AND IT DOESN’T WORK. I HATE THIS . I’M DONE.
lets make a subroutine that can set or clear a selected bit in the CTC control register. To call the routine, we need to tell it:
– The CTC channel
– Which bit we’re adjusting
– If we’re setting or clearing the bit
Lets put the CTC channel’s port on the B register, the bit we’re adjusting in the C register, and…. do we use a register or a flag for setting or changing? Lets use the zero flag and see what trouble that gets us into.
Oh snap, there’s bit setting instructions, and they work with any register! set n,A would set the n’th bit. Can the n be derived from the value of a register? Lets assume not.
Actually, before we do any of that, lets see if we can get relative jumps working correctly. Maybe we can get the number of jumps from the B register, and have a series of SET instructions
Using JR 01, I’m getting a compiler error “Byte out of range”. When I look at the listing, I see the offset listed as EE, which doesn’t make sense… AH – The assembler automatically subtracts 2 to make up for the fact that the PC has already incremented by 2! So when I was trying 1 or 2, it would fail! OK, but 4 fails too…
There’s something about how this assembler handles JR that I don’t understand.
OK – it looks like you can only use JR with a label in assembler. Oh wait, you can’t do what I was originally thinking – you can’t get the offset of a Jump from a register??
From Cowlark.com :
“There is no variable indexing (like the 6502’s LDA (ptr), Y addressing mode) at all. The index registers support a constant -128..127 displacement only.”
OK, well THAT’S Stupid. Damn. I thought even the lowly 6802 did that…
so can we
1 – modify the operand byte after the JR Instruction?
2 – use one of the index registers? Does this mean 16 bit addition?
What if you…
1 – jumped to a routine
2 – set IX to the next address
3 – add C to the IX
4 – JP(IX)
So, for whatever reason, I can’t use include files, which means I just have to have everything in one large assembly file. I guess that’s not the end of the world.
Lets start by trying to set up the timer and output a simple message.
TIMER0 EQU 40H ;LOCATION OF THE FIRST TIMER
TWORD0 EQU 00010101B ;TIMER WORD (A STATIC VALUE FOR NOW)
TTCON0 EQU 01H ;TIMER CONSTANT (STATICFOR NOW)
; LETS FIRST TRY TO SET UP THE TIMER WITH A SINGLE, HAND BUILT CONTROL WORD
OK, that worked!
Next I’d like to try to build that word in code, using a number of variables. Up until now we’ve mostly been dealing with symbols, using the EQU pseudo-0p. I think if we want to define single byte storage areas, we can use DB, and give those locations a label.
To build up a control byte from a number of single bits, we have to shift each bit into it’s correct place, then OR it with the control byte we’re building. Right now, I’m thinking of this where each ‘bit’ is actually a byte that’s 1 or 0, and needs to be left shifted by the number of times as it’s position is in the byte. Since we’re dealing with bytes that should only be a 0 or a 1, we don’t need to worry about shift vs rotate vs carry vs whatever makes a bit pop back onto the other end. So lets use…. RLCA – Rotate Left Circular Accumulator. This will put a bit into the carry flag, but won’t put the carry flag back into the byte.
So we have to: 1) load each byte into the Accumulator 2) perform some number of RLCAs 3) load the control word into another register 4) perform an OR with that register 5) put the result from the accumulator back into that other register 6) load the next byte into the Accumulator and keep going
So I tried this little snip of code
;LETS TRY TO BUILD A CONTROL WORD WITH LEFT SHIFTS
; LETS OUTPUT THIS ONTO THE CONSOLE. IT SHOULD BE AN @ CHARACTER, SINCE
; WE SHIFTED 1 OVER 5 TIMES TO THE 64'S BIT.
With these DB entries below
;TIMER CONTROL WORD BITS (EVEN THOGH THESE ARE REALLY BYTES.. THIS IS DUMB BUT WHATEVES)
TB0CV DB 1 ;CONTOL WORD OR VECTOR ADDRESS - 1 BYTE IS CTRL WRD
TB1RS DB 0 ;RESET
TB2TC DB 1 ;IS NEXT BYTE TIME CONSTANT
TB3TT DB 0 ;TIMER TRIGGER - 0 STARTS RIGHT AWAY
TB4ES DB 1 ;EDGE SELECTION - 1 RISING
TB5PS DB 0 ;PRESCALER - 0 = 16, 1 = 256
TB6MD DB 0 ;MODE - 0 = TIMER, 1 = COUNTER
TB7IN DB 1 ;INTERRUPT - 0 DISABLES, 1 ENABLES
TWRD0 DB 00 ;THE WHOLE WORD
And I get errors on both lines that reference those memory locations: DAY142.Z80 – Byte Out of Range Line 00039 LD A,TB7IN DAY142.Z80 – Byte Out of Range Line 00045 LD B,TWRD0
Lets find out why: It’s been really useful to run the compiler with the /F option, which forces the creation of a listing. That way, we can see what opcodes were used, and see where the difference between what we thought should happen, and reality.
So the LD opcodes were 5A & 06, For LD A, I think I wanted opcode 3A, which expects a 16 bit address. Did I need to put the name of the variable in parentheses ? I assumed it would have figured that out for me. OK! Using parentheses for the first entry worked, and it chose the right opcode. It didn’t chose the right opcode for B, because there isn’t one.
Not all registers are created equal. The A register can be loaded from any 16 bit address in memory, while the other registers have more limited instructions. If we want to load the B register from memory, we must first place that address in the HL register. So lets try that.
OK! Well that compiled without error, but I don’t know if it worked – More tomorrow.
What I’m still not 100% sure on is when to use parentheses with memory location labels, and when not to. I should really investigate the various flavors of LD tomorrow.
Also, in retrospect, I don’t think we actually need separate areas in memory to hold the individual bits. I think we’re better off just writing routines that can set or clear each bit in the control word.
And… Looking at the list file, we can see that LD A,(LOCATION) loads the contents of LOCATION into A, while LD HL,LOCATION loads the address of LOCATIOn into HL.
Before I make a simple program to test the CTC, I’d like to learn how the assembler deals with multiple files. The simplest way is to use the INCLUDE pseudo-op:
INCLUDE <FILENAME.EXT> will simply open the .ext file and insert it at the location of the deceleration. Lets try it with yesterday’s HELLO example – we’ll move STROUT, CHOUT & NEWLN to a file called CHARIO.EXT.
So it compiles without error, but when I run it, I get
+++ BAD INT 00H: @0507[0044:D782:540A:0500:FEF4:EC20:11C6]
Shit. The listing looks clean, so I really have no idea. Well, that was fun.
I’m gonna have a beer.
Yesterday proved I need to take a step back. So let me actually take my time and write a proper ‘Hello World’ program. I’m going to do this in VS Code, since it’s just nicer to type on a 21st century editor (no offense, ZDE, you were totally awesome for the early 80s).
First thing I need to figure out is where does the stack start? I was originally confused at putting the top of the stack at 00 (where there’s a bunch of BDOS vectors), but I think that really translates to 100H, the top of the TPA, which we set with the ORG pseudo-instruction.
In the following example, I’m using both the write character call, 2, and the write string call, 9. Write string is handy – it handles the looping for you, you just have to put the address of the beginning of the string in the DE register. The only downside is the string is terminated with ‘$’ so the string can’t contain that character. There’s a good overview of all the BDOS calls here.
A few other learnings:
ZDE has both hard and soft tabs. I think if it detects hard tabs in the file, it switches to that mode. Likewise, you should set VS Code to use hard tabs, 8 spaces.
I thought some other assembler had a label limit of 5 characters, but it’s more like 16 on Z80ASM. I’m trying to limit to 6 just for formatting (any longer and I need another tab)
Make sure to set your editor to use CR & LF. I don’t think the assembler had a problem with LF only, but you need both for proper formatting in ZDE, which is handy if you want to make a quick edit ‘on the box’.
;HELLO WORLD FOR Z80 ASSEMBLER
;Z80ASM BY SLR SYSTEMS
BDOS EQU 5 ;CALL FOR BDOS FUNCTIONS
WCHR EQU 2 ;WRITE CHARACTER, IN REG C
WSTR EQU 9 ;WRITE STRING, IN REG C
RCHR EQU 1 ;READ CHAR, IN REG C
RBOOT EQU 0 ;RETURN TO CP/M
TPA EQU 100H ;TRANSIENT PROGRAM AREA, START HERE
CR EQU 0DH ;CARRAIGE RETURN
LF EQU 0AH ;LINE FEED
CTRLZ EQU 1AH ;CTRL Z
START: LD SP,STACK ;SETUP STACK POINTER
BANG: LD E,"#"
;LOADS THE ADDR OF BEGIN OF MESG IN HL
HELLO: LD E,(HL) ;LOAD E W/ FIRST CHAR FROM STRING
STRNG: CALL NEWLN
JP EXITEXIT: JP RBOOT ;RETURN TO CCP
;STROUT - OUTPUTS A STRING WHOS ADDRESS IS IN DE REGISTER
STROUT: PUSH BC ;SAVE REGISTERS
;CHOUT - OUTPUT A CHARACTER THATS IN E REGISTER
CHOUT: PUSH BC ;SAVE REGISTERS
LD C,WCHR ;PUT WCHR IN C FOR BDOS CALL
CALL BDOS ;CALL BDOS
;NEWLN - DOES WHAT IT SAYS
NEWLN: LD E,CR ;NOT SURE IF WE NEED CR & LF?
CALL CHOUT ;YEP, WE SEEM TO
HELO: DB "HELLO WORLD!",255
MESG: DB "THIS IS A TEST OF A STRING OUT$"
DS 64 ;HOLD 64 BYTES FOR STACK
STACK DB 0 ;TOP OF STACK
I lost 2 days – one to being knocked out by the booster, and the other from apathy, laziness, and distractions. That’s so fucking like me… OK, back at it. I doubt I’m going to get a light blinking today, but I’m going to dig into what It’ll take to get a timer set up in Assembly language.
The Z80 CTC first expects a Channel Control word, which is optionally followed by a Time Constant. The Control Word is a series of 8 bits, each of which represents a single toggle, outlined below (I recreated this table from this video). We could assemble this byte a few different ways:
Hard code it – 00010101B would be a control word, followed by a time constant, rising edge. The assembler wants the B suffix for binary. Efficient, but dumb.
Assign each bit it’s decimal or hex value, and then add them up together. To get the same word as above, we’d do (128*0)+(64*0)+(32*0)+(16*1)+(8*0)+(4*1)+(2*0)+(1*1)
Have 8 discrete 1 bit values, and left shift them onto the accumulator – Bit 7 would be shifted 7 times, bit 6 shifted 6 times, etc…
Lets first experiment with shifting a value. But even before we do that, we have to see about prompting the user with what to enter. JFC, you gotta do everything yourself in assembly.
OK, I’ve been trying to write a simple god damn message to the terminal for the last 90 minutes.
; HELLO WORLD
; SETUP SOME THINGS
BDOS EQU 5
WCONF EQU 2
RCONF EQU 1
RBOOT EQU 0
TPA EQU 100H
CR EQU 0DH
LF EQU 0AH
CTRLZ EQU 1AH
START1: LD E,LF ;OUTPUT LINE FEED
DERP: LD E,'$' ;OUTPUT A $
MESG: DB "HELLO WORLD!"
PRNT: LD E,(HL) ;HL POINTS TO CHAR TO LOAD INTO A
INC HL ;INCREMENT HL
LD C,WCONF ;SET C W/ WRITE BYTE
JP PRNT ;GO BACK TO THE TOP OF THE PRINT ROUTINE
I hate this.
I’m dumb. I rewrote the program after some folks pointed out a few extra-stupid mistakes, and it at least now vomits onto the screen!
Gotta have the DB block at the end, otherwise it’s just interpreted as code (duh) Gotta PUSH & POP the HL register, cause it gets trashed by the BDOS Call.
What you see below is what happens when you don’t have a test at the end of the string. The loop is printing everything (the contents of the entire memory map).
I got my booster today and it’s Friday, so I’m a little drained, but at least want to try to do something. So I’ll do a little CP/M housekeeping.
The CF Card is IDE0. ROMWBW introduces partitioning that it calls slices. ‘Out of the box’, these slices are 8MB ea, and slice 0 – 7 are assigned drive letters C – J. For some reason, drive D is returning garbage on a DIR command. Running ‘CLRDIR D:’ fixed that. Note that for the most part, commands can be entered in either case, but when prompted to answer ‘Y’, you must match case.
CP/M doesn’t include move or copy commands, though it does have REName. The syntax backwards from what you’d expect from the POSIX ‘mv’ command:
REN NEWNAM.TXT=OLDNAM.TXT. RENname cannot be used to move files across drives.
If we want to copy a file, we’ll need to use the PIP command. PIP stands for Peripheral Interchange Program. PIP dates back to the 60s, and existed on most of DECs operating systems for the PDP line of computers. I suppose I should try to learn it.