Re: 6502 Machine Language Monitor for 4 x 20 LCD - HOWTO
Posted: Sun Feb 12, 2023 9:09 pm
Part 4
Hex Mode
Drawing 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.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. 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. 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:
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._PM_ClearFB is not particularly interesting, but I'll include it for the sake of completeness:_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.One thing we still don't have is a way to transfer the framebuffer to the LCD. That's what _PM_refresh does:_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: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!
Hex Mode
Drawing 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: Select all
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 framebufferCode: Select all
_PM_PutChar:
phy
ldy PM_fbPtr
sta PM_frameBuffer,y
iny
cpy #80
bne .nowrap
ldy #0
.nowrap:
sty PM_fbPtr
ply
rtsOne 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: Select all
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_PutHexIt'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: Select all
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.Code: Select all
_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_displayCode: Select all
_PM_ClearFB:
pha
phy
ldy #0
lda #' '
.loop:
sta PM_frameBuffer,y
iny
cpy #80
bne .loop
ply
pla
rtsCode: Select all
_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
Code: Select all
_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_refreshAt what point should _PM_refresh be called? I added it to the main loop:
Code: Select all
_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
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!