NerdKits - electronics education for a digital generation

You are not logged in. [log in]

NEW: Learning electronics? Ask your questions on the new Electronics Questions & Answers site hosted by CircuitLab.

Everything Else » SD card for PCM sound

November 26, 2009
by pbfy0
pbfy0's Avatar

I wrote a program for reading a SD card and playing the sound on it. It can also take sound from the computer over the UART and load it into the SD card. You send the nerdkit 'l' and then PCM data in binary. use libsndfile (compile & install) and pcm2nlsv to get PCM audio with values separated by newlines. Here's a pinout of a SD card

pinout

and the connections are this:

SCK -- 1.8K -- SD pin 5 -- 3.3K -- GND
MISO -- SD pin 7
MOSI -- 1.8K -- SD pin 2 -- 3.3K -- GND
SS -- 1.8K -- SD pin -- 3.3K -- GND
SD pin 3 -- GND
SD pin 6 -- GND
SD pin 4 -- LD1086V33 -- VCC

I can't find the source right now, but I'll post it when I find it.

November 27, 2009
by pbfy0
pbfy0's Avatar

I found the source, and I changed it so it takes a number in ASCII instead of bin. here it is. I'll make a perl script to send a file with PCM values separated by newlines, and post it when I'm done.

November 27, 2009
by pbfy0
pbfy0's Avatar

Libsndfile should be here.

November 27, 2009
by pbfy0
pbfy0's Avatar

I have the perl script, here, and I have the complete process for getting a wav file onto the chip.

1) install libsndfile
    *) download it
    **) cd, ./configure, make, make install
2) compile & install pcm2nlsv
    *) download from http://nerdkits.pastebin.com/f185f2ff8
    **) run gcc -o pcm2nlsv pcm2nlsv.c
    ***) if you want, copy pcm2nlsv to /usr/bin
3) run conversion commands
    *) run these commands
    sndfile-convert -pcmu8 <infile> <outfile.raw>
    pcm2nlsv <infile.raw> <outfile>
4) upload file
    *) upload the program in the previous post
    **) run this:
    ./sound.pl <infile> <port>

<infile> for sndfile-convert is any music file

<outfile.raw> for sndfile-convert is <infile.raw> for pcm2nlsv

<outfile> for pcm2nlsv is <infile> for sound.pl

<port> for sound.pl is the serial port address. Only do steps 2 & 3 if you're repeating the sequence. Remember, the C program is UNTESTED.

November 29, 2009
by pbfy0
pbfy0's Avatar

I noticed an error in the code, here's the fixed one.

March 09, 2010
by pbfy0
pbfy0's Avatar

the SS pin on the connections table should be connected to SD pin 1 where it just says SD pin

March 20, 2010
by brian
brian's Avatar

I thought SD cards ran off 3.3 volts, doesn't the NerdKit use 5V?

March 22, 2010
by pbfy0
pbfy0's Avatar

the LD1086V33 is a 3.3 volt regulator, and the resistors regulate the data levels.

October 12, 2010
by brockmjp
brockmjp's Avatar

I'm trying to reproduce this project, but I'm having some problems with the C code. I installed and ran libsndfile and pcm2nlsv to process a small wav file. No problem there. I compiled the C code on the chip with no error. When I use the Perl script to send the processed file, it takes a while to transfer, but eventually it tells me its finished. However, no sound plays. I know that my speaker works, because I did the musicbox tutorial and got "Happy Birthday" to play. The wiring, as described, is quite simple and I've checked all my connections. For the SD card, I'm using a 2GB microSD with an SD adaptor. I have pin headers soldered to the adaptor contacts so I can plug it into my breadboard.

I'm troubleshooting the C code and I've found that my first problem (not likely to be the last) seems to be initiating the SD card. For troubleshooting, I've got six LEDs plugged into PORTC (pins PC0 to PC5). I've stripped down the C code (see below) to isolate the problem. The only modifications I've made to the code other than deleting all code that comes after MMC initiation, is to add instructions to turn on the LEDs at various points. When I run this code on the chip, I get the first three LEDs to light up. The first one indicates that I've entered the main loop. The second indicates that SPIinit() executed successfully. The third and fourth are in the for loop of the MMC_Init subroutine. The third LED lights up in the first round of the loop (when i==0) after SPI(0xFF) is executed. The fourth LED should be turned on in the subsequent round of the loop (when i==1), but it doesn't, indicating that SPI(0xFF) fails after the first time. Any ideas for next troublshooting steps? Do I need to format the microSD card somehow? I've been using this card with a camera, so I know it works (and it still works, so I know I haven't killed it).

Thanks for your time, -Joel

/*********************************************
 * Chip type           : ATmega168           *
 * Clock frequency     : 14745600Hz          *
 *********************************************/
#include <avr/io.h>
#include <avr/interrupt.h>
#include <inttypes.h>
#include <avr/io.h>
#include <stdio.h>
#include <avr/pgmspace.h>
#include <stdint.h>

#define F_OSC 14745600                 /* oscillator-frequency in Hz */
#define F_CPU F_OSC
#define SPIDI   6   // Port B bit 6 (pin7): data in (data from MMC)
#define SPIDO   5   // Port B bit 5 (pin6): data out (data to MMC)
#define SPICLK  7   // Port B bit 7 (pin8): clock
#define SPICS   4   // Port B bit 4 (pin5: chip select for MMC

void SPIinit(void) {
    DDRB &= ~(1 << SPIDI);  // set port B SPI data input to input
    DDRB |= (1 << SPICLK);  // set port B SPI clock to output
    DDRB |= (1 << SPIDO);   // set port B SPI data out to output 
    DDRB |= (1 << SPICS);   // set port B SPI chip select to output
    SPCR = (1 << SPE) | (1 << MSTR) | (1 << SPR1) | (1 << SPR0);
    PORTB &= ~(1 << SPICS); // set chip select to low (MMC is selected)
}

char SPI(char d) {  // send character over SPI
    char received = 0;
    SPDR = d;
    while(!(SPSR & (1<<SPIF)));
    received = SPDR;
    return (received);
}

char Command(char befF, uint16_t AdrH, uint16_t AdrL, char befH ) { 
    // sends a command to the MMC
    SPI(0xFF);
    SPI(befF);
    SPI((uint8_t)(AdrH >> 8));
    SPI((uint8_t)AdrH);
    SPI((uint8_t)(AdrL >> 8));
    SPI((uint8_t)AdrL);
    SPI(befH);
    SPI(0xFF);
    return SPI(0xFF);   // return the last received character
}

int MMC_Init(void) { // init SPI
    char i;
    PORTB |= (1 << SPICS); // disable MMC

    // start MMC in SPI mode  // send 10*8=80 clock pulses
    for(i=0; i < 10; i++) {
        SPI(0xFF); 
        if(i==0) {
            // Turn on the third LED
            DDRC |= (1<<PC2);
            PORTC |= (1<<PC2);
        }else{  
            // Turn on the fourth LED
            DDRC |= (1<<PC3);
            PORTC |= (1<<PC3);
        }
    }

    PORTB &= ~(1 << SPICS); // enable MMC

    if (Command(0x40,0,0,0x95) != 1) goto mmcerror; // reset MMC

st: // if there is no MMC, prg. loops here
    if (Command(0x41,0,0,0xFF) !=0) goto st;
    return 1;
mmcerror:
    return 0;
}

int main() {

    // Turn on the first LED
    DDRC |= (1<<PC0);
    PORTC |= (1<<PC0);

    SPIinit();

    //Turn on the second LED
    DDRC |= (1<<PC1);
    PORTC |= (1<<PC1);

    MMC_Init();

    while (1){
    }
    return 0;
}
October 13, 2010
by brian
brian's Avatar

To be totally honest, I can't speak for the code you're using - but MicroSD cards don't always implement SPI. Is there a chance you can try with another (full size) SD card?

October 15, 2010
by pbfy0
pbfy0's Avatar

well, I didn't write this code myself. It partly came from Arduino. It looks like the sd card isn't responding correctly, though.

October 17, 2010
by brockmjp
brockmjp's Avatar

I'm using a SanDisk 2GB microSD with a SanDisk microSD to SD adaptor. I've read in at least two other forums that SPI is implemented on the SanDisk microSD.

Its clear from the comments in the C code that it was intended for an Arduino ATmega168 running at 16MHz. It looks like the speaker is run off of PB3.

I've had a little more success with an alternative circuit described here. This circuit replaces the 5v regulator with the 3.3v regulator, so you run the whole thing on 3.3v and get rid of the voltage dividers.
The author claims he can run his ATmega168 chip on 3.3v at 16MHz without a problem. The nerdkit runs at 14.75MHz so I should be ok with 3.3v too. I got rid of the battery as well. I power the circuit by running the positive lead from the usb to the input on the 3.3v regulator.

I am now getting past the MMC_init() step, but it seems to be failing somewhere in the startPlayback(). What I need now is some simple code to test that I can write to and read from the card. I don't want to depend on the uart for this test so I want hard coded values for the data to write and what sector to write to. Something like:

int main() {
    uint8_t buffer[512];
    SPIinit();
    MMC_Init();
    while (1){

        // turn on the first LED
        DDRC |= (1<<PC0);
        PORTC |= (1<<PC0);

        write512block(1, 0);

        // turn on the second LED
        DDRC |= (1<<PC1);
        PORTC |= (1<<PC1);

        get512block(buffer, 0);

        // turn on the third LED
        DDRC |= (1<<PC2);
        PORTC |= (1<<PC2);

        if (buffer == 1) {

            // turn on the fourth LED
            DDRC |= (1<<PC3);
            PORTC |= (1<<PC3);
        }else{
            // turn on the fifth LED
            DDRC |= (1<<PC4);
            PORTC |= (1<<PC4);      
        }
    }
    return 0;
}

When I do this, the first LED comes on right away and then the second LED comes on after a short, but noticable delay (less than 1sec). The other LEDs remain off.

October 17, 2010
by pbfy0
pbfy0's Avatar

I think the problem is that set and get512block take arrays, not single values.

October 17, 2010
by brockmjp
brockmjp's Avatar

OK, I've replaced the LEDs with more informative print statements to a putty session using the libnerdkits uart library. There are some error handling print statements in the write512block() and get512block() functions. I'm still just trying to get a simple write and read to happen so I want to hard code the data to write for now. I don't want to use the Perl script to send the data, because its not necessary and I wouldn't be able to read the print statements coming from the chip unless I figure out how to receive data with the Perl module (I'll go down that rabbit hole another day).

Let's take a look at the main in the C code and step through what happens when we read one or two lines from the infile (that would be) sent by the Perl script:

int main() {

    setup();

    uint32_t lastS = 0;
    uint8_t buffer[512];
    uint32_t tmpb = 0;
    FILE uart_stream = FDEV_SETUP_STREAM(uart_write, uart_read, _FDEV_SETUP_RW);

    int i, n1, j;
    while (1){

        startPlayback();

        while (uart_getc() != 'l');
        n1 = 1;
        cli();
        fscanf_P(&uart_stream, PSTR("%u\n"), &tmpb);
        buffer[0] = tmpb & 0xFF000000;
        buffer[1] = tmpb & 0x00FF0000;
        buffer[2] = tmpb & 0x0000FF00;
        buffer[3] = tmpb & 0x000000FF;
        lastS = buffer[0];
        lastS <<= 8;
        lastS |= buffer[1];
        lastS <<= 8;
        lastS |= buffer[2];
        lastS <<= 8;
        lastS |=  buffer[3];
        i = 0;
        j = 1;
        if(n1) {
            i = 4;
        }
        while (i < lastS + 1) {

            i %= 512;
            fscanf_P(&uart_stream, PSTR("%u\n"), &(buffer[i]));
            if(!i){
                write512block(buffer, j);
                j++;
            }
            i++;
            n1 = 0;
        }
        lastSample = buffer[i - 1];
        section = 1;
        sei();
    }
    return 0;
}

First the SPI and MMC are initiated and then startPlayback() will play any sound data that is stored on the SD card. At this point the code sits waiting until it receives data from the Perl script: while (uart_getc() != 'l'); Once the Perl script sends an 'l', the code continues. So after that, when fscanf_P(&uart_stream, PSTR("%un"), &tmpb); executes, does tmpb == 'l' or does it equal the first line of the infile (which is '125' in my case? Regardless, I have no idea what the next 11 lines are doing. Can someone explain it to me? All I can tell is that values are assigned to the first four elements of the buffer array. In the while loop that follows, it looks like each line of the infile sent by the Perl script is assigned to an incremented element of the buffer array starting with the fifth element (buffer[4]). When i reaches 512 then i %= 512 changes i to 0, which causes if(i!) to be true so write512block(buffer, j); executes.

So I should be able to write a 512 block with the following code (assuming that tmpb should equal the first line of the infile):

int main() {

    SPIinit();
    MMC_Init();

    // start up the serial port
    uart_init();
    FILE uart_stream = FDEV_SETUP_STREAM(uart_putchar, uart_getchar, _FDEV_SETUP_RW);
    stdin = stdout = &uart_stream;
    // wait until terminal connection is made and user presses the'T' key to indicate that their terminal is ready to receive messages
    while (uart_read() != 'T');
    printf_P(PSTR("Initialized\r\n"));

    uint32_t lastS = 0;
    uint8_t buffer[512];
    uint32_t tmpb = 0;

    int i, j;   
    while (1) {
        printf_P(PSTR("Entered while loop\r\n"));   
        tmpb = '125';
        buffer[0] = tmpb & 0xFF000000;
        buffer[1] = tmpb & 0x00FF0000;
        buffer[2] = tmpb & 0x0000FF00;
        buffer[3] = tmpb & 0x000000FF;
        lastS = buffer[0];
        lastS <<= 8;
        lastS |= buffer[1];
        lastS <<= 8;
        lastS |= buffer[2];
        lastS <<= 8;
        lastS |=  buffer[3];

        for(i = 4; i < 513; i++) {
            buffer[i] = 125;
        }

        j = 1;      
        write512block(buffer, j);

        printf_P(PSTR("Returned from write512block\r\n"));

        get512block(buffer, j);

        printf_P(PSTR("Returned from get512block\r\n"));

        printf_P(PSTR("buffer[4] = %d\r\n"), buffer[4]);

        if (buffer[4] == 125) {
            printf_P(PSTR("Data read equals data written\r\n"));
        }else{
            printf_P(PSTR("Unable to read the data that was written\r\n")); 
        }
    }
    return 0;
}

I'm able to flash this onto the chip without error. I switch on the chip, start a putty session and send a 'T', and the output I get is:

Initialized
Entered while loop
MMC: write error 2
Returned from write512block

The 'MMC: write error 2' comes from the write512block() function:

int write512block(uint8_t block[512], uint32_t segment) { // write RAM sector to MMC
    segment *= 512;
    int i;
    uint8_t c;
    // 512 byte-write-mode
    uint16_t ix;
    char r1 =  Command(0x58,(uint16_t) (segment>>16), (uint16_t) segment,0xFF);
    for (ix = 0; ix < 50000; ix++) {
        if (r1 == (char)0x00) break;
        r1 = SPI(0xFF);
    }
    if (r1 != (char)0x00) {
        //uart_puts("MMC: write error 1 ");
        printf_P(PSTR("MMC: write error 1\r\n"));

        return 1;
    }
    SPI(0xFF);
    SPI(0xFF);
    SPI(0xFE);
    // write ram sectors to MMC
    for (i=0;i<512;i++) {
        SPI(block[i]);
    }
    // at the end, send 2 dummy bytes
    SPI(0xFF);
    SPI(0xFF);

    c = SPI(0xFF);
    c &= 0x1F;  // 0x1F = 0b.0001.1111;
    if (c != 0x05) { // 0x05 = 0b.0000.0101
        //uart_puts("MMC: write error 2 ");
        printf_P(PSTR("MMC: write error 2\r\n"));

        return 1;
    }
    // wait until MMC is not busy anymore
    while(SPI(0xFF) != (char)0xFF);
    return 0;
}
November 19, 2010
by brockmjp
brockmjp's Avatar

I finally have this project working, however there is one problem with it that seems intractable. There is a rapid clicking noise in the playback, which I belive is caused by a delay when reading from the SD card. The mcu reads a 512 byte block of samples from the SD card and plays them at a rate of 8kHz. After the 512th sample is played, the mcu reads another 512 byte block from the SD card. For smooth playback, it must do this in the time between playing the 512th sample of the last block and playing the 1st sample of the next block (0.125ms). However, the maximum data rate setting for master mode in the SPI registers is clk/2 which is 7,372,800Hz with the nerdkit crystal. That means it takes 0.556ms to read 512 bytes. So we get a delay (click) of 0.431ms every 64ms. The 8kHz sample rate already seems low. I can't see reducing that 5-fold, and we can't run the mcu 5-fold faster, so how can we get smooth playback? Would be great to have a solution. Thanks to pbfy0 for the original post, and Mike and Humberto for the email support, you guys are awesome. Here is the working C code and here is my version of the Perl script to send the PCM data.

-Joel

November 20, 2010
by pbfy0
pbfy0's Avatar

I don't really know, but loading the next block on the 508th sample might make it work.

November 20, 2010
by Rick_S
Rick_S's Avatar

I don't know if this will help any or not, but HERE is a link to someone who has devleoped a wav player on a USB Based AVR. It appears he double buffers so while one buffer is playing, the other loads. I haven't messed around with any of this so what info this provides, may or may not be of help...

Rick

November 21, 2010
by brockmjp
brockmjp's Avatar

Thanks for the ideas guys. I tried triggering the get512block at the 508th sample (and earlier). It did seem to reduce the click, but its still there. I'll look into the double buffering strategy.

-Joel

November 25, 2010
by brockmjp
brockmjp's Avatar

Double buffering did not remove the click either. It sounds exactly the same. Apparently the timer interrupt is unable to interrupt the SPI data transfer from the SD? Here is how I implemented the double buffer in the timer interrupt. I had to switch to a ATmega328 to have enough RAM to accomodate both 512 byte buffers.

// This is called at 8000 Hz to load the next sample.
ISR(TIMER1_COMPA_vect) {
    if (sample >= sounddata_length) {
        lastSample = buffer1[(sounddata_length % 512)-1]; // divide by 512 because length is 1-based, but subtract 1 because blocks are 0-based
        if (sample == sounddata_length + lastSample) {
            stopPlayback();
        }else{
            // Ramp down to zero to reduce the click at the end of playback.
            OCR2B = sounddata_length + lastSample - sample;
            sample++;
        }
    }else{
        if (section % 2 == 0) { // current section is even so play from buffer2
            OCR2B = buffer2[j];
            if (j == 511) { // end of buffer2, so refill and increment section while next buffer1 starts playing
                get512block(buffer2, section);
                j = 0;
                section++;
            }
        }else{ // current section is odd, so play from buffer1
            OCR2B = buffer1[j];
            if (j == 511) { // end of buffer1, so refill and increment section while next buffer2 starts playing
                get512block(buffer1, section);
                j = 0;
                section++;
            }
        }
        j++;
        sample++;
    }
}
November 27, 2010
by bretm
bretm's Avatar

Are you sure the remaining click is caused by the reason you think it is? After a section is finished you reset j to 0, but then you immediately increment it to 1. That means j only spans 1 to 511 and you're not playing all 512 samples. This line also seems wrong:

        lastSample = buffer1[(sounddata_length % 512)-1]; // divide by 512 because length is 1-based, but subtract 1 because blocks are 0-based

The comment suggests that sounddata_length is 1-based. If it ranges from 1 to 512 and you do %512 on it, the values will go from 1 to 511 and then 512 will become 0. If you subtract 1 from that, the values will go from 0 to 510 and then -1. That means the last sample is being read from beyond the buffer bounds for certain values of sounddata_length, but that should only affect the tail end of the playback.

November 28, 2010
by brockmjp
brockmjp's Avatar

Thanks bretm. Good catches. I moved the j++ to an else at the end of the j==511 conditionals. As for the lastSample line,I believe [(sounddata_length-1) % 511] should be the way to do it, but as you said, that would only affect the end of play and wouldn't help with the clicks during play. In fact, I tried just going to stopPlayback() when sample >= sounddata_length, as shown below, but the playback still sounds exactly the same:

ISR(TIMER1_COMPA_vect) {
    if (sample >= sounddata_length) {
        stopPlayback();
    }else{
        if (section % 2 == 0) { // current section is even so play from buffer2
            OCR2B = buffer2[j];
            if (j == 511) { // end of buffer2, so refill and increment section while next buffer1 starts playing
                get512block(buffer2, section);
                j = 0;
                section++;
            }else{
                j++;
            }
        }else{ // current section is odd, so play from buffer1
            OCR2B = buffer1[j];
            if (j == 511) { // end of buffer1, so refill and increment section while next buffer2 starts playing
                get512block(buffer1, section);
                j = 0;
                section++;
            }else{
                j++;
            }
        }
        sample++;
    }
}

I also added a low pass RC filter (100nF cap, 5k trimmer). That improves the quality a little by removing a slight ringing overtone, but does nothing with respect to the clicking noise.

-Joel

November 28, 2010
by bretm
bretm's Avatar

Inside the timer ISR, interrupts are disabled. That means samples won't be processed during get512block. Try calling sei() before get512block, or else do the block management outside the ISR (preferred).

November 29, 2010
by brockmjp
brockmjp's Avatar

bretm,

I tried calling sei() before the get512block calls and that didn't help. I tried moving the block management outside of the ISR (see code below) and no sound played at all. I did notice an error (fixed in the code below) in the sound upload part of the code similar to the increment error in the ISR you pointed out earlier. That fix by itself seemed to help the sound quality a little, but the click is definitely still there. Please let me know if you see any error in the way I coded the block management in the main loop. Seems like it should work.

I took a look at the SanDisk SD card product manual and it appears that its possible to read partial blocks. If I can get just 32 or 64 bytes with each read, that should be fast enough to complete between timer interrupts.

    // <snip>
    ISR(TIMER1_COMPA_vect) {
        if (sample >= sounddata_length) {
                stopPlayback();
        }else{
            if (j < 512) {
                if (section % 2 == 0) { // current section is even so play from buffer2
                    OCR2B = buffer2[j];
                }else{ // current section is odd, so play from buffer1
                    OCR2B = buffer1[j];
                }
                j++;
                sample++;
            }
        }
    }
    // <snip>
    int main() {
        SPIinit();
        MMC_Init();

        // start up the serial port
        uart_init();
        FILE uart_stream = FDEV_SETUP_STREAM(uart_putchar, uart_getchar, _FDEV_SETUP_RW);
        stdin = stdout = &uart_stream;

        uint32_t tmpb = 0;
        while (1){

            startPlayback();

            while (sample < sounddata_length) {
                if (j > 511) {
                    if (section % 2 == 0) {
                        get512block(buffer2, section);
                    }else{
                        get512block(buffer1, section);
                    }
                    j = 0;
                    section++;  
                }
            }

            while (uart_read() != 'l');
            cli();
            printf_P(PSTR("mcu: Received 'l'\r\n"));
            scanf_P(PSTR("%d"), &tmpb);
            printf_P(PSTR("mcu: Received %u tmpb\r\n"), tmpb);

            // The sound data length is a 32-bit number, but we can only store one byte in each element of the buffer1 array
            // The pointer will automatically split the the 32-bit number into 4 bytes and put them in the first 4 elements of the 8-bit buffer1 array
            uint32_t *ptr = (uint32_t *) buffer1;
            *((uint32_t *) ptr) = tmpb;

            printf_P(PSTR("mcu: sounddata_length is a uint32_t in 4 bytes = %d %d %d %d\r\n"), buffer1[0], buffer1[1], buffer1[2], buffer1[3]);

            i = 4;
            j = 1; // start with first sector
            while ( ((j-1)*512+i) < tmpb) {
                scanf_P(PSTR("%d"), &buffer1[i]);
                i++;
                i %= 512; //original code would have reset buffer1[0] in the first sector BEFORE writing the block
                if(!i) {
                    write512block(buffer1, j);
                    j++;
                }
            }
            write512block(buffer1, j);// original code would have truncated remaining samples

            printf_P(PSTR("mcu: (%d - 1)*512 + %d, data length = %d\r\n"), j, i, sounddata_length);

            //lastSample = buffer1[i - 1];
            //section = 1;  
            sei();
        }
        return 0;
    }

-Joel

November 30, 2010
by bretm
bretm's Avatar

That's not going to work. You don't call get512block until j reaches 512. Once that happens, j remains at 512 and doesn't get reset to 0 until get512block is done, so best case is that there will be a click. You need to update section and reset j to 0 before you call get512block so that the timer can immediately continue with the next buffer. That would also explain why sei() didn't help before. I didn't catch that.

That means you probably need to switch things around so that you're calling get512block with the opposite buffers and with "section - 1" instead of section since you will have already incremented the section variable.

Since section and j are now shared between the ISR and main, they need to be declared as "volatile" otherwise they may be optimized into constants in the ISR. That could explain why you hear no sound in this version.

November 30, 2010
by brockmjp
brockmjp's Avatar

More good ideas. I tried all your suggestions, except instead of section - 1, I just made section = 2 coming out of startPlayback, instead of 3 as it was before. I tried this with block management inside the ISR and alternatively in the main, calling sei() before the get512blocks either way. For clarity, I will include the startPlayback loop in the code below (version with block management in main). The clicking seems to be unaffected by these changes, but at least the sound plays when block management is done in the main loop. So the "volatile" declaration helped in that regard.

As a sanity check I tried again my program that plays the same sound samples, but from program memory instead of the SD card. That still sounds perfect, no clicking.

Is it really possible that the SPI data transfer cannot be interrupted? Or maybe only data reception on the mcu is being interrupted while the SD continues to send data resulting in some dropped bytes with every block read.

    // <snip>
    uint32_t sounddata_length = 0;
    uint8_t buffer1[512];
    uint8_t buffer2[512];
    volatile uint8_t lastSample;
    volatile uint32_t sample;
    volatile uint16_t section;
    volatile int j;
    // <snip>
    ISR(TIMER1_COMPA_vect) {
        if (sample >= sounddata_length) {
                stopPlayback();
        }else{
            if (j < 512) {
                if (section % 2 == 0) { // current section is even so play from buffer1
                    OCR2B = buffer1[j];
                }else{ // current section is odd, so play from buffer2
                    OCR2B = buffer2[j];
                }
                j++;
                sample++;
            }
        }
    }
    void startPlayback() {

        get512block(buffer1, 1);
        get512block(buffer2, 2);

        // pull out the 32-bit number from the first 4 bytes of the buffer1 array to get the number of samples
        sounddata_length = *((uint32_t *) buffer1);

        // OC2B is on PD3 (pin 5),  set to output high
        DDRD |= (1<<PD3);
        PORTD |= (1<<PD3);

        // Set up Timer 2 to do pulse width modulation on the speaker pin

        // Use internal clock (datasheet p.160)
        ASSR &= ~( (1<<EXCLK) | (1<<AS2) );

        // Set fast PWM mode  (p.157)
        TCCR2A |= (1<<WGM21) | (1<<WGM20);
        TCCR2B &= ~(1<<WGM22);

        // Do non-inverting PWM on pin OC2B (p.155)
        //// Would be possible to do on OC2A PB3 instead of OC2B PD3 with opposite settings (COM2B1=0, COM2B0=0, COM2A1=1, COM2A0=0)
        //// Clear OC2B on Compare Match, set OC2B at BOTTOM, (non-inverting mode)
        TCCR2A = (TCCR2A | (1<<COM2B1)) & ~(1<<COM2B0);
        TCCR2A &= ~( (1<<COM2A1) | (1<<COM2A0) );

        // No prescaler (p.158)
        TCCR2B = (TCCR2B & ~( (1<<CS22) | (1<<CS21) )) | (1<<CS20);

        // Set initial pulse width to the first sample.
        OCR2B = buffer1[4];

        // Set up Timer 1 to send a sample every interrupt.

        cli();

        // Set CTC mode (Clear Timer on Compare Match) (p.133)
        // Have to set OCR1A *after*, otherwise it gets reset to 0!
        TCCR1B = ( TCCR1B & ~(1<<WGM13) ) | (1<<WGM12);
        TCCR1A = TCCR1A & ~( (1<<WGM11) | (1<<WGM10) );

        // No prescaler (p.134)
        TCCR1B = (TCCR1B & ~( (1<<CS12) | (1<<CS11) )) | (1<<CS10);

        // Set the compare register (OCR1A).
        // OCR1A is a 16-bit register, so we have to
        // do this with interrupts disabled to be safe.
        OCR1A = F_CPU / SAMPLE_RATE; //  compare register is in cycles/sample   (cycles/sec  *  sec/sample)

        // Enable interrupt when TCNT1 == OCR1A (p.136)
        TIMSK1 |= (1<<OCIE1A);

        j = 4; //  buffer position 0-512
        sample = 1;
        section = 2;
        sei();
    }
    int main() {
        SPIinit();
        MMC_Init();

        // start up the serial port
        uart_init();
        FILE uart_stream = FDEV_SETUP_STREAM(uart_putchar, uart_getchar, _FDEV_SETUP_RW);
        stdin = stdout = &uart_stream;

        int i;
        uint32_t tmpb = 0;
        while (1){

            startPlayback();

            while (sample < sounddata_length) {
                if (j > 511) {
                    j = 0;
                    section++;
                    sei();
                    if (section % 2 == 0) {
                        get512block(buffer2, section);
                    }else{
                        get512block(buffer1, section);
                    }   
                }
            }

            while (uart_read() != 'l');
            cli();
            printf_P(PSTR("mcu: Received 'l'\r\n"));
            scanf_P(PSTR("%d"), &tmpb);
            printf_P(PSTR("mcu: Received %u tmpb\r\n"), tmpb);

            // The sound data length is a 32-bit number, but we can only store one byte in each element of the buffer1 array
            // The pointer will automatically split the the 32-bit number into 4 bytes and put them in the first 4 elements of the 8-bit buffer1 array
            uint32_t *ptr = (uint32_t *) buffer1;
            *((uint32_t *) ptr) = tmpb;

            printf_P(PSTR("mcu: sounddata_length is a uint32_t in 4 bytes = %d %d %d %d\r\n"), buffer1[0], buffer1[1], buffer1[2], buffer1[3]);

            i = 4;
            section = 1; // start with first sector
            while ( ((section-1)*512+i) < tmpb) {
                scanf_P(PSTR("%d"), &buffer1[i]);
                i++;
                i %= 512; //original code would have reset buffer1[0] in the first sector BEFORE writing the block
                if(!i) {
                    write512block(buffer1, section);
                    section++;
                }
            }
            write512block(buffer1, section);// original code would have truncated remaining samples

            printf_P(PSTR("mcu: (%d - 1)*512 + %d, data length = %d\r\n"), section, i, sounddata_length);

            sei();
        }
        return 0;
    }

-Joel

December 01, 2010
by bretm
bretm's Avatar

The buffer management seems right now. I don't see anything in get512block that would be preventing the timer interrupt from happening on schedule.

There's a race condition that might be causing problems, but it's hard to say. Once j reaches 512, you set it to 0 and increment section in the main block. But an interrupt could happen between those statements, which would cause the ISR to re-play the first sample from the same buffer. You should cli() before doing those two statements and sei() after. But this should only happen once in a while, not every block.

Technically, since j is more than one byte, you should theoretically protect reading from it the same way, but I don't think that's necessary in this case because by waiting for 512 you're really only waiting for the high byte to change. If the number wasn't a multiple of 256 it would be another race condition.

So if the clicking still happens after fixing that race condition, I don't know why it's still clicking. If it is SPI causing the problem, your program is about to get more complicated. You'll have to stop using the timer interrupt and instead check for timer overflow during your SD card reading to see if it's time to play another sample. It should be doable, but ugly and seemingly unnecessary.

Post a Reply

Please log in to post a reply.

Did you know that talking to the microcontroller over the USB/Serial link is easy under Windows, Linux, and OS X? Learn more...