Microdrive data line format

Nagging hardware related question? Post here!
User avatar
gertk
Hardware Hero
Posts: 41
Joined: Mon Aug 19, 2013 10:00 pm

Re: Microdrive data line format

Postby gertk » Fri Jul 07, 2017 6:23 pm

For any of you who would like to tinker with the source (it is working reasonably well, sometimes the drives get mixed up alas) I will post it here:
Feel free to try it on the mbed LPC11U24

Code: Select all

// ===================================================
// QL microdrive emulator
// by Gert van der Knokke (c)2013
// adapted for LPC11U24 @ 48 MHz
// ===================================================
#include "mbed.h"

#include "SDHCFileSystem.h"
#include "FATFileSystem.h"


#define VERSION 0.01

// setup a pointer to the GPIO Toggle register
#define LPC_GPIO_PORT0_NOT0   ((volatile uint32_t *) 0x50002300)

// setup pointer to GPIO byte pin register for P0.7 (MD_DATA1) and P0.17 (MD_DATA2)
#define LPC_GPIO_BYTE_DATA1    ((volatile uint8_t *) 0x50000000+7)
#define LPC_GPIO_BYTE_DATA2   ((volatile uint8_t *)  0x50000000+17)

extern "C" void get_bit();

// debug/status leds
DigitalOut myled1(LED1);
DigitalOut myled2(LED2);
DigitalOut myled3(LED3);
DigitalOut myled4(LED4);

// IO pin definition
DigitalInOut    MD_DATA1(p20);          // MD track1 data P0.22
DigitalInOut    MD_DATA2(p19);          // MD track2 data P0.16
DigitalIn       MD_RW(p18);             // MD Read/Write line P0.14
InterruptIn     MD_COMMS_CLK(p17);      // MD Selection clock P0.13
DigitalIn       MD_COMMS_IN(p16);       // MD Selection data P0.12

// debug output to PC
Serial linktopc(USBTX, USBRX); // tx, rx

// pointer to 32k buffer space (second 32k RAM in mbed)
unsigned char buffer[2048];

// some standard definitions
#define DEBUG_BLOCK_SIZE  32
#define NR_ZERO_BITS 24
#define NR_ONE_BITS 8
#define SAMPLE_DELAY 25     // 250 ns

//#define HALF_BIT_TIME  480    // 5 usec  (500/1.04)
#define HALF_BIT_TIME  (430/2)    // 5 usec  (500/1.04)
#define BIT_TIME    (HALF_BIT_TIME * 2)
//#define GAP1_TIME       346154    // original
#define GAP1_TIME       (370000/2)      // longer to be able to sample R/W
#define GAP2_TIME       (530769/2)
#define BIT_SHIFT    4   // define the number of bits between track 1 and 2
#define TRACK_OFFSET    120

#define SELECT_IGNORE_TIME  100 // ignore select signals shorter than this

// QL microdrive parameters
#define BLOCK_SIZE          538         // 538
#define HEADER_SIZE         28          // 28
#define PREAMBLE_SIZE       12          // 12 bytes sync data
#define MDV_SECTOR_SIZE     686         // sector offset in MDV file

// Start sector to emit first after reset
#define START_SECTOR        127

// how many devices do we support
#define MAX_DEVICES          8

// which is the first device we emulate
#define START_DEVICE         3

// numbers of sectors to skip after motor off command received
#define OVERSHOOT           10

// persistent space for bit storage
volatile uint8_t old1,old2;
// setup our track1 and track2 shift registers
volatile uint8_t track1_register,track2_register;

// setup some pointers to the pseudo shift registers
volatile uint8_t *track1_register_address=&track1_register;
volatile uint8_t *track2_register_address=&track2_register;

extern int stdio_retargeting_module;

/**
 * Initialize the system for use with the RC oscillator
 *  originally by Chris
 * @param  none
 * @return none
 *
 * @brief  Setup the microcontroller system.
 *         Initialize the System.
*/
extern "C" void $Sub$$SystemInit (void)
{

// select the PLL input
    LPC_SYSCON->SYSPLLCLKSEL  = 0x0;                // Select PLL Input source 0=IRC, 1=OSC
    LPC_SYSCON->SYSPLLCLKUEN  = 0x01;               /* Update Clock Source      */
    LPC_SYSCON->SYSPLLCLKUEN  = 0x00;               /* Toggle Update Register   */
    LPC_SYSCON->SYSPLLCLKUEN  = 0x01;
    while (!(LPC_SYSCON->SYSPLLCLKUEN & 0x01));     /* Wait Until Updated       */

// Power up the system PLL
    LPC_SYSCON->SYSPLLCTRL    = 0x00000023;
    LPC_SYSCON->PDRUNCFG     &= ~(1 << 7);          /* Power-up SYSPLL          */
    while (!(LPC_SYSCON->SYSPLLSTAT & 0x01));       /* Wait Until PLL Locked    */

// Select the main clock source
    LPC_SYSCON->MAINCLKSEL    = 0x3;                // Select main Clock source, 0=IRC, 1=PLLin, 2=WDO, 3=PLLout
    LPC_SYSCON->MAINCLKUEN    = 0x01;               /* Update MCLK Clock Source */
    LPC_SYSCON->MAINCLKUEN    = 0x00;               /* Toggle Update Register   */
    LPC_SYSCON->MAINCLKUEN    = 0x01;
    while (!(LPC_SYSCON->MAINCLKUEN & 0x01));       /* Wait Until Updated       */

    LPC_SYSCON->SYSAHBCLKDIV  = 0x00000001;

    LPC_SYSCON->PDRUNCFG     &= ~(1 << 10);         /* Power-up USB PHY         */
    LPC_SYSCON->PDRUNCFG     &= ~(1 <<  8);         /* Power-up USB PLL         */
    LPC_SYSCON->USBPLLCLKSEL  = 0x0;                // 0=IRC, 1=System clock, only good for low speed
    LPC_SYSCON->USBPLLCLKUEN  = 0x01;               /* Update Clock Source      */
    LPC_SYSCON->USBPLLCLKUEN  = 0x00;               /* Toggle Update Register   */
    LPC_SYSCON->USBPLLCLKUEN  = 0x01;

    while (!(LPC_SYSCON->USBPLLCLKUEN & 0x01));     /* Wait Until Updated       */
    LPC_SYSCON->USBPLLCTRL    = 0x00000023;

    while (!(LPC_SYSCON->USBPLLSTAT   & 0x01));     /* Wait Until PLL Locked    */
    LPC_SYSCON->USBCLKSEL     = 0x00;               /* Select USB PLL           */

    LPC_SYSCON->USBCLKSEL     = 0x00000000;      /* Select USB Clock         */
    LPC_SYSCON->USBCLKDIV     = 0x00000001;      /* Set USB clock divider    */

    /* System clock to the IOCON needs to be enabled or
    most of the I/O related peripherals won't work. */
    LPC_SYSCON->SYSAHBCLKCTRL |= (1<<16);
    // stdio_retargeting_module = 1;

}


// ---------------------------------------------------------
// FM decode single bits and store them in track1 and track2
// shift registers
// ---------------------------------------------------------
// see receive.s

// show me the data (aka hexdump)
void hexdump(unsigned char *start, int size)
{
    int n,m;
    unsigned char d;

    for (m=0; m<size; m+=16) {
        // print offset in 4 bytes hex
        printf("%04x: ",m);

        // print individual bytes in hex
        for (n=0; n<16; n++) {
            d=start[m+n];
            printf("%02x ",d);
        }

        // and translate the readable ones into ASCII
        for (n=0; n<16; n++) {
            d=start[m+n];
            if ((d>31) && (d<128)) putchar(d);
            else putchar('?');
        }
        printf("\n\r");
    }
    printf("\n\r");
}

// ---------------------------------------------------------------
// wait for tape preamble: 10 bytes of $00 then two bytes of $ff
// ---------------------------------------------------------------
void wait_for_preamble()
{
    bool found;
    int n;

    found=0;
    // myled2=0;

    // try to find a header, loop until found flag is set
    while (!found) {
        // wait for defined number of zero bits
        track1_register=0xff;
        track2_register=0xff;

        while (track1_register | track2_register) get_bit();

        track1_register=0xff;
        track2_register=0xff;
        while (track1_register | track2_register) get_bit();

        // signal zeroes found
        //        myled3=1;

        // wait for $FF (first bit of the two $FF bytes)
        do {
            get_bit();
        } while (!(track1_register & 0x80));

        // read to end of FF FF header
        for (n=0; n<(NR_ONE_BITS-1); n++) {
            get_bit();
        }

        // if we have a $FF in register 1 we have arrived
        if ((track1_register & 0xff) == 0xff) found=1;
        // else printf("T1:%04x ",track1_register);
    }
    // signal sync found
    // myled4=0;
}


// send two bits on track 1 and track 2 FM modulated
void send_bit(int tr1, int tr2)
{
    LPC_CT32B0->TC=0;         // reset timer

    // toggle both pins simultanously by using the NOT register
    // create start edge
    *LPC_GPIO_PORT0_NOT0=((1<<22) | (1<<16));

    // wait half bit time
    while (LPC_CT32B0->TC < HALF_BIT_TIME);

    // if a '1' then reverse output halfway
    // selectively toggle pins according to value
    *LPC_GPIO_PORT0_NOT0=( (tr1<<22) | (tr2<<16) );

    // wait until end of bit time
    while (LPC_CT32B0->TC < BIT_TIME);
}


// send bytes from the buffer in FM format on two tracks
void send_buffer(unsigned char *buffer, int size)
{
    int n,m;
    unsigned int track1=0,track2=0;

    // send them bytes
    n=0;
    while (n<size) {
        track1 = buffer[n++];
        for (m=0; m<4; m++) {
            send_bit(track1 & 1, track2 & 1);
            track1>>=1;
            track2>>=1;
        }
        track2 |= buffer[n++];
        for (m=0; m<4; m++) {
            send_bit(track1 & 1, track2 & 1);
            track1>>=1;
            track2>>=1;
        }
    }
    // send last bits of track 2
    for (m=0; m<4; m++) {
        send_bit(track1 & 1, track2 & 1);
        track1>>=1;
        track2>>=1;
    }
    // set idle level (high)
    MD_DATA1=1;
    MD_DATA2=1;
}

// receive updated sector information
void do_receive()
{
    int m,n;

    // wait for pre-amble
    wait_for_preamble();

    // we are now at the start of the block data (minus preamble!)
    // track1 and track2 register still contain the FF

    // copy data to buffer
    for (m=0; m<(BLOCK_SIZE-PREAMBLE_SIZE); m+=2) {

        // at t0
        buffer[m]=track1_register;

        // get 4 bits
        for (n=0; n<4; n++) get_bit();

        // at t4
        buffer[m+1]=track2_register;

        // wait 4 bits
        for (n=0; n<4; n++) get_bit();

        // at t8
        // printf("R%d\n\r",m);
    }
    // save the last 4 bits of track 1
    buffer[m]=track1_register;

    // get 4 more bits
    for (n=0; n<4; n++) get_bit();

    // and store track2 in buffer
    buffer[m+1]=track2_register;
}

// send a complete sector to the QL
// during the sending the QL can pull the write line down
// to update the current sector!
void do_send_sector(int sector, FILE *fp)
{
    bool update=0;
    // myled4=0;

    // set pins to output
    MD_DATA1.output();
    MD_DATA2.output();

    // send the header
    send_buffer(buffer,HEADER_SIZE);

    // reset timer
    LPC_CT32B0->TC=0;

    // wait at least GAP1 time
    // while waiting check R/W line for update and break if so
    while (LPC_CT32B0->TC < GAP1_TIME) {
        if (!MD_RW) {
            myled4=1;
            update=1;
            break;
        }
    }

    // check if we should rewrite this sector
    if (update) {
        MD_DATA1.input();
        MD_DATA2.input();

        // computer wants to update this sector
        // so receive the data from the QL
        do_receive();

        // reset timer
        LPC_CT32B0->TC=0;

        // seek to start of data block of this sector in SD card file
        fseek(fp, (sector*MDV_SECTOR_SIZE)+HEADER_SIZE+PREAMBLE_SIZE, SEEK_SET);
        // write data block part from buffer (skip first two FF bytes)
        fwrite(buffer+2,BLOCK_SIZE-PREAMBLE_SIZE,1,fp);
        myled4=0;
    } else {
        // else send data block from buffer
        send_buffer(buffer+HEADER_SIZE,BLOCK_SIZE);

        // reset timer
        LPC_CT32B0->TC=0;
    }
    // wait GAP2 time
    while (LPC_CT32B0->TC < GAP2_TIME);

    // reset pins to input
    MD_DATA1.input();
    MD_DATA2.input();
}


// volatile storage for MD select
volatile unsigned char select;
volatile unsigned char jk_data;

// ISR for MD selection register
// each Microdrive has a single bit shift register inside
// in fact a JK flipflop so we need to emulate that..
void md_comms_clock_isr()
{
    // shift register
    select<<=1;
    // or in the previous data bit
    select|=jk_data;
    // save current data bit
    jk_data=MD_COMMS_IN;
}


// =================================================================================
// the main event...
// =================================================================================
int main()
{
    unsigned int current_sector[MAX_DEVICES];
    DIR *mydir;
    struct dirent *p;
    char filename_buffer[MAX_DEVICES][32];
    FILE *fp[MAX_DEVICES];
    int c;
    int l;
    int index=0xff;
    int old_select=0;

    // setup debug port
    //linktopc.baud(115200);
    linktopc.baud(921600);
    setbuf(stdout, NULL); // no buffering for this filehandle

    // init SD card system
    SDFileSystem sd(p11, p12, p13, p14, "sd"); // mosi, miso, sclk, cs

    // Init 32 bit counter to run at 1:1 CPU clock (48 MHz)
    LPC_SYSCON->SYSAHBCLKCTRL |= (1<<9); //enables clock for the 32-bit counter/timer 0
    LPC_CT32B0->TCR = 2;        // reset timer
    LPC_CT32B0->PR = 0;         // set Prescale to 0
    LPC_CT32B0->PC = 0;         // set Prescale counter to 0
    LPC_CT32B0->TCR = 1;        // start timer

    // announce ourselves to the debug host
    // printf("\n\rQL microdrive emulator V%1.2f (c)2013 KGE\n\r",VERSION);
    myled1=0;
    myled2=0;
    myled3=0;
    myled4=0;


    // set COMMS_IN data line to 'pull down' otherwise the motor
    // of the previous MD in the chain will run when we are idle
    MD_COMMS_IN.mode(PullDown);

    // reset the select shift register
    select=0;
    jk_data=0;
   
    // init the interrupt driven select routine
    MD_COMMS_CLK.fall(&md_comms_clock_isr);

    c=0;

    // open SD card storage and show file list
    // assign up to MAX_DRIVES to mdv devices
    mydir = opendir("/sd");
    if (mydir != NULL) {
        while ((p = readdir(mydir)) != NULL) {
         //   printf("%s\n\r",p->d_name);
            l=strlen(p->d_name);
            if ((!strncmp(p->d_name+(l-4),".MDV",4))||(!strncmp(p->d_name+(l-4),".mdv",4))) {
                if (c<MAX_DEVICES) {
                    // printf("found MD file: %s assigned mdv%d_\n\r",p->d_name,c+3);
                    strncpy(filename_buffer[c],"/sd/",32);
                    strncpy(filename_buffer[c++]+4,p->d_name,32);
                }
            }
        }
    } else {
        // Panic mode.. no usable SD card found..
        while(1) {
            myled1=1;
            wait(0.2);
            myled1=0;
            wait(0.2);
        }
    }

    // reset sector counters
    for (c=0; c<MAX_DEVICES; c++) {
        current_sector[c]=START_SECTOR;
    }


    // preset old_select
    old_select=select;

    // set data pins to input
    MD_DATA1.input();
    MD_DATA2.input();

    // main endless loop
    while(1) {

        // we could go to sleep here while waiting for
        // select to become active....
        // deepsleep();  // Deep sleep until external interrupt

        // are we selected ? (wait a small time to ignore reset glitches)
        if (select) {
            old_select=select;
            wait_us(SELECT_IGNORE_TIME);
        }
        // if select is stable then continue
        if (select==old_select) {
 //   printf("SEL:%02x\n\r",select);
            index=0xff;
            // if (old_select & 0x80) index=7;
            // if (old_select & 0x40) index=6;
            if (old_select & 0x20) index=5;
            if (old_select & 0x10) index=4;
            if (old_select & 0x08) index=3;
            if (old_select & 0x04) index=2;
            if (old_select & 0x02) index=1;
            if (old_select & 0x01) index=0;


            // if we have a valid index
            if (index != 0xff) {
   //             printf("index: %d using file: %s\n\r",index,filename_buffer[index]);

                // open the file image
                fp[index] = fopen(filename_buffer[index], "r+");

                // file opened succsfully ?
                if (fp[index]) {
                    // switch on 'drive active' indicator
                    myled1=1;

                    // loop while we are selected simulating the tape running
                    // the sector counter will roll over at 255 to zero
                    do {
                        // seek start of current sector
                        fseek(fp[index], current_sector[index]*MDV_SECTOR_SIZE, SEEK_SET);

                        // debug output
                        // printf("Reading sector %d for index %d\n\r",current_sector[index],index);
                        // read sector in buffer
                        fread(buffer,MDV_SECTOR_SIZE,1,fp[index]);

                        // send sector to QL
                        do_send_sector(current_sector[index]--, fp[index]);

                        // limit sector counter..
                        current_sector[index] &= 0xff;
                    } while (select);
                    // simulate sector overshoot and close file
                    // current_sector[index]-=OVERSHOOT;
                    // current_sector[index] &= 0xff;
                    fclose(fp[index]);
                }
                else
                {
     //               printf("file not found..\n\r");
                }
            }
            // switch off 'drive active' led
            myled1=0;
        }
    }
}


and the small assembler piece:

Code: Select all

    AREA asm_func, CODE, READONLY
   
; Export my_asm function location so that C compiler can find it and link
    EXPORT get_bit
   
    IMPORT track1_register_address
    IMPORT track2_register_address
   
    ALIGN
   
get_bit
    push    {r4,r5}
   
    ldr     R0,=0x50000000      ; pointer to LPC_GPIO_BYTE_DATA
    ldr     R3,=0x40014008      ; pointer to Timer 0 counter register

    ; wait for an edge on DATA1
    ldrb    R1,[R0,#22]          ; sample pin 0.7
wait
    ldrb    R2,[R0,#22]          ; compare with pin 0.7
    cmp     R1,R2
    beq     wait
   

    ; reset timer
    ldr     R1,=0x00000000      ; reset value
    str     R1,[R3]             ; store in timer
   
    ldrb    R1,[R0,#22]         ; sample DATA 1 (old1)
    ldrb    R2,[R0,#16]         ; sample DATA 2 (old2)

           
    ; wait a fixed half bit time then sample in the middle
    ldr     R5,=240             ; compare value
wait2
    ldr     R4,[R3]             ; get timer value
    cmp     R4,R5               ; wait until timeout
    blt     wait2
   
    ; sample data lines again
    ldrb    r4,[r0,#22]          ; sample DATA 1
    ldrb    r5,[r0,#16]          ; sample DATA 2
   
    eors    R1,R4                   ; FM decode DATA 1
    ldr     r0,=track1_register_address     ; point to track 1 shift register
    ldr     r0,[r0]
    ldrb    r4,[r0]                 ; get current value
    lsrs    R4,#1                   ; shift right track register
    lsls    R1,#7                   ; shift left decoded bit
    orrs    R4,R1                   ; logic OR over R4
    strb    r4,[r0]                 ; and store back into track 1 register

    eors    R2,R5                   ; FM decode DATA 2
    ldr     r0,=track2_register_address     ; point to track 2 shift register
    ldr     r0,[r0]
    ldrb    r4,[r0]                 ; get value
    lsrs    R4,#1                   ; shift right track register
    lsls    R2,#7                   ; shift left decoded bit
    orrs    R4,R2                   ; logic OR over R4
    strb    r4,[r0]                 ; and store back into track 2 register
   
    pop     {r4,r5}
    BX      LR                      ; return to caller   
       
    ALIGN
    END
   


Have fun!

Gert


Paul
Gold Card
Posts: 257
Joined: Mon May 21, 2012 8:50 am

Re: Microdrive data line format

Postby Paul » Sat Jul 08, 2017 4:40 pm

Hello Gert

Thank you very much for sharing this.
We talked about it on the ClassicComputing 2016 ;)
I will definitely give it a try.
Kind regards
Paul


xelalex
ROM Dongle
Posts: 1
Joined: Thu Aug 04, 2011 9:55 am

Re: Microdrive data line format

Postby xelalex » Mon Mar 19, 2018 7:55 pm

Hello,

I've recently finished a similar prototype for the Spectrum with Interface I, although with a few differences: I'm not storing the data on a card, but rather get/put the sectors from/to a PC via USB, so the adapter is rather a protocol converter. I'm also using a far slower micro controller, an Arduino Nano. Just liked the challenge of doing things "Sinclair style", with as little and small components as possible ;) I started a discussion here:

https://spectrumcomputing.co.uk/forums/viewtopic.php?f=22&t=533

One of the next steps would be to try this with my QL. I already know about the different frequency for the FM modulation. On the Spectrum a bit cycle is 12us, while a one causes a level change half way. On the QL, cycle time is 10us. But what about other differences? Is the header/record structure identical? From what I've gathered so far it seems to be. But what I'm mostly interested in is whether the gap times are the same, since that's the most critical part. On the Spectrum, the header-record gap is 3.75ms, and the record-header gap 7.0ms.

Regards,

Alex



Who is online

Users browsing this forum: No registered users and 4 guests