This is James' Laboratory Notebook, which aims to be a collection of all of my thoughts on the things that I am working on, researching, thinking about, etc.

It is meant to be primarily useful for me to organize my thoughts, and potentially as reference material for sharing my notes with other people.

If you would like a more "stream of consciousness" style view of what I am working on, consider following me on twitter.

Pull requests that add relevant details are welcome, pull requests that restructure, rearrange, or add details that aren't arbitrarily considered useful to me will not be accepted.

This document is tracked on GitHub in this repository.

This document was inspired by whitequark's Lab Notebook, though is structured on a topic basis, rather than on a chronological basis. Chronological notes can be found in the Notes section.


This document is licensed under a CC BY-SA 4.0 license.


These are projects of mine that I am working on or have worked on. They may be software based, hardware based, or a combination of both.

Personal projects are rarely "done", but they occasionally reach new levels of usefulness.

Unless otherwise specified, any software for these projects is written in Rust.

This section serves as a combination of "showcase" and "current status", depending on the state of the project.

All items in this section are either "work in progress" or "complete for now". For project ideas that are still in the concept phase, see the Ideas Section.

Embedded - The Missing Parts

"Embedded - The Missing Parts" is a work-in-progress book that aims to track all of the concepts I believe are important in "real world" embedded engineering projects, but are not necessarily taught as part of a university education, or could be easy to miss when self-taught.

It started as a Twitter Thread, and is tracked online at The project uses mdBook, which also powers this lab notebook. It spawned from a table of contents written with other engineers from the embedded industry.

LiPo Stamp

LiPo Stamp Panel

The LiPo Stamp is a small sized, general purpose LiPo charge controller with power path and a 3v3 regulator. Currently the targeted size is 0.7" by 0.7", or just under 18mm square. It is intended to be used with single cell 18650, 21700, pack, or similar batteries.

I intend to use this in projects such as Kuma's Collar.


LiPo Stamp Render

  • TP4056 Charge Controller
    • 500mA charging current
    • Low voltage trickle charging
  • Over/Under-voltage Protection
    • AP9101CK6 + FS8205
    • Disables battery on over/under charge
  • Power Path
    • Provides power via USB when connected
    • 500mA max output (USB or Battery)
  • Switchable 3v3 Regulator
    • Default off
    • 100mA+ output
  • Resettable Polyfuse
    • For high current, direct battery applications (e.g. LEDs)
    • Max 2A continuous output

The main project page for the LiPo Stamp is hosted on GitHub.


Test Setup drawing

Implementation of the automated tester is currently going on in this pull request

I plan to build a hardware in the loop test rig for the LiPo stamp. It will probably be a proto-version of the KTA, though probably not strictly at the start.

Unsorted test brainstorming

I think I will need the following equipment/setup items for the test cases.

  • A LiPo Stamp
    • Mounted to a breakout board for easy wiring
    • Maybe with headers or screw terminals
  • A KTA
    • This will "Run" the test
    • Also use some ADCs to measure voltage
    • Also look control GPIO input/outputs for LiPo stamp
  • A 5v supply
    • This only needs to provide 500mA for charging, so USB might be suitable if it can power everything
    • Otherwise use an external AC/DC supply
  • INA219 breakouts
    • Measure battery input/output
    • Measure 5v charging line
  • Relays to connect/disconnect:
    • 5v charging source
    • Battery connection (high side)
    • LED connection (high side)
  • WS2812B strip/panel for programmable load
  • Some kind of display for monitoring output?
  • Protected 18650 cell
    • Probably NCR-18650B, rated to 5A discharge, 2.5v cutout
    • 3.6v nominal
    • PDF Datasheet
  • Unprotected 18650/21700 cell
  • I might need some kind of custom holder for protected battery cells (if they don't fit the holders I have)

I think I will need at least the following test cases for the board:

  1. No battery, 5v present
    1. Check vmax output
    2. Check 3v3 output (disabled)
    3. Check 3v3 output (enabled)
  2. Protected cell
    1. 5v connected
      1. Check vmax output
      2. Check 3v3 output (disabled)
      3. Check 3v3 output (enabled)
      4. Verify charging
      5. Wait for charge complete
      6. Verify charging complete (no charging)
      7. Verify LEDs (manual)
    2. 5v disconnected
      1. Check vmax output
      2. Check 3v3 output (disabled)
      3. Check 3v3 output (enabled)
      4. Begin discharge pattern
      5. Verify cutout at low voltage
      6. Verify LEDs (manual)
  3. Polyfuse test
    1. Charge all the way
    2. Discharge at steps
      • 500mA
      • 1000mA
      • 1500mA
      • 1900mA
    3. Verify discharge okay, no cutout
    4. Push discharge above levels
      • 2000mA
      • 2100mA
    5. Ensure cutout occurs after XX seconds
    6. Disconnect load
    7. Ensure board restores after YY seconds
  4. Unprotected Cell
    • Probably repeat steps above to verify cutout happens because of LiPo Stamp protections, not protected battery
    • How to generate load at voltages below the cut-out voltage? We probably need to go as low as 2.5v to verify cut-out.

Test board pinout

  • I2C for the INA boards and Display?
    • SCL
      • PORTB-06
    • SDA
      • PORTB-07
  • Analog Inputs
    • Battery+ (might be available on INA219?)
      • ???
    • 5v Source
      • Also VBUS for the test system?
        • Maybe not? Going through a regulator?
      • ???
    • 3v3 Reg Output
      • ???
    • VMax Output (May be greater than 5v!)
      • Voltage Divider?
      • ???
  • Digital Outputs
    • 3v3-EN
      • ???
    • RELAY: 5v Input NO
      • ???
    • RELAY: Battery Input NO
      • ???
    • RELAY: WS2812 Panel Connection NO
      • ???
  • SPI output
    • WS2812b Panel
      • PORTB-08

M60 Keyboard

part image

TODO: Picture of my keyboard with caps and a case

I am building a mechanical keyboard, based on the makerdiary m60. This PCB is based on an nRF52840 m.2 SOM. It is a 60% keyboard, e.g. no numpad, arrow pad, PgUp, PgDn, etc.

Right now the code for this only exists as part of the Anachro project, and only outputs using the Anachro protocol. USB and BT support has not been written.



I intend to use this in multiple ways:

  • As a USB-C keyboard for my PC
  • As a Bluetooth keyboard for my PC or Tablet
  • As a wired keyboard for Anachro-PC
    • This will use the debugging header on the PCB

Knurling Test Adapter

Note: Add picture or render of the breakout board

This is intended to be used as part of Hardware in the Loop testing. The idea is the ability to write host-based "integration tests" that exercise the behavior of a Unit Under Test (UUT) by mocking the outside world, or serving as a data Sink or Source for instrumentation commands or logging.

See HIL Testing Theory for more design and concept details.

It is currently a breakout board for the [Black Pill], based on the STM32F411. The breakout board adds:

  • Dual RS-485 channels
  • RJ-45 connectors for daisy-chaining power and RS-485
  • A WS2812B LED for status
  • PMOD breakouts for:
    • GPIO
    • I2C
    • SPI
    • UARTs

In the future, the hope is to donate this project to the Knurling-rs project.

Kuma Collar

TODO: Picture or render of the collar

TODO: Picture of Kuma

Kuma is my dog. He is a corgi. For walks at night, I would like to have an LED collar for him.

To make this happen, this will involve:

  • A WS2812b strip for LEDs, capable of up to 1A of output
  • A 18650 or 21700 battery cell, providing 3Ah-5Ah of power
  • An nRF52840 MCU for control and communications, probably an MS88SF2 module
  • A LiPo stamp for battery management and charging
  • A custom PCB to hold all the parts
  • A waterproof case of some kind, possibly cut acrylic
  • Aviation connectors for:
    • USB data + power (4 pins)
    • WS2812b strip connection (3 pins)

Home Fleet

TODO: More details here

Home fleet is my project to build a home sensor network entirely from scratch.

See the Home Fleet Repo for more information.

Home fleet uses the Anachro Protocol over the ESB wireless protocol.


Anachro is an umbrella projects that consists of two main things:

  • A flexible and lightweight communication protocol designed to work over a variety of transports (e.g. wireless, UART, RS-485, SPI)
  • A microcontroller based PC architecture

See the Anachro Docs and the Anachro Repo for more details.

You can also watch a talk I gave about Anachro at RustLab 2020.


BBQueue is a queue that aims to be a lock-free, performant, and misuse-resistant library for use with DMA on embedded systems. It is loosely based on BipBuffers, as written by Simon Cooke.

In the near future, it will be rewritten to use the const-generic features that are stabilizing in Rust 1.50. This will mark the 1.0.0 release of bbqueue.

Useful links:


bitpool is intended to be a lightweight and lock-free allocator for embedded systems.

It aims to support all of the following use cases:

  • As a global static memory pool (without language support)
  • As an implementor of the global_alloc, allowing for use with the alloc crate
  • As an implementor of whatever future "local allocator" traits are provided, allowing it to be used as a custom allocator for collections

bitpool uses a 32-bit word as a tree of booleans which express the ability to break a page into blocks ranging up to 5 power-of-two allocations.

For example, when using a 1024 byte "page", you could have any combination of the following blocks allocatable:

  • 1x 1024 byte block
  • 2x 512 byte blocks
  • 4x 256 byte blocks
  • 8x 128 byte blocks
  • 16x 64 byte blocks

Because a bit is used for each "tier" of blocks, this uses 31 bits of metadata per block (1 + 2 + 4 + 8 + 16) = 31. This allows for atomic CAS operations on each block metadata word.

In the future, it should be possible for users to choose any power-of-two as the page size, meaning you could have two (or more) pools that cover a wider range, e.g.:

  • Pool 1 (small): 1024-64 byte allocations
  • Pool 2 (medium): 16KiB-2KiB allocations
  • Pool 3 (large): 512KiB-32KiB allocations

See the github repo for more information.

Brick Mount

Brick Mount Boards

Interested in the Brick Mount Boards? Do me a favor and take my survey!

"Brick Mount" is a project to use plastic building bricks as a fundamental basis for laying out circuit boards and modules while prototyping.

This is achieved by designing boards that have holes compatible with the "pegs" of a brick, e.g. 4.8mm pegs with a spacing of 8.0mm. With cheap board design services like OSHPark or JLCPCB, it is relatively straightforward to design these holes onto breakout boards or other assemblies.

This allows for arrangement of components into pretty much any 3d structure, and even prototyping (or shipping!) enclosures using plastic bricks directly.

This idea is not original (and has likely been done many times before), but I was inspired by LOOKMUMNOCOMPUTER's Kosmo Minis parts, which use exactly this scheme.

I have collected KiCAD footprints as well as the boards I have designed so far in this GitHub repo. At the moment, I have not yet been able to verify the footprints or boards (I'm waiting for boards to be delivered).


When working on projects consisting of multiple boards, it is useful to group them onto a single physical medium, so they don't turn into a web of connectors, to relieve cable strain when moving or storing them, and to make it easy for them to "travel together".

It could be possible to use a breadboard (or three) for this purpose, but I haven't gotten into the habit of having a stack of breadboards around, and they always seem finnicky to me. Perma-proto boards are also an option, but then I still need to mount them to something, and it can be difficult to rework.

Over the past months, I've used a variety of methods, including cardboard or foam core board as a backboard, with spacers to mount the PCBs. This is always a little tedious, and I didn't love it. It also meant having the right set of spacers around, and I really should have ordered some kind of punch tool for M3 or similar screws.

For lighter boards, I've even duct-taped PCBs to cardboard, usually by their cables. This works, but was inelegant.

Current Footprint Design

brick mount footprints

Currently, I have seven footprints that I have designed:

  • 1x1 peg
  • 1x2 pegs
  • 1x3 pegs
  • 1x4 pegs
  • 1x6 pegs
  • 1x8 pegs
  • 2x4 pegs

Each part has the following features:

  • A courtyard that wraps around each peg.
    • A 1x1 peg would have an 8x8mm courtyard
    • A 1x2 peg would have an 8x16mm courtyard
  • A hole with a drill diameter of 4.9mm, to allow some clearance of the peg diameter of 4.8mm.
    • This +0.1mm tolerance is influenced by the capabilities of my usual PCB fabricator
    • I don't know yet if these will be tight enough tolerance to "snap" to the pegs of the brick, or if they will require glue or a cap.
  • An annular ring with a diameter of 6.0mm
    • These could be electrically connected, or used with some kind of connection wire or alligator clip.
    • So far, I've left these unconnected or grounded in all of my designs

These features were based on this page's CAD analysis of LEGO bricks.

That's pretty much it. In my boards, I've left 0.25-0.50mm edge-cut clearance (inside the courtyard) to ensure that my boards will fit "inside" of the footprint of an actual brick.

Current Board Designs

I've designed a couple of boards to test out working with this concept. These boards have been designed and ordered (the KiCAD projects are in the repo), though I have not received or tested them yet.

Feather Carrier - Small

Feather Small Render Feather Small image

This board is meant to mount to a single 2x4 brick. Because it mounts internally, it is probably best suited for boards that don't require a lot of movement, or for boards with a lot of top mounted buttons.

This carrier can be used for mounting an Adafruit Feather compatible board, or Featherwing expansion board.

It features:

  • Two horizontal QWIIC connector footprints (daisy-chainable)
  • A single set of breakout headers for each feather pin
  • Two additional breakout pins for each of GND, VBUS, VBAT, and +3v3 power rails
  • Feather mounting holes

Feather Carrier - Medium

Feather Medium Render Feather Medium Image

This board is top and bottom mounted. It features additional cutouts for running wires within the 4x8 peg footprint, and has additional prototyping space.

This carrier can be used for mounting an Adafruit Feather compatible board, or Featherwing expansion board.

It features:

  • Two horizontal QWIIC connector footprints (daisy-chainable)
  • Three breakout pins for each feather pin
  • Three additional breakout pins for each of GND, VBUS, VBAT, and +3v3 power rails
  • A mix of vertical bus connections and connectionless prototyping pins
  • Feather mounting holes

Feather Carrier - Large

Feather Large Render Feather Large Image

This board is side mounted. It features additional cutouts for running wires within the 6x8 peg footprint, and has additional prototyping space.

This carrier can be used for mounting an Adafruit Feather compatible board, or Featherwing expansion board.

It features:

  • Two horizontal QWIIC connector footprints (daisy-chainable)
  • Three breakout pins for each feather pin
  • Three additional breakout pins for each of GND, VBUS, VBAT, and +3v3 power rails
  • A mix of vertical bus connections and connectionless prototyping pins
  • Feather mounting holes

Proto Board - Quarter

Proto Quarter Render Proto Quarter Image

This board is a general purpose prototyping board, inspired by the Adafruit Perma Proto board.

It features:

  • Two vertical QWIIC connector footprints (daisy-chainable)
  • A row of 6 pins for each of SDA and SCL that are common with both QWIIC connectors
  • A column of 10 pins for each of GND and +3v3 that are common with both QWIIC connectors
  • 20 5-pin rows, arranged as a 10x10 pin prototyping space
  • Fits a 4x6 peg footprint

NOTE: In the future, I will likely revise the boards to use the same horizontal QWIIC connector as the feather boards. This will likely reduce the number of power rail pins by one for each rail, and the top prototyping row to three pins instead of five to allow for connector room. It will also swap the rows used for SDA and SCL breakout.

Proto Board - Half

Proto Half Render Proto Half Image

This board is a general purpose prototyping board, inspired by the Adafruit Perma Proto board.

It features:

  • Two vertical QWIIC connector footprints
    • These require a connection on the board to daisy-chain
  • Two 30-pin power rails
  • Two 24-pin power rails, that can be connected to the QWIIC connectors via Solder Bridges
  • A row of pins for each of SDA, SCL, +3v3, and GND, for each of the two QWIIC connectors
  • 60 5-pin rows, and 12 5-pin center columns arranged as a 30x12 pin prototyping space.
  • Fits a 12x6 peg footprint

NOTE: In the future, I will likely revise the boards to use the same horizontal QWIIC connector as the feather boards. This will not affect the number of prototyping pins, though will change the ordering of the breakout rows. I may also provide some way to short the SDA and SCL pins without running a discrete wire, e.g. using another pair of solder jumpers.


I realized that I will reach for my phone or for a python REPL window for most basic math operations.

Additionally, there are a couple things that I could probably stand to offload from my phone, in order to look at it less often. This includes:

  • Calculator
  • Unit Conversion
  • Alarm Clock
  • Timer
  • Counting mode
  • Time tracking (a la Vintage)
  • todo list/checklist

I figured I'd try my hand at making a replacement for my old-school TI-83, and maybe make a REPL with CircuitPython or even a Rust based scripting language like RHAI.

The Keypad


See Notes from 2021-01-22 for the original KeyPad investigation and brainstorming.

So, I have a limited number of keys to use, so I started looking at what kind of keys I typically used in my Python REPL. It turns out, they were surprisingly low.

  • Numbers
  • Parenthesis
  • Operators
  • Period
  • Equals
  • Letters (for variables - T9?)
  • Comment (#)
  • Arrows
  • Enter

I came up with a 4x6 arrangement that I thought would be pretty reasonable (for a calculator):

+   -   *   /
7   8   9   (
4   5   6   )
1   2   3   #
=   0   .   FUNC

For the "Special" keys:

  • "FUNC": Some kind of menu for python keywords (for, min, max, sin, cos)
  • "LET": Switch the keypad into T9 mode for entering letters
  • "ARR": Switch the keypad into arrow mode for navigation
  • "SPC": Space Bar
  • "ENT": Enter/Return


choc keypad v1 render

Inspired by the MNT Reform, I wanted to use low profile mechanical keys. Luckily, the MNT Reform hardware is all open source, which means I could see the footprints, circuit, and spacing used.

I put together a 4x6 keyboard matrix PCB, with LEDs under each key, and a row of 19 LEDs at the bottom, maybe to use for palm lighting or notifications.

It has one connector for the Row/Column IO, and one connector for the WS2812-alike smart LEDs.

The hardware project is here.

I plan to use:

See my parts pages for more details about the switches and keycaps, and for the smart leds.

I might use black keycaps for the numbers, and clear keycaps for the "meta" keys. TBD.

So far, I haven't chosen an MCU, but I'll use a feather board or something, and likely prototype the UI and behavior from my PC before digging into the embedded side of things.

The Screen

I want to have some kind of screen that fits above the keypad. I've thought about keeping the total size of the calculator device roughly to the size and shape of my current cellphone, which is 150.0mm x 75.0mm.

Right now, the Keyboard (minus LEGO mounting rails) takes up about 112.0mm x 80mm.

Ideally, I'd find some sort of 4:3 ratio LCD or epaper display that has dimensions of rougly 60.0mm x 80.0mm. This would give a total device size of 172.0mm x 80mm, which is close enough to the target.

For a low volume 4:3 screen, the Adafruit TFT FeatherWing 3.5" seems to be close to perfect, at 65.0mm x 85mm. It has a 480x320px screen, which should allow for a reasonable amount of visibility. It also has a resistive touchscreen, which would be a good secondary input interface.

With a 16:9 ratio, I would have a display of 45.0mm x 80.0mm, which would give a total size of 157.0mm x 80mm, which would be almost exactly the size of my phone.

I've also thought about mixing LCD and e-paper, think like an Accountant's calculator with the paper feed, but with e-paper instead of the paper reel (a single row of LCD/LED characters for quick updates, e-paper for history).

I could also consider using one of the Sharp transflective displays, though having some way of backlighting would probably be desirable. Joey Castillo uses these for his PyCorder, which I think is the 2.7" Sharp Memory LCD LS027B7DH01A which is $27 in singles. This display has an outline of 62.8mm x 42.82mm, and a 400x240px resolution.

They also seem to offer a 4.4" Sharp Memory LCD LS044Q7DH01, though this is a bit lower resolution (320x240) strangely, and a bit big for my ideal (94.8mm x 75.2mm).

Outside of Memory LCDs, Sharp also seems to have a 3.7" RGB option with the LS037V7DW06 and LS037V7DW05, though these are considerably more expensive at $70 or so, and are not currently in stock or available as singles, at least through Digikey. These also seem to require a parallel RGB interface, and higher current requirements.


I think a single LiPo pouch cell is probably the best idea here. The battery width could be 70.0mm to 80mm pretty comfortably, as well as 110.0mm to 150.0mm, depending on the size of the display.

For reference, Adafruit's largest pouch cell is 2500mAh, at 62.5mm x 50.5mm x 8.1mm.


I'll probably want some kind of scripting language with support for UI/HW control. I think my best options at the moment are:

  • Neotronian - from the Monotron/Neotron family, designed for constrained environments
  • Rhai - a configurable DSL/embedded scripting language

Next Steps

Now I need to wait for the keypad to arrive, and likely make a pretty "dumb" USB peripheral out of it. Then I can start working on a plan for the UI, likely simulated on-PC, until I find a design I like.

Sensor Clock

Sensor Clock in LEGO case

The sensor clock is intended to be a general purpose clock/alarm clock, as well as a host to a number of environmental sensors, including CO2, Temperature, and Humidity sensors.


The parts communicate over I2C. The top and bottom boards are connected via QWIIC connectors.


The Seven Segment display rotates between:

  • Time
  • CO2 measurement
  • Temperature measurement
  • Humidity measurement
  • Uptime measurement

The ePaper display will be used to show a graph of the last 3- or 48-hours of CO2 levels.


Wiring of the Sensor Clock

This is using two Half Proto Boards for the current prototype.

Future Plans

  • Add notification LEDs
  • Add Wireless for sensor readings
  • Implement buzzers

Sprocket Boards

Sprocket Board Renders

This is a breakout board for the STM32G031. Each board is 15.0x47.0mm.

Each board costs under 2.00 EUR/ea at 100 units.

I documented the design process here:

And streamed most of the original design on YouTube here:

The Board

It has:

  • A 28-pin STM32G031
    • 64MHz Cortex M0+
    • 8KiB RAM
    • 64KiB Flash
  • Two WS2812b-style Smartleds
  • Two User buttons
  • Reverse Protection Diode
  • One Power LED (always on)
  • Two User LEDs
  • 3.3v LDO
  • Four QWIIC connectors
  • Three main connectors:
    • One with 8 GPIOs, all with PWM, six with ADC channels and Power
    • One with I2C, SPI, UART, and Power
    • One with SWD connectors, reset pin, and power

The board is meant to be brick mount compatible, and is the same size as a LEGO 2x6 Plate.

The four mounting pins each electrically connect to:

  • +5V0
  • Ground
  • SDA (3v3)
  • SCL (3v3)

These connectors are intended for use with a testing, programming, or configuration jig.

Compatible Add-On Boards

I don't know if I'll make a lot of accessories, but I'll certainly make some.

Clink board

clink board render

This board is intended to take PWM as inputs (on up to eight channels). The PWM signal is buffered by a TI 74HC244D, and sent through an RC circuit, taken from the Raspberry Pi 3B+ Audio.

I don't expect Hi-Fi audio, but might be useful to make appropriate bleep-bloops.

The board also contains it's own 5V LDO to reduce signal noise.


This section keeps track of parts or components that I am using, have purchased, or have interest in using.

It includes usage notes, important datasheet sections, and links to other projects that use this part (mine or others) that can be used as a reference.

When possible, I'll try to link to a supplier of these parts, including where I bought them (if applicable).


LEDs are a specific kind of Actuator. I work with LEDs more often than many other parts, so they get their own section.

Often this section will be used to track a variety of "Smart LEDs", or digitally controlled LEDs that contain their own Logic ICs.

Smart LEDs

TODO: Link the following datasheets:

  • WS2812b
  • SK6812w
  • APA102

TODO: Explain smart leds and their protocol

TODO: Explain techniques for using SPI, I2S, PWM, etc. for controlling smart leds


TCWIN TC2020RGB front image

I found this small (2.1mm x 2.1mm) LED that seems to use the same Smart LED protocol as the WS2812/SK6812/AP102 families. At the time, it was available in a small-ish quantity (600 stock or so) on JLCPCB parts list, which was enough for 5 of the original Calculator Keypad prototypes.

I haven't yet tested them to see if they require different control schemes or modifications from standard techniques.

HUB75 Panels

part image

I have ordered a pair of HUB75 panels from AliExpress.

These use a different protocol to the WS2812B or other smart-leds, and I haven't figured out how I will drive them yet.

WS2812 Panels

Part Image

I currently have a couple of flexible WS2812b panels ordered from Aliexpress. They come in the following configurations:

  • 8x8 pixels, 80x80mm
  • 16x16 pixels, 16x16mm
  • 8x32 pixels, 80x320mm

I plan to use these as a programmable load when testing the LiPo Stamp.


Sensors are the "input" from the "real world". They provide a specific view on something measurable.

This section may contain individual parts, or modules that are used prototyping.


The INA219 is a "High Side" DC current sensor. It communicates over I2C.


I plan to use this part for:


Adafruit INA219 Breakout

Part Image

  • Default config, 26V, +/- 3.2A, 800uA resolution
  • Selectable I2C addresses (7-bit):
    • 0x40
    • 0x41
    • 0x44
    • 0x45
  • Docs and How-To


Actuators are any part that act as "output" to the "real world".

Quad Relay

part image

I bought a couple of opto-isolated quad relays off of Amazon. They seem to be similar to parts available on Aliexpress.

I use these as part of Home Fleet to control fixed-color DC LED strips.

FS90R Micro Servo

part image

I have ordered a couple of FS90R continuous rotation micro servos from Adafruit.

I haven't decided what to do with these yet, or how to control them.


Modules are assemblies that contain multiple parts necessary for operation. These are often meant to be a single complex part of a larger assembly.

MS88SF2 - nRF52840 SOM

MS46SF11 - nRF52805 SOM

Communication Parts

These are parts that are used to (assist with) device-to-device communications. These are often transcievers or other similar components.



Part Picture

  • LCSC Datasheet
  • LCSC Part Number: C68934
  • Bidirectional
  • No RX/TX enable


Part Picture

Logic ICs

Buffers, Drivers, Tranceivers


Part Picture


ECP5 i5 Module

Upduino iCE40

MCU Dev Boards

STM32F411 Black Pill

STM32H743 Core



Qwiic/Stemma QT

Aviation Plug




part image


part image

Battery Clips

These are clips for a LiPo Battery Cells


Keystone 54

  • THT
  • Digikey: 36-54-ND

Keystone 254

  • SMT
  • Digikey: 36-254-ND


Keystone 247

  • 20700/21700
  • THT
  • Digikey: 36-247-ND
  • Clip Spacing
    • (2:1 pins): 9.6mm +/- 0.13
    • (clip:clip): 2x700 - 47.03mm +/- 0.13
  • Clip Size
    • Length: 14.99mm
    • Height (exl pins): 17.87mm

Keystone 246

  • 20700/21700
  • SMT
  • Digikey: 36-246-ND

Memory ICs



Buttons, switches, etc.

Things for mechanical input to a circuit.

Kailh Choc Low Profile 1350s

kailh switch image

These low profile switches have a size of 15mm x 15mm x 8mm. These are the same switches used on the MNT Reform. I also use this for my Calculator project.

The switches have a 5.00mm x 3.15mm window for LEDs.

The MNT Reform uses the following center-to-center spacing for the keys:

  • X spacing of keys: 18.60mm
  • Y spacing of keys: 17.60mm

This spacing seems to give an approximately 1.1mm gap between the keycaps of each key.

They are not compatible with Cherry-MX switches. The MNT Reform has a variety of footprints and symbols available for use.

These switches seem to come in three varieties:

  • Red - "Linear"
  • Brown - "Tactile"
  • White - "Clicky"

I haven't been able to find a good source of bulk switches, with the product pages above offering them in 10 packs or 20 packs, with limited stock, and expensive from-the-US shipping.


I haven't been able to find an official source of Keycaps, or a good bulk source.

I have ordered these two different "artisan" sets of 1u caps (expensive, especially with shipping from the US):

The Ergo Alphas state they have a size of 17.5mm x 16.5mm, while the Natural keys don't list a size.

In this blog post, Lukas from MNT Research states "Blank keycaps are sourced from Kailh", though I don't know how he ordered them (probably in bulk), maybe I could buy a batch of keycaps and keys from him.


This section tracks relevant standards for my project. This may include:

  • Physical standards
  • Electrical standards
  • Communication standards

This aims to capture important or relevant aspects of these standards, as they relate to my projects.


This section captures concepts that may someday become a project, but for now are just a loosely captured idea in my head.

These ideas are likely to be an area of research, and may at some point "graduate" to the Projects section.

These ideas may be incomplete, or even incorrect, based on my current understanding.

Standard PCB Sizes

Problem Statement

As a PCB designer, it is difficult to pick a "standard" size for designing hardware, where it will be possible to find off the shelf cases or accessories that will fit that board.


Invent a new standard!

Reference Material/Background

  • IEC-602970-3 - Eurocard standard
    • I'm stealing the base card size
    • Based around a 100x160mm card
    • Typically used for test equipment
    • Also influnced Eurorack hardware
  • ISO 216 - Standard Paper Sizes
    • I'm stealing the idea of "half sizes"
    • Every card is 1/2 the larger dimension of the next size up
    • ISO 216 keeps the same ratio - I don't.
  • Sick of Beige v1 - OSHW Board Sizes
    • Really similar approach
    • Based on the "golden ratio"
    • Also has an "alt" square format
    • Steal same spacing for mounting screws/edge keepout

Standard rules

  1. Start with a "base size" of 200x160mm
  2. For each step, reduce the longest dimension by 1/2
  3. For each size, there are four variants:
    1. AR: Full rectangle, corner mounted holes
    2. BR: Full rectangle, "plus" mounted holes
    3. AS: "Slim" rectangle, corner mounted holes
    4. BS: "Slim" rectangle, "plus" mounted holes
  4. For all boards, mounting holes are M3 screws
    • 4x4mm from each corner
    • 3.2mm holes
    • TODO: What about small boards?
    • 6mm keepout around holes (diameter?)
    • 1.7mm edge keepout for case clearance


  • "Plus" mounted holes are for cases that have physical posts in the corners where the PCB must "keep out"

Full case size listing

FamilyVariantFull NameHole Location?Board ShapeWidthLengthcm^2
P0ARP0ARCorner HolesRegular200160320
P0ASP0ASCorner HolesSlim20080160
P0BRP0BRPlus HolesRegular200160320
P0BSP0BSPlus HolesSlim20080160
P1ARP1ARCorner HolesRegular160100160
P1ASP1ASCorner HolesSlim1605080
P1BRP1BRPlus HolesRegular160100160
P1BSP1BSPlus HolesSlim1605080
P2ARP2ARCorner HolesRegular1008080
P2ASP2ASCorner HolesSlim1004040
P2BRP2BRPlus HolesRegular1008080
P2BSP2BSPlus HolesSlim1004040
P3ARP3ARCorner HolesRegular805040
P3ASP3ASCorner HolesSlim802520
P3BRP3BRPlus HolesRegular805040
P3BSP3BSPlus HolesSlim802520
P4ARP4ARCorner HolesRegular504020
P4ASP4ASCorner HolesSlim502010
P4BRP4BRPlus HolesRegular504020
P4BSP4BSPlus HolesSlim502010
P5ARP5ARCorner HolesRegular402510
P5ASP5ASCorner HolesSlim4012.55
P5BRP5BRPlus HolesRegular402510
P5BSP5BSPlus HolesSlim4012.55
P6ARP6ARCorner HolesRegular25205
P6ASP6ASCorner HolesSlim25102.5
P6BRP6BRPlus HolesRegular25205
P6BSP6BSPlus HolesSlim25102.5
P7ARP7ARCorner HolesRegular2012.52.5
P7ASP7ASCorner HolesSlim206.251.25
P7BRP7BRPlus HolesRegular2012.52.5
P7BSP7BSPlus HolesSlim206.251.25



HIL Testing Theory

I think that Hardware in the Loop testing breaks down into three "dimensions", or aspects that represents some aspect of an individual test or test case. These include:

  • Interfaces - or how you interact with your unit under test
  • Behaviors - or how your test adapter behaves over one or more Interfaces
  • Paradigms - or how your test is structured between the testing host and the testing adapter, usually guided by a balance of max acceptable latency and necessary automation required to reach lower latency numbers

IMO, every test will be some combination of all three of these dimensions.

This list discusses hosts, or the PC orchestrating the test cases, the adapter, which manages real time communications and simulation, and the unit under test, the device that is being tested.


  • Interfaces
    • GPIOs
    • SPI
    • I2C
    • UART
    • ADCs
    • DACs/PWM
    • I2S
    • CANBus
    • RS-485
    • Long tail of domain specific interfaces/protocols
  • Behaviors
    • Immediate Read (one value)
    • Immediate Write (one value)
    • Stream In (sniffing)
    • Stream Out (dumping)
    • React/Response (potentially chained)
      • e.g. GPIO goes low, send UART message
    • Table/Register Based
      • e.g. SPI/I2C
    • Sequencing/Timing?
    • Controlling External Tools, e.g.
      • Power Supply
      • Relay
      • Current sensor
      • Expansion Boards/"Hats"/"Shields"
  • Paradigms
    • Adapter: 0% automated; Host: 100% automated - Raspberry Pi use cases
      • Immediate "host side" controls
      • Assume 10-100ms Round Trip Latency
    • Adapter: 33% automated; Host: 66% automated (IFTTT-lite) - Arduino use cases
      • Host: Preloads specific response/trigger behavior
      • Adapter: Limited number of trigger responses
      • 0ms latency for "automated" tasks, 10-100ms for Roundtrip
    • Adapter: 66% automated; Host: 33% automated (Full sequencer)
      • Host: Set up specific sequences
      • Adapter: Can hold up to N actions, triggered by M conditions
    • Adapter: 100% automated; Host: 0% automated
      • Adapter has a full scripting environment (WASM, Python, 'pre-compiled')
      • Host: Just loads scripts and executes test cases

Robot Face

Idea: Make a dev board that looks like a "robot face". Optionally put it on some kind of Servo arm to give it pitch/roll/yaw, and potentially a neck to rotate or tilt up/down.

To Be Researched

  • Which micro servos should I get?
    • FS-90R not good, continuous
    • Find something quiet AND torque'y?
  • 3d layout
    • Probably want to learn a 3d CAD to make sure I have clearance
  • How to make brackets?
    • Pre-fab?
    • Acrylic?
    • Cut metal?

Original brainstorming

Development Board Notes

Arm Logistics and Servo Control

Robot Arm Sketch


Summary: The goal is to create an emulated "Peripheral Access Crate" for the purposes of learning and teaching embedded Rust. This would mean we would not require physical hardware to teach embedded Rust, and could also provide visualization and debugging tools that could help users diagnose issues they have.

Rather than using a more accurate tool for simulating an existing architecture, such as Arm or RISC-V, this would be a "native" target, capable of running on the host PC.

Ideally, this would allow people to create "fake" peripherals such as UART, SPI, and I2C, that could be exposed via simulated PAC interactions.

The idea would be to provide learning material, like the Discovery Book, based around this, so EVERYONE can learn a little embedded Rust (or at least the "higher in the stack" parts) without needing to buy specific hardware.

Once an emu-hal has been built on top of the emu-pac, the usual embedded-hal portability techniques could be used to integrate "off the shelf" drivers.

Prior Art

This isn't a totally novel idea, RTOS' like RIOT-OS have a "native" target that does something similar, though they simulate at the OS level. This is a similar technique, but moves the emulation layer down to the PAC, which I think could still be reasonable enough for Rust, where we still have the register interfaces to draw a boundary at, also sort of like disasm's avatar-rs does for passing CPU actions through to probe-rs.

Other projects to investigate:

  • litex_sim:
    • From disasm: "have you seen litex_sim? It emulates a full SoC with cycle accuracy. Some of the peripherals have "real world" connections, you can even talk via Ethernet. With verilator (or maybe even cxxrtl) it's possible to emulate this with a decent frequency. Also litex generates SVD for a SoC if you ask it"
  • renode
  • TinyGo Preview

Architecture Diagram

architecture diagram

Moving pieces

This is a brainstorm of the pieces needed to make this happen.

  • Multiple threads for simulating each component
    • One "main" thread that acts as the entry point
    • One thread for each interrupt
    • One thread for "hardware simulation", running concurrently to the "main" thread
  • An AtomicU32 based primitive for each hardware register
    • This allows for multi-threaded sharing of "registers" between interrupts, "main", and the simulation context
    • We may want to have some sort of shared global mutex to simulate non-concurrency between interrupts, e.g. a critical section prevents all interrupts from touching any registers
    • We may want some kind of "rate limiting" of register access (read or write) to make the operational speed somewhat more reasonable, e.g. only allow one access per (1/64MHz) time scale, at least from the main/interrupt threads
  • Simulators of external devices, such as a WS2812b
    • This will be necessary, as we won't have physical hardware
  • We need to "invent" our own peripherals, e.g. define the register layout, capabilities, and behaviors of the provided UART, SPI, I2C, and Timer peripherals
    • We could also write an external connector, allowing for the simulated CPU to talk "real" SPI using a USB FT2232H (or similar) adapter
    • We probably want a "fake" RTT peripheral, which can be used for rprintln or defmt capabilities
  • A "board simulator", which would be a 2d or 3d rendering of the board and any connected components, allowing users to "see" things like LEDs blinking, motors spinning, etc. based on the operation of the emulated CPU
  • We probably need some kind of "notification system" where writes to certain registers can trigger behaviors, such as a peripheral starting operation


There are a couple things I don't know how to handle, including:

  • Use of svd2rust
    • Should we fake an svd2rust style interface?
    • Or should we make an actual SVD to stay coupled?
  • How to build an emu-rt crate
  • How to add rtic support

Other people's projects

This section tracks projects by other people that I am interested in following. It primarily aims to be a "bookmarks folder" of sorts, and contain notes on why I am interested in these projects.


Funny snippets of history


Load bearing build system nop

This is code from the pre-1.0 version of Rust's build system.

Code available here.

# Copy a dylib or rlib
# $(1) is the filename/libname-glob
# XXX: Don't remove the $(nop) command below!
# Yeah, that's right, it's voodoo. Something in the way this macro is being expanded
# causes it to parse incorrectly. Throwing in that empty command seems to fix the
# problem. I'm sorry, just don't remove the $(nop), alright?
    @$(call E, prepare: $(PREPARE_WORKING_DEST_LIB_DIR)/$(1))
    $(Q)LIB_NAME="$(notdir $(lastword $(wildcard $(PREPARE_WORKING_SOURCE_LIB_DIR)/$(1))))"; \
    MATCHES="$(filter-out %$(notdir $(lastword $(wildcard $(PREPARE_WORKING_SOURCE_LIB_DIR)/$(1)))),\
                        $(wildcard $(PREPARE_WORKING_DEST_LIB_DIR)/$(1)))"; \
    if [ -n "$$MATCHES" ]; then                                              \
      echo "warning: one or libraries matching Rust library '$(1)'" &&       \
      echo "  (other than '$$LIB_NAME' itself) already present"     &&       \
      echo "  at destination $(PREPARE_WORKING_DEST_LIB_DIR):"                    &&       \
      echo $$MATCHES ;                                                       \

Bastion of the TurboFish

Code available here.

// Bastion of the Turbofish
// ------------------------
// Beware travellers, lest you venture into waters callous and unforgiving,
// where hope must be abandoned, ere it is cruelly torn from you. For here
// stands the bastion of the Turbofish: an impenetrable fortress holding
// unshaking against those who would dare suggest the supererogation of the
// Turbofish.
// Once I was young and foolish and had the impudence to imagine that I could
// shake free from the coils by which that creature had us tightly bound. I
// dared to suggest that there was a better way: a brighter future, in which
// Rustaceans both new and old could be rid of that vile beast. But alas! In
// my foolhardiness my ignorance was unveiled and my dreams were dashed
// unforgivingly against the rock of syntactic ambiguity.
// This humble program, small and insignificant though it might seem,
// demonstrates that to which we had previously cast a blind eye: an ambiguity
// in permitting generic arguments to be provided without the consent of the
// Great Turbofish. Should you be so naïve as to try to revolt against its
// mighty clutches, here shall its wrath be indomitably displayed. This
// program must pass for all eternity, fundamentally at odds with an impetuous
// rebellion against the Turbofish.
// My heart aches in sorrow, for I know I am defeated. Let this be a warning
// to all those who come after. Here stands the bastion of the Turbofish.

// See
// and
// for context.

fn main() {
    let (oh, woe, is, me) = ("the", "Turbofish", "remains", "undefeated");
    let _: (bool, bool) = (oh<woe, is>(me));


This section contains unsorted chronological notes. These might include research items that will be used to fill in concepts in the more topic-ordered sections.

This section is strongly inspired by whitequark's Lab Notebook.

All notes are in an RFC8601 compatible date specification, e.g., and may include a topic, e.g.

Transcribed notes from iPad

Want to do items

  • Design 50x80mm nrf52840 board
    • Use a power fet for LED cutout?
    • Add PMOD and QWIIC connectors?
    • Include a non-pop USB-C footprint?
  • Write an Anachro Bootloader
    • A/B side image
    • Update-able bootloader image (maybe)
  • Clean up and publish anachro crates
    • Split out Stargazer to its own repo
  • Design LiPo Stamp test stand
  • Something with the SCD30 CO2 sensor?
  • const-fn bbqueue implementation
  • Design Acrylic cases
    • Water resistant one (Kuma Collar)
    • General purpose "sandwich" case
  • Document "Eye of Sauron" idea
    • Grab sketch from iPad
  • Think about Anachro protocol versioning
  • Document "Tweet to Lab Notebook Importer"
    • Bot to open PR with my tweets and replies on a daily basis
    • Include text for search (not just links)
  • Mail out LiPo stamps to testers

Thoughts for me

I probably want to stream starting out the LiPo stamp tester, because I'll need that for the Kuma Collar/80x50 board.

2020-12-26 Thoughts

Board Stackup

Video about board stackup

Dumb Terminal for Smart Devices

Rough idea: Some kind of simple USB device that talks to "smart devices" and provides a basic USB/Serial interface that exposes data in some kind of table format.

But, problems:

  • No one has standard schemas
  • Doesn't solve this problem
  • Even if we have a schema, everyone does the BT thing
    • "data over serial/proprietary protocol"

Podcasting notes

I need to set up a couple things:

  • Recording
    • Currently done with Zoom, audio quality isn't great
    • I might want to switch to something else with higher quality audio
  • Leveling
    • Currently using Auphonic, I like it
  • Editing
    • I'll probably use Audacity
  • Theme Music
    • I'll probably use Louie Zong - Polaroid
  • Album Art
    • ...?
  • Hosting
    • I'll use Digital Ocean Spaces
  • RSS Feed
    • I'll probably just host on my website
    • for validation

Task Manager

  • Pocket based device for time tracking
  • Only shows you one task at a time

Using eMMC chips

See this twitter thread

Videos to watch


Plan for 2021-01-02 Stream

  • Document my Standard case size/PCB size standard ideas
  • Look up some IDC cable stuff
  • Do some board design for my nrf52/kuma collar board

BBQueue abstract storage

I'd like for bbqueue to be abstract over the storage medium of the BBBuffer.

There are two main components here:

  • The tracking header (fixed size)
  • The data buffer (variable-ish size)

Right now both of these items are stored in the current-day BBBuffer, and they always travel together.

Ideally, we'd like to be able to support any of the following kinds of data buffers:

  • [u8; N] - a const generic "owned" buffer - likely in static storage. This is the embedded use case, but could also be useful for local usage? Though probably not because lifetimes will get complicated if they are not 'static, but this is still supported today, even if not very useful
  • &'a mut [u8] - A "user provided" buffer. Again, this is probably not very useful when 'a is not 'static, which would require unsafe to acquire in most cases. However I think that unsafe-ty can be deferred to the caller?
  • Arc<[u8]>, or something like that

I think the Arc case is really just a subset of the &'a mut [u8] case (where 'a is 'static if handled correctly, but this exposes the necessity to think of the tracking header and the data buffer in the same context.

Really, they need to have the same lifetime. It feels like I should have a struct like this:

fn main() {
struct BBBuffer {
    header: BBHeader,
    storage: BBStorage,

And then when handing out something that holds both, they need to have the same lifetime.

I guess not the SAME lifetime, but rather any time we create a reference to BBHeader, it must have a lifetime less than or equal to BBStorage. e.g. 'storage: 'header.

However, BBStorage feels like it should be generic over something. At the very least, I want three potential situations:

  • A statically allocated "owned" buffer. This needs inner mutability to ensure that the buffer can only be taken once, e.g. a Singleton.
  • A "borrowed" buffer provided by the user, we just rely on lifetimes and the borrow checker to make sure this works out.
  • A reference counted buffer, which de-allocates itself when no handles (e.g. Buffer, Producer, Consumer, Grants) are held.

Brainstorming with fake code

What if we make BBStorage a trait? This way I could provide some convenience types for the three above situations, and a user could also construct their own if they had some other use case, like shared memory between CPUs or IPC.

I worry a little bit about the generic types making the API look pretty garbage or overly complicated. Especially with fully qualified types when using as a static. I wonder if I can "paper over" this with type aliases or dependant traits.

fn main() {
// I don't know if this lifetime syntax is right
trait BBStorage<'header, 'storage: 'header> {
    // We need some kind of "associated lifetimes"
    type StorageLifetime = PhantomData<'storage>;
    type HeaderLifetime = PhantomData<'header>;

    // I think `self` needs to have `'storage` lifetime, as it is longer.
    fn get_storage(&'storage self) -> &'storage impl Deref<Target=Storage>;
    fn get_header(&'storage self) -> &'header impl Deref<Target=Header>;

I think with this approach, I could:

  • Create a struct that owns both Storage and Header (with a const constructor), which each would have the same lifetime as the storage structure
  • Create a struct that borrows both Storage and Header
  • A wrapper type that holds an Arc of the owned version, and returns a reference

But wait, if these two lifetimes are always the same, can't I just have a parent structure that references both children?

The main problem is I need some kind of "hook" to increment refcounts in the Arc case. I wonder if I can simplify it to just this:

fn main() {
struct BBBorrow<'header, 'storage: 'header> {
    header: &'header Header,
    storage: &'storage Storage,

trait BBGetter<'bbqueue> {
    fn get_borrow(&'bbqueue self) -> impl Deref<Target = BBBorrow<'bbqueue, 'bbqueue>;

Hmm, maybe? Not sure. I think the issue here is that all the types need to be generic over the BBGetter. This would mean that I need:

fn main() {
struct Producer<B: BBGetter> {
    bbq: B,
    // ...

struct GrantW<B: BBGetter> {
    bbq: B,
    // ...

I'm dancing around the concept of BBBorrows being Clone-able, but those clones would be bound to the lifetime of the BBBorrow itself, which is what I want, and in the case of 'static, this is no problem. I just need the borrow checker to understand that BBBorrow can't outlive 'storage, which I think the borrow checker can understand already?

Okay, let's go write some playground code to prove this out.

Attempt 1

use core::sync::atomic::AtomicUsize;
use std::sync::Arc;

struct Header {
    fake: AtomicUsize,

struct Storage {
    fake: AtomicUsize,

struct OwnedBuffer {
    hdr: Header,
    sto: Storage,

struct BBBorrow<'header, 'storage: 'header> {
    header: &'header Header,
    storage: &'storage Storage,

struct BBArc {
    barc: Arc<OwnedBuffer>,

trait BBGetter<'hdr, 'sto: 'hdr> {
    fn get_borrow(&self) -> BBBorrow<'hdr, 'sto>;

impl<'hdr, 'sto: 'hdr> BBGetter<'hdr, 'sto> for BBBorrow<'hdr, 'sto> {
    fn get_borrow(&self) -> BBBorrow<'hdr, 'sto> {
        return self.clone()

// impl BBGetter<'static, 'static> for BBArc {
//     fn get_borrow(&self) -> &BBBorrow<'static, 'static> {
//         return self.barc.get_borrow()
//     }
// }

fn main() {
    println!("Hello, world!");

Attempt 2

use core::sync::atomic::AtomicUsize;
use std::sync::Arc;

struct Header {
    fake: AtomicUsize,

struct Storage {
    fake: AtomicUsize,

struct OwnedBuffer {
    header: Header,
    storage: Storage,

impl OwnedBuffer {
    fn borrow(&self) -> BBBorrow {
        BBBorrow {
            header: &self.header,
            storage: &,

struct BBBorrow<'header, 'storage: 'header> {
    header: &'header Header,
    storage: &'storage Storage,

struct BBArc {
    barc: Arc<OwnedBuffer>,

trait BBGetter<'hdr, 'sto: 'hdr>: Clone {
    fn get_header(&self) -> &'hdr Header;
    fn get_storage(&self) -> &'sto Storage;

impl<'hdr, 'sto: 'hdr> BBGetter<'hdr, 'sto> for BBBorrow<'hdr, 'sto> {
    fn get_header(&self) -> &'hdr Header {
    fn get_storage(&self) -> &'sto Storage {

impl<'hdr, 'sto: 'hdr> BBGetter<'hdr, 'sto> for BBArc {
    fn get_header(&self) -> &'hdr Header {
        // Can't borrowck
    fn get_storage(&self) -> &'sto Storage {

fn main() {
    println!("Hello, world!");

Attempt 3

The problem with this attempt is that the Producer and Consumer need to OWN something that can reference the data, but NOT for longer than the reference to the Arc (in this case) lives. How can I introduce this?

use core::sync::atomic::AtomicUsize;
use std::sync::Arc;
use core::marker::PhantomData;

struct Header {
    fake: AtomicUsize,

struct Storage {
    fake: AtomicUsize,

// TODO: Wrap this in a "Singleton"?
// Should this be a private type? This probably
// wont play nice with being `'static`...
// Probably requires `MaybeUninit` and such
struct OwnedBuffer {
    header: Header,
    storage: Storage,

impl OwnedBuffer {
    // Should this be a private or unsafe method?
    fn borrow(&self) -> BBBorrow {
        BBBorrow {
            header: &self.header,
            storage: &,

    fn split(&self) -> Result<(Producer<BBBorrow>, Consumer<BBBorrow>), ()> {
            Producer {
                bbq: self.borrow(),
                pds: PhantomData,
                pdh: PhantomData,
            Consumer {
                bbq: self.borrow(),
                pds: PhantomData,
                pdh: PhantomData,

struct BBBorrow<'header, 'storage: 'header> {
    header: &'header Header,
    storage: &'storage Storage,

struct BBArc {
    barc: Arc<OwnedBuffer>,

trait HeaderGetter<'hdr>: Clone {
    fn get_header(&self) -> &'hdr Header;

trait StorageGetter<'sto>: Clone {
    fn get_storage(&self) -> &'sto Storage;

impl<'hdr, 'sto: 'hdr> HeaderGetter<'hdr> for BBBorrow<'hdr, 'sto> {
    fn get_header(&self) -> &'hdr Header {

impl<'hdr, 'sto: 'hdr> StorageGetter<'hdr> for BBBorrow<'hdr, 'sto> {
    fn get_storage(&self) -> &'sto Storage {

impl<'hdr, 'sto: 'hdr> HeaderGetter<'hdr> for BBArc {
    fn get_header(&self) -> &'hdr Header {

impl<'hdr, 'sto: 'hdr> StorageGetter<'hdr> for BBArc {
    fn get_storage(&self) -> &'sto Storage {

struct Producer<'hdr, 'sto: 'hdr, G>
    where G: StorageGetter<'sto> + HeaderGetter<'hdr>,
    bbq: G,
    pds: PhantomData<&'sto ()>,
    pdh: PhantomData<&'hdr ()>,

struct Consumer<'hdr, 'sto: 'hdr, G>
    where G: StorageGetter<'sto> + HeaderGetter<'hdr>,
    bbq: G,
    pds: PhantomData<&'sto ()>,
    pdh: PhantomData<&'hdr ()>,

fn main() {
    println!("Hello, world!");

Attempt 4

Hmm, this feels better, though it feels like .borrow() and .split() could be part of a combined Getter trait that doesn't split the header and storage borrows...

use core::sync::atomic::AtomicUsize;
use std::sync::Arc;
use core::marker::PhantomData;

struct Header {
    fake: AtomicUsize,

struct Storage {
    // TODO: This should probably be something
    // like `MaybeUninit<[u8; N]>`
    fake: AtomicUsize,

// TODO: Wrap this in a "Singleton"?
// Should this be a private type? This probably
// wont play nice with being `'static`...
// Probably requires `MaybeUninit` and such
struct OwnedBuffer {
    header: Header,
    storage: Storage,

impl OwnedBuffer {
    // Should this be a private or unsafe method?
    fn borrow(&self) -> BBBorrow {
        BBBorrow {
            header: &self.header,
            storage: &,

    fn split(&self) -> Result<(Producer<BBBorrow>, Consumer<BBBorrow>), ()> {
            Producer {
                bbq: self.borrow(),
            Consumer {
                bbq: self.borrow(),

impl BBArc {
    // Should this be a private or unsafe method?
    fn borrow(&self) -> BBBorrow {

    fn split(&self) -> Result<(Producer<BBArc>, Consumer<BBArc>), ()> {
            Producer {
                // bbq: self.borrow(),
                bbq: self.clone()
            Consumer {
                // bbq: self.borrow(),
                bbq: self.clone()

struct BBBorrow<'header, 'storage: 'header> {
    header: &'header Header,
    storage: &'storage Storage,

struct BBArc {
    barc: Arc<OwnedBuffer>,

trait HeaderGetter: Clone {
    fn get_header(&self) -> &Header;

trait StorageGetter: Clone {
    fn get_storage(&self) -> &Storage;

impl<'hdr, 'sto: 'hdr> HeaderGetter for BBBorrow<'hdr, 'sto> {
    fn get_header(&self) -> &Header {

impl<'hdr, 'sto: 'hdr> StorageGetter for BBBorrow<'hdr, 'sto> {
    fn get_storage(&self) -> &Storage {

impl HeaderGetter for BBArc {
    fn get_header(&self) -> &Header {

impl StorageGetter for BBArc {
    fn get_storage(&self) -> &Storage {

struct Producer<G>
    where G: StorageGetter + HeaderGetter,
    bbq: G,

struct Consumer<G>
    where G: StorageGetter + HeaderGetter,
    bbq: G,

fn main() {
    println!("Hello, world!");

Let's try combining the traits and see what that does to the lifetimes everywhere...

Attempt 5

Okay, combining the traits works, but I don't think I can move split to the trait, because that NEEDS to be a singleton.

The Arc option could never give you the original BBArc back for convenience, leaving split to be a static singleton artifact.

use core::sync::atomic::AtomicUsize;
use std::sync::Arc;
use core::marker::PhantomData;

struct Header {
    fake: AtomicUsize,

struct Storage {
    // TODO: This should probably be something
    // like `MaybeUninit<[u8; N]>`
    fake: AtomicUsize,

// TODO: Wrap this in a "Singleton"?
// Should this be a private type? This probably
// wont play nice with being `'static`...
// Probably requires `MaybeUninit` and such
struct OwnedBuffer {
    header: Header,
    storage: Storage,

impl OwnedBuffer {
    // Should this be a private or unsafe method?
    fn borrow(&self) -> BBBorrow {
        BBBorrow {
            header: &self.header,
            storage: &,

    fn split(&self) -> Result<(Producer<BBBorrow>, Consumer<BBBorrow>), ()> {
            Producer {
                bbq: self.borrow(),
            Consumer {
                bbq: self.borrow(),

impl BBArc {
    // Should this be a private or unsafe method?
    fn borrow(&self) -> BBBorrow {

    fn split(&self) -> Result<(Producer<BBArc>, Consumer<BBArc>), ()> {
            Producer {
                // bbq: self.borrow(),
                bbq: self.clone()
            Consumer {
                // bbq: self.borrow(),
                bbq: self.clone()

struct BBBorrow<'header, 'storage: 'header> {
    header: &'header Header,
    storage: &'storage Storage,

struct BBArc {
    barc: Arc<OwnedBuffer>,

trait BBGetter: Clone {
    fn get_header(&self) -> &Header;
    fn get_storage(&self) -> &Storage;

impl<'hdr, 'sto: 'hdr> BBGetter for BBBorrow<'hdr, 'sto> {
    fn get_header(&self) -> &Header {

    fn get_storage(&self) -> &Storage {

impl BBGetter for BBArc {
    fn get_header(&self) -> &Header {

    fn get_storage(&self) -> &Storage {

struct Producer<G>
    where G: BBGetter,
    bbq: G,

struct Consumer<G>
    where G: BBGetter,
    bbq: G,

fn main() {
    println!("Hello, world!");

Thoughts on 6

  • I should have three types exposed:
    • ArcBBQueue
      • Arc
      • Only have an interface create prod/cons
        • Takes ownership
    • OnceBBQueue
      • Singleton
      • Interface to create prod/const
        • Takes &self
    • BorrowBBQueue // Never instantiatable
      • Wait, how can we "take ownership" of an immutable borrow?
      • I guess we just have something like:
        • unsafe fn with_storage(&'sto mut [u8]) -> Borrow<'sto>
      • Can this ever be sound?

bullet notes

Books Recommendations

Using Lego blocks as circuit rails

Video Screenshot

In this youtube video, Look Mum No Computer has circuit boards that use Lego pegs as a mounting tool.

According to this page, Lego bricks have an 8mm center-to-center spacing, with a 4.8mm outer diameter for the brick pegs. This means that the PCB hole would need to be just larger than 4.8mm, depending on the fab's tolerance.

It also seems that he sells the modules used in the video on his store. I haven't been able to find any online design files though.

Idea for message serialization

keev - a Key-Value Serialization format with a concept of optionality on each side.


  • All messages exist in a "top level" namespace
  • There is no versioning of messages. New message, new number.
  • Protocol speakers should expect to "negotiate" which versions they CAN speak, and which they will use.
    • Input/Output capabilities?
    • This should probably only happen once, on connection
  • For anachro: Remove the concept of paths
  • Use an enum, make it non-exhaustive
  • Have a method for generating the RX and TX capabilities

Lego Shopping

So, I want to use Legos as a physical design medium as described in my prior note.

Adafruit Feather mounting board

I think as a test run, I'm going to make a demo for mounting an Adafruit Feather. Brainstorming:

  • Have THT holes for the edge pins
  • Have mounting holes for the main corner mounts
  • Have an optional pair of SMT QWIIC connectors at the front and back
  • Maybe break out some other pins too?
  • Maybe make a "side mount" and a "top/bottom" mount

Lego parts shopping

I want some base plates and parts to use for basic mounting (and maybe a little for playing with).

Here's my notes on shopping. These are all links, because I'm in Germany.

Other resources

I had these two online stores recommended to me, which look like a good choice if I want specific parts/colors. Not sure on the cost of shipping or anything.

    • There seem to be a lot of people selling batches here
    • The going price seems to be about 20-25EUR/kg, which claims ca. 700 pieces

JLC Tolerances

  • Drill Hole Size Tolerance:
    • +0.13/-0.08mm

PCB design

2.0" / 8mm => 6.350 0.9" / 8mm => 2.858

X: 89.84 + Y: 119.05

23 + 39.50 + 32 + 23.50

30x12pin 2x30pin 2x22pin 4xsolder bridge 2x 1x6brick 2x QWIIC

Interesting approach to serial comms

I wonder if I could use something like this, with pre-sending the schema, then sending binary packed messages?

Connector review

proto-half * Connector_JST:JST_SH_BM04B-SRSS-TB_1x04-1MP_P1.00mm_Vertical proto-quarter * Connector_JST:JST_SH_BM04B-SRSS-TB_1x04-1MP_P1.00mm_Vertical feather-small * Connector_JST:JST_SH_SM04B-SRSS-TB_1x04-1MP_P1.00mm_Horizontal feather-medium * Connector_JST:JST_SH_SM04B-SRSS-TB_1x04-1MP_P1.00mm_Horizontal feather-large * Connector_JST:JST_SH_SM04B-SRSS-TB_1x04-1MP_P1.00mm_Horizontal

Slim Key Switches

These are footprints for the keyswitches for the MNT reform.

And the switches themselves:

But what would I want an external keyboard to do?

It would be sort of nice to have a standalone device that I can run a python-ish style calculator app on, but that would require a significant number of keys.

Maybe something that can do some basic functionality as a standalone device, but when plugged in, could be some kind of macro hotkey style device?

Should I just prototype this with the blackberry featherwing?

Brainstorm use cases

  • Timer
  • Calculator
  • Alarm Clock

What keys would I use for a python calculator?

  • Parenthesis
  • Operators
  • Period
  • Equals
  • Letters (for variables - T9?)
  • Comment (#)
  • Arrows

What if:

Keys: 15x15mm

Note: Xiaomi Mi Mix 2 Dimensions: 151.8x75.5mm

4x6 keypad: 90.0x60.0mm w/ 5mm space: 120x80mm

+   -   *   /
7   8   9   (
4   5   6   )
1   2   3   #
=   0   .   FUNC

Do a prototype with a break-away fifth column?

320x240 display: 1.333 ratio 2.6" screen: 66.04mm 40x53mm

400x300px 103x78.5mm outline 84.8x63.6mm display

MNT Reform Notes

X spacing of keys: 18.60mm Y spacing of keys: 17.60mm

LED Notes

TC2020RGB-3CJH-TX1812Z5 - 2.1x2.1mm WS2812 knockoff?

Guide for the Colorlight i5

Digikey Shopping list

  • 3.5mm TRRS THT

    • Maybe not, disasm had negative things to say about TRRS reliability
  • Vertical JST-SH 1.0mm 4 pin

    • JST_SH_BM04B-SRSS-TB_1x04-1MP_P1.00mm_Vertical
  • Horizontal JST-SH 1.0mm 4 pin

    • JST_SH_SM04B-SRSS-TB_1x04-1MP_P1.00mm_Horizontal
  • IDC Cable (roll?)

  • IDC Connectors

    • 2x4
    • 1x6
    • 2x10 or 2x12 for fifth-proto?
  • IDC crimp tool

  • Larger THT 2.54mm screw blocks?

  • Pogo pins for 2.54mm THTs?

Aliexpress shopping list

  • jst pre-made cables
  • TRRS cables (4-pole)

Board Ideas

  • Daisy Chainable TRRS Lego
  • Eth Breakout
    • Common bar for GND shorting?
    • Right angle breadboard variant?
  • STLink/JTAG connector to Eth?
  • RS-485/Serial adapter Lego
  • IDC cable to breadboard adapter
    • Parallel?
    • Right Angle?
  • IDC cable proto board?
  • Pin expansion w/ PCF whatever?
  • black pill lego breakout?
  • WS2812 lego breakout?
  • Level shifter lego breakout?

Misc Ideas and Shopping

  • USB extension cables
  • HS-Probe lego breakout
    • Mating connector? With screw terminals/headers?
  • EMI spray?
  • Get 4.8mm OD pipe/rods cut to different lengths?
  • Get some kind of spacer/pipe w/ 4.8mm ID/6.0mm OD?
  • Get acrylic pegboard w/ 4.9mm holes for non-lego mounting?
    • Make more permanent cases?
  • More lego base boards?
  • White out pen
  • 3d printing and milling service -

Assembly to-do

  • INA-219
    • INA-219 Boards
      • Header pins on INA-219 (2x)
      • Terminal blocks on INA-219 (2x)
    • INA breakouts
      • Header sockets on fifth-proto for INA (2x)
      • screw terminals for daisy chain power and I2C
  • Debugger
    • Debugger side SWD cable -> eth adapter
    • target side eth adapter + debugger breakout
  • Black Pill
    • Black Pill breakout board
      • Header pins on pill
      • Header socket on BoB
      • 5v/GND terminal block(s?)
      • I2C terminal block
      • 3v3/GND terminal block(s?)
      • 2x ADC (3v3, vmax)
        • Resistor bridge?
      • 2x digital (stat out, 3v3_en in)
  • LED panel
    • LED panel breakout w/ screw terminals
    • Panel mounting? Sugru?
  • LiPo Stamp
    • LiPo stamp x3 header pins
    • proto lipo stamp header socket
    • Level shifter breakout board (maybe)
  • Relays
    • Relay breakout adapter?
      • Cable? Solder breakout board on top?
      • Glue lego mounting? Sugru?
  • Battery
    • Breakout
    • Mounting (Sugru? Glue?)

KiCad export automation

Memory Unsafety Talk

  • Commentary:
  • Presentation:

RPI Writing Machine

Samtec IDC page

Useful family overview page.

Four Letters

XX  X   X
--  |   `--- S:  Single, D:  Double
 |  `------- S:  Socket, M:  Male
 `---------- ID: Slim-Bodied, HC: "Classic"
  • TST - Shrouded header with notch


480x320px, 12x22 characters (3.5")

560 chars


Lorem ipsum dolor sit amet, consectetur
adipiscing elit. Fusce mollis pharetra a
nte, at mollis ex rhoncus vel. Praesent
auctor dapibus nibh ut venenatis. Nam ma
ttis tellus fringilla augue fermentum pu
lvinar. Sed gravida accumsan convallis.
Cras pretium neque vel scelerisque aucto
r. Curabitur sagittis magna non tortor s
odales sodales. Morbi vitae ullamcorper
neque. Nunc ac sapien aliquam, commodo l
eo in, lobortis mi. Quisque auctor sapie
n vel lectus facilisis, eu elementum urn
a luctus. Vestibulum congue, nisl ac sem
per blandit, metus erat laoreet gravida.

480x320, 16x30 characters (3.5")

300 chars


Lorem ipsum dolor sit amet, co
nsectetur adipiscing elit. Fus
ce mollis pharetra ante, at mo
llis ex rhoncus vel. Praesent
auctor dapibus nibh ut venenat
is. Nam mattis tellus fringill
a augue fermentum pulvinar. Se
d gravida accumsan convallis.
Cras pretium neque vel sceleri
sque auctor. Curabitur sagitti

240x160px, 12x22 characters (3.5")

140 chars


Lorem ipsum dolor si
t amet, consectetur
adipiscing elit. Fus
ce mollis pharetra a
nte, at mollis ex rh
oncus vel. Praesent
auctor dapibus nibh


96x64px, 6x8 characters (3.0")

128 chars (16x8)


Lorem ipsum dolo
r sit amet, cons
ectetur adipisci
ng elit. Fusce m
ollis pharetra a
nte, at mollis e
x rhoncus vel. P
raesent auctor d

Sunday Work

  • Look at pinout for TFT featherwing
  • Look at pinout for airlift featherwing
  • Probably start directly on an nrf52840 feather
  • Get the display up and running
  • Implement an embedded-graphics driver
  • Mess around with layout and style
  • Invent some style primitives
  • Find/rearrange some pins for the black pill
    • Chip selects
    • SPI peripheral (not the LED)

Feather Pinouts

       RST |         | }
       3v3 |         | } LiPo Connector
       ??? |         | }
       GND |         | }
    D18/A0 |         | VBAT
    D19/A1 |         | En
    D20/A2 |         | VBUS
    D21/A3 |         | D13
    D22/A4 |         | D12
    D23/A5 |         | D11
  SCLK/D15 |         | D10/A10
  COPI/D16 |         | D9/A9
  CIPO/D14 |         | D6/A7
UART RX/D0 |         | D5
UART TX/D1 |         | SCL/D3
       ??? |         | SDA/D2

Note: ??? pins change per feather

Adafruit TFT FeatherWing - 3.5" 480x320

Product link:

    nRESET |         | }
       3v3 |         | } LiPo Connector
         x |         | }
       GND |         | }
         x |         | VBAT---.
         x |         | EN     |-- Power Path ("+5V")
         x |         | VBUS---'
         x |         | x
         x |         | x
         x |         | x
      SCLK |         | TFT_DC   (Display Data/Command)
      COPI |         | TFT_CSn  (Display Select)
      CIPO |         | RT_CSn   (Touch Control Select)
         x |         | SD_CSn   (SD Card Select)
         x |         | x
         x |         | x

Schematic Link:

I may also want an extra pin for backlight control (PWM) or touch IRQs.

The main chips used for control are:

  • HX8357D - Display Control
  • STMPE610 - Touch Control

Adafruit AirLift FeatherWing – ESP32 WiFi Co-Processor

Product link:

      RST? |         | }
         x |         | } LiPo Connector
         x |         | }
       GND |         | }
        x? |         | VBAT---.
        x? |         | En     |-- Power Path (to 3v3 reg)
        x? |         | VBUS---'
        x? |         | ESP_CSn (gates CIPO)
        x? |         | ESP_RESET
        x? |         | ESP_BUSY
SCK        |         | ESP_GPIO0(nc)
COSI       |         | x?
CIPO       |         | x?
ESP_TX(nc) |         | x?
ESP_RX(nc) |         | SCL (ATECC)
           |         | SDA (ATECC)

Schematic Link:

  • En is part of of the power path routing, enables 3v3 reg
    • Common with main EN, hard to just gate wifi power
    • Maybe held in reset is enough? or dont common en?
  • CIPO is gated by ESP_CSn through a 74AHC1G125
  • ESP does not seem to connect directly to ATECC
  • ESP_RESET and RESET seem not to be connected

Boards seem compatible as long as we don't connect ESP_GPIO0, which is disconnected by default.

Feathers and the Calculator

We probably don't have enough feather pins to control:

  • Keyboard
  • Keyboard LEDs
  • WiFi
  • Display
  • Touch

Without some kind of port expander for either some of the selects, or for some kind of keypad controller.

What might make sense is to take an approach like BB Q10 Keyboard PMOD, and have a keyboard controller MCU that can connect via SPI or I2C.

I might want to pick something cheap + USB, like an STMG0 TSSPO20, and have the keyboard be standalone USB or SPI/I2C controlled. JLC seems to stock the STM32G031F8P6 (no USB, TSSOP-20, 0.88 EUR @ 10, extended part), the STM32G030C8T6 (no USB, LQFP-48, 0.78 EUR @ 10, extended part), or the classic STM32F103R8T6 (USB, LQFP64, 2.65 EUR @ 1, basic part)

I set up a poll to see what people think:

Display Notes

Initial command sequence for the HX8357D

// Step one: SW Reset and delay
HX8357_SWRESET, // 0x01
0x80 + 100 / 5, // Soft reset, then delay 10 ms(sic)
                // NOTE(AJM): actually 100ms

// Step two: SETC and delay
//   "Enable extension command"
HX8357D_SETC,   // 0xB9
3,              // NOTE(AJM): Three, because the next part
0xFF,           //   is the NOP+sleep command
0xFF,           // NOP command to cause a delay
0x80 + 500 / 5, // No command, just delay 300 ms(sic)
                // NOTE(AJM): actually 500ms

// Step three: SETRGB
//   "Set RGB interface"
HX8357_SETRGB,  // 0xB3
0x06,           // 0x80 enables SDO pin (0x00 disables)

// Step four: SETCOM
//   "Set VCOM voltage"
HX8357D_SETCOM, // 0xB6
0x25,           // -1.52V

// Step five: SETOSC
//   "Set oscillator"
HX8357_SETOSC,  // 0xB0
0x68,           // Normal mode 70Hz, Idle mode 55 Hz

// Step six: SETPANEL
//   "Set Panel"
HX8357_SETPANEL,    // 0xCC
0x05,               // BGR, Gate direction swapped

// Step seven: SETPWR1
//   "Set power control"
HX8357_SETPWR1,     // 0xB1
0x00,               // Not deep standby
0x15,               // BT
0x1C,               // VSPR
0x1C,               // VSNR
0x83,               // AP
0xAA,               // FS

// Step eight: SETSTBA
//   "Set source option"
HX8357D_SETSTBA,    // 0xC0
0x50,               // OPON normal
0x50,               // OPON idle
0x01,               // STBA
0x3C,               // STBA
0x1E,               // STBA
0x08,               // GEN

// Step nine: SETCYC
//   "Set display cycle reg"
HX8357D_SETCYC,     // 0xB4
0x02,               // NW 0x02
0x40,               // RTN
0x00,               // DIV
0x2A,               // DUM
0x2A,               // DUM
0x0D,               // GDON
0x78,               // GDOFF

// Step ten: SETGAMMA
//   "Set Gamma"
HX8357D_SETGAMMA,   // 0xE0

// Step eleven: COLMOD
//   "Color mode"
HX8357_COLMOD,  // 0x3A
0x55,           // 16 bit

// Step twelve: MADCTL
//   "Memory access control"
HX8357_MADCTL,  // 0x36

// Step thirteen: TEON
//   "Tear enable on"
HX8357_TEON,    // 0x35
0x00,           // TW off

// Step fourteen: TEARLINE
//   "(unknown)"
HX8357_TEARLINE,    // 0x44

// Step fifteen: SLPOUT and delay
//   "Exit sleep mode"
HX8357_SLPOUT,  // 0x11
0x80 + 150 / 5, // Exit Sleep, then delay 150 ms

// Step sixteen: DISPON and delay
//   "Display off(sic)"
HX8357_DISPON, // 0x29
0x80 + 50 / 5, // Main screen turn on, delay 50 ms

// End.
0,             // END OF COMMAND LIST

Initial transaction uses "sendCommand", which includes:

  • Begin transaction
  • Set CS Low
  • Set DC Low
  • Send command byte
  • Set DC High
  • Send data (N)
  • Set CS High

setAddrWindow uses "writeCommand", which

  • Sets DC low
  • Writes command (1B)
  • Sets DC high

Comments state that CS and transaction must already be set

Hmm, it seems the HX8357 is somewhat similar/compatible with the ILI9341? Seems like the biggest differences are in the command IDs, and some extended control options.

I should probably base my library on the style of the existing ILI9341 crate

It seems the Adafruit driver basically:

  • Set CS Low
  • Sets the bounding box + starts write to ram command
    • HX8357_CASET (x1, x2)
    • HX8357_PASET (y1, y2)
    • HX8357_RAMWR (...)
  • Writes pixels
  • Sets CS high

Calculator Planning

Okay, I should probably start gathering requirements and architecting the calculator project. Today is 2021-02-08, and I want to have a stable-ish "product" by 2021-02-14, which gives me six working days or so.

Let's start by gathering requirements.

Raw Requirement Brainstorming

These are not sorted, or qualified. I'll try to rate them as I go. Let's call these "system requirements". Some of these are bound to already-made decisions, like the display or keyboard. Some of these are soft-made decisions, like what parts I have on-hand already (or could get within my delivery window).

  1. The system shall have a 4x6 24-key keyboard
    • PRIO: HIGH
  2. The system shall reliably read keyboard inputs
    • PRIO: HIGH
  3. The system shall have software-remappable keyboard bindings
    • PRIO: HIGH
  4. The system shall support RGB LEDs per-key
    • PRIO: HIGH
  5. The system shall support a 19-pixel RGB LED notification bar
  6. The system shall support an RGB 480x320px display
    • PRIO: HIGH
  7. The system's display shall be readable from "arms length"
    • PRIO: HIGH
  8. The system shall support USB-C for power
    • PRIO: HIGH
  9. The system shall be programmable over USB-C connection
    • PRIO: HIGH
  10. The system shall be programmable using JTAG/SWD
    • PRIO: HIGH
  11. The system shall support WiFi connectivity
  12. The system shall support Bluetooth connectivity
    • PRIO: LOW
  13. The system shall support pre-loaded apps
    • PRIO: HIGH
  14. The system shall support SD-Card loaded apps
    • PRIO: LOW
  15. The system shall support text-grid-based apps
    • PRIO: HIGH
  16. The system shall support graphical-based apps
    • PRIO: LOW
  17. The system shall expose raw keyboard control to applications
    • PRIO: LOW
  18. The system shall expose a pre-mapped and managed "key stream" to applications
    • PRIO: HIGH
  19. The system shall expose a way to select a pre-mapped and managed key mapping by applications
    • PRIO: HIGH
  20. The system shall expose RGB LED control to applications
    • PRIO: HIGH
  21. The system shall support managed RGB functionality on a per-LED basis, such as fades, cycles, etc.
  22. The system shall support a Piezo-based buzzer
    • PRIO: LOW
  23. The system shall have a REPL-based calculator application
    • PRIO: HIGH
  24. The system shall have a time tracking application
    • PRIO: HIGH
  25. The system shall have a todo/checklist based application
  26. The system shall expose the ability to load/save files on the SD card to the application
  27. The system shall expose the ability to make GET/POST requests to arbitrary URLs to the application
    • PRIO: LOW
  28. The system shall support TLS for GET/POST requests
    • PRIO: LOW
  29. The system shall support T9-style text entry
  30. The system shall support rechargeable battery power
    • PRIO: LOW
  31. The system shall recharge the battery via the USB-C port
    • PRIO: LOW

Architecture Raw Thoughts

Okay, this is feeling a little formal to even me. Let's think a bit about how to arrange all of this.

I'll probably need some kind of "OS", either a bit of a scheduler on top of RTIC, or Neotron, or something.

Dynamically loading apps will probably be a bit of a pain, since I'll need a nice C-FFI boundary layer. I'd probably get that for free from Neotron OS.

SD Card and network operation will probably be sort of awful too. Especially if I want "multitasking" with background operations.

For now, I'll probably use something relatively light-weight, hardware wise, either an nRF52 (64MHz, 1MiB flash, 256KiB RAM), or a black pill (96MHz, 512KiB flash, 128KiB RAM). In the future, I might upgrade to an STM32H743VIT6 (480MHz, 2MiB flash, 1MiB RAM, external SD/QSPI/SPI flash), but probably wont for this week because I don't think I could physically mount the board well.

My gut wants to write it all from scratch, but JP has definitely covered a lot of ground with the neotron work. Maybe I should do a one-day "spike" to try and get neotron working on the STM32H7 and just shortcut some of the other hard work? I could probably make a shenanigans breakout board that would last a couple weeks.

I could probably start with the BIOS porting:

And the neotron user's manual:

Working on Stream

  • Try running code on STM32H7
  • Try to port the Monotron BIOS
  • Maybe figure out pin assignments

Board details:

Upside-down pinout

GND 5v0         GND 3v3
E1  E0          E2  E3
B9  B8          E4  E5
B7  B6          E6  VB
B5  B4          C13 NR
B3  D7          C0  C1
D6  D5          C1  C3
D4  D3          GND V+
D2  D1          A0  A1
D0  C12         A2  A3
C11 C10         A4  A5
A15 A12         A6  A7
A11 A10         C4  C5
A9  A8          B0  B1
C9  C8          B2  E7
C7  C6          E8  E9
D15 D14         E10 E11
D13 D12         E12 E13
D11 D10         E14 E15
D9  D8          B10 B11
B15 B14         3v3 5v0
B13 B12         GND GND

Topside Pinout

3v3 GND         5v0 GND
E3  E2          E0  E1
E5  E4          B8  B9
VB  E6          B6  B7
NR  C13         B4  B5
C1  C0          D7  B3
C3  C2          D5  D6
V+  GND         D3  D4
A1  A0          D1  D2
A3  A2          C12 D0
A5  A4          C10 C11
A7  A6          A12 A15
C5  C4          A10 A11
B1  B0          A8  A9
E7  B2          C8  C9
E9  E8          C6  C7
E11 E10         D14 D15
E13 E12         D12 D13
E15 E14         D10 D11
B11 B10         D8  D9
5v0 3v3         B14 B15
GND GND         B12 B13
        SD   USB

Of A and B, only pick one row (mostly)

Needed parts:

  • debug uart
    • Maybe USB? Maybe both?
  • SWD (maybe SWO?)
    • Internal SPI flash Seems to use SWO pin (PB3)
  • SPI bus 1 (regular), could be split
    • Display
    • Display SD Card (maybe, probably not)
    • WiFi Radio
  • SPI bus 2
    • WS2812 comms (can't be shared)
  • SDIO (on-board SD-Card)
  • Maybe SPI for on-board ext Flash
    • Maybe shared with "SPI Bus 1"?
  • Maybe QSPI for on-board ext Flash
  • GPIOs
    • 10 GPIOs for Row/Column
      • Next to eachother, one connector?
    • 4 for Display (CSn, D/C, SD CSn, Touch CSn)
    • 3 for ESP (Reset, Busy, CSn)
  • USB FS pins
  • I2C for QUIIC connector

What do I have?

  • 5 SPI ports?
  • 4 I2C ports
    • 1 LPUART
    • 4 UARTs
    • 4 USARTs
  • 1 QSPI
  • 2 SDMMC
 3v3    GND                  5v0     GND
xE3x   xE2x  - LED,QSPI      E0      E1
 E5     E4                   B8      B9
 VB     E6                  xB6x     B7   - QSPI
 NR    xC13x - Button       ?B4?     B5   - SPI1 (on-board flash)
 C1     C0                  ?D7?    ?B3?  - SPI1 (on-board flash)
 C3     C2                   D5     ?D6?  - SPI1 (on-board flash)
 V+     GND                  D3     xD4x  - SD Card
 A1     A0                   D1     xD2x  - SD Card
 A3     A2                  xC12x    D0   - SD Card
 A5     A4                  xC10x   xC11x - SD Card
 A7     A6                  xA12x    A15  - USB
 C5     C4                   A10    xA11x - USB
 B1     B0                   A8     xA9x  - USB
 E7    xB2x  - QSPI         xC8x    xC9x  - SD Card
 E9     E8                   C6      C7
!E11!  !E10! - On-TFT        D14     D15
!E13!  !E12! - On-TFT       xD12x   xD13x - QSPI
 E15   !E14! - On-TFT        D10    xD11x - QSPI
 B11    B10                  D8      D9
 5v0    3v3                  B14     B15
 GND    GND                  B12     B13
 ^^^    ^^^                  ^^^     ^^^
  X1     X2                   Y1      Y2
                SD   USB


  • V+ is "VREF+"
  • NR is "NRST"
  • VB is "VBAT_Pin"
  • SB2 (for SD Card SW) is NOT soldered by default

PH0 PH1 - HFCLK PC14 PC15 - LFCLK A11 A12 A9 - USB FS PC8 PC9 PC10 - SD Card PC11 PC12 PD2 - SD Card PD4 - SD Card (option, switch) PD13 PE2 PD12 - QSPI PD11 PB2 PB6 - QSPI PB4 PD7 - On-Board SPI flash 1, 3, 6 PB3 PD6 - On-Board SPI flash PE14 PE13 PE12 - Used for On-Board TFT Display PE11 PE10 - Used for On-Board TFT Display PC13 - On-Board Button PE3 - On-Board LED PA13 PA14 - SWD Pins


  • PLL Source: HSE
  • DIVM1: /5
  • DIVM3: /5
  • DIVN1: x192
  • DIVN3: x192
  • DIVP1: /2
  • DIVQ3: /20
  • System Clock Mux: HSE
  • D1CPRE: /1
  • HPRE: /2
  • D1PPRE: /2
  • D2PPRE1: /2
  • D2PPRE2: /2
  • D3PPRE: /2
  • USB Clock Mux: PLL3Q
 col  |  A1     A0  | col
______+  A3     A2  +_______
 SPI6 |  A5     A4  |
______+  A7     A6  | row
 row  |  C5     C4  |
______+  B1     B0  +_______

10 pin Keyboard IOs here? ^


  • 1: (PB7 or PB9) and PB8
  • 2: PB10 PB11
  • 3: N/A (SD and USB inhibit)
  • 4: (PB7 or PB9) and PB8


  • COPI (PC1, PC3, PB15)
  • CIPO (PC2, PB14)
  • SCK (PB10, PB13, PD3)

Misc notes:

  • MDIOS - remote config interface? For a host PC?
  • SWPMI - Single Wire Protocol... something





Neotron bringup steps

NOTE: Some of these links will be file-local for me, and won't work on the web rendered version of this document. Sorry. I'll try and get a hosted version working at some point.

  1. Fill out or stub out each of the BIOS API functions.
    1. api_version_get - Should be easy
    2. bios_version_get - Also easy
    3. serial_get_info - Not sure. Could return None for all calls, might not be useful though. I could probably fake a UART via defmt for output, but might need something for input. I could probably get a USB or TTL UART going from the HAL examples.
    4. serial_configure - I could probably force it to only accept a single configuration, based on whatever serial device I set up. Or it could lie and accept whatever config and just ignore it.
    5. serial_write - Output is easy, on defmt or a serial port.
    6. time_get - We can lie. Or use some clock or something. The 60Hz thing might be weird to handle. 1 frame is 546.133333 LFCLK ticks. 546 ticks would be an error of 0.024%, which is probably fine for now, or 60.015 FPS.
    7. video_memory_info_get - Setting up a framebuffer is probably easy, but the question is what to do with it?
    8. configuration_get - Probably okay to stub? Failing to store it is probably fine.
    9. configuration_set - Storing it in volatile memory to start with is probably fine.
  2. Determine memory layout for BIOS, OS, and apps
    • Probably just chunks of RAM for the OS/apps for now
  3. Write some kind of RAM loader for the OS from the BIOS
    • Maybe just a UART thing? Basic IHEX loader or something?
  4. Figure out how Neotron actually reads from a keyboard, uses audio, sdcard, etc.
    • Video is actually done here:

Share with Tanks

"How I Teach Embedded Systems"

Thursday work

  • Set up a UART - either USB or TTLUART, mostly for debugging
  • Set up rudimentary display drawing, even if it's just embedded-graphics with no optimization.
    • Probably keep the line-at-a-time DMA, if I can swing it.
    • One row (16x30 pix font) would take a little less than 4ms to draw in a blocking manner. This means I could get 25Hz at full-blast, probably constrain to 5Hz?
    • I could trigger a line update every 20ms, which would give me 5Hz net update rate. This would leave 16ms (80%) CPU remaining for everything else. Minus font rendering time :p
    • Figure out what the "option byte" is per character
      • 4 bit fg color, 4 bit bg color. Where are these colors defined?
    • Hope that the "every frame is 60Hz" is going to cause problems with my 5Hz real update rate
  • Figure out what needs to be done to report scan codes to the OS. Or lie, and pretend to be the ATMega keyboard (over UART) for now.
  • Actually build a physical 743 keyboard + display
    • Use a double-proto?
      • Would limit to single rows, probably BYO ribbon header? Split top and bottom.
    • Use a physical mount with 4x 2x6 breakouts on quarter-protos and fly-wires/ribbons to the main board?
      • Would require BYO ribbon directly to the MCU board
    • Needs:
      • SPI pinout for display
      • Row/Column pinout for keyboard
      • Level shfted SPI for WS2812b
      • UART pinout?
      • "Expansion Port" pinout?
  • Get a booting OS, even if it is 100% hot chip and lie
    • linker script? Flash location?
  • Get clocks and other misc interrupts set up

Cable pinouts

Options: 12, 8, 6. All 2xN.

  • Display 2x6 - Half Proto Compat - SPI2 + 5 GPIO
    1. COPI
    2. GND
    3. CIPO
    4. GND
    5. SCK
    6. GND
    7. TFT CSn
    8. GND
    9. TFT D/C
    10. GND
    11. SD CSn
    12. Touch CSn
  • Keyboard 2x6 - NOT Half-Proto Compatible! - 10 GPIO
    • TODO: direct pinout? Also 5v0 and GND?
    • Could skip pow/gnd if fulfilled by LED header (direct to dev board?)
  • LED 2x3 (TODO: Make sure not backwards). Can be done with three common-columns. Quarter/Half Proto Compat
    1. GND
    2. GND
    3. N/C!
    4. DIN
    5. 5v0
    6. 5v0
  • Debug 2x10 to eth to eth to fly wire
    • See spreadsheet
  • Expansion Header: 2x10 - Maybe?
  • WiFi Radio - Maybe? Needs at least 2x4

Rough Layout

o[                ]o       |     o[ e    ]o
o[                ]o       |     o[ x  s ]$----> SPI1
o[   xxxx         ]o       |     o[ t  p ]$----> I2C1
o[       >----12--]$-------|--.  $[xx  i ]o
o[ xxxxxx         ]o       |  +--8----<
o[                ]o   o[ prog  ext      ]o
  ----------------     o[ eth   con disp ]o
o[ xxx       xxx  ]o   o[                ]o
o[  v        xxx  ]o   o[espi============]o
o[  |         v   ]o   o[+i2c swd    usb ]$----> USB-C
o[  |         |   ]o   o[________________]o
o[  |         |   ]o
o[  5         8   ]o   o[      STM32H7   ]o
o[  |         |   ]o   o[             sd ]$----> SD Card
o[  |         |   ]o   o[    ============]o
o[  |         '---]$---$[----v    |      ]o
o[  |             ]o   o[  v------'      ]o
o[  |             ]o   o[ lvl            ]o
o[  |             ]o       v
o[  '-------------]$-------'
o[                ]o
o[                ]o
o[                ]o

3v3 GND         5v0 GND
E3  E2          E0  E1
E5  E4          B8  B9
VB  E6          B6  B7
NR  C13         B4  B5
C1  C0          D7  B3
C3  C2          D5  D6
V+  GND         D3  D4
A1  A0          D1  D2
A3  A2          C12 D0
A5  A4          C10 C11
A7  A6          A12 A15
C5  C4          A10 A11
B1  B0          A8  A9
E7  B2          C8  C9
E9  E8          C6  C7
E11 E10         D14 D15
E13 E12         D12 D13
E15 E14         D10 D11
B11 B10         D8  D9
5v0 3v3         B14 B15
GND GND         B12 B13
        SD   USB

Keypad pins

______               ______
 col  |  A1     A0  | col
______+  A3     A2  +......
 SPI6 |  A5     A4  |
______+  A7     A6  | row
 row  |  C5     C4  |
______|  B1     B0  |______

Edge Connector

1x10 - To Breakout

 1  | A0
 2  | A1
 3  | A2
 4  | A3
 5  | A4
 6  | A6
 7  | C4
 8  | C5
 9  | B0
 10 | B1

1x1 - To Level Shifter

1   | A5


On Quarter Proto

TODO: Find something that matches display

|  1      2  |
|  3      4  |
|  5      6  |
|  7      8  |
|  9     10  |
|  11    12  |

+5V0 and GND

Display SPI


TOUCH_CSn | D10    XXX | reserved
TFT_DC    | D8     D9  | SD_CSN
SPI2_MISO | B14    B15 | SPI2_MOSI
TFT_CSn   | B12    B13 | SPI2_SCK

Edge Connector

TODO: Double check pinout isn't backwards
2x6 Right Angle Pins
TODO: invert!

     COPI |  1      2  | GND
     CIPO |  3      4  | GND
      SCK |  5      6  | GND
  TFT_CSn |  7      8  | GND
  TFT_D/C |  9     10  | GND
   SD_CSn |  11    12  | Touch CSn

Expansion Port


    unused | E0    E1  | RESETn (exp out)
  I2C1_SCL | B8    B9  | IRQn
  reserved | xx    B7  | I2C1_SDA
 SPI1_MISO | B4    B5  | SPI1_CSn
 SPI1_MOSI | D7    B3  | SPI1_SCK

Edge Connector

1x8 Right Angle Pins
TODO: Top? Up? Bottom? Down?
TODO: Leave extra rows for extra expansion ports?

 I2C1_SCL  | 1
 RESETn    | 2
 IRQn      | 3
 I2C1_SDA  | 4
 SPI1_MISO | 5
 SPI1_CSn  | 6
 SPI1_MOSI | 7
 SPI1_SCK  | 8


TODO: Fixed expansion port addr? We'll only have one (for now)

     SPI_COPI | 1      2 | GND
     SPI_CIPO | 3      4 | GND
      SPI_CLK | 5      6 | GND
      ~SPI_CS | 7      8 | ~IRQ
      I2C_SDA | 9     10 | I2C_SCL
 EEPROM_ADDR2 | 13    14 | ~RESET
           5V | 15    16 | 5V
          3V3 | 17    18 | 3V3
          GND | 19    20 | GND

Make sure to pin out QWIIC connectors too

NOTE: The "Large" feather would work for an expansion header

Debug UART

Add some GPIOs here?

 UART4_TX | D1      xxx |
          | xxx     D0  | UART4_RX
      GND | 1
       TX | 2
       RX | 3

Board Layout

Top Row

             North West
          $  5v0     GND  $
          #  E0      E1   | RESETn (exp out)    }
 I2C1_SCL |  B8      B9   | IRQn                }
(CSn)QSPI - xB6x     B7   | I2C1_SDA            } To EXP
SPI1_MISO | ?B4?     B5   | SPI1_CSn            } 8
SPI1_MOSI | ?D7?    ?B3?  | SPI1_SCK            }
          #  D5     ?D6?  - SPI1 Flash CSn
          #  D3     xD4x  - SD Card (SW)
 UART4_TX |  D1     xD2x  - SD Card (CMD)       } To UART
  SD Card - xC12x    D0   | UART4_RX            } 2
  SD Card - xC10x   xC11x - SD Card
      USB - xA12x    A15  #
          #  A10    xA11x - USB
          #  A8     xA9x  - USB (vbus)
  SD Card - xC8x    xC9x  - SD Card
          #  C6      C7   #
          #  D14     D15  #
(IO1)QSPI - xD12x   xD13x - QSPI (IO3)
TOUCH_CSn |  D10    xD11x - QSPI (IO0)          }
TFT_DC    |  D8      D9   | SD_CSN              } To Display
SPI2_MISO |  B14     B15  | SPI2_MOSI           } Breakout
TFT_CSn   |  B12     B13  | SPI2_SCK            } 7
             North East

Top Top Row:

  • 01..=08 - 8 eth pins
  • 09..=10 - 2 dead pin
  • 11..=18 - 8 exp pins
  • 19..=20 - 2 dead pin
  • 21..=23 - 3 UART pins
  • 24 - 1 dead pins
  • 25..=30 - 6 display pins (+inner row)

Bottom Row

             South West
            Camera Conn.
          $  3v3     GND  $
      LED - xE3x    xE2x  - QSPI(IO2)
          #  E5      E4   #
          $  VB      E6   #
 SWD_nRST |  NR     xC13x - Button
          #  C1      C0   #
          #  C3      C2   #
          $  V+      GND  $
     COL2 |  A1      A0   | COL1                }
     COL4 |  A3      A2   | COL3                } To Keyboard
 spi6 sck -  A5      A4   | ROW1                } Row/Scan +
ws2812 DO |  A7      A6   | ROW2                } WS2812b
     ROW4 |  C5      C4   | ROW3                } 4 + 6 + 1
     ROW6 |  B1      B0   | ROW5                }
          #  E7     xB2x  - QSPI(CLK)
          #  E9      E8   #
   On-TFT - !E11!   !E10! - On-TFT
   On-TFT - !E13!   !E12! - On-TFT
          #  E15    !E14! - On-TFT
          #  B11     B10  #
          $  5v0     3v3  $
          $  GND     GND  $
               SD Card
             South East

Bottom Bottom Row

  • 4 Level Shifter pins
  • 5 LED pins

Soldering Guide

        =======     =======
    3v3 | GND |     | 5v0 | GND
    E3  | E2  |     | E0  | E1
    E5  | E4  |     | B8  | B9
    VB  | E6  |     | B6  | B7
    NR  | C13 |     | B4  | B5
    C1  | C0  |     | D7  | B3
    C3  | C2  |     | D5  | D6
    V+  | GND |     | D3  | D4
  ! A1    A0  !     | D1  | D2
  ! A3    A2  !     | C12 | D0
  x A5  x A4  !     | C10 | C11
  | A7  | A6  !     | A12 | A15
  ! C5    C4  !     | A10 | A11
  ! B1    B0  !     | A8  | A9
    E7  | B2  |     | C8  | C9
    E9  | E8  |     | C6  | C7
    E11 | E10 |     | D14 | D15
    E13 | E12 |     | D12 | D13
    E15 | E14 |     | D10 | D11
    B11 | B10 |     | D8  | D9
    5v0 | 3v3 |     | B14 | B15
  | GND | GND |     | B12 | B13
        =======     =======

Battery Powering

So, this board wasn't SUPER designed to be powered by a battery.

I think the easiest way to mod it would be to:

  • rip up U2 - the 3v3 reg
    • This is the 5-pin IC in the PWR cluster
  • provide a 3v3 rail via the header
  • rip up D10
    • This is the diode at the "top" of the power cluster, above the LED
    • Cut the far side, desolder the close side to preserve the pad
  • bodge VBUS to battery charger
    • Right side of the diode (closest to the USB port)
  • Provide VMax to +5v

The other option would be to provide my own USB-C connector, and connect the lines via header pins

Cool parts



Interesting project for audio

Clock Parts

Clock parts

Two 6x12 boards

  • Upper
    • Front
      • E-Ink
      • Piezo?
      • LEDs?
    • Back
      • Feather
      • Level Shifter
  • Lower - All I2C
    • Front
      • 7-seg
      • piezo?
      • LEDs?
    • Back
      • SCD30
      • RTC

2x12 WS2812 mini?

    • "Refreshing the display takes about 25 seconds.""

LED display driver

Arduino oscilloscope

Maybe something for the knurling test adapter?

Automation platform

E Paper Display




Adafruit EInk Display


Microchip 23K640

8KB/64Kb SRAM - Max Clock 20 MHz


Control chip seems to be part of the display assembly?

Resolution is 212 x 104

Guessing from the Adafruit code, the controller is either an IL0373 ("Z16" variant) or SSD1686 ("RW" variant).

Connector reads "WFT0213CZ16LW"

Seems to match Waveshare's similar hat product:

EPD waveshare doesn't seem to support 2.13" displays

Waveshare code seems to be here:

This folder has a couple potential matches?

This one looks like it PROBABLY is enough:

Ah! It looks like the "Z16" variant has the matching resolution (212x104), while "RW" is probably the more recent display which has a higher resolution (250x122).

Oh. This one is even called the "RW Panel with SSD1680":

And the original one says "Driver chip is the IL0373". I should practice my reading skills.

SVG IC Pinout

BeyondCompare Merge Scripts

# james@archx1c6g ➜  ~ cat ~/.scripts/
# COMPARE TWO REFERENCES (Branch, Tag, Commit), both in Read-Only Mode
#   Use (from the git repo root):
#     <path to script>/ master 123abcd

set -euxo pipefail

fixed_base=$(echo $1 | sed "s#/#_#g")
fixed_dest=$(echo $2 | sed "s#/#_#g")

# Build destination folder
DATEHASH=`echo "obase=16; $(date +%s)" | bc`

# Export Reference
git archive $1 | tar -xC $DESTFOLDER1
git archive $2 | tar -xC $DESTFOLDER2

# Fire Diff
bcompare -ro1 -ro2 $DESTFOLDER1 $DESTFOLDER2 -title1=$1 -title2=$2

# Clean Up
# james@archx1c6g ➜  ~ cat ~/.scripts/
#   Use (from the git repo root):
#     <path to script>/ master

set -euxo pipefail

fixed_base=$(echo $1 | sed "s#/#_#g")

# Build destination temp folder
DATEHASH=`echo "obase=16; $(date +%s)" | bc`

# Export Reference
git archive $1 | tar -xC $DESTFOLDER

# Fire Diff
bcompare -ro1 $DESTFOLDER $(pwd) -title1=$1 -title2="Working Directory"

# Clean Up
# Relevant .gitconfig sections
    lg = log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cI) %C(bold blue)<%an>%Creset' --abbrev-commit --
    e  = !sh
    e2 = !sh
    tool = bc3
[difftool "bc3"]
    trustExitCode = true
    tool = bc3
[mergetool "bc3"]
    trustExitCode = true

TODO: I should add this to my setup:

Board Design Weekend - 000


  • Design a simple board
  • Design a board with no SMT parts
  • STM32F4x1 "Black Pill"
  • Lego Mounts
  • PMOD-eque pinouts
  • Maybe a fancy 12 pin connector idea I have
  • Maybe some QWIIC connectors
  • Maybe a fancy debugger connector

Post scratch Layout notes

  • STM32F4 Black Pill
  • Ethernet Connector (as debugger)
  • 5xPMOD headers
    • 1x SPI
    • 1x I2C
    • 1x UART
    • 2x GPIO
    • NOTE: ALL PMODs repeated 3x
  • 1x12 pin "Everything" connector
  • 2x 1x6 Mounting Holes
  • QWIIC connectors (common to I2C PMOD)
    • Probably on the bottom
    • 1: GND
    • 2: VCC
    • 3: SDA
    • 4: SCL


Top: P1 Left Bot: P1 Right

TODO for tonight

  • Edit video stream
  • Start editing podcast
  • Think about newsletters?
    • Start with a blog?
    • Monthly?

screen idea:

Top Half

  • Stackup (top to bottom):
    • 1mm Full plate (256x180mm, or 32x22.5 lego or so)
    • 1mm screen gap
      • 219x156.5 hole for screen
      • bottom hole cutout
      • other board cutout?
    • 1mm screen gap
      • 219x156.5 hole for screen
      • no bottom hole
      • 1mm foam tape
      • Other circuit board?
    • 1mm top glass (256x180mm)

4mm total?

Bottom Half

  • Stackup (top to bottom):
    • 1mm acrylic top plate / 8mm keys + 2.2mm keys
    • 1.6mm keyboard PCB
    • ?? Spacer (components, battery)
    • 1.6mm mainboard PCBs
    • 1mm acrylic bottom plave

14.4mm - 20mm total

17.5 - 24mm total

Thicker than x1c (15mm)

Laser Cutters

    • Seems to do metals?
    • Seems expensive (from 50$ for 1)

Acrylic suppliers

    • 10 EUR + 5 EUR delivery
    • 1000 x 600 x 1 mm
    • 15 EUR w/ delivery
    • 300 x 300 x 3 mm

pwm audio filter

Another 70%:

8 pin buffer chip:

C5622, Nexperia 74HC244D,653 C96123, 1206 47uF cap C17557, 220 ohm, 1/8w, 0805 resistor C17408, 100 ohm, 1/8w, 0805 resistor C1525, 0402 100nF cap C17398, 1.8k, 1/8w, 0805 resistor C45783, 0805 22uF cap 2x8 pin outputs, each with a ground

Spring Project Survey

Here's a brainstorm of the projects that I have in flight. Maybe with some progress notes.

LiPo Stamp

  • Boards built
  • Partially assembled test rig
  • Need to finish test rig, new black pill breakouts can help
  • Not blocked by anything
  • Need to update project page

Sensor Clock

  • PoC running for ~2-3 weeks
  • e-paper display PoC working, needs to be wired up to embedded-graphics
  • Needs cleanups, probably i2c timeout handling, though watchdog is suitable.
  • Needs wireless comms to track data remotely
  • Maybe design more "final" PCB?
  • Needs speakers wired and driver written
  • Maybe wants some LEDs?
  • Maybe switch to bigger e-paper display?
  • Need to add project page

USB Keypad

  • Planning to "ship" first version for Laura
  • Finally got keycaps!
  • Needs "final" PCB(s) designed and built
  • Needs further keyberon integration (currently panics when using Keyberon)
  • Need to clean up the wire rats nest, either:
    • Some IDC cable assembly?
    • Hand wired/soldered connections?
    • Mix of the two?
  • Need to write a project page


  • Parts ordered (e-paper screen(s))
  • Dev board built, extension port to-do
  • Basic display work done
  • Need to do system bring up
    • Neotron bios hello-world done
  • Could design a PCB? no rush though, dev board should be suitable
  • Could think about case design, maybe with Laura?
  • Need to solder switches to keypad
  • Think about second (e-paper) display
  • Could use LiPo stamp
  • Need to update project page


  • 9.7" screen ordered
  • Probably will have same core as the calculator, should bring that up as a platform first.
  • Could use LiPo stamp
  • Needs a project page


  • Boards ordered
  • Need to get a basic BSP crate set up
  • I have some ideas for add-on shields:
    • RS-485 or Differential I2C for long range comms?
    • Battery shield?
    • Wireless shield?
    • "clink" PWM audio card
  • Design a lego "shadow box" with Laura?
  • Need to add project page

Fairy Lights project

  • Did some research on supercaps
  • It seems like supercaps + a buck regulator are going to make each board sort of expensive and large
  • Need to do some more research on cables, or even energy harvesting? Solar Panels? Precision charging laser?
    • Feb 22
    • Mar 6th
    • Mar 7th
    • Mar 9th

Feb 21 - Mar 9

I2C Register thoughts

Device  | Reg   | wire  |
Addr    | Addr  | size  |
        |       | (B)   |
        | 0x01  | 1     |
0x27    | 0x02  | 2     |
        | 0x03  | 4     |
        | 0x04  | 6     |
        | 0x01  | 1     |
0x35    | 0x02  | 2     |
        | 0x03  | 4     |
        | 0x04  | 6     |
        | 0x10  | 1     |
        | 0x11  | 9     |
  • We need a custom type? for each address
    • At least a collection of type/addrs
  • Feels sort of like a radix tree
  • Feels like an allocator would be nice
    • Maybe just a static/bump allocator? all data known at boot, maybe comptime
  • For 100+ devices, the "shadow table" could get pretty big

bbqueue based defmt sink

  • I need an atomic mutex container, to hold the Producer
  • Used framed messages
  • Way to recover messages in case of failure?
    • RAM buffer to drain to from watchdog interrupt?
    • Sort of application specific...
    • Any way to "recover" bbqueue state?
      • Probably not, stored in BSS...

BBQueue Storage Work

Use cases

Current: "Embedded Use Case"

  • Statically allocate storage
  • BBBuffer lives forever
  • User uses Producer and Consumer


  • Have the STORAGE for BBBuffer be provided seperately
  • Allow for uses like:
    • Statically allocated storage (like now)
    • Heap Allocation provided storage (Arc, etc.)
    • User provided storage (probably unsafe)

Sample Code for Use Cases

Static Buffer

static BB_QUEUE: BBQueue<OwnedBBBuffer<1024>> = BBQueue::new(OwnedBBBuffer::new());

fn main() {
    let (prod, cons) = BB_QUEUE.try_split().unwrap();
    // ...

Heap Allocation provided storage

Choice A: Simple

fn main() {
    // BBQueue<Arc<BBBuffer<1024>>
    // Producer<Arc<BBBuffer<1024>>, Consumer<Arc<BBBuffer<1024>>
    // Storage is dropped when `prod` and `cons` are BOTH dropped.
    let (prod, cons) = BBQueue::new_arc::<1024>();

Choice B: Explicit

fn main() {
    // EDIT: This is sub-par, because this would require `arc_queue`,
    // `prod`, and `storage` to ALL be dropped
    // before the buffer is dropped.
    let arc_queue: BBQueue<Arc<BBBuffer<1024>> = BBStorage::new_arc();
    let (prod, cons) = arc_queue.try_split().unwrap();

NOTE: This is not yet possible as of the current state of the repo. I do intent do support it.

User provided storage

Choice A: Naive

EDIT: Not this. See below

static mut UNSAFE_BUFFER: [u8; 1024] = [0u8; 1024];

fn main() {
    let borrowed = unsafe {
        // TODO: Make sure BBQueue has lifetime shorter
        // than `'borrowed` here? In this case it is
        // 'static, but may not always be.
        BBStorageBorrowed::new(&mut UNSAFE_BUFFER);
    let bbqueue = BBQueue::new(borrowed);

    // NOTE: This is NOT good, because the bound lifetime
    // of prod and cons will be that of `bbqueue`, which
    // is probably not suitable (non-'static). In many cases, we want
    // the producer and consumer to also have `Producer<'static>` lifetime
    let (prod, cons) = bbqueue.try_split().unwrap();

Choice B: "loadable" storage?

This would require EITHER:

  • The BBStorage methods are failable
  • The split belongs to the BBStorage item
    • (Could be an inherent or trait method)
  • Loadable storage panics on a split if not loaded
static mut UNSAFE_BUFFER: [u8; 1024] = [0u8; 1024];
static LOADABLE_BORROWED: BBStorageLoadBorrow::new();

fn main() {
    // This could probably be shortened to a single "store and take header" action.
    // Done in multiple steps here for clarity.
    let mut_buf = unsafe {
        &mut UNSAFE_BUFFER
    let old =;    // -> Result<Option<&mut [u8]>>
                                            // Result: Err if already taken
                                            // Option: Some if other buffer already stored
    assert_eq!(Ok(None), old);

    let bbqueue = BBQueue::new(LOADABLE_BORROWED.take_header().unwrap());

    // Here prod and cons are <'static>, because LOADABLE_BORROWED is static.
    // BUUUUUT we still probably allow access of BBStorage methods, which would be totally unsafe
    // EDIT: Okay, sealing the trait DOES prevent outer usage, so we're good on this regard!
    let (prod, cons) = bbqueue.try_split().unwrap();

NOTE: This is not yet possible as of the current state of the repo. I do intent do support it.

Environment, Send, and Sync blog post

These are some notes for a blog post I will eventually write.

NOTE: Want to discuss this post? Disagree with some of my claims? Chat about it on the tracking issue, or open a Pull Request to this markdown file.


  • Environment - All the resources available to a program. This environment could be provided by hardware, or managed by other software, including an operating system. Could include:
    • Address Space/Mapped Memory
    • Hardware peripherals (e.g. UART, FIFOs)
    • System Calls, other ABI-ish things
    • Access control to resources (managed by hardware like an MMU/MPU or silicon support such as Arm's TrustZone)
  • Program - A single compiled binary/application. May include statically or dynamically linked dependencies. Examples include:
    • A Rust OR C binary executed on a Linux hosted platform
    • A Rust OR C binary executed on a bare-metal application
  • Process - A single program (2) executing within a single environment. Examples include:
    • A single "process" on an operating system (todo: define 'operating system process' better)
    • A program executing on a bare-metal CPU
  • Thread - A single execution flow within a process. Every process contains at least one thread, and a single process may contain multiple threads, which are cooperatively or pre-emptively scheduled (1). Examples include:
    • A Rust thread spawned by std::thread::spawn
    • The main function of a Rust program (bare metal or hosted)
    • An Interrupt Service Routine running on a bare metal CPU
  • Send - A Rust term denoting that data may be TRANSFERRED between one thread and another. This is checked at compile time. Examples include:
    • Fully owned data
  • Sync - A Rust term denoting that data may be SIMULTANEOUSLY SHARED between one thread and another. This is checked at compile time.
    • data behind &'static references (inner mutability, etc.)

(1): Note: cooperative userspace scheduling, such as async/await in Rust, is not considered separately here, and only the single/multi-threaded nature of the executor is relevant.

(2): Note: In this doc, I only discuss "native" applications, and do not consider applications with an active language runtime or interpreter. These act as their own "environment" that may differ from that of the host.

Background Reading

Programming Languages vs Systems

TODO: More than just ideas

  • PLs serve as a tool to help address some system-domain concerns
  • PLs cannot (reasonably) understand all system-domain concerns, particularly WRT portability. This may be due to HW or OS differences
    • Ex: Mutexes and Deadlocks - Spinlocks vs Critical Sections
  • PLs (and compilers) can only act on concepts they understand, and that can be observed at compile time
  • Operating Systems are programs that manage the real-world environment, and provide abstract environments for the programs they run.

Embedded Metaphor claims

  • In a bare-metal environment on a single core with a single application, we would model execution as a single process, with the main task (non-interrupt context) acting as one thread, and each interrupt acting as a separate thread. However, some exceptions:
    • With an underlying OS, such as Tock-OS, which may partition the environment with an MPU/MMU, would then consider the OS and applications to be separate processes
    • With an underlying hardware support environment, such as Arm's TrustZone, the environment may be partitioned between secure/non-secure, and we would therefore consider the Secure and Non-Secure contexts as separate processes.
  • In a bare-metal multi-core environment, we must generally assume the code executing on each core is it's own PROCESS, UNLESS the environment is truly 100% shared.
    • TODO: Do caches and registers mean environment never truly converges?
  • Although multi-threaded programs (on the desktop) may run on multiple cores, this is ONLY possible because the OS (in this case the "environment-giver" manages to provide a consistent environment/abstraction to code running on all cores, including single memory address space, etc.)

General Claims

  1. PLs should not attempt to abstract/address all systems concerns
  2. PLs are generally only "natively" capable of addressing concerns within a single environment
  3. PLs may have tools for crossing environmental boundaries, but will not generally be able to make compile-time assumptions of correctness across these boundaries
  4. Processes entail (potentially) divergent/partitioned environments, and therefore cannot be handled "natively" by programming languages
  5. Libraries may be used to abstract over platform/environment-specific behaviors (ex: Rust's stdlib)
  6. Send and Sync are guarantees over threads, e.g. within a single environment, and can therefore not help us with interprocess comms
  7. Use of FFI code (including OS interfaces, if in C, or even in Rust today) entails an environment-crossing, unless the languages provide compatible guarantees, OR the compiler used understands both 'programs' that are stitched together.
    • TODO: This is still one environment, but the compiler can't necessarily help us. The developer must guarantee safety or other guarantees are upheld. This claim may need to be split/refactored.
  8. Although Rust and rustc can not be used to enforce correctness across multiple environments, we can still provide safe interfaces
    • Subclaim: This is due to runtime non-determinism, which can't be avoided as programs/hardware may behave unpredictably outside of Rust's view

Twitter/Matrix Quotes from me:

As a succinct example, while it may be safe to pass a shared reference from one thread to another, it may NOT be safe to pass a shared reference from one process to another, for example if that memory is not mapped in the other process. Dereferencing it would cause a segfault.

But Rust's safety guarantees can really only extend to the bounds of "one environment", which means that at the OS seam (or wherever you draw that for a multicore MCU), there are certain not-so-documented rules for environment traversal and management.

You see this when you have things like libc re-mapping memory in one space (violating Rust's environmental rules), or when you have assets that can't be sent from one process to another (without the OS "fudging" the environment to make it work)

That being said: You have to draw the line somewhere, and I think this is a fair place to say "this is a systems/OS/hardware problem/detail, not a programming language problem", at least for the current future.

To investigate

  • How to Tock-OS/Redox OS handle IPC, or multi-core kernels?
    • Thread Local variables?
  • This touches around thing like ABIs/ABI stability, but I don't really address it outside of FFI concern. I guess today we treat different versions of Rust/rustc as separate languages.
  • How do we talk about multiple langs in one binary? The compiler can't help us, but this is still a single program/environment. This gets back to "system concerns", where the developer must understand the interactions between languages.
  • Note that libcore does NOT make makes very few environmental assumptions, but libstd does has additional assumptions.

I really like this as a project template!

RS-485 thoughts

MAX3485ESA+T * 10mbps * 32 devices MAX3485EESA+T * 12mbps * 32 devices MAX3485AEASA+T * 20mbps * 32 devices MAX3491ESD+T * 10mbps * 32 devices

"The MAX3488AE/90AE/91AE have a 1/4-unit load receiver input impedance, allowing up to 128 transceivers on the bus."

Thoughts for the weekend:

  • Do some podcast episode(s)?
  • Do Laura's Keypad
  • Do M60 Keyboard

Project Shelf

Maybe use european gastronorm sizes to use metal trays instead of plastic?

  • "GN1/2" - 325 x 265 mm

    • Fits a 32x32 plate
    • 256 x 256 mm
  • "GN1/4" = 265 x 162 mm

    • Fits a 32x16 plate
    • 256 x 128 mm
  • US Quarter = 357 x 330

  • US Eighth = 241 x 165

  • GN1/2 on Amazon:

      • 5.75 EUR/ea
      • 3.99 EUR/ea
  • GN1/4 on Amazon:

      • 3.20 EUR/ea + 5 EUR delivery
      • 7.99 EUR/ea
  • Kallax is 13" inner

    • 33.02cm

You can get gastronorm racks, but most seem to be 1/1.

To read: Post about opportunity cost of information:

LLVM volatile semantics:

  • White Switch
    • 3x 10ct
  • Socket
    • 1x 90ct
  • Brown switch
    • 10x 10ct
  • Transparent caps
    • 1x 60ct
  • White caps
    • 2x 60ct
  • Black caps
    • 2x 60ct

I2C Bootloader Thoughts

  • Have the bootloader sit at the end of the region. The app image's reset vector must always point to the bootloader's start location.
    • This way the app gets to set the vector table EXCEPT for the reset vector
  • I could probably use DFU, but I probably also want the ability to customize the behavior.
    • e.g. "chain loading"
  • I probably want to have some ability to have the bootloader program to RAM, in order to update the bootloader itself. This could be done later, or you could boot to an app that reprograms the bootloader region
  • I need a second device to act as a test stand.
    • I could use two debuggers
    • I already have the nrf52 patches that include auto timeout for i2c
      • But no USB
    • I could add timeouts to the stm32f4 hal
      • Then use a pretty hal machine style RPC interface?
  • I need to implement i2c peripheral code for the g0
  • Should I just make an entire test rack?
    • I already have an everything connector
    • I could then just splice together two GPIO interfaces
  • Most of my boards provide 3v3 to QWIIC
    • Could use a custom breakout
    • Could use a custom cable
  • g0 hal already has flash page support!

Order of operations

  1. Write a basic flash-aware program to figure out the API
  2. Write an STM32F4 pretty hal machine (kta?)
  3. Probably port the timeout driver to the stm32f4
  4. Test it with an existing I2C device
  5. Write an STM32G0 I2C peripheral driver
  6. Write an application that can write to flash with I2C commands
  7. Write the rest of the bootloader



Thoughts on the STM32G0 I2C Peripheral Mode

Using RM0444 - dm00371828, for the STM32G0x1 platform

  • Supports Standard Mode, Fast Mode, and Fast Mode Plus
  • Has DMA support
  • Can do multimaster
  • Can do 7/10 bit addressing
  • Multiple 7-bit addresses, "2 addresses, 1 with configurable mask"
  • Supports optional clock stretching
7 bit addrXX
10 bit addrXX
Independent clockX
Wakeup from stopX

32.4.4 talks about clocking requirements.

Probably relevant when doing clock configuration.

32.4.6 talks about initialization

  • You must config + enable I2C peripheral clock

  • You enable the I2C by setting PE bit in I2C_CR1 register

  • When you disable with PE bit, a "software reset" occurs (see 32.4.7)

  • Analog filter is enabled by default, disabled with ANFOFF bit, or use a digital filter with DNF3..0 in the I2C_CR1 register

  • Table 164 talks about tradeoffs between analog/digital

    • Analog is available in stop mode w/ addr wakeup, digital isn't
    • Analog is variable with temp, voltage, etc.
    • Digital is programmable
  • Don't change filter while I2C enabled

  • Table 165 (and preceding text) talks a LOOOOT about timing calculation. This is probably important, but overwhelming

  • Don't change the timing while I2C enabled

Figure 287 is a flow chart of initialization steps. Basically:

  • Turn off I2C_CR1::PE
  • Configure I2C_CR1::ANFOFF and I2C_CR1::DNF
  • Configure I2C_CR1::NOSTRETCH
  • Turn on I2C_CR1::PE

32.4.7 Talks about software reset

  • It has a list of affected bytes
  • You need to keep it low for at least 3 APB cycles to do the software reset. They recommend:
    • Write PE 0
    • Read PE
    • Write PE 1

32.4.8 Talks about data transfer

There is a one byte buffer for each direction, shifted in/out.


  • Data is shifted in
  • On full byte:
    • If RXNE=0, then byte is put in I2C_RXDR
    • If RXNE=1, then clock is stretched until RXNE=0


  • Data is shifted out
  • On empty byte:
    • If TXE=0, then byte is copied from I2C_TXDR
    • If TXE=1, then clock is stretched until TXE=0

HW Transfer Management

  • I2C has a byte counter, enabled by default in master, disabled in slave
  • Controlled through I2C_CR2::SBC bit
  • Number of bytes for counter is I2C_CR2::NBYTES byte.
  • Also has a RELOAD bit for generating TCR flag when you need to load more than 255 bytes
  • You must clear RELOAD on last chunk

When not using RELOAD as a master, you can use AUTOEND to send a stop automatically when transfered. Or you can disable to possibly get a flag/interrupt when you need to START (restart) or STOP w/ software control

Table 166 shows master/slave configurations of SBC, RELOAD, and AUTOEND.

32.4.9 talks about slave mode

  • You must set at least one address
    • Configured with I2C_OAR1 and I2C_OAR2
    • OA1 is 7 bit by default, set in OA1MODE in I2C_OAR1
    • Enabled by OA1EN in I2C_OAR1 register
    • OA2 has masks for matching multiple wildcard'y addresses
    • Mask logic is odd, I don't get it yet.
    • OA2 is always 7 bit
    • OA2 is enabled by setting OA2EN in O2C_OAR2
  • You can enable General Call address by setting GCEN in I2C_CR1
  • You can get a flag or interrupt on address match
  • Usually the slave uses clock stretching. can be disabled
  • If using wildcard addrs, I2C_ISR::ADDCODE can be checked. Also check DIR for direction
  • Must clear ADDR flag to release stretch
  • If you don't stretch, you'll get all kinds of over/underrun error flags set

Figure 290 has a slave initialization flow chart. Preceeding text has some info about what can be updated when.

Figures 291-296 show the data transfer flow when stretching is enabled or not, and when sending or receiving

32.4.10 talks about master mode

Not relevant for now

32.4.11 shows timing calc examples

Probably just look at this when writing the clocking code

32.4.12-32.4.15 talk about SMBus

Not relevant for now

32.4.16 talks about wake from stop

Not relevant for now

32.4.17 talks about error conditions

Main errors:

  • Bus Error (BERR)
  • Arbitration Lost (ARLO)
  • Overrun/Underrun Error (OVR)
  • Packet Error Checking Error (PECERR)
    • SMBus
  • Timeout Error (TIMEOUT)
    • SMBus
  • Alert (ALERT)
    • SMBus

32.4.18 talks about DMA

  • Enable with TXDMAEN/RXDMAEN in the I2C_CR1 register
  • Slave mode: You have to set up the DMA before clearing the ADDR register (TX and RX)

32.4.19 talks about Debug mode

Probably don't care for now

32.5 talks about I2C low power modes

probably don't care for now

32.6 talks about interrupts

Super handy table of mnemonics, what sets flags, what interrupts are paired with flags, how to enable, how to clear, etc.

32.7 is a listing of all the registers



I have a fork of the g0xx hal, which implements a reasonable amount of i2c master capabilities

I probably want to offer:

  • blocking i2c slave (maybe? just for prototyping?)
  • interrupt i2c slave
  • dma i2c slave

I might want to de-macro the driver a bit

Initialization steps

  • initial settings
  • Clear {OA1EN, OA2EN} in I2C_OAR1 and I2C_OAR2
  • Configure:
    • OA1[9:0]
    • OA1MODE
    • OA1EN
    • OA2[6:0]
    • OA2MSK[2:0]
    • OA2EN
    • GCEN
  • Configure SBC in I2C_CR1
  • Enable interrupts and/or DMA in I2C_CR1
    • lol later
  • End


  • Pick an address (for now)
    • In the future, I should probably store this somewhere that can be re-flashed

Memory map

AddressPage IDXNotes
Application range
0x0800_00000NOTE: Reset vector MUST point to 0x0800_C000, MSP must be... ? No Flip Link?
0x0800_B80023Last App Page
Bootloader range
0x0800_E00028TODO: Move Bootloader Here?
0x0800_F80031Settings Page

Settings Page

  • I2C address?
    • App
    • Bootloader
  • Listen to pin(s) for bootload?
  • Listen to pin(s) for I2C addr?
  • Checksum

Do I want a separate command for changing settings? Or just allow it like any other subpage write?

Do sanity checks on settings?


  1. Bootloader boots
    • Disable all interrupts
  2. Is button held?
    • Y: GOTO 5
    • N: Continue
  3. Is bootload RAM flag set + magic word valid?
    • Y: GOTO 5
    • N: Continue
  4. Boot app at 0x0000_0000
  5. Bootload.
    • LED blink?
  6. Listen to I2C
    • Write transactions:
      • 0x40 - Start bootload
        • 4 bytes - total checksum (later crc32)
        • 1 byte - total subpages to write
          • NOTE: must be less than 23 * 8 for now
      • 0x41 - write page command
        • Must have set 0x40 already
        • Always 1 + 1 + 256 + 4 bytes
          • 1: 0x41
          • 1: Page/Subpage
            • 0bPPPPP_SSS (32 pages, 8 sub-pages)
            • 0bPPPPP must be <= 23
          • 256: subpage contents
          • 4 byte: For now: 32-bit checksum. Later, CRC32 or Poly1305?
        • On writes to subpage zero: erase page
        • Stretch write ack until checksum + erase + write?
          • I can't use the flash while erasing (or writing?) anyway
        • On writes to 0:0: make sure the reset vector is 0x0800_C000
          • Make sure MSP is maxval?
          • 0:0 must be the last thing written
      • 0x42 - reboot to new image
      • 0x43 - abort bootload
        • No data
      • 0x44 - Offer image downstream
        • Only after finishing 0x40 transaction
    • Read transactions
      • wr-then-rd 0x10 + 16 bytes => b'sprocket boot!!!'
      • wr-then-rd 0x11 + 4 bytes => maj.min.triv.reserved
      • wr-then-rd [0x21, P:S] + 260 bytes => Read subpage
      • wr-then-rd 0x22 + 1 byte => status
      • wr-then-rd 0x23 + 4 bytes => children flashed



  • Is bootload (of me): idle, active, complete, error (2-bit)
  • Does my app look reasonable? yes/no (1-bit)
  • Is bootload (of chain): invalid, idle, active (next), active (downstream), complete

TODO: On (critical?) error?

  • Finish current transaction
  • Disable I2C (auto-naks)
  • reboot
  • Probably not for checksum errors, unless a lot

Compile time to-do for bootloader

  • Change memory range
  • Disable interrupts
  • Ensure Reset is at base of flash (add linker assert?)

Boot Flow Chart

  • Boot
  • Check Memory
    • Are we commanded boot? No Command?
  • Check Flash
    • Is the flash settings page sane?
  • Were we specifically commanded to boot to app?
    • Yes? Boot.
    • No? Don't.
  • Start clocks
  • Check buttons

Bootloader power on sequence

  • Sequence
    • RAM: Is a command set?
      • Yes(App): Boot App
      • Yes(BL): Force Bootload
      • No: Unset
    • Flash:
      • Load RSV + MSP + ?
      • If good_app(rsv/msp) + good_settings(rsv/msp) + !force bootload
        • Yes => If Boot App:
          • Boot App Immediate
          • No: Unset
        • No => Force Bootload
    • Power on Clocks
    • Buttons: Both pressed?
      • No: If Unset => Set command, reboot
      • Yes => Force Bootload
    • If Boot App: Boot
    • If Bootload: Continue
    • Start I2C, run boot machine

Sprocket Ideas

  • 4-device test stand?
    • Use defmt-test as the orchestrator?
  • Set up HAL/BSP
    • Include rolling timer
    • Include I2C support
    • Include LED support
    • Include smartled support
    • Include button support
    • Include bootloader support
  • Set up repo/docs
  • Set up more reasonable programming/debugging stand
  • Set up defmt-over-i2c

Non Sprocket ideas

  • LED panel shield
  • RS-485 shield
  • LED cube design
  • Podcast editing
  • Laura's Keyboard
  • Breakout boards for Ali purchases
    • Limit switch
    • Toggle switch
    • MX switches
    • Momentary LED buttons
      • 12mm
      • 25mm
  • Breakout boards for Ama purchases
    • ESP8266
    • Potentiometers
    • Soil probe
    • Displays
      • LED
      • 7 seg
      • OLED

Thoughts on Sequenced State Machines

So, the boot machine works well because the type is always known:

fn main() {
fn(&mut Self) -> Result<bool, E>;

However, this generally forces runtime checking to be:

  • up to the user to state and verify preconditions
  • potentially faulty at runtime

I could end up doing something like async/await, but that requires (probably) boxing

I can sort of see this as a tree of states, with each leaf state specifying a "goto" (for lack of a better word) of where on the graph to return to.

The queue doesn't play well with different types, unless I use dynamic dispatch.

I don't want to repeatedly pay checking costs, on state transition might be okay.

I wonder if I'm just looking for generators.

I already have the control flow and nodes stored, I wonder if there was a more type safe way I could define the states and transitions.

How can I make sure that the sequences ONLY describe an acceptable flow? e.g

    -> B
    -> C
    -> D
    -> A
    -> D
    -> B
    -> A

Hmm, this can also describe success and error conditions, but also many success conditions?

Is this just HSMs with de-duplicated common trees?


  • AB

Subroutines and States

Maybe it makes sense to separate states (which only schedule), vs subroutines, which strictly perform nonblocking actions.

Basically in my boot machine code now, any code that sets the state queue, should be a state. Anything that doesn't, is a subroutine, or part of a subroutine.

Project Survey - Q2 2021

These are projects that are in flight at the moment.

  • LED Cube
    • Current Status
      • Proof of concept done, has one damaged LED (probably)
    • Potential Next Steps
      • Build version with Sprocket?
      • Design PCB, mass produce-ish?
  • Keypad
    • Current Status
      • PCBs designed and built, PoC Keyberon app written
    • Next Steps
      • Finish Laura's configuration

316L Wire

28 AWG SS316L is 2.910 ohms/ft

Thinking of async demos

  • Something with I2C periph + controller?
  • I have buttons, leds
  • Something with message filling and deserialization?
  • async is a property of IO

Okay, what would I actually want to await on in an embedded system?

  • Timer
    • This would probably be for timeouts or "scheduling".
    • I could probably use groundhog for a global instance?
    • I guess I could have a queue that wakes intelligently, with a fallback to polling if the queue is too long or something?
  • Serial Protocol
    • I guess the peripheral could be owned?
    • We're probably waiting for one of two things to happen:
      • Waiting for an event (e.g. peripherals, UART RX, buttons) we don't control
        • Would this make sense to even handle in async context? Wait for a flag?
        • Operate the entire async state machine in the interrupt context?
      • Waiting for a transfer to complete (could be rx or tx, DMA basically)

Is there any other kind of thing we could be waiting for?

I guess I could see "downstream" futures/async tasks waiting on async items from other tasks

What if we made those go through channels? Then we'd still probably have to poll on the channels, or implement a waker of some kind.

I guess the problem with wakers is that they are of a variable size. I guess we could go full RTIC and have a declarative DSL that registers all wakers ahead of time?

This might be possible if we disallow dynamic task spawning. I wonder if I could make this happen in an RTIC task with message sending or something?

I need to come up with a good use case to have an axe to grind. I think the keyboard might be a really good use case, because I have buttons and LEDs and stuff, the LEDs could be doing some kind of visual pattern, but still be responsive to input?

Something like:

  • Leds go through a pattern
  • If there is a key input, they trigger a ripple or something?
  • We have buttons that do some kind of debouncing, e.g. wait for low, wait 2ms, check again, if still low, trigger, wait for going high (with debounce?)
    • Can I really have interrupt driven keys with row/scan behavior? I guess that's more polling.
    • Maybe just look at one column or something so I can use interrupts still.

Async thoughts

Are these good async demos?

  • An async button or two, that do something like cycle colors or turn off the LEDs
  • an async SPI driver that handles sending the WS2812 commands
  • An async i2c port listening for color/brightness/mode values for 2 LEDs

I2C port

  • Event driven: We are responding to incoming commands
    • Have a couple different API items:
      • Mode (off, solid, blink, updown)
      • Brightness
      • Color
    • Have two register sets
    • Could benefit from interrupts
  • Outputs...?
    • Event stream to another Cassette?
    • Poll API for current state?
    • Pub Sub?


  • Purely event driven
    • Basically this is just the exti interrupt? Or polling I guess?
  • Outputs...?
    • Event stream?
    • Poll API?


  • Really just sequencing outputs
  • Could have it's own state for sequencing?
  • Could potentially benefit from DMA/interrupts?
  • Could potentially allow for multiple concurrent senders?


Brain Default

  • RTIC
  • EXTI interrupt task
  • SPI task
    • Mostly for DMA reload?
  • I2C task
    • emit changes/updates?
  • IDLE task
    • Doing periodic updates


  • EXTI future
    • allows for waiting on events like transitions?
    • async task that awaits on futures, and sends messages to N channels?
  • SPI/smartled task
    • Listens for messages to send (but how does it know to wake up?)
    • Calls future enabled SPI driver
    • Has some sort of "current status" interface?
  • I2C Peripheral
    • pretty much all in the interrupt?

“When you build a thing you cannot merely build that thing in isolation, but must also repair the world around it... so that the larger world at that one place becomes more coherent, and ...takes its place in the web of nature.”

— Christopher Alexander (A Pattern Language, 1977)

static FUTURES: impl Trait = Option;

#[interrupt] fn im_an_interrupt() {


fn real_main() { loop { cassette_1.poll_on(); } }

async fn main() { loop { match select!(queue1, queue2, queue3) { Queue1(msg) => { cassette::spawn_hi_prio(async || { // impossible right now dispatch(msg).await; } } LowPrioQueue(msg) => { foo(msg).await; bar(msg).await; // HERE (waiting for a 10s timer) baz(msg).await; } _ => todo!() } } }

TODO: There's basically no way I can "short circuit" handling of a slow task to handle interrupts correctly, and there is no way to move async processing into a task.

Does this mean interrupts serve only as a waker optimization?

Intro to Embedded Course Material

Note: This is an unofficial draft. Content may change, and the course has not yet been offered.

16x 60-90m video lectures, Q&A section at end, take-home assignments based on material from lectures.

Potentially additional office hours.

Stage One - Circuits

  • 01 - Introducing the parts + Concepts
    • Microcontroller
    • USB Helper device (second MCU?)
    • Switches, LEDs, Resistors, etc.
    • Any other external components
  • 02 - voltage and current
    • Using a multimeter
    • Ohms Law
    • Passive components
  • 03 - Reading a circuit diagram and common components
    • Building circuits on a bread board
  • 04 - Active Devices
    • Transistors
    • Regulators
    • Digital Components

Stage Two - Microcontroller

  • 05 - Writing Code, Flashing, Logging, Debugging
  • 06 - GPIOs and Timers
  • 07 - Peripherals, and Memory Mapped Registers
  • 08 - ADCs + PWM
  • 09 - RAM and Flash Memory
  • 10 - Reading a datasheet

Stage Three - External Devices

  • 11 - Serial Protocols: UART, SPI, I2C
  • 12 - Talking over UART - To PC
  • 13 - Talking over I2C - To Sensors
  • 14 - Talking over SPI - To Display

Stage Four - Doing Less (in software)

  • 15 - Interrupts
  • 16 - DMA

Anachro Bus Routing plan

I want some kind of "message board table" based on received/sent message types

For routing, you always echo messages from one bus to the other.

Everyone just sends to "everyone" (for now?) If you are a dom, you periodically transmit frames on your turn If you're a sub, schedule in downstream incoming frames either: send out a dedupe message every 15-30s

When joining a network:

  • Send dedupe code (maybe also make this the announce message?)
    • This triggers other device to start sending message?
  • Wait for N seconds to see if an echo occurs

Dedupe message is just 4 bytes of unique id, 4 bytes random number. If you hear the message come back, disregard that channel for 30-60 seconds, set some warning code (yellow light)?

Device discovery?

Avoiding fixed device ids?


In the future: maybe have some kind of crypto here? Probably not wanted/needed here yet.

Overall summary

  • Have a "broadcast" window, something like 1ms
    • Dom sends out random u32 and largest available ID range
    • Anyone not on the bus must jitter between 100us-900us
    • Then send their IDu8, plus (ID as u32 muladd random)
    • "READY" state achieved!
    • If not ready, wait for next round
    • Send ACK for any success number
  • For each READY number, next
    • Send new random number challenge
    • Respond (jittered) with response
    • If multiple good responses for one number, NAK number
    • "SET" state achieved!
  • For each SET number, next
    • Send new random number challenge
    • respond EXACTLY 10us (or something)
    • "ACTIVE" state achieved
    • Send "active ack"


  • Spend 999ms doing normal stuff
  • Top of every second:


fn main() {
// Note: opt_ids: None => "all call" response message
async fn broadcast_initial(min_us, max_us, total_us) -> (Timer::Tick, Vec<Response>) {
    let routing_mtx = acquire_routing().await;
    let (min_id, max_id) = routing_mtx.get_id_range();

    let bus_mtx = acquire_bus().await;

    let rand_nonce: u32 = rng.gen();
        gen_broadcast(rand_nonce, min_us, max_us, min_id, max_id)
    let start = timer::now();

    let messages: Vec<BroadcastMsg, 8> = Vec::new();

    // TODO: How do we keep the messages coming?

    loop {
        let elapsed = timer::micros_since(start);
        if (elapsed >= total_us) || messages.is_full() {
        let remaining = total_us - elapsed;

        if let Ok(Some(msg: BroadcastMsg)) = bus_mtx.receive_timeout(remaining) {
            // TODO: Checks ID is free (needs routing table!)
            if msg.check(rand_id) {


    // We are now 1s later, immediately send ACKs
    let mut seen_ids = hash_set![];
    let mut dupe_ids = hash_set![];

    resps.iter().for_each(|r| {
        let dupe = seen_ids.insert(;
        if dupe {

    let good_resps = resps
        .filter(|r| !dupe_ids.contans(

        for_each(|r| bus_mtx.send_ack(;

    (start, good_resps)

async fn manage_discovery() {
    async_sleep_millis(timer::now(), rng.rand_range(250..=750)).await;

    loop {
        // NOTE: on success, takes 1ms or so
        let (ready_start, readys) = broadcast_initial(None, 1_000, 9_000, 10_000)

        if readys.is_empty() {

        defmt::info!("{:?} Readys, checking...", readys.len());
        async_sleep_ms(ready_start, rng.rand_range(700..=1000)).await;

        // NOTE: on success, takes 250-1000us or so
        let (steady_start, steadys) = {
            let bus_mtx = acquire_bus().await;
            for ready in readys {
                broadcast_ping(&mut bus_mtx, /* ??? */).await;
                // TODO
            // ?

        if steadys.is_empty() {

        defmt::info!("{:?} Steadys, checking...", steadys.len());
        async_sleep_ms(steady_start, rng.rand_range(700..=1000)).await;

        // Note: takes 50us
        let (actives_start, new_actives) = {
            let bus_mtx = acquire_bus().await;
            for steady in steadys {
                broadcast_ping(&mut bus_mtx, /* ??? */).await;
                // TODO
            // ?

        if new_actives.is_empty() {

        // Add new devices to be processed
        let routing_mtx = acquire_routing().await;
        new_actives.for_each(|r| routing_mtx.push_new_id(r));

        defmt::info!("{:?} New devices!", steadys.len());
        async_sleep_ms(actives_start, rng.rand_range(700..=1000)).await;


Mostly the opposite as above, but also:

  • If in the READY/STEADY/ACTIVE/CONNECTED state, don't respond to all call
  • If >= 3 seconds from last ACK without moving forward (except in connected), reset to IDLE
  • MAYBE don't bid on every all-call? 1/2? 1/4? 1/8?

Broadcast/response - broadcast_initial

Dom sends

# broadcast initial
[ random_number: u32 ][ min_wait_us: u32 ][ max_wait_us: u32 ][ min_id: u32 ][ max_id: u32 ]

After rng.gen_range(min_wait_us..=max_wait_us) us, Sub sends back:

# broadcast ack
[ own_id: u32 ][ own_id_checksum: u32 ]


  • own_id: rng.gen_range(min_id..=max_id)
  • own_id_checksum: own_id.wrapping_mul(random_number).wrapping_add(random_number)

If successful, Dom sends:

# broadcast ackack
[ own_id: u32 ][ SUCCESS_MAGIC_WORD: u32 ]

Then twice: - broadcast_ping

Dom sends

# broadcast confirm
[ random_number: u32 ][ min_wait_us: u32 ][ max_wait_us: u32 ][ id: u32 ]

where id should == own_id and random_number is a TOTALLY NEW random number

TODO: Maybe just make this a dest field, with the initial broadcast being 0 or something

After rng.gen_range(min_wait_us..=max_wait_us) us, Sub sends back:

# broadcast ack
[ own_id: u32 ][ own_id_checksum: u32 ]


  • own_id: rng.gen_range(min_id..=max_id)
  • own_id_checksum: own_id.wrapping_mul(random_number).wrapping_add(random_number)

If successful, Dom sends:

# broadcast ackack
[ own_id: u32 ][ SUCCESS_MAGIC_WORD: u32 ]

Routing thoughts

Okay, so above I described "everyone broadcasts everwhere", but I think I need to decide what kind of stuff I need.

I think I have two main choices:

  1. Keep the bus everywhere idea
    • All devices would basically just look for specific message IDs on the bus
    • All devices would have to listen and decode (or at least filter packed ID) continuously
    • This probably wouldn't work great with Anachro, which is more point-to-point
    • This wouldn't require any kind of "coordinator" role
  2. Do real routing, focus on (sender, receiver) pairs
    • Easiest way: Expect a DAG, with the arbitrator "rooted" at the terminal node
      • Have "send up" and "send down" primatives
        • They push an addr on each level
          • moving up push on sub -> dom
          • moving down push on dom -> sub
          • What about sub/sub devices?
        • When we hit the terminal node, the message stops propigating

Prop routing

  • dom only (head)
  • sub only (bus OR tail)
  • dom/sub (head + bus)
  • sub/sub (tail + bus)
  • dom/dom (head + head, not possible yet)

Example network 1

  • Device A - dom only
    • Device B (id: 1) - sub only (bus)
    • Device C (id: 2) - sub only (bus)
    • Device D (id: 3) - dom/sub (bus)
      • Device F (id: 1) - sub only (bus)
      • Device G (id: 2) - sub only (bus)
      • Device H (id: 3) - sub (tail)
    • Device E (id: 4) - sub only (tail)


Device A would be prompted (how?) to send a "Send down" message:

  • to B: [1]
    • Terminates here
  • to C: [2]
    • Terminates here
  • to D: [3]
    • To F: [3, 1]
      • Terminates here
    • To G: [3, 2]
      • Terminates here
    • To H: [3, 3]
      • Terminates here
  • to E: [4]
    • Terminates here

Foreach terminator, respond with the end message. Responses:

  • [1, $anachro_id]
  • [2, $anachro_id]
  • [3, 1, $anachro_id]
  • [3, 2, $anachro_id]
  • [3, 3, $anachro_id]
  • [4, $anachro_id]

I guess what I really want is to be able to include a unique ID in the response, like the anachro uuid. I guess it's my bus, why not?

OH, but I guess I don't need a GLOBAL routing table, just:

  • Which way is the anachro arbitrator
  • Which anachro devices are on which sub nodes

Can I use this to handle the sub/sub tail case?

  • Rooted node starts
    • Subs log onto the bus
    • If a Sub is also a dom:
      • Start domming

The process for "joining" the anachro message bus is:

  • If we're a power-dom (anachro arbitrator), just start domming immediately
  • Otherwise, wait for one of the ports to become active


  • power-dom: anachro router + 1x dom port
  • switch: anachro device + 1x dom port + 1x sub port

Pains in the ass

  • What if a tail is also a sub on a bus?
    • This is annoying, because they are a dom on neither interface
    • This requires subs to ALSO have a routing table, I guess

Routing thoughts

Routing Thoughts

The algorithm

Quick reminder on terminology:

  • We have nodes that have two "networks" they are connected to:
    • A single cap port on one network, where they are used to terminate either end of the bus
    • A pair of bus ports on the SAME network, which are used for daisy-chaining
  • We have two roles a device can play on each "network" they exist on:
    • dom: Which means they coordinate/"command" the bus
    • sub: Which means they respond to commands from the dom

Previously: I thought that doms would always operate on their cap port, but this isn't/doesn't always have to be true!

Since my goal is to make a DAG from the "PC" link (or some generic "uplink" connection), I might actually do my routing by basically having all the nodes being "asleep"/"silent" until spoken to, then they wake up and figure out what role they take.

Basically: if they get "woken up" on a network, they are a sub on that network.

After being "woken up", they then check their other network connection. If anyone responds, they become the dom of the other interface.

The PC uplink will be... a little different, in that the PC-attached device will be a dom of both busses, even though it was woken up by the PC. I guess this is still consistent, as it is basically a sub on the PC link, and a dom on all other networks.

The example

So, I guess the sequence for III (in the diagram above) would be:

  1. A is woken up by the PC link
  2. It checks its Cap port. It gets some kind of response on the bus
  3. It checks its Bus port. It gets no response. That port now becomes idle (maybe checking again later)
  4. A begins enumerating devices on the bus. At some point, it now knows about B, G, and L.
  5. Once B, G, and L are awake, they each check their OTHER port. They each find:
  6. B enumerates C, D, E, and F - so B is a sub on it's bus, and a dom on its cap. C, D, E, and F are all only subs (because they find no devices on their other ports
  7. G enumerates H and I - so G is a sub on its bus, and a dom on its cap. H and I are only subs (same as above)
  8. L enumerates J, K, M, O, P, and Q - so it is a sub on the cap, and a dom on the bus
  9. J, K, M, O, P, and Q go through the same dance
  10. J, K, O, P, and Q are subs-only
  11. M enumerates N, so M is a sub on the bus, and a dom on the cap
  12. N checks, finds nothing, sub only, process complete

Since the PC could potentially "see" (or be notified) of all of these changes, I guess the PC itself could determine the DAG, or the shortest path, even in the case of a loop, and just command-back the routing path necessary to return to the PC.

I don't really need or want the bus to be totally self standing (famous last words)

Thoughts on PowerBus Rev 2

  • Have a shunt resistor + op amp for measuring 5v usage
  • Have some buttons on-board
  • Include the stm32g 8x booster board? Optionally?


Take a timer and a uarte and two gpios

Basic RX state machine:

  1. Idle, but pended
    • If TX pending: goto 6
    • Else: goto 2
  2. Prep Idle RX
    • Disable timer
    • Set GPIOs
    • Clear "Receiving" flag
    • Clear "Flush" flag
    • Enable RXDRDY Interrupt
    • Enable STOP interrupt
    • Start receiving with some max size
  3. On First Byte
    • Verify+clear RXDRDY event
    • Disable RXDRDY interrupt
    • Set "Receiving" flag
    • Enable RXDRDY -> Timer Clear shortcut
    • Start timer
  4. On Timeout
    • Disable timer
    • Set "Flush" flag
    • Trigger STOPRX event
  5. On Stop
    • Check+clear end_rx
    • trigger FLUSHRX
    • Wait for Endrx
    • clear end_rx
    • goto 1
  6. Start TX
  7. On completion: goto 1

What to do

Okay, the problem is:

  • Some parts of my network stack expect "direct responses" to certain messages - like the discovery code. It might be okay to special case this, but it might not be.
  • Some parts of my network stack expect a "stream of responses", where you are essentially just shuttling the queue

Honestly after worring about this so much - I'm inclined to just ngaf about it. I can declare (for now at least) that you just SHOULDN'T queue packets from a device (on the sub side) until you have a confirmed address, and you shouldn't queue packets to a device (on the dom side) until you have completed the discovery process.

Doing this will let me get on with my life.

Additionally, for now, I can probable cheat with functionality:

Use one port per thing, and just send all broadcast messages.

Since there is no routing yet, everyone will hear everything. Thats fine. We can just process messages using the port as the index to which data type we expect here. For the simple use case of:

  • Set the color
  • Set the brightness
  • Report motion

Ideally also:

  • Send logs
  • Do OTA/"go to bootloader" (where the bootloader also can talk on the bus)

More thoughts

Okay, this is MOSTLY correct, but still problematic on the dom side, because we need to interleave discovery with normal operation, which means we need a completely clear queue to do discovery. I feel like the only method right now would be to inhibit outgoing messages?

I guess the dom could do a couple of messages, and they'd just get interleaved? I guess the bigger problem

More more thoughts

Okay, the high prio outgoing queue seems to fix things, but I'm starting to get to the point for the dispatch and uarte-485 parts might want to be specialized into Dom and Sub variants.

Specifically the sub doesn't need a high prio queue, the dom needs different "authorized to send" logic, etc.

Rethink logic

Okay, let's think about the IO-side logic.

Mostly: How to poke a running system? For the dom side, that isn't so bad. We have receive windows, and will progressively drain the queue. But on the Sub side, we need SOME way for dispatch to "poke" the driver to drop out of receiving and to start a send when prompted. But right now, all the logic involves pending the interrupt, which means I need some back and forth.

Oh, we also need to poke the dom if it is currently idle.

Dom side

  • Should always default to sending
    • Should have a (min?) window of time to listen for
    • Auto-reload listening
    • Once timer expires, return to sending or idle

Sub side

  • Should always default to receiving
  • On command, send one message, then return to receiving

Thoughts for anachro

  • Process N messages from one RX, in case they overlap?
  • When responding, wait at least 100us after message rx to make sure messages not overlapping


Okay, let's build a simple bootloader

We have 256 pages, 4KiB each, for a total of 1MiB.

I rarely need more than like... 64KiB, so let's just chop it up into for sections:

  • Section 0:
    • Pages 0..64
    • Bootloader goes here (fixed addr)
  • Section 1:
    • Pages 64..128
    • This is the "active" sector
    • Application goes here (fixed addr)
  • Section 2:
    • Pages 128..192
    • This is "backup page A"
  • Section 3:
    • Pages 192..256
    • This is "backup page B"

The bootloader

The bootloader is really simple. It just does the "flash to active section" operation, and jumping to main.

It should:

  • On boot, check and see if a new image was flashed (some kind of header metadata?)
    • If so, check the validity, at least:
      • Checksum/signature of the image
      • Maybe check the reset vector is within section 1?
    • Erase Section 1
    • Flash Section N to section 1
    • Write metadata page
    • GOTO boot
  • Check and see if an image was just flashed, but didn't meet "live" check
    • If so, check the validity of the "other" image
    • If valid
      • Do the flashing steps above
    • If not:
      • Blinky forever :(
      • TODO: Serial bootloader recovery
  • Boot
    • set vtor
    • set "info passing" page
    • bootload

Data Layouts

Section 0

It's just a regular app image. Nothing special.

Section 1

  • Page 64: Metadata
  • Page 65..128: Application


0000001616Image UUID
0016003216Image Poly1305 Tag
003200364Image Len (pages)
012801324Boot sequence number
013201364Has been flashed tagword
013601404Has fully booted tagword

Section 2/3

  • Page 64: Metadata
  • Page 65..128: Application


0000001616Image UUID
0016003216Image Poly1305 Tag
003200364Image Len (pages)
012801324Boot sequence number
013201364Has been flashed tagword
013601404Has fully booted tagword

Memory comms

Put... somewhere in memory?

| Start | End | Len | Name | | 0000 | 0004 | 4 | Pointer to Section 1 Hdr (Self) | | 0004 | 0008 | 4 | Pointer to Section N Hdr (Self) | | 0008 | 0012 | 4 | Pointer to Section M Hdr (Other) | | 0012 | 0013 | 1 | Is first boot? |

Thinking slow


  • Bootload
  • Active - 001
  • Slot A - 001
  • Slot B - EMPTY


  • Bootload
  • Active - 002
  • Slot A - 001 <-- should be written, needs erase
  • Slot B - 002

Thinking flow

how am I gunna bootload a dom? is it way easier?

Sub Dom <--------- "Erase partition" ---------> "Ack" (5ms erase) ---------> "Erase 1% complete..." (15ms wait) (5ms erase) ---------> "Erase 2% complete..." ... ---------> "Erase completed" <--------- "Begin bootload (metadata)" ---------> "Ack" ---------> "Gimme 0..256" <--------- 256 byte payload (2.5ms write) (7.5ms wait) ---------> "Gimme 256..512" <--------- 256 byte payload (2.5ms write) (7.5ms wait) ... ---------> "Bootload complete" <--------- "Reset" (device resets) (5s active erase) (2.5s active write) (device boots)

Thoughts on a second board

  • Single board
  • STM32G030F6P6 for network comms
    • TSSOP-20
    • $1.1524/ea
    • 32KiB Flash
    • 8KiB RAM
    • 64MHz
  • SPI Flash - W25Q128JVSIQ
    • 128Mib/16MiB
    • SOIC-8
    • $1.4631/ea
    • SPI/Dual/Quad
    • 256 byte pages
    • 4KiB sectors
    • 32KiB/64KiB blocks
    • 133MHz max speed
  • 2x MAX3485EESA
    • 12Mbps
    • SOIC-8
    • $1.3231/ea
  • 1x fixed 5v buck
  • 3v3 LDO
  • nRF52 SoM
  • What to do for power connections?
  • INA180 for power measurement

Unsorted thoughts

  • How to do cases/mounting?
  • How to do power measurement?
  • How to connect not-an-nrf52?
    • Should I leave that to the gen1 boards?
  • Make a rp2040 breakout?


  • Get USB task up and going, 1khz or so?
    • Interrupt pended?
  • Define wire format for USB
  • Get a desktop + USB anachro client working
  • Get to the point where I can subscribe to * from the host client
  • Start publishing hello world messages in the firmware
  • Switch to defmt bbqueue
  • In the future, I might not want the USB connection to actually be a server/dom? What about connecting to USB devices that AREN'T the dom? Does that make sense?
    • Let's just build it and see what I actually need

One day build: Self-Care Meter


Okay, I want some kind of meter that reminds/motivates me to take care of little self care tasks.

This could be basics that sometimes get procrastinated on, like doing the dishes, eating a healthy meal, or going for a walk; to mental health tasks, like reading a book, or taking time to meditate or sit and think; to more aspirational goals, like exercise or completing side projects.

Implementation details

I think there will be a couple major parts in play here:

A block diagram of the below components

  • A 50W 5V switch mode power supply
    • Provides power for all electronics including LEDs
  • A Powerbus mini as the "brains" of the operation, based on the Nordic nRF52840
  • A Powerbus mini expansion card, which has pins broken out, as well as an 8KiB EEPROM for storing data
  • A Solder Party Keyboard FeatherWing as the main User Interface
    • This has an Adafruit Feather connector on the back.
    • I'll use a FeatherWing Proto as an adapter between the wiring harness and the FeatherWing.
  • A Smart LED strip in a diffused, 1 meter housing, with 60-144 LEDs
  • A pair of wiring harnesses to connect the Keyboard FeatherWing and Smart LED strip to the Powerbus mini

Build log

I came up with this idea a few days before January 7th, and I plan to make as much progress as possible during this weekend.

If you're reading this on the 7th, stay tuned! I'll push updates here as I go.