I built, what I guess you could call, a Sound Card for the Amiga that connects via the Floppy Drive Port. Why? Because I could, and took way more effort than a silly stunt like this really should. However, it has given me an idea for an actual practical use for what I've learnt.
But the floppy drive port does have some advantages going for it. Aside from I'm giving Paula two more audio channels, all of it's high speed data is all done using DMA meaning a very low CPU overhead.
Paula will happily clock out data at 500kbps. Thats right, Paula handles the floppy disk as well as audio, and that data rate is actually quite fast! 500000/8 gives you 62500 bytes/second, and with the help of a logic analser and lots of patience I ended up with the first demo.
AMOS
I started with AMOS because I hadn't done much with C++ on the Amiga. I built a little circuit based around a simple Arduino UNO. The circuit is connected to the floppy drive port on specific connections.
Note in the playback rthe obvious glitches and pops between each buffer as its streamed to the floppy drive port. The audio is a RAW file containing Unsigned 8-bit audio at 62.5khz and has to be loaded into Chip Memory before disabling multitasking.
AMOS Source Code:
Set Buffer100Hide On' Maximum size MFMSIZE_WORDS=$3FFFDSIZE=MFMSIZE_WORDS*2Open Random1,"8bit_unsigned.raw"Field1,DSIZEAsD$Close WorkbenchBLOCKS=23Reserve As Chip Work1,DSIZEReserve As Chip Work2,DSIZECurs OffPaper0Pen5ClsPrint"Precalc...[";
ForA=1ToBLOCKSPrint" ";
NextAPrint"]"Locate11,0' Encode timing bits Reserve As Work3,DSIZE*BLOCKSP=Start(3)
ForA=1ToBLOCKSGet1,APrint"#";
CopyVarptr(D$),Varptr(D$)+DSIZEToPForG=PToP+DSIZE-1Step4LokeG,Leek(G) or%1000000010000000100000001NextGAddP,DSIZENextAPrint""Print"Playing..."_INTREQR=$DFF01E_INTREQ=$DFF09C' Amiga registersCIAAPRA=$BFE001CIABPRB=$BFD100ADKCON=$DFF09EADKCONR=$DFF010DSKPTH=$DFF020DSKLEN=$DFF024DMACON=$DFF096DMACONR=$DFF002DMASTATUS=Deek(DMACONR)
Break OffMulti No' Reset FlagsPokeCIABPRB,%11111111' Enable MTR PokeCIABPRB,%1111111' Select DF1 PokeCIABPRB,%1101111M=Start(3)
CopyM,M+DSIZEToStart(1)
AddM,DSIZEBUF=1Doke_INTREQ,2ForA=1ToBLOCKS' Setup DMA memory source DokeStart(BUF),%1LokeDSKPTH,Start(BUF) and$FFFFFFFE' Setup DMA Transfer:' 1. Enable DMA and Disk DMA DokeDMACON,%1000001000010000' 2. Set DSKLEN to $4000 DokeDSKLEN,$4000' 4. Write it again to start dma! DokeDSKLEN,MFMSIZE_WORDSor$C000DokeDSKLEN,MFMSIZE_WORDSor$C000' copy the next bit in while the DMS is happenning IfBUF=1ThenBUF=2ElseBUF=1CopyM,M+DSIZEToStart(BUF)
AddM,DSIZE' wait RepeatD=Deek(_INTREQR)
UntilBtst(1,D)
Doke_INTREQ,2DokeDSKLEN,$4000NextA' Select DF0 PokeCIABPRB,%11101111' De-Select DF0 PokeCIABPRB,%11111111Multi YesBreak On
The strange connection between pin 1 and 21 tricks the Amiga into detecting the floppy drive! The pins on the floppy drive connector that I use are:
Pin
Name
1
/READY
7
GND
12
5V
13
/SIDE/HEAD
16
/WRITE_DATA
17
/WRITE_GATE
21
/SEL1
Arduino UNO
The Arduino waits for the WRITE_GATE line to drop low, a signal that means data can be written to the disk, and then starts monitoring the WRITE_DATA pin for data.
It knows that data will be arriving at 500kbps, or intervals of 2uS, although, on the two Amigas I tested this on, it was more like 1.98us but thats another story.
To synchronise all of the data, I made a rule that the LSB of each byte MUST be a '1'. I could then use this to keep the byte decoder in sync with the data without having to resort to MFM encoding.
I decoded a single byte at a time, and then wrote this out of one of the Arduino Ports, which had a simple R-2R digital to analog converter connected.
The LEDs you see in the photo are connected to READY/SELECT, WRITE_GATE, and WRITE_DATA.
The Arduino
The Arduino source code is actually fairly short and makes use of the hardware Input Capture to precisly measure when the pulses were received from the Amiga, and also one of the hardware timers to keep track of which bit it's using.
Because it uses these hardware features it's able to lock on to the bit-stream easily.
Arduino Source Code:
#definePIN_READ_DATA8// Reads RAW floppy data on this pin. #definePIN_WRITE_GATE9// Reads RAW floppy data on this pin. #definePIN_WRITE_GATE_MASKB00000010// The mask for the port#definePIN_READ_DATA_MASKB00000001// The mask for the port#definePIN_READ_DATA_PORTPINB// The port the above pin is on#definePIN_WRITE_GATE_PORTPINBvoidsetup() {
// Disable all interrupts - we dont want them!Serial.begin(9600);
cli();
TIMSK0=0;
TIMSK1=0;
TIMSK2=0;
PCICR=0;
PCIFR=0;
PCMSK0=0;
PCMSK2=0;
PCMSK1=0;
EIMSK&=~B00000011; // disable INT0/1 interrupt maskTCCR1A=0;
TCCR1B=bit(CS10); // Capture from pin 8, falling edge. falling edge input capture, prescaler 1, no output compareTCCR1C=0;
pinMode(PIN_READ_DATA, INPUT_PULLUP);
pinMode(PIN_WRITE_GATE, INPUT_PULLUP);
pinMode(10,OUTPUT);
pinMode(11,OUTPUT);
pinMode(12,OUTPUT);
// Use timer 0 to count the correct number of msTCCR0A=0; // Simple counterTCCR0B=bit(CS00); // No prescaler, ticks = 62.5uSecTCCR0A=bit(WGM01);
TCNT0=0; // Reset counter; OCR0A=31;
OCR0B=0; // default timing windowpinMode(13,OUTPUT);
DDRD=B11111111;
}
voidloop() {
PORTB&= ~B1100;
while (PIN_WRITE_GATE_PORT&PIN_WRITE_GATE_MASK) {};
PORTB|=B1000;
while (!(PIN_WRITE_GATE_PORT&PIN_WRITE_GATE_MASK)) {
uint8_tdata=1;
// Wait for first pulse to sync with - this is the LSB of the first WORD and it's discarded as a SYNC clockTIFR1|=bit(ICF1);
while (!(TIFR1&bit(ICF1))) {};
TIFR1|=bit(ICF1);
TCNT0=0;
for (uint8_tb=0; b<7; b++) {
data<<=1;
// read a byte// Wait a maximum of 2usTIFR0|=bit(OCF0A);
// PORTB |= B0100;while (!(TIFR0&bit(OCF0A))) {
// Was a pulse detected?if (TIFR1&bit(ICF1)) {
TIFR1|=bit(ICF1);
data|=1;
TCNT0=1; // reset maximum timeout for SYNCbreak;
}
}
}
PORTD=data;
}
}
Amiga Trackdisk.device (OS Legal)
There's not too much to say here, this was an attempt to improve upon the AMOS version by not having to disable multitasking. I wanted to use the Trackdisk.device to actually perform the disk writing directly rather than messing around with hardware registers.
This doesn't work as well as it should because the Amiga is scheduling these writes mixed between other disk activities such as stepping the head back and forth and checking for the presence of disks. This causes a much more noticable glitching/popping in the audio played back.
However there was a massive improvement in the time it took to add the sync bits to the data loaded.
Amiga C++ Trackdisk.device Code Version
#include<proto/exec.h>#include<proto/utility.h>#include<clib/alib_protos.h>#include<exec/types.h>#include<exec/memory.h>#include<devices/trackdisk.h>#include<proto/exec.h>#include<proto/dos.h>#defineDRIVE_UNIT1/* DF1: */#defineTRACK_SIZE32766// Max out the DMA#defineCHIP_BUF_SIZETRACK_SIZE#defineNUM_BUFFERS2// Allocate CHIP memory buffer UBYTE*chipBuf[NUM_BUFFERS];
structIOExtTD*diskIO[NUM_BUFFERS];
// Helper to encode and fire a buffer async voidsendBuffer(intslot, LONGbytes) {
UWORD*wordBuf= (UWORD*)chipBuf[slot];
LONGwordCount=bytes/2;
for (LONGi=0; i<wordCount; i++)
wordBuf[i] =wordBuf[i] |0x8080;
diskIO[slot]->iotd_Req.io_Command=ETD_RAWWRITE;
diskIO[slot]->iotd_Req.io_Flags=0;
diskIO[slot]->iotd_Count=0xFFFFFFFF;
diskIO[slot]->iotd_Req.io_Data=chipBuf[slot];
diskIO[slot]->iotd_Req.io_Length=bytes;
diskIO[slot]->iotd_Req.io_Offset=0;
SendIO((structIORequest*)diskIO[slot]);
}
// Entry of our codeintmain(intargc, char**argv)
{
structMsgPort*diskPort=NULL;
BPTRfh=NULL;
LONGerr=0;
LONGtrack=0;
LONGbytesRead;
LONGtotalBytes=0;
structIOExtTD*diskIOSingle;
// Initialize SysBase from absolute address 4SysBase=*((structExecBase**)4);
// Open DOS library DOSBase= (structDosLibrary*)OpenLibrary("dos.library", 0);
if (!DOSBase) return1;
Printf("FloppyAudio - Raw audio streamer\n");
Printf("Opening trackdisk.device unit %ld...\n", (LONG)DRIVE_UNIT);
diskPort=CreateMsgPort();
if (!diskPort) {
Printf("ERROR: Failed to create message port\n");
err=1; gotodoneit;
}
// Open trackdisk.device unit 1 (DF1:)if (OpenDevice(TD_NAME, DRIVE_UNIT, (structIORequest*)diskIOSingle, 0)) {
Printf("ERROR: Failed to open trackdisk.device unit %ld\n", (LONG)DRIVE_UNIT);
err=3; gotodoneit;
}
Printf("trackdisk.device opened OK\n");
// Allocate both buffers and IO requests for (inti=0; i<NUM_BUFFERS; i++) {
chipBuf[i] =AllocMem(CHIP_BUF_SIZE, MEMF_CHIP|MEMF_CLEAR);
Printf("2");
diskIO[i] = (structIOExtTD*)CreateIORequest(diskPort, sizeof(structIOExtTD));
Printf("3");
diskIO[i]->iotd_Req.io_Device=diskIOSingle->iotd_Req.io_Device;
Printf("4");
diskIO[i]->iotd_Req.io_Unit=diskIOSingle->iotd_Req.io_Unit;
}
Printf("CHIP buffers allocated");
// Open raw audio file Printf("Opening audio.raw...\n");
fh=Open("8bitpcm.raw", MODE_OLDFILE);
if (!fh) {
Printf("ERROR: Failed to open audio.raw\n");
err=5; gotodoneit;
}
Printf("File opened OK, streaming...\n");
// Load entire file into chip RAM upfront LONGfileSize=1024*1024; // 1M of itUBYTE*audioData=AllocMem(fileSize, MEMF_CHIP|MEMF_CLEAR);
Read(fh, audioData, fileSize);
// Pre-encode all MSB sync bits UWORD*wordBuf= (UWORD*)audioData;
LONGwordCount=fileSize/2;
for (LONGi=0; i<wordCount; i++)
wordBuf[i] =wordBuf[i] |0x0101;
// Stream directly from chip RAM UBYTE*ptr=audioData;
UBYTE*end=audioData+fileSize;
// Prime both buffers before the loop diskIO[0]->iotd_Req.io_Command=ETD_RAWWRITE;
diskIO[0]->iotd_Req.io_Flags=0;
diskIO[0]->iotd_Count=0xFFFFFFFF;
diskIO[0]->iotd_Req.io_Data=ptr;
diskIO[0]->iotd_Req.io_Length=CHIP_BUF_SIZE;
diskIO[0]->iotd_Req.io_Offset=0;
SendIO((structIORequest*)diskIO[0]);
ptr+=CHIP_BUF_SIZE;
diskIO[1]->iotd_Req.io_Command=ETD_RAWWRITE;
diskIO[1]->iotd_Req.io_Flags=0;
diskIO[1]->iotd_Count=0xFFFFFFFF;
diskIO[1]->iotd_Req.io_Data=ptr;
diskIO[1]->iotd_Req.io_Length=CHIP_BUF_SIZE;
diskIO[1]->iotd_Req.io_Offset=0;
SendIO((structIORequest*)diskIO[1]);
ptr+=CHIP_BUF_SIZE;
// Prevent DF0 being accessed while we do thisstructMsgPort*dummyPort;
structIOExtTD*dummyIO;
dummyPort=CreateMsgPort();
dummyIO= (structIOExtTD*)CreateIORequest(dummyPort, sizeof(structIOExtTD));
// Open DF0: just to hold it OpenDevice(TD_NAME, 0, (structIORequest*)dummyIO, 0);
// CMD_STOP queues a stop - prevent *any* other use of the floppy busdummyIO->iotd_Req.io_Command=CMD_STOP;
DoIO((structIORequest*)dummyIO);
intcur=0;
intnext=1;
while (ptr<end) {
LONGremaining=end-ptr;
LONGchunkSize= (remaining>CHIP_BUF_SIZE) ?CHIP_BUF_SIZE:remaining;
// Wait for oldest request WaitIO((structIORequest*)diskIO[cur]);
if (diskIO[cur]->iotd_Req.io_Error) {
Printf("ERROR: %ld\n", (LONG)diskIO[cur]->iotd_Req.io_Error);
break;
}
// Requeue it immediately for the next chunk diskIO[cur]->iotd_Req.io_Command=ETD_RAWWRITE;
diskIO[cur]->iotd_Req.io_Flags=0;
diskIO[cur]->iotd_Count=0xFFFFFFFF;
diskIO[cur]->iotd_Req.io_Data=ptr;
diskIO[cur]->iotd_Req.io_Length=chunkSize;
diskIO[cur]->iotd_Req.io_Offset=0;
SendIO((structIORequest*)diskIO[cur]);
ptr+=chunkSize;
cur^=1;
next^=1;
}
WaitIO((structIORequest*)diskIO[cur]);
WaitIO((structIORequest*)diskIO[next]);
FreeMem(audioData, fileSize);
// Release DF0: dummyIO->iotd_Req.io_Command=CMD_START;
DoIO((structIORequest*)dummyIO);
CloseDevice((structIORequest*)dummyIO);
DeleteIORequest((structIORequest*)dummyIO);
DeleteMsgPort(dummyPort);
Printf("\nStreaming complete. %ld tracks, %ld bytes total.\n", track, totalBytes);
doneit:
if (fh) Close(fh);
if (diskIOSingle) {
CloseDevice((structIORequest*)diskIOSingle);
DeleteIORequest((structIORequest*)diskIOSingle);
}
if (diskPort) DeleteMsgPort(diskPort);
if (err) Printf("Exited with error code %ld\n", err);
returnerr;
}
__attribute__((used)) __attribute__((section(".text.unlikely"))) void_start(intargc, char**argv)
{
main(argc,argv);
}
Amiga Trackdisk.device (Less OS Legal)
The next iteration was an improvement over the previous, although in a slightly less legal way. This version locks out all of the floppy drives to stop the Amiga OS from seeking/checking for disks etc meaning we have exclusive access to them without disabling multitasking.
This means we can go back to controlling the floppy disk DMA ourselves and the result is much better. The only downside is it makes all the drives "busy" while you play back the audio.
Amiga C++ Trackdisk.device Less OS/Safe DMA Version
#include<exec/types.h>#include<exec/memory.h>#include<exec/tasks.h>#include<devices/trackdisk.h>#include<hardware/custom.h>#include<hardware/dmabits.h>#include<hardware/intbits.h>#include<proto/exec.h>#include<proto/dos.h>#include<hardware/cia.h>#defineCIABPRB (*(volatileUBYTE*)0xBFD100)
#defineDSKPTH (*(volatileUWORD*)0xDFF020)
#defineDSKPTL (*(volatileUWORD*)0xDFF022)
#defineDSKLEN (*(volatileUWORD*)0xDFF024)
#defineDMACON (*(volatileUWORD*)0xDFF096)
#defineINTREQ (*(volatileUWORD*)0xDFF09C)
#defineINTREQR (*(volatileUWORD*)0xDFF01E)
#defineCHUNK_SIZE32766#defineNUM_DRIVES4structExecBase*SysBase;
structDosLibrary*DOSBase;
#definecustom (*(structCustom*)0xDFF000)
structMsgPort*drivePort[NUM_DRIVES];
structIOExtTD*driveIO[NUM_DRIVES];
BOOLdriveOpen[NUM_DRIVES];
voidstopAllDrives(void) {
inti;
for (i=0; i<NUM_DRIVES; i++) {
if (driveOpen[i]) {
driveIO[i]->iotd_Req.io_Command=CMD_STOP;
DoIO((structIORequest*)driveIO[i]);
}
}
}
voidstartAllDrives(void) {
inti;
for (i=0; i<NUM_DRIVES; i++) {
if (driveOpen[i]) {
driveIO[i]->iotd_Req.io_Command=CMD_START;
DoIO((structIORequest*)driveIO[i]);
}
}
}
voidcloseAllDrives(void) {
inti;
for (i=0; i<NUM_DRIVES; i++) {
if (driveOpen[i]) {
CloseDevice((structIORequest*)driveIO[i]);
driveOpen[i] =FALSE;
}
if (driveIO[i]) { DeleteIORequest((structIORequest*)driveIO[i]); driveIO[i] =NULL; }
if (drivePort[i]) { DeleteMsgPort(drivePort[i]); drivePort[i] =NULL; }
}
}
intmain(intargc, char**argv) {
inti;
LONGerr=0;
BPTRfh=NULL;
UBYTE*audioData=NULL;
SysBase=*((structExecBase**)4);
DOSBase= (structDosLibrary*)OpenLibrary("dos.library", 0);
if (!DOSBase) return1;
for (i=0; i<NUM_DRIVES; i++) {
drivePort[i] =NULL;
driveIO[i] =NULL;
driveOpen[i] =FALSE;
}
// Open and CMD_STOP all drives for (i=0; i<NUM_DRIVES; i++) {
drivePort[i] =CreateMsgPort();
if (!drivePort[i]) { Printf("Can't create port %ld\n", (LONG)i); continue; }
driveIO[i] = (structIOExtTD*)CreateIORequest(drivePort[i], sizeof(structIOExtTD));
if (!driveIO[i]) { Printf("Can't create IO %ld\n", (LONG)i); continue; }
if (OpenDevice(TD_NAME, i, (structIORequest*)driveIO[i], 0) ==0) {
driveOpen[i] =TRUE;
Printf("Opened DF%ld\n", (LONG)i);
} else {
Printf("DF%ld not available\n", (LONG)i);
}
}
// Load audio file fh=Open("8bitpcm.raw", MODE_OLDFILE);
if (!fh) { Printf("Can't open file\n"); err=1; gotodoneit; }
LONGfileSize=1024*1024;
audioData=AllocMem(fileSize, MEMF_CHIP|MEMF_CLEAR);
if (!audioData) { Printf("Can't alloc chip RAM\n"); err=2; gotodoneit; }
Read(fh, audioData, fileSize);
Close(fh); fh=NULL;
// Pre-encode sync bits UWORD*wordBuf= (UWORD*)audioData;
LONGwordCount=fileSize/2;
for (i=0; i<wordCount; i++)
wordBuf[i] =wordBuf[i] |0x0101;
// Boost priority and stop all drives structTask*myTask=FindTask(NULL);
BYTEoldPri=SetTaskPri(myTask, 1);
stopAllDrives();
// Stream direct to hardware UBYTE*ptr=audioData;
UBYTE*end=audioData+fileSize;
// Reset flagsPrintf("Reset CIA\n");
CIABPRB=0xFF;
// Short delay for drive to settlefor (volatileinti=0; i<1000; i++);
// Enable motorPrintf("Reset Enable Motor Line\n");
// Short delay for drive to settle for (volatileinti=0; i<1000; i++);
// Select DF1Printf("Select DF1\n");
CIABPRB=0xFF^ (CIAF_DSKSEL1);
// Short delay for drive to settle for (volatileinti=0; i<1000; i++);
Printf("Playback starting\n");
DSKLEN=0x4000;
DMACON=DMAF_SETCLR|DMAF_MASTER|DMAF_DISK;
DSKLEN=0x4000;
WORDfirst=1;
while (ptr<end) {
LONGremaining=end-ptr;
LONGchunkSize= (remaining>CHUNK_SIZE) ?CHUNK_SIZE:remaining;
UWORDdsklen= (chunkSize/2) |0xC000;
ULONGdskptr= ((ULONG)ptr) &0xFFFFFFFE;
// Wait for previous transfer to finish fetching from RAM if (!first) {
while (!(INTREQR&INTF_DSKBLK));
INTREQ=INTF_DSKBLK;
} elsefirst=0;
// Immediately reload DSKPTH= (UWORD)(dskptr>>16);
DSKPTL= (UWORD)(dskptr&0xFFFF);
DSKLEN=dsklen;
DSKLEN=dsklen;
ptr+=chunkSize;
}
// Short delay for drive to settle for (volatileinti=0; i<1000; i++);
CIABPRB=0x7F;
// Short delay for drive to settlefor (volatileinti=0; i<1000; i++);
CIABPRB=0xFF;
// RestorestartAllDrives();
SetTaskPri(myTask, oldPri);
Printf("Done.\n");
doneit:
if (fh) Close(fh);
if (audioData) FreeMem(audioData, fileSize);
closeAllDrives();
if (DOSBase) CloseLibrary((structLibrary*)DOSBase);
returnerr;
}
__attribute__((used)) __attribute__((section(".text.unlikely"))) void_start(intargc, char**argv)
{
main(argc,argv);
}
V2 - Pi Pico
I wanted to increase the quality and get away from this silly 7-bit playback. I realised that the data being clocked out of Paula would be at a very precise interval, but the Arduino just didnt have the CPU clock speed to measure it properly. So, I switched to the Pi Pico.
The code for this uses a small PIO program to simulate the input capture used by the Arduino so once again I could measure the time between pulses. On the arduino you got 32 clock ticks between each pulse. With the Pi Pico, you end up with around 266! Quite the improvement.
Because of the increase in accuracy I was sure I'd be able to track every single bit without needing additional clock bits. To do this though I would ned to measure the actual clock time, which I did by starting every transfer with two specific WORDs
By timing how long it takes to get between those two '1's we then know how long 16-bits of data take, so dividing by 16 and we have a good idea of how long a single bit takes. After the second '1' is received, the receiver is also armed to read the rest of the data.
The Pi Pico is actual a dual-core device, so I opted to use the main core to monitor the Amiga floppy disk lines, and the second core to handle the SPI communication to the Digital to Analog converter LTC2602 device, which supports two 16-bit channels!
Now that the data had no clock bits in it, it was simply a matter of extracting every WORD for 16-bit MONO, and every byte pair for 8-bit stereo. The AHI driver however output SIGNED data, not unsigned, so to take the conversion burden (it's not much) off the Amiga, I opted to let the Pi Pico do it.
PI Pico Source Code:
#include<stdio.h>#include<SPI.h>#include"hardware/timer.h"#include"hardware/structs/systick.h"#include"pico/stdlib.h"#include"pico/multicore.h"#include"hardware/pio.h"#include"hardware/clocks.h"// Pin definitions#defineCS_PIN17// SPI CS#defineDATA_PIN15// Bitstream input#defineSPI_SCK18#defineSPI_MOSI19#definePIN_SELECT11#definePIN_SIDE12// Floppy disk side#definePIN_WRITE_DATA14// Reads RAW floppy data on this pin.#definePIN_WRITE_GATE15// Enable RAW floppy data on this pin.//#define PIN_LED 25#defineDAC_A0#defineDAC_B1#defineDAC_BOTH15#defineCMD_WRITEUPDATE48PIOpio=pio0;
uintsm=0;
// Core 1 - handles SPI output to LTC2602voidcore1_entry() {
// Setup SPIspi_init(spi0, 8000000);
spi_set_format(spi0, 8, SPI_CPOL_0, SPI_CPHA_0, SPI_MSB_FIRST);
gpio_set_function(SPI_SCK, GPIO_FUNC_SPI);
gpio_set_function(SPI_MOSI, GPIO_FUNC_SPI);
gpio_init(CS_PIN);
gpio_set_dir(CS_PIN, GPIO_OUT);
gpio_put(CS_PIN, 1);
uint8_tbuffer[3] = {CMD_WRITEUPDATE|DAC_BOTH, 0, 0};
while (1) {
uint32_tsample=multicore_fifo_pop_blocking();
if (sample&0x80000) {
// 15-bit monosample^=0x8000; // signed/unsigned conversionbuffer[0] =CMD_WRITEUPDATE|DAC_BOTH;
buffer[1] =sample>>8;
buffer[2] =sample&0xFF;
} else {
buffer[0] =CMD_WRITEUPDATE|DAC_A;
buffer[1] = ((sample>>8) &0xFF) ^0x80;
buffer[2] =0;
gpio_put(CS_PIN, 0);
spi_write_blocking(spi0, buffer, 3);
gpio_put(CS_PIN, 1);
buffer[0] =CMD_WRITEUPDATE|DAC_B;
buffer[1] = (sample&0xFF) ^0x80;
buffer[2] =0;
}
gpio_put(CS_PIN, 0);
spi_write_blocking(spi0, buffer, 3);
gpio_put(CS_PIN, 1);
}
}
voidsetup() {
set_sys_clock_khz(133000, true);
Serial.begin(115200);
gpio_init(PIN_WRITE_GATE); gpio_set_dir(PIN_WRITE_GATE, GPIO_IN);
gpio_init(PIN_WRITE_DATA); gpio_set_dir(PIN_WRITE_DATA, GPIO_IN);
gpio_init(PIN_SIDE); gpio_set_dir(PIN_SIDE, GPIO_IN);
gpio_init(PIN_DIR); gpio_set_dir(PIN_DIR, GPIO_IN);
gpio_init(PIN_SELECT); gpio_set_dir(PIN_SELECT, GPIO_IN);
gpio_init(PIN_LED); gpio_set_dir(PIN_LED, GPIO_OUT);
delay(2000);
}
// Capture and playbackvoidcapture(boolstereo) {
while (!pio_sm_is_rx_fifo_empty(pio, sm)) pio_sm_get(pio, sm); // flush anything else// The Sync bits are the LSB of each block as they arrive first// Read initial SYNC - NOTE the value from pio_sm_get_blocking COUNTS DOWNuint32_tlastTime=pio_sm_get_blocking(pio, sm);
uint32_tnextTime=pio_sm_get_blocking(pio, sm);
// Time between bits - should be around 131 ishuint32_tbitPeriod= (lastTime-nextTime) /16;
if ((bitPeriod<110) || (bitPeriod>145)) {
// This is wrong and means there was a spurious bit from somewhere or somethinglastTime=nextTime;
nextTime=pio_sm_get_blocking(pio, sm);
bitPeriod= (lastTime-nextTime) /16;
}
lastTime=nextTime;
uint32_tdata=0;
constint32_tbias=bitPeriod/2;
uint32_trunningCounter=0;
uint32_ttotalTime=0;
uint32_tstereoMarker=stereo?0x80000:0;
uint32_tsamplesPlayed=0;
while (runningCounter<20) {
if (!gpio_get(PIN_WRITE_GATE)) runningCounter=0; elserunningCounter++;
// Bit detectedif (!pio_sm_is_rx_fifo_empty(pio, sm)) {
uint32_tcurr=pio_sm_get_blocking(pio, sm);
// Work out what bit this should beuint32_tdiff=lastTime-curr;
totalTime+=diff;
lastTime=curr;
uint32_tbit= (totalTime-bias) /bitPeriod;
if (bit>=15) {
if (samplesPlayed&&samplesPlayed<16380) {
multicore_fifo_push_blocking(data|stereoMarker);
}
samplesPlayed++;
// This will go wrong if theres a lot of silenceif (bit>15) {
bit&=0x0F;
data=1<< (15-bit);
totalTime=bitPeriod* (bit+1);
} else {
totalTime=0;
data=0;
}
} else {
data|=1<< (15-bit);
}
}
}
}
// PIO program - counts cycles between falling edges// X decrements every 2 cycles, push value on falling edgestaticconstuint16_tcapture_program_instructions[] = {
(uint16_t)pio_encode_jmp_x_dec(1), // decrement X, jump to instruction 1
(uint16_t)pio_encode_jmp_pin(0), // if pin HIGH, jump back to 0 (keep waiting)// got falling edge - pin is LOW and fell through
(uint16_t)pio_encode_mov(pio_isr, pio_x),
(uint16_t)pio_encode_push(false, false),
// wait for rising edge
(uint16_t)pio_encode_jmp_pin(0), // pin HIGH, jump to top
(uint16_t)pio_encode_jmp_x_dec(4), // still LOW, decrement and loop
};
staticconststructpio_programcapture_program= { .instructions=capture_program_instructions, .length=6, .origin=-1};
voidsetup_pio(uintpin) {
uintoffset=pio_add_program(pio, &capture_program);
pio_sm_configc=pio_get_default_sm_config();
sm_config_set_wrap(&c, offset, offset+capture_program.length-1);
sm_config_set_in_pins(&c, pin);
sm_config_set_jmp_pin(&c, pin);
// Set clock divider to 1 for maximum resolutionsm_config_set_clkdiv(&c, 1.0f);
// Shift ISR left, no autopushsm_config_set_in_shift(&c, false, false, 32);
pio_gpio_init(pio, pin);
gpio_set_dir(pin, GPIO_IN);
pio_sm_init(pio, sm, offset, &c);
// Initialise X to 0xFFFFFFFFpio_sm_exec(pio, sm, pio_encode_set(pio_x, 0));
pio_sm_exec(pio, sm, pio_encode_mov_not(pio_x, pio_x));
pio_sm_set_enabled(pio, sm, true);
}
// Core 0 - handles bitstream inputvoidloop() {
multicore_launch_core1(core1_entry);
setup_pio(PIN_WRITE_DATA);
gpio_put(PIN_LED, 1);
while (1) {
// Wait for sync pulseif (!gpio_get(PIN_SELECT) &&!gpio_get(PIN_WRITE_GATE)) {
gpio_put(PIN_LED, 0);
capture(gpio_get(PIN_SIDE));
gpio_put(PIN_LED, 1);
}
}
}
AHI Retargetable Audio Driver
Originally I'd wanted to mod the Protracker source code, but 68k Assembly Language is just not something I have much experience with. I downloaded the source and examples for building an AHI driver, and spent *ages* trying to get one to compile.
In the end I borrowed the source code for the ZZ9000ax audio driver as a starting point, ripped out most of the code and replaced it with my own. The result was that it worked. It took me a while to get it stable too, and not just stable but functional.
The only downside is once the driver is active you can't access any floppy drives meaning if you press the VOLUMES button on some of the file selectors the dialog will hang. Oh well, this was just for fun...
You can download all of the above in a handy ZIP file. Why a ZIP? Well, you may want to try this on real hardware so you'll need to program up a Pi Pico or Arduino. I have to admit though, I'd be suprised if anyone else actually does build this.