Sunday, 13 October 2013

Programming the Pi GPIO and SPI

GPIO Software

The original software ran under RISC OS. I chose this OS because it is single-user with cooperative multitasking and because I am not an experienced Linux programmer. The cooperative multitasking would hopefully avoid having the program interrupted during time-sensitive operations. The single-user environment allows software very free access to hardware and memory with few security constraints. I have since ported it to Rasbpian as Linux has better documentation and more people working on the kernel.

I used the bcm2835 C library for GPIO for the project. I modified the original library, which was written for Linux, to work under RISC OS. It's a very simple, bare-bones library that directly accesses the various hardware registers using Memory-mapped I/O. Since it runs under both Linux and RISCO OS (with my modifications) this made porting to Linux easier.

When I ported to Linux I found that simple edge detection did not work. I'm fairly sure that's because edge detect interrupts are now supported on the newer kernels and the interrupt service routine is clearing the edge detect bit before my code can detect it. Therefore I switched to using the /sys/class/gpio sysfs interface as described here (see paragraph 'Sysfs interface for Userspace) for the step signal.

The program starts by initializing the library with the call bcm2835_init(). This function memory-maps the registers into shared memory so that the library can access them directly. Under Linux this calls mmap. Under RISC OS this calls OS_Memory 13 to map in the same memory regions as under Linux.

To accommodate any differences in the GPIO pins selected for various function I define which pins are assigned to which function in a header "pin.h" shown below. The definitions are based on bcm2835.h. Since I have a Model B, revision 2.0 Raspberry Pi I use the revision 2.0 definitions.

// input_pins
#define DS0_IN   RPI_V2_GPIO_P1_03 // GPIO 2 connects (through 74LS06) to cable 10
#define DS1_IN   RPI_V2_GPIO_P1_22 // GPIO 25 connects (through 74LS06) to cable 12
#define DS2_IN   RPI_V2_GPIO_P1_15 // GPIO 22 connects (through 74LS06) to cable 14
#define MOTOR_ON RPI_V2_GPIO_P1_07 // GPIO 4 connects (through 74LS06) to cable 16
#define DIR_SEL  RPI_V2_GPIO_P1_11 // GPIO 17 connects (through 74LS06) to cable 18
#define DIR_STEP RPI_V2_GPIO_P1_13 // GPIO 27 connects (through 74LS06) to cable 20
#define WRITE_GATE  RPI_V2_GPIO_P1_10 // not currently implemented
#define WRITE_DATA  RPI_V2_GPIO_P1_21 // not currently implemented

// output pins

#define TRACK_0  RPI_V2_GPIO_P1_16 // GPIO 23 connects (through 74LS06) to cable 26
#define WRITE_PROTECT RPI_V2_GPIO_P1_12 // GPIO 18 connects (through 74LS06) to cable 28
#define READ_DATA     RPI_V2_GPIO_P1_19 // GPIO 10 connects (through 74LS06) to cable 30
#define INDEX_PULSE  RPI_V2_GPIO_P1_18 // GPIO 24 connects (through 74LS06) to cable 8

I also define a few other macros to make typing faster. They are shown below.

#define GPIO_IN   BCM2835_GPIO_FSEL_INPT
#define GPIO_OUT  BCM2835_GPIO_FSEL_OUTP
#define PULL_UP   BCM2835_GPIO_PUD_UP

The next step after initialization is to configure the various GPIO pins as inputs and outputs according to which floppy disk signal is being handled. The first group of library calls set the input signals as GPIO inputs and enable the pull-up resistors for all the inputs. You must enable pull-up resistors for all inputs because the ICs are open collector.

    bcm2835_gpio_fsel(DS0_IN,GPIO_IN);
    bcm2835_gpio_set_pud(DS0_IN,PULL_UP);
    bcm2835_gpio_fsel(DS1_IN,GPIO_IN);
    bcm2835_gpio_set_pud(DS1_IN,PULL_UP);
    bcm2835_gpio_fsel(DS2_IN,GPIO_IN);
    bcm2835_gpio_set_pud(DS2_IN,PULL_UP);
    bcm2835_gpio_fsel(MOTOR_ON,GPIO_IN);
    bcm2835_gpio_set_pud(MOTOR_ON,PULL_UP);
    bcm2835_gpio_fsel(DIR_SEL,GPIO_IN);
    bcm2835_gpio_set_pud(DIR_SEL,PULL_UP);
    bcm2835_gpio_fsel(DIR_STEP,GPIO_IN);
    bcm2835_gpio_set_pud(DIR_STEP,PULL_UP);

Though they are not used currently, we can also enable the floppy disk Write Gate and Write data signals if they are connected to the board.

    bcm2835_gpio_fsel(WRITE_GATE,GPIO_IN);
    bcm2835_gpio_set_pud(WRITE_GATE,PULL_UP);
    bcm2835_gpio_fsel(WRITE_DATA,GPIO_IN);
    bcm2835_gpio_set_pud(WRITE_DATA,PULL_UP);

Next the SPI interface must be initialized through the library call bcm2835_spi_begin() and the appropriate clock rate set. Finally the 'step' signal must be configured through the /sys/class/gpio interface for rising edge detection. I wrote my own small class to do this.

Virtual Disk Image

I currently support DMK virtual disk images as described here. This format is comprehensive enough to hopefully allow supporting of copy-protected disk images as well as normal formats. Since I don't have a double-density controller I only implemented single-density. The Pi has enough memory available that the entire disk image can be loaded into memory into a suitable C++ class. Since the floppy disk controller expects an actual drive to take several milliseconds to step the read head from one track to the next we have sufficient time to convert the active track to SPI output format during that time.

Converting the raw track to SPI output format is done by a lookup table. Currently I support two different SPI clock rates. The 'single' rate outputs one clock and data bit per byte so a single track byte is sent as eight SPI output bytes for a total single-density track size of 25,000 bytes. The 'double' rate outputs either a clock or a data bit per byte so a single track byte is sent as sixteen SPI output bytes for a total single-density track size of 50,000 bytes. After the initial translation we then have to fix up the various special index and data address marks which have special clock and data patterns. Fortunately there are only seven of these special marks and the location of these special bytes are stored as part of the disk format. Another lookup table is used to set the index and data address marks to the correct pattern.

SPI Output

The SPI output is time critical since we are trying to fill the FIFO in a very short period of time as well as toggle the index pulse up and down in synchronization with the SPI output. I created a TransmitTrack class to handle this class. It uses a separate thread which runs at high priority and directly accesses the SPI registers. It runs continuously until a flag is set (and a condition signaled) to exit. At the beginning of the track the index signal is set to HIGH and the current time is recorded. The index pulse should remain high for around 4-5 milliseconds so we add that to the current time so we know when to set the index pulse LOW. Next we write to the SPI FIFO until the SPI register indicates the input FIFO is full. Then we enter a loop where we call pthread_cond_timedwait for 100 microseconds or until the exit condition is signaled. Each time pthread_cond_timedwait returns we either exit (because the condition was signaled) or we continue to fill the FIFO. We also check to see if the index pulse needs to be set low. A real floppy disk spins continuously so when we reach the end of the track we wait until the last byte has been transmitted and start all over again.

The SPI interface reads and writes data at the same time and expects the input FIFO to be emptied as data arrives. Currently we read the data from the input FIFO but discard it as virtual write has not been implemented. I suspect the write access will be require much higher data rates and will probably have to use DMA for SPI input/output.

Source Code
Source code on Google Drive includes a Geany project and a simple makefile. The binary file ldos.dmk is a disk image of a bootable LDOS single-density disk. Note that you must run the program using sudo or from a root terminal.

Note: current code doesn't use the kernel spi driver so you might need to blacklist spi-bcm2708.