License
+Copyright © 2021 Bdale Garbee and Keith Packard
+This document is released under the terms of the Creative Commons ShareAlike 3.0 License
+1. Overview
+AltOS is a operating system built for a variety of +microcontrollers used in Altus Metrum devices. It has a simple +porting layer for each CPU while providing a convenient +operating enviroment for the developer. AltOS currently +supports three different CPUs:
+-
+
-
+
STM32L series from ST Microelectronics. This ARM Cortex-M3 +based microcontroller offers low power consumption and a +wide variety of built-in peripherals. Altus Metrum uses this +in the TeleMega, MegaDongle and TeleLCO projects.
+
+ -
+
CC1111 from Texas Instruments. This device includes a +fabulous 10mW digital RF transceiver along with an +8051-compatible processor core and a range of +peripherals. This is used in the TeleMetrum, TeleMini, +TeleDongle and TeleFire projects which share the need for a +small microcontroller and an RF interface.
+
+ -
+
ATmega32U4 from Atmel. This 8-bit AVR microcontroller is one +of the many used to create Arduino boards. The 32U4 includes +a USB interface, making it easy to connect to other +computers. Altus Metrum used this in prototypes of the +TeleScience and TelePyro boards; those have been switched to +the STM32L which is more capable and cheaper.
+
+
Among the features of AltOS are:
+-
+
-
+
Multi-tasking. While microcontrollers often don’t +provide separate address spaces, it’s often easier to write +code that operates in separate threads instead of tying +everything into one giant event loop.
+
+ -
+
Non-preemptive. This increases latency for thread +switching but reduces the number of places where context +switching can occur. It also simplifies the operating system +design somewhat. Nothing in the target system (rocket flight +control) has tight timing requirements, and so this seems like +a reasonable compromise.
+
+ -
+
Sleep/wakeup scheduling. Taken directly from ancient +Unix designs, these two provide the fundemental scheduling +primitive within AltOS.
+
+ -
+
Mutexes. As a locking primitive, mutexes are easier to +use than semaphores, at least in my experience.
+
+ -
+
Timers. Tasks can set an alarm which will abort any +pending sleep, allowing operations to time-out instead of +blocking forever.
+
+
The device drivers and other subsystems in AltOS are +conventionally enabled by invoking their _init() function from +the 'main' function before that calls +ao_start_scheduler(). These functions initialize the pin +assignments, add various commands to the command processor and +may add tasks to the scheduler to handle the device. A typical +main program, thus, looks like:
+void +main(void) +{ + ao_clock_init(); - /* Turn on the LED until the system is stable */ - ao_led_init(LEDS_AVAILABLE); - ao_led_on(AO_LED_RED); - ao_timer_init(); - ao_cmd_init(); - ao_usb_init(); - ao_monitor_init(AO_LED_GREEN, TRUE); - ao_rssi_init(AO_LED_RED); - ao_radio_init(); - ao_packet_slave_init(); - ao_packet_master_init(); - #if HAS_DBG - ao_dbg_init(); - #endif - ao_config_init(); - ao_start_scheduler(); - } -
- As you can see, a long sequence of subsystems are initialized - and then the scheduler is started. -
Table of Contents
- The 8051 is a primitive 8-bit processor, designed in the mists - of time in as few transistors as possible. The architecture is - highly irregular and includes several separate memory - spaces. Furthermore, accessing stack variables is slow, and the - stack itself is of limited size. While SDCC papers over the - instruction set, it is not completely able to hide the memory - architecture from the application designer. -
- The __data/__xdata/__code memory spaces below were completely - separate in the original 8051 design. In the cc1111, this - isn't true—they all live in a single unified 64kB address - space, and so it's possible to convert any address into a - unique 16-bit address. SDCC doesn't know this, and so a - 'global' address to SDCC consumes 3 bytes of memory, 1 byte as - a tag indicating the memory space and 2 bytes of offset within - that space. AltOS avoids these 3-byte addresses as much as - possible; using them involves a function call per byte - access. The result is that nearly every variable declaration - is decorated with a memory space identifier which clutters the - code but makes the resulting code far smaller and more - efficient. -
- The 8051 can directly address these 128 bytes of - memory. This makes them precious so they should be - reserved for frequently addressed values. Oh, just to - confuse things further, the 8 general registers in the - CPU are actually stored in this memory space. There are - magic instructions to 'bank switch' among 4 banks of - these registers located at 0x00 - 0x1F. AltOS uses only - the first bank at 0x00 - 0x07, leaving the other 24 - bytes available for other data. -
- There are an additional 128 bytes of internal memory - that share the same address space as __data but which - cannot be directly addressed. The stack normally - occupies this space and so AltOS doesn't place any - static storage here. -
- This is additional general memory accessed through a - single 16-bit address register. The CC1111F32 has 32kB - of memory available here. Most program data should live - in this memory space. -
- This is an alias for the first 256 bytes of __xdata - memory, but uses a shorter addressing mode with - single global 8-bit value for the high 8 bits of the - address and any of several 8-bit registers for the low 8 - bits. AltOS uses a few bits of this memory, it should - probably use more. -
- All executable code must live in this address space, but - you can stick read-only data here too. It is addressed - using the 16-bit address register and special 'code' - access opcodes. Anything read-only should live in this space. -
- The 8051 has 128 bits of bit-addressible memory that - lives in the __data segment from 0x20 through - 0x2f. Special instructions access these bits - in a single atomic operation. This isn't so much a - separate address space as a special addressing mode for - a few bytes in the __data segment. -
- Because stack addressing is expensive, and stack space - limited, the default function call declaration in SDCC - allocates all parameters and local variables in static global - memory. Just like fortran. This makes these functions - non-reentrant, and also consume space for parameters and - locals even when they are not running. The benefit is smaller - code and faster execution. -
- All functions which are re-entrant, either due to recursion - or due to a potential context switch while executing, should - be marked as __reentrant so that their parameters and local - variables get allocated on the stack. This ensures that - these values are not overwritten by another invocation of - the function. -
- Functions which use significant amounts of space for - arguments and/or local variables and which are not often - invoked can also be marked as __reentrant. The resulting - code will be larger, but the savings in memory are - frequently worthwhile. -
- All parameters and locals in non-reentrant functions can - have data space decoration so that they are allocated in - __xdata, __pdata or __data space as desired. This can avoid - consuming __data space for infrequently used variables in - frequently used functions. -
- All library functions called by SDCC, including functions - for multiplying and dividing large data types, are - non-reentrant. Because of this, interrupt handlers must not - invoke any library functions, including the multiply and - divide code. -
- Interrupt functions are declared with with an __interrupt - decoration that includes the interrupt number. SDCC saves - and restores all of the registers in these functions and - uses the 'reti' instruction at the end so that they operate - as stand-alone interrupt handlers. Interrupt functions may - call the ao_wakeup function to wake AltOS tasks. -
- SDCC has built-in support for suspending interrupts during - critical code. Functions marked as __critical will have - interrupts suspended for the whole period of - execution. Individual statements may also be marked as - __critical which blocks interrupts during the execution of - that statement. Keeping critical sections as short as - possible is key to ensuring that interrupts are handled as - quickly as possible. -
Table of Contents
- This chapter documents how to create, destroy and schedule AltOS tasks. -
- void - ao_add_task(__xdata struct ao_task * task, - void (*start)(void), - __code char *name); -
- This initializes the statically allocated task structure, - assigns a name to it (not used for anything but the task - display), and the start address. It does not switch to the - new task. 'start' must not ever return; there is no place - to return to. -
- void - ao_sleep(__xdata void *wchan) -
- This suspends the current task until 'wchan' is signaled - by ao_wakeup, or until the timeout, set by ao_alarm, - fires. If 'wchan' is signaled, ao_sleep returns 0, otherwise - it returns 1. This is the only way to switch to another task. -
- Because ao_wakeup wakes every task waiting on a particular - location, ao_sleep should be used in a loop that first - checks the desired condition, blocks in ao_sleep and then - rechecks until the condition is satisfied. If the - location may be signaled from an interrupt handler, the - code will need to block interrupts by using the __critical - label around the block of code. Here's a complete example: -
- __critical while (!ao_radio_done) - ao_sleep(&ao_radio_done); -
-
- void - ao_wakeup(__xdata void *wchan) -
- Wake all tasks blocked on 'wchan'. This makes them - available to be run again, but does not actually switch - to another task. Here's an example of using this: -
- if (RFIF & RFIF_IM_DONE) { - ao_radio_done = 1; - ao_wakeup(&ao_radio_done); - RFIF &= ~RFIF_IM_DONE; - } -
- Note that this need not be enclosed in __critical as the - ao_sleep block can only be run from normal mode, and so - this sequence can never be interrupted with execution of - the other sequence. -
- void - ao_alarm(uint16_t delay) -
- Schedules an alarm to fire in at least 'delay' ticks. If - the task is asleep when the alarm fires, it will wakeup - and ao_sleep will return 1. -
- ao_alarm(ao_packet_master_delay); - __critical while (!ao_radio_dma_done) - if (ao_sleep(&ao_radio_dma_done) != 0) - ao_radio_abort(); -
- In this example, a timeout is set before waiting for - incoming radio data. If no data is received before the - timeout fires, ao_sleep will return 1 and then this code - will abort the radio receive operation. -
- void - ao_start_scheduler(void) -
- This is called from 'main' when the system is all - initialized and ready to run. It will not return. -
- void - ao_clock_init(void) -
- This turns on the external 48MHz clock then switches the - hardware to using it. This is required by many of the - internal devices like USB. It should be called by the - 'main' function first, before initializing any of the - other devices in the system. -
Table of Contents
- AltOS sets up one of the cc1111 timers to run at 100Hz and - exposes this tick as the fundemental unit of time. At each - interrupt, AltOS increments the counter, and schedules any tasks - waiting for that time to pass, then fires off the ADC system to - collect current data readings. Doing this from the ISR ensures - that the ADC values are sampled at a regular rate, independent - of any scheduling jitter. -
- uint16_t - ao_time(void) -
- Returns the current system tick count. Note that this is - only a 16 bit value, and so it wraps every 655.36 seconds. -
- void - ao_delay(uint16_t ticks); -
- Suspend the current task for at least 'ticks' clock units. -
- void - ao_timer_set_adc_interval(uint8_t interval); -
- This sets the number of ticks between ADC samples. If set - to 0, no ADC samples are generated. AltOS uses this to - slow down the ADC sampling rate to save power. -
Table of Contents
- AltOS provides mutexes as a basic synchronization primitive. Each - mutexes is simply a byte of memory which holds 0 when the mutex - is free or the task id of the owning task when the mutex is - owned. Mutex calls are checked—attempting to acquire a mutex - already held by the current task or releasing a mutex not held - by the current task will both cause a panic. -
- void - ao_mutex_get(__xdata uint8_t *mutex); -
- Acquires the specified mutex, blocking if the mutex is - owned by another task. -
Table of Contents
- The CC1111 contains a useful bit of extra hardware in the form - of five programmable DMA engines. They can be configured to copy - data in memory, or between memory and devices (or even between - two devices). AltOS exposes a general interface to this hardware - and uses it to handle radio and SPI data. -
- Code using a DMA engine should allocate one at startup - time. There is no provision to free them, and if you run out, - AltOS will simply panic. -
- During operation, the DMA engine is initialized with the - transfer parameters. Then it is started, at which point it - awaits a suitable event to start copying data. When copying data - from hardware to memory, that trigger event is supplied by the - hardware device. When copying data from memory to hardware, the - transfer is usually initiated by software. -
- uint8_t - ao_dma_alloc(__xdata uint8_t *done) -
- Allocates a DMA engine, returning the identifier. Whenever - this DMA engine completes a transfer. 'done' is cleared - when the DMA is started, and then receives the - AO_DMA_DONE bit on a successful transfer or the - AO_DMA_ABORTED bit if ao_dma_abort was called. Note that - it is possible to get both bits if the transfer was - aborted after it had finished. -
- void - ao_dma_set_transfer(uint8_t id, - void __xdata *srcaddr, - void __xdata *dstaddr, - uint16_t count, - uint8_t cfg0, - uint8_t cfg1) -
- Initializes the specified dma engine to copy data - from 'srcaddr' to 'dstaddr' for 'count' units. cfg0 and - cfg1 are values directly out of the CC1111 documentation - and tell the DMA engine what the transfer unit size, - direction and step are. -
- void - ao_dma_start(uint8_t id); -
- Arm the specified DMA engine and await a signal from - either hardware or software to start transferring data. -
- void - ao_dma_trigger(uint8_t id) -
- Trigger the specified DMA engine to start copying data. -
Table of Contents
- AltOS offers a stdio interface over both USB and the RF packet - link. This provides for control of the device localy or - remotely. This is hooked up to the stdio functions in SDCC by - providing the standard putchar/getchar/flush functions. These - automatically multiplex the two available communication - channels; output is always delivered to the channel which - provided the most recent input. -
- void - putchar(char c) -
- Delivers a single character to the current console - device. -
- char - getchar(void) -
- Reads a single character from any of the available - console devices. The current console device is set to - that which delivered this character. This blocks until - a character is available. -
- void - flush(void) -
- Flushes the current console device output buffer. Any - pending characters will be delivered to the target device. - xo
- void - ao_add_stdio(char (*pollchar)(void), - void (*putchar)(char), - void (*flush)(void)) -
- This adds another console device to the available - list. -
- 'pollchar' returns either an available character or - AO_READ_AGAIN if none is available. Significantly, it does - not block. The device driver must set 'ao_stdin_ready' to - 1 and call ao_wakeup(&ao_stdin_ready) when it receives - input to tell getchar that more data is available, at - which point 'pollchar' will be called again. -
- 'putchar' queues a character for output, flushing if the output buffer is - full. It may block in this case. -
- 'flush' forces the output buffer to be flushed. It may - block until the buffer is delivered, but it is not - required to do so. -
Table of Contents
- AltOS includes a simple command line parser which is hooked up - to the stdio interfaces permitting remote control of the device - over USB or the RF link as desired. Each command uses a single - character to invoke it, the remaining characters on the line are - available as parameters to the command. -
- void - ao_cmd_register(__code struct ao_cmds *cmds) -
- This registers a set of commands with the command - parser. There is a fixed limit on the number of command - sets, the system will panic if too many are registered. - Each command is defined by a struct ao_cmds entry: -
- struct ao_cmds { - char cmd; - void (*func)(void); - const char *help; - }; -
- 'cmd' is the character naming the command. 'func' is the - function to invoke and 'help' is a string displayed by the - '?' command. Syntax errors found while executing 'func' - should be indicated by modifying the global ao_cmd_status - variable with one of the following values: -
- The command was parsed successfully. There is no - need to assign this value, it is the default. -
- A token in the line was invalid, such as a number - containing invalid characters. The low-level - lexing functions already assign this value as needed. -
- The command line is invalid for some reason other - than invalid tokens. -
-
- void - ao_cmd_lex(void); -
- This gets the next character out of the command line - buffer and sticks it into ao_cmd_lex_c. At the end of the - line, ao_cmd_lex_c will get a newline ('\n') character. -
- void - ao_cmd_white(void) -
- This skips whitespace by calling ao_cmd_lex while - ao_cmd_lex_c is either a space or tab. It does not skip - any characters if ao_cmd_lex_c already non-white. -
- void - ao_cmd_hex(void) -
- This reads a 16-bit hexadecimal value from the command - line with optional leading whitespace. The resulting value - is stored in ao_cmd_lex_i; -
- void - ao_cmd_decimal(void) -
- This reads a 32-bit decimal value from the command - line with optional leading whitespace. The resulting value - is stored in ao_cmd_lex_u32 and the low 16 bits are stored - in ao_cmd_lex_i; -
- uint8_t - ao_match_word(__code char *word) -
- This checks to make sure that 'word' occurs on the command - line. It does not skip leading white space. If 'word' is - found, then 1 is returned. Otherwise, ao_cmd_status is set to - ao_cmd_syntax_error and 0 is returned. -
Table of Contents
- The CC1111 contains a full-speed USB target device. It can be - programmed to offer any kind of USB target, but to simplify - interactions with a variety of operating systems, AltOS provides - only a single target device profile, that of a USB modem which - has native drivers for Linux, Windows and Mac OS X. It would be - easy to change the code to provide an alternate target device if - necessary. -
- To the rest of the system, the USB device looks like a simple - two-way byte stream. It can be hooked into the command line - interface if desired, offering control of the device over the - USB link. Alternatively, the functions can be accessed directly - to provide for USB-specific I/O. -
- void - ao_usb_flush(void); -
- Flushes any pending USB output. This queues an 'IN' packet - to be delivered to the USB host if there is pending data, - or if the last IN packet was full to indicate to the host - that there isn't any more pending data available. -
- void - ao_usb_putchar(char c); -
- If there is a pending 'IN' packet awaiting delivery to the - host, this blocks until that has been fetched. Then, this - adds a byte to the pending IN packet for delivery to the - USB host. If the USB packet is full, this queues the 'IN' - packet for delivery. -
- char - ao_usb_pollchar(void); -
- If there are no characters remaining in the last 'OUT' - packet received, this returns AO_READ_AGAIN. Otherwise, it - returns the next character, reporting to the host that it - is ready for more data when the last character is gone. -
- char - ao_usb_getchar(void); -
- This uses ao_pollchar to receive the next character, - blocking while ao_pollchar returns AO_READ_AGAIN. -
- void - ao_usb_disable(void); -
- This turns off the USB controller. It will no longer - respond to host requests, nor return characters. Calling - any of the i/o routines while the USB device is disabled - is undefined, and likely to break things. Disabling the - USB device when not needed saves power. -
- Note that neither TeleDongle nor TeleMetrum are able to - signal to the USB host that they have disconnected, so - after disabling the USB device, it's likely that the cable - will need to be disconnected and reconnected before it - will work again. -
- void - ao_usb_enable(void); -
- This turns the USB controller on again after it has been - disabled. See the note above about needing to physically - remove and re-insert the cable to get the host to - re-initialize the USB link. -
Table of Contents
- The CC1111 provides two USART peripherals. AltOS uses one for - asynch serial data, generally to communicate with a GPS device, - and the other for a SPI bus. The UART is configured to operate - in 8-bits, no parity, 1 stop bit framing. The default - configuration has clock settings for 4800, 9600 and 57600 baud - operation. Additional speeds can be added by computing - appropriate clock values. -
- To prevent loss of data, AltOS provides receive and transmit - fifos of 32 characters each. -
- char - ao_serial_getchar(void); -
- Returns the next character from the receive fifo, blocking - until a character is received if the fifo is empty. -
- void - ao_serial_putchar(char c); -
- Adds a character to the transmit fifo, blocking if the - fifo is full. Starts transmitting characters. -
- void - ao_serial_drain(void); -
- Blocks until the transmit fifo is empty. Used internally - when changing serial speeds. -
- void - ao_serial_set_speed(uint8_t speed); -
- Changes the serial baud rate to one of - AO_SERIAL_SPEED_4800, AO_SERIAL_SPEED_9600 or - AO_SERIAL_SPEED_57600. This first flushes the transmit - fifo using ao_serial_drain. -
Table of Contents
- 1. ao_radio_set_telemetry
- 2. ao_radio_set_packet
- 3. ao_radio_set_rdf
- 4. ao_radio_idle
- 5. ao_radio_get
- 6. ao_radio_put
- 7. ao_radio_abort
- 8. ao_radio_send
- 9. ao_radio_recv
- 10. ao_radio_rdf
- 11. ao_packet_putchar
- 12. ao_packet_pollchar
- 13. ao_packet_slave_start
- 14. ao_packet_slave_stop
- 15. ao_packet_slave_init
- 16. ao_packet_master_init
- The CC1111 radio transceiver sends and receives digital packets - with forward error correction and detection. The AltOS driver is - fairly specific to the needs of the TeleMetrum and TeleDongle - devices, using it for other tasks may require customization of - the driver itself. There are three basic modes of operation: -
- Telemetry mode. In this mode, TeleMetrum transmits telemetry - frames at a fixed rate. The frames are of fixed size. This - is strictly a one-way communication from TeleMetrum to - TeleDongle. -
- Packet mode. In this mode, the radio is used to create a - reliable duplex byte stream between TeleDongle and - TeleMetrum. This is an asymmetrical protocol with - TeleMetrum only transmitting in response to a packet sent - from TeleDongle. Thus getting data from TeleMetrum to - TeleDongle requires polling. The polling rate is adaptive, - when no data has been received for a while, the rate slows - down. The packets are checked at both ends and invalid - data are ignored. -
- On the TeleMetrum side, the packet link is hooked into the - stdio mechanism, providing an alternate data path for the - command processor. It is enabled when the unit boots up in - 'idle' mode. -
- On the TeleDongle side, the packet link is enabled with a - command; data from the stdio package is forwarded over the - packet link providing a connection from the USB command - stream to the remote TeleMetrum device. -
- Radio Direction Finding mode. In this mode, TeleMetrum - constructs a special packet that sounds like an audio tone - when received by a conventional narrow-band FM - receiver. This is designed to provide a beacon to track - the device when other location mechanisms fail. -
-
- void - ao_radio_set_telemetry(void); -
- Configures the radio to send or receive telemetry - packets. This includes packet length, modulation scheme and - other RF parameters. It does not include the base frequency - or channel though. Those are set at the time of transmission - or reception, in case the values are changed by the user. -
- void - ao_radio_set_packet(void); -
- Configures the radio to send or receive packet data. This - includes packet length, modulation scheme and other RF - parameters. It does not include the base frequency or - channel though. Those are set at the time of transmission or - reception, in case the values are changed by the user. -
- void - ao_radio_set_rdf(void); -
- Configures the radio to send RDF 'packets'. An RDF 'packet' - is a sequence of hex 0x55 bytes sent at a base bit rate of - 2kbps using a 5kHz deviation. All of the error correction - and data whitening logic is turned off so that the resulting - modulation is received as a 1kHz tone by a conventional 70cm - FM audio receiver. -
- void - ao_radio_idle(void); -
- Sets the radio device to idle mode, waiting until it reaches - that state. This will terminate any in-progress transmit or - receive operation. -
- void - ao_radio_get(void); -
- Acquires the radio mutex and then configures the radio - frequency using the global radio calibration and channel - values. -
- void - ao_radio_abort(void); -
- Aborts any transmission or reception process by aborting the - associated DMA object and calling ao_radio_idle to terminate - the radio operation. -
- In telemetry mode, you can send or receive a telemetry - packet. The data from receiving a packet also includes the RSSI - and status values supplied by the receiver. These are added - after the telemetry data. -
- void - ao_radio_send(__xdata struct ao_telemetry *telemetry); -
- This sends the specific telemetry packet, waiting for the - transmission to complete. The radio must have been set to - telemetry mode. This function calls ao_radio_get() before - sending, and ao_radio_put() afterwards, to correctly - serialize access to the radio device. -
- void - ao_radio_recv(__xdata struct ao_radio_recv *radio); -
- This blocks waiting for a telemetry packet to be received. - The radio must have been set to telemetry mode. This - function calls ao_radio_get() before receiving, and - ao_radio_put() afterwards, to correctly serialize access - to the radio device. This returns non-zero if a packet was - received, or zero if the operation was aborted (from some - other task calling ao_radio_abort()). -
- In radio direction finding mode, there's just one function to - use -
- void - ao_radio_rdf(int ms); -
- This sends an RDF packet lasting for the specified amount - of time. The maximum length is 1020 ms. -
- Packet mode is asymmetrical and is configured at compile time - for either master or slave mode (but not both). The basic I/O - functions look the same at both ends, but the internals are - different, along with the initialization steps. -
- void - ao_packet_putchar(char c); -
- If the output queue is full, this first blocks waiting for - that data to be delivered. Then, queues a character for - packet transmission. On the master side, this will - transmit a packet if the output buffer is full. On the - slave side, any pending data will be sent the next time - the master polls for data. -
- char - ao_packet_pollchar(void); -
- This returns a pending input character if available, - otherwise returns AO_READ_AGAIN. On the master side, if - this empties the buffer, it triggers a poll for more data. -
- void - ao_packet_slave_start(void); -
- This is available only on the slave side and starts a task - to listen for packet data. -
- void - ao_packet_slave_stop(void); -
- Disables the packet slave task, stopping the radio receiver. -
- void - ao_packet_slave_init(void); -
- Adds the packet stdio functions to the stdio package so - that when packet slave mode is enabled, characters will - get send and received through the stdio functions. -
As you can see, a long sequence of subsystems are initialized +and then the scheduler is started.
+