Table of Contents
- Compiling and degubbing code for Tiva
- Hardware Abstraction Layer
- Debugging, heap, and display
- Analog Digital Converter and Timers
- Playing Nokia Tunes
- Random Number Generator, Rendering Engine, and the Game
- A real-time operating system
Hardware Abstraction Layer
I'd like the game to be as portable as possible. As far as the game logic is concerned, the actual interaction with the hardware is immaterial. Ideally, we just need means to write a pixel to a screen, blink an LED or check the state of a push-button. It means that hiding the hardware details behind a generic interface is desirable. This interface can then be re-implemented for a different kind of board, and the whole thing can act as a cool tool for getting to know new hardware.
In this project, we will use one static library (libio.a
) to provide the
interface. This library will implement all the hardware independent functions as
well as the stubs for the driver (as weak symbols). Another library (libtm4c.a
)
will provide the real driver logic for Tiva and the strong symbols. This kind of
approach enables us to use the linker to easily produce the final binary for
other platforms in the future.
Initialization PLL and FPU
To initialize the hardware platform, the user calls IO_init()
. The stub for
this function is provided by libio.a
as follows:
1int32_t __IO_init()
2{
3 return -IO_ENOSYS;
4}
5
6WEAK_ALIAS(__IO_init, IO_init);
The actual implementation for Tiva in libtm4c.a
initializes PLL to provide
80MHz clock and turns on microDMA. It also sets the access permissions to the
FPU by setting the appropriate bits in the CPAC
register and resetting the
pipeline in assembly. We will likely need the floating point in the game, and
it comes handy when calculating UART transmission speed parameters.
1int32_t IO_init()
2{
3 TM4C_pll_init();
4 TM4C_dma_init();
5
6 // Enable the floating point coprocessor
7 CPAC_REG |= (0x0f << 20);
8 __asm__ volatile (
9 "dsb\r\n" // force memory writed before continuing
10 "isb\r\n" ); // reset the pipeline
11 return 0;
12}
Simple read/write interface and functions
We provide an IO device abstraction called IO_io
and implement four generic
functions for accessing it:
1int32_t IO_write(IO_io *io, const void *data, uint32_t length);
2int32_t IO_print(IO_io *io, const char *format, ...);
3int32_t IO_read(IO_io *io, void *data, uint32_t length);
4int32_t IO_scan(IO_io *io, uint8_t type, void *data, uint32_t param);
IO_read
and IO_write
push to and fetch bytes from the device. IO_print
writes a formated string to the device using the standard printf
semantics.
IO_scan
reads a word (a stream of characters surrounded by whitespaces) and
tries to convert it to the requested type.
Each subsystem needs to provide its initialization function to fill the IO_io
struct with the information required to perform the IO operations. For instance,
the following function initializes UART:
1int32_t IO_uart_init(IO_io *io, uint8_t module, uint16_t flags, uint32_t baud);
It needs to know which UART module to use, what the desired mode of operation is (non-blocking, asynchronous, DMA...) and what should be the speed of the link. This approach hides the hardware details from the user well and is very generic, see test-01-uart.c. For instance, you can write something like this:
1IO_init();
2IO_io uart0;
3IO_uart_init(&uart0, 0, 0, 115200);
4IO_print(&uart0, "Hello %s\r\n", "World");
Passing 0
as flags to the UART initialization routine creates a blocking
device that is required for IO_print
and IO_scan
to work.
Non-blocking and asynchronous IO
A blocking IO device will cause the IO functions to return only after they have
pushed or pulled all the data to or from the hardware. If, however, you
configure a non-blocking (IO_NONBLOCKING
) device, the functions will process
as many bytes as they can and return. They return -IO_WOULDBLOCK
if it is
not possible to handle any data.
The IO_ASYNC
flag makes the system notify the user about the device readiness
for reading or writing. These events are received and processed by a
user-defined call-back function:
1void uart_event(IO_io *io, uint16_t event)
2{
3 if(event & IO_EVENT_READ) {
4 }
5
6 if(event & IO_EVENT_WRITE) {
7 }
8}
9
10int main()
11{
12 IO_init();
13 IO_io uart0;
14 IO_uart_init(&uart0, 0, IO_NONBLOCKING|IO_ASYNC, 115200);
15 uart0.event = uart_event;
16 IO_event_enable(&uart0, IO_EVENT_READ|IO_EVENT_WRITE);
17 while(1) IO_wait_for_interrupt();
18}
See test-02-uart-async.c.
DMA
The DMA mode allows for transferring data between the peripheral and the main
memory in the background. It uses the memory bus when the CPU does not need it
for anything else. When in this mode, IO_read
and IO_write
only initiate a
background transfer. The next invocation will either block or return
-EWOULDBLOCK
, depending on other configuration flags, as long as the current
DMA operation is in progress. The memory buffer cannot be changed until the DMA
transfer is done. Passing IO_ASYNC
will generate completion events for DMA
operations. It enables us to implement a pretty neat UART echo app:
1#include <io/IO.h>
2#include <io/IO_uart.h>
3
4char buffer[30];
5
6void uart_event(IO_io *io, uint16_t event)
7{
8 if(event & IO_EVENT_DMA_READ)
9 IO_write(io, buffer, 30);
10
11 if(event & IO_EVENT_DMA_WRITE)
12 IO_read(io, buffer, 30);
13}
14
15int main()
16{
17 IO_init();
18 IO_io uart0;
19 IO_uart_init(&uart0, 0, IO_DMA|IO_ASYNC, 115200);
20 uart0.event = uart_event;
21 IO_read(&uart0, buffer, 30);
22 while(1) IO_wait_for_interrupt();
23}
See test-03-uart-dma.c.
The driver
There was nothing ultimately hard about writing the driver part. It all boils down to reading the data sheet and following the instruction contained therein. It took quite some time to put everything together into a coherent whole, though. See: TM4C_uart.c.
If you like this kind of content, you can subscribe to my newsletter, follow me on Twitter, or subscribe to my RSS channel.