RC2024 – Part 15 – Building A Music Player That Uses The Rotary Encoder Module

If you have been following along with my Retro Challenge 2024 posts, you’ll know that I have designed and built a rotary encoder module for the RC2014 computer.

I wanted to put together all my learning and build a music player for the RC2014. This will run using my RC2014 Classic 2 computer. It uses the LCD Driver Module, and the YM2149 Sound Card Module from Z80Kits. In addition, it also uses my ROM board, and of course the Rotary Encoder Module.

The plan is to encode a few of the example tunes provided with the SDK for the YM2149 Sound Card into one program. I will then display the tunes on the LCD, and use the Rotary Encoder Module to allow the different tunes to be selected and played. The ROM board is used to allow me easier access to the SCM ROM to load my assembled program.

I used the example code I wrote earlier for the LCD to display the 3 track titles. I also used the example code for the rotary encoder to move up and down inside the menu.

Moving an arrow in the LCD

I did have to develop some new functionality to move an error up and down in the LCD. I didn’t want to rewrite the whole screen, just select a character and either write an arrow or a space to that location.

I firstly refactored my Z80 code to send a command or data to the LCD screen. This was based on a suggestion I received on social media. The code sends a byte then waits for the LCD to say it’s ready for the next byte.

LCD_R   EQU 218
LCD_D   EQU 219

; Send a command byte to the LCD.
; A - Command in
; A, C registers used.
send_command:
    out (LCD_R),a
    jr lcd_busy

; Send a data byte to the LCD
; A - Byte in
; A, C registers used.
send_data:
    out (LCD_D),a

lcd_busy:
    in a,(LCD_R)
    rlca
    jr c,lcd_busy
    ret

The LCD layout isn’t sequential in the 4×20 character display I am using. Line 3 follows line 1 by 20 characters. Line 2 is then offset by 64 characters, followed by line 4 at 84 characters.

LineOffset
10
264
320
484

The command to move an arrow is the offset from the above table OR’d with $80 (which is the command to set the DDRAM in the LCD module). So to draw an arrow on line 2, and to wipe line’s 3 and 4 I could use the following code.

    ld a,$80|64     ; $80 is the set address command, 64 is the offset.
    call .draw_arrow
    ld a,$80|20     
    call .wipe_arrow
    ld a,$80|84
    call .wipe_arrow

.draw_arrow:
    call send_command
; show an arrow
    ld a,%01111110	; this is the arrow character from the manual
    call send_data
    ret

.wipe_arrow:
    call send_command
; show a space
    ld a,' '
    call send_data
    ret

Sending Debug Information To The Serial Port

While I was developing the code I needed to send some debugging information to the serial port to make sure I knew I was moving through the lines correctly.

The code to play the selected track had a small piece of code that could send to the serial port.

TX:  
    push af
.txbusy     
    in a,($80)          ; read serial status
    bit 1,a             ; check status bit 1
    jr z, .txbusy       ; loop if zero (serial is busy)
    pop af
    out ($81), a        ; transmit the character
    ret

To send a single character I could load the character into register a and then call TX. So to send the character ‘R’ to the serial port, I could do the following.

    ld a,'R'
    call TX

I was keeping track of the current track as either 1, 2, or 3. To send this to the serial port I needed to convert the number into it’s ASCII character code. This turned out to be very simple due to the way the designed of ASCII chose the code for the digits. The ASCII code for $1 is $31, for $2 is $32, and $3 is $33. I just have to OR $30 to the value to convert it to ASCII. So to send the character ‘1’ to the serial port, I could do the following.

    ld a,$1
    or $30
    call TX

Building The Music Player

Thankfully, the work I’ve completed over the course of the Retro Challenge month worked well together. I was able to tweak the existing PTPlayer example code from the sound module to insert my rotary encoder detection routines into the main loop. In the loop I would then move the arrow in the display if necessary, and change the current track if the rotary encoder was pressed.

Here is a video of the music player in action.

The one thing I have found is that if I turn the rotary encoder too fast, it doesn’t always correctly pick up the turn. This is because there is a lot more happening in the loop playing the music so the encoder isn’t being sampled as frequently as in my test code. A possible solution to this could be to look at using interrupts, but I won’t have time to do this before the end of the Retro Challenge.

RC2024 – Part 13 – My RC2014 Mac Development Environment

As part of this year’s Retro Challenge, I’ve been writing Z80 assembly language. I wanted to cover what tools I’ve been using to do this on my Mac.

Visual Studio Code is a great programmer’s text editor from Microsoft. It’s free and has a lot of extensions. I use the Z80 Assembly extension. This provides syntax highlighting for my code.

SjASMPlus is a free Z80 assembler. I use this to assemble my source code into a binary file. In your source code you need to include an OUTPUT statement. This is the filename of the output binary file. To keep things easy I use the same filename as source code, but with a different extension. It is capable to splitting the output into multiple files, but that is too advanced for me at the moment.

I then need to get this binary file onto my RC2014. To do this I use z88dk-appmake from the Z88DK development tools. This can take the binary and turn it into Intel formatted hex. This can then be pasted into a hexloader on the RC2014. SCM has one built in.

Visual Studio Code offers Tasks, which lets us run jobs directly inside Visual Studio Code. I have created several tasks. One runs SjASMPlus on the current file. One runs z88dk-appmake on the generated binary to create the hex file. One uploads it to the RC2014. One runs it on the RC2014. There is also a combined build task that runs assembles, transfers, and runs the current code on the connected RC2014.

I make some assumptions in this tasks.json file.

I assume this is always connected to my RC2014 Classic 2 on a fixed device that is already connected using minicom. I could include stty commands in the tasks.json file to configure the connection. However, I always have minicom open in another window so this isn’t needed.

I assume I’ve always set the OUTPUT to be the same filename as the source code, just with a .z80 extension.

I assume the code has been assembled to address $9000.

The individual tasks work well, but the combined task that chains them together can sometimes fail. The issue here seems to be when I cat the hex to the RC2014. I’ve found piping this through an echo instead of directly redirecting to the device is more likely to succeed. If this fails, I manually cat the hex file to the RC2014 in a shell window.

I’ve found these tasks have really sped up my development time.

This is my current tasks.json setup for Visual Studio Code RC2014 development.

{
    // See https://go.microsoft.com/fwlink/?LinkId=733558
    // for the documentation about the tasks.json format
    "version": "2.0.0",
    "tasks": [
        {
            "label": "RC2014: sjasmplus",
            "type": "shell",
            "command": "sjasmplus --fullpath ${file}", 
            "group": {
                "kind": "build",
                "isDefault": false
            },
            "options": {
                "cwd": "${fileDirname}"
            },
            "presentation": {
                "group": "RC2014"
            }
        },
        {
            "label": "RC2014: appmake",
            "type": "shell",
            "command": "z88dk-appmake +hex --org 0x9000 -b ${fileBasenameNoExtension}.z80",
            "group": {
                "kind": "build",
                "isDefault": false
            },
            "options": {
                "cwd": "${fileDirname}"
            },
            "presentation": {
                "group": "RC2014"
            }
        },
        {
            "label": "RC2014: Deploy to SCM",
            "type": "shell",
            "command": "cat ${fileDirname}${/}${fileBasenameNoExtension}.ihx | echo > /dev/tty.usbmodem06351",
            "group": {
                "kind": "build",
                "isDefault": false
            },
            "options": {
                "cwd": "${fileDirname}"
            },
            "presentation": {
                "group": "RC2014"
            }
        },
        {
            "label": "RC2014: Run on SCM",
            "type": "shell",
            "command": "echo -e \"g 9000\r\n\" > /dev/tty.usbmodem06351",
            "group": {
                "kind": "build",
                "isDefault": false
            },
            "options": {
                "cwd": "${fileDirname}"
            },
            "presentation": {
                "group": "RC2014"
            }
        },
        {
            "label": "RC2014: Build",
            "dependsOrder": "sequence",
            "dependsOn": ["RC2014: sjasmplus", "RC2014: appmake"],
            "group": {
                "kind": "build",
                "isDefault": false
            }
        },
        {
            "label": "RC2014: Build, Deploy, and Run",
            "dependsOrder": "sequence",
            "dependsOn": ["RC2014: sjasmplus", "RC2014: appmake","RC2014: Deploy to SCM","RC2014: Run on SCM"],
            "group": {
                "kind": "build",
                "isDefault": true
            }
        }
         
    ]
}

RC2024 – Part 10 – Using The Rotary Encoder To Scroll The LCD

So far in this year’s Retro Challenge I’ve designed and built my own Rotary Encoder Module for the RC2014 computer. I’ve also worked out how to control an LCD screen from Z80 assembly language. I now want to combine the two and use the rotary encoder to scroll text on the LCD screen.

I’m building this on the RC2014 Classic 2, so I don’t have access to a file system. I will have to hardcode the text into the program.

I’ve chosen to use the classic hacker song, Puff The Fractal Dragon.

The LCD screen is 20 characters wide, so I will make things easy for myself and ensure every line is 20 characters long. I will pad shorter lines with spaces if necessary.

I’m going to need a pointer to store my current position in the text. I’m calling this puffpointer. I also need to know the start of the text, I’m calling this puff. I’ll also need to know 4 lines before the end of the text. I’m calling this maxpuff. This is calculated in the assembler as the end of the text – 80. The 80 is 4 lines * 20 characters.

I’m using the right turn to scroll down the text, and the left turn to scroll back to the top.

In the right turn I need see if I’m at the end of the text or not. I need to compare puffpointer to maxpuff to see if they match. If they do, I’m at the button so I don’t want to go any further.

The Z80 doesn’t allow us to directly compare 16bit values, so we have to do a bit of a workaround. We can instead clear the a register, then load the values we want to compare into de and hl register pairs. We can then subtract de from hl, and add de back to hl. If they are the same value the Z flag will be set so can test this. In this case, if Z is set we don’t want to do anything else so we can jump back to the main program loop.

    or a
    ld de,maxpuff
    ld hl,(puffpointer)
    sbc hl, de
    add hl, de
    jp z,loop

So if we are get past this point, we are safe to scroll down. We load the pointer to the current line in the text and add 20 to it. This moves us down a line. We then save it, and call our display routine.

    ld hl,(puffpointer)
    ld bc,20
    add hl,bc
    ld (puffpointer),hl
    call show_four_lines

When turning left do a very similar procedure, except we check if puffpointer is at the start of the text. If it isn’t we subtract 20 from puffpointer.

Our final code looks like this.

    OUTPUT LCDScroll.z80

    ORG $9000

ROTARYENCODER EQU $DE
LCD_R   EQU 218
LCD_D   EQU 219

; The input bits from the rotary encoder.
CLK1    EQU %00000001
DT1     EQU %00000010
SW1     EQU %00000100

; show the inital first 4 lines on the LCD.
    call setup_LCD

    ld hl,(puffpointer)      ; the address of the text
    call show_four_lines

loop:
; load the last clk value into register b
    ld  a,(lastclk)
    ld  b,a

; read the input port and store in "input"
    in  a,(ROTARYENCODER)
    ld  (input),a

; now check if the switch on first rotary encoder has been
; pressed. If it has jump to end
    and SW1
    cp  SW1
    jr  z, end

; now see if clk1 matches the lastclk. If it does loop
    ld  a,(input)
    and CLK1
    ld  (lastclk),a
    cp  b
    jr  z, loop

; now work out what direction we are moving.
; if CLK1 is 1 then we can can check DT1 to get the
; direction of rotation. If it's 0, we need to go 
; back to the start of the loop.
    ld  a,(input)          
    and CLK1
    cp  CLK1 
    jr  nz, loop            

; this is where we check DT1. If 1 we are turning left.
    ld  a, (input)
    and DT1
    cp  0 
    jr  nz, left

; we must be turning right, so we need to advance 
; our text. We see if we are at the maximum, and
; if not we advance a line and display.
right:
    or a
    ld de,maxpuff
    ld hl,(puffpointer)
    sbc hl, de
    add hl, de
    jp z,loop

    ld hl,(puffpointer)
    ld bc,20
    add hl,bc
    ld (puffpointer),hl
    call show_four_lines

    jr  loop

; we must be turning left, so we need to go
; back. We see if we are at the start of the 
; text and if not we go back a line and display.
left:
    or a
    ld de,puff
    ld hl,(puffpointer)
    sbc hl, de
    add hl, de
    jp z,loop

    ld hl,(puffpointer)
    ld bc,20
    sub hl,bc
    ld (puffpointer),hl
    call show_four_lines

    jp  loop

; the switch has been pressed, so we clear the output
; and exit.
end:
    call clear_screen

    ret


; Sends a command byte to the LCD.
; A - Command in
; A, C registers used.
send_command:
    out (LCD_R),a
.lcd_busy:
    in a,(LCD_R)
    rlca
    jr c,.lcd_busy
    ret

; Sends a data byte to the LCD
; A - Byte in
; A, C registers used.
send_data:
    out (LCD_D),a
.lcd_busy:
    in a,(LCD_R)
    rlca
    jr c,.lcd_busy
    ret

; setup the LCD screen
setup_LCD:
    ld a,56         ; Function 8 bit, 2 lines, 5x8 dot font
    call send_command
    ld a,12         ; Display on, cursor off, no blink
    call send_command

    call clear_screen

    ret

; clear the LCD screen
clear_screen:
    ld a,1          ; clear the display
    call send_command
    ret

; Display 4 lines of consecutive text on the LCD
; lines are shown 1-20,41-60,21-40,61-80 so we 
; need to jump around to display in order.
; HL - address of text to display on the LCD
; A, B, C, D, E, H, L registers used.
show_four_lines:

; show the first 20 lines
    ld b,20
.line1loop:
    ld a,(hl)
    inc hl
    call send_data
    djnz .line1loop

; jump forward 20 characters, and show
    ld de,20
    add hl,de
    ld b,20
.line2loop:
    ld a,(hl)
    inc hl
    call send_data
    djnz .line2loop    

; jump back 40 characters, and show
    ld de,40
    sub hl,de
    ld b,20
.line3loop:
    ld a,(hl)
    inc hl
    call send_data
    djnz .line3loop

; jump forward 20 characters, and show
    ld de,20
    add hl,de
    ld b,20
.line4loop:
    ld a,(hl)
    inc hl
    call send_data
    djnz .line4loop 

    ret      


; stores the current input from the rotary encode.
input:
    db  0
; stores the last value of CLK1.
lastclk:
    db  0

; stores a pointer to our current position in the text.
puffpointer:
    dw  puff
; the text to show, each line must be 20 bytes long.
puff:
    db "Puff the fractal    "
    db "dragon was written  "
    db "in C,               "
    db "And frolicked while "
    db "processes switched  "
    db "in mainframe memory."
    db "                    "
    db "No plain fanfold    "
    db "paper could hold    "
    db "that fractal Puff   "
    db "                    "
    db "He grew so fast no  "
    db "plotting pack could "
    db "shrink him far      "
    db "enough.             "
    db "Compiles and        "
    db "simulations grew so "
    db "quickly tame        "
    db "And swapped out all "
    db "their data space    "
    db "when Puff pushed    "
    db "his stack frame.    "
    db "                    "
    db "Puff the fractal    "
    db "dragon was written  "
    db "in C,               "
    db "And frolicked while "
    db "processes switched  "
    db "in mainframe memory."
    db "Puff the fractal    "
    db "dragon was written  "
    db "in C,               "
    db "And frolicked while "
    db "processes switched  "
    db "in mainframe memory."
    db "                    "
    db "Puff, he grew so    "
    db "quickly, while      "
    db "others moved like   "
    db "snails              "
    db "And mini-Puffs      "
    db "would perch         "
    db "themselves on his   "
    db "gigantic tail.      "
    db "All the student     "
    db "hackers loved that  "
    db "fractal Puff        "
    db "But DCS did not     "
    db "like Puff, and      "
    db "finally said,       "
    db "\"Enough!\"           "
    db "                    "
    db "Puff the fractal    "
    db "dragon was written  "
    db "in C,               "
    db "And frolicked while "
    db "processes switched  "
    db "in mainframe memory."
    db "Puff the fractal    "
    db "dragon was written  "
    db "in C,               "
    db "And frolicked while "
    db "processes switched  "
    db "in mainframe memory."
    db "                    "
    db "Puff used more      "
    db "resources than DCS  "
    db "could spare.        "
    db "The operator killed "
    db "Puff's job -- he    "
    db "didn't seem to care."
    db "A gloom fell on the "
    db "hackers; it seemed  "
    db "to be the end,      "
    db "But Puff trapped    "
    db "the exception, and  "
    db "grew from naught    "
    db "again!              "
    db "                    "
    db "Puff the fractal    "
    db "dragon was written  "
    db "in C,               "
    db "And frolicked while "
    db "processes switched  "
    db "in mainframe memory."
    db "Puff the fractal    "
    db "dragon was written  "
    db "in C,               "
    db "And frolicked while "
    db "processes switched  "
    db "in mainframe memory."
puffend:
maxpuff EQU puffend - 80

Here’s a video of the rotary encoder in action scrolling through the text of Puff The Fractal Dragon.

RC2024 – Part 9 – Using The LCD Module From A Z80 Assembly Language Program

I am planning on using an LCD screen as part of this year’s Retro Challenge. I want to use my new Rotary Encoder Module to be able to scroll the content on the screen.

I have bought the official RC2014 LCD Driver Module, and I have a 4*20 screen attached to it. This gives me 4 lines of 20 characters.

Spencer provides example BASIC code to control the screen. I want to control it using Z80 assembly language. Spencer does warn that this will not be as straight forward as the LCD will not respond fast enough. He does point us towards Mike Sutton’s blog for a solution. This involves us reading the status register on the LCD to see if it is ready for the next instruction.

I created two routines to send data to the LCD screen in Z80 assembly language. One to send a command byte and return when completed. The other to send a data byte and return when completed.

LCD_R   EQU 218
LCD_D   EQU 219

; Sends a command byte to the LCD.
; A - Command in
send_command:
    out (LCD_R),a
.lcd_busy:
    in a,(LCD_R)
    rlca
    jr c,.lcd_busy
    ret

; Sends a data byte to the LCD
; A - Byte in
send_data:
    out (LCD_D),a
.lcd_busy:
    in a,(LCD_R)
    rlca
    jr c,.lcd_busy
    ret

Another issue is the layout of the LCD screen. The first 20 characters are the first line, the next 20 are the third line, the next 20 are the second line, and the last 20 are the last line. This means we can’t simply loop through all characters in order to display them. Instead, we must be aware that every 20 characters we need to pick the next batch from elsewhere in memory.

In our case, the first line is at start of our data block. The second line is 40 bytes from the start. The third line is 20 bytes from the start. The fourth line is 60 bytes from the the start. This gives us 80 bytes in total.

This is how I solved this in Z80 assembly language.

; Display 4 lines of consecutive text on the LCD
; HL - address of text to display on the LCD
show_four_lines:
    ld b,20
.line1loop:
    ld a,(hl)
    inc hl
    call send_data
    djnz .line1loop

    ld de,20
    add hl,de
    ld b,20
.line2loop:
    ld a,(hl)
    inc hl
    call send_data
    djnz .line2loop    

    ld de,40
    sub hl,de
    ld b,20
.line3loop:
    ld a,(hl)
    inc hl
    call send_data
    djnz .line3loop

    ld de,20
    add hl,de
    ld b,20
.line4loop:
    ld a,(hl)
    inc hl
    call send_data
    djnz .line4loop 

    ret      


puff:
    db "Retro Challenge 2024"
    dc "                    "
    db "  LCD RC2014 Test   "
    db "    Robert Price    "

An alternative could be to store the lines in screen order. This would be easier to iterate, but would make the text harder for a human to read in the source code.

My final code looks like this. I have to send some commands to setup the LCD before I can display my text.

    OUTPUT LCD.z80

    ORG $9000

LCD_R   EQU 218
LCD_D   EQU 219

    ld a,56         ; Function 8 bit, 2 lines, 5x8 dot font
    call send_command
    ld a,12         ; Display on, cursor off, no blink
    call send_command
    ld a,1          ; clear the display
    call send_command

    ld hl,puff      ; the address of the text
    call show_four_lines

    ret

; Sends a command byte to the LCD.
; A - Command in
; A, C registers used.
send_command:
    out (LCD_R),a
.lcd_busy:
    in a,(LCD_R)
    rlca
    jr c,.lcd_busy
    ret

; Sends a data byte to the LCD
; A - Byte in
; A, C registers used.
send_data:
    out (LCD_D),a
.lcd_busy:
    in a,(LCD_R)
    rlca
    jr c,.lcd_busy
    ret

; Display 4 lines of consecutive text on the LCD
; HL - address of text to display on the LCD
; A, B, C, D, E, H, L registers used.
show_four_lines:
    ld b,20
.line1loop:
    ld a,(hl)
    inc hl
    call send_data
    djnz .line1loop

    ld de,20
    add hl,de
    ld b,20
.line2loop:
    ld a,(hl)
    inc hl
    call send_data
    djnz .line2loop    

    ld de,40
    sub hl,de
    ld b,20
.line3loop:
    ld a,(hl)
    inc hl
    call send_data
    djnz .line3loop

    ld de,20
    add hl,de
    ld b,20
.line4loop:
    ld a,(hl)
    inc hl
    call send_data
    djnz .line4loop 

    ret      

puff:
    db "Retro Challenge 2024"
    dc "                    "
    db "  LCD RC2014 Test   "
    db "    Robert Price    "

RC2024 – Part 8 – Refining The Rotary Encoder Z80 Assembly Language Program

In my last Retro Challenge post I was able to read the rotary encoder from Z80 assembly language on my RC2014 Classic 2 computer. However, the code was too sensitive. This was because I was looking for all changes to the CLK1 signal, instead of just the transition from low to high.

This turned out to be simpler than I though, and I was able to remove a large(ish) block of code. The old code I had a check_case1 and check_case2 that can be removed and replaced with the following.

; now work out what direction we are moving.
; if CLK1 is 1 then we can can check DT1 to get the
; direction of rotation. If it's 0, we need to go 
; back to the start of the loop.
    ld  a,(input)          
    and CLK1
    cp  CLK1 
    jr  nz, loop            

; this is where we check DT1. If 1 we are turning left.
    ld  a, (input)
    and DT1
    cp  0 
    jr  nz, left

In the first part I’m loading the input data from memory. I then mask off everything but the CLK1 bit and see if it is high. If it’s not, I go back to the start of the program.

In the second part I do the same for the DT1 bit, but check if it’s low. If high, I jump to the turning left code. If it is low, I carry on the to the turning right code, which directly follows.

The completed code looks like this.

    OUTPUT rotaryencoderledcontrol2.z80

; On the RC2014 Classic 2 running from BASIC the Z80
; code runs from address $9000. 
    ORG $9000

; The input and output ports to use.
INPUT_PORT  EQU $DE
OUTPUT_PORT EQU $03

; The input bits from the rotary encoder.
CLK1    EQU %00000001
DT1     EQU %00000010
SW1     EQU %00000100

; show the inital led value
    ld  a,(output)
    out (OUTPUT_PORT),a

loop:
; load the last clk value into register b
    ld  a,(lastclk)
    ld  b,a

; read the input port and store in "input"
    in  a,(INPUT_PORT)
    ld  (input),a

; now check if the switch on first rotary encode has been
; pressed. If it has jump to end
    and SW1
    cp  SW1
    jr  z, end

; now see if clk1 matches the lastclk. If it does loop
    ld  a,(input)
    and CLK1
    ld  (lastclk),a
    cp  b
    jr  z, loop

; now work out what direction we are moving.
; if CLK1 is 1 then we can can check DT1 to get the
; direction of rotation. If it's 0, we need to go 
; back to the start of the loop.
    ld  a,(input)          
    and CLK1
    cp  CLK1 
    jr  nz, loop            

; this is where we check DT1. If 1 we are turning left.
    ld  a, (input)
    and DT1
    cp  0 
    jr  nz, left

; we must be turning right so rotate the output to the right
; and store it before going back to the start of the loop.
right:
    ld  a,(output)
    rrca
    out (OUTPUT_PORT),a
    ld  (output),a

    jr  loop

; we must be turning left so rotate the output to the left
; and store it before going back to the start of the loop.
left:
    ld  a,(output)
    rlca
    out (OUTPUT_PORT),a
    ld  (output),a

    jp  loop

; the switch has been pressed, so we clear the output
; and exit.
end:
    ld  a,0
    out (OUTPUT_PORT),a

    ret

input:
    db  0
output:
    db  %00000001
lastclk:
    db  0

I also swapped the JP instructions to JR instructions because the code is small. This saves a few bytes.

This is only a small refinement, but it makes the Rotary Encoder Module so much more usable.

RC2024 – Part 7 – Reading The Rotary Encoder Using Z80 Assembly Language

Previously as part of this year’s Retro Challenge, I learnt how to build and run Z80 assembly language programs on my RC2014 Classic 2 computer.

I now want to recreate the BASIC program that moves LEDs using Z80 Assembly Language.

I am using IO address $DE for my Rotary Encoder module. I also have the Digital I/O module using IO address $03. I can define these as constants so I can easily change them if necessary.

I need to store values for the input, the output, and the last value of the CLK pin. The input will just be the value from the IN operation. The output value will be the byte we want to show on the LED output. I will set this to be %00000001 initially. When I turn the encoder, I want this to shift to either the left or right. If it reaches the edge, I want it to wrap. The Z80 operations RLCA and RRCA will do this for me.

To check if a bit is high or low, I can use an AND operation to mask out out other values. For example, to check if the SW1 is being pressed, I can do the following.

    SW1         EQU %00000100
    INPUT_PORT  EQU $DE

    in a,(INPUT_PORT)
    and SW1
    cp SW1
    jp z, end    ; jump to end if SW is high.

I can repeat this logic to check the values of CLK1 and DT1.

This is the code I have come up with.

    OUTPUT rotaryencoderledcontrol.z80

; On the RC2014 Classic 2 running from BASIC the Z80
; code runs from address $9000. 
    ORG $9000

; The input and output ports to use.
INPUT_PORT  EQU $DE
OUTPUT_PORT EQU $03

; The input bits from the rotary encoder.
CLK1    EQU %00000001
DT1     EQU %00000010
SW1     EQU %00000100

; show the inital led value
    ld a,(output)
    out (OUTPUT_PORT),a

loop:
; load the last clk value into register b
    ld a,(lastclk)
    ld b,a

; read the input port and store in "input"
    in a,(INPUT_PORT)
    ld (input),a

; now check if the switch on first rotary encode has been
; pressed. If it has jump to end
    and SW1
    cp SW1
    jp z, end

; now see if clk1 matches the lastclk. If it does loop
    ld a,(input)
    and CLK1
    ld (lastclk),a
    cp b
    jp z, loop

; now work out what direction we are moving
check_case1:
    ld  a,(input)          
    and CLK1
    cp  CLK1             
    jr  nz, check_case2

    ld  a,(input)         
    and DT1
    cp  DT1
    jr  z, left     ; if both CLK and DT are high then left

check_case2:
    ld  a, (input)
    and CLK1
    cp  0 
    jr  nz, right

    ld  a, (input)
    and DT1
    cp  0 
    jr  nz, right  

; we must be turning left so rotate the output to the left
; and store it before going back to the start of the loop.
left:
    ld a,(output)
    rlca
    out (OUTPUT_PORT),a
    ld (output),a

    jp loop

; we must be turning right so rotate the output to the right
; and store it before going back to the start of the loop.
right:
    ld a,(output)
    rrca
    out (OUTPUT_PORT),a
    ld (output),a

    jp loop

; the switch has been pressed, so we clear the output
; and exit.
end:
    ld a,0
    out (OUTPUT_PORT),a

    ret

input:
    db  0
output:
    db  %00000001
lastclk:
    db  0

The LED successfully moves left and right depending on how I turn the rotary encoder. However, because I am checking in both high and low states of CLK1, it is moving two steps per turn. This will be too senstive to use in an application, so my next job is to change this to check once per turn.

RC2024 – Part 6 – Getting Z80 Assembly Language Programs On To The RC2014 Classic 2

As part of this year’s Retro Challenge, I am building a rotary encoder module for the RC2014 computer.

I have built a custom PCB, and I can use it from BASIC. However, I would also like to be able to use it from Z80 machine code.

To run Z80 machine code on my RC2014 Classic 2 I have a few options.

  1. Use BASIC to load in a hex dump of assembled code.
  2. Use the SCM ROM image to load in a hex dump of assembled code.
  3. Burn my assembled code into a ROM and insert that into the RC2014.

I have designed a new ROM PCB to help me do options 2 and 3 in the future. For now, I will use BASIC to load in assembled code and run it.

Before I can load assembled code, I need to write and assemble it.

I am going to do this on an Apple Macbook Pro. I’m going to need an assembler, and something to create hex dumps from the assembled code.

I am going to use SJASMPLUS as my Z80 assembler. On a Mac this needs to be built from the source code. In a terminal window the following should work.

make clean
make
sudo make install

To create the hex files, I am going to use the z88dk-appmake command from z88dk. z88dk is also provides an assembler and a C compiler that can build applications for the RC2014. I’m not going to use these at the moment. There are installation instructions and a binary that can easily be installed on a Mac.

You can use any text editor you want, but I’m going to be using Visual Studio Code. I’m also using the Z80 Assembly extension for syntax highlighting.

I’m going to write a simple Z80 assembly language program to read the the input from the rotary encoder and show it on the Digital I/O module’s LEDs. It’s going to exit when the rotary encoder’s switch is pressed.

The Rotary Encoder module is on input address $DE. The Digital I/O module is on input address $03.

    OUTPUT rotaryencodertest.z80

; On the RC2014 Classic 2 running from BASIC the Z80
; code runs from address $9000. 
    ORG $9000

; The input and output ports to use.
INPUT_PORT  EQU $DE
OUTPUT_PORT EQU $03

; The input bits from the rotary encoder.
CLK1    EQU $1
DT1     EQU $2
SW1     EQU $4


loop:
; read the input port
    in a,(INPUT_PORT)
; send the input directly to the output port
    out (OUTPUT_PORT),a

; now check if the switch on first rotary encode has been
; pressed. If it hasn't, loop back.
    and SW1
    cp SW1
    jp nz, loop

; the switch has been pressed, so we clear the output
; and exit.
    ld a,0
    out (OUTPUT_PORT),a

    ret

I’ve saved this as rotaryencodertest.s.

To assemble to code I need to use the following line in a terminal…

sjasmplus rotaryencodertest.s

To convert the output to intel format hex, I need to use the following line in a terminal…

z88dk-appmake +hex --org 0x9000 -b rotaryencodertest.z80

I should now I have a file called rotaryencodertest.ihx.

To load this onto the RC2014 Classic 2, I can use the example hexload.bas program. I’ll include the full code here.

new
clear
10 REM Created by Filippo Bergamasco,
11 REM and modified by DaveP for the RC2014
12 REM Adapted for z88dk by feilipu
20 REM Version 1.0
30 Print "Loading Data"
40 let mb=&H8900
50 print "Start Address: ";hex$(mb)
60 REM Go to READ Subroutine.
70 GOSUB 1000
80 print "End Address:   ";hex$(mb-1)

90 REM Change USR(0) Pointer for HexLoad
100 GOSUB 1100

110 REM RUN THE HEXLOAD CODE!
120 print usr(0)

130 REM Change USR(0) Pointer to 0x9000
140 GOSUB 1200

150 REM RUN THE PROGRAMME CODE!
160 print usr(0)
170 END 

1000 REM Routine to load Data
1010 REM Needs var mb set to start location
1020 read a
1030 if a>255 then RETURN
1040 rem print HEX$(mb),a
1050 poke mb, a
1060 let mb=mb+1
1070 goto 1020

1100 REM Location of usr address &H8049
1110 print "USR(0) -> HexLoad"
1120 let mb=&H8049 
1130 doke mb, &H8900
1140 RETURN 

1200 REM Location of usr address &H8049
1210 print "USR(0) -> 0x9000, z88dk default"
1220 let mb=&H8049
1230 doke mb, &H9000
1240 RETURN

9010 data 33,116,137,205,109,137,215,254,58,32,251,14
9040 data 0,205,83,137,71,205,83,137,87,205,83,137
9070 data 95,205,83,137,254,1,40,23,254,0,32,33
9100 data 205,83,137,18,19,16,249,205,83,137,121,183
9130 data 32,26,62,35,207,24,207,205,83,137,121,183
9160 data 32,14,33,206,137,205,109,137,201,33,172,137
9190 data 205,109,137,201,33,189,137,205,109,137,201,205
9220 data 100,137,7,7,7,7,111,205,100,137,181,111
9250 data 129,79,125,201,215,214,48,254,10,216,214,7
9280 data 201,126,183,200,207,35,24,249,72,69,88,32
9310 data 76,79,65,68,69,82,32,98,121,32,70,105
9340 data 108,105,112,112,111,32,66,101,114,103,97,109
9370 data 97,115,99,111,32,38,32,102,101,105,108,105
9400 data 112,117,32,102,111,114,32,122,56,56,100,107
9430 data 10,13,58,0,10,13,73,110,118,97,108,105
9460 data 100,32,84,121,112,101,10,13,0,10,13,66
9490 data 97,100,32,67,104,101,99,107,115,117,109,10
9520 data 13,0,10,13,68,111,110,101,10,13,0,0
9550 data 999
9999 END
run

Paste this into a terminal connected to the RC2014 and it will prompt you to enter hex. Cut and paste the rotaryencodertext.ihx and it should load and execute.

Now turning the rotary encoder will show the binary input on the output LEDs. Pressing the switch on the rotary encoder attached to port 1 will return you to BASIC.

This is it running. Ignore the poor soldering on the Digital I/O board. I did that a few years ago and (I think) I have improved since then.

Taking part in Retro Challenge 2024

It’s the 20th anniversary of the Retro Challenge, and also the 10th anniversary of the RC2014 Z80 computer.

So to celebrate, I’m planning on taking part this year, and undertaking a project with my own RC2014 computer. The idea is to complete a project using a retro computer in a month. The RC2014 itself was an entry in 2014.

As I will only have a month to try to complete a project, I’m looking for something achievable.

My plan at the moment is to look at adding rotary encoders to my RC2014, and to be able to read them using Z80 machine code. A rotary encoder is a sensor that measures the rotational position and speed of an object by converting the motion into an electrical signal. An example would be the scroll wheel on a computer mouse.

I have ordered a couple of rotary encoders ahead of the start date so they should be here in time for the start of the project. I have used a rotary encoder on an Arduino a few years ago, but that had a library to simplify the process. This time I will have to write from scratch, and it will give me a good opportunity to write some real Z80 assembly.

I will also need to design a circuit board, and for this I plan to use EasyEDA. This is an area I really need to learn more about, so the challenge gives me a good excuse to get stuck in with it. I will need to start on this early as ordering a PCB can take a few weeks to arrive, so I won’t have time for multiple revisions if I make a mistake.

Let’s see if I can produce something by the end of the project!