When I signed up for the challenge I wanted to look at adding rotary encoders to the RC2014 platform. This I achieved. I now have several working modules and example applications, including a music player.
I ended up designing and building 4 versions of the rotary encoder module that plugs into the RC2014. They all worked, but they all improved on the previous versions.
One mistake I consistently made on my early boards was not setting a suitable spoke width to my ground plane in EasyEDA. This made the boards hard to solder when connected to ground as the heat from my soldering iron was being wicked away. I finally fixed this on the last two versions of the module.
Spencer Owen (the designer of the RC2014) has been very supportive through the development of the module. He has even assigned port D7 (and A7 as backup) for rotary encoders. This means others can design their own rotary encoder modules and use the same port to avoid clashes with other modules.
I have learnt a lot about Z80 assembly language programming. I am certainly no expert, but I know a lot more than I did a month ago. I even now own a copy of Rodnay Zak’s classic book Programming The Z80.
I built a development environment that allowed me to quickly build and test new programs directly on the RC2014 from my Macbook. When I first started out I was running multiple commands by hand each time, and having to cut and paste intermediate hex code into a terminal. Now, I just run a single build task in Visual Studio Code, and my program is assembled, sent to the RC2014, and run there. I even build a module that allows me to quickly change ROMs on the RC2014.
As well as learning how to decode the input from my rotary encoders, I have learnt how to program the LCD module, and the sound module. These extra modules all work together through my new assembly language programs.
My code has all targeted the RC2014 Classic 2 computer. This was the first RC2014 I bought. However, I did also buy a RC2014 Zed Pro Pride last year. This supports CP/M. I was too nervous to built it initially, but through building my rotary encoder modules I gained confidence and decided to just get on with building it. It took me far longer than I expected. I completed it only a few days before the end of the Retro Challenge. This means I’ve not had time to look at CP/M development yet. This could be next year’s challenge!
All in all, I’ve had a great time designing and building the rotary encoder module and supporting code. I’ve learnt a lot along the way, and I’m really happy with the outcome.
I’m looking forward to doing more with the RC2014 going forward.
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.
Line
Offset
1
0
2
64
3
20
4
84
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.
There are two versions, one that allows any address between 0 and FF to be selected. The other uses only address D7 or A7. These are the addresses that Spencer has reserved for Rotary Encoders on the RC2014.
Apart from the addressing, the main changes were to add pull up resistors to the input pins. This means when a rotary isn’t attached, they are held high. This matches the behaviour when a rotary encoder is attached. To save space on the PCB, I swapped from using separate resistors to using resistor arrays.
They work perfectly.
I do like the look of the two new boards. They look like a real product.
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.
Earlier on in the Retro Challenge I wrote about designing a new ROM board for my RC2014. This was going to be higher than the standard narrow board for the RC2014 Classic 2 computer. It was also going to have switches on it to swap the memory map around, and a ZIF socket to allow me to program new ROMs.
The updated PCB arrived from JLCPCB, so I attempted to solder it up. I made the same mistake as on my Rotary Encoder Module PCB, and left the spokes to the ground plane too large. This made it difficult to solder the ground pins as the heat from my soldering iron was being wicked away. It took some time, but I was eventually able to solder those pins.
I removed the old ROM module and swapped the ROM chip over into my new board. I turned on and… nothing. I checked my soldering and it all looked OK. I then touched the ROM chip, ouch! That super hot. What had a done wrong? I went back to check my circuit diagram and that all looked right. The traces were going to the right places. Then I realised what I had done. The comment I’d put on the board to remind me which way round the ROM chip went as pointing in the wrong direction. Reversing the chip brought my RC2014 back to life. Thankfully I hadn’t destroyed the chip.
By default the board booted into Microsoft BASIC. So I switched it off, and flipped the switches to all be on. Turning the power back on, I was greeted by the SCM (Small Computer Monitor). The board was working as expected! I could now easily swap between BASIC and SCM without removing the card and having to swap jumpers on the board.
Using My Own ROM
Apart from being able to swap between BASIC and SCM, I wanted to be able to use my own ROM chip.
I have some Winbond 27C512 EEPROM chips. These are pin compatible with PROM chip that Spencer supplies with the RC2014 Classic 2, but are reprogrammable.
I have a Xgecu T48 programmer for programming chips, but I could not get this to work on Windows 11 using Parallels on my Mac. Although it sees it as a USB device, the programming software would not recognise it. I needed a Mac solution.
For this I found minipro. It’s command line only on the Mac, but it works with the Xgecu T48 programmer. It can easily be installed using Homebrew
I was able to easily swap the chips over in my new ROM board thanks to the ZIF socket. I turned on the RC2014, and I was greeted with the SCM prompt.
I now have the ability to program my own ROM chips and use them on my RC2014 Classic 2.
Next Steps
I have tweaked the PCB layout so the spoke to the ground plane are a more sensible size. More importantly, I have corrected the text showing which way round the ROM chip should be placed in the ZIF socket. While I was tweaking the layout, I decided to rotate the 74HCT32 chip so it had the same orientation as the as the ROM chip. I also moved the decoupling capacitors to the left hand side so they are next to the the 5V pins.
This has been sent to JLCPCB for manufacture. As the current board works, I have used their cheapest and slowest service. The new board should be with me in a few weeks.
The biggest change I wanted to make was to introduce a sensible spoke size to the ground plane. Imagine my disappointment when I still struggled to solder to ground. I went back and looked at my design. The ground plane spoke sizes looked sensible at 0.25mm. Then I realised what I had done. I had placed a ground plane on both the front and back of the PCB. I never changed the spoke sizes on the back. This meant as I was soldering the heat was being wicked away in the large copper ground plane.
I did eventually complete the PCB, and this time I used blue switches for the address port select and for the LEDs. This helps me tell the two different versions of the boards apart.
Plugging it in and running my tests, I was happy to see everything still worked.
She mentioned she had spoken to Spencer Owen about the best port to use. I have followed her example and reached out to Spencer. He has kindly reserved port D7 for rotary encoders on the RC2014. There is also A7 as a reserve port in case of clashes. This port can be used by other rotary encoder modules in the future, not just mine.
One thing Spencer mentioned was about making it simpler to set an address port on the board. Rather than having 8 switches that allow any 8 bit port address to be used, a simple jumper to allow either D7 or A7 could be used.
I have taken this on board and decided to design two more versions of the PCB.
The first is essentially the same as my existing board, but with a single ground plane to the rear of the PCB. This also has smaller spoke sizes for ground so should be easier to solder.
The second is a version without the full 8 bit port select switch. Instead I now have a single jumper to allow either D7 or A7 to be selected.
This is what the binary forms of D7 and A7 look like. They are the same apart from bits A4, A5, and A6.
D7 1101 0111 A7 1010 0111
To allow the switching bits A6, A5, and A4 must be inverted when the jumper is swapped. I can just connect the jumper to either GND or +5V, that will allow A6 and A4 to swap, but A5 must always be the inverse of them. This will need to use an inverter.
Rather than adding an extra chip to invert a single value, I decided I could swap the 74HCT32 OR chip to a 74HCT02 NOR gate. I was only using one of the existing OR gates, so swapping that over to a NOR, then running that through another joined NOR gate will give me the same result. I can also use one of the spare gates as an inverter for A5.
When no rotary encoder is connected
At present, when no rotary encoder to port 1 or 2, the output for the Schmitt Trigger is high. When a rotary encoder is present it is low. This could cause a false reading as I check for high in my code.
To solve this, I have added a pull up resistor array between the rotary encoder pins and the Schmitt Trigger. This is 100k resistor connected to +5v.
To test this works I added a 100k resistor array to a breadboard and connected it between the PCB and Rotary Encoder. When the encoder was present it worked as expected. When I removed the encoder, it also worked.
I have added this array to both of the new PCBs.
The final change was to remove the ground hook from the top right of the board. This was only for testing and ground is available on the debug port anyway.
Will the new PCBs arrive in time
There are only 9 days left before the official end of the 2024 RetroChallenge. I have ordered the new PCBs from JLCPCB, but I don’t know if they will arrive in time. I have also had to order some NOR gates from AliExpress, so that will also be over a week to arrive.
Will they arrive before the end of the challenge? I hope so!
The existing PCBs work, I just know they could be better.
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.
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.
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 "
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.
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.