Bare-metal
Teensy
3.1
Libraries
(Last modified 20 July
2014)
Updated 20 July 2014
I've added support for a recursive-descent parser, which is a string
processing tool commonly used in interpreters and compilers. It's
a cool way to add integer math functions to your embedded
programs. It's lightweight, easily modifiable, and should work on
nearly any platform, not just Teensy.
I have received email from people having problems compiling my code
from my makefiles. In all cases, the problem has been caused by
not updating the VPATH variable in the project's makefile.
You need to edit the project's makefile (such as spitest.mak), locate
the line that sets the value for VPATH, and change the directory path
to match your installation. For example, if you have put
sysinit.c and crt0.s in a project folder different from the one I used,
edit the directory path assigned to VPATH to point to that
directory. If VPATH is not set correctly, your builds will not be
able to find these routines and your links will fail because make can't
build sysinit.o or crt0.o.
The Teensy 3.1 (Freescale KL20 MCU) is a lot of fun to play with, darn
cheap, and has a bunch of I/O on-board. Since writing bare-metal
libraries from scratch can be a pain, here is a set of basic I/O
libraries to help you get started. These are intended for
hobbyists who want to code in C without the Teensyduino or other
prebuilt IDE/library suite. Even if you do use Teensyduino or the
like, you might still like a peek under the hood..
Please note that all references to devices in this page use zero-based
numbering; the first UART is UART0. I don't use Teensyduino or
any other 'uino tools, so I don't know what their numbering convention
is. But the card that comes with the Teensy 3.1 refers to UARTs
1, 2, and 3, which the Freescale docs call UARTs 0, 1, and 2.
When in doubt, consult the schematic and Freescale header file
(mk20d7.h).
This code was edited using Visual Studio C++ 2008 Express Edition and
compiled with the CodeSourcery GCC toolset. You can find details
on setting up and using these tools here.
Functions available for each library are defined in an associated
header file (xxx.h), which resides in the /include folder in your
Teensy 3.1 development tree.
All library files discussed here are built with the above toolset and
written to the /library folder in your Teensy 3.1 development
tree. If you need to change any directories, refer to the
associated Makefile (xxx.mak) for the library.
You can find a link to a compressed collection of these files at the bottom of this page.
Please note that many of these source files are in a state of flux and
contain code that has been commented out or blocked with #if 0
clauses. This represents code I've expereimented with or have set
aside for later.
UART
The UART library code is in a source file named uart.c using Makefile
uart.mak. It compiles to a library named libuart.a.
UARTInit() initializes a UART. This function takes a UART
selector (0-2) and a baudrate. The baudrate is specified as a
uint32_t containing the actual baud; to select 152,000 baud, use a baud
rate argument of 152000. Upon exit, the selected UART will be
configured for the baud rate at 8 data bits, one stop bit, no
parity. Additonally, the UART just initialized automatically
becomes the active UART. This means later calls to UART I/O
routines, like UARTRead(), will use this UART until you change the
active UART.
UARTAssignActiveUART() allows your program to change the active
UART. This lets you quickly change the target of your serial
I/O. This function takes a UART selector (0-2). Upon exit,
it returns the selector of the previously active UART. Your code
can use this information to restore the active UART, if necessary.
UARTWrite() outputs a number of bytes to the active UART. Note
that this routine does NOT use a null-terminated string, it sends a
counted block of bytes. This lets you send binary data through
the active UART, should you choose. This function takes a pointer
to the char buffer and a count of the number of bytes to send.
Upon exit, this routine returns the number of characters sent.
UARTAvail() returns the number of characters waiting in the active
UART's recieve queue. Your code can use this function to poll the
active UART without having to lock, waiting for chars to arrive.
UARTRead() moves characters from the active UART's receive queue to a
buffer. This function takes a pointer to the destination char
buffer and a count of the number of bytes to transfer. This
function locks until the requested number of characters have been
transferred. Upon exit, this function returns the number of chars
transferred.
All UART handlers use polling on transmit and interrupt on
receive. You can adjust the size of the receive interrupt queue
by editing the uart.c file.
termio
The termio library code is in a source file named termio.c using
Makefile termio.mak. It compiles to a library named libtermio.a.
This library contains a set of character and string I/O routines.
The original source code came from Martin Thomas' website for one of
his STM projects. I've modified some of the code to support
multiple UARTs and added a string input routine that supports
background operation while waiting for the user to type a string on the
active UART..
In general, these routines are similar to the common character and
string I/O routines, such as puts() and printf(). However, these
routines do not need nor support the newlib functions (notably,
sbrk()). To avoid confusion, the names of these routines
generally have an "x" prefix, as in xputs().
xatoi() converts a character string to a 32-bit integer. It was
specifically coded to support the other termio routines, but may be of
use to other routines, so it is accessible in the library. This
function takes a pointer to a pointer to a character array, assumed to
hold a null-terminated string, and a pointer to a 32-bit variable where
the converted number will be stored. Upon exit, this routine
returns non-zero if conversion was successful, else it returns 0.
xitoa() converts a 32-bit integer into a null-terminated string.
Again, it was originally written to support the other termio routines,
but might be of use in other programs. This function takes a
32-bit value, a 32-bit radix, and a 32-bit maximum length.
However, the radix and length values may be negative; these conditions
act as formatting instructions. If you intend to use this xitoa()
function, check carefully through the source code so you understand how
to set up the arguments properly.
xputc() writes the char passed in its argument to the active UART.
xgetc() reads (with blocking) a character from the active UART and
returns that char to the caller.
xavail() returns the number of chars currently in the recieve queue of
the active UART.
xputs() writes the null-terminated string pointed to by its argument to
the active UART. This routine supports many of the standard C
escape sequence; refer to the source code for details.
xprintf() writes a formatted string to the active UART. This
function emulates much of the printf() functionality, but data
formatting is limited to integers only. Refer to the source code
for details on
supported formatting.
put_dump() writes a block of data to the active UART, formatted as a
traditional memory dump.
get_line() reads (with blocking) a line of input from the active UART
and writes those chars as a null-terminated string to the buffer passed
in as an argument. This function also takes a maximum length for
the input string. This function supports backspace for
line-editing.
get_line_r() reads a line of input from the active UART and writes
those chars as a null-terminated string to the buffer passed in as an
argument. This function also takes a maximum length for the input
string. Unlike get_line(), however, this function also takes as
an argument a pointer to an integer variable used to hold an index into
the holding buffer. This routine will copy any available chars
into the holding buffer, then return immediately to the caller.
If this function returns a 0, the user on the console has not yet
entered a CR to terminate the string. If this function returns a
1, the user has entered a CR; the string will be terminated with a null
but the CR will not appear in the string.
SPI
The SPI library code is in a source file named spi.c using
Makefile spi.mak. It compiles to a library named libspi.a.
The Teensy 3.1 uses the 64-pin variant of the KL20 device, which is the
same die used in the 100-pin variant. This means the Freescale
engineers had to drop 36 die signals when they created the 64-pin
device. Unfortunately, one of the signals dropped was SCK for
SPI1.
This means SPI0 is a full-function SPI channel, while SPI1 contains
only MISO and MOSI. This crippled SPI can still be used if your
target device isn't a true SPI device but only needs a serial stream.
My SPI library tries to compensate for the crippled SPI1 by supporting
a bit-banged SPI channel, SPI2. My SPI2 supports some of the
regular SPI features, such as bit fields of 4 through 16 bits, but does
not support variable SPI clock frequency. The SCK frequency for
SPI2 is going to be appriximately the core clock divided by 32; for a
64 MHz project, this works out to about 2 MHz. SPI2 also does not
support slave mode. Feel free to modify the source code to add
other features; if you do, please share!
SPIInit() configures the selected SPI channel (0-2) for master
mode. SCK frequency is determned by the value passed in argument
sckfreqkhz (in kHz). SPI transfer size is defined by the number
of bits (4-16) passed in argument numbits. The SPI channel will
be configured for CPHA=0, CPOL=0. SPI transfers will be done by
polling the SPI status register; interrupt on complete is not
supported. Upon exit, SPIInit() returns the SCK frequency; this
might be less than the requested value, due to system clock and
prescaler limits, but will not exceed the requested value.
Note that using SPIInit() to configure SPI2, a bit-banged SPI channel,
still requires a non-zero value for sckfreqkhz, but that value will be
ignored.
SPIExchange() sends a single value to the selected SPI channel and
returns a value from that channel.
SPISend() sends a single value to the selected SPI channel.
Note that for hardware SPI (SPI0 and SPI1), routines SPIExchange() and
SPISend() are essentially duplicates; you can simply call SPIExchange()
and ignore the returned value. However, SPI2 pays a speed penalty
in SPIExchange() as it bit-bangs in the response. If you need
maximum speed on SPI2 for send-only transfers, use SPISend().
PIT
The PIT library code is in a source file named pit.c using
Makefile pit.mak. It compiles to a library named libpit.a.
The Periodic Interrupt Timer (PIT) allows your code to generate precise
timing intervals, firing a selected interrupt on completion. The
KL20 supports four PITs, 0 through 3.
PITInit() configures a PIT channel (0-3) for interrupts based on either
a number of microseconds (passed in argument usecs) or in a number of
peripheral clock tics (passed in argument tics). You must pass a
non-zero value to only one of these two arguments; pass a 0 to the
unused argument. Argument priority assigns the interrupt priority
for the selected PIT; legal values are 0 (highest) to 15.
Argument isshandler assigns the address of a void function that expects
a single char argument. Upon exit, this routine returns the PIT
reload value. Note that the selected PIT will be running upon
exit; make sure your ISR code is prepared for an interrupt.
The function pointed to by argument isrhandler must be suppled in your
code. This is the ISR that will process a PIT interrupt.
When control enters your handler function, it will pass in a single
char argument, which is the PIT channel number (0-3) that caused the
interrupt. Your code can use this info to decide how to process
the interrupt. Note that your ISR does not need to clear or rearm
the PIT; that has already been done.
PITStop() disables the selected PIT.
PITStart() reenables the selected PIT. The first interval
following a call to PITStart() is guaranteed to be a full interval.
SD/SDHC
The SDcard library code is in a source file named sdcard.c using
Makefile sdcard.mak. It compiles to a library named libsdcard.a.
This code is the current version of a body of code constantly
undergoing changes and tweaks. It appears to be reliable but has
not been thoroughly tested and I would not be surprised if a bug or two
surfaces. I certainly wouldn't rely on it for a product; consider
yourself warned.
SDRegister() provides the connection between the SD card library and
your target device's SPI channel. Note that the SD card library
does absolutely NO manipulation of I/O lines! All SPI transfers
and all chip-select toggling must be done by code in your project.
This routine requires three arguments, each a pointer to a
function. Argument pselect points to a function that pulls the SD
card's chip-select line low. Argument pxchg points to a function
that writes an eight-bit value to the SD card via SPI and also returns
a value read from the card. Argument pdeselect points to a
function that pulls the SD card's chip-select line high. Upon
exit, this routine returns SDCARD_OK if registration was successful,
else it returns SDCARD_REGFAIL.
SDInit() initializes the SD card and records the card type in global
variable SDType. Upon exit, this routine returns SDCARD_OK if
successful, else it returns an appropriate error code.
SDReadBlock() reads 512 bytes of data from the address passed as
argument blocknum and saves that data to the buffer pointed to by
argument buff. Upon exit, this routine returns SDCARD_OK if
successful, else it returns an appropriate error code.
SDWriteBlock() writes 512 bytes of data from the buffer pointed to by
buff to the SD card at the address passed as argument blocknum.
Upon exit, this routine returns SDCARD_OK if successful, else it
returns an appropriate error code.
SDStatus() checks the current status of the SD card and the
registration of the SPI information. Upon exit, this routine
returns SDCARD_OK if successful, else it returns an appropriate error
code.
SDReadOCR() reads the SD card's 4-byte OCR data and writes it to the
buffer pointed to by argument buff. Upon exit, this routine
returns SDCARD_OK if successful, else it returns an appropriate error
code.
SDReadCSD() reads the SD card's 16-byte CSD data and writes it to the
buffer pointed to by argument buff. Upon exit, this routine
returns SDCARD_OK if successful, else it returns an appropriate error
code.
SDReadCID() reads the SD card's 16-byte CID data and writes it to the
buffer
pointed to by argument buff. Upon exit, this routine returns
SDCARD_OK if successful, else it
returns an appropriate error code.
SDWriteCSD() writes the 16 bytes of data in the buffer pointed to by
argument buff to the SD card's CSD data register. Upon exit, this
routine returns SDCARD_OK if successful, else it
returns an appropriate error code.
FAT file system
The FatFS library code is in a source file named ff.c using Makefile
ff.mak. It compiles to a library named libff.a.
This is a slightly customized version of ChaN's FAT32 file system; you
can find the original code here: http://elm-chan.org/fsw/ff/00index_e.html
I've talked at length about how much I've used this code and how much
of a service ChaN has done for the hobbyist community, so I won't
belabor the point here, other than to say it should be a part of every
embedded hobbyist's toolkit.
The FatFS library code uses a ffconfig.h file to customize ChaN's code
for use in the Teensy. Feel free to change the settings based on
ChaN's excellent comments, then rebuild the library.
Refer to ChaN's documentation and source files for full details on the
supported API calls.
Note that FatFS requires one routine for accessing device-specific
hardware. This routine is get_fattime(), used to generate a
timestamp for file writes. I don't yet have RTC code for the
Teensy, so I kludged together a bogus substitute, which always returns
the timestamp of 11:05:01 on 28 Jul 13. If you have an external
RTC or have enabled support for the KL20 RTC, please substitute your
code; and please share!
Recursive-descent parser
The recursive-descent parser (RDP) library code is in a source file
named rdp.c using
Makefile rdp.mak. It compiles to a library named librdp.a.
An RDP translates a string of characters into a numerical value using
assigned hierarchy of operations. For example, you can pass the
string "1 + 2 * 3" to an RDP and get back the integer value 7.
The code for this RDP handles 32-bit signed integers and includes basic
math operations (add, subtract, multiply, divide), modulus, and
exponentiation. It supports unary negationn, logical AND, OR, and
XOR, and order of operation by parentheses. It accepts numeric
literals in decimal, hexadecimal (0x), or binary (0b) radices.
Operators are based on the standard C operators, with the exception of
exponentiation:
Addition
+
Subtraction
-
Multiplication *
Division
/
Logical-AND
&
Logical-OR
|
Logical-XOR
^
Exponentiation **
Unary negation -
The library consists of one function call:
uint32_t
rdp(char *str, int32_t *answer);
The matching rdptest program shows how easy it is to add the parser to
your project. Here is a simplification of the rdptest program:
while (1)
{
answer = 0;
error = rdp(buff, &answer);
xprintf("Answer = %d 0x%08x\n\r",
answer, answer);
}
Note that the calling program should always reset the variable holding
the result to 0 before calling rdp(). In practice (and in the
rdptest program), you would check the error code before assuming the
answer is believeable.
This parser is a distillation of the code from my SBasic compiler of
years ago. The SB parser did much more than this simple version,
including converting the input string to a series of tokens reordered
for later execution by a run-time executive. I have left parts of
the original tokenizer code in rdp.c so you can see a bit of what is
involved in making a full tokenizer. You don't need it to run the
RDP routine, but perhaps you'll find a review interesting.
Test cases
I've built a very simple demonstration program for each of the above
libraries. These are not rigorous tests; they simply show
examples of how to call some of the routines. Each test program
contains the name of the tested library followed by the word
"test". For example, the SPI test program is spitest.c.
Summary
Here is a link to the collection of
library files and the corresponding test files. Note that this is
a large archive (about 20 MB) because it includes the full suite of
Freescale Kinetis header files.
I've built quite a few simple projects using the above libraries.
Between the elegance of the Teensy design and the wide range of library
support, even projects that normally take a boat-load of code go
together quickly. This all makes the Teensy a fun bare-metal
platform. I'm looking forward to more projects in the coming
months.
Please drop me an email if you find these libraries useful or (even
better) if you improve or add to them.
Home