RC2025 – Part 5 – Writing a reusable Z80 subroutine to read the NES controller

I wanted to write a reusable Z80 assembly language subroutine to read the NES controller and return the status of the buttons in a register.

The principle is the same as my previous posts in that I have to pulse the Latch and Clock lines, and read data 1 bit at a time from the Data line. However, this time I want to store all 8 returned bits in a single byte that I can return in a register.

I had some feedback from Jon Jones on Bluesky who suggested I could optimise my previous code by replacing the ld a,0 instruction with xor a. This is a byte smaller, and is quite a neat trick as a value exclusive or’d with itself will always be 0.

I have also been learning more about how shifting and rotating works on the Z80. When I read my bit from the data line, if I use a slr a instruction, it will push the bit that has just been read onto the carry flag. I can then use rr c to move the contents of register c one to the right and fill bit 7 with the contents of the carry flag. Doing this 8 times will fill the c register with all the values I need.

The finished subroutine looks like this.

; NESController.inc
; Definitions and subroutine to read NES controller button states
; Robert Price - 19th October 2025


; The Z80 port address to use for the controller interface.
NES_PORT    EQU $01

; The bit masks for the controller interface lines.
CLOCK   EQU $01
LATCH   EQU $02
DATA    EQU $01


; subroutine to read the NES controller button states.
; returns the button states in register a.
; all other registers are preserved.
get_buttons:
        push bc           ; save bc registers

; pulse the LATCH line low to high and back to low again.
        xor a               ; set a to 0 
        out (NES_PORT), a
        ld a, LATCH
        out (NES_PORT), a
        xor a               ; set a to 0
        out (NES_PORT), a

; setup the loop counter to read 8 buttons.
        ld b, 8             ; 8 buttons to read. This will be decremented to 0.

; now we rebuild the byte into register c. As we are setting all 8 bits,
; we don't need to worry about clearing c first.
.loop:
; read the controller button states
        in a, (NES_PORT)    ; read the DATA line

; move the DATA bit into register c to rebuild the send byte
        srl a               ; shift right to get the DATA bit into carry
        rr c                ; rotate carry into bit 7 of c

; pulse the CLOCK line to read the next button.
        ld a, CLOCK
        out (NES_PORT), a
        xor a               ; set a to 0
        out (NES_PORT), a

; loop 8 times to read all buttons.
        djnz .loop

; return the button states in register a
        ld a, c             ; put the final button states in a 

        pop bc              ; restore bc registers

        ret                 ; return to caller with button states in a

Testing the subroutine

To test the new subroutine I made use of the SC134 LED output card I was kindly given at RC2014 Assembly. The standard RC2014 Digital IO card will also work. I have assigned this to IO address 2.

The code simply reads the NES controller, inverts the bits using the cpl instruction, outputs the value to the LEDs, and loops.

I need to use the cpl instruction as the NES controller returns a pressed button as 0. If I didn’t do this, the LEDs would be on apart from any button being pressed. cpl inverts all the bits in the a register so any button being pressed is now lit.

; the Z80 port address to use for the LED output.
LED_PORT    EQU $02

main:
; get the button states from the NES controller
        call get_buttons     ; call the get_buttons subroutine
        cpl                  ; invert the bits in register a so pressed buttons are 1, and unpressed are 0
        out (LED_PORT), a    ; output the button states to the LED port
        jr main              ; repeat forever

Here is a video of the code in action.

RC2025 – Part 4 – Bit-banging a NES controller using Z80 assembly language

In my last post, I wrote about using MS-Basic on my RC2014 to bit-bang an NES controller. In this post, I want to use Z80 assembly language to do the same. I want to be able to press a button on the controller and have its name printed on the serial terminal.

The first thing we need to do is set up a few things.

The code will need to run at 0x9000, so an ORG statement will tell the assembler to build the code to run from there.

        ORG $9000

The NES controller module will be located at the Z80 IO port 1.

PORT    EQU $01

The Clock bit is 1, the Latch bit is 2, and the Data bit is 1.

CLOCK   EQU $01
LATCH   EQU $02
DATA    EQU $01

I’ll be using Stephen Cousins’ SCM on the RC2014. The SCM API provides a routine to print a string, so I’ll be using that later on. This is API call 0x06.

OUTPUT_LINE EQU $06

The first thing the code will need to do is to pulse the Latch line low, high, low. This will capture the current state of the buttons being pressed on the NES controller.

start:
        ld a, 0
        out (PORT), a
        ld a, LATCH
        out (PORT), a
        ld a, 0
        out (PORT), a

Now we need to loop 8 times to capture each of the 8 bits the NES controller is going to be sending us.

        ld b, 8 
loop:
        djnz loop

Inside the loop, we first need to read the Data line to get the current bit. We mask out all but the Data bit and then see if it is 0 or not. If it’s not zero, skip the next block of code.

        in a, (PORT)

        and DATA
        jr nz, .skip

If a button is being pressed, we need to look up the name of the button in a lookup table and print it out using the SCM API. To do this, we get the current iteration minus 1 and store it in the HL register. As this is a 16 bit register, we need to set the L register to the iteration value, and H to 0. We add this value to the address of the lookup table to get the address of the string to print. We then pass this to the SCM API.

        ld de, lookuptable  ; point to the lookup table
        ld a, b             ; put the current iteration from b into a
        dec a               ; delete 1 to make it zero based
        ld l, a             ; place the interation in l
        ld h, 0             ; zero h, hl should now be value of the iteration
        add hl, hl          ; multiply by 2 (size of address)
        add hl, de          ; add to base address of table
        ld e, (hl)          ; get low byte of string address to use
        inc hl              ; point to high byte
        ld d, (hl)          ; get high byte of string address
        ld c, OUTPUT_LINE   ; SCM output line
        push bc             ; save the bc registers to the stack
        rst $30             ; Call SCM API
        pop bc              ; restore the bc registers from the stack
lookuptable:
        dw right_txt
        dw left_txt
        dw down_txt
        dw up_txt
        dw start_txt
        dw select_txt
        dw b_txt
        dw a_txt

a_txt:      db "A",5,0
b_txt:      db "B",5,0
select_txt: db "Select",5,0
start_txt:  db "Start",5,0
up_txt:     db "Up",5,0
down_txt:   db "Down",5,0
left_txt:   db "Left",5,0
right_txt:  db "Right",5,0

Next, we pulse the CLOCK line high to low before we end the loop.

.skip:
        ld a, CLOCK
        out (PORT), a
        ld a, 0
        out (PORT), a

Finally, we loop back to the very start of the program.

        jr start

The complete Z80 assembly language program

Here’s the code as a single program that can be assembled using the sjasmplus assembler.

 ; A simple button reading program for the RC2014 Z80 computer running SCM
 ; Robert Price - 15th October 2025

        ORG $9000

; The Z80 port address to use for the controller interface.
PORT    EQU $01

; The bit masks for the controller interface lines.
CLOCK   EQU $01
LATCH   EQU $02
DATA    EQU $01

; The SCM API value to output a line.
OUTPUT_LINE EQU $06

start:
; pulse the LATCH line low to high and back to low again.
        ld a, 0
        out (PORT), a
        ld a, LATCH
        out (PORT), a
        ld a, 0
        out (PORT), a

; setup the loop counter to read 8 buttons.
        ld b, 8             ; 8 buttons to read. This will be decremented to 0.
loop:
; read the controller button states
        in a, (PORT)        ; read the DATA line

        and DATA            ; mask out all but DATA bit
        jr nz, .skip        ; skip if a button was not pressed

; print out the button pressed using a lookup table.
        ld de, lookuptable  ; point to the lookup table
        ld a, b             ; put the current iteration from b into a
        dec a               ; delete 1 to make it zero based
        ld l, a             ; place the interation in l
        ld h, 0             ; zero h, hl should now be value of the iteration
        add hl, hl          ; multiply by 2 (size of address)
        add hl, de          ; add to base address of table
        ld e, (hl)          ; get low byte of string address to use
        inc hl              ; point to high byte
        ld d, (hl)          ; get high byte of string address
        ld c, OUTPUT_LINE   ; SCM output line
        push bc             ; save the bc registers to the stack
        rst $30             ; Call SCM API
        pop bc              ; restore the bc registers from the stack

.skip:
; pulse the CLOCK line to read the next button.
        ld a, CLOCK
        out (PORT), a
        ld a, 0
        out (PORT), a

; loop 8 times to read all buttons.
        djnz loop

; forever loop to read buttons again.
        jr start



; the lookup table stores the addresses of the text strings for each button.
lookuptable:
        dw right_txt
        dw left_txt
        dw down_txt
        dw up_txt
        dw start_txt
        dw select_txt
        dw b_txt
        dw a_txt

; The text strings to print for each button.
; each string is terminated with a CR (5) and a null (0).
a_txt:      db "A",5,0
b_txt:      db "B",5,0
select_txt: db "Select",5,0
start_txt:  db "Start",5,0
up_txt:     db "Up",5,0
down_txt:   db "Down",5,0
left_txt:   db "Left",5,0
right_txt:  db "Right",5,0

Once assembled, I convert it to Intel Hex using z88dk-appmake and send it to my RC2014 running SCM. It is then executed using g 9000 .

It is very fast code, so even the slightest tap of a button will register multiple times.

RC2025 – Part 3 – Bit-banging a NES controller from BASIC

In my earlier articles, I’ve written about how an NES controller works and designed some hardware for the RC2014 to talk to one.

Since then, my PCBs have arrived, and I’ve built the RC2014 module. I had to cut the track from RESET I was using to enable one of the chips and wire that to GND. I explained why in my previous post.

RC2014 NES Controller module with bodge wire in place

I’ve given the module IO address 1 using the jumper switches. This means I can talk to it using the MS-Basic commands OUT 1,X for output, and INP(1) for input.

RC2014 NES Controller module plugged into an RC2014 Picasso

As the output from the NES controller is active low, the green LED attached to the Data input is lit unless a button is being pressed. I should probably have wired this so it only lights if a button is being pressed. This is just a cosmetic issue.

First BASIC program

The first thing I need to do to talk to the NES controller is to toggle the Latch line. This is D1. I need to make this go from low to high, then back to low.

Next, I need to iterate the following 8 times, 1 for each bit. Read the input and toggle the Clock line high to low to get the next bit ready. If I see a zero value on my input, I know a button has been pressed.

Finally, I restart the program from the beginning.

10 REM Bit Banging an NES Controller from an RC2014.
11 REM Robert Price - October 2025.
20 REM First we toggle the Latch line on D1. 
30 OUT 1,0
40 OUT 1,2
50 OUT 1,0
60 REM Iterate 8 times to get all 8 bits.
70 FOR I=0 TO 7
80 REM Get the current bit in D0.
90 LET A= INP(1)
100 REM Toggle the Clock line in D0.
110 OUT 1,1
120 OUT 1,0
130 REM If we have a non zero value, then a button has been pressed.
140 IF A=0 THEN PRINT I
150 NEXT I
160 REM loop back to the beginning.
170 GOTO 10

When I run this code and press a button, the value of I is printed on the terminal.

Improving the BASIC program

Instead of just printing a numeric value, I can print the name of the button being pressed. In my earlier article, I listed what these values were. To do this, I look at the value of variable I, and print what that value actually means.

10 REM Bit Banging an NES Controller from an RC2014.
11 REM Robert Price - October 2025.
20 REM First we toggle the Latch line on D1. 
30 OUT 1,0
40 OUT 1,2
50 OUT 1,0
60 REM Iterate 8 times to get all 8 bits.
70 FOR I=0 TO 7
80 REM Get the current bit in D0.
90 LET A= INP(1)
100 REM Toggle the Clock line in D0.
110 OUT 1,1
120 OUT 1,0
130 REM If we have a non zero value, then a button has been pressed.
140 IF A<>0 THEN GOTO 230
150 IF I=0 THEN PRINT "A"
160 IF I=1 THEN PRINT "B"
170 IF I=2 THEN PRINT "SELECT"
180 IF I=3 THEN PRINT "START"
190 IF I=4 THEN PRINT "UP"
200 IF I=5 THEN PRINT "DOWN"
210 IF I=6 THEN PRINT "LEFT"
220 IF I=7 THEN PRINT "RIGHT" 
230 NEXT I
240 REM loop back to the beginning.
250 GOTO 10

Running this and pressing a button now prints the name of that button to the terminal.

RC2025 – Part 2 – Designing a PCB for bit-banging a NES controller

In the first part of my RetroChallenge, I ended by saying I was planning to bit-bang the data from the NES controller into my RC2014.

Now it’s time to design a PCB to let me connect the NES controller to the RC2014.

I’m going to use Z80 IO, but I’m not sure what address yet. I’m going to use a 74HCT688 8-bit comparator to allow myself the ability to select a specific IO port on the Z80. I will use a DIP switch with pull-down resistors so I can change the IO port at will without having to get a new PCB made. The /IORQ line from the Z80 will be used to enable the 74HCT688.

I need to send Clock and Latch signals to the NES controller, so I need a way to output data on the Z80 IO port. I will use a 74HCT374 to do this. This will allow me to buffer the data lines from Z80. I will enable output by using a 74HCT02 and NORing the output from the 74HCT688 and the Z80 /WR line.

For reading data back to the Z80, I will use a 74HCT245. This will let me put data onto the databus when the correct IO Port is being read. I will use the 74HCT02 to NOR the output of 74HCT688 and the Z80 /RD line to enable the chip. However, this needs to be inverted to work, so I will just use another NOR gate as a NOT gate to do this.

I will add LEDs on the input and output lines to the NES controller socket to help with any debugging I may need to do. I will also add plenty of test points to see what is going on during debugging. As there are plenty of unused input and output pins, I will expose these on headers for possible future use.

Bit-banging PCB for the NES controller on an RC2014

I have decided to use the following pins to talk to the NES controller.

PINREAD / WRITENES
D0READDATA
D0WRITECLOCK
D1WRITELATCH

Laying out the PCB

I have laid out the PCB using Spencer Owen’s guidelines for RC2014 modules.

The spare output pins have been placed at the top for easy access. The NES controller socket has been placed on the right-hand side, along with the test points for it. The indicator LEDs have been placed above this for visibility.

I have added a ground plane to the back of the PCB, and let EasyEDA handle the routing for me.

PCB layout for the NES controller module

You may have noticed that the diagram is for revision 1.1 of the circuit. 1.0 was essentially the same, but I had the output enable on the 74HCT374 connected to the Z80 /RST line. The idea being I only want this to present data on the bus if the Z80 wasn’t being reset. This pin is only low on reset, so data was never being presented. I just tied this to GND in revision 1.1.

Unfortunately, revision 1.0 is the version I sent off to be manufactured, so it will need a bodge when I come to solder up the PCB. The trace from /RST will need to be cut, and OE will need to be wired to GND.

RC2025 – Part 1 – How does the NES controller send data?

For this year’s RetroChallenge, I am hoping to interface an NES / Famicon controller to my RC2014 computer.

The NES controller has 8 buttons: Up, Down, Left, Right, Select, Start, A, and B.

The cable only has 7 pins, so how can it send up to 8 button presses using 7 wires?

Well, the NES controller only actually uses 5 of those wires. Two of those wires are GND and +5V. The other three are Latch, Clock, and Data.

A diagram of the NES controller port showing pin outs.

Inside the controller, there is a 4021 shift register. When the Latch is pulsed from high to low, the 4021 captures the state of the buttons in 8 bits. The first capture bit will then be placed on the Data line. When Clock is pulsed from high to low, the 4021 will then shift the internal data one place forward, and the next bit will be placed on the Data line. When the next Clock pulse arrives, the next bit is sent, and so on. This needs to happen 8 times until all 8 bits have been sent. A low bit on the Data line means the button has been pressed; otherwise, the bit will remain high.

The bits are sent in the following order.

BitButton
0A
1B
2Select
3Start
4Up
5Down
6Left
7Right

Using software bit-banging, I should be able to send the Latch and Clock signals, and reassemble the received 8 bits back into a single byte.

RC2025 – Taking part in RetroChallenge 2025

After taking part in the very enjoyable RetroChallenge in 2024, I’ve decided to take part again in 2025.

Last year, I worked on a rotary encoder module for the RC2014 computer.

This year, my plan is to interface a classic Nintento Entertainment System joypad to my RC2014.

There is already a module for reading Atari-style joysticks, but I don’t think there is anything for a NES controller.

There are 8 buttons on the NES controller, but the connector has only 7 pins. This means that there is something interesting going on, and it’s not just each button shorting one pin.

Using a ZX Printer on an RC2014

In 1981, Sinclair Research introduced the ZX Printer. This was a small printer that connected to their ZX81 computer. It used special paper with an aluminium coating that could be burnt off by a passing print head. Sinclair’s later ZX Spectrum computer was also compatible with the ZX Printer.

A compatible printer called the Alphacom 32 / Timex Sinclair 2040 was also released. This used standard thermal paper. It used an external power supply instead of relying on that of the ZX81 or ZX Spectrum.

Both the ZX81 and ZX Spectrum are Z80-based, and so is the RC2014 computer. So I wondered if the RC2014 could use a ZX Printer? The belt inside my ZX Printer has perished and no longer works. I do have a working Alphacom 32 printer. Researching how I could interface the two devices, I found that Spencer Owen had already attempted this. He was able to get some output on the printer, but nothing usable.

The ZX Printer uses the following lines from the Z80 microprocessor.

  • A2 – LOW when addressing the printer.
  • IORQ – LOW when addressing the printer.
  • RD – LOW when reading
  • WR – LOW when writing
  • D0 – HIGH when reading if the printer is ready for the next data bit
  • D1 – Write HIGH to slow the motor. Write LOW for a faster motor speed.
  • D2 – Write HIGH to stop the motor, write LOW to start the motor.
  • D6 – LOW when reading if the printer is present.
  • D7 – HIGH when reading if it’s the start of a new line. Write HIGH to print a bit.
  • A7 – HIGH on the Alphacom 32 / Timex Sinclair 2040 when addressing the printer. The ZX Printer ignores this.

The printer also uses GND and +5V. The ZX Printer also uses +9V on the ZX81 and ZX Spectrum edge connector. The Alphacom 32 / Timex Sinclair 2040 doesn’t use +9V.

As decoding on just A2 would mean the printer showing on many addresses, I decided to use a 74HCT688 to decode A0 to A7 instead. This means I can avoid IO address clashes with other RC2014 modules. I decided to make this configurable via DIP switches to make it easy to move IO addresses. I send the output from the 74HCT688 to A2 on the printer. I wired A7 to +5V as I no longer need to decode this. I pass the other lines from the RC2014 to a ZX Spectrum compatible edge connector.

As the RC2014 doesn’t have a +9V power supply, I added a barrel socket for an external supply. I wired this to be center negative so I could use a ZX Spectrum power supply. As I don’t have a working ZX Printer, I’ve not been able to test this part of the circuit yet. The markings on the PCB are from the footprint, and these are the wrong way round.

RC2014 ZX Printer module circuit diagram
The RC2014 ZX Printer module

Example Z80 Assembly Language

I set the DIP switches on my board to port $1. I also have an RC2014 Digital IO module on port $0. In my assembly language program, I use this to show the current line being printed, but this is optional.

The ZX Printer’s resolution is 256 pixels wide, so for this example, I converted the RC2014 logo to 256 pixels wide. This gave me a height of 42 lines for the image. The image I converted from didn’t scale very well, so it’s a bit of messy print.

I based my code on the printer routines in the ZX Spectrum ROM. The COPY command on a Spectrum prints out the current contents of the screen. I used this as my starting point, and modified the code to see a bitmap instead.

As timing is important, I have disabled interrupts while the code runs.

	OUTPUT zxprinter.z80

; Assemble using SjASMPlus.
;
; This program will print a bitmap image to a ZX printer.
; This code is based off code in the ZX Spectrum ROM.
; https://skoolkid.github.io/rom/asm/0ECD.html
;
; Robert Price - www.robertprice.co.uk

;        DEFINE+ UseDIO         ; Uncomment this line to use the Digital            
                                ; IO board to show the current line being        
                                ; printed.

PORT    EQU $1                  ; The output port to use for the printer.

        IFDEF UseDIO
DIOPORT EQU $0                  ; the output port for a RC2014 digial IO 
                                ; board. This is used to show the current
                                ; line being printed.
        ENDIF

WIDTH   EQU 256                 ; the width of the image in pixels.         
HEIGHT  EQU 42                  ; how many lines in the image to print. 

		
        ORG $9000               ; The start of the program. This is where
                                ; the program will be loaded into memory.
        
        di                      ; disable interrupts

        ld b, HEIGHT            ; the number of lines to print is in B.
        ld hl, Buffer           ; The address of the bitmap stored in HL

Copy_Buffer:
        push bc
        call Copy_Line          ; print the current line
        pop bc

        IFDEF UseDIO
        ld a, b                 ; show the current line being printed on
                                ; the Digital IO LEDs.
        out (DIOPORT), a
        ENDIF

        djnz Copy_Buffer        ; loop back to print the next line.

        IFDEF UseDIO
        ld a, 0                 ; turn off the Digital IO LEDs    
        out (DIOPORT), a
        ENDIF

Copy_End:
        ld a, $04               ; Bit 2 high stops the printer
        out (PORT), a           ; stop the printer

        ei                      ; enable interrupts

        ret                     ; end the program


Copy_Line:
        ld a, b                 ; Copy the pixel-line number.
        cp $03                  ; The A register will hold 0 until the 
                                ; last two lines are being handled.
        sbc a, a                 
        and $02              
        out (PORT), a           ; slow the motor for the last two lines.
        ld d, a                 ; the D register will hold either 0 or 2.

Copy_L_1;
                                ; on a ZX Spectrum this would test for 
                                ; breaks and stop the printer.
                                ; let's add our own delay here to allow
                                ; the printer to catch up.
        push bc
        ld b, $ff
.delay:
        nop
        djnz .delay
        pop bc

Copy_L_2:
        in a, (PORT)            ; fetch the status of the printer.
        add a, a                ; double the value of A. This moves bit 6
                                ; which is the printer present flag to bit
                                ; 7 and the sign flag.    
        ret m                   ; make an immediate return if the printer
                                ; is not present. (sign negative flag)

        jr nc, Copy_L_1         ; wait for the stylus to be ready.
        ld c, $20               ; there are 32 bytes.
    
Copy_L_3:
        ld e, (hl)              ; fetch a byte from the buffer.
        inc hl                  ; update the pointer
        ld b, $08               ; eight bits per byte

Copy_L_4:
        rl d                    ; move D left
        rl e                    ; move each bit into the carry
        rr d                    ; move D back again, picking up the carry
                                ; from E. The carry bit was the bit to print.

Copy_L_5:
        in a, (PORT)            ; fetch the status of the printer
        rra                     ; move bit 0 into the carry flag. 
                                ; If bit 0 is high, 
                                ; the printer is ready to receive data.
        jr nc, Copy_L_5         ; loop until the printer is ready

        ld a, d                 ; load the byte to send to the printer. 
                                ; Bit 2 low starts the motor, 
                                ; bit 1 high slows the motor, 
                                ; bit 7 high prints
        out (PORT), a           ; send to the printer
        djnz Copy_L_4           ; print each bit.
        dec c                   ; decreate the line byte counter
        jr nz, Copy_L_3         ; loop until all 32 bytes are printed.
        ret

; The image we want to print
; In this case, it's the RC2014 logo at 256 pixels wide and 42 pixles high.
Buffer:
        dc $00, $00, $00, $00, $82, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 
        dc $00, $00, $00, $00, $82, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 
        dc $00, $00, $00, $00, $82, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 
        dc $00, $04, $10, $43, $08, $20, $00, $02, $08, $20, $82, $00, $00, $01, $04, $10 
        dc $40, $00, $04, $10, $c2, $08, $00, $00, $02, $08, $00, $00, $06, $18, $01, $04 
        dc $00, $04, $10, $c2, $08, $30, $00, $02, $08, $20, $06, $00, $00, $41, $04, $10 
        dc $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $10 
        dc $60, $00, $04, $30, $82, $0c, $00, $00, $02, $0c, $00, $00, $04, $18, $01, $06 
        dc $00, $04, $30, $82, $08, $30, $00, $02, $08, $01, $06, $00, $00, $41, $04, $10 
        dc $c0, $00, $08, $20, $82, $0c, $00, $00, $06, $1c, $00, $00, $04, $18, $00, $06 
        dc $00, $0c, $30, $c6, $18, $70, $00, $06, $18, $61, $86, $00, $00, $c3, $0c, $30 
        dc $c0, $00, $0c, $31, $86, $1c, $00, $00, $06, $18, $00, $00, $0c, $38, $03, $0c 
        dc $00, $ff, $ff, $ff, $ff, $f0, $00, $7f, $ff, $ff, $fe, $00, $0f, $ff, $ff, $ff 
        dc $c0, $00, $ff, $ff, $ff, $fc, $00, $00, $7f, $f8, $00, $00, $7f, $f8, $3f, $fc 
        dc $00, $08, $37, $df, $70, $40, $00, $06, $fb, $ef, $0c, $20, $00, $df, $7d, $e0 
        dc $84, $00, $0d, $f7, $84, $10, $00, $00, $04, $18, $00, $00, $08, $30, $02, $0c 
        dc $00, $08, $20, $00, $00, $41, $01, $04, $00, $00, $08, $30, $20, $c0, $00, $01 
        dc $86, $02, $18, $00, $04, $10, $40, $01, $04, $10, $00, $00, $08, $30, $02, $08 
        dc $00, $18, $70, $00, $00, $41, $81, $06, $00, $00, $08, $30, $20, $c0, $00, $01 
        dc $07, $06, $18, $00, $04, $10, $60, $01, $08, $30, $00, $00, $08, $30, $00, $18 
        dc $00, $10, $60, $00, $00, $43, $01, $06, $00, $00, $08, $30, $20, $c0, $00, $01 
        dc $06, $04, $18, $00, $04, $00, $c0, $02, $08, $30, $00, $00, $08, $30, $04, $18 
        dc $00, $38, $e0, $00, $00, $c3, $03, $1c, $00, $00, $1c, $70, $71, $c0, $00, $23 
        dc $8e, $06, $38, $00, $0c, $71, $c0, $07, $1c, $70, $00, $00, $1c, $70, $0e, $38 
        dc $01, $ff, $e0, $00, $0f, $ff, $3f, $fc, $00, $00, $ff, $e3, $ff, $80, $00, $1f 
        dc $fe, $7f, $f8, $00, $7f, $ff, $c0, $3f, $ff, $f0, $00, $01, $ff, $e0, $7f, $f8 
        dc $00, $10, $40, $00, $00, $82, $02, $0c, $00, $00, $00, $00, $00, $00, $00, $c3 
        dc $00, $0c, $30, $02, $08, $20, $80, $02, $00, $60, $00, $00, $18, $00, $04, $10 
        dc $00, $00, $c0, $00, $00, $82, $02, $08, $00, $00, $00, $00, $00, $00, $00, $c3 
        dc $00, $0c, $30, $03, $00, $00, $80, $00, $10, $60, $00, $04, $18, $00, $08, $30 
        dc $00, $20, $c0, $00, $00, $06, $00, $18, $00, $00, $00, $00, $00, $00, $00, $83 
        dc $00, $08, $30, $03, $00, $41, $80, $00, $10, $60, $00, $04, $18, $00, $08, $30 
        dc $00, $20, $c0, $00, $01, $06, $04, $18, $00, $00, $00, $00, $00, $00, $00, $83 
        dc $00, $08, $30, $03, $00, $41, $80, $00, $10, $60, $00, $04, $18, $00, $08, $30 
        dc $03, $ff, $c0, $00, $0f, $fe, $3f, $f8, $00, $00, $00, $00, $00, $00, $0f, $ff 
        dc $00, $ff, $f0, $7e, $07, $ff, $80, $01, $ff, $e0, $00, $7f, $f0, $00, $ff, $f0 
        dc $00, $61, $80, $00, $03, $fe, $0e, $38, $00, $00, $00, $00, $00, $00, $01, $ff 
        dc $00, $18, $70, $0e, $00, $c3, $80, $00, $30, $c0, $00, $1c, $70, $00, $18, $70 
        dc $00, $41, $04, $10, $41, $00, $04, $30, $00, $00, $00, $00, $02, $08, $21, $80 
        dc $00, $10, $60, $04, $00, $03, $00, $00, $20, $c0, $00, $08, $20, $00, $10, $60 
        dc $00, $41, $04, $10, $43, $00, $00, $30, $00, $00, $00, $00, $06, $18, $41, $80 
        dc $00, $10, $60, $06, $00, $83, $00, $00, $20, $c0, $00, $08, $30, $00, $10, $60 
        dc $00, $41, $04, $10, $83, $00, $08, $30, $00, $00, $00, $00, $06, $10, $41, $80 
        dc $00, $10, $60, $0e, $00, $83, $00, $00, $20, $c0, $00, $00, $60, $00, $10, $60 
        dc $00, $41, $0c, $30, $83, $00, $08, $30, $00, $00, $00, $00, $04, $10, $41, $80 
        dc $00, $10, $60, $0c, $00, $83, $00, $00, $21, $c0, $00, $18, $60, $00, $10, $e0 
        dc $0f, $ff, $ff, $ff, $ff, $00, $ff, $f0, $00, $00, $00, $00, $7f, $ff, $ff, $80 
        dc $03, $ff, $c0, $fc, $1f, $ff, $00, $07, $ff, $80, $01, $ff, $e0, $03, $ff, $e0 
        dc $00, $c3, $ff, $ff, $86, $00, $18, $70, $00, $00, $00, $00, $0e, $fb, $ff, $00 
        dc $00, $20, $c0, $0c, $01, $86, $00, $00, $61, $80, $00, $10, $c0, $00, $30, $80 
        dc $00, $83, $00, $00, $04, $10, $10, $60, $00, $00, $00, $01, $04, $00, $00, $00 
        dc $00, $20, $80, $08, $01, $06, $00, $00, $41, $00, $00, $10, $42, $08, $20, $82 
        dc $00, $83, $00, $00, $04, $18, $10, $60, $00, $00, $00, $03, $0e, $00, $00, $00 
        dc $00, $20, $c0, $18, $01, $06, $00, $00, $41, $80, $00, $30, $82, $08, $20, $83 
        dc $00, $83, $00, $00, $04, $18, $10, $60, $00, $00, $00, $02, $0c, $00, $00, $00 
        dc $00, $00, $c0, $18, $01, $06, $00, $00, $c3, $80, $00, $20, $82, $08, $20, $03 
        dc $01, $86, $00, $00, $0c, $38, $10, $e0, $00, $00, $00, $02, $0c, $00, $00, $00 
        dc $00, $41, $80, $18, $03, $0e, $00, $00, $c3, $00, $00, $20, $82, $18, $61, $86 
        dc $1f, $fe, $00, $00, $ff, $fb, $ff, $e0, $00, $00, $00, $3f, $fc, $00, $00, $00 
        dc $07, $ff, $81, $f8, $3f, $fc, $00, $0f, $ff, $00, $03, $ff, $ff, $ff, $ff, $fe 
        dc $01, $06, $00, $00, $08, $30, $30, $c0, $00, $00, $00, $06, $1c, $00, $00, $00 
        dc $00, $41, $00, $f0, $02, $0c, $00, $00, $87, $00, $01, $f7, $df, $70, $41, $be 
        dc $01, $04, $00, $00, $08, $30, $20, $c0, $00, $00, $06, $00, $18, $00, $00, $10 
        dc $c0, $40, $0c, $00, $02, $0c, $00, $00, $82, $00, $00, $00, $00, $00, $41, $00 
        dc $01, $0e, $00, $00, $08, $30, $20, $c0, $00, $01, $06, $04, $18, $00, $00, $30 
        dc $c0, $02, $0c, $00, $02, $0c, $00, $01, $87, $00, $00, $00, $00, $00, $41, $80 
        dc $03, $0c, $00, $00, $08, $30, $20, $c0, $00, $01, $06, $04, $18, $00, $00, $20 
        dc $c0, $82, $0c, $00, $06, $1c, $00, $01, $06, $00, $00, $00, $00, $00, $03, $00 
        dc $03, $0c, $00, $00, $18, $60, $61, $c0, $00, $03, $0e, $0c, $38, $00, $00, $30 
        dc $c0, $c3, $0c, $00, $06, $18, $00, $01, $8e, $00, $00, $00, $00, $00, $c3, $00 
        dc $3f, $fc, $00, $01, $ff, $e3, $ff, $80, $00, $1f, $fc, $7f, $f8, $00, $03, $ff 
        dc $cf, $ff, $f8, $00, $7f, $f8, $00, $1f, $fe, $00, $00, $00, $00, $0f, $ff, $00 
        dc $02, $18, $00, $00, $10, $60, $41, $00, $00, $03, $00, $08, $20, $80, $00, $61 
        dc $80, $06, $18, $40, $06, $00, $00, $03, $08, $00, $00, $00, $00, $00, $82, $00 
        dc $02, $18, $00, $00, $10, $40, $01, $00, $00, $83, $00, $08, $20, $82, $18, $61 
        dc $80, $04, $10, $41, $06, $00, $10, $c2, $08, $20, $c0, $00, $00, $00, $02, $00 
        dc $04, $18, $00, $00, $00, $60, $00, $08, $20, $83, $00, $08, $20, $86, $10, $41 
        dc $80, $04, $10, $41, $06, $00, $30, $82, $08, $20, $c0, $00, $00, $01, $06, $00 
        dc $04, $18, $00, $00, $00, $c0, $02, $08, $20, $83, $00, $08, $01, $04, $10, $41 
        dc $80, $04, $10, $c2, $0e, $00, $20, $82, $08, $20, $c0, $00, $00, $01, $06, $00 
        dc $ff, $f8, $00, $03, $ff, $c0, $3f, $ff, $ff, $ff, $01, $ff, $ff, $ff, $ff, $ff 
        dc $80, $ff, $ff, $ff, $fc, $03, $ff, $ff, $ff, $ff, $80, $00, $00, $1f, $fe, $00 
        dc $7f, $f8, $00, $01, $ff, $c0, $3f, $ff, $ff, $fe, $00, $ff, $ff, $ff, $ff, $ff 
        dc $80, $7f, $ff, $ff, $fc, $01, $ff, $ff, $ff, $ff, $80, $00, $00, $1f, $fe, $00 
        dc $3e, $f8, $00, $01, $f7, $c0, $1f, $7d, $ff, $be, $00, $7b, $ef, $fe, $fb, $ef 
        dc $80, $3e, $fb, $df, $7c, $01, $f7, $df, $79, $ef, $80, $00, $00, $0f, $bc, $00        
        

Trying to generate DTMF tones using the SID-Ulator on the RC2014

I recently purchased a SID-Ulator module for my RC2014 computer.

The SID-Ulator emulates the famous SID sound chip found on the Commodore 64 computer and provides a way to access it from the RC2014.

I wondered if I could use the SID-Ulator to generate DTMF tones. These are the tones you are likely to hear when pressing the buttons on your phone.

I have written about using the AY-3-8910 chip to generate DTMF tones on the RC2014, so you can find more details on the tones there.

Essentially, DTMF uses two tones played at the same time to encode a digit.

To send the digit ‘1’, I need to play a 697Hz tone and 1209Hz.

The SID chip can play multiple tones at the same time, so this should be possible. However, the DTMF tones use sine waves, which the SID chip can’t generate. It can generate triangle waves which I think would be the closest equivalent.

The timing on the original SID was dependent on the frequency of the TV signal. In PAL regions this is 50Hz, and in NTSC regions this is 60Hz. I’m not sure which the SID-Ulator is using, so I had to generate values for the frequencies for both.

For PAL regions this is

f * 17.0284087

For NTSC regions this is

f * 16.4043888

So in the PAL region, the integer value of ‘f’ for 697 would be

697 * 17.0284087 = 11869

I calculated these values for both PAL and NTSC in Excel, along with the hex values. The hex values are easier to split into high and low bytes that are needed by the SID chip.

PALPAL (Hex)NTSCNTSC (Hex)
69711868.800852E5C11433.858972CA9
120920587.34609506B19832.906024D78

I then needed to actually play these notes, so I wrote a BASIC program to do this. I went with the NTSC values initially for the high and low bytes of the frequency.

Instead of using memory mapping like the Commodore 64, the SID-Ulator module uses Z80 IO ports. So rather than POKEing an address to configure the SID, we use the Z80 OUT command. The SID-Ulator is configured to use IO ports 212 for the register, and 213 for the data.

10 S=0 :REG=212 :DAT=213
20 REM CONFIGURE CHANNEL 1 (697 Hz)
30 OUT REG,S+1: OUT DAT, 0    : REM CLEAR FREQUENCY LOW BYTE
40 OUT REG,S: OUT DAT, 0      : REM CLEAR FREQUENCY HIGH BYTE
50 OUT REG,S+5: OUT DAT, 9    : REM SET ATTACK=0.6ms, DECAY=0.6s (9 = 00001001)
60 OUT REG,S+6: OUT DAT, 240  : REM SET SUSTAIN=MAX, RELEASE=6s (240 = 11110000)
70 OUT REG,S+4: OUT DAT, 17   : REM SET TRIANGLE WAVE (BIT 4) AND GATE (BIT 0)
80 OUT REG,S: OUT DAT, &ha9     : REM SET FREQUENCY LOW BYTE FOR 697
90 OUT REG,S+1: OUT DAT, &h2c   : REM SET FREQUENCY HIGH BYTE FOR 697
100 REM CONFIGURE CHANNEL 2 (1209 Hz)
110 OUT REG,S+8: OUT DAT, 0   : REM CLEAR FREQUENCY LOW BYTE
120 OUT REG,S+7: OUT DAT, 0   : REM CLEAR FREQUENCY HIGH BYTE
130 OUT REG,S+12: OUT DAT, 9  : REM SET ATTACK=0.6ms, DECAY=0.6s (9 = 00001001)
140 OUT REG,S+13: OUT DAT, 240: REM SET SUSTAIN=MAX, RELEASE=6s (240 = 11110000)
150 OUT REG,S+11: OUT DAT, 17 : REM SET TRIANGLE WAVE (BIT 4) AND GATE (BIT 0)
160 OUT REG,S+7: OUT DAT, &h78 : REM SET FREQUENCY LOW BYTE FOR 1209 Hz
170 OUT REG,S+8: OUT DAT, &h4d  : REM SET FREQUENCY HIGH BYTE FOR 1209 Hz
180 REM PLAY BOTH TONES FOR 1 SECOND
190 FOR T = 1 TO 1000 : REM LOOP FOR 1 SECOND (APPROXIMATELY)
200 NEXT T
210 REM RELEASE BOTH NOTES
220 OUT REG,S+4: OUT DAT, 16  : REM RELEASE CHANNEL 1 NOTE (CLEAR GATE BIT)
230 OUT REG,S+11: OUT DAT, 16 : REM RELEASE CHANNEL 2 NOTE (CLEAR GATE BIT)
240 END

Running this, I got something that certainly sounded like DTMF tones.

I tried plugging the output of the SID-Ulator into my DTMF decoder to confirm this was correct. Unfortunately, the DTMF decoder could not detect a valid tone.

I thought this must be because I needed to use the PAL values. Again, it certainly sounded like DTMF tones when run, but the DTMF decoder could not detect a valid tone.

Unfortunately, I’ve not been able to generate DTMF tones using the SID-Ulator.

What went wrong

I’m not sure what has gone wrong exactly, but I have a few ideas.

The calculations for the frequencies could be incorrect. I used the formula I found on the SID reference website. This may not be quite right. I don’t have a scope to check the frequencies that are being output.

The triangle wave isn’t a suitable replacement for the sine wave. There isn’t much I can do about this as the SID doesn’t support sine waves.

The output from the SID-Ulator isn’t suitable for my DTMF decoder. I don’t think this is the case as I can hear the tones when connected to my speaker.

I may also have just used incorrect values for the settings causing the output to distort.

If anyone has any suggestions, I’d love to hear them.

Generating DTMF Tones Using An AY-3-8910 On A RC2014

I have recently built a DTMF decoder module for the RC2014 computer.

Now I want to go the opposite way, and have my RC2014 generate DTMF tones.

I own one of Ed Brindley’s excellent YM2149 Sound Cards for the RC2014, so I want to use this to generate the DTMF tones. Mine has Colin Piggot’s Quazar ZXTT card attached to match the clock speeds used in the ZX Spectrum. This gives a clock speed of 1.7734MHz, which will be important when generating the DTMF tones.

DTMF works by sending two tones for each key. One high frequency, one low frequency. These are mapped to the numbers 0 to 9, characters * and #, and the letters A to D.

1209 Hz1336 Hz1477 Hz1633 Hz
697 Hz113A
770 Hz456B
852 Hz789C
941 Hz*0#D

The AY-3-8910 has 3 channels of sound available, so we only need two of these to generate a DTMF tone.

We need to calculate a divider for the based on the clock speed for each tone. This is the clockspeed divided by (16 x the frequency).

So for a 697Hz tone being generated with a clock speed of 1.7734Mhz, the calculation would be 1.7734 / ( 16 * 697) = 159.

We can do the same for the other tones, which gives us the following values.

697 Hz159
770 Hz144
852 Hz130
941 Hz118
1209 Hz92
1336 Hz83
1477 Hz75
1633 Hz68

We are going to use channels A and B on the AY-3-8910 to generate our DTMF tone. So to play the DTMF tone for “1”, we need to play a 697 Hz tone on channel A, and a 1209H Hz tone on channel B. Looking at the table above, this means channel A needs the value 159, and channel B the value 92.

A simple Z80 assembly language program to play these tones would look something like this.

AY_REGISTER_PORT EQU 0xD8
AY_DATA_PORT    EQU 0xD0

CHANNEL_A       EQU 0x00
CHANNEL_B       EQU 0x02

		ld a, CHANNEL_A
		out (AY_REGISTER_PORT), a
		ld a, 159
		out (AY_DATA_PORT), a
		
		ld a, CHANNEL_B
		out (AY_REGISTER_PORT), a
		ld a, 92
		out (AY_DATA_PORT), a

We can expand this and write a Z80 assembly language program to play the tones for each key once using the following code. In this example I keep the tone pairs in memory and then look up the values before playing them for a short duration then stopping the sound output before moving to the next pair.

		OUTPUT DTMFAYEncoder.z80
		
		ORG $9000
		
AY_REGISTER_PORT EQU 0xD8
AY_DATA_PORT    EQU 0xD0

CHANNEL_A       EQU 0x00
CHANNEL_B       EQU 0x02
ENABLE          EQU 0x07
AMPLITUDE_A     EQU 0x08
AMPLITUDE_B     EQU 0x09

VOLUME          EQU 0x0F

; the tones we need for the AY chip
DIVIDER_697     EQU 159
DIVIDER_770     EQU 144
DIVIDER_852     EQU 130
DIVIDER_941     EQU 118
DIVIDER_1209    EQU 92
DIVIDER_1336    EQU 83
DIVIDER_1477    EQU 75
DIVIDER_1633    EQU 68
		
init:		
; set volume on channel A
		ld a, AMPLITUDE_A
		out (AY_REGISTER_PORT), a
		ld a, VOLUME
		out (AY_DATA_PORT), a
		
; set volume on channel B
		ld a, AMPLITUDE_B
		out (AY_REGISTER_PORT), a
		ld a, VOLUME
		out (AY_DATA_PORT), a
		

		
; iterate over all the codes we have and play them out
		ld b, tonecodelen
		ld hl, tonecodes
.loop:
		ld d, (hl)
		call playTone
		call enableAB
		call delay
		call stopTone
		call shortdelay
		
		inc hl
		djnz .loop
		
; stop the tones
		call stopTone
		
		ret
		
		
		
; -----------------------------
; SUBROUTINES
; -----------------------------
		
; a short delay
delay:		
		push bc
		ld bc, $8888
		call dodelay
		pop bc
		ret
		
; an even shorter delay
shortdelay:	
		push bc
		ld bc, $1500
		call dodelay
		pop bc
		ret
		
; dodelay does the actual delaying
; pass the delay length in BC
dodelay:	
		push de
		push af
.loop:		
		dec bc
		ld a, b
		or c
		jr nz, .loop
		pop af
		pop de
		ret
		
; enable channels A and B on the AY chip
enableAB:	
		push af
		ld a, ENABLE
		out (AY_REGISTER_PORT), a
		ld a, 0xFC
		out (AY_DATA_PORT), a
		pop af
		ret
		
; stop tones playing on the AY chip
stopTone:	
		push af
		ld a, ENABLE
		out (AY_REGISTER_PORT), a
		ld a, 0x3F 		        ; disable all channels
		out (AY_DATA_PORT), a
		pop af
		ret
		
; play a tone
; pass the ASCII character for the tone in D
playTone:	
		push af
		push bc
		push de
		
		call getTone
		
		ld a, CHANNEL_A
		out (AY_REGISTER_PORT), a
		ld a, b
		out (AY_DATA_PORT), a
		
		ld a, CHANNEL_B
		out (AY_REGISTER_PORT), a
		ld a, c
		out (AY_DATA_PORT), a
		
		pop de
		pop bc
		pop af
		ret
		
; get the tones two tones for character in D
; return the two tones in registers BC
getTone:	
		push af
		push hl
		
		ld e, 0
		
		ld hl, tonecodes
.loop:		
		ld a, (hl)
		cp d
		jr z, .gottone
		inc hl
		inc e
		inc e
		jr .loop
.gottone:	
		ld a, e
		ld hl, tones
		
; we now need to add A to HL
		add a, l
		ld l, a
		adc a, h
		sub l
		ld h, a
; get the first tone in B
		ld b, (hl)
		inc hl
; get the second tone in C
		ld c, (hl)
		
		pop hl
		pop af
		ret
		
; the tone codes in order. We use this to get
; the both tone codes from tones
tonecodes:	dc '1234567890*#ABCD'
tonecodelen     EQU $ - tonecodes
		
tones:		
tone1:		dc DIVIDER_697, DIVIDER_1209            ; 1
tone2:		dc DIVIDER_697, DIVIDER_1336            ; 2
tone3:		dc DIVIDER_697, DIVIDER_1477            ; 3
tone4:		dc DIVIDER_770, DIVIDER_1209            ; 4
tone5:		dc DIVIDER_770, DIVIDER_1336            ; 5
tone6:		dc DIVIDER_770, DIVIDER_1477            ; 6
tone7:		dc DIVIDER_852, DIVIDER_1209            ; 7
tone8:		dc DIVIDER_852, DIVIDER_1336            ; 8
tone9:		dc DIVIDER_852, DIVIDER_1477            ; 9
tone0:		dc DIVIDER_941, DIVIDER_1336            ; 0
tonestar:	dc DIVIDER_941, DIVIDER_1209            ; *
tonehash:	dc DIVIDER_941, DIVIDER_1477            ; #
toneA:		dc DIVIDER_697, DIVIDER_1633            ; A
toneB:		dc DIVIDER_770, DIVIDER_1633            ; B
toneC:		dc DIVIDER_852, DIVIDER_1633            ; C
toneD:		dc DIVIDER_941, DIVIDER_1633            ; D

It’s easy to test our code as we build the DTMF decoder module. We can simply plug the output from the sound card into the DTMF decoder. We can see the decoded tones showing on the debugging LEDs.

Using the DTMF Decoder Module for the RC2014 from C

I wanted to see how easy it would be to use my new DTMF Decoder module for the RC2014 from C.

I have previously written about using the z88dk C compiler to target the RC2014. However, I’ve not tried to use Z80 IO using the z88dk before.

To perform IO on the Z80 I needed to use a SFR (Special Function Register). When a variable created using an SFR is read or written to, this is converted directly into Z80 in and out instructions.

My DTMF Decoder module is running on an RC2014 Classic 2 on IO port 1. To declare an SFR in my C code to access this port, I can use the following…

#define __IO_DTMF_PORT  0x01
__sfr __at __IO_DTMF_PORT io_dtmf;

Now when I read from variable io_dtmf, this is mapped to an in instruction on the Z80. So to read this port, I can do the following…

uint8_t in = io_dtmf;

Now the unsigned 8 bit integer variable “in” holds the value of whatever was on IO port 1 when the variable was assigned.

To write the decoder program, I can use the same approach as my earlier Z80 assembly language program. This reading the IO port, and seeing if the STQ bit is set. If it is, we can then read the input and map this to the known DTMF codes. This is then printed to the console. We also need to make sure we’ve not already handled the current tone, so we need to store and check against this.

This is my working C code to decode incoming DTMF tones using my DTMF Decoder module for the RC2014.

// zcc +rc2014 -subtype=basic -clib=sdcc_iy -v -m -SO3 --max-allocs-per-node200000 dtmf.c -o dtmf -create-app
// this will compile to $8000, so can be run in SCM using "g 8000"

#pragma output CRT_ORG_CODE = 0x8000
#pragma output REGISTER_SP = 0xFC00

#define __IO_DTMF_PORT  0x01

__sfr __at __IO_DTMF_PORT io_dtmf;

#include <stdio.h>

char *codes = "D1234567890*#ABC";
uint8_t previous = 0xff;

void main(void) {
    printf("DTMF Decoder\n");
    while (1) {
        uint8_t in = io_dtmf;
        // is the STQ bit set?
        if (in & 0b00010000) {
            // we only want the lower 4 bits so mask out any others.
            uint8_t current = in & 0b00001111;
            if (current != previous) {
                printf("%c", codes[current]);
                previous = current;
            }
        } else {
            previous = 0xff;
        }
    }
}