PJRC.COM Offline Archive, February 07, 2004
Visit this page on the live site

skip navigational linksPJRC
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 Firmware Library Calls Search PJRC

PJRC Store
Main Board, $150
LCD & Pushbuttons, $42
LCD/Backlight/PB, $77
IDE Cable, $9
Complete Parts List
MP3 Player
Main Page
Detailed Info
User Photo Gallery
Connecting The Board
Firmware Download
Side Projects
Technical Docs
Freq. Asked Questions
FAQ #2
News And Updates

Library Functions, C Programming

This is a list of some of the useful functions that you may need to use while hacking on the C code.

 

Memory Allocation For Strings, Structs and Other Small Stuff

When you need to allocate memory to store strings, structs, or other small items (less than 4000 bytes), simm_malloc is the function to use. You call it with the number of bytes needed, and it returns a "simm_id" data type. A simm_id is not a pointer, it is a number that represents the location within the SIMM that was allocated for you. There are addrX functions which will map these simm_id references into the processor's address space and return a pointer.

simm_id simm_malloc (unsigned int num_bytes)
Allocate num_bytes of space within the SIMM and return a simm_id reference to the allocated memory.
void simm_free (simm_id addr32)
Release the memory previously allocated by simm_malloc.
xdata void * addr5 (simm_id addr32)
xdata void * addr6 (simm_id addr32)
xdata void * addr7 (simm_id addr32)
Map allocated memory into the processor's address space and return a pointer to it. This function is almost always used with a type cast, such as (xdata char *)addr5(string_id). Each of these functions maps to into a particular section of the processor's address space. Any pointers previously obtained in that section are no longer valid. In other words, you can only work with three different allocated objects

 

Printing To The Serial Port (and LCD Display)

When you print characters, they are transmitted to both the 9 pin serial port and the 4 pin LCD port. To manipulate the LCD, you must print messages that follow the LCD Protocol. Normally, messages to the LCD begin with \[ and end with \], so that the LCD will "hear" only that message and ignore the text before and after it. You may print messages using printf, or a combination of lightweight print functions which run fast, consume little stack space and use less code to call.

void printf (code char *fmt, ...)
This printf has many, but not all of the features you know from your favorite C library. The format string must be a string contant (you can not build a format string in a buffer and pass it to printf). Here are the list of format conversions it recognizes:
FormatData TypeUse CastDescription
%s char * No ! A string with null termination. It may be in any memory type (xdata, code, idata, etc).
%29s char * No ! You can specify a minimum field width for your string.
%c char Yes Print a single ASCII character (or whatever's in that char).
%d int if > 16 bits A normal signed integer, from -32768 to 32767.
%ld long if < 32 bits A 32 bit signed integer, from -2147483648 to 2147483647.
%hd char Yes An 8 bit signed integer, from -128 to 127.
%8d int if > 16 bits All %d formats allow a minimum field width to be specified.
%u unsigned int if > 16 bits A 16 bit unsigned integer, from 0 to 65535.
%lu unsigned long if < 32 bits A 16 bit unsigned integer, from 0 to 4294967295.
%hu unsigned char Yes An 8 bit unsigned integer, from 0 to 255.
%13lu unsigned long if < 32 bits All %u formats also recognize minimum field width.
%x int / unsigned int if > 16 bits A 16 bit number printed hexidecimal.
%lx long / unsigned long if < 32 bits A 32 bit number printed hexidecimal.
%hx char / unsigned char Yes An 8 bit number printed hexidecimal.
SDCC has strange requirements for type casting variable arguements. Variables of char and unsigned char must be explicitly cast, even if the variable is already a char or unsigned char type. Pointers must not be cast (unless you're a SDCC guru). 16 bit arguements only require a cast if the variable is 32 bits, and 32 bit arguements only need a cast if the type is less than 32 bits. The main thing to remember is to always explicitly cast when using %c.
  Example: printf("Simple string constant (should use lightweigh print instead of printf)\r\n");
  Example: printf("String: %s (must not cast) Char %c (must cast)\r\n", my_str, (char)*my_str);
  Example: printf("Same number three times: %d %ld %hd\r\n", my_int, (long)my_int, (char)my_int);
  Example: printf("Same number: %u (cast optional) %c (cast reqd)\r\n", my_uchar, (unsigned char)my_uchar);
Yes, this last example is a bit crazy. SDCC automatically converts char and unsigned char to 16 bits when passing in a va_arg list, so no cast is needed to pass an unsigned char to the 16 bit %u. You do need to explicitly cast it to unsigned char (despite already being an unsigned char) in order to pass to %c, should you want to see it as the ASCII character (or binary byte) that it is.

void printfd (code char *fmt, ...)
This works exactly like printf, but it only prints if the firmware is running in "debug" mode.

void print (code char *str)
Lightwieght print a simple string constant. The pointer must be to code memory (generally only useful for a string constant in double quotes). For a simple string constant, this is much faster and uses less code and stack space than calling printf.
  Example: print("\\[This will appear on the LCD\\]\r\n");
void print_str (xdata char *str)
Lightwieght print a string constant. An extra space is printed after the string.
  Example: print_str((xdata char *)addr6(string_id));
void print_hex32 (unsigned long ul)
Lightwieght print a 32 bit number is hexidecimal. Eight digits are always printed (leading zeros if needed). A space is printed after the eigth character.
void print_hex16 (unsigned int ui)
Lightwieght print a 16 bit number is hexidecimal. Four digits are always printed (leading zeros if needed). A space is printed after the eigth character.
void print_uint8 (unsigned char c)
Lightwieght print a 8 bit unsigned number, from 0 to 255. Leading zeros are suppressed, and space is printed after the number.
void print_char (char c)
Lightwieght print a single character. No extra space is printed.
void print_crlf (void)
Lightwieght print a carriage return and line feed. Typical debug messages would use a variety of the above functions followed by this one to end the line nicely in a terminal emulator window.


Library Functions, Assembly Language Drivers

This section is somewhat old and in need of updates. Please use with caution... of course, hacking on the assembly code requires a lot of tedious caution anyway :)

Memory Allocation

All DRAM memory is managed in 4k blocks. The SIMM will provide between 1024 to 8192 of these blocks, depending on its size. Blocks are refered to with a block number (0 to 8191). After initialization, groups of 4k blocks may be allocated and freed. When a group of blocks is allocated, usually the number of the first block is used, and a function is called to access the blocks in sequence. The DRAM controller allows any 15 blocks to be mapped into the 8051's addressable memory, from 0x0000 to 0xEFFF.

init_memory_mgr
Initialize the memory manager. The SIMM size is detected and a message about the size is printed. Some small portion of the memory is reserved for a linked list of all blocks, which is used to track which blocks are free and which are in use. When the application requests a group of blocks, this reserved memory stores that list. This function must be called before using the other functions.
Status:
Working
Input:
none
Output:
Carry: clear=ok, set=no simm installed

malloc_blocks
Allocate 1 or more 4k blocks of memory. The number of the first block is returned. If more than one is allocated, use next_block to access the others. This function only allocates the blocks, they are not automatically mapped into the 8051's addressable memory.
Status:
Working
Input:
Acc = number of blocks to allocate
Output:
Carry: clear=ok, set=memory not available
R2/R3: Number of the first block

map_block
Map a 4k block of memory into the 8051's address space, so that it may be used.
Status:
Working
Input:
Acc = page to map, 0 to 14 (0=0000-0FFF, 1=1000-1FFF, 2=2000-2FFF, etc)
R2/R3: Number of the block to map
Output:
none

free_blocks
Return allocated blocks back to the free memory pool. For a group of blocks, the first one should be given, and all in the group will be freed. Freed blocks are not automatically unmapped.
Status:
Working
Input:
R2/R3: Number of first block to free
Output:
none

next_block
When working with groups of blocks (all alloced with a single call to malloc_blocks), this function allows you to traverse a the list of blocks. Generally, the first block is stored, and then to reach, for example, the third block, this function would be called twice.
Status:
Working
Input:
R2/R3: Number of the current block
Output:
R2/R3: Number of the next block
num_blocks_free
Report the number of free 4k blocks of memory.
Status:
Unimplemented
Input:
Output:

File/Directory Reading

First, you open a file, by specifying it's starting cluster. Sorry, not by filename, though maybe that will happen someday. At startup, you only know the starting cluster of the root directory, so you open it. Once you open, cache, read and parse a directory, you find starting cluster numbers of files and subdirectories, which you can also open. I'm planning to support 64 file descriptors. If you attempt to open a 65th file before closing one of the 64 that are open, you'll get an error code. To the low level code, there is no difference between files, the root directory and subdirectories.... you just call file open with the starting cluster and you get a file descriptor which you pass to the other functions.

Next, you request that some or all of the file be cached, by specifying a byte range to be cached, and of course the file descriptor from fopen. To actually get the data cached, you repetitively call the cache worker function, until it returns a status code that all the bytes you requested are in the cache (it may also return an out-of-memory error). The cache worker is the only function that causes the drive to spin... the application should have some strategy to close or uncache as much as possible, open new files, set up their cache requests, and then call the cache working function rapidly until memory gets low, then call the drive sleep function.

Once the data is cached, you must call the seek function to initialize the current read position in the file. It isn't initialized to zero by default (the default is an undefined state that prohibits reading). Once the current byte position is defined, you can read the file with one of two read functions. The first copies up to 255 bytes into a buffer you supply, the other always gives 4096 bytes as a block pointer. The second one is intended for playback, the first is intended for building code to parse directories, ID3 tags, playlists, etc. These read functions will only work for data that was cached... if you try to read something that isn't in the cache, you get an error code. The only function that causes the drive to spin is the cache worker function (which give the main program complete control over what gets cached and when the drive motor is on), the read function calls only read from memory. Both maintain a "current position", that is advanced forward as you read. You can set it with a "seek" call. The seek call will only take 0-based offset from the beginning, so it you want to read the last 128 bytes for the ID3 tag, you'd compute that position from the file length obtained from the directory info.

There's also a third read function (to be implemented any day now) which reads directories. It actually just calls the normal read function, so it will read any file, but it parses the data and returns the directory information in a nice way.

I'm planning an uncache function, though I'll probably release code without it. The uncache function will free a portion of a file from the memory. The file_close function (also not yet written, but planned for an intial release) will completely remove the cached data, and all FAT sectors that were only needed for that file. Update: FAT sectors will leak memory in the initial release... call to "free_fat_memory" to reclaim their memory.

Once this new file I/O is working, or partially working, I could use a lot of help with mid-level functions, like parsing the directories, ID3 tags, etc. There will also be lots of opportunities to work on the main program's details, like the strategy of what to cache. For example, I was thinking that the player would track if the user had pressed any buttons recently, and be in an "interactive mode" where it would cache the first 100k of several upcoming files with a good portion of the available memory, in anticipation that the user will skip forward (or maybe backward, or to another playlist, etc). In the absence of user activity, only the upcoming uninterrupted playback would be cached. Figuring out what to do for caching ID3 tags and playlists is also an interesting problem. Maybe the portions of the files get cached, or perhaps the program would read them while the drive is running and tranfering MP3 data, and maybe even close and free them before filling the cache? Should the calls to cache_file_work burn up all the available memory, or does some have to be left available for use while the drive isn't spinning? The caching strategy also needs to be able to anticipate when the cached data won't be able to maintain playback, so that it can start calling the cache_file_work function soon enough to avoid a stoppage of the sound, but not too soon to waste battery life.

For directories, the main program would open them just like a file, cause them to be cached just like a file, and then call to a function that would use the read function to fetch 32 byte chunks and parse them, returning useful file info to the main program. This parsing function is outside the scope of what I'm doing right now. The initial release of this new code will probably have something hacked together from the code of 0.5.1. To the low level routines, files and directories are the same, just a stream of bytes from cached clusters. Only the main program knows which are files and directories.

When the new low-level code is working, any code that parses the main directory should work the same way on subdirectories, the only difference being a different single-byte file descriptor.

I haven't made much in the way of detailed plans for the higher level code, other than converting to C, because there's a lot of people who want to work on this sort of thing but they can't use ASM code.

I was thinking about having a few "playlist modes", which sub-directories would be handled like playlists, perhaps the code that does the UI would call to a next_track function that would either read a playlist or a subdirectory, and a "next_playlist" that would move to the next subdirectory or next playlist file. I've had a lot of emails from a lot of people with a lot of ideas (some really good, others less well thought out). It's been my plan to build up these lower-level details and hide them all in "drivers.asm" , and convert the main application (player.asm) to C. Actually, a good portion of player.asm would stay as ASM in bank0 as callable functions from the C code. Unfortunately, much of this is still just a dream until these file access routines and other low and mid level functionality is working.

file_open_by_1st_cluster
Open a file or directory. You must provide the first cluster number of the file (see "dir_read" function). This open function doesn't actually do any disk access, and there is no checking that the cluster number is valid or really points to a file on the drive. All this function really does is allocate a slot in the table of files and returns the file descriptor to you, for using the other file access functions. This function (and all the others described here) doesn't "know" if you're opening a file or directory. Directories are handled just like files, as a stream of bytes, though there is a special function to read and parse directories. Up to 64 files/directories may be open at one time.
Status:
Working
Input:
@R0 = 32 bit cluster number
Output:
Acc = File descriptor
Carry: clear=file opened, set=too many files already open

file_cache
Prepare to cache some or all of a file. This function initializes the parameters that are used by "file_cache_work". It does not actually move data or allocate any memory, but the somewhat time consuming setup calculations are made and stored, so that you can be "ready to go" with several files open and prepared to cache before making the first file_cache_work call that spins up the drive.
Status:
Working
Input:
@R0 = first byte to cache (32 bit unsigned)
@R1 = number of bytes to cache (32 bit unsigned)
R4 = file descriptor
Output:
None

TODO: the return value from file_cache_work has changed... for now, chech the comments in the source code.

file_cache_work
Load data from the IDE drive into memory. This is the only function that causes the IDE drive activity, which gives the application complete control over when the drive will be active. This function must be called repetitively, until is returns indicating that all of the file has been cached, or that it has run out of memory. What this function actually does is allocate memory and issue requests to the IDE drive to full them with the required sectors. It never waits for a sector to be read... it always returns as quickly as possible with a status code telling you that it must be called again. When the return status finally shows complete, it still doesn't mean that all the data is really in memory. In only means that no more calls to this function should be made, because all the read requests necessary are pending in the IDE driver's request queue.
Status:
Looking Good
Input:
R4 = file descriptor
Output:
Carry: clear=caching completed, set=see Acc
Acc: zero=call again later to cache more, non-zero=out of memory

ide_sleep
Put the IDE drive into sleep mode. The drive will be shut down after all pending requests are completed.
Status:
Needs work... doesn't respect queue status yet, uses old IDE driver, serious conflicts can occur between the two drivers
Input:
None
Output:
None

file_seek
Seek to an absolute byte offset within a file. You MUST seek to a position in a file before attempting to read, even if you only want to begin reading at byte 0, you must use this function to intialize the current position in the file.
Status:
Looking Good
Input:
@R0 = byte position (32 bit unsigned)
R4 = file descriptor
Output:
Carry: clear=ok, set=location not available in the cached data

file_read
Read bytes from a cached file. This function does a relatively slow memory to memory copy, so it's best used for only small pieces of data, such as ID3 tags and playlists. The requested bytes of the file must be in the cache. This function doesn't "know" the filesize, and will be able to read slightly past the true end of the file (to the end of the last cluster). It is the call's responsibility to know the filesize (obtained from dir_read) and only read bytes which are part of the actual file.
Status:
Looking good (does it properly read across block 4k block and cluster boundries?)
Input:
R1 = number of bytes to read
R4 = file descriptor
R6/R7 = memory location to copy data into
Output:

file_read_block
Get the next 4k block from a cached file. No data is copied, the block number of the cached data is returned. The file's current read position is advanced to the beginning of the next block. This function is useful for obtaining the blocks of a MP3 file, to pass them to "play block" for playback. The file must be in the cache. This function doesn't "know" the filesize. It will always return 4k block numbers, even if that block has less than 4096 bytes of actual file data, or even if that block an unused part of the last cluster of the file. It is the caller's job to know the filesize and use only the portion of the returned block that actually contains bytes from the file.
Status:
Working
Input:
R4 = file descriptor
Output:
R2/R3 = block number

file_close
Close a file, freeing all memory used to cache it.
Status:
Looking good (does not free FAT sectors)
Input:
R4 = file descriptor
Output:
None

file_tell
Return the current byte offset within a file.
Status:
Unimplemented (not planned for initial release)
Input:
R4 = file descriptor
Output:
TBD

dir_read
Read the next filename and related info from a directory. Aside from the filename and attributes, this function returns the first cluster number (needed for opening the file) and the size of the file, which the application will often need because these low-level functions don't "know" the file size and may access beyond the end of the actual data (into the unused portion of the last cluster).
Status:
Looking good (still missing long filename support)
Input:
R4 = file descriptor
Output:
TBD

file_uncache
Remove a portion of a file from the cache.
Status:
Unimplemented (not planned for initial release)
Input:
TBD
Output:
None

free_fat_memory
Free all blocks that are caching FAT sectors. After files are cached, the FAT sectors that were used to find the positions of the clusters on the drive aren't needed anymore. An agressive strategy may be to call this function as soon as "file_cache_work" says it has run out of memory, to free up just a bit more memory, and then make more calls to file_cache_work until is says it's out of memory again, and then call ide_sleep. A more conservative strategy would be to call here after calling ide_sleep, when there's no chance that the cached FAT sectors will be needed anymore. This function is something of a kludge for file_close not (yet) being able to free the FAT sectors. Until file_close can do that, this function will be needed to avoid slowly leaking memory. For a defragmented FAT32 volume, very few blocks should be needed for FAT sectors compared to the clusters, but a very badly fragmented drive with a small 4k cluster size could, at least in theory, need as much memory for FAT sectors as the clusters. The FAT32 code has a limit of caching 2048 groups of 8 FAT sectors (16384 FAT sectors total), so it is theoretically possible (though highly unlikely) to run into this limit with a large SIMM and very large drive (somehow) formatted with a small cluster size and very bad fragmentation. If "file_cache_work" says it's run out of memory, but there really is memory available (see "num_blocks_free"), then this 2048 FAT sector groups limit has been reached, and maybe calling this function will help (at the expense of speed as FAT sectors are re-read, which is to be expected with so much fragmentation).
Status:
Untested as the FAT sectors never get free'd)
Input:
None
Output:
None

MP3 Playback

TODO: update this section... more functions have been added. For now, check the source code.

play_block
Play a block of MP3 data. The block is actually added to a queue of blocks waiting to be played. This queue allows the application to have a large amount of MP3 data "ready to go". The queue can hold approx 1/2 megabyte.
Status:
Looking Good
Input:
R2/R3 = block number with MP3 data
R4/R5 = number of bytes of data to use from this block
Output:
Carry: clear=ok, set=request queue is full, try again later

play_num_sent
Report the number of blocks that have been sent to the MP3 decoder, since the last time this function was called. This function is intended to allow the application to find out how many blocks have actually been sent to the decoder, so that it can generate user feedback, such as time elapsed or remaining in the currently playing file.
Status:
Untested
Input:
None
Output:
R4/R5 = number of blocks transfered

play_abort
Flush the playback queue, all blocks waiting to play are removed from the queue.
Status:
Unimplemented
Input:
None
Output:
None

TODO: add a little note about calling bank1 functions, and maybe some general description of this stuff... of course, that'll all be burried in some calling functions when the main program is converted to C, so this todo will turn into providing the ANSI C function prototypes that call functions to take care of those nasty little details.

Status Codes


MP3 Player, Detailed Specs and Information, Paul Stoffregen.
http://www.pjrc.com/tech/mp3/library.html
Last updated: November 28, 2003
Questions, Comments?? <paul@pjrc.com>