Epson HX-20 Emulator Sound Support
I have added piezo speaker sound support to my hex20 Epson HX-20 emulator and increased the version number to "0.2". On the real hardware the speaker is controlled by an I/O pin from the slave MCU and timed to produce a square wave pulse with different frequencies. I tried some different approaches to emulation but ended up with simply sampling this I/O pin and writing the value into a FIFO at the MCU clock frequency of 612900 Hz. The FIFO is then read by the SDL audio callback 14 times and averaged for each sample to closely match the 44100 Hz audio sample rate. Not exactly Hi-Fi but it is simple and it works.
The main goal with this update was to the able to run the "Attack of the Sine Wave from Outer Space" game in the emulator. In addition to producing a lot of sound, this game also relies on the POINT() instruction in Epson BASIC which reads data from the LCD display. Reading the uPD7227 LCD display controllers happens through a clocked serial interface which has been partially implemented.
The game can be found in the pjspot collection. Here is a video from 10 years ago running it on real hardware, and here is a video running it on the the updated emulator.

Version 0.2 can be downloaded here but the GitHub repository has also been updated.
Kermit for CP/M-68K
I needed a way to transfer files to a CP/M-68K system with only a serial console and a RAM disk. For this I once again implemented the "Baby Kermit" reference program in C specifically to be compiled for CP/M.
Since CP/M-68K is from the early 80's there are some quirks regarding the C dialect that I discovered:
* K&R C syntax is of course expected, which affects function argument definitions.
* The "void" datatype must be declared using "VOID" in uppercase.
* NULL did not work as expected, so empty "" 0-length strings are used instead.
* To open a file in binary mode, the fopenb() call must be used since the regular fopen() will always open a file in ASCII mode.
The binary can be built using this emulator which happens to have the C toolchain available, using this command:
CC KERMIT.C -O KERMIT.REL
For convenience, here is a built KERMIT.REL ready for use.
I used the C-Kermit program on Linux to transfer files, which is very picky about having the correct settings. Here are the full instructions that has worked for me:
set modem type none set line <tty-device> set carrier-watch off set flow none set parity none set stop-bits 1 set prefixing all set streaming off send <file>
Here is the CP/M-68K compatible C source code, which can also be compiled and run on Linux by the way:
#include <stdio.h> #ifdef __unix__ #include <string.h> #ifndef VOID #define VOID void /* Non-CP/M compatibility */ #endif #endif #define SIZE 100 char send_buffer[SIZE]; char recv_buffer[SIZE]; char packet_data[SIZE]; int seq; int ctl; int eol; VOID send_packet(type, data) char type; char *data; { int i; int len; char cs_send; len = strlen(data); if (len + 6 > SIZE) { return; /* Overflow! */ } send_buffer[0] = 1; /* 0x01 */ send_buffer[1] = len + 35; send_buffer[2] = seq + 32; send_buffer[3] = type; for (i = 0; i < len; i++) { send_buffer[i + 4] = data[i]; } cs_send = 0; for (i = 1; i < len + 4; i++) { cs_send += send_buffer[i]; } cs_send = (cs_send + ((cs_send & 192) / 64)) & 63; send_buffer[len + 4] = cs_send + 32; send_buffer[len + 5] = eol; send_buffer[len + 6] = '\0'; i = 0; while (send_buffer[i] != '\0') { fputc(send_buffer[i], stdout); i++; } } VOID send_ack(data) char *data; { send_packet('Y', data); seq = (seq + 1) & 63; } VOID decode_packet(recv_seq, recv_type, dec_len) char *recv_seq; char *recv_type; char *dec_len; { int i; int t; int t7; int p; int flag; int c; int mark; char len; unsigned cs_calc; unsigned cs_recv; /* Receive until a CR/LF/EOF appears. */ i = 0; mark = -1; while (1) { c = fgetc(stdin); if (c == '\r' || c == '\n' || c == EOF) { break; } if (c == 1) { /* 0x01 */ mark = i; /* Store position of MARK in packet. */ } recv_buffer[i] = c; i++; if (i >= SIZE) { break; /* Overflow! */ } } if (mark == -1) { *recv_type = 'Q'; /* No MARK found! */ return; } len = recv_buffer[mark + 1] - 32; *recv_seq = recv_buffer[mark + 2] - 32; *recv_type = recv_buffer[mark + 3]; if ((mark + len + 1) >= SIZE) { *recv_type = 'Q'; /* Length exceeds buffer. */ return; } cs_recv = recv_buffer[mark + len + 1] - 32; cs_calc = 0; for (i = mark + 1; i < (mark + len + 1); i++) { cs_calc += recv_buffer[i]; } cs_calc = (cs_calc + ((cs_calc & 192) / 64)) & 63; for (i = 0; i < SIZE; i++) { packet_data[i] = '\0'; } p = 0; flag = 0; for (i = mark + 4; i < mark + len + 1; i++) { t = recv_buffer[i]; if (*recv_type != 'S') { if (flag == 0 && t == ctl) { flag = 1; continue; } t7 = t & 127; if (flag == 1) { flag = 0; if (t7 > 62 && t7 < 96) { t ^= 64; } } packet_data[p] = t; p++; } } *dec_len = p; if (cs_recv != cs_calc) { *recv_type = 'Q'; /* Checksum mismatch! */ } } char get_packet(len) char *len; { int try; char recv_seq; char recv_type; char recv_len; /* Try to get a valid packet with the desired sequence number. */ decode_packet(&recv_seq, &recv_type, &recv_len); for (try = 0; try < 5; try++) { if (recv_seq == seq && recv_type != 'Q') { *len = recv_len; return recv_type; } send_packet('N', ""); decode_packet(&recv_seq, &recv_type, &recv_len); } *len = 0; return 'T'; } int main(void) { FILE *fh; char type; char len; /* Default values */ ctl = '#'; eol = '\r'; seq = 0; fh = NULL; /* Get Send Initialization packet, exchange parameters. */ type = get_packet(&len); if (type != 'S') { send_packet('E', "Bad packet in S state"); return 1; } if (len > 4) { eol = packet_data[5]; } if (len > 5) { ctl = packet_data[6]; } send_ack("H* @-#N1"); /* Get a File Header packet. If a B packet comes, we're all done. */ while (1) { type = get_packet(&len); if (type == 'B') { send_ack(""); break; /* Outer loop */ } if (type != 'F') { send_packet('E', "Bad packet in F state"); return 1; } #ifdef __unix__ fh = fopen(packet_data, "wb"); #else /* CP/M */ fh = fopenb(packet_data, "w"); #endif if (fh == NULL) { send_packet('E', "Error opening file"); return 1; } send_ack(""); /* Get Data packets. If a Z packet comes, the file is complete. */ while (1) { type = get_packet(&len); if (type == 'Z') { fclose(fh); send_ack(""); break; /* Inner loop */ } if (type != 'D') { send_packet('E', "Bad packet in D state"); fclose(fh); return 1; } fwrite(packet_data, sizeof(char), len, fh); send_ack(""); } } return 0; }
Terminal Mode Commodore 64 Emulator SID Support
I have added SID sound support to my tmce64 emulator and bumped the version number to "0.4". The SID is quite complicated so instead of trying to emulate it myself I have added a wrapper around the excellent reSID implementation, which is also used by the VICE emulator I believe.
I have based my wrapper around reSID version 0.16 which can be found here. There seems to be a newer experimental version available also, but I have not tried it.
I did not install reSID system wide, but instead just compiled it:
tar -xvzf resid-0.16.tar.gz cd resid-0.16 ./configure make
This will result in a static "libresid.a" library being built in the hidden ".libs" sub-directory which needs to be pointed to in the tmce64 Makefile along with the reSID header files.
The tmce64 0.4 release also contains some other improvements and bug-fixes, but most notably a simple implementation of the VIC-II raster interrupt which is used by a lot of demos. However the accuracy is far from perfect so expect many demos to fail, especially later ones that rely on very precise timings. Support for undocumented 6502 opcodes was merged from the ASCII Atari 2600 Emulator project. A joystick handled by SDL2 is now also supported, but only plugged in the C64 port 2.
A challenge with reSID and SDL2 audio is to keep them in sync, so that reSID writes audio data at approximately the same speed as the SDL2 audio callback can read it. I have solved this by letting reSID directly control the overall emulation speed to catch up or let go if one or the other falls behind. There is still some problems if "warp mode" is activated where the sync is lost which will cause bad audio while attempting to catch up.
Version 0.4 can be downloaded here but the GitHub repository has also been updated.
I have uploaded some videos to YouTube that shows the reSID sound implementation:
* Late 80's "Future Composer 2.0 Intro" demo.
* Late 80's "In Full Gear" demo in monochrome mode.
* Modern "Digioi" game with heavy PETSCII graphics.
ASCII Atari 2600 Emulator
Here is another emulator called "atarascii" that can run in a Linux terminal using curses, this time targeting the Atari 2600 system. Much inspiration and code is based on my earlier work on the lazyboNES emulator. Compared to the NES graphics handled by it's PPU, the Atari 2600 and it's TIA graphics chip is quite different. The NES PPU is based around a tile-set that is updated each frame, but the TIA uses some simple registers that are updated for every scanline.
The Atari 2600 TIA can output 228 visible scanlines of video, so this emulator uses some approximation to fit this into lines/rows on the terminal. A classic terminal has only 24 lines/rows, but by using an X terminal the font size can be reduced and the window size increased to fit everything if needed. To improve a little bit on the possibly reduced screen real estate, the vertical blankling scanlines are also filtered out by default. Wether or not high graphics fidelity is needed depends entirely on the game. Make sure to run with a 256-color supported terminal for the best experience. The color palette should match well with the default "xterm-256color" and "rxvt-unicode-256color" types at least.
The emulator also depends on SDL2 for audio and input support. It is highly recommended to use a joystick/gamepad through SDL2 to play, since the keyboard input on the terminal is sketchy and not possible to emulate fully. Audio is supported but not entirely accurate since part of the implementation has been simplified. Video output on SDL is also possible and is shown in parallel with the terminal output. A CSV file can be used for TAS input as well.
ROM cartridge support is currently limited to the most common 2K, 4K and 8K sizes. I have only tested a handful of games and have no idea about the extent of compatibility. Pressing Ctrl+C in the terminal will break into a debugger where various data can be dumped, including a CPU trace.
You can download version 0.1 here or check the GitHub repository which might be updated.
Some screenshots:



Here are some video demos of "Dragster", "Tennis" and "River Raid".
CP/M-68K for Motorola 68332
I have created a CP/M BIOS to be able to run CP/M-68K on an embedded system with a Motorola 68332 MCU. The 68332 contains a "Queued Serial Module" that the BIOS interacts with to provide a console for CP/M over a RS-232 serial port. There is only support for one disk, which is a RAM disk implementation, also handled by the BIOS. During development I relied heavily on the BDM functionality which I made an adapter for, but this can also be used to load the CP/M system and RAM disk image directly into memory through the use of S-records.
Since CP/M-68K programs are written for pure Motorola 68000 CPUs there is a known incompatibility with one particular "MOVE SR,xx" instruction in Motorola 68010 and later CPUs including the 68332 core. It is possible to patch this in software, so this BIOS implements the DeciGEL method originally used by some upgraded Amiga systems.
The particular board I am using has 256KB of RAM available, mapped starting at 0x100000 and I ended up with the following memory map by dividing it into 16x16KB parts:
|--------|--------|----------| | Start | End | Use | |--------|--------|----------| | 100000 | 103FFF | VBR | | 104000 | 107FFF | TPA | | 108000 | 10BFFF | TPA | | 10C000 | 10FFFF | TPA | | 110000 | 113FFF | TPA | | 114000 | 117FFF | RAM-Disk | | 118000 | 11BFFF | RAM-Disk | | 11C000 | 11FFFF | RAM-Disk | | 120000 | 123FFF | RAM-Disk | | 124000 | 127FFF | RAM-Disk | | 128000 | 12BFFF | RAM-Disk | | 12C000 | 12FFFF | RAM-Disk | | 130000 | 133FFF | RAM-Disk | | 134000 | 137FFF | RAM-Disk | | 138000 | 13BFFF | CP/M | | 13C000 | 13FFFF | CP/M | |--------|--------|----------|
This board already has it's own BIOS that sets up the hardware (e.g. the UART in the Queued Serial Module) and ends up pointing the CPU Vector Base Register (VBR) to 0x100000. TPA is the "Transient Program Area" used by programs that run within CP/M.
The RAM disk image can be prepared on another Linux computer using cpmtools and the following definition:
diskdef ram-68332 seclen 128 tracks 72 sectrk 16 blocksize 1024 maxdir 64 skew 0 boottrk 0 os 2.2 end
The commands involved in preparing the disk and then a S-record representation would typically be:
mkfs.cpm -f ram-68332 ramdisk.bin cpmcp -f ram-68332 ramdisk.bin <file> 0:<file> srec_cat ramdisk.bin -binary -offset 0x114000 -o ramdisk.s68 -motorola -disable=data-count -execution_start_address 0
Assembling the BIOS and linking CP/M-68K for a new system requires another already running CP/M-68K system with the development tools. The Digital Research documentation from the 80's suggests using a Motorola EXORmacs development system, but luckily we have emulators available for this purpose now.
From within the emulator the following steps are used to assemble, link and relocate the CP/M-68K system binary. The final command dumps the binary in S-record format to the console:
AS68 BIOS332.S LO68 -R -UCPM -O CPM.REL CPMLIB BIOS332.O RELOC -B138000 CPM.REL CPM.SYS SENDC68 CPM.SYS
The CPMLIB library I got from the CP/M-68K version 1.3 disk set here.
Here is the BIOS assembly source code (BIOS332.S) in 68000 assembly language, it is based on the "ERG" BIOS from the Digital Research manuals:
***************************************************************** * * * CP/M-68K BIOS * * Basic Input/Output Subsystem * * For Motorola 68332 Embedded System * * * ***************************************************************** .globl _init * BIOS initialization entry point .globl _ccp * CCP entry point _init: move.l #traphndl,$10008c * Set up trap #3 handler (on VBR offset) move.l #privhndl,$100020 * Catch privilege exception (on VBR offset) move.l #welcome,a0 * Display welcome message weloop: move.b (a0)+,d1 cmpi.b #$24,d1 * Compare against '$' beq wedone jsr conout bra weloop wedone: clr.l d0 * Log on disk A:, user 0 rts traphndl: cmpi #nfuncs,d0 bcc trapng lsl #2,d0 * Multiply bios function by 4 movea.l 6(pc,d0),a0 * Get handler address jsr (a0) * Call handler trapng: rte biosbase: .dc.l _init * 0 - Initialization .dc.l wboot * 1 - Warm boot .dc.l constat * 2 - Console status .dc.l conin * 3 - Read console character .dc.l conout * 4 - Write console character .dc.l lstout * 5 - List character output .dc.l pun * 6 - Auxiliary output .dc.l rdr * 7 - Auxiliary input .dc.l home * 8 - Home .dc.l seldsk * 9 - Select disk drive .dc.l settrk * 10 - Select track number .dc.l setsec * 11 - Select sector number .dc.l setdma * 12 - Set DMA address .dc.l read * 13 - Read sector .dc.l write * 14 - Write sector .dc.l listst * 15 - Return list status .dc.l sectran * 16 - Sector translate .dc.l home * 17 - N/A .dc.l getseg * 18 - Get address of memory region table .dc.l getiob * 19 - Get I/O byte .dc.l setiob * 20 - Set I/O byte .dc.l flush * 21 - Flush buffers .dc.l setexc * 22 - Set exception handle address nfuncs=(*-biosbase)/4 wboot: jmp _ccp constat: move.b $fffc0d,d0 * Get status from SCSR register andi.b #$40,d0 * Check for RDRF=1 data available? beq noton * Branch if not moveq.l #$1,d0 * Set result to true rts noton: clr.l d0 * Set result to false rts conin: bsr constat * See if key pressed tst d0 beq conin * Wait until key pressed move.b $fffc0f,d0 * Get key from SCDR register rts conout: move.b $fffc0c,d0 * Get status from SCSR register andi.b #$01,d0 * Check for TDRE=1 transmit OK? beq conout * Wait until our port has aged... move.b d1,$fffc0f * And output it to SCDR register rts lstout: rts pun: rts rdr: rts listst: move.b #$ff,d0 * Device not ready rts home: rts seldsk: moveq #0,d0 cmpi.b #1,d1 * Only disk A: RAM disk supported bpl selrtn * Return 0 in d0 for other disks move.l #$114000,selmem * Prepare base memory address for RAM disk move.l #dph,d0 * Point d0 at dph selrtn: rts settrk: move.b d1,track rts setsec: move.b d1,sector rts setdma: move.l d1,dma rts sectran: move.w d1,d0 * No sector translation, just 1-to-1 mapping rts read: clr.l d0 move.b track,d0 mulu #16,d0 * Multiply by SPT add.b sector,d0 mulu #128,d0 * Multiply by sector size add.l selmem,d0 * Add offset for RAM disk address in memory move.l dma,a0 move.l d0,a1 move.l #127,d0 * Read 128 bytes rloop: move.b (a1)+,(a0)+ * Transfer byte dbra d0,rloop clr.l d0 rts write: clr.l d0 move.b track,d0 mulu #16,d0 * Multiply by SPT add.b sector,d0 mulu #128,d0 * Multiply by sector size add.l selmem,d0 * Add offset for RAM disk address in memory move.l dma,a0 move.l d0,a1 move.l #127,d0 * Write 128 bytes wloop: move.b (a0)+,(a1)+ * Transfer byte dbra d0,wloop clr.l d0 rts flush: clr.l d0 * Return successful rts getseg: move.l #memrgn,d0 rts getiob: rts setiob: rts setexc: andi.l #$ff,d1 * Do only for exceptions 0 - 255 cmpi #8,d1 beq noset * Skip touch privilege exception cmpi #9,d1 * and Trace exception beq noset lsl #2,d1 * Multiply exception number by 4 movea.l d1,a0 add.l #$100000,a0 * Add VBR to address move.l (a0),d0 * Return old vector value move.l d2,(a0) * Insert new vector noset: rts * DeciGEL patch originally made for Amiga systems using Motorola 68010 privhndl: movem.l D0/A0,-(SP) * Save registers move.l 8+2(SP),A0 * Pointer to opcode move.w (A0),D0 * Pickup opcode andi.w #$FFC0,D0 * Mask out EA field cmpi.w #$40C0,D0 * Is it a MOVE SR,ea? bne.s privsk bset #1,(A0) * Convert it to MOVE CCR,ea privsk: movem.l (SP)+,D0/A0 * Restore regs rte * Rerun new opcode .data * Welcome text welcome: .dc.b 13,10,'CP/M for Motorola 68332',13,10,'$' * Disk variables selmem: .dc.l 0 * Disk (RAM disk base address) requested by seldsk track: .dc.b 0 * Track requested by settrk sector: .dc.b 0 * Sector requested by setsec dma: .dc.l 0 * DMA address requested by setdma * Memory region definition memrgn: .dc.w 1 * 1 memory region .dc.l $104000 * Start .dc.l $ffff * Length/size * Disk parameter header dph: .dc.l 0 * No translation table used .dc.w 0 * Dummy .dc.w 0 .dc.w 0 .dc.l dirbuf * Pointer to directory buffer .dc.l dpb * Pointer to disk parameter block .dc.l 0 * Check vector not used for unremovable RAM disk .dc.l alv * Pointer to allocation vector * Disk parameter block dpb: .dc.w 16 * Sectors per track .dc.b 3 * Block shift .dc.b 7 * Block mask .dc.b 0 * Extent mask .dc.b 0 * Dummy fill .dc.w 143 * Disk size .dc.w 63 * 64 directory entries .dc.w 0 * Reserved .dc.w 0 * Check vector not used for unremovable RAM disk .dc.w 0 * Track offset zero since no boot sector on RAM disk .bss dirbuf: .ds.b 128 * Directory buffer alv: .ds.b 18 * Allocation vector = (disk size / 8) + 1 .end
Motorola BDM Debugger in DOS QEMU
I have some boards with a Motorola 68332 MCU (which later became Freescale and then NXP) that have a Motorola 68000 core. There is a debugging port on these boards known as "BDM" or "Background Debug Mode" which is very useful.
I have managed to connect to this port using an adapter that can be connected to a standard PC parallel port. There seems to be two versions of this adapter found on the Internet, one using a 74LS74 flip-flop and another using a 74LS76 flip-flop.
I built the 74LS74 variant using a prototype circuit board. To help with this I converted the schematic to another form to show where to connect all the wires using color coding:

Here is a picture of the finished adapter without the logic chips. 14-pin DIP sockets should have been used but I only had 20-pin DIP sockets available:

Here are the signals on the pin header that can be connected to a parallel port on a PC:
|-------|------------|--------| | DB-25 | Signal | Header | |-------|------------|--------| | 17 | STEP_OUT | A | | 10 | PWR_DN/VCC | B | | 14 | RSTOUT | C | | 1 | DSCLK | D | | 15 | FREEZE | E | | 12 | DSO | F | | 16 | DSI | H | | 11+18 | NC/GND | I | |-------|------------|--------|
Here are the signals going to the target BDM port:
|---------|--------| | Signal | Header | |---------|--------| | DSCLK | 4 | | VSS/GND | 5 | | FREEZE | 6 | | RESET | 7 | | DSI | 8 | | VDD/VCC | 9 | | DSO | 10 | |---------|--------|
I have put MS-DOS 6.22 on a QEMU x86 emulator image and then installed the DOS-based "BD32" program from Motorola which can still be found on NXP's pages.
The modern PC I am using has a PCI-express board with a parallel port and enabling passthrough in the QEMU emulator on Linux done by simply passing this on the command line arguments:
-parallel /dev/parport0
It is important to "slow down" the communication speed in BD32 when running on faster hardware, so I have used the setting "1000" which can be configured in the "bd32.cfg" file like so:
lpt1 1000
Here is a screenshot of QEMU running BD32 in DOS:
