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.

No comments:

Post a Comment