Tuesday, 6 November 2012

Another video interruption - serial audio with a PIC 16F1825

While writing up how SD cards work, and reading files using FAT16, we've also been developing our little audio board a bit further. We got as far as playing audio files from our SD card and fitting them to the alarm clocks for a tester project the other day.

We've now taken the project one stage further, and introduced serial/uart support.
So rather than play a file immediately, you can send serial commands to the audio board.

  • # - send back a list of all files found in the root directory
  • | - pause playback
  • > - resume playback 
  • \ - half current volume (max four times)
  • / - double current volume (to max)

At any time you can send in a filename over serial.
If the filename is in the format xxxxx.yyy (where xxx is any string up to 8 characters and .yyy is a full stop followed by a three-character extension) the chip will search the root directory for a file with the same filename and extension.

If a file is found, it begins playing immediately.
If the chip is in the middle of playing a wav, the current wav file is stopped, the current sector is closed, and the chip plays the new sound from the start.



Note - our video is blurry (again) thanks to wonderful camera-phone technology! And trying to hold the speaker in one hand, the phone in the other, and typing at the same time ;-)

The PWM carrier signal generates a hum which is much more noticeable in the video than in real life, but in a future revision, we'll look to remove this using a low-pass filter or capacitor across the speaker terminals.

At present we're playing raw (headerless) PCM wav files.
The frequency is hard-coded to 22khz and all sounds are played in mono at 8-bit. This requires the sounds to be prepared before copying them to the SD card. A future revision will be able to play different types of wav files, but for now, we've a single-chip, low-cost, low component-count audio player board which works with all SD cards we've tried it with!

Saturday, 3 November 2012

Understanding FAT tables

While it's possible to simply read and write data to and from our SD card serially (treating the card as one big eeprom chip) the most useful way to use the card is to format it using FAT16.
This will allow the card to be removed from the microcontroller and the data either read back or written to using a PC. This is far more useful that having to use some dedicated hardware to stream read/write data to/from the card and report it's finding back over UART/serial.

FAT is quite a complicated structure and needs careful understanding; it's very easy to get lost and end up reading chunks of data from the wrong sector and mangling files - especially if files on the card are frequently added or deleted and the files end up fragmented (i.e. written over a number of non-consecutive sectors).

To get started, download a hex editor (we used HxD) and format your sd card to FAT or FAT16 (not FAT32) format.


Here's your first gotcha:
When you mount an sd card into a hex editor to view the entire file contents, the hex editor usually starts from the LBA (logical block address). It will often call this sector zero.
But on the SD card, this may not actually be physical sector zero. If you use the previous card reading functions and write out the contents of sector zero over serial, you'll see it doesn't look like a boot record - it's mostly zeros. Sector zero usually just contains information about where you'll find the actual master boot record (and where the hex editor will call sector zero).

We found the easiest thing to do was use the UART tool on the PicKit2 software to view the serial data as it was read back from the sd card. We used the "save to disk" option and then read through the file after it had been saved as a text file. You can use whichever serial-based tools you're happy with for debugging at this point:


The last two bytes of the entire sector were 0x55 and 0xAA
This is the FAT signature to say that we're reading a FAT-formatted drive. We just double-checked that our code was working and actually reading back sensible data by looking for this signature at the end of the sector-ful of data.

Most of our sector zero data was zero - 0x00.
Having read up a little about FAT tables and file layouts, we were expecting a bit more than this - we were told to expect things like the OS name/type, volume label and so on, but nothing here looked anything like it. But that's because we weren't looking at "logical sector zero" - the first sector in the file structure - we were looking at physical sector zero.

So our first job was to find the logical block address LBA.
In amongst all the zeros, there was a little bit of data, starting at byte 447.
This is the data describing the actual drive itself - sectors, clusters, heads and cylinders (used in older drives). The data we're after is the two bytes at 454 (0x1C6) and 455 (0x1C7).
This is the sector number containing our master boot record MBR.

Another gotcha coming up - where data is written using two or more bytes, the order of bytes is back-to-front to what you'd expect: the least significant byte comes first. So in our example, we've got 0x87 followed by 0x00. But this actually translates to 0x0087.

Using the same routines as we did for reading sector zero, we get the PIC to read back the contents of sector 0x87 (135 decimal).


Straight away we can see that this has some useful information. Already, we can see the name MSDOS5.0 being spelled out. So what about all the other junk in there? Here's how the boot record is laid out:

  • Jump instruction (3 bytes)
  • Operating system (8 bytes)
  • Bytes per sector (2 bytes)
  • Sectors per cluster (1 byte)
  • Number of reserved sectors (2 bytes)
  • Number of file allocation tables (1 byte)
  • Max number of possible root entries (2 bytes)
  • Small sectors (2 bytes)
  • Media type (1 byte)
  • Sectors per file allocation table (2 bytes)
  • More stuff we're not particularly interested in

The bits of information we're interested in are the bytes per sector (11 bytes into the record), sectors per cluster (13 bytes in) number of reserved sectors (14 bytes in) number of FAT tables (16 bytes in) the maximum possible number of entries in the root directory (17 bytes in) and the number of sectors used by each FAT table (22 bytes into the record).

Looking at our log files, we find entries 11 and 12 are 0x00 and 0x02 respectively.
But we have to remember that data is stored back-to-front, so the bytes per sector value is actually 0x0200 which is 512 in decimal. This looks like a sensible value so we can assume we're correctly reading the data back.

Sectors per cluster (a single byte) returns 0x20 which in decimal is 32. Again, a sensible-looking value and one we'd expect (32 sectors of 512 bytes = 16kb per sector, which is the FAT16 standard).

Number of reserved sectors is important for us to work out where the FAT tables and data actually reside on the card. Reading bytes 14 and 15 we get the (don't forget to flip them around) value of 0x0008 - i.e. there are eight reserved sectors following this boot record.

Usually FAT tables are duplicated to help provide a means of data recovery should one table become corrupted. Although very few people ever use the second table (and in fact, should either of the tables become corrupt, the host machine should stop reading/writing to the sd card and report a FAT mismatch error) it still takes up space and needs to be accounted for when working out where the data starts on the disk. Byte 16 is 0x02 - ie the FAT table is duplicated once; there are two working copies of the FAT system on our card - again, the expected value.

Maximum possible number of entries in the root directory is important - we can work out where the root directory is, and the data is stored immediately after it, so we need to know how many sectors the root directory takes up. Bytes 17 and 18 tell us that the max number of records in our root directory is 0x0200 or 512 in decimal.

The number of sectors used by each FAT table is in bytes 22 and 23.
In our case, we have the value 0x00ec or 236 in decimal.

From all this information we can deduce:

  • Our boot record is at sector 135 (in your hex editor, this is sector zero)
  • After 8 reserved sectors, the FAT tables begin at physical sector 143 (in your hex editor, these would start at sector 8 - we're going to stick with physical sectors to avoid confusion, but be aware that your sectors will be offset in the hex editor)
  • Each FAT table is 236 sectors in size
  • There are two FAT tables
  • Our root directory starts at sector 135 + 8 + (2*236) = 615
  • The root directory can hold up to 512 entries. Since this is FAT16 (each entry uses 16 bytes) this means our root directory takes up 512/16 = 32 sectors
  • The actual data starts at sector 615 + 32 = 647


Our SD card already contains a few files.
Since we're going to (eventually) be playing audio, we put some RAW audio data on there (files imaginatively called wav001.raw, wav002.raw etc.) and a short text file called FAT16.txt



From our deductions, we run our "read-sector" routines on sector 615 and see what we get:
(your card may have different offset values for the boot record, reserved sectors, FAT table sizes etc, so use the equations above, but substitute with your card's values to get the actual sector to read from - your card may not have the root directory starting at sector 615. In fact, we're not sure that ours does yet.....)


Holy cow Batman, it looks like we've found our root directory........

Friday, 2 November 2012

Reading and writing to a single sector on an SD card with a PIC microcontroller

Having got our sd card to respond to the SPI initialisation routines and "boot up" correctly, it's time to actually start reading and writing data to the card.
The first thing to understand is that an SD card is made up from a series of sectors. Each sector is 512 bytes in size (almost always for SD cards) and the SD card likes to read and write entire sectors at a time.

not to scale ;-)

Reading data is a case of sending the appropriate "command packet" - 6 bytes which are:

  • the command byte (CMD17)
  • 4 parameter bytes (the address of the sector to read)
  • two CRC bytes (an accurate crc value is not actually necessary in SPI mode, but two bytes still need to be sent)

It is possible to read multiple sectors of data, one after the other, and use the sd card like one massive serial eeprom, but for now, we're going to just work one sector at a time (this approach is much better suited to working with FAT16-based files, which we'll discuss later).

The command value to read a single sector of data is CMD17. After sending the command, we have to check for an "ok" response from the sd card then wait for the "not-busy" response (any non-0xFF value).


Boolean sdReadBlock(UInt32 sec){
     UInt8 v;
     UARTPrint("Starting to read block ");
     UARTPrintNumber(sec);
     UARTPrintLn(" ");
     
     v =  sdCommandAndResponse (17, ((UInt32)sec) << 9);
     if(v & FLAG_TIMEOUT) {
          UARTPrintLn("start read block command 17 timeout");
          fatal(1);
          return false;
     }else{          
          do{
               v = sdSpiByte(0xFF);
          }while(v == 0xFF);
          if(v != 0xFE) {
               UARTLog("Read block response",v);
               fatal(2);
               return false;
          }     
          
          curPos=0;
          return true;
     }
}


This function accepts a single 4-byte parameter, the sector address to open, and returns true or false to indicate whether the card responded correctly.
It also resets a counter curPos, which will keep track of the number of bytes we've read from the current sector on the disk. When we've had exactly 512 bytes back from the disk, we'll have to "close" the current sector by reading back the two bytes that make up the crc for the previous 512-byte data stream.


void sdSecReadStop(){     
     
     if(curPos<512){
          UARTPrint("add stuff bytes to close sector - ");
          UARTPrintNumber(curPos);
          UARTPrintLn(" ");
     }
     
     while(curPos < 512){
          sdSpiByte(0xFF);
          curPos++;
     }
     
     // read back the two CRC bytes
     UARTPrint("CRC ");
     r=readByte();
     UARTByte(r);
     UARTPrint(" ");
     r=readByte();
     UARTByte(r);
     UARTPrintLn(" ");
     UARTPrintLn("End read sector");
     UARTPrintLn(" ");
     
}



Now we can open a sector and close it again, the last piece of the puzzle is to stream the data back from the sd card and do something with it! When we come to make our audio player, we'll use the data immediately - sending it to the speaker - but for testing, or for more general purpose use, we'll put the data into a series of buffers and use it later.

Now - here's a thing.
So far, everything discussed can be ported to another platform (AVR/Ardunio for example) but when it comes to storing the data in our internal buffers, there's a peculiarity with the PIC microcontroller - or at least, with most compilers that support using arrays. It may be the same with Arduino and/or other compilers, but we've always found that large arrays are often difficult to use.
This is because of the way PICs store data in RAM - it uses "banks" to keep the data in memory, and one array can't usually span more than one bank of data. In practice, this means a limit of about 90 elements in an array.

To keep things simple, we're going to store an entire sector's worth of data (512 bytes) in 8 eight arrays of 64 elements.


unsigned char wavData1[64];
unsigned char wavData2[64];
unsigned char wavData3[64];
unsigned char wavData4[64];
unsigned char wavData5[64];
unsigned char wavData6[64];
unsigned char wavData7[64];
unsigned char wavData8[64];


unsigned char readSector(){
     unsigned short cy;
     unsigned short cv;
        
     r=sdReadBlock(iSector);               
     for(cy=0; cy < 512; cy++){
         r=readByte();               
         UARTByte(r);    //write to serial for debugging
                  
         if(cy< 64){
            cv=cy;
            wavData1[cv]=r;
         }else if(cy<128){
            cv=cy- 64;
            wavData2[cv]=r;
         }else if(cy<192){
            cv=cy-128;   
            wavData3[cv]=r;
         }else if(cy<256){
            cv=cy-192;   
            wavData4[cv]=r;
         }else if(cy<320){
            cv=cy-256; 
            wavData5[cv]=r;
         }else if(cy<384){
            cv=cy-320;   
            wavData6[cv]=r;
         }else if(cy<448){
            cv=cy-384;   
            wavData7[cv]=r;
         }else if(cy<512){
            cv=cy-448;   
            wavData8[cv]=r;
         }
     }           
                                        
     sdSecReadStop();     
     UARTPrintLn(" ");
     return(1);
}


Now we can read a sector-ful of data, it's time to write some data back.
There are any number of ways you can get data into the PIC/microcontroller in order to write them to the sd card. We'll leave that side of things for another post - for now, here are some functions to write data to a specific sector on the card.

The command to write a single sector-ful of data to the card is CMD24
(you can write an entire stream across multiple sectors if you're using the card as a large serial eeprom chip, but this makes things difficult if we're going to work with FAT16 formatted cards in future).

When writing data, the 6-byte data packet consists of:
  • single command byte
  • four parameter bytes (sector to write to)
  • single crc byte
  • a single byte representing a token confirming the command request

Boolean sdWriteBlockStart(UInt32 sec){
     UInt8 v;
     v =  sdCommandAndResponse(24, ((UInt32)sec) << 9);
     if(v) {
          UARTLog("write block start response",v);
          fatal(1);
          return false;
     }else{          
          UARTPrintLn("write block started");     
          // keep track of how many bytes we've written in this sector
          // (when we hit 512 we should expect some extra bytes in the packet data)     
          bytesWritten=0;     
          
          // send the correct token for CMD17/18/24 (0xFE)
          // REMEMBER the token for CMD25 is 0xFC               
          r=sdSpiByte(0xFE);
          return true;
     }
}


Once the card has responded with an "ok" after starting to write a sector, the next bytes streamed to the card are written to the selected sector. We need to keep track of the number of bytes written - when we hit 512, it's time to close the current sector, read back the crc bytes, then open another - you don't have to write to the sequentially next sector every time, you can write to any old location if you like!


Boolean sdWriteByteToSector(UInt8 b){     
     UInt8 ix;
     r=sdSpiByte(b);          
     bytesWritten++;     
}


Boolean sdWriteToSectorClose(){          
     // finish closing the sector
     UARTPrintLn("writing stuff bytes to close sector");
     while(bytesWritten<512){
          sdSpiByte(0xFF);
          bytesWritten++;               
     }
          
     // send two CRC bytes
     sdSpiByte(0x00);
     sdSpiByte(0x00);
          
     // response should immediately follow
     // (for writing, we're looking for 0bXXX00101 data accepted)
     r=sdSpiByte(0x00);     
     UARTLog("write finish response",r);
     
     // now wait for the sd card to come out of busy
     while(r!=0x00){
          r=sdSpiByte(0x00);
     }
          
     UARTPrintLn("write finished");          
}



// write some data to sector numbered five:
void writeSomeData(){
    UARTPrintLn("Writing one block");
    ret=sdWriteBlockStart(0x05);          
    if(!ret){
         fatal(3);
    }else{

         for(iy=0; iy<=255; iy++){
              sdWriteByteToSector((255-iy));
         }                    
     
         sdWriteByteToSector(0x10);
         sdWriteByteToSector(0x11);
         sdWriteByteToSector(0x12);
         sdWriteByteToSector(0x13);
         sdWriteByteToSector(0x14);                    
                    
         sdWriteToSectorClose();                         
    }
}


That's pretty much it.
We can now read and write to an individual sector on the sd card.
There are commands for reading and writing to/from multiple blocks at a time, but we'll leave those for again; for FAT formatted cards, we need to be able to read data out-of-sequence and possibly even write files into different (non-consecutive) sectors.

How SPI works with an SD card

SD cards have two main operating modes.
Their default mode is high-speed through 4-bit wide port but we're going to be working with the "legacy" SPI (two-wire) mode.

In SPI mode, the master device (our microcontroller) talks to the slave device (the sd card) using a data and a clock line. Every time the clock line goes from low-to-high (or, if you prefer, from high-to-low - you can change this to suit the application needs) the receiving device looks at the data line. If it's high, it receives the single-bit value 1, if it's low, zero.


in this example, when the clock line goes from low-to-high (sometimes called a rising edge trigger) as denoted by the red vertical lines in the clock timing diagram, the state of the data line is converted into a value

The great thing about SPI is that it's not time dependent. Because the master device sends the clock line along with the data, it can be speeded up and slowed down (this is not possible using methods such as UART/serial, which has a fixed data rate; ie. the data has to be moved within a specific time period).

Using this clock-and-data method, we can send commands to the sd card, to tell it to do specific things. So we can send a specific value (the sd format sets out specific values to send for specific commands) to get it to reset, for example.

When an SD card has received and understood a command, it can remain in a busy state for quite some time. It is important to wait until the card has finished doing whatever you asked it to do, before blasting more data or commands at it. To do this, we poll the card (continuously ask it for data) until it gives us a "ready" token.

SPI is actually a data exchange mechanism. There's no difference between reading and writing bytes between the devices. As one device sends a byte of data, so the other transmits one. After sending a single byte of data from an SPI buffer another (possibly different) byte may appear in it's place - this is the byte that has been received.

So to read a byte from the sd card, we have to send it a byte too. It's normal to send either all zeros (0x00) or all ones (0xFF in hex is 255 in decimal, which is 11111111 in binary) for "don't care" bytes - so if you're just reading data from the other device, and it's not important what data you send, it's common to either send 0x00 or 0xFF.

Whenever a command is sent to an SD card, it follows a specific format:
There is a single command byte
There are four "parameter" bytes - data which tells the recipient how to perform the command requested
There is a single CRC (checksum) byte to prove that the previous bytes have been transmitted correctly.

To send data to our SD card, we need a couple of functions:



UInt8 sdSpiByte(UInt8 data){
     ssp1buf = data;
     while(!(ssp1stat & (1<<BF)));
     return ssp1buf;
} 



static inline void  sdSendCommand(UInt8 cmd, UInt32 param){     
     UInt8 send[6];
     
     send[0] = cmd | 0x40;
     send[1] = param >> 24;
     send[2] = param >> 16;
     send[3] = param >> 8;
     send[4] = param;
     send[5] = (sdCrc7(send, 5, 0) << 1) | 1;
     
     for(cmd = 0; cmd < sizeof(send); cmd++){
          sdSpiByte(send[cmd]);
     }
}


This provides us with a simple method of sending commands (and their parameters) to the SD card.

The first function simply puts a value into the hardware SPI buffer then waits for the SPI busy register value to go "not-busy" before returning the value it finds in the buffer (which is now the value received from the other device - data exchange remember!)

The second function actually sends commands to the sd card. Every command byte sent to an SD must have bit 6 set (so the device can recognise it as a command and not some data).
Bit 6 in binary is 01000000 which is 64 in decimal or 0x40 in hex.
So to make sure that every command byte has bit 6 set, we always OR the command byte with 0x40 (so when we send command zero, for example, it's actually transmitted as 0x40, command one is sent as 0x41 and so on).

But every time we send a command (and it's parameters), we have to wait for a not-busy response from the SD card. While the SD card is busy, it holds its "output pin" high - the data clocked out of it is always 11111111 (or 0xFF) So we build these little functions:


static inline UInt8  sdReadResp(void){
     UInt8 v, i = 0;     
     do{                
          v = sdSpiByte(0xFF);
     }while(i++ < 128 && (v == 0xFF));     
     return v;
}

static UInt8 sdCommandAndResponse(UInt8 cmd, UInt32 param){   
     UInt8 ret;          
     sdSpiByte(0xFF);
     sdSendCommand(cmd, param);
     ret = sdReadResp();     
     return ret;
}



This sdCommandAndResponse function sends a "dummy byte" to the sd card.
It then sends the command byte, followed by the 4-byte parameter value(s).
Next it calls the read-response function, which continuously sends the dummy byte 0xFF to the sd card, until the response back is not busy. When the response goes not busy, the response value is returned to the sdCommandAndResponse function. The response could either be "all ok" or it may be some kind of error code to explain why the command given could not be completed.

One last function to mention is the CRC generating function.
Strictly speaking, once we've told our card to work in SPI legacy mode, we don't actually need to generate the CRC values, but it's included here for completeness.

Note: We didn't actually create this function, we ported it from another sd card library for another platform:

static UInt8 sdCrc7(UInt8* chr,UInt8 cnt,UInt8 crc){
     UInt8 i, a;
     UInt8 Data;

     for(a = 0; a < cnt; a++){          
          Data = chr[a];          
          for(i = 0; i < 8; i++){               
               crc <<= 1;
               if( (Data & 0x80) ^ (crc & 0x80) ) {crc ^= 0x09;}               
               Data <<= 1;
          }
     }     
     return crc & 0x7F;
}


Before we can actually start sending data over SPI, we need to set up the PIC to use the hardware SPI peripheral. This means writing some values to particular registers in the chip. The names of these registers should be similar across different PIC models, but may not be exactly the same if you're using a different chip:


static void sdSpiInit(void){
     ssp1add          = 21;              //slow clock down to < 400khz
     ssp1con1     = 0b00101010;          //spi master, clk=timer2
     ssp1stat     = 0b11000000;          //CPHA = 0
}


The important registers here are the SSP1ADD (multiplier value) ad SSP1CON1 register.
When the last four bits of SSP1CON1 are 1010 this slows down the SPI clock speed - by how much depends on the value in the SSP1ADD register (and the actual speed, as in time taken to send each clock pulse, is dependent on the overall processor clock speed so will change with each chip model). When the last four bits are set to 0000, the SPI clock changes on every instruction cycle. When it's set to 1010, it changes on every x clock cycles, where x is the value held in the SSP1ADD register.

The SD card initialisation routine after powering up is:

  • Set the clock speed to less than 400khz
  • Hold the chip select line on the card low and send in about 80 clock pulses
  • Pull the chip select line high to tell the card we're talking to it
  • Send in the "soft reset" command (CMD0)
  • Wait for the sd card to respond "ok" with the value 0x01
  • Send in the "initialise card" command (CMD1)
  • Repeat sending CMD1 until the card responds with an ok value of 0x00
  • Set the sector size using CMD16 with a parameter 512 (each sector is 512 bytes)
  • Turn of the CRC requirement by sending CMD59
  • (all future transmissions do not require a valid crc value)
  • The next time the card responds with an "ok" value, it has been initialised and we can ramp the clock up to full speed.


All of the initialisation routines have to be carried out at the relatively slow clock speed of not more than 400khz. So we need a couple of extra functions to enable us to set the clock speed and enable the chip select line:


static void sdClockSpeed(Boolean fast){
     if(fast){
          ssp1con1 = 0b11110000
     }else{
          ssp1con1 = 0b11111010
     }
     
}

void sdChipSelect(bool active){
     portc.3 = !active;
}


For debugging, we've written a simple "fatal error" function to let us know which part of the initialisation failed (if there are any problems). When a fatal error is hit, this function reports it, then puts the microcontroller into "sleep mode" so that the program flow is permanently interrupted.


void fatal(UInt8 val){          //fatal error: flash LED then go to sleep
     UInt8 i, j, k;

     for(j = 0; j < 5; j++){     
          for(k = 0; k < val; k++)
          {
               delay_ms(100);
               P_LED = 1;
               delay_ms(100);
               P_LED = 0;
          }
          
          UARTLog("Error",val);
          
          delay_ms(250);
          delay_ms(250);
     }
     
     while(1){
          asm sleep
     }
}



By using all these functions together, we can now write our sdInit routine:


Boolean sdInit(){     
     UInt8 v, tries = 0;
     Boolean SD;
     
     SD = false;
     
     sdSpiInit(); // initialise SPI
     sdClockSpeed(false); // slow clock     
     sdChipSelect(false); // CS inactive          
     
     for(v = 0; v < 20; v++) {
          sdSpiByte(0xFF);     //lots of clocks to give card time to init
     }

     sdChipSelect(true); // CS active
     
     v = sdCommandAndResponse(0, 0, true);
     if(v != 1){fatal(2);}

     v = sdCommandAndResponse(1, 0, true);     
     for(int i=0;v != 0 && i<50;++i){
          delay_ms(50);
          v = sdCommandAndResponse(1, 0, true);
     }
     if(v){fatal(3);}
     
     v = sdCommandAndResponse(16, 512, true);          //sec sector size
     if(v){fatal(5);}
     v = sdCommandAndResponse(59, 0, true);          //crc off
     if(v){fatal(6);}
     
     // now set the sd card up for full speed
     sdClockSpeed(true);                    
     return true;
}


If this function successfully returns true, the SD card has been successfully initialised and the SPI lines set to run at maximum speed for the fastest (spi-based) data transfer.

Thursday, 1 November 2012

Talking to an SD card from a PIC 16F1825 microcontroller

Following on from our earlier post, we're continuing with our create-an-audio-module project which reads wav file data from an SD card and plays it through a speaker.

So far, we've written the basic framework, using Sourceboost C so now it's time to start fleshing it out.
Before we actually start making any sound, we're going to make extensive use of the UART/serial comms to report back at every stage of the data transfer, so we can pinpoint any problem areas. We're aiming to read an entire file off the sd card and stream it's contents back to the PC over serial.
Once we've done this, we'll modify the code to send the data to a PWM module to actually play the sound.

First up, with any Sourceboost project, we need our main() function.


void main(){
     unsigned char response;
     Boolean ret;     

     init();
     delay_ms(50);     
     ret = sdInit();
     if(!ret){
          UARTPrintLn("SD init failed");
          fatal(1);
     }else{
     
          setupFAT();
          response=openFile(1);
          if(response){
                         
               readCluster();
                              
               while(response!=0xFF){
                    response=openNextBlock();
                    if(response!=0xFF){
                         readCluster();
                    }else{
                         UARTPrintLn("end of file clusters");
                    }
               }
          }
          
          //just to prove we got here!
          UARTPrintLn("Done");
               
     }

     while(1){}
}


The main function pretty much speaks for itself, since it's calling a whole heap of other (hopefully appropriately named) functions:

Firstly, we initialise the microcontroller.
This involves setting the registers to define how all our different multi-function pins are set up. One of the great things about the PIC range of microcontrollers is their versatility - each pin can have multiple functions and be used for a variety of different things. Unfortunately, this can also mean hours and hours of headaches, as it can make debugging quite difficult.
So in the main, we should always call an initialise routine after powering up, to put the microcontroller into a "known state". In Oshonsoft, this includes commands such as
  • CONFIG PORTA=OUTPUT 
  • ALLDIGITAL 
etc.
These BASIC commands are simply macros to put specific values into specific registers on the chip. In this program, we're going to to that manually, following the datasheet.


void init(){
     osccon                    = 0b11110000;     //32 MHz clock     
     intcon                    = 0b10000000;     //ints enabled, all sources masked
     apfcon0                    = 0b01000000;    //MOSI on RA4
     trisa                    = 0b11001100;      //out on 0, 1, 4, 5, in on
     trisc                    = 0b00000010;      //portc.1 is our spi input
     ansela                    = 0;              //no analog inputs
     anselc                    = 0;              //no analog inputs
     option_reg               = 0b10000000;      //no weak pull-ups, timer0=osc/8
     wdtcon &= ~(1<          
     UARTInit();
     curPos=0;     
}



The first register value OSCON sets the PIC to internal oscillator, 32Mhz.
The INTCON register is where we set up interrupts (bit 7 is the global interrupts bit)
APFCON0 is the alternative pin configuration register - we've put the SPI data out (MOSI = master out, slave in) onto pin RA.4 - on this particular chip the SPI out pin can go on different pins if required.
TRISA is the register where we set which pins are inputs and which are outputs. A 1 makes the corresponding bit an input, zero makes it an output (so bit 7 of the TRISA register maps to pin 7 on PORTA).
ANSELA and ANSELC are the analogue registers for ports A and C - setting these both to zero is the same as the Oshonsoft command ALLDIGITAL.

Once we've got the chip up and running, and in a known state, we allow a few milliseconds just for all the internal voltages to settle and allow any multi-function pins to take the state requested.

Next, we call the initialise SD card routine.
This function will return a value to say whether or not the SD card started up correctly. If it didn't we report an error back and stop the code executing there (there's no point trying to read data back if the SD card isn't responding as we expect it to!)

If the SD card does respond to say it's set up correctly and ready to receive commands, we then call a function to read the FAT16 tables on the card. This involves finding the boot sector on the disk, finding the location of the actual FAT tables themselves, finding the location of the root directory on the disk and calculating where the actual data starts.
This is a long and complicated process and will be explained fully over a number of blog posts! It's hard going, but worth sticking with - being able to read and write data in a PC readable format is very very useful.

The setupFAT function returns a value to say whether or not the PIC was able to make sense of the contents of the card, and if it did, we open a file and start reading clusters of data from the card.
As long the file consists of more data, the function will keep calling and reading data back.

But already things are getting a bit confusing - clusters, sectors, boot sectors..... it's time to look into how FAT works

A short interruption for a demo

Here's a quick movie showing our final audio player actually being used in a final product.
The PIC has been programmed to read a value from eeprom, increase it by one, rollover back to zero once a certain value is reached, and use this value to decide which sound to play.
In doing this, we can get our alarm clock to play a different sound each morning!


The original alarm clock mechanism has been kept the same - when the alarm time is reached, the clock mechanism pulls a line low. We use this as the ground for our board, so that it is switched on an off by the alarm clock mechanism (the mechanism keeps the line low for about 15 minutes after passing the alarm time). By doing this, we're conserving the batteries, since the entire circuit is only every powered for about 30 minutes each day (once for 15 mins in the morning, once in the evening, unless the alarm on/off switch has been moved to disconnect the ground rail from the circuit).

Monday, 29 October 2012

Playing audio with a PIC 16F1825 microcontroller and SD card

It's taken literally weeks to get right, but finally, we've got a working example of how to play a raw/wav file from an SD card using a PIC16F1825 microcontroller.

In a departure from previous projects, we've had to put our favoured Oshonsoft compiler to one side and learn how to use Sourceboost C compiler. This is because the 16F1825 isn't supported by Oshonsoft, and we're using this micro because
a) it has loads of RAM and ROM (compared to similar chips in the same price range)
b) it runs at 32Mhz from an internal oscillator (we need plenty of clock cycles to keep the sound playing)
c) of course, it's cheap

Rather than spend time writing about each and every frustration we had during this project (and there were plenty!) we decided to get the thing working, then write up what's going on - there's nothing more frustrating than reading how someone did something, only to get to the end to be told "it didn't work, so don't you do it like this in future".
One of the biggest hurdles was moving from software to hardware periperhals.
Had we a super-fast processor, we'd probably have bit-banged everything (i.e. done all the interfacing  - PWM and SPI - through software) but we needed the data to keep flowing without interruption so that the sound played smoothly, so we had to use hardware SPI and hardware PWM. This isn't always as straightforward as the datasheets suggest!

This is the first of a multi-part post.
We're going to, first of all, interface with a SD card via SPI. Once we've got this interface working, we're going to investigate how FAT works and finally, read some files off the disk.

The first thing to do is hook up our PIC and SD card.
This particular PIC has registers which allow you to move the pins around (e.g. put the SPI output onto different pins) so there are probably lots of different ways you can hook things up. But this is how we did it, and it works ;-)



For testing, we've driven the speaker through a BS170 FET on the ground line. This isn't the best way to do it and in the final version we'll use a proper amp (like a LM386 or TS922) but it's good enough for now! Note that putting the transistor to ground does give a slightly better/louder signal than putting it on the powered side (but both will work).

Now before we get started, we need a couple of header files and some "serial comms helper functions" - these are functions we'll use to write messages back to the PC so we can see what's going on inside that little black box!


#ifndef _SD_H_
#define _SD_H_

#include "common.h"
#define SD_BLOCK_SIZE 512

Boolean sdInit();
UInt8 sdSpiByte(UInt8 byte);

Boolean sdReadStart(UInt32 sec);
void sdNextSec();
void sdSecReadStop();

#endif

This is our sd.h file



#ifndef _COMMON_H_
#define _COMMON_H_

#include

typedef signed char Int8;
typedef unsigned char UInt8;
typedef unsigned char Boolean;
typedef unsigned long UInt32;
typedef unsigned short UInt16;

#define inline

#define true 1
#define false 0

void log(UInt8);
void fatal(UInt8 val);
#define P_LED portc.2

#endif

This is our common.h file

And in the main file, main.c, we need a few functions to get us started.


#include "common.h"
#include "SD.h"

#define FLAG_TIMEOUT            0x80
#define FLAG_PARAM_ERR            0x40
#define FLAG_ADDR_ERR            0x20
#define FLAG_ERZ_SEQ_ERR      0x10
#define FLAG_CMD_CRC_ERR      0x08
#define FLAG_ILLEGAL_CMD      0x04
#define FLAG_ERZ_RST            0x02
#define FLAG_IN_IDLE_MODE      0x01

#pragma CLOCK_FREQ 32000000
#pragma DATA _CONFIG1, _FOSC_INTOSC & _WDTE_SWDTEN & _PWRTE_ON & _MCLRE_OFF & _CP_ON & _CPD_ON & _BOREN_OFF & _CLKOUTEN_OFF & _IESO_OFF & _FCMEN_OFF
#pragma DATA _CONFIG2, _WRT_OFF & _PLLEN_OFF & _STVREN_ON & _LVP_OFF

void UARTInit(){
      //
      // UART
      //
      baudcon.4 = 0;      // SCKP      synchronous bit polarity
      baudcon.3 = 0;      // BRG16      enable 16 bit brg
      baudcon.1 = 0;      // WUE      wake up enable off
      baudcon.0 = 0;      // ABDEN      auto baud detect
     
      txsta.6 = 0;      // TX9      8 bit transmission
      txsta.5 = 1;      // TXEN      transmit enable
      txsta.4 = 0;      // SYNC      async mode
      txsta.3 = 0;      // SEDNB      break character
      txsta.2 = 0;      // BRGH      high baudrate
      txsta.0 = 0;      // TX9D      bit 9

      rcsta.7 = 1;      // SPEN serial port enable
      rcsta.6 = 0;      // RX9 8 bit operation
      rcsta.5 = 1;      // SREN enable receiver
      rcsta.4 = 1;      // CREN continuous receive enable
     
      spbrgh = 0;      // brg high byte
      spbrg = 51;      // brg low byte ()
     
      apfcon0.2=1;      // tx onto RA.0            
}

void UARTSend(unsigned char c){
      txreg = c;
      while(!txsta.1);
}

void UARTPrint(char *s){
      while(*s) {
      UARTSend(*s);
      s++;
      }     
}

void UARTPrint(char c){UARTSend(c);}

void UARTPrintNumber(unsigned long n){
      unsigned long k=1000000000;
      while(k>0) {
            UARTSend('0'+n/k);
            n%=k;
            k/=10;
      }
}

void UARTPrintLn(char *s){
      UARTPrint(s);
      UARTPrint("\r\n");
}

void UARTByte(unsigned char b){
      const char *hex="0123456789abcdef";
      UARTPrint("0x");
      UARTSend(hex[b>>4]);
      UARTSend(hex[b & 0xf]);
}

void UARTLog(char *s, unsigned short iv){
      UARTPrint(s);
      UARTPrint(" ");
      UARTPrintNumber(iv);
      UARTPrintLn(" ");
}

void fatal(UInt8 val){            //fatal error: flash led then go to sleep
      UInt8 i, j, k;

      for(j = 0; j < 5; j++){     
            for(k = 0; k < val; k++)
            {
                  delay_ms(100);
                  P_LED = 1;
                  delay_ms(100);
                  P_LED = 0;
            }
           
            UARTLog("Error",val);
           
            delay_ms(250);
            delay_ms(250);
      }
     
      while(1){
            asm sleep
      }
}



We'll be calling on these functions a lot, so it's good to get them down good and early!
They should be pretty self explanitory if you're familiar with the C language; the things to look out for are the PIC internal registers used to set up the hardware peripherals.

UARTInit( ) is where the hardware UART/serial comms is set up.
We've put the serial TX pin onto pin RA.2 because we're using a PicKit2 (clone) to program our PIC. Why does this matter? Because if we use the same pins for serial data as we do for programming, we can also use the built in serial/UART tool built into the PicKit2 software without having to change any wiring!

PicKit2 in UART mode


PicKit2 in programming mode

It just so happens that in data mode, pin4 on the programmer is used for receiving serial/UART data (RX). In programming mode, pin4 on the programmer is programming data (PGD). The programming pin on the PIC is RA.0 (pin 13). By putting the UART TX pin onto RA.0 in the UARTInit function we can leave the programmer connected and simply flip between programming and UART/serial modes in the PicKit2 software.