Part 4Hex ModeDrawing the hex mode display is fairly straightforward. I (now) use an 80 byte array as a
framebuffer and a variable as an index into the array for accessing specific screen positions. I also defined some handy constants to make getting around the display a little more convenient.
Code:
PM_FB_LINE1 = 0
PM_FB_LINE2 = 20
PM_FB_LINE3 = 40
PM_FB_LINE4 = 60
PM_frameBuffer = $0300 ; - $034F (80 bytes)
PM_fbPtr = $0350 ; index into framebuffer
To illustrate how it works, I'll bring back the _PM_PutChar routine from part one. The caller passes the character to print in the .A register. This version of _PM_PutChar uses the
absolute, indexed .Y addressing mode to store the character in the framebuffer at the location currently pointed to by fbPtr, then it increments fbPtr so that it points to the next location. There are only 80 characters in the buffer (characters number 0 through 79) so if, after incrementing, fbPtr is equal to 80 it is set to 0, wrapping back to the start of the display.
Code:
_PM_PutChar:
phy
ldy PM_fbPtr
sta PM_frameBuffer,y
iny
cpy #80
bne .nowrap
ldy #0
.nowrap:
sty PM_fbPtr
ply
rts
With _PM_PutChar we can randomly access any location on the screen, just by changing the value of PM_fbPtr and loading the character we want to output into the .A register.
One thing we will need to do a lot of in hex mode is take a byte and print its value as two hexadecimal digits. My hex output routine is one of the very first things I wrote for my LCD library. This is an example of some old code that I'm still using because it works. It uses a small lookup table to do the conversion. I've since learned more compact ways to do this, but I'll go ahead and share this old version.
Code:
HEX_TABLE: .asciiz "0123456789ABCDEF"
_PM_PutHex:
pha
phy ; save registers
pha
ror
ror
ror
ror
and #%00001111 ; get high nibble
tay
lda HEX_TABLE,y
jsr _PM_PutChar
pla
and #%00001111 ; get low nibble
tay
lda HEX_TABLE,y
jsr _PM_PutChar
ply ; restore registers
pla
rts ; _PM_PutHex
To make this work with the framebuffer I only had to replace two calls to the LCD character output routine with calls to _PM_PutChar. (Also, while I was in there I updated the tya,pha/pla,tay register saving/restoring blocks to use CMOS instructions.)
It's time for a brief excursion into
stacks. I recently started using a zero-page data stack like the one Garth describes in the
stack treatise. I'm still only using it in basic ways, and I don't have cool macros like Garth's, but I do use it for two very useful things in PAGIMON: dynamically allocating ZP space to use for temporary variables, and passing addresses (two-byte parameters) to subroutines. Here's how it works:
Code:
STACK = 0
STACKL = 0
STACKH = 1
; These constants are used with the zero page, indexed .X addressing mode to access data on the stack.
ldx #$FF
txs ; set up hardware (call stack)
dex ; .X = $FE
; In the reset routine when the hardware stack is set up, .X is set to $FE. Stack cells will always be two bytes in size,
; so STACKL,x (= 0,x) will refer to $00FE while STACKH,x (= 1,x) will refer to $00FF. When we store (push) something
; onto the stack it looks like this:
lda #<A_STRING ; Low byte of A_STRING's address
sta STACKL,x
lda #>A_STRING ; High byte of A_STRING's address
sta STACKH,x
dex
dex ; push ADDRESS - assuming this is our first push of the day, now .X = $FC
; .X is kept pointing to the next empty cell, so whenever anything is stored on the stack .X is decremented two times. To
; pull something back off the stack you just reverse the process:
inx
inx ; .X = $FE again
lda STACKL,x
sta POINTER
lda STACKH,x
sta POINTER+1 ; pull ADDRESS and store it in POINTER to A_STRING
; Because .X is always pointing at hitherto unclaimed memory, this system also gives you immediate and constant access to two
; ZP bytes if you need access temporary storage for a few instructions. If you need more space, you can always get more space:
dex ; get 2 more bytes of temporary ZP storage
dex ; STACKL,x; STACKH,x; STACKL+2,x;STACKH+2,x are now available for scratchpad use
; Just be sure to clean up when you're done.
inx
inx ; No memory leaks!
; Another cool thing about this is that you can use zero page, pre-indexed indirect addressing to transparently access absolute
; memory locations:
lda (STACK,x) ; Print first character of A_STRING
jsr _PM_PutChar
; Unfortunately, there is no (STACK,x),y addressing mode to get to the second character of A_STRING. But, as forum member
; Martin_H pointed out to me, you can increment the pointer directly on the stack:
inc STACKL,x
bne .skipHB
inc STACKH,x
.skipHB:
lda (STACK,x)
jsr _PM_PutChar ; Print next character of A_STRING
etc
; I put all of these features to use to construct PAGIMON's hex-mode output screen.
Since, as shown in a previous post, the output format is the same for each line of the hex mode display, _PM_hexMode_display doesn't actually have very much to do. It just clears the framebuffer and calls the subroutine that prints a line.
Code:
_PM_hexMode_display:
pha
jsr _PM_ClearFB ; Clear the framebuffer
lda PM_UI_addressL
sta STACKL,x
lda PM_UI_addressH
sta STACKH,x
dex
dex ; Push current address on data stack
lda #PM_FB_LINE1
jsr _PM_RAMline
lda #PM_FB_LINE2
jsr _PM_RAMline
lda #PM_FB_LINE3
jsr _PM_RAMline
lda #PM_FB_LINE4
jsr _PM_RAMline ; dump dump dump dump!
inx
inx ; Pull address back off stack
pla
rts ; _PM_hexMode_display
_PM_ClearFB is not particularly interesting, but I'll include it for the sake of completeness:
Code:
_PM_ClearFB:
pha
phy
ldy #0
lda #' '
.loop:
sta PM_frameBuffer,y
iny
cpy #80
bne .loop
ply
pla
rts
_PM_RAMline is where all the real work gets done. It expects the caller to send it the address to dump from on the data stack, and the character cell to start printing from (0, 20, 40, or 60) in the .A register. It increments the address directly on the stack as it prints. When it's finished it leaves the address on the stack, so you can call it over and over again and it will pick up where it left off.
Code:
_PM_RAMline:
phy
sta PM_fbPtr
lda #'$'
jsr _PM_PutChar ; Print the base (hexadecimal) signifier character
; I'm going to use STACKL,x as temporary storage soon, so rather than inx,inx I will access the passed address like this:
lda STACKH+2,x
jsr _PM_PutHex ; Print the high order byte of the offset
lda STACKL+2,x
jsr _PM_PutHex ; Print the low order byte of the offset
lda #':'
jsr _PM_PutChar ; Print the separator character
lda #7 ; We have to jump over some spaces to print the ASCII interpretation of the byte. Use STACKL to keep track.
sta STACKL,x ; Each hex byte takes two spaces, so we need to jump one fewer space each time around the loop.
ldy #4 ; print 4 bytes
.loop:
lda (STACK+2,x) ; get the byte
jsr _PM_PutHex ; print it
lda PM_fbPtr
pha ; save framebuffer pointer
clc
adc STACKL,x ; add offset to ASCII interpretation
sta PM_fbPtr
lda (STACK+2,x)
jsr _PM_PutChar ; print ASCII interpretation
pla ; restore frame buffer pointer
sta PM_fbPtr
inc STACKL+2,x
bne .skipHB
inc STACKH+2,x ; increment the address pointer directly on the stack
.skipHB:
dec STACKL,x ; decrement the offset into the ASCII interpretation
dey
bne .loop
ply
rts ; _PM_RAMline
One thing we still don't have is a way to transfer the framebuffer to the LCD. That's what _PM_refresh does:
Code:
_PM_refresh:
pha
phy
lda #0
sta LCD_cursor
jsr _LCD_set_cursor
ldy #0
.loop:
lda PM_frameBuffer,y
jsr _LCD_chrout
iny
cpy #80
bne .loop
ply
pla
rts ; _PM_refresh
_PM_refresh is the only other one of PAGIMON's routines aside from _PM_GetChar that needs to call my local routines. To make it work with a "stock" Ben Eater 6502 it would have to be modified, but not too much. I'm really pleased that using the framebuffer let me move all the connections with the "outside world" into two simple subroutines.
At what point should _PM_refresh be called? I added it to the main loop:
Code:
_PM_main:
php
lda #PM_REG_MODE
sta PM_UI_mode
.loop:
lda PM_UI_mode
cmp #PM_REG_MODE
beq .regmode
cmp #PM_HEX_MODE
beq .hexmode
; If PM_UI_mode gets corrupted somehow, fall through (default) to register mode
.regmode:
jsr _PM_regMode_display
jsr _PM_refresh
jsr _PM_regMode_update
bra .loop
.hexmode:
jsr _PM_hexMode_display
jsr _PM_refresh
jsr _PM_hexMode_update
bra .loop
Now the overall structure for both modes is: 1 - construct the display (in the framebuffer), 2 - send the framebuffer to the LCD, 3 - dispatch user input.
Part 5 will be the register mode display. I've saved it until now, because I've been working on updating it to incorporate Ed's suggestions from earlier - break out the status register, and include the stack register. I have managed to do these things, but some of the methods seem pretty inelegant to me. I'm hoping the experts can show some ways to improve it!