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:).
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).
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.
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, #0x02to 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).
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 ...
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.
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)
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).