firmware
v0.1.2
Chromation Spectrometer Dev-Kit
|
All the useful firmware information is under Files. This browses the source code.
The firmware is divided into folders:
Each folder has src
(source code) and test
(unit tests).
The first thing C programmers will notice is that the lib
source code does not follow convention:
lib
folder are inline
.h
files.c
filesThis is for performance reasons.
The firmware runs on the Chromation dev-kit. Documentation for the dev-kit is here:
https://microspectrometer.github.io/dev-kit-2020/
All dev-kit hardware and firmware files are open-source and are available from the GitHub repository:
https://github.com/microspectrometer/dev-kit-2020
The dev-kit has two ATmega328 microcontrollers and a spectrometer chip.
The firmware is for the Flash memory in those two microcontrollers:
vis-spi-out
PCB handles spectrometer configuration and readoutusb-bridge
PCB simply passes messages between the application on the host computer (e.g., a Python script) and the 5-wire SPI interface on the vis-spi-out
PCBJump to the main()
.c
file for the two microcontrollers:
There is no firmware for the spectrometer chip itself, but the chip does have programmable registers.
The programmable registers are part of the LIS-770i, the CMOS image sensor used in the spectrometer chip. These registers initialize in an unknown state and must be written to after the spectrometer chip is powered on.
All registers are programmed at once by sending the LIS-770i a 28-bit word. The value of this 28-bit word determines:
See details in Lis.h.
The dev-kit firmware writes these registers with recommended default values when the dev-kit is powered on. The recommended defaults are to turn on pixel binning, set gain to 1x, and turn on all pixels.
The microspec Python API includes command setSensorConfig()
for applications to write to these registers, but most applications do not require changing these register values:
The serial communication protocol is defined in the JSON file microspec.json:
class
that is auto-generated from microspec.json.Install Python and the microspec package to develop and test the firmware:
TODO: Add an example with the usb-bridge
and vis-spi-out
firmware that corresponds to one of the commands in the JSON file, and how the resulting call-and-response looks from Python.
The library code started out in the conventional way:
.c
files.h
filesThe code was "clean" – no macros, lots of seams. And it had almost 100% test coverage.
Unfortunately, this style of programming is not compatible with producing optimal assembly instructions for the ATmega328. It results in system-breaking speed penalties.
The ATmega328 microcontroller clock is only 10MHz: 10 cycles worth of instructions takes a microsecond. At this clock speed, inefficient assembly code causes noticeable slow-downs and makes critical timing impossible, resulting in bad behavior that is sometimes obvious and sometimes subtle.
The avr-gcc compiler was generating inefficient assembly code for three reasons:
The last item is "easy" to fix: modify the ISR code, count cycles in the generated assembly, repeat until the time consumed by the ISR is acceptable.
But the first two items are not easy to fix. One approach is to take the carefully segregated code base and mush it back together:
Compared with that, a small break with convention is OK.
To use the fast instructions, the compiler must know register addresses at compile-time. Object files generated without these values are forced to use the much slower, generic load and store instructions.
How do I know what instructions should be used? I manually look up the address of the register and figure it out. I also wrote some shortcuts to cut and paste disassembly into the code.
That explains why the code is full of comments like this:
In more complicated cases, there are several lines of assembly.
Pasting the disassembly into the code helped me check that the compiler was still using the right instructions after a code change. This is how I determined the correct steps to make inline
actually inline the code.
The simplest example is the bit manipulation functions discussed below. The same reasoning applies to all the library code.
The ReadWriteBits library defines bit manipulation functions (for readability and testability). In the conventional approach, this source file is compiled without knowing the register addresses that client code will pass to its functions. But the register address determines which instructions are allowed! Without knowing the address, the compiler has to use the most generic instruction.
The bit manipulation function might be passed an I/O register in the range 0x00 - 0x1F
. These registers allow fast instructions like sbi
, cbi
, etc.
Or the bit manipulation function might be called on an I/O register in the range 0x20 - 0x3F
. In this range the bit manipulation has to be done with entire bytes using instructions in
and out
.
Or the bit manipulation might be performed on dataspace, 0x40 - 0x5F
, or extended I/O space, 0x60 - 0xFF
in SRAM, in which case instructions like ld
and st
are used.
Again, the compiler does not know what register address the function is called with, so it has to use the generic ld
and st
instructions. The result is that calls to the bit manipulation library take a massive speed penalty.
To avoid this, the inline
copies the bit manipulation source code into all the places it is called.
Now the code is part of the compilation unit that has the register addresses and the compiler knows if it can use the faster instructions. In addition to using faster instructions, the function call overhead is eliminated. And the code still retains its readability and reusability.
In order for the compiler to inline
, the function body must be visible in the compilation unit, therefore the function body goes in the header.
The result is a glaring break with C convention. Most (all?) of the library functions are defined in .h
files, with the function signatures declared in the .c
file.
The only practical drawback to this approach is that it is incompatible with using function pointers to replace functions with fakes for unit tests.
Instead of function pointers, the code gets marked up with test macros to handle the function faking. This comes up when:
In this scenario, the definition for the function-under-test gets bracketed with #ifdef USE_FAKES
statements to #define
and #undef
the fakes like this: