PJRC.COM Offline Archive, February 07, 2004 Visit this page on the live site |
| ||
Shopping Cart Checkout Shipping Cost Download Website |
Home | MP3 Player | 8051 Tools | All Projects | PJRC Store | Site Map |
You are here: MP3 Player Technical Docs Old MP3 Player Design Firmware Source | Search PJRC |
This code is distributed in the hope that it will be useful, but without any warranty; without even the implied warranty of merchantability or fitness for a particular purpose.
lba
variable stores which sector will be
read next. This number is initialized to 256, where the MP3
files are expected to begin, and it's just incremented as
each sector is "played". When the last sector is
reached, well, the firmware isn't very smart and it just keeps
going. This needs to be fixed someday.
The process is basically to request a sector read from the drive, and let the drive work on fetching that sector to its internal buffer while the idle loop transfers the previously read sector to the decoder chip. After all of the available data is sent, the decoder chip has some buffering, which provides enough time to let the drive finish reading (if it didn't already) and copy the sector from the drive to memory. The sector that was transfered from the drive on this pass through the loop will be sent to the decoder on the next pass, immediately after the drive is instructed to begin fetching the next sector.
In a bit more detail, the main_loop check for button presses, and checks if the player is paused. When not paused, the loop calls to read_sector, which actually just tells the drive to read the sector into its buffer, but doesn't wait for the drive to actually have the sector read. Next, the idle loop is responsible for sending the 512 bytes currently in the buffer to the decoder chip. When all the bytes have been sent, the main_loop waits for the drive to have the sector available, and copies it from the drive's buffer to the 8051's external memory buffer. When the process is repeated, the next sector is requested, and the data that was just stored gets sent to the decoder chip.
When the pause button is pressed, the pause flag is toggled
and the main_loop will just avoid doing anything while paused.
The next/previous track buttons are more difficult. The player
does not keep track of which song is playing... it just copies
sectors in linear order. When either button is pressed, the
current_song routine compares the
current sector address to the list of all song starting addresses
(which are read into memory at start-up). A number of subroutines
are used only by current_song
, as it does its
search to figure out which song is currently being played.
Once the current song is known, the starting
sector number of the next or previous song is looked up and
the current sector number is updated. Some dummy bits are
sent to the decoder chip, to avoid a click sound. When
main_loop
runs again, it will just skip to the
next or previous song, because the lba
variable
with the sector number was updated.
; First attempt at a stand-alone MP3 player. ; see http://www.pjrc.com/tech/mp3/ for more info. ; This code is an original work by Paul Stoffregen, written ; in December 1999. This code has been placed in the ; public domain. You may use it without any restrictions. ; You may include it in your own projects, even commercial ; (for profit) products. ; This code is distributed in the hope that they will be useful, ; but without any warranty; without even the implied warranty of ; merchantability or fitness for a particular purpose. ;.equ location, 0x2000 ;where this program will exist .equ location, 0x8000 ;where this program will exist .equ sect_buf, 0x2E00 ;512 byte buffer, currently playing sector .equ list_buf, 0x3000 ;4096 byte buffer, list of song addresses ;------------------------------------------------------------------ ; Hardware Configuration ;8255 chip. Change these to specify where the 8255 is addressed, ;and which of the 8255's ports are connected to which ide signals. ;The first three control which 8255 ports have the control signals, ;upper and lower data bytes. The last two are mode setting for the ;8255 to configure its ports, which must correspond to the way that ;the first three lines define which ports are connected. .equ ide_8255_lsb, 0x4000 ;lower 8 bits .equ ide_8255_msb, 0x4001 ;upper 8 bits .equ ide_8255_ctl, 0x4002 ;control lines .equ cfg_8255, 0x4003 .equ rd_ide_8255, 10010010b ;ide_8255_ctl out, ide_8255_lsb/msb input .equ wr_ide_8255, 10000000b ;all three ports output ;ide control lines for use with ide_8255_ctl. Change these 8 ;constants to reflect where each signal of the 8255 each of the ;ide control signals is connected. All the control signals must ;be on the same port, but these 8 lines let you connect them to ;whichever pins on that port. .equ ide_a0_line, 0x01 ;direct from 8255 to ide interface .equ ide_a1_line, 0x02 ;direct from 8255 to ide interface .equ ide_a2_line, 0x04 ;direct from 8255 to ide interface .equ ide_cs0_line, 0x08 ;inverter between 8255 and ide interface .equ ide_cs1_line, 0x10 ;inverter between 8255 and ide interface .equ ide_wr_line, 0x20 ;inverter between 8255 and ide interface .equ ide_rd_line, 0x40 ;inverter between 8255 and ide interface .equ ide_rst_line, 0x80 ;inverter between 8255 and ide interface ;------------------------------------------------------------------ ; More symbolic constants... these should not be changed, unless of ; course the IDE drive interface changes, perhaps when drives get ; to 128G and the PC industry will do yet another kludge. ;some symbolic constants for the ide registers, which makes the ;code more readable than always specifying the address pins .equ ide_data, ide_cs0_line .equ ide_err, ide_cs0_line + ide_a0_line .equ ide_sec_cnt, ide_cs0_line + ide_a1_line .equ ide_sector, ide_cs0_line + ide_a1_line + ide_a0_line .equ ide_cyl_lsb, ide_cs0_line + ide_a2_line .equ ide_cyl_msb, ide_cs0_line + ide_a2_line + ide_a0_line .equ ide_head, ide_cs0_line + ide_a2_line + ide_a1_line .equ ide_command, ide_cs0_line + ide_a2_line + ide_a1_line + ide_a0_line .equ ide_status, ide_cs0_line + ide_a2_line + ide_a1_line + ide_a0_line .equ ide_control, ide_cs1_line + ide_a2_line + ide_a1_line .equ ide_astatus, ide_cs1_line + ide_a2_line + ide_a1_line + ide_a0_line ;IDE Command Constants. These should never change. .equ ide_cmd_recal, 0x10 .equ ide_cmd_read, 0x20 .equ ide_cmd_write, 0x30 .equ ide_cmd_init, 0x91 .equ ide_cmd_id, 0xEC .equ ide_cmd_spindown, 0xE0 .equ ide_cmd_spinup, 0xE1 ;------------------------------------------------------------------ ;internal ram usage .equ lba, 0x10 ;4 bytes, 28 bit Logical Block Address .equ song, 0x14 ;2 bytes, index of song we're playing .equ blk, 0x16 ;4 bytes, 32 bit block number (temp usage) .equ b0_state, 0x1A ;1 byte, nonzero if button 0 is down .equ b1_state, 0x1B ;1 byte, nonzero if button 1 is down .equ b2_state, 0x1C ;1 byte, nonzero if button 2 is down .equ paused, 0x1D ;1 byte, nonzero if we're paused .equ stack, 0x40 ;------------------------------------------------------------------ ; Main Program, a simple menu driven interface. .org location .db 0xA5,0xE5,0xE0,0xA5 ;signiture bytes ;.db 35,255,0,0 ;id .db 249,255,0,0 ;id .db 0,0,0,0 ;reserved .db 0,0,0,0 ;reserved .db 0,0,0,0 ;reserved .db 0,0,0,0 ;reserved .db 0,0,0,0 ;user defined .db 255,255,255,255 ;length and checksum (255=unused) .db "Simple MP3 Player",0 .org location+64 ;executable code begins here mov sp, #stack mov r1, #0 ;wait for any serial data to leave djnz r1, * djnz r1, * djnz r1, * djnz r1, * ;hope that's long enough clr p1.7 ;bring the MAS3507 out of reset clr p3.4 ;switch the TXD pin to the MAS3507 mov scon, #00011100b ;config uart as shift register djnz r1, * setb ti orl tmod, #00000001b ;config timer0 as 16 bit anl tmod, #11110001b setb tr0 setb tf0 clr a mov b0_state, a mov b1_state, a mov b2_state, a mov paused, a ;initialize the drive. If there is no drive, this may hang acall ide_hard_reset acall ide_init mov a, #ide_sec_cnt mov r2, #1 acall ide_wr mov r7, #0 ;read the list of song addresses clr a mov lba+0, a mov lba+1, a mov lba+2, a mov lba+3, a acall read_sector acall inc_lba mov dptr, #list_buf + 0x000 acall read_data acall read_sector acall inc_lba mov dptr, #list_buf + 0x200 acall read_data acall read_sector acall inc_lba mov dptr, #list_buf + 0x400 acall read_data acall read_sector acall inc_lba mov dptr, #list_buf + 0x600 acall read_data acall read_sector acall inc_lba mov dptr, #list_buf + 0x800 acall read_data acall read_sector acall inc_lba mov dptr, #list_buf + 0xA00 acall read_data acall read_sector acall inc_lba mov dptr, #list_buf + 0xC00 acall read_data acall read_sector acall inc_lba mov dptr, #list_buf + 0xE00 acall read_data ;start at address of the first song clr a mov song+0, a mov song+1, a acall play_song ajmp main_loop button_prev: mov a, b0_state jnz any_button mov a, paused jnz any_button acall current_song acall dec_song acall send_dummy_bits acall play_song mov b0_state, #255 ajmp any_button button_pause: mov a, b1_state jnz any_button mov a, paused cpl a mov paused, a mov b1_state, #255 ajmp any_button button_next: mov a, b2_state jnz any_button mov a, paused jnz any_button acall current_song acall inc_song acall send_dummy_bits acall play_song mov b2_state, #255 ajmp any_button any_button: mov tl0, #0 mov th0, #120 clr tf0 ajmp main2 main_loop: jnb tf0, main2 ;don't check buttons if recent activity clr a jnb p1.0, button_prev mov b0_state, a jnb p1.1, button_pause mov b1_state, a jnb p1.2, button_next mov b2_state, a main2: mov a, paused jnz main_loop acall read_sector ;tell the drive to read a sector acall inc_lba wait_buf: acall idle mov a, r7 jnz wait_buf ;wait for buffer to be available mov dptr, #sect_buf acall read_data ;transfer the sector to buffer1 mov r7, #sect_buf >> 8 mov r6, #0 sjmp main_loop inc_lba: mov a, #1 add a, lba+0 mov lba+0, a clr a addc a, lba+1 mov lba+1, a clr a addc a, lba+2 mov lba+2, a clr a addc a, lba+3 mov lba+3, a ret msg_1: .db "Playing MP3 Data",13,10,13,10,0 ;at 14.7 MHz crystal seems to be fast enough to play ;MP3's at 192 kbps, but it can't keep up with 11 MHz, ;so it's unlikely we can do 256 or 320 kbps! ;this idle loop sends the data to the MP3 chip idle: ;push dpl ;push dph mov a, r7 jz idle_ret mov dph, a mov dpl, r6 idle_loop: jnb p1.6, idle_end ;2 wait for the chip to want data clr a ;1 movc a, @a+dptr ;2 jz idle2 ;2 acall flip_bits ;0 / 6 idle2: mov sbuf, a ;1 inc dptr ;2 mov a, dpl ;1 jnz idle_loop ;2 mov a, dph jb acc.0, idle_loop mov dph, #0 idle_end: mov r6, dpl mov r7, dph idle_ret: ;pop dph ;pop dpl ret ;send 2048 dummy (zero) bits to the MP3 chip. This is useful ;when changing songs of moving to a new location in the data, ;so that the chip will play silence, lose sync, and then ;re-sync to the new data.... instead of using some of the new ;data as spectral energy from the old sync'd stream. send_dummy_bits: mov r2, #0 sdummy: jnb p1.6, sdummy ;2 wait for chip to want data clr a ;1 mov sbuf, a ;1 nop ;1 nop ;1 nop ;1 nop ;1 nop ;1 nop ;1 djnz r2, sdummy ret flip_bits: movc a, @a+pc flip2: ret .db 128,64,192,32,160,96,224,16,144,80,208,48,176,112,240,8,136 .db 72,200,40,168,104,232,24,152,88,216,56,184,120,248,4,132,68 .db 196,36,164,100,228,20,148,84,212,52,180,116,244,12,140,76,204 .db 44,172,108,236,28,156,92,220,60,188,124,252,2,130,66,194,34 .db 162,98,226,18,146,82,210,50,178,114,242,10,138,74,202,42,170 .db 106,234,26,154,90,218,58,186,122,250,6,134,70,198,38,166,102 .db 230,22,150,86,214,54,182,118,246,14,142,78,206,46,174,110,238 .db 30,158,94,222,62,190,126,254,1,129,65,193,33,161,97,225,17 .db 145,81,209,49,177,113,241,9,137,73,201,41,169,105,233,25,153 .db 89,217,57,185,121,249,5,133,69,197,37,165,101,229,21,149,85 .db 213,53,181,117,245,13,141,77,205,45,173,109,237,29,157,93,221 .db 61,189,125,253,3,131,67,195,35,163,99,227,19,147,83,211,51 .db 179,115,243,11,139,75,203,43,171,107,235,27,155,91,219,59,187 .db 123,251,7,135,71,199,39,167,103,231,23,151,87,215,55,183,119 .db 247,15,143,79,207,47,175,111,239,31,159,95,223,63,191,127,255 ;update "song" to reflect which song we're currently playing ;just do a (slow) linear search of all the known addresses current_song: clr a mov song, a mov song+1, a mov dptr, #list_buf cs_loop: mov r1, #blk acall get_4_bytes ;fetch the next song's address mov r1, #blk acall iszero_4_bytes ;if zero, we hit the end of list jz cs_done ;so assume we're on the last song mov r0, #blk mov r1, #lba acall cmp_4_bytes ;if we're playing a block after the jc cs_next ;beginning of this song, keeping mov r0, #blk ;looking mov r1, #lba acall eq_4_bytes ;if we're playing the first block jz cs_ret ;of this song, then we're done sjmp cs_done ;... or else it's the previous song cs_next: acall inc_song cjne a, #4, cs_loop cs_done:acall dec_song cs_ret: ret ; load "lba" with the block address of "song" play_song: clr c mov a, song rlc a mov dpl, a mov a, song+1 anl a, #3 rlc a mov dph, a clr c mov a, dpl rlc a mov dpl, a mov a, dph rlc a add a, #list_buf >> 8 mov dph, a mov r1, #lba acall get_4_bytes mov r1, #lba acall iszero_4_bytes jz ps_zero ret ps_zero: clr a mov song+0, a mov song+1, a sjmp play_song inc_song: mov a, song add a, #1 mov song, a clr a addc a, song+1 mov song+1, a ret dec_song: clr c mov a, song subb a, #1 mov song, a mov a, song+1 subb a, #0 mov song+1, a ret ;compare (subtract) @r0 and @r1, carry bit is returned ; carry clr: @r0 >= @r1 ; carry set: @r0 < @r1 cmp_4_bytes: clr c mov a, @r0 subb a, @r1 inc r0 inc r1 mov a, @r0 subb a, @r1 inc r0 inc r1 mov a, @r0 subb a, @r1 inc r0 inc r1 mov a, @r0 subb a, @r1 ret eq_4_bytes: clr c mov a, @r0 subb a, @r1 jnz eq4n inc r0 inc r1 clr c mov a, @r0 subb a, @r1 jnz eq4n inc r0 inc r1 clr c mov a, @r0 subb a, @r1 jnz eq4n inc r0 inc r1 clr c mov a, @r0 subb a, @r1 eq4n: ret ;read 4 bytes from external memory (@dptr) into ;internal memory (@r1) get_4_bytes: clr a movc a, @a+dptr mov @r1, a inc dptr inc r1 clr a movc a, @a+dptr mov @r1, a inc dptr inc r1 clr a movc a, @a+dptr mov @r1, a inc dptr inc r1 clr a movc a, @a+dptr mov @r1, a inc dptr ret iszero_4_bytes: mov a, @r1 jnz isz4e inc r1 mov a, @r1 jnz isz4e inc r1 mov a, @r1 jnz isz4e inc r1 mov a, @r1 isz4e: ret ;------------------------------------------------------------------ ; Routines that talk with the IDE drive, these should be called by ; the main program. ;read a sector, specified by the 4 bytes in "cylinder", ;"head" and "sector". ;Return, acc is zero on success, non-zero for an error read_sector: acall wr_lba mov a, #ide_command mov r2, #ide_cmd_read acall ide_wr acall ide_drq jb acc.0, rs_err clr a ret rs_err: mov a, #ide_err acall ide_rd mov a, r2 jz rs_err2 ret rs_err2:mov a, #255 ret ;initialize the ide drive ide_init: ;acall ide_hard_reset ;usually not necessary mov a, #ide_head mov r2, #10100000b acall ide_wr ;select the master device mov a, #ide_status acall ide_rd mov a, r2 ;should probably check for a timeout here jnb acc.6, ide_init ;wait for RDY bit to be set jb acc.7, ide_init ;wait for BSY bit to be clear mov a, #ide_head mov r2, #0xAF acall ide_wr ;what should this config parm be? mov a, #ide_sec_cnt mov r2, #64 acall ide_wr ;what should this config parm be? mov a, #ide_command mov r2, #ide_cmd_init acall ide_wr ;do init parameters command acall ide_busy mov a, #ide_command mov r2, #ide_cmd_recal ;do recal command (is this necessary?) acall ide_wr acall ide_busy ret ;------------------------------------------------------------------ ; Not quite as low, low level I/O. These routines talk to the drive, ; using the low level I/O. Normally a main program should not call ; directly to these. ;Read a block of 512 bytes (one sector) from the drive ;and store it in memory @ DPTR read_data: mov r0, #0 mov r1, #2 mov r2, dph mov p2, dph mov dptr, #cfg_8255 mov a, #rd_ide_8255 movx @dptr, a ;config 8255 chip, read mode rdataloop: mov dptr, #ide_8255_ctl ;mov a, #ide_data ;movx @dptr, a ;drive address onto control lines mov a, #ide_data | ide_rd_line movx @dptr, a ;assert read pin mov dptr, #ide_8255_lsb clr a movc a, @a+dptr ;read the lower byte movx @r0, a inc r0 mov dptr, #ide_8255_msb clr a movc a, @a+dptr ;read the upper byte movx @r0, a inc r0 mov dptr, #ide_8255_ctl clr a movx @dptr, a ;deassert all control pins mov a, r0 jnz rdataloop mov a, r2 inc a mov p2, a djnz r1, rdataloop mov p2, #255 ret ;write the logical block address to the drive's registers wr_lba: mov a, lba+3 anl a, #0x0F orl a, #0xE0 mov r2, a mov a, #ide_head acall ide_wr mov a, #ide_cyl_msb mov r2, lba+2 acall ide_wr mov a, #ide_cyl_lsb mov r2, lba+1 acall ide_wr mov a, #ide_sector mov r2, lba+0 acall ide_wr ;mov a, #ide_sec_cnt ;mov r2, #1 ;acall ide_wr ret ;Wait for the ide drive to not be busy. ;Returns the drive's status in Acc ide_busy: mov a, #ide_status ;wait for RDY bit to be set acall ide_rd mov a, r2 ;should probably check for a timeout here jb acc.7, ide_busy ret ;Wait for the drive to be ready to transfer data. ;Returns the drive's status in Acc ide_drq: mov a, #ide_status ;wait for DRQ bit to be set acall ide_rd acall idle mov a, r2 ;should probably check for a timeout here jb acc.7, ide_drq ;wait for BSY to be clear jnb acc.3, ide_drq ;wait for DRQ to be set ret ;------------------------------------------------------------------ ; Low Level I/O to the drive. These are the routines that talk ; directly to the drive, via the 8255 chip. Normally a main ; program would not call to these. ;Do a read bus cycle to the drive, using the 8255. This ;is slow, because we have to manipulate the 8255 and use ;the 8051's limited moxv and movx to do it (via dptr). ;Note that the drive is read using MOVC, to run on a board ;where the 8255 is read using PSEN. If your board uses ;RD instead of PSEN, chance the MOVC's to MOVX's. ;input acc = ide regsiter address ;output r2 = lower byte read from ide drive ;output r3 = upper byte read from ide drive ;dptr is changed ide_rd: push acc mov dptr, #cfg_8255 mov a, #rd_ide_8255 movx @dptr, a ;config 8255 chip, read mode mov dptr, #ide_8255_ctl pop acc movx @dptr, a ;drive address onto control lines orl a, #ide_rd_line movx @dptr, a ;assert read pin mov dptr, #ide_8255_msb ;clr a ;movc a, @a+dptr ;read the upper byte ;mov r3, a mov dptr, #ide_8255_lsb clr a movc a, @a+dptr ;read the lower byte mov r2, a mov dptr, #ide_8255_ctl clr a movx @dptr, a ;deassert all control pins ret ;Do a write bus cycle to the drive, via the 8255 ;input acc = ide register address ;input r2 = lsb to write ;input r3 = msb to write ;dptr is changed ide_wr: ;push acc mov r4, a mov dptr, #cfg_8255 mov a, #wr_ide_8255 movx @dptr, a ;config 8255 chip, write mode mov dptr, #ide_8255_lsb mov a, r2 movx @dptr, a ;drive lower lines with lsb (r2) ;mov dptr, #ide_8255_msb ;mov a, r3 ;movx @dptr, a ;drive upper lines with msb (r3) mov dptr, #ide_8255_ctl ;pop acc mov a, r4 movx @dptr, a ;drive address onto control lines orl a, #ide_wr_line movx @dptr, a ;assert write pin ;nop clr a movx @dptr, a ;deassert all control pins ;mov dptr, #cfg_8255 ;mov a, #rd_ide_8255 ;movx @dptr, a ;config 8255 chip, read mode ret ;do a hard reset on the drive, by pulsing its reset pin. ;this should usually be followed with a call to "ide_init". ide_hard_reset: mov dptr, #cfg_8255 mov a, #wr_ide_8255 movx @dptr, a ;config 8255 chip, write mode mov dptr, #ide_8255_ctl mov a, #ide_rst_line movx @dptr, a ;hard reset the disk drive mov r2, #250 djnz r2, * ;delay ^gt; 25 us (reset pulse width) clr a movx @dptr, a ;no ide control lines asserted ret