|
|
Old MP3 Player Design
|
Click For New Design |
This new design has replaced the old one.
The design shown on this page is obsolete.
The new design has more features, is flash upgradable,
is much easier to use, costs less, and is actually available if you'd
like to buy one! It's better than this old design is almost
every way. These old pages are still available, mostly for
reference.
Firmware Source Code, For Free...
You may download this code in a ZIP file.
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.
How It Works
The player just reads sectors, in order, from the drive and
sends them to the MAS3507D decoder chip.
The 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.
The Code
; 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
|