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
#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
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.