What are "stubs"?
You know that funny little option you turned on when compiling newlib?
The one that went "--disable-newlib-supplied-syscalls"? Let me explain what that
is and just how powerfull this is to an embedded system.
In a real operating system, such as Linux and Windows (cygwin), newlib
expects that the operating system will provide the means to move data
(bytes) between itself and the hardware. Newlib merely provides an
abstraction of the operations, it doesn't implement the whole operation
from start to finish. For example, we want to write a string of data
to one of the uarts (serial ports) we could do something like:
void function (void)
{
FILE * fooFile;
fooFile = fopen("/com1/9600,n,8,1", "w");
fwrite("this is a line of text\r\n", fooFile);
fclose(fooFile);
}
While the absolute procedure of conditioning a serial port is not done
this way on either operating system, the example does serve to
show the idea of what we want to do. What will really happen is that
the newlib function calls of: fopen(), fwrite() and fclose() serve to
provide a pathway for the data string to reach the UART. Newlib does
not touch the hardware, as with all modern software systems,
"that is someone else's problem". Newlib merely keeps track of the
"channel" that fooFile represents. Newlib doesn't really know what
the ultimate hardware is, that is someone else's problem (the
operating system).
TopWhat are all those funny "_r.c" files about?
Without an operating system, we don't have some body of code
automagically moving bytes between newlib and the hardware. Normally,
this would be done by making System Calls (SysCall()). In our case
of an embedded system where it is running
our operating system
that we wrote, it is now up to us to move that data to the hardware.
The movement of data places certain requirements on us:
1. We need to prepare the hardware for use.
2. We need to be handed the data to pass to the hardware.
3. We need to identify the hardware that is to get the data.
Working in conjunction with newlib, through the stubs, we can accomplish this.
To open a resource (hardware), newlib calls the open_r() function. This is
a function which we will have to write, normally open_r() is a SysCall
to the operating system. Newlib hands us two important pieces of
information from the fopen() call we made: filename + mode. In
the above example: "/com1/9600,n,8,1" is the filename and "w" is
in the flags. The filename is the textual name that we used in the
fopen call, the mode of "w" has been digested (cooked) and
is a integer containing bits to indicate the mode.
These flags are the same bits as defined in the newlib include
of sys/fcntl.h .
TopA practical open_r()
Get the RDCF2 sources from elsewhere within this site and unpack them.
We will look at how a file would be opened on the SD drive. The
process is essentially identical to that of opening a hardware serial
port, to the point of entering the RDCF2 code itself.
Open_r() first scans the device table looking for a device name that
matches that of the desired filename. Note the '/' chars surrounding
the name "com1"? That delineates the name of the device. So, if
we have a device named "com1" in the device table, the entry point
of open() is called with parameters passed to it. This is the line
that says
return device_table_list [i].item->open( ptr, &file[len+2], flags, mode).
In the case of the MMC device (SD drive), open() is in the mmc_module.c
file. Look there now. You will now see process of opening a file via
the rdcf2 code. We keep track of file handles here in this code. It
is necessary for us, here within mmc_module.c, to assign a unique
identifier to this filestream. Newlib
does not assign filehandles,
we do (the operating system).
A filehandle is a means for the operating system to track a filestream
to hardware destination point. Since we can have multiple files open
on the SD drive, we use filehandles to keep track of them.
The mmc_module.c code also uses File Control Blocks for it's own
management of the files. Newlib never sees these FCBs. We never
see the internal FILE buffers of newlib. The filehandle is the logical
representation of the data stream to hardware connection.
TopA practical fwrite()
The process of an fputs() or fwrite() are the same: write_r() is called
by newlib with a filehandle (stream identifier), data to be written
(buffer) and a count (how many bytes in the buffer to write). Before we
can call the appropriate device write function, we have to see what
device would receive the data. Look at write_r.c now.
The write_r() function looks up the device using the filehandle value
as the device ID + buffer ID. If you recall, we told newlib to use
these values back in the open_r()? If a device of that number exists,
the write function is then called for that device. Again we look at
mmc_module.c to see how the write function proceeds. In the case of
the SD drive, the filehandle points to the buffer, and the buffer
names the file which will receive the data.
Hmmm, couldn't the buffer also tell us what serial port the data would
go to?! Yes! It can.. The filebuffer (or more correctly: File Control
Block) is what
we decide it should represent / contain. It is
important to note that newlib keeps it's own file buffers, but the
contents of those buffers are of no importance to us. Our FCBs are
what we concern ourselves with!
TopHow do I add a new device?
To add a new device, say one called "pong", or more correctly "/pong/".
Take the mmc_module.c as an example: strip out all but the
DEVICE_TABLE_ENTRY structure. Rename, if necessary, the calls within
the call table to more descriptive names to match your device name.
Then replace the name "mmc" with "pong". Next, edit devices.c and add
the reference to the "pong" device table. Finally, edit sysdefs.h and
create a new DEVICE_ID similar to the MMC_DEVICE.
From here, you will focus on the writing of the functions within your
device module code (renamed mmc_module.c file). Your serial port
device will be simpler than the MMC device. The MMC device has to keep
track of multiple connections to multiple files, hence the rather complex
FCB structure and filehandles. The serial port device merely needs to
return a unique filehandle so that write_r() and read_r() will properly
call your module code.
TopSummary
While this page does not attempt to intimately detail each and every step
of the process of how the MMC device was implemented, it does attempt to
outline the general process. The process is that newlib eventually calls
our stub functions (open_r(), write_r(), etc.) to touch the hardware.
We, in turn, take the process a step further in the stub function of
resolving which hardware module will receive the newlib call. And finally,
our module code takes the operation that newlib has requested and
causes it to occur on the desired hardware.