Theory of Operation
Communication Protocols
The theory of operation for I2C and SPI are documented in their datasheets and are not further elaborated upon here. The hardware implementations are based on the microcontrollers’ datasheets:
Display Modules
The lower-level control of the display modules is based on their datasheets:
File Streams
Note
In the current version, the stdin/stdout file streams are implemented distinctly from those for the discrete display modules.
This is consequent from the file streams for the display modules to be a recent introduction to the CowPi / CowPi_stdio libraries.
We anticipate harmonizing the stdin/stdout code with the display modules’ code in a future release.
Creating File Streams on AVR and ARM
While AVR-libc has fdev_open(put, get) that returns a FILE *, we choose to use fdev_setup_stream(FILE *, put, get, rwflag) instead.
Given the limited program memory available, we opt for statically-allocated FILE variables instead of bringing malloc() into the program’s footprint.
For stdin/stdout, the statically-allocated FILE variable for AVR targets is simply a scalar FILE.
For display modules, it is part of a richer data structure, described below.
For ARM targets we use newlib‘s funopen(cookie, readfn, writefn, seekfn, closefn) that returns a FILE *.
Both newlib and glibc define a similar fopencookie() function, but it doesn’t appear to be always available.
The AVR-libc put and get function pointers expect functions that operate on one character at a time.
The newlib/glibc readfn and writefn function pointers expect functions that can operate on an arbitrary number of characters.
Another notable difference is the cookie used by the newlib/glibc code.
A cookie “is an object … which records where to fetch or store the data read or written.
It is up to (the designer setting up a FILE stream) to define a data type to use for the cookie.
The stream functions in the library never refer directly to its contents, and they don’t even know what the type is;
they record its address with type void *.”
We define put and get functions that act as wrappers for writefn and readfn functions.
Because the writefn and readfn functions operate on an arbitrary number of characters, they can also operate on one character at a time.
For stdin/stdout, the cookie is the Arduino Serial object.
For display modules, it is a custom data structure, described below.
Buffered Display Modules
In the interest of minimizing the program’s footprint, non-scrolling displays do not use a buffer. We accept the possible brief loss of responsiveness in exchange for a smaller program – if you’re using this library for an application with hard real-time constraints, you may wish to reconsider.
Some display must use a buffer – specifically, scrolling text on a 7-segment buffer display, scrolling text on an LED matrix, and Morse Code. All such displays (if you have more than one in your circuit) share a single buffer. Microcontrollers with more memory will have a larger buffer, and microcontrollers with little memory will have a small buffer.
For buffered displays, the writefn function creates a symbol_t variable and adds it to the buffer.
If, and only if, the buffer is full, then the function blocks until there is room for the next symbol. The symbol_t structure has four fields:
callback– a function pointer to the function that will actually send the symbol to the display module; while its parameter is declared to bevoid *, the argument is assumed to besymbol_t *stream_data– a pointer to the cookiesymbol– the byte to be sent to the display modulesymbol_duration– ⅛ of the number of milliseconds before the next symbol should be sent to the display module – by scaling the duration, some timing precision is lost; however, we gain the ability to have up to 2 seconds between symbols without wasting most of a second byte
A timer handler removes the next symbol from the buffer and calls symbol->callback().
The callback function extracts the display module and communication protocol data from the cookie contained within the symbol_t argument and uses this to send the symbol itself to the display module.
For ATmega328P and ATmega2560 microcontrollers, we use Output Comparison B on Timer0, the same timer that the Arduino framework has already set to overflow every 1.024ms.
In doing so, we leave Timer1 and Timer2 available for application programs, and we do not make any changes that could affect Arduino’s millis() or delay() functions.
For the RP2040 microcontroller (and probably the nRF52840 microcontroller), we use the Mbed OS Ticker.attach() function to configure the timer interrupts.
Buffered displays are not yet implemented for other targets.
Special Functions
We expect that clearing the display, and placing it into and out of a low-power state, are the only functions that might drive an application programmer to try to use the display module’s lower-level functions (“only”, except for creating custom characters for HD44780 devices – perhaps we will remedy this in a future release). As these have the possibility of producing undesirable behavior from the file stream, particularly clearing the display, we provide functions to perform these actions in a manner that will keep the FILE stream in a predictable state.
These functions take a FILE * argument and determine which display
module should be acted upon.
This is accomplished by obtaining the appropriate cookie and comparing the put function pointer to the addresses of the writefn functions for each of the display modules.
On AVR microcontrollers, obtaining the cookie is again a simple matter of casting the FILE * argument to stream_data_t *.
For ARM microcontrollers, we iterate over the array of cookies (being a very small array, this happens quickly) and compare the FILE * argument to the FILE *stream field in each cookie to determine which is the correct cookie.