2013年1月17日

DiyNote: AVR SD播放器

得从大约两年前说起,那会儿amobbs的马潮老师出了个题目:8*8LED屏,中间四个常亮,最外圈亮一个转圈跑。这东西没啥难度吧?我正好有块点阵屏闲着,就焊了一块板,从开始写程序计算,39分钟解决了。 

讨论过程中一师弟(好象是TwoPerson?)大概出口不慎,把这位马潮老师惹怒了——于是他又出了个题,原文如下:

"如果不服,可以再次比试。你在北大找5个学生,组成一个小组。

m16加一片lm324,设计一个读取sd卡上wave文件,并播放的wave播放器,看谁做的好。

给你们一个月的时间,下个学期开学我到北大找你,比试实物效果。 "

首先声明一下,本人纯粹是外行,本科化学,研究生改物理了,以上纯属业余爱好。马老师这题目在我这个外行看来,也就是一个人两三天到一周左右的工作量,至于五个人搞一个月么?不过后来事情一多也就把这回事忘了。

前段时间突然记起来了,于是动手开干。电路图太简单就不画了。觉得没必要上m16,于是用了一片m8,硬件SPISD卡,8PWM输出,用一片TL061搭成低通滤波,后面一只8050缓冲接喇叭。从学习SD协议、实现FAT开始,焊接、写程序到调试全算上,用了两个晚上、周六全天加周日半天,基本完事。当然8PWM的音质有点惨不忍睹……

程序如下,太乱了,懒得整理了……

----------------------init.c-------------------------

//初始化部分,是用zx提供的appbuilder生成的,好象是从iar里扒出来的?

//基本没啥有用的,直接跳过好了

#include <avr/io.h>

#include <avr/interrupt.h>

void port_init(void) {

    PORTB = 0x0;

    DDRB  = 0xff;

    PORTC = 0xff; //m103 output only

    DDRC  = 0xff;

    PORTD = 0xff;

    DDRD  = 0xff;

}

void timer1_init(void) {

 

    TCCR1B = 0x00; //stop

    TCNT1H = 0xFF; //setup

    TCNT1L = 0x00;

    OCR1AH = 0x00;

    OCR1AL = 0xFF;

    OCR1BH = 0x00;

    OCR1BL = 0xFF;

    ICR1H  = 0x00;

    ICR1L  = 0xFF;

    TCCR1A = 0xC1;

    TCCR1B = 0x09; //start Timer

}

 

//SPI initialize

// clock rate: 132300hz

void spi_init(void) {

    DDRB &= ~(1<<4); // MISO as input

    SPCR = 0x53; //setup SPI

    SPSR = 0x00; //setup SPI

}

 

//UART0 initialize

// desired baud rate: 2400

// actual: baud rate:2400 (0.0%)

void uart0_init(void) {

    UCSRB = 0x00; //disable while setting baud rate

    UCSRA = 0x00;

    UCSRC = (1<<URSEL) | 0x06;

/*    UBRRL = 54; //set baud rate lo 19200

    UBRRH = 0x0; //set baud rate hi */

    UBRRL = 207;

    UBRRH = 0;

    UCSRB = 0x08;

}

 

//call this routine to initialize all peripherals

void init_devices(void) {

    //stop errant interrupts until set up

    cli(); //disable all interrupts

    port_init();

    timer1_init();

    spi_init();

    uart0_init();

 

    MCUCR = 0x00;

    GICR  = 0x00;

    TIMSK = 1<<TOIE1; //timer interrupt sources

    sei(); //re-enable interrupts

    //all peripherals are now initialized

}

 

-----------------------uart_spi_sd.c------------------------

//所有uartspi的底层操作放这里,sd的一些基本操作也一起

#include <avr/io.h>

#include <util/delay.h>

 

#define SET_SPI_CS do { PORTB |= _BV(2); } while(0)

#define CLR_SPI_CS do { PORTB &= ~_BV(2); } while(0)

 

 

uint8_t sd_command( uint8_t cmd, uint32_t arg, uint8_t crc );

 

void  uart_tx( uint8_t    data ) {

 

    while (!(UCSRA & _BV(UDRE) ));

    UDR = data;

}

 

void    uart_puts( char *str ) {

  

   while(*str) {

       uart_tx(*str);

       str++;

   }

}

 

void    uart_write( char *buf, uint16_t n ) {

 

    while( n ) {

        uart_tx( *buf );

        buf++;

        n--;

    }

}

 

uint8_t    spi_writebyte( uint8_t  data ) {

 

    SPDR = data;

    while( !(SPSR & (1<<SPIF) ) );

 

    return SPDR;

 

}

#define spi_readbyte() spi_writebyte(0xff)

 

//找了个现成的抄来了,有点傻,不过还算好用

uint8_t sd_idle_state(void) {

 

    uint8_t i, retry, r1;

 

    CLR_SPI_CS;

    _delay_ms(1);

    for( i=0; i<20; i++ )

        spi_writebyte(0xff);

 

 

    retry = 0;

    do {

        r1 = sd_command( 0, 0, 0x95 );

        retry++;

    } while( r1 != 1 && retry < 100);

    if( retry >= 100 )

        return 1;

    retry = 0;

    do {

        r1 = sd_command( 1, 0, 0x95 );

    } while( r1 && retry < 100 );

    if( retry >= 100 )

        return 2;

 

    r1 = sd_command( 59, 0, 0x95 );

    r1 = sd_command( 16, 512, 0x95 );

 

    _delay_ms(1);

    SPCR &= ~(0x3);

    SET_SPI_CS;

 

    return 0;

}

 

uint8_t sd_command( uint8_t cmd, uint32_t arg, uint8_t crc ) {

 

    uint8_t r1, retry = 0;

    SET_SPI_CS;

    spi_writebyte( 0xff );

    spi_writebyte( 0xff );

    spi_writebyte( 0xff );

    CLR_SPI_CS;

    spi_writebyte( cmd | 0x40 );

    spi_writebyte( 0xff & (arg >> 24) );

    spi_writebyte( 0xff & (arg >> 16) );

    spi_writebyte( 0xff & (arg >> 8) );

    spi_writebyte( 0xff & arg );

    spi_writebyte( crc );

 

    do {

        r1 = spi_writebyte(0xff);

        retry++;

    } while( r1==0xff && retry < 100 );

 

    return r1;

}

 

uint8_t sd_read_sector( uint32_t addr, uint8_t *p ) {

 

    uint8_t r1=0;

    uint16_t i;

    addr <<= 9;

    r1 = sd_command( 17, addr, 0x95 );     //读出RESPONSE    

//    { char buf[32]; sprintf( buf, "r1=%d", r1 ); uart_puts(buf); }

    while ( spi_writebyte(0xff)!= 0xfe); //直到读取到了数据的开始头0XFE,才继续

    for(i=0; i<512; i++)

    {

        *p = spi_readbyte();

        p++;

    }

    spi_readbyte();

    spi_readbyte();

    SET_SPI_CS;

    return(r1);

}

 

-------------------------main.c------------------

#include <avr/io.h>

#include <avr/wdt.h>

#include <avr/interrupt.h>

#include <string.h>

#include <stdio.h>

#include <stdlib.h>

//#include <avr/pgmspace.h>

#include <util/delay.h>

#include "init.h"

#include "uart_spi_sd.h"

static struct {

    uint16_t    addr_boot;

    uint16_t    addr_fat;

    uint16_t    addr_rootdir;

    uint16_t    addr_cluster;

    uint8_t     sectors_per_cluster;

    uint16_t    num_reserved_sectors;

    uint16_t    sectors_per_fat;

} g_sd; //自己凑和实现了个fat,下次不这样了,还是找现成的petit之类东西吧

 

volatile static uint8_t g_sector[512]; //还好m81k sram,可以挥霍,m48就没法这么用了

volatile static uint8_t g_buf1[32], g_buf2[32]; //双缓冲,不太好用,下次还是老实写个fifo

volatile static uint8_t g_currbuf = 1, g_updatebuf = 0;

 

struct  fileinfo {

    uint16_t    first_cluster;

    uint32_t    filesize;

};

 

void    abortmsg( char *msg, uint8_t data );

 

void    get_sd_info(void ) {

 

    uint8_t   r1;

 

    r1 = sd_idle_state();

    if ( r1 != 0 )

        abortmsg( "idle failed", r1 );

   

    memset( (void *)g_sector, '\0', 512 );

    r1 = sd_read_sector( 0, (void *)g_sector);    //MBR

    if( g_sector[510] != 0x55 || g_sector[511] != 0xaa )

        abortmsg( "read mbr failed", 0 );

 

    memcpy( &g_sd.addr_boot, (void *)&g_sector[454], 2 ); //取引导区

 

    r1 = sd_read_sector( g_sd.addr_boot, (void *)g_sector );

    if( g_sector[510] != 0x55 || g_sector[511] != 0xaa )

        abortmsg( "read boot g_sector failed", 0 );

 

    memcpy( &g_sd.sectors_per_cluster, (void *)&g_sector[0xd], 1 ); //SD卡的基本数据

    memcpy( &g_sd.num_reserved_sectors, (void *)&g_sector[0xe], 2 );

    memcpy( &g_sd.sectors_per_fat, (void *)&g_sector[0x16], 2 );

    g_sd.addr_fat = g_sd.addr_boot + g_sd.num_reserved_sectors; //文件分配表

    g_sd.addr_rootdir = g_sd.addr_boot + g_sd.num_reserved_sectors + 2*g_sd.sectors_per_fat; //根目录

    g_sd.addr_cluster = g_sd.addr_rootdir + 32 - 2 * g_sd.sectors_per_cluster;

 

}

 

int8_t    get_file_info( char *fname, struct fileinfo *p ) {

 

    int16_t i, j;

    uint8_t r1, type, status, *tt;

    char    *t, filename[12], *extname;

 

    //笨办法拆解文件名

    strcpy( filename, fname );

    t = filename + strlen(filename) - 1;

    while(*t != '.' && t>filename )

        t--;

    if ( t == filename )

        extname = NULL;

    else {

        *t = '\0';

        extname = t+1;

    }

 

    status = 0;

    for ( j = 0; j < 32; j++ ) {

        r1 = sd_read_sector( g_sd.addr_rootdir + j, (void *)g_sector);

 

        for( i = 0; i < 16; i++ ) {

            type = g_sector[i*32];

            if( type == 0 || type == 0xe5 || type == '.' ) // empty, deleted, currdir, parentdir

                continue;

            if( g_sector[i*32+0xb] & 0x10 ) // is subdir

                continue;

 

            //不是上面几种,说明是正常文件

            tt = &g_sector[i*32+8-1];

            while( *tt == ' ' ) {

                *tt = '\0';

                tt--;

            }

            tt = &g_sector[i*32+11-1];

            while( *tt == ' ' ) {

                *tt = '\0';

                tt--;

            }

 

            if( strncasecmp( filename, (char *)(&g_sector[i*32]), 8 ) == 0

                    && strncasecmp( extname, (char *)(&g_sector[i*32+8]), 3 ) == 0 ) {

 

                //目标文件的首簇号和字节数

                p->first_cluster = *(uint16_t *)(&g_sector[i*32+0x1a] );

                p->filesize = *(uint32_t *)(&g_sector[i*32+0x1c] );

 

                status = 1;

                break;

            }

            if( status == 1 )

                break;

        }

    }

    return status;

}

 

//timer1中断里把缓冲区数据送PWM输出

ISR( TIMER1_OVF_vect ) {

 

    static  uint8_t t = 0, n = 0;

 

    TCNT1H = 0xff;

    TCNT1L = 0x1;

 

    t++;

    if(t%2)

        return;

 

    if( g_currbuf == 1 )

        OCR1A = g_buf1[n%32];// >> 8) & 0xff) | ( (g_buf1[n&0xf] << 8) & 0xff00);

    else

        OCR1A = g_buf2[n%32];// >> 8) & 0xff) | ( (g_buf2[n&0xf] << 8) & 0xff00);

       

    n++;

    if( n%32 == 31 ) {

        g_currbuf = 3 - g_currbuf;

        g_updatebuf = 1;

    }

}

 

//喂缓冲区,双缓冲不好玩,下次还是改用fifo实现

void    feed_data( uint8_t  *pdata, uint16_t size ) {

 

    uint8_t n;

    do {

        n = 32;

        if ( size < n )

            n = size;

           

        while( g_updatebuf == 0 );

        if( g_currbuf == 2 ) {

            memcpy( (void *)g_buf1, pdata, n );

        }

        else {

            memcpy( (void *)g_buf2, pdata, n );

        }

        g_updatebuf = 0;

        size -= n;

        pdata += n;

    } while( size > 0 );

 

}

 

//先实现了个显示文本文件内容的函数,然后把输出文本的部分改成喂缓冲区了

void    view_file( char *filename ) {

 

    struct  fileinfo finfo;

    uint16_t    n_clusters, next_cluster;

    uint8_t i, ret;

 

    ret = get_file_info( filename, &finfo );

    if( ret == 0 )

        abortmsg( "Open file error!", 0 );

 

    n_clusters = finfo.filesize / ( g_sd.sectors_per_cluster * 512 ) + 1 ;

 

    char buf[32];

    //sprintf( buf, "n_clusters:%d\r\nfirstcluster:%d\r\n", n_clusters, finfo.first_cluster );

    uart_puts(buf);

    next_cluster = finfo.first_cluster;

 

    do {

 

        // read a cluster

        for( i = 0; i < g_sd.sectors_per_cluster; i++ ) {

            sd_read_sector( g_sd.addr_cluster + ((uint32_t)next_cluster) * g_sd.sectors_per_cluster + i, (void *)g_sector );

            if( finfo.filesize > 512 ) {

//                uart_write( (char *)g_sector, 512 );

                feed_data( (void *)g_sector, 512 );

                finfo.filesize -= 512;

            }

            else {

//                uart_write( (char *)g_sector, finfo.filesize );

                feed_data( (void *)g_sector, finfo.filesize );

                break;

            }

        }

 

        // get addr of next cluster

        sd_read_sector( g_sd.addr_fat + next_cluster / (512/2), (void *)g_sector );

        next_cluster = *( (uint16_t *)(&g_sector[ ( next_cluster % (512/2) ) * 2 ]) );

        n_clusters --;

 

    } while( n_clusters > 0 );

 

}

 

void    abortmsg( char *msg, uint8_t data ) {

 

    char buf[32];

    //sprintf( buf, "%s\t%d\n", msg, data );

    uart_puts( buf );

    while(1);

}

 

//取剩余可用内存块,傻了点,不过能用

uint16_t    get_avail_mem(void) {

    uint8_t *p;

    uint16_t    i;

    for( i = 1; i < 65534; i++ ) {

        p = malloc(i);

        if( !p ) //分配失败,说明可用内存就剩这么多了……

            break;

        free(p);

    }

    return i-1;

}

 

int main(void) {

 

    static  char buf[32];

    struct  fileinfo finfo;

 

    init_devices();

    _delay_ms(50);

    PORTB &= _BV(2);

    _delay_ms(50);

 

    get_sd_info();

{

    //一开始用16.9344M的晶振,发现不好用,16.9344M/44.1k=384,正好一个半PWM周期……手里没有11.2896M的晶振,只好用8M内部RC慢慢调整到接近11.2896M。用r-2r方式的话就不存在这个问题,直接上24M27M晶振然后把PWM频率调整到接近44.1kHz就行。

        uint8_t old_osccal, i;

        old_osccal = OSCCAL;

 

        // 242是用示波器试出来的,得慢慢调快,以免抽筋

        for( i = old_osccal; i <= 242; i++ ) {

            OSCCAL = i;

            _delay_ms(5);

        }

        // 得重新设定uart分频比例,不然串口输出全变乱码了

        UBRRL = 37;

        UBRRH = 1;

    }

/*

    { abortmsg( "OSCCAL:", OSCCAL ); }

    while(1);

  */ 

 

    //先取文件首簇号和文件大小看看

    memset( &finfo, 0, sizeof(finfo) );

    get_file_info( "se_m_26.wav", &finfo );

    //sprintf( buf, "%u\t%lu\r\n", finfo.first_cluster, finfo.filesize );

    uart_puts(buf );

 

    //播放

    uart_puts("-------------start-------------");

    sei();

    view_file( "face1.wav" );

//    cli();

    uart_puts("--------------end--------------");

 

    //sprintf( buf, "available mem:%d\r\n\r\n", get_avail_mem() );

    uart_puts( buf );

 

    while(1);

 

    return 0;

}

 

下一步的目标是改用r-2r ladder来实现16位输出。马老师的意见是用两个8pwm1:255分压来组成16位,不知道哪个方案更蛋疼一些?r-2r就是占用io多了点,m8一共23ioSD卡用4个,reset一个,txd一个,凑和够了。哪天想起来了再按这个方案试试~~