|
|
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:
Format | Data Type | Use Cast | Description |
%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
- Working - A reasonable number of tests have been done and this
code appears to be working properly.
- Looking Good... - Working in initial tests, but not enough tests have
been done to know if it really works well.
- Untested - It's implemented and ought to work, but no tests
have been done yet to see if it's actually working
- Unimplemented - No code yet, but it's planned
- Missing - Not mentioned on this page, but ought to be :)
|