A Scheme Interpreter for ARM Microcontrollers: Language (050)
Overview:
The Armpit Scheme language implementation is based on the description of Scheme in the
Revised^5 Report on the
Algorithmic Language Scheme (r5rs),
with few extensions and omissions.
One notable omission is that arbitrary precision numbers are not implemented.
The system includes some functions from r6rs that are useful for microcontrollers and can
be optionally reduced to r3rs (to fit on the smaller microcontrollers: NXP 1343, 2103 and 2131).
This web page describes Armpit Scheme language extensions to r5rs.
For this purpose, language elements are separated into 7 categories: Core, Base, Ports,
Read-Write, Library, R6RS Library and System 0.
The Base, Read-Write and Library categories represent most of r5rs and their conformance
to the standard is presented on an other page, here.
For these three categories, this web page presents only the extensions that Armpit Scheme provides
(for example read/write from/to MCU registers).
For the R6RS Library category, the focus of the description is also on extensions to the standard
(for example, applying exact bitwise arithmetic operations to bytevectors of size 4).
The library functionality of R6RS (import, export, ...) is part of Core language elements.
The page organization follows the 7 categories listed above but starts, below, with some general items
followed by an approach for listing the language elements included on a given MCU.
These elements differ between regular MCUs and the 3 small MCUs (NXP 1343, 2104, 2131)
and some elements may be included conditionally (those related to input/output on specific
ports).
General Items:
When interacting with Armpit Scheme it may at times be necessay to interrupt the system,
for example to exit from an infinite loop.
The 2-key combination ctrl-c can be used (in most cases) for this purpose and returns
the user to the rep.
On startup, Armpit Scheme looks (by default) for a file named "boot" on its default storage space
(on-chip flash, on-board flash or an SD-card) and, if found, executes the scheme code in that
file before entering the rep (if the "boot" code does end).
If the boot file is absent, the system indicates so with a (FILE throw "boot") message (which is
purely indicative and of no consequence to system operation).
The default behavior of loading the boot file is controlled by a general purpose input pin of the
MCU (attached to a button on several boards) such that the user can prevent the system from
loading the boot file if its code has been found to contain errors (or if it is an infinite loop
and needs modifications).
The specific pin or button to actuate for this purpose is found in the source code
(under mcu-specific directories, in files named *_init_io*.s, at label FlashInitCheck:).
Listing Language Elements:
For a regular MCU, the symbols (functions, syntax elements and ground items) known to
Armpit Scheme can be listed using:
(let* ((envs (vector-ref (_GLV) 13))
(ne (vector-length envs)))
(let elop ((e 1))
(newline)
(newline)
(if (= e ne)
#t
(let* ((syms (vector-ref envs e))
(ns (vector-length syms)))
(let slop ((s 0))
(if (= s ns)
(elop (+ e 1))
(begin
(write (vector-ref syms s))
(write-char #\ )
(slop (+ s 2)))))))))
On a BeagleBoard-XM, for example, this produces (with category headers added for clarity):
R5RS functions are found under the BASE, READ-WRITE and LIBRARY headers, along with some
additions.
The conformance of the language under these headers to the r5rs standard is presented
here
The R6RS extensions are under the R6RS LIBRARY header while CORE, PORTS and SYSTEM 0
list additional system functions.
The listing is similar on other MCUs (except small MCUs: NXP 1343, 2103 and 2131),
apart from SYSTEM 0 that is mcu-specific, and
SD card, i2c, USB, on-board file system and fixnum (r6rs) functionality
which may be excluded
(the on-board file system is excluded on chips or boards without flash, i.e. for
Live-SD versions).
For small MCUs (NXP 1343, 2103 and 2131)
the implementation symbols can be listed using (let, let* and newline
are not defined on small MCUs):
((lambda ()
(define (newline) (write-char (integer->char 13)))
(define (elop envs ne e)
(newline)
(newline)
(if (= e ne)
#t
(begin
(slop (vector-ref envs e) (vector-length (vector-ref envs e)) 0)
(elop envs ne (+ e 1)))))
(define (slop syms ns s)
(if (= s ns)
#t
(begin
(write (vector-ref syms s))
(write-char #\ )
(slop syms ns (+ s 2)))))
(elop (vector-ref (_GLV) 13) (vector-length (vector-ref (_GLV) 13)) 1)))
On the Tiny2131, for example, this produces (with category headers added):
In comparison to regular MCUs, the small MCUs do not include the r5rs LIBRARY functions.
The pack function in their CORE is not functional (it returns #f).
Their BASE does not include macro functionality
and is limited to integers (no float, complex or rationals and associated functions,
like sqrt, sin, exp, ... in CORE, BASE, READ-WRITE, ...).
They also have fewer pre-defined PORTS and do not include R6RS LIBRARY fixnum functions.
These simplifications (and others) make the small MCU system fit in 24KB of flash
(vs 50KB to 64KB for the regular version).
Core:
The Core language elements implement some aspects of the inner functionality of the system.
This variable stores the Armpit Scheme version number (here: 050).
Performs garbage collection and returns the number of free bytes of the heap.
This variable is used to bind the winders used by r5rs' dynamic-wind function in extended environment frames.
This is the top-level startup program (typically the rep) and can be listed with (cdddr _prg).
This function gives access to the internal global scheme vector (see
implementation).
It can be used to modify interrupt callbacks, unlock the file system, view the list of
open files and view the user environment, among others.
(throw source value)
(_catch error)
These functions implement the user-level error reporting system.
Throw generates an error (a list of the form '(source throw value)) and invokes
_catch with this error as input.
The startup program sets _catch to the continuation of a write process that preceded
entry to the rep such that catching an error displays it on screen and restarts the rep.
Performs a file-system cleanup whereby all user files are copied to the bottom of file
FLASH and the freed up FLASH space is erased (not available on Live-SD versions).
(address-of object offset)
(address-of base-address offset)
This function returns the address, in RAM or FLASH, of a scheme object, with an added offset (mandatory)
or shifts a base-address (integer) left by 4 bits and adds the offset (mandatory) to it.
The resulting 32-bit address is returned as a bytevector (little-endian).
For example, (address-of + 0) gives the address of the header of the built-in (+ ...) function.
(pack object)
(unpack packed-object)
(unpack packed-object destination)
(packed-data-set! packed-object position value)
These functions allow movement of scheme objects from their current location (eg. heap) to
other locations (eg. above the heap, to FLASH library space or to another MCU).
The pack function copies an object and all its dependencies into a position-independent
version stored in a bytevector.
The unpack function performs the opposite conversion and stores the result in the heap if the destination
is unspecified or null, above the heap if the destination is a positive integer (eg. 1)
or into library FLASH if the destination is a negative integer (if such FLASH is available).
The packed-data-set! function is used to modify the contents of a packed object prior to unpacking
(used for example in the ARMSchembler).
(library ...)
(import ...)
(export ...)
These functions implement a subset of the R6RS library functionality (R6RS Section 7).
The library form is used to define a new library that is automatically written to FLASH library space
(where available) or RAM (where FLASH library space is non-existent).
The import function is used to load libraries at top-level into a running system or to load libraries
into other libraries when they are defined (i.e. within a (library ...) form).
The export form is used within a library form to specify which symbols of a library are to be
available when the library is imported (i.e. which symbols will be public).
Other R6RS library functionality is not available (eg. renaming exported symbols).
_lkp, _mkc, _apl, _dfv, _alo, _cns, _sav, _isx, _ism, _gc, _err
These symbols are bound to entry points for internal Armpit Scheme functions
that may be used by a compiler or ARMSchembler to avoid external duplication of built-in functionality.
Base:
(read-char)
(read-char port)
(read-char base-address offset)
(write-char char)
(write-char char port)
(write-char char base-address offset)
These functions work as in the r5rs standard except for the addition of a potential
base-address/offset pair as source or destination (both are integers).
If such a pair is specified then a single byte is read from or written to
the memory address given by 16*base-address+offset.
For read-char, the byte is returned as a scheme character while for write-char
it is obtained from the ascii code of the specified character and written to memory
(use char->integer and integer->char for appropriate conversions if needed).
ARM's ldrb and strb instructions are used for these bytewise operations.
This function unlocks the file system if it was inadvertently left locked by an aborted
file operation.
This function initializes communication with an attached SD-card (if available).
It returns #f if communication cannot be established.
It must be used (once, after card insertion) before performing any file listing
or file input/output operations with the SD-card.
(files)
(files port-model)
This function is used to list user files on the default file storage unit or on the
unit specified by the port-model.
There are 2 possible port-models for this operation: FILE and SDFT, where SDFT
is an attached SD-card (if available).
This function, used without input argument, erases all user files on the on-chip or on-board
flash.
The optional input argument, opt-arg, if a positive integer, specifies the start address
of a single flash sector to be erased (shifted right by 4-bits, i.e. one hexadecimal digit).
If a negative integer, it specifies the number of flash libraries to be erased from the
system (eg. -1 to erase the most recently added library).
(fpgw pseudo-file-descriptor file-flash-page)
This function can be used to write a page of data to on-chip flash.
It may be useful to repair a broken installation.
(open-input-file filename)
(open-input-file filename port-model)
(open-output-file filename)
(open-output-file filename port-model)
(close-input-port file-port)
(close-output-port file-port)
(close-output-port file-port mode)
These functions operate as in r5rs, with some extensions.
First, the file-ports returned by the open-*-file functions are integers and it is those
same integers that are given as port identifiers to the close-*-port functions
(as well as to read/write functions).
A given input file can be opened simultaneously several times and each instance gets its own
unique file-port number.
Opening an output file that is already opened for input or output generates an error.
The optional port-model in the open-*-file functions can be either FILE for on-chip or on-board flash files
or SDFT (where available) for files on a FAT formatted SD-card (max of 2GB).
The optional mode in close-output-port, if non-null, closes the file port but does not complete
the file writing operation (it is meant to be used for file deletion whereby a file would be
opened for output and then immediately closed with a non-null mode. Alternatively, it could be
closed as an input-port to achieve the same effect).
(expand expression)
(match form pattern initial-bindings literals)
(substitute bindings template)
These language items are used by the pattern language sub-component of Armpit Scheme
and exposed at top-level.
Expand is a syntax procedure that expands the macros in the expression it receives as input,
without evaluating that expression (eg. (expand (and 1 2 3))).
Match forms bindings between a pattern and a form and substitute substitutes these bindings
into a template.
An example of the usage of match is:
(define form '(plus 2 3 4))
(define pattern '(_ x ...))
(define old-bindings '((z . 1)))
(define literals '(else =>))
(define new-bindings (eval `(match ,form ,pattern ,old-bindings ,literals)))
new-bindings ; -> ((x 4 3 2) (_ . plus) (z . 1)) == an a-list of bindings
Subsequently, substitute can be used as follows:
(define template '(+ z x ...))
(define new-expr (eval `(substitute ,new-bindings ,template)))
new-expr ; -> (+ 1 2 3 4)
(eval new-expr) ; -> 10
Ports:
FILE, MEM, UAR0, UAR1, USB, SDFT, I2C0, I2C1
These variables are bound to port models (scheme vectors) used by Armpit Scheme to
identify the base address of a port (if any) and the sub-functions
used to open, close, read from and write to, or list files on, ports of that type.
Read-Write:
Displays the prompt on the current output port.
(parse expression-string)
Converts a string into an s-expression.
For example:
(parse "(+ 2 3 4)") ; -> (+ 2 3 4)
(eval (parse "(+ 2 3 4)")) ; -> 9
(load filename)
(load filename port-model)
Load works as in r5rs but allows an additional port-model parameter
that can be either FILE or SDFT to load code from on-chip/on-board flash
or from an attached SD-card, respectively.
(read)
(read port)
(read base-address offset)
(read i2c-base-address i2c-remote-address-vector n)
(write object)
(write object port)
(write object base-address offset)
(write object i2c-base-address i2c-remote-address-vector n)
The read and write functions implement the r5rs standard and provide extensions
to access MCU registers (or memory addresses at large)
and to communicate via an i2c interface (where available).
The port, if specified, is typically a small integer (for files) or the base address
of a perihperal, shifted right by 4-bits (one hexadecimal digit).
Register oriented read/write operations are specified by replacing the port with a base-address
and offset that define the source/target memory location as:
16 * base-address + aligned-absolute-value-of offset.
The value obtained from a register read is a scheme integer if the offset is non-negative
and a bytevector if the offset is negative. The bytevector contains all 32-bits from the source location
but the integer contains only the lower 30 bits (the upper 2 bits are lost when the value is tagged).
Similarly, the object written to a memory location can be either an integer or a bytevector provided
that the offset is set accordingly to a non-negative or negative integer.
In the case of an integer, the upper 2 bits written to memory are a copy of the upper
bit (2's complement sign bit) of the integer.
The offset is automatically aligned (truncated) to a multiple of 4 such that an offset of -1 can be used
to write a 32-bit bytevector (4 bytes) to a base-address with zero offset.
The i2c functionality is invoked either by specifying the i2c-base-address as port, or
by specifying the i2c-base-address as port and an address vector as 2nd or 3rd argument to
the read-write functions, followed by an optional number of bytes to receive or send.
The first form is used to read-write scheme objects as an i2c slave device interacting
with a remote master.
The objects are automatically packed and unpacked during this transfer.
The second form is used to transmit either 1 to 3 bytes of data or scheme objects via the i2c
interface.
The transfer is done in master mode if the i2c-remote-address-vector is non-null (or in slave mode
if it is null).
The transfer is performed for a limited number of data bytes (1 to 3) if that number (n) is
specified, or for whole scheme objects if n is omitted or null.
In the first case, the data bytes are obtained as a single scheme integer on read and specified as
a single scheme integer on write (little endian, LSB sent first if I remember --
check examples or source code to make sure if needed).
For master transfers, the i2c-remote-address-vector must be a scheme vector whose 1st element
(index 0) is the i2c address of the remote device and whose subsequent elements specify
internal registers of the remote device (typically there is just one of those and it is
used only for data transfers of 1 to 3 bytes, i.e. with a value given to the optional input n).
The i2c-base-address is the base address of the i2c peripheral block in the MCU, shifted
right by 4-bits (i.e. by one hexadecimal digit).
It can be obtained using, for example: (vector-ref I2C0 0).
Note that this flexible i2c read-write functionality, which enables multiprocessing,
is not implemented in all MCUs at this time.
Library:
This function can be used in ARMSchembled code to make a promise using internal
functionality.
var0, var1, var2, var3, var4, var5, var6
These symbols are used in built-in macros (let, cond, case, ...) to represent distinct
variables.
An example of their use can be seen in the implementaion of the and macro, viewed with: (cddr and).
They can be re-used in user code.
(call-with-input-file filename proc)
(call-with-input-file filename port-model proc)
(call-with-output-file filename proc)
(call-with-output-file filename port-model proc)
These functions work as in r5rs except for the option to add a port-model to
the list of input arguments.
The port-model can be FILE or SDFT to direct file operations towards an on-chip/on-board flash file
or towards a file on an attached SD-card, respectively.
This function is used to assess whether a given variable is defined in the currently
applicable lexical environment, without producing a CORE throw.
The following example illustrates:
(defined? 'xyz) ; -> #f
(define xyz 10)
(defined? 'xyz) ; -> #t
The link function links ARMSchembled code to built-in functions, variables, libraries and possibly
other (installed) ARMSchembled code on a given MCU.
In other words, it resolves addresses and values of external objects used in ARMSchembled code bytevectors
and inserts them into that code.
Code may be ARMSchembled (or compiled) on one MCU and the resulting bytevector may be linked,
unpacked and run on a separate MCU where built-in functions have different addresses
(provided that the target objects exist on the destination MCU and that the code was
ARMSchembled for the correct target type: Cortex-M3 (Thumb-2) or ARM).
This function is used to list the installed libraries, available for import, on a given system.
This function is a convenient specialization of the erase function for the case where
all flash libraries are to be erased.
(unpack-above-heap packed-object)
(unpack-to-lib packed-object)
These functions are convenient specializations of the unpack function of the Core category
for specific destinations.
R6RS Library:
bytevector?, make-bytevector, bytevector-length, bytevector-copy!, bytevector-u8-ref,
bytevector-u8-set!, bytevector-u16-native-ref, bytevector-u16-native-set!,
bytevector-s32-native-ref, bytevector-s32-native-set!
These functions constitute the subset of the Bytevectors library of R6RS
(Standard Libraries, Section 2) found in Armpit Scheme.
The bytevector-s32-native-ref/set! functions are limited by the 30-bit internal representation
of integers in Armpit Scheme such that the upper 2 bits of the bytevector's MSB are dropped
by the -ref function and these bits are set to copies of the MSb of the input integer in the -set!
function. Examples for these corner cases are:
ap> (bytevector-s32-native-ref #vu8(0 0 0 #xc0) 0) ; -> 0
ap> (let ((bv #vu8(0 0 0 0)))
(bytevector-s32-native-set! bv 0 #x20000000)
bv) ; -> #vu8(0 0 0 224)
bitwise-ior, bitwise-xor, bitwise-and, bitwise-not,
bitwise-arithmetic-shift, bitwise-if, bitwise-bit-set?,
bitwise-copy-bit, bitwise-bit-field, bitwise-copy-bit-field
These functions represent the subset of R6RS Exact bitwise arithmetic library functionality
(R6RS Standard Libraries, Section 11.4) implemented
in Armpit Scheme.
They have been extended so that the inputs that are operated upon can be either exact integers
or 4-byte-long bytevectors.
Operations involving bytevectors return bytevectors as output, where applicable.
Bytevectors and exact integers can be mixed, for example: (bitwise-ior #vu8(1 2 4 8) #xf0).
Note that bytevectors are presented in little-endian format.
(register-copy-bit base-address offset ei2 ei3)
(register-copy-bit-field base-address offset ei2 ei3 ei4)
These functions perform operations similar to bitwise functions of equivalent names but
do so on the contents of memory locations with address: 16*base-address+offset.
All inputs must be exact integers.
Internally, a read-modify-write process is applied to the contents of the
specified memory location (or register).
fx=?, fx?, fx?, fx=?, fx=?,
fxmax, fxmin, fx+, fx-, fx*, fx/
These functions represent the subset of R6RS Fixnum library functionality
(R6RS Standard Libraries, Section 11.2) implemented
in Armpit Scheme.
Their input arguments must be 2 exact integers, for example: (fx+ 3 4).
They are included conditionally in the system and their purpose is to speed-up
integer computations relative to generic numeric functions like +, *, max and min.
System 0:
The System 0 category contains symbols and functions whose definitions are
specific to each MCU family and potentially vary between MCUs within a family
as well. It is best to check the examples as well as the source code in the
mcu_specific directories, in files named *_system_0.s to ascertain their
specific uses. Some of the most commonly used items are described here.
sysc (or pmc, scu, ...)
VIC (or AIC)
These symbols, where available, are bound to the base address of a system
control register and of the MCU's interrupt controller, shifted right by 4-bits
(one hexadecmal digit).
gio0, gio1, ... or gioa, giob, ...
tmr0, tmr1, ...
uar0, uar1, ...
pwm0, pwm1, ...
i2c0, i2c1, ...
spi0, spi1, ...
adc0, adc1, ...
These variables, if available, are bound to the base addresses
of the corresponding peripheral registers of the MCU,
shifted right by 4-bits (i.e. one hexadecimal digit).
The numbering may differ between Armpit Scheme and the MCU
reference and can be checked using, for example: (number->string gio1 16).
(config-power ...)
(config-pin ...)
(pin-set-dir ...)
Where available, these functions are used to power-up peripherals or pin pads,
configure the multiplexed functionality of pins and set the direction of gpio pins as eiher
input or output.
(pin-set gpio-base-address pin)
(pin-clear gpio-base-address pin)
(pin-set? gpio-base-address pin)
These functions are used to set (high), clear (low) and identify the state of gpio pins.
(restart timer-base-address)
(stop timer-base-address)
These functions are used to start or stop a timer, where available.
(spi-put value spi-base-address)
(spi-get spi-base-address)
Where available, these functions are used to send and receive data through an SPI
interface.
Last updated January 14, 2012
bioe-hubert-at-sourceforge.net