I am building a laptop using some MCUs to emulate a 6502, 256kb memory, SID-sound and filesystem.
I've finalised the latest version of the HW now. The new 40x4 text display instead of the old 40x3. I soldered everything porperly, also melt-glued and screwed the stuff onto a board so I can bring it with me when needed (soon).
The graphical display is not connected right now, it will be an add-on accessory later. I am fully occupied coding the multitasking system which does not need any graphics.
The I2S sound was scrapped. It worked VERY well, but is not needed on this lo-fi sound adventure :) Mono sound for SID and 8-bit samples works very well.
It took me longer to implement than expected, but was very rewarding. The debugger is not done in 6502, but in the Teensy-level. Much easier and quicker to get at all the stuff, and also much easier to format responses. Now I can set a break point, single step, look at memory, set any memory/registers, do NMI/IRQ and run again.
My first session dealt with single-stepping startup code from reset, and how IRQ and NMI was bouncing around in my scheduler. Turned out I had counted stack positions wrong in my threads. It was indeed a quick session to realise what was wrong :D
One of the things a 6502 is bad at is multitasking. I've tried to understand how to mitigate the shortcomings by designing the (virtual) HW around it so that it becomes as useful as possible. Always with a mind that a real implementation in HW would be possible and not too complex.
I've implemented a simple bank-switched system with 8KiB banks. The top page $FFxx is always locked to the same memory block, so vectors and its code can be the same for all tasks. The I/O-page at $FExx is also always locked.
A task gets 1-8 blocks of 8KiB memory, and the first always reside in bank 0. A task will thus always get a FULL zero page and stack of its own, which is very useful on a 6502. A task will get minimum 7.5KiB for code and data, maximum 63KiB without bank switching. And don't forget: a full zero page for each task.
The timer is designed so that it was as easy as possible to implement an efficient pre-emptive multitasking scheduler.
I got a new display yesterday, a Newhaven NHD-0440AZ-FL-YBW 40x4 text display with backlight. Not a giant upgrade from the 40x3 display I've used, but this one can be bought new from Mouser, has 33% more lines, has backlight and also a HD44780-compatible controller. Soldering and changing the code to run took less than 4h, so not too bad.
During this I also remember why I have a secondary processor dealing with the keyboard and text-display: it takes A LOT of time to wait for each byte sent to the display. Running this on the Teensy would quickly eat up precious CPU-cycles better spent on heavy lifting (emulation).
I had missed the fact that the backlight draws 2W. It always pays to read the documentation thoroughly... Might have to fix some PWM control for the backlight level later.
Almost forgot: I could ditch the DC-DC converter used on the old display :D
I just added the possibility to change irq-rate by writing to a 16-bit register in the PZ1. The irq used to be hard-coded @50Hz, coinciding with the frame counter, but those are now separated. This also leads to the possibility to change play-back rate of SID-songs with a simple DOKE in basic, very handy :)
I've added commands in ehBasic now so I can play music in the background.
SIDPLAY :REM PLAY DEFAULT SUBSONG
SIDPLAY2 :REM PLAY SUBSONG 2
SIDSTOP :STOP PLAYING
The text display I use is 40x3 characters, not a lot. I implemented a local 40x30 copy that can be scrolled around in with Alt-arrowkeys, a bit like looking at the print-out on an old paper terminal. The primary MCU does not have to deal with any of the scrolling, and it is possible to actually get some overview on printed stuff.
I'd really like more than 30 lines back-buffer, but the Mini Pro MCU is very limited on RAM, so this is what's possible with the HW at hand.
I've hinted that I use basic as an easy way to test the system functions as I code them. Here is an example:
10 PRINT DEEK($FE42), DEEK($FE44) : GOTO 10
DEEK is the equivalent of PEEK, but on a 16-bit word. So the line of code prints two 16-bit values, from I/O-ports $FE42 and $FE44, which are the touch-screen X- and Y-coordinate. It also loops forever. A very simple and informative way to test the implementation.
The 6502 in the PZ1 has 256kb RAM, no ROM. Any ROM contents are copied at startup to the correct place in memory by the emulator.
The memory is divided into 8kb memory banks, with each bank having an 8-bit register to choose where in the 256kb-space it points. It is possible to change memory bank 0 to swap between several Zero pages. Multi-tasking possibilities abound :D
I/O-area is always overlaid at $FE00-$FEFF.
One very fun and interesting aspect of retro computing is the different methods of limited sound capabilities that were available. I chose to implement a SID-emulator and a simple 8-bit DAC with FIFO.
The SID-emulator was hard to get right, and I spent a LOOONG time on it. I started with a library that sounded really wrong, then I switched to tinySID by Christian Bauer. I've stripped the filters out, only use one SID-chip and use floating point math. It sounds good and doesn't take too many CPU cycles mixing at 44100Hz. I've written hard-coded test-code that can play most psids from the C64, sounding the way they are supposed to. One of the near-future projects is to write a loader that can use the bank-switched memory and have a tune play in the background while running other code.
A small trick I picked up from the BBC micro SID-player: the PZ1 uses its own SID-addresses. After each SID-play loop, the contents of the memory addresses the C64-SID use are copied to the PZ1 equivalents. It takes a few more 6502-cycles but works really well.
The 8-bit DAC is simple to use in the 6502-space. Every frame there are 44100 / 50 = 882 samples to poke to a specific address, that will fill the FIFO. The emulation code mixes these samples with the SID sound.
I started out using the built-in MQS sound output of the Teensy, which sounded decent, but I wanted something a bit better. A cheap external I2S 16-bit stereo DAC is now connected to the Teensy. The sound is the same in both channels.