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;
        }
    }
}

Decoding DTMF tones on an RC2014

Dual-tone multi-frequency, commonly known as DTMF, is a technology used in telecoms systems that uses audible tones to represent keys on a touch-tone phone.

You have probably heard this will navigating a menu on your phone, such as “Press 1 for …, Press 2 for….”, etc.

While browsing on AliExpressing for some parts for another project, it suggested I may like the MT8870 module (affiliate link). This is a small 5V DTMF decoder module with digital outputs. I thought this would be ideal for an RC2014 project.

We’re interested in 7 pins on this module. +5v and GND are for powering the module. STQ, D0, D1, D2, and D3 are for the decoded tones. The STQ line goes high when a decoded tone is present. The actual code is a 4 bit value stored on D0, D1, D2, and D3.

We can pass this output to a 74HCT245 transceiver which we can enable using a 74HCT688 to decode an input address on the RC2014, along with a 74HCT32 to check the RD and IORQ lines.

I’m using an RC2014 Classic 2 for this project, so inout port 1 is free, but I know this isn’t necessarily the case on more advanced RC2014s. Because of this I’ve put an 8 inline switch module on the 74HCT688 so I change the address if necessary.

I am also adding some LEDs on the input lines so I can see the incoming data visually. The MT8870 has some SMD LEDs on board to do this, but the built-in pin headers I want to use will mean these face the RC2014 module PCB and so won’t be visible. I’ve gone for green for the STQ line, blue for the data lines.

Circuit diagrams for the DTMF Decoder Module designed for the RC2014

The PCB looks like this.

DTMF Decoder Module PCB designed for the RC2014.

Decoding the input using Z80 assembly language

I want to write a small Z80 assembly language program to print out any incoming DTMF tones to the terminal.

I’m using the SCM monitor program on my RC2014 Classic 2 computer to load and execute my assembly language program. This gives a handy print character to the terminal routine I can use. I pass the ASCII character I want to print in register A, the value $02 in register C, and execute RST $30. I can wrap this into my own subroutine called printchar. This gives me the flexibility to swap to another output method such as an LCD screen in future.

; pass the character to print in register A
; This uses SCM to print back to the terminal.
printchar:	
		push bc
		ld c, $02 	; SCM API function $02 - print character
		rst $30
		pop bc
		ret

I will need to debounce any input I read from the decoder module. This is because the value will be present for longer than the first read I make. To do this I will need to check if STQ is high and compare this to the last value read. If it’s the same, I know I have already handled this value. However, when STQ goes low, I need to reset the last value read to a dummy value else it won’t detect multiple presses of the same button.

Once I have the data, I can map it to the correct character. I do this by having the valid decoded characters in order, and then simply the incoming character value as an offset to this list.

My final code looks like this.

		OUTPUT DTMFDecoder.z80
		
		ORG $9000
		
; Input data from the DTMF encoder is
; Bits 0-3 = number data (D0, D1, D2, D3)
; Bit 4    = new data flag (STQ)
; Bits 5-7 = unused.
		
; The input port where we can find the DTMF decoder
DTMFDecoder     EQU 1
		
; masks for handling the input data.
NEW_INPUT_MASK  EQU %00010000
DATA_INPUT_MASK EQU %00001111
		
; Flags to show if the data is new or not.
DataRead        EQU $FF
DataClear       EQU $00
		
; Register C holds the data read flag. This is set to DataRead when we've
; read a byte, and set to DataClear when we are safe to read a byte.
		ld c, DataClear
		
mainloop:	
; read the data from the input port DTMFDecoder and store this in register B.
		in a, (DTMFDecoder)
		ld b, a
; see if the new data flag bit has been set. If it has go to datapresent.
		and NEW_INPUT_MASK
		jr nz, datapresent
		
; clear Register C as we have no data to read, and loop again.
		ld c, DataClear
		jr mainloop
		
datapresent:	
; we have data present so check if register C is set to DataRead. If it
; is we know we have already handled that input so can loop again.
		ld a, DataRead
		cp c
		jr z, mainloop
		
; set register C to DataRead to flag we have handled this input.
		ld c, DataRead
		
; get the input into register A and mask out the button press bit.
		ld a, b
		and DATA_INPUT_MASK
		
; decode the character from the input and find it in the charmap.
		ld hl, charmap
		
; add the value of a to hl
		add a, l
		ld l, a
		adc a, h
		sub l
		ld h, a
		
; get the character from the charmap and print it.
		ld a, (hl)
		call printchar
		
; now loop again.
		jr mainloop
		
		
; pass the character to print in register A
; This uses SCM to print back to the terminal.
printchar:	
		push bc
		ld c, $02 	; SCM API function $02 - print character
		rst $30
		pop bc
		ret
		
; The DTMF characters in order.
charmap:	
		db 'D1234567890*#ABC'

Writing C Code For The RC2014 Classic II Running SCM

I wanted to try writing some C code for my RC2014 Classic II computer, so I thought I’d document how I did this.

I’ve not written C for a few years, so I’m quite rusty. My example code may not be the most efficient, but I have compiled it, and then run it on my RC2014.

I use a Mac, so I installed z88dk. This is a C and Assembly language toolkit especially designed to target Z80 based computers like the RC2014.

One of the simplest programs is “Hello World”. This simply prints the string “Hello World” to the screen. In this case, the screen on the RC2014 is actually a serial terminal.

I’m planning on running this using SCM. This is a simple monitor program that the RC2014 Classic II ships with in ROM. I can use this to load in hex code and execute it from a serial terminal.

This is my example Hello World C program. I saved it as “hello.c”.

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

#include <stdio.h>

int main(void) {
    printf("Hello World\n");
    return 0;
}

This looks pretty much standard C, but you may have noticed the two #pragma’s at the start.

CRT_ORG_CODE tells the compiler to assemble this code to run at address 0x8000. This is the default for SCM.

REGISTER_SP tells the compiler to set the stack pointer to 0xFC00.

To compile the C code, we have to run the following on the command line.

zcc +rc2014 -subtype=basic -clib=sdcc_iy -v -m -SO3 --max-allocs-per-node200000 hello.c -o hello -create-app

+rc2014 tells the compiler we are targeting the RC2014 when generating code.

-subtype=basic tells the compiler we are building code to run under BASIC or SCM. More advanced RC2014s can run CPM, and we could change the subtype if we wanted to target that.

-clib=sdcc_iy tells the compiler which C library we are using.

-v gives us a verbose output.

-m generates map files when compiling.

-SO3 tells the compiler to use level 3 optimisation. This should be the most efficient.

–max-allocs-per-node 200000 is another optimisation that is recommended in the documentation.

-create-app will create .ihx hex files that can be sent to SCM to load the program.

Once compiled, I use a cat command to send the generated hello.ihx file to SCM over an active serial connection.

cat hello.ihx > /dev/tty.usbXXXXX

Finally, in my terminal (I use minicom) I type the following into SCM to execute my program from address 0x8000.

g 8000

And I magically see the words “Hello World” in my terminal.

RC2024 – Part 16 – Retro Challenge Completed

It’s the 31st of October 2024, so this is the end of the Retro Challenge 2024.

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.

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 14 – New Rotary Encoder PCBs

Last week I wrote about refining my Rotary Encoder Module for the RC2014. The updated PCBs have arrived so I have soldered them up.

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.

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 12 – Building And Testing The Updated ROM Board

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

brew install minipro

Spencer has the RC2014 ROMs available as binary downloads on Github, so I found the one compatible with the RC2014 Classic 2 and downloaded it. The image I used was R000009.BIN.

To burn it to the EEPROM, I had to use the following command.

minipro -p "w27c512@dip28" -w ~/Downloads/R0000009.BIN

This tells minipro I’m using a Winbond 27C512 EEPROM, and I want to write the file R000009.BIN to it.

Once programmed, I could verify the contents using the following command.

minipro -p "w27c512@dip28" -m ~/Downloads/R0000009.BIN

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.