Home |  Overview |  Registers |  Memory |  Interrupts |  Representations |  I/O Ports |  Exports |  Extending


A Scheme Interpreter for ARM Microcontrollers: Implementation (00.0160)

SourceForge.net Logo
 

Overview:


Armpit Scheme is written in ARM assembly using the unified syntax (ARM and THUMB-2) for the ARM7TDMI , ARM920T and Cortex-M3 cores (ARMv4T and ARMv7M architectures). The ARM part of the relevant assembly language is summarized in Quick Reference Cards here and here, and the ARM7TDMI core's technical reference (eg. operating modes, stack usage, ...) is available here.

The Armpit Scheme source code is organized into 4 major sections and several subsections as follows:

    0.   SYSTEM PARAMETERS and ASSEMBLER MACROS
      0.A.   Initialization and Operation Parameters Specific to each Board / MCU
      0.B.   Common Definitions (heap size, buffers, code address)
      0.C.   HARDWARE CONSTANTS for each MCU Family
      0.D.   RUNNING MODES
      0.E.   Scheme Masks, Tags and Constants
      0.F.   REGISTER RENAMING for SCHEME
      0.G.   ASSEMBLER MACROS for SCHEME

    I.   HARDWARE-INDEPENDENT INITIALIZATION, I/O AND UTILITIES
      I.A.   Exported Symbols for assembly and debugging
      I.B.   MCU Initialization Code
      I.C.   HARDWARE-INDEPENDENT INTERRUPT ROUTINES (ISRs)
      I.D.   MEMORY ALLOCATION AND MANAGEMENT

    II.  SCHEME IMPLEMENTATION
      II.A.  SCHEME FUNCTIONS AND MACROS:	CORE, SUPPORT -- Level 1
      II.B.  SCHEME FUNCTIONS AND MACROS:	LIBRARY       -- Level 1
      II.C.  EXTERNAL REPRESENTATIONS                         -- Level 1
      II.D.  SCHEME FUNCTIONS AND MACROS:	ADDENDUM      -- Level 1
      II.E.  IMPLEMENTATION ENVIRONMENT
      II.F.  EXTERNAL REPRESENTATIONS                         -- Level 2
      II.G.  SCHEME FUNCTIONS AND MACROS:	CORE, SUPPORT -- Level 2
      II.H.  SCHEME FUNCTIONS AND MACROS:	LIBRARY       -- Level 2
      II.I.  SCHEME FUNCTIONS AND MACROS:	ADDENDUM      -- Level 2

    III. HARDWARE-DEPENDENT CODE, USB and STARTUP
      III.A. Nearly identical Hardware-Dependent code: LED on/off, FILE system crunch
      III.B. MCU-DEPENDENT INITIALIZATION and I/O ROUTINES
      III.C. COMMON COMPONENTS OF USB I/O and ISR
      III.D. MCU-DEPENDENT COMPONENTS OF USB I/O and ISR
      III.E. STARTUP CODE FOR LPC 2800 and EP_93xx -- .data section

The scheme language implemented in section II of the source code is presented on the Armpit Scheme Language web page.

For MCUs with small amounts of RAM and FLASH (eg. LPC2103 and LPC2131) only the Level 1 part of Section II is included in the assembled image. For other MCUs, both Level 1 and 2 are included.

Armpit Scheme runs the MCU in User Mode (not Priviledged Mode). All interrupts processed by the Interrupt Service Routines (ISRs) are categorized as IRQ (not FIQ).

The executable code starts in section I.B and the rep is entered at the end of section I.B.3. (label rep:).

 

Register Usage:


ARM registers are renamed within the Armpit Scheme source to better reflect their purpose during normal program execution (outside of interrupts). The 16 ARM User Mode registers, their name within the source code and their use outside of interrupts is as follows:

   ARM  Armpit
   Name  Name   Usage
    r0    fre   pointer to the next free memory cell (also memory reservation status)
    r1    cnt   scheme continuation register
    r2    rva   raw value work register a (not garbage collected)
    r3    rvb   raw value work register b (not garbage collected)
    r4    sv1   scheme value work register 1
    r5    sv2   scheme value work register 2
    r6    sv3   scheme value work register 3
    r7    sv4   scheme value work register 4
    r8    sv5   scheme value work register 5
    r9    env   pointer to current environment
    r10   dts   pointer to data/return stack
    r11   glv   pointer to global scheme vector
    r12   rvc   raw value work register c (not garbage collected)
    r13         system stack pointer (ARM sp)
    r14   lnk   system link return (ARM lr)
    r15         system program counter (ARM pc)

The value in fre (r0) is updated by the memory management code of subsection I.D., each time new memory is allocated on the heap, during garbage collection and possibly during interrupts. The memory is allocated in 2-word (8-byte) chunks such that the binary value in fre would normally end with 3 zeros in its lsbs (least-significant bits). In Armpit Scheme, the 2 lsbs in fre are used to indicate the reservation status of memory to support post-interrupt restarting of memory allocation (Software Transactional Memory - STM). Outside of memory allocation, the memory is de-reserved and fre ends in #b10. During a memory allocation operation (cons, list, save zmaloc), the memory is reserved at level 1 and fre ends in #b01. During garbage collection, the memory is reserved at level 2 and fre ends in #b00. If a memory-allocating interrupt (eg. context-switch) occurs while memory is reserved at level 1, the return address of the interrupted code is modified so that its interrupted memory allocation is restarted when the code is resumed. If a memory-allocating interrupt occurs while memory is reserved at level 2 (i.e. during gc), the interrupted gc is continued with interrupts disabled prior to continuing with the interrupt's own memory allocation process. Garbage collection is triggered when memory allocation would cause fre to be greater or equal to the current value of heaptop, in which case the allocation is restarted after the gc completes.

The scheme continuation register, cnt (r1), stores the address at which an Armpit scheme function should return upon completion. On ARMv4T MCUs, cnt ends in #b00 (32-bit instructions for ARM assembly) and cnt (an address) always points to code memory that is outside of the heap (it is gc-safe). On ARMv7M MCUs (Cortex), cnt may end in #b00 or #b10 (mix of 16- and 32-bit Thumb-2 instructions) and may be interpreted as a pointer into code space or a floating point number, both of which are gc-safe. The ARM lnk register (r14, lr) behaves slightly differently on ARMv7M as discussed below.

The rva, rvb and rvc registers (r2, r3 and r12) are used to manipulate non-scheme entities in the code, for example the raw (un-tagged) value of an integer or ASCII character. The content of these registers is not garbage collected (they are actually modified by the gc). Their content is side-effected by memory allocation functions (zmaloc, save, list, cons) and is (generally) not preserved across such operations. They are however preserved during context-switching (saved in a special vector) as required for code resumption. The register rvb is also used to specify how many bytes to allocate when zmaloc is called and how many bytes were to be allocated when a gc was triggered. The gap in ARM registers between rvb and rvc (r3 and r12) is used to support the automatic register-stacking on interrupt performed by the Cortex cores (the eight registers r0-r3 and r12-r15 are automatically stacked on that core -- all of which are non-gceable in Armpit Scheme, such that the auto-stacking is properly gc-safe).

Registers sv1 to sv5 (r4 to r8) are used to temporarily store scheme values within the code. The content of these registers is garbage collected and must always be interpretable as a proper scheme object (scheme integer, character, list, vectcor, ...). On function entry, sv1 holds the 1st function argument, sv2 holds the second argument and so on, unless a function is specified as having 0 input arguments in which case sv1 holds a list (possibly empty) of all its input arguments. The unpacking of input arguments into registers is done by apply (code section II.A) just before it calls a scheme function. On function exit, sv1 holds the function's return value.

The env register (r9) stores the address of the current environment (a scheme list on the heap) that eval uses to evaluate an expression. In Armpit Scheme, a list (such as env) consists of cons cells that each occupy 8 contiguous bytes of RAM (2 contiguous words). The first word of the cons cell contains the car and the second word contains the cdr of the cell. For example, in ARM assembly language, the car of the environment list (eg. 1st binding frame) is refered to by [env] and the cdr of the environment list is refered to by [env, #4].

The address (in heap) of the data/return stack is stored in dts (r10). This stack is implemented as a scheme list and hence all of its components need to be gc-safe (scheme integer, character, list, vector, ...). This (gc-safety) includes the contents of cnt, env, sv1-sv5, and sometimes rva-rvc (when they store an immediate, eg. a scheme int or char). The scheme stack in dts is used to store both scheme functions' return addresses (cnt) and scheme data (sv1-sv5) when these values need to be preserved against upcoming computations. The Armpit Scheme assembly macros save and restor (code section 0.G) add and remove items from the stack. Unlike the system stack (r13, sp) this scheme stack is functional (i.e. not side-effected) such that captured continuations can extend it and resume from it independently of one another. The bottom of this stack consists of a null circular cons cell (a cell whose car is null and whose cdr points to its car), stored in FLASH, and labeled stkbtm: in the source code.

The glv (r11) register contains the heap address of the global scheme vector that stores a variety of scheme objects used by Armpit Scheme as follows:

       index    object
         0      scheme interrupt callback function
         1      address of top of current heap (as pseudo scheme integer)
         2      scheme data object received from i2c0 port
         3      scheme data object received from i2c1 port
         4      scheme current input port
         5      scheme current output port
         6      scheme open file list
         7      scheme global environment (top-level)
         8      scheme obarray
         9      address of top of lower heap (as pseudo scheme integer)
        10      address of top of upper heap (as pseudo scheme integer)
The scheme interrupt callback is a user-defined function accessed by writing to a special offset within the timer0 base address space (it is used fo all interrupts except USB). The address of the top of the current heap is updated by garbage collection or when non-moving objects are installed and the heap is shrunk. The top of the lower and upper heap are equal when mark-and-sweep gc is used but differ when stop-and copy gc is used (they are also updated when the heap shrinks). The i2c0 and i2c1 data objects are not used in the present implementation but are reserved for storing packed gc-eable objects received via i2c when the i2c subsystem is re-deployed within Armpit Scheme. The open file list is updated by file manipulation functions such as (open-input-file ...) and (close-output-port ...). The global environment (a list of binding frames) is the user environment used by the rep and (load ...) and returned by (interaction-environment), that sits on top of the built-in implementation environment. The scheme obarray is an association list of bindings between symbol names and variable IDs (object array) and is updated by the parser as well as functions like (string->symbol ...).

The system stack (r13, sp) is used (exclusively) for interrupts and during some file-oriented operations (rather than to stack return addresses during ordinary Scheme function calls). It points to a small region at the top of RAM that is not part of the heap (not garbage collected). It is a standard, fast, raw stack, rather than a more flexible, slower, scheme object (eg. list).

The system link register, lnk (r14, lr), is used to provide a return address when bl is used to branch to a non-scheme function (that returns via: set pc, lnk). On ARMv4T MCUs, lnk ends in #b00 and is gc-safe (except in special cases of writing to FLASH where it may temporarily point to heap RAM in which case the code is protected against interrupts) and can be temporarily stored in a scheme value register (sv1-sv5, and stack) if it needs to be preserved against a second bl to another non-scheme function. On ARMv7M MCUs (Cortex), the value of lnk produced by bl may end in #b01 or #b11 (the lsb is automatically set to indicate Thumb mode) and the lsb needs to be cleared to make lnk gc-safe if it needs to be saved in a scheme value register, and then the lsb needs to be restored prior to returning via lnk. To help with this process, the constant lnkbit0 (#b00 on ARMv4T and #b01 on ARMv7M) is defined and used where necessary in the Armpit Scheme code.

The system program counter (pc, r15) keeps track of which instruction the ARM core is executing. It has the same gc-safety characteristics as the lnk register discussed above (can be preserved in scheme variable registers). During a scheme function call (cf. call macro in code section 0.G), the pc is stored in cnt to provide a return address. In the case of Cortex MCUs, the return code is padded with nops to account for the variable instruction length (16- and 32-bits, while pc is 8-bytes ahead when captured).

 

Memory Usage:


Armpit Scheme runs the Scheme core from FLASH, provided that the FLASH starts at memory address 0x00000000 (the exceptions being the LPC-H2888 and CS-EP9302 where it runs from RAM as the external RAM is remapped by the MMU to start at 0x00). User files (created with open-output-file) are stored in FLASH above the Armpit Scheme code, except on the LPC-H2214 where internal MCU FLASH stores the code and external FLASH stores the files (the other MCU boards have either a single internal or external FLASH space).

Most of the RAM is defined as heap space. Some RAM below the heap is typically used for Armpit Scheme I/O buffers, except on the LPC2148/2158 where the buffers are in USB RAM and the heap is in the main RAM, and on the LPC-H2214 where buffers are in the 16 KB of internal RAM and the heap is in external RAM. The system stack, user-modifiable ISR VECTOR and user-installed machine code space are found in RAM above the heap. Typical memory maps for most MCUs and for the LPC-H2888/CS-E9302 are depicted below:


     ARMPIT SCHEME COMMON MEMORY MAP                    ARMPIT SCHEME MAP FOR CS-E9302, LPC-H2888
     -------------------------------                    -----------------------------------------

                                                      Address space top --------------------------
Address space top --------------------------        	                         empty  
		            empty                    Top of Peripherals --------------------------
Top of Peripherals --------------------------        System Peripherals PERIPHERAL REGISTERS    
System Peripherals PERIPHERAL REGISTERS                                 --------------------------
                   --------------------------		                         empty  
		            empty                         Top of FLASH  --------------------------
       Top of RAM  --------------------------             + #x00010000  USER SCHEME FILES  
                   SYSTEM STACK (160 bytes)            Bottom of FLASH  ARMPIT SCHEME MACHINE CODE
                   ISR VECTOR   (144 or 288 bytes)                      --------------------------   
                   INSTALLSPACE (16 or more bytes) 	  	                  empty      
                   HEAP1        (3.5 KB to 32 MB)           Top of RAM  --------------------------
                   HEAP0        (3.5 KB to 32 MB)                       SYSTEM STACK (160 bytes)
                   WRITE BUFFER (0 or more bytes)                       ISR VECTOR   (144 or 288 bytes)
                   READ BUFFER  (752 or 1936 bytes)                     INSTALLSPACE (16 or more bytes)
                   USB  Buffers (64 bytes)                              HEAP1        (16 MB)
                   FILE LOCK    (4 bytes)                   #x00100000  HEAP0        (16 MB)
                   I2C Buffers  (2 x 20 bytes)                          WRITE BUFFER (0 or more bytes)
    Bottom of RAM  I2C Address  (4 bytes)                               READ BUFFER  (1936 bytes)
                   --------------------------                           USB  Buffers (64 bytes)
		            empty                                       FILE LOCK    (4 bytes)   
     Top of FLASH  --------------------------                           I2C Buffers  (2 x 20 bytes) 
       #x00010000  USER SCHEME FILES                        #x00020000  I2C Address  (4 bytes)
FLASH: #x00000000  ARMPIT SCHEME MACHINE CODE          RAM: #x00000000  ARMPIT SCHEME MACHINE CODE

The I2C address at the bottom of RAM is used on MCUs that do not otherwise have a peripheral register to store it (SAM7, EP9302). The I2C buffers are used to store the state of I2C transactions and a single word (4 bytes) of I2C data. The USB buffers store the state of USB transactions and partial data used during enumeration. The READ BUFFER space is used to store a scheme string that contains the characters that have been received from uart or usb. Scheme functions such as read-char and read get their characters or expressions from this READ BUFFER and then crunch it appropriately such that it always represents a string of the appropriate length with the first unread character at index 0. The size of the READ BUFFER limits the maximum number of characters that can be typed-in as a single input expression in Armpit Scheme (user beware). The WRITE BUFFER is used only by USB and is the scheme string where output data to be sent from the MCU to the USB Host are temporarily stored (waiting for a USB interrupt). The INSTALLSPACE is a resizeable zone of RAM where the user can install non-gceable objects such as fixed data items (fonts) or machine code (drivers). It grows downwards and the heap is resized automatically to account for the size of installed objects (the install function is at label instal: in section II.D.4. of the source code). The ISR_VECTOR is a user-modifiable scheme vector that stores machine code Interrupt Service Routines (ISRs) for the 32 or 64 interrupts of the MCU (except USB) as described in the next section. The SYSTEM STACK is that pointed to by the MCU's sp register and is used during interrupts.

The heap is the dynamic memory area where user Scheme data are stored, including the user environment, user obarray, scheme stack, global scheme vector, user-defined functions (closures) and user data (strings, vectors, lists, ...). It also stores temporary data used to evaluate functions and to process files. Space on the heap is allocated 8-bytes (2 words) at a time by primtives _maloc:, _cons:, _save: and _list: (code section I.D.3.). These primitives are designed such that memory allocation can be transactional. They reserve memory at level 1 when they start and de-reserve it upon completion. A memory-allocating interrupt occuring while memory is reserved at level 1 will cause the interrupted allocation to be restarted when user code resumes after the interrupt. This eliminates the need for disabling and re-enabling interrupts around memory allocation which potentially speeds up code execution and reduces interrupt latency. Within the Armpit Scheme source code, cons, list and save are typically called via macros defined in section 0.G. In the case of cons and list, the macro performs the de-reservation of memory. In the case of _maloc (zmaloc) de-reservation is to be performed explicitly after it is safe to do so (for example, after the items of a newly allocated vector have been stored in the vector -- because vector contents are subject to gc). The 4 memory allocation functions call the garbage collector prior to storing an object if the memory to be allocated for this object would extend past the top of the heap.

Two garbage collection algorithms are implemented in Armpit Scheme: stop-and-copy (label gc: in section I.D.1.) and mark-and-sweep (label gc: in section I.D.2.). The stop-and-copy algorithm (default) is the most mature of the two, it is fast and stable against interrupts but uses two semi-heaps leading to half the useable RAM for Scheme at any time. The mark-and-sweep algorithm is significantly slower and not as stable against memory-allocating (Scheme) interrupts but places all available RAM in a single heap which maximizes the space available to Scheme. Mark-and-sweep also uses more code FLASH space than stop-and-copy and uses a small amount of RAM above the heap to keep track of grey and black sets (tri-color marking scheme). Both algorithms are fully compacting and the user selects between them at assembly time by commenting or uncommenting a constant definition (for mark-and-sweep) at the top of the source. Neither algorithm uses the system stack.


 

Interrupt Service:


Two Interrupt Service Routines (ISR) are defined in Armpit Scheme: The USB ISR with label usbisr: (code section III.C) processes USB interrupts only and the generic ISR with label genisr: (code section I.C.1.) processes all other interrupts. USB is implemented only for selected MCUs and USB interrupts are enabled in Armpit Scheme when these MCUs are connected to an active USB line. During character reception, the USB ISR operates similarly to the uart ISR described below but obtains its input characters from the USB line. The USB system also uses a WRITEBUFFER to send characters out through USB. Scheme "write" functions targeted to the USB port write the relevant external representations to the USB WRITEBUFFER and the ISR sends the contents of this buffer out through the USB line when the appropriate request (which generates an interrupt in Armpit Scheme) is received from the USB Host. The echo of characters received via USB is handled similarly. The ISR further handles standard, interrupt-driven, USB management tasks, such as device enumeration.

The generic ISR processes all other interrupts with some enabled by default (uart, timer, i2c) and others left for the user to enable if desired. On interrupt entry, the 8 registers fre, cnt, rva, rvb, rvc, lnk_usr, pc_usr and spsr are pushed onto the IRQ mode stack (for compatibility with Cortex cores). The number (ID) of the interrupt source is then identified and stored in rvb. The code then checks if an executable, user-specifiable, machine code interrupt service routine is available for this interrupt ID at that ID's index in the ISR VECTOR, and, if not, exits the generic ISR, clearing the interrupt in the core and popping the stack to return to the interrupted user mode process (label gnisxt:). Otherwise, it executes the ISR from the ISR VECTOR. By default, the ISR VECTOR contains full ISRs for uart and i2c and a partial ISR for timer interrupts. A full ISR is one that performs all needed ISR operations, including clearing the interrupt in the peripheral and in the core, and exiting the ISR properly, returning to user mode upon completion. A partial ISR is one that just clears the interrupt in the peripheral and then returns (via lnk) to the generic ISR for further processing. A partial ISR must not modify rvb, sv1-sv5, env, dts, glv and sp such that genisr can be properly resumed. The return to genisr makes it possible for the ISR to eventually call a user-specified interrupt service routine written in Scheme that executes in the top-level as described below.

The uart interrupt is enabled when the MCU is not connected to a valid USB line (for USB-enabled MCUs). This interrupt is set to fire for each character received by the interface. The uart full ISR (label puaisr: in code section I.C.2.) is branched to from genisr via the ISR VECTOR and reads the received character from the uart's receive-hold register which it then stores at the next available location in Armpit's READBUFFER, or otherwise updates this buffer appropriately when a backspace, "Enter" or ctrl-c is received. The ISR also echoes received characters to the uart's Tx line and eventually exits back to resume the interrupted user code by using the exitisr macro of code section 0.G.

The i2c full ISR is a Level 2 function (not used on SMALL MEMORY MCUs) found at label pi2isr: in code section II.I.7.. This ISR is not fully implemented and not fully functional in the current version of Armpit Scheme and therefore will not be discussed further here. It will be re-instated in a future code version.

The partial timer ISR is found at label ptmisr: in section I.C.3. of the source. It is branched to (using the bl operation) by genisr, using the ISR VECTOR. It clears the timer interrupt in the timer peripheral block and then uses lnk to return to the generic ISR. This partial ISR is archetypical of partial ISRs that a user may want to add to the ISR VECTOR such that the targeted system interrupt can be processed by a Scheme ISR at top level. It is a pseudo-primitive, that is, a block of code that starts with the primitive function tag and ends with a return instruction such as set pc, lnk or set pc, cnt. Such blocks of machine code are not gc-eable and must be stored in FLASH or non-heap RAM. In the latter case, the address of the pseudo-primitive code block (to be stored in the ISR VECTOR) is obtained as output of the Armpit Scheme install function.

When the generic ISR is resumed by returning from a partial ISR, it first checks to see if a memory allocation operation or garbage collection was preempted by the interrupt and, if so, updates the saved pc_user to either restart or complete the memory allocation once the interrupt completes, or resumes the gc, with interrupts disabled, prior to going forward with the remainder of interrupt processing. These operations are performed by the code at label genism:. The identification of situations where a memory allocation operation was interrupted is based on the lower 2 bits stored in the free-memory pointer (fre) that indicate the memory reservation status. The decision to restart memory allocation or to complete it is based on the uniform use of the instruction:

     orr	fre, rva, #0x02
to de-reserve memory and simultaneously commit the memory allocation process. Interruptions prior to this instruction lead to a restart of the memory allocation whereas interruptions right at this critical instruction lead to completion (in genism:) of the allocation.

After dealing with memory management issues, the generic ISR checks the global vector (glv) at index 0 for a user-specified Scheme ISR. If no such ISR is found, the generic ISR exits via gnisxt: (clear interrupt in core, resume interrupted user process). If a user Scheme ISR is found, the generic ISR proceeds to save the context of the interrupted Scheme execution in two vectors: a gc-eable vector (scheme vector) for sv1-sv5, env and dts, and a non-gceable vector (scheme symbol) for cnt, rva-rvc, lnk_usr, pc_usr and psr_usr (spsr). It then clears the interrupt in the core, stores the context vectors on a new Scheme stack, sets the Scheme continuation (cnt) to restore the interrupted context from that stack (rsrctx:), sets the IRQ stack content to resume the built-in apply function with sv1 set to the user Scheme ISR and sv2 set to the interrupt ID and then exits the interrupt via the isrexit macro. This process returns the system to user mode, making it execute (apply scheme_ISR interrupt_ID) with resumption of the saved context of the interrupted scheme process as continuation. In other words, the system executes the user-specified Scheme ISR with the interrupt ID as input argument and, when this Scheme ISR is done, the system resumes the interrupted scheme process.

The user-specified Scheme ISR is a function of one argument (the interrupt ID). It is executed in user mode with interrupts enabled like the rest of user Scheme code. It can use a conditional statement to perform different actions based on which interrupt triggered its execution. Its continuation is the resumption of the interrupted Scheme process and can be captured with call/cc and stored on a queue for preemptive (or otherwise interrupt-driven) multitasking. The input argument to that continuation is a dummy argument. The user-defined Scheme ISR is stored on the glv at index position zero from user-mode, by writing it to the base address of timer0 at an offset of #x010000 or greater. It can also be read from that same location (see pmmred: in code section II.A.6.6.2. input SUPPORT 3 and pmmwrt: in code section II.A.6.6.3. output SUPPORT 3). It is worth remembering at this stage that this Scheme callback (ISR) can be executed only if there is a partial ISR for the desired interrupt source in the ISR VECTOR (and if that source is enabled in the system and set to branch to genisr: on interrupt).


 

Internal Representations:


Armpit Scheme uses the lower bits of data objects to identify their type. The tagging is adaptive with 2 or 8 bits giving full type and 3 or more bits providing category information. Two-bit tags are used to fully identify addresses, integers and floats. The type of an object can be recognized by inspecting its two least significant bits as follows:

      00 -> address
      01 -> integer
      10 -> float
      11 -> other object

Addresses are aligned to word boundaries and hence natively have their lowest two bits as 00 in the 32-bit Armpit system. Integers are stored as a raw value in the 30-bits above their type tag, using two's complement. A raw integer is shifted left by 2 bits and orred with the #b01 type tag to make a scheme integer. The converse operation is performed by an arthmetic shift, two bits to the right, to preserve the sign and value of the raw integer. Floats are represented using a shortened form of the IEEE-754 32-bit standard, namely the lower two bits of the mantissa are replaced by the type tag #b10. The following illustrates these internal representations, with uppercase letters representing hexadecimal values (X if arbitrary), lower case letters representing bits, and numerical digits representing actual values (s, e and m stand for sign, exponent and mantissa):

      XXXXXXXbb00 -> address, aligned to word boundary
      XXXXXXXbb01 -> integer
      s-eeeeeeee-mmmmmmmmmmmmmmmmmmmmm-10 -> float

The 3 lsbs of an 8-bit tag indicate whether an object consists of a single word or not, as follows:

      011 -> more than one word (listed oject, sized object)
      111 -> single word

In regular operation (outside of gc and packing/unpacking operations) the 4 lsbs of the 8-bit tag of a single word object differentiate between variables/syntax-IDs and scheme constants/characters:

      0111 = #x7 -> variable or syntax-ID
      1111 = #xF -> null, #t, #f or character object

The full internal representations of these 8-bit tagged single-word objects are (in hex):

      00-VRID-17 -> syntax,   VRID is the 16-bit ID of the syntax item
      MC-VRID-27 -> variable, MC is the MCU ID or 0, VRID is the 16-bit variable ID
      #x0000000F -> '()
      #x0000001F -> #t
      #x0000002F -> #f
      #x0000CC3F -> character, CC is the character's ASCII code

Scheme variables have the 8-bit tag #x27. Bytes at offsets 1 and 2 represent the unique numerical ID of the variable, assigned to it by string->symbol (eg. as used by read/parse), and stored as a raw 16-bit integer. The upper byte of the variable stores the ID of the MCU on which the variable was created, or zero if it is an implementation variable. The MCU ID is included in the variable to differentiate user-defined variables from implementation variables and to enable multiprocessing where the MCU may receive variables defined on another MCU that must not be confused with variables that have the same 16-bit numerical IDs but that were defined on the local MCU. The MCU ID used in the internal representation of variables is that defined for I2C communication over the i2c0 interface and is user-defined at the top of the Armpit Scheme source code (where a user may also select MCU board and gc options). It can be modified by writing to the appropriate I2C register of the MCU (I2C0ADR on LPC2000 devices).

Syntax variables are represented internally with the 8-bit tag #x17 and a 16-bit numerical (VRID) as shown above. The MCU-ID is zero because syntax variables have heretofore been implementation (built-in) items. User functions such as (define-syntax ...) define a regular variable that is bound to a macro rather than a distinct syntax variable.

The VRID and MCU-ID of variables can be obtained at top-level in a running Armpit Scheme system using (for example):

      (number->string (ash 'quote -6) 16)  ; -> "0000000A", quote  has VRID #x0A = decimal 10
      (number->string (ash 'lambda -6) 16) ; -> "0000000B", lambda has VRID #x0B = decimal 11
      (number->string (ash 'fail -6) 16)   ; -> "0064007D", user var fail has VRID #x7D, MCU-ID #x64

The two remaining 8-bit tagged, single-word objects, are used by the garbage collector and in packed objects. Their internal representation is:

      #x00000007 -> broken-heart
      #x00000037 -> offset tag for packed object

Multi-word object tags, with 3 lsbs of 011, are separated into those indicating fixed size objects (eg. strings, vectors, ...) and those indicating a listed tagged object (eg. procedure, continuation, ...) or a primitive. The mask #xC3 differentiates between these two types:

      tag AND #xC3
          #x43 -> fixed size object
          #xC3 -> listed tagged object or primitive
          else -> single word object or pointer

The mask #xCB can further differentiate between gc-eable and non-gceable fixed size objects and between executable and non-executable multi-word objects:

      tag AND #xCB
          #x43 -> fixed size object with non-gceable content (symbol, string ...)
          #x4B -> fixed size object with gc-eable content (vector)
          #xC3 -> non-executable tagged list (promise, macro)
          #xCB -> executable tagged list (continuation, procedure ...) or primitive
          else -> single word object or pointer

Fixed size objects are stored in a set of contiguous memory locations starting with a word that combines its size and tag. The raw (size 0) tags are:

      0100-0011 = #x43 -> symbol
      0101-0011 = #x53 -> string
      0111-0011 = #x73 -> packed-object
      0100-1011 = #x4B -> vector

The 24-bits above the 8-bit tag indicate the number of items in the object (bytes for symbols, strings and packed objects, words for vectors) as a raw unsigned integer. Considering the foregoing, the full internal representation of implemented fixed size objects becomes:

      ------------------ ------------------- ------------------- ------------------- -------------------
      object:            symbol              string              packed-object       vector
      ------------------ ------------------- ------------------- ------------------- -------------------
      word at offset  0: #xnnnnnn43          #xnnnnnn53          #xnnnnnn73          #xNNNNNN4B
      word at offset  4: ascii-chars-0123    ascii-chars-0123    bytes-0123          scheme-object-1
      word at offset  8: ascii-chars-4567    ascii-chars-4567    bytes-4567          scheme-object-2
      ...                ...                 ...                 ...                 ...
      ------------------ ------------------- ------------------- ------------------- -------------------

where nnn... represents object size in number of bytes and NNN... in number of words. Note that the 2 msbs of the 8-bit tag are #b01 and correspond to the 2-bit integer tag. Hence, by shifting the 8-bit sized-tag of a fixed size object to the right by 6 bits, one obtains a scheme integer that gives the size of the symbol, string or vector (as in (string-length...), (vector-length ...), ...). Also, within the heap, these fixed size objects are followed by an appropriate amount of empty space to satisfy the 8-byte alignment of the implementation.

Promises, macros, continuations and procedures (closures) are implemented as tagged list objects. Each such object is represented as a cons cell (8 contiguous memory bytes) whose car is an 8-bit tag and whose cdr is a list (the object's content). Scheme promises have the tag #xC3 and are generated by the scheme syntax function delay, for example (delay expression). The promise's content includes the delayed expression and the environment (env) in which it is to be evaluated. A null list is included in the content list and is replaced by the promises's value when it is forced such that subsequent forcing will simply return the original forced value:

      promise         <-  (#xC3 . promise-content)
      promise-content <-  (env () expression)

Macros are built by the scheme function syntax-rules: (syntax-rules literals transformer1 transformer2 ...) and are tagged with #xD3. The macro-body is a regular list of literals and transformers, all of which are represented internally as regular lists (without tags):

      macro        <-  (#xD3 . macro-body)
      macro-body   <-  (literals transformer1 transformer2 ...)
      literals     <-  (variable1 variable2 ...)
      transformer  <-  (pattern template)

Scheme continuations are tagged with #xCB and built by the scheme function call/cc. The continuation's content consists of the current return address (cnt), the current environment (env) and the address of the scheme stack (dts) as needed to continue the current computation. The scheme stack is functional in Armpit Scheme, that is, it is not side-effected by code:

      continuation <-  (#xCB . cont-content)
      cont-content <-  (cnt env dts)

Scheme closures are tagged with #xDB and built by the scheme syntax function lambda as in: (lambda (variable1 variable2 ...) body). The closure's internal representation list includes the environment in which the closure is defined, its list of input variables and its body:

      procedure    <-  (#xDB . proc-content)
      proc-content <-  (env vars-list body)
      vars-list    <-  (variable1 variable2 ...)

Primitive procedures (both regular and syntax) are blocks of machine code tagged with #xEB and stored in a contiguous region of either FLASH or non-heap RAM (not subject to gc). The tagged start word is complemented in bits 8 to 10 by the number of input arguments that the primitive takes (from 0 to 4). This information is used by (apply ...) to unpack the input argument list into scheme value registers sv1 to sv5 prior to calling the primitive which can then start manipulating its inputs directly from the contents of these registers. If the specified number of input arguments (n) is smaller than that in the input list provided to the function then the list of remaining arguments is stored in register sv_n+1. In particular, if n=0, the whole list of input arguments (possibly null) is found in sv1. To differentiate between regular primitives (whose input arguments are evaluated prior to calling the function) and syntax primitives (whose input arguments are not evaluated) the Armpit Scheme primitives are preceded, in FLASH or non-heap RAM, by an empty (ID = 0) variable tag (#x27) or syntax tax (#x17). The internal representation of a primitive (block of code) is then:

      ------------------------
      offset -4:  #x27 or #x17      <- variable or syntax
      offset  0:  #x00000nEB        <- primitive tag, n is the number of input arguments, 0 to 4
      offset  4:  Machine Code
      offset  8:  Machine Code
         ...          ...
      ------------------------

User environments (in env and on the glv) and the user obarray (on the glv) are dynamically extended by user interaction and represented as scheme lists on the heap. The dynamic obarray is a list of bindings between variables and their symbols where each binding is a cons cell with the variable as cdr and its symbol's address as car:

      obarray      <-  (obinding1 obinding2 ...)
      obinding     <-  (address-of-var's-symbol . variable)

Dynamic environments are represented as lists of frames where each frame is a list of bindings between variables and their values. As with the obarray, the bindings are implemented as cons cells whose car is the variable and the cdr is its value (a scheme object). The bindings within a frame are sorted in ascending order of variable IDs to provide for faster search (see bndenv: and mkfrm: in section II.A. of the source code).

     environment   <- (frame1 frame2 ...)
     frame         <- (ebinding1 ebinding2 ...)   <- sorted by variable ID
     ebinding      <- (variable . value)

In contrast to the above dynamic objects, the built-in implementation environment and obarray are static and expressed implicitly in the code rather than explicitly as lists. The built-in scheme environment is at label scmenv: in section II.E. of the source and is a scheme-null-terminated block of memory containing addresses of built-in symbols. The code is structured such that each built-in symbol is followed directly (with 4-byte alignment) by its built-in type (variable or syntax) and value (constant or primitive). The VRID of internal variables is determined by their position in scmenv and their MCU ID is zero. Because of the mix of 16- and 32-bit instructions on Cortex cores, it is necessary to explicitly align symbols, code and labels to word boundaries for proper linkage via scmenv:. The following examplifies (the actual source code uses constants and macros to simplify):

     .balign   4                 <- word alignment for scmenv
     scmenv:   address_of_true   <- beginning of build in environment and entry for #t
               ...
               address_of_quote  <- entry for quote function
               ...
               #x0000000F        <- end of built-in environment (scheme null)
              ...

     .balign   4                 <- word alignment for true
     address_of_true:            <- assembly address label for true
	       #x00000243        <- symbol tag for true (2 letters)
	       #x00007423        <- ascii chars of "#t" (external representation)
	       #x00000027        <- type tag (variable) for true
               #x0000001F        <- value of true (scheme true)
             ...

     .balign   4                 <- word alignment for quote
     address_of_quote:           <- assembly address label for quote
	       #x00000543        <- symbol tag for quote (5 letters)
	       #x746F7571        <- ascii chars of "quote" (external representation)
               #x00000065        <- ascii chars of "quote" (continued)
	       #x00000017        <- type tag (syntax) for quote
               #x000001EB        <- primitive tag for quote (1 input argument)
	       #xE1A0F001        <- machine code of quote syntax function
             ...

 

Input and Output Ports:


Armpit Scheme input and output ports consist of 2 parts aimed at maintaining the simplicity of use of prior versions while also enabling re-use of port code and user extensions towards new peripherals. The first part is (typically) the port argument with which a top-level port function is called (eg. port-base-address, register offset, file-id, ...) and the second part is a vector of port data and pseudo-primitives used to operate on the port. The first part is the same as used in prior versions and the 2nd part is new. The two parts are joined in a cons cell to form a "full" input or output port. For the built-in ports (uart, usb, file, memory), these "full" ports are built by the top-level input/output functions (read, write ...) via calls to internal functions setipr and setopr (code section II.A.6.6.1. ports SUPPORT 1). For user-defined ports, the "full" ports should be built by the user and passed to the top-level I/O functions as input (or set as return values for the current-input/output-port functions). The top-level I/O functions call specific port-vector pseudo-primitives to accomplish their work and do so through the iprtfn and oprtfn dispatchers (code sections II.A.6.6.2. input SUPPORT 1 and II.A.6.6.3. output SUPPORT 1, respectively).

An input port vector is a Scheme vector that consists of at least 5 elements (eg. see nulipr: in code section II.A.6.6.2. input SUPPORT 2):

        Position   Item                   Type
        --------   ---------------------  ---------------------------------
	index 0:   port-type	          scheme integer
	index 1:   close-input-port       pseudo-primitive (returns via lnk)
	index 2:   read-char / peek-char  pseudo-primitive (returns via lnk)
	index 3:   char-ready?            pseudo-primitive (returns via lnk)
	index 4:   read                   pseudo-primitive (returns via cnt)
The port-type is 1 for an input port (2 for output, 3 for input-output). The 4 pseudo-primitives are machine code functions that receive the full port as input in sv1, perform appropriate processing and return via either lnk or cnt as indicated above. These blocks of code are meant to be used only internally by the system and are not typed as variable or syntax. Additionally, they are called by top-level port functions (read-char, peek-char, ...) through a dispatcher rather than through apply and therefore their number of input arguments (1) does not need to be specified in the 8-bit primitive tag that precedes their code.

For most input ports, close-input-port does nothing but set the return value to #t or the non-printing-object, #npo, (in sv1) and return, while for file ports it removes the requested file descriptor from the open-file-list stored in glv prior to returning #t (or 0 if the file descriptor was not there). The read-char/peek-char function returns the next character available from the port, possibly using aditional port-vector data and pseudo-primitives stored at indices above 4. Read-char is differentiated from peek-char internally by temporarily modifying the port-base-address from a scheme integer to a scheme float. For uarts and usb the character is obtained from the READBUFFER (scheme string) that is then updated or not (read vs peek), for files it is obtained from FLASH and the file descriptor is updated or not accordingly (read vs peek), for memory the character is obtained from the byte at the specified offset above the port-base-address. For character input ports (uart, usb, file) a helper datum at index 5 in the port vector is used to determine whether no character available should cause read/peek to hang or to return the end-of-file character. The char-ready? pseudo-primitive returns a boolean indicating whether a character can be read or peeked from the port without hanging. The read pseudo-primitive at index 4 returns a parsed full datum (internal representation of the aquired datum) from a character input port or a 32-bit integer from a memory port. For character input ports, the helper datum at index 5 determines whether the input datum is carriage-return (cr) terminated (uart, usb) or if it may also be terminated by a space (file). The additional port-vector items for a typical character input port are as follows:

        Position   Item                   Type
        --------   ---------------------  ---------------------------------
        index 5:   wait-for-cr?           pseudo-primitive (returns via lnk)
	index 6:   init                   pseudo-primitive (returns via lnk)
	index 7:   getc                   pseudo-primitive (returns via lnk)
	index 8:   finish-up              pseudo-primitive (returns via lnk)
The init function sets the offset from which to read the port's input buffer (READBUFFER or file descriptor), the getc function returns a raw character from the port's buffer and the finish-up function extracts a full datum from the port's buffer.

An output port vector is a Scheme vector that consists of at least 4 elements (eg. see nulopr: in code section II.A.6.6.3. output SUPPORT 2):

        Position   Item                    Type
        --------   ---------------------   ---------------------------------
	index 0:   port-type	           scheme integer
	index 1:   close-output-port       pseudo-primitive (returns via lnk)
	index 2:   write-char/write-string pseudo-primitive (returns via lnk)
	index 3:   write / display         pseudo-primitive (returns via cnt)
The port-type is 2 for an output port (1 for input, 3 for input-output). The 3 pseudo-primitives receive as input a scheme object (to output) in sv1 (or a mode with which to close an output port) and the full port in sv2. They then perform appropriate processing and return via either lnk or cnt as indicated above. They are used internally by the system and not typed as variable or syntax. Additionally, they are called by top-level port functions (write-char, write, ...) through a dispatcher like the input port pseudo primitives and therefore their number of input arguments (2) does not need to be specified in the 8-bit primitive tag that precedes their code.

For most output ports, close-output-port does nothing but return. For file output ports, it checks the file closing mode in sv1 to identify whether file data should be written out to FLASH or not and, if so, writes the data out to FLASH. In either case it also removes the file descriptor form the open-file-list. The write-char/write-string function writes the single scheme character, or the set of characters from the scheme string, that it receives as input in sv1 to the output port that it receives in sv2. For memory ports, this writes a single byte of data at the specified base-address + offset. For character ports (uart, usb, file) write-char/string uses a helper function at index 4 to perform character-wise output either directly or via a buffer (port-specific). The write/display port pseudo-primitive writes a word to a memory location for a memory port or writes the external representation of the datum it receives in sv1 to the port it receives in sv2 using the helper function at index 4. The additional port-vector item (helper function) for a typical character output port is as follows:

        Position   Item                    Type
        --------   ---------------------   ---------------------------------
	index 4:   putc   	           pseudo-primitive (returns via lnk)
The putc function outputs the scheme character it receives as input in sv1 either directly to the port it receives as input in sv2 and sv4 (uart) or to the port's WRITEBUFFER (usb) or to the port's file descriptor (file).

It is expected that this implementation of ports is sufficient to enable the user to define a keyboard input port and a display output port which could turn an Armpit Scheme system into a standalone (yet still rudimentary) Scheme Machine. A keyboard (raw matrix) input port might re-use most components of the uart input port-vector for example, along with an interrupt service routine (in machine code) that would decode the key input and then store it in the READBUFFER (eg. extended from puaisr in code section I.C.2.). It may (or not) be sufficient to implement networking ports and FAT-16 ports to get/store data on SD/MMC cards.


 

Exported Internal Objects:


Armpit Scheme supports installation of non-gceable objects and code above the heap of a running system. A few internal objects that are normally not easily accessible from the rep have been exported (made available at Top-Level) to ease this process when code assembled on the system itself is concerned (eg. via an ARMSchembler). The exported objects are as follows:

  Source      Top-Level
  Section       Symbol      Object
  -------     ---------     ------
   0.B.         _ISR        machine coded interrupt service routines (scheme list of primitives, non-heap)
   0.B.         _RBF        internal READBUFFER (scheme string, non-heap RAM)
   I.D.3.       _maloc      zmaloc: memory allocation primitive
   I.D.3.       _cons       cons:   cons-cell building primitive
   I.D.3.       _list       list:   list building primitive
   I.D.3.       _save       save:   primitive for saving object onto scheme stack (dts)
  II.A.6.6.2.   _IPR        uaripr: uart input port (scheme vector, FLASH or non-heap RAM)
  II.A.6.6.3.   _OPR        uaropr: uart output port (scheme vector, FLASH or non-heap RAM)

 

Extending the System with New Functions:


Adding new functions to the Armpit Scheme built-ins consists of 3 steps: 1) add a primitive with assembly language code for the function, preferably in the Level 2 Addendum (code subsection II.I.); 2) add an external representation symbol for the function, right above the primitive, and; 3) add the function's symbol address to the scmenv: (code subsection II.E). These steps are examplified below for the addition of a function named "revenu" (a kind of "backwards" enumeration used here for simplicity of code, and, hopefully, completely unrelated to the concept of revenue).

The target new function, revenu, takes either a positive integer count and an ending integer or a positive integer count, an integer step and an ending integer as input values, and returns a list of "count" integers that ends with the ending integer, and decrements by the step (or 1 as default). Once the function has been added to the code and the code has been reassembled and uploaded to the MCU, it will be possible to use this new function at top-level and perform operations such as:

30824 armpit> revenu
#primitive
30824 armpit> (revenu 3 7)
(9 8 7)
30824 armpit> (revenu 5 3 2)
(14 11 8 5 2)
30824 armpit> (revenu 4 -10 100)
(70 80 90 100)

Much like the revenu function, the implementation example is easiest to present "backwards", starting with the entry in scmenv: (step 3). The external representation symbol of the function will be placed at label revenu: and thus we add this label to scmenv: in Addendum Level 2 which becomes (the last 2 lines are the new ones):


@---------------------------------
@ ADDENDUM -- level 2
@---------------------------------
@ Addendum: pack/unpack
		.word	packed, punpak,	ppack
@ Addendum: Read Buffer, uart input-port vect (read-only), uart output-port vect (read-only)
		.word	pRBF, pIPR, pOPR
@ Addendum: Revenu (reverse enumeration)
                .word   revenu

Next, we build the symbol for the revenu function. The external representation of revenu is the symbol of 6 characters: r, e, v, e, n, and u and its full symbol tag (including size) is hence #x0643. We install this in the Level 2 Addendum code space, after the last code there which is at label i2cxit: (section II.I.7). The source code at that location becomes (the first 6 lines are from existing code, for orientation):


i2cxit:	@ standard i2c exit
	bl	hwi2cs			@ clear SI, if needed
	@ restore registers and return
	ldmia	sp!, {sv1-sv5}
	clearVicInt
	exitisr

@-------------------------------------------------------------------------------------
@  II.I.8. Revenu:			revenu
@-------------------------------------------------------------------------------------

.balign	4                              @ word alignment for revenu
	
revenu:	@ (revenu count end|{step} {end})
	@ on entry:	sv1 <- count
	@ on entry:	sv2 <- end  or step
	@ on entry:	sv3 <- null or (end)
	@ on exit:	sv1 <- result (list of numbers)
	.word	0x0643			@ number of bytes, symbol tag
	.ascii	"revenu"                @ ASCII chars of the external representation
	.balign 4                       @ word alignment for revenu code (upcoming)

The revenu function will need its argumens to be evaluated and is therefore of variable (rather than syntax) type. This primitive normally takes 2 arguments as input and optionally a third. By specifying it as having 2 input arguments, Armpit Scheme will place its optional 3rd argument as a list in sv3 (null if only 2 arguments are provided). Using the constants specified in section 0.E of the source, the function type and primitive tag with number of inputs can be added to the above code (right below the symbol and alignment) to obtain (2 new lines, at end):


.balign	4                              @ word alignment for revenu
	
revenu:	@ (revenu count end|{step} {end})
	@ on entry:	sv1 <- count
	@ on entry:	sv2 <- end  or step
	@ on entry:	sv3 <- null or (end)
	@ on exit:	sv1 <- result (list of numbers)
	.word	0x0643			@ number of bytes, symbol tag
	.ascii	"revenu"                @ ASCII chars of the external representation
	.balign 4                       @ word alignment for revenu code
	.word	variable_tag            @ revenu is a regular (not syntax) function
	.word	prmtv+0x0200		@ revenu is a primitive with 2 input args (3rd optional)

Finally (step 1), the assembly language code of the function revenu is developed and placed right below the primitive tag (using constants from section 0.E and macros from section 0.G in the source code):


.balign	4                               @ word alignment for revenu
	
revenu:	@ (revenu count end|{step} {end})
	@ on entry:	sv1 <- count
	@ on entry:	sv2 <- end  or step
	@ on entry:	sv3 <- null or (end)
	@ on exit:	sv1 <- result (list of numbers)
	.word	0x0643			@ number of bytes, symbol tag
	.ascii	"revenu"                @ ASCII chars of the external representation
	.balign 4                       @ word alignment for revenu code
	.word	variable_tag            @ revenu is a regular (not syntax) function
	.word	prmtv+0x0200		@ revenu is a primitive with 2 input args (3rd optional)
        @ start of code
	sub	sv5, sv1, #4		@ sv5 <- count - 1	                 (scheme integer) Line  1
	nullp   sv3                     @ was step not specified (i.e. {end} = '()) ?             Line  2
        itEE    eq                      @ If-Then instruction (for Cortex)                        Line  3
        seteq   sv4, #5                 @	if so,  sv4  <- 1 = default step (scheme integer) Line  4
	setne	sv4, sv2		@	if not, sv4  <- step             (scheme integer) Line  5
	carne	sv2, sv3	        @	if not, sv2  <- end              (scheme integer) Line  6
	set     sv1, sv2                @ sv1 <- end, 1st value to cons to result(scheme list)    Line  7
	list    sv2, sv1                @ sv2 <- (end) = initial result          (scheme list)    Line  8
rvnulp:	@ loop over values to cons
        eq	sv5, #i0		@ is count = 0 (done) ?                  (#i0 is 0 int)   Line  9
        beq     rvnuxt		        @       if so,  jump to exit                              Line 10
	int2raw	rva, sv1		@ rva <- latest value consed to result   (raw integer)	  Line 11
	int2raw	rvb, sv4		@ rvb <- step                            (raw integer)    Line 12
	add	rva, rva, rvb	        @ rva <- next value to cons to result    (raw integer)	  Line 13
	raw2int	sv1, rva        	@ sv1 <- next value to cons to result    (scheme integer) Line 14
	cons    sv2, sv1, sv2           @ sv2 <- (... end) == updated result     (scheme list)    Line 15
        sub	sv5, sv5, #4		@ sv5 <- updated count                   (scheme integer) Line 16
	b       rvnulp                  @ jump to add next item                                   Line 17
rvnuxt: @ exit
        set     sv1, sv2                @ sv1 <- result                          (scheme list)    Line 18
        set     pc,  cnt		@ return with result in sv1                               Line 19

The function receives its input arguments (scheme objects) in scheme value registers sv1 to sv3, its environment is in the register env (used, for example, if the function calls eval or bndenv) and its continuation (return address) is in the register cnt. The function code can use registers sv1 to sv5 (gc-ed) to manipulate scheme values and rva to rvc (not gc-ed) to manipulate raw values. Scheme values can be temporarily saved on the scheme stack, dts (gc-ed), if needed, and, if so, that stack needs to be popped back to its entry state prior to returning from the function. When ready to return, the function will need to store its return value in sv1 and will then set the program counter (pc) to its continuation (cnt). If the function needs to call another scheme function to perform its work (eg. eval or apply) it can save its return address (cnt) on the dts prior to that call, then use the macro call (code section 0.G) to call that other scheme function (the macro sets cnt for the appropriate return) and then restore its own continuation from the dts upon return from the call. The revenu function is a simple code example that does not use the env and dts registers and does not call other scheme functions.

In Line 1 of the revenu code, the number of items to cons onto the result list, in additon to the end value, is computed from count (scheme integer in sv1) and stored in sv5 for later use (scheme ints are shifted left by 2 bits relative to raw ints and therefore adding 4 to them is equivalent to adding 1 to a raw int. The same holds for subtraction if numbers are and remain positive or 0). Line 2 tests to see if 2 or 3 input arguments were provided to the function unsing the nullp macro that checks if the content of sv3 is null. The next 4 lines store the end value (scheme integer) in sv2 (for later use) and store the step in sv4 (scheme integer) based on whether 2 or 3 input arguments were provided. The IF-THEN instruction on line 3 is included for compatibility with Cortex cores (unified syntax). In this code, set (seteq, ...) is an alias (macro) for ARM's mov instruction and car (carne ...) is a macro for: ldr reg1, [reg2], which obtains the car of the scheme list in reg2 and stores it in reg1. Line 7 copies the end value to sv1 and Line 8 builds the initial result list, using the list assembler macro, and stores it in sv2. The statement: list sv2, sv1, builds a cons between the contents of sv1 and null, and stores the result in sv2 (Note: the list macro side-effects raw value registers rva to rvc but does not modify sv1-sv5 except the destination register for the list).

The main code loop starts on Line 9 by using eq (alias to ARM's teq) to test whether more integers should be consed to the front of the result list (i.e. if count, stored in sv5 as a scheme integer, is scheme zero = #x01). If no more numbers need to be consed the code jumps to rvnuxt: for function exit. If more numbers are to be consed, the last number added (in sv1) is converted to a raw integer by the macro int2raw and stored in raw value register a (rva) on Line 11. Similarly, the step (in sv4) is converted to a raw integer and stored in raw value register b (rvb) on Line 12. The sum of the last number (rva) and step (rvb) is then stored in rva (raw integer) and then converted back to a scheme integer with the raw2int macro and stored in scheme value register 1 (sv1) on Lines 13 and 14. The sum (in sv1) is consed to the front of the result list (in sv2) using the cons macro and the resulting list is stored back in sv2 on Line 15 (Note: the cons macro side-effects rva-rvc but preserves sv1-sv5 except the destination register for the cons which is updated. This is why raw values in rva and rvb need to be re-computed from sv1 and sv4 at each pass through the loop. Also, one could potentially replace Lines 11-14 with just 2 lines: (1) bic rva, sv4, #int_tag (2) add sv1, sv1, rva). The count of numbers remaining to be consed (in sv5) is decreased by 1 (as scheme int) on Line 16 and the code jumps back to repeat the loop on Line 17.

When the cons-loop is complete the function's result list is in sv2 and the code jumps to rvnuxt:. There, the result is moved to sv1 which is where it needs to be for proper return (Line 18). The function then returns by setting the program counter (pc) to the function's continuation (cnt).



Last updated February 1, 2009

bioe-hubert-at-sourceforge.net