The tools and tech I use to run a one-woman hardware company
Winterbloom is a boutique, open-source synthesizer company and it has exactly one engineer - me. I am responsible for everything - from the hardware design, to the firmware, to the documentation, and everything else! Because this is a ton of work I've had to be very deliberate with the tools and tech that I use.
This article is a look into our current tech stack one year in and some ideas I have for the future. I'll go from the microcontrollers, the firmware, and all the way up to user guide. I hope this is helpful, but if you want more details about something or if something doesn't make sense, feel free to reach out.
A one-woman company requires a far different approach than any sort of company with more than one engineer. As I go through this tech stack, remember that these choices and decisions are made with that context in mind- they probably won't hold up as well when taken out of that context!
"Lateral thinking with withered technology" - Gunpei Yokoi
The quote above is something that really resonates with my philosophy: I'm not trying to chase the state-of-the-art, I'm trying to use well-understood tools and technology to accomplish my goals. While I've used a relatively broad set of languages, tools, and frameworks throughout my career, with Winterbloom I try to leverage stuff that I feel "fluent" in as much as possible.
I'm also keenly aware of the impact of creating physical devices. I don't want to just create more e-waste. I want to create things that can truly be owned, repaired, re-used, and re-purposed for as long as possible. I hold in my mind that there is a real moral argument that could be made against creating new hardware at all1, so as I bring precious materials together into new forms I really try to weigh the long-term impact of my creations. This is another reason I choose "withered" technology - it is more easily understood and maintained by others.
While this philsophy can lead to choosing solutions that aren't "perfect", I try to pick the best tool for the job that works for me right now and gives my creations a fair shot at longevity.
Most of our products require a microcontroller and there are a lot of choices out there. Using a different microcontroller for every product isn't feasible- I would have to read thousands of pages of documentation to learn to use the new controller, its peripherals, its board layout requirements, and how implement drivers. It's much better for me to pick two or three microcontrollers to focus on so I can build expertise as I create more products.
Winterbloom's primary microcontroller is the Microchip SAM D series- specifically, the SAM D21, SAM D51, and SAM D11. These three general-purpose microcontrollers cover a variety of use cases and they actually have a common set of peripherals. That is awesome- it lets me re-use knowledge and code across projects that use any of these three microcontrollers.
It also helps that these microcontrollers are well established and have a very strong community. They're used in the Arduino Zero, Adafruit's M0 and M4 boards, SparkFun's Things, and more. This means there are lots of existing designs and firmware that can be adapted.
Here's some features common to all three that are super helpful for me:
- USB device: Most of our products can operate as USB MIDI devices so having this is a must. USB also makes programming, debugging, configuration, and firmware updates so much easier.
- Flexible SERCOM peripheral: SERCOMs can be configured for various serial communication protocols including SPI, I2C, and USART. This is perfect for communicating with external peripherals.
- Multi-channel 12-bit ADC: All of our products need to read analog signals from the outside world. Having a built-in, multi-channel analog-to-digital converter is super convenient and simplifies our designs.
- Advanced timer peripherals: These timers can be used for high-resolution timekeeping, PWM generation, and waveform generation.
Here's a little comparison table for these three microcontrollers:
|SAM D11||SAM D21||SAM D51|
|Clock speed||48 MHz||48 MHz||120 MHz|
|Max flash||16 kB||256 kB||1024 kB|
|Max RAM||4 kB||32 kB||256 kB|
This gives me a clear set of criteria for choosing which SAM D to use - the SAM D11 is great for super simple things, the SAM D21 is a great all-arounder, and the SAM D51 is great for more demanding applications.
Looking towards the future I'm considering two other microcontrollers - the RP2040 and the STM32H7. The RP2040 could be great for applications that don't require a lot of analog I/O, and the STM32H7 is an absolute powerhouse that could be useful for applications where sound generation needs to happen within the firmware itself.
I design all of our hardware using KiCAD, a free and open source electronics design automation suite. KiCAD matches well with our philosophy: since our hardware is open source, we want the program used to view and edit the hardware documentation to be open source as well.
There's actually several different bits that come together to form the firmware for our products- the programming language, the build system, the testing framework, etc. The next couple of sections will cover each part in turn.
The C Programming Language, GCC, and CMSIS
I write our firmware using C. While some might gasp at using such an "old" language, it turns out that the combination of my familiarity with C, the maturity of its resources and tooling, and the low-level nature of writing firmware means that C happens to be a joyful language for me to write firmware in.
There are several options in terms of toolchains for microcontrollers. You can pay a lot of money for commercial compiliers from Keil, IAR, and others, but it's really important to that our products, which are open source, use free and open source tools. We use the GCC ARM Embedded Toolchain.
--std=gnu11 as our C dialect, which comprises of the latest C standard and GCC's C language extensions. We also compile with
-Wall -Wextra and a few other useful warnings enabled to help catch undefined behavior.
Creating firmware can feel like a daunting task. Most microcontroller vendors provide a set of tools, frameworks, and examples - and most importantly - a hardware abstraction layer (HAL) library. For example, Microchip offers several options including Atmel START and MPLAB Harmony. These vendor resources can simultaneously be very helpful and extremely hard to work with. They often comprise of a lot of generated code with very little in the way of commenting and documentation.
I personally find the Microchip-provided HAL to be a little too unwieldy2. Because of that, we use the low-level CMSIS library directly and I write very small, usually project-specific, abstractions over that3. While it has a steep initial learning curve, getting familiar with working at this level has allowed me to better understand and utilize the hardware.
I don't write everything from scratch. There are several high quality libraries out there that really speed up the process of writing firmware. However, I am extremely cautious about using libraries in our firmware. Carelessly using a bunch of third-party code that I don't understand can end up hurting more than helping.
Here's some of the libraries we use:
- TinyUSB: an excellent and small USB library.
- Marco Paland's printf: a printf implementation optimized for microcontrollers.
- libfixmath: a small library for fixed-point arthemetic, which is super handy on microcontrollers that don't have a floating-point unit.
- µnit: a very small, very useful testing framework.
When we use a third-party library, we pull its source directly into the firmware's source tree (similar to a monorepo). This has a few benefits:
- All of the code needed to build the firmware is in one place.
- We get to choose how updates are applied to the library.
- We can make changes to the library without worrying about impacting other things.
- We have a clear picture of all of the code that we're building and shipping.
- We have a clear idea of which licenses the code and resulting firmware are under.
There's also a bit of re-usable code that we share across projects. I've collected it all into libwinter. It includes small helpers for things like random numbers, GPIO, colorspace conversion, MIDI, and timing.
$ python3 configure.py $ ninja
I initially started with Makefiles, but it quickly became hard to work with. Makefiles are wonderful, but at a certain level of complexity it makes more sense to move to an actual turing-complete language. Ninja is intentionally designed to be used by a higher-level build system4 and Python was an obvious choice for me due to my familiarity with it. Again, it helps that Ninja and Python are well-established with lots of resources.
You might wonder why I ended up kinda rolling my own instead of using some existing high-level build system like CMake, Meson, etc. It mostly comes down to preference and the ability to completely understand how the build system works. It's much easier for me to reason about 300 lines of Python that are specifically tailored to our use case than to try to reason about a sprawlingly complex general-purpose build system.
wintertools has a variety of tools used during firmware development- it helps generate
ninja files, analyses RAM and flash usage, adds detailed build info, and so on. Most of these are used by each product's
configure.py script (like the one mentioned in the last section).
Second, it has tooling for programming and testing our products- it has modules for interacting with our debug probe, our oscilloscope, our bench multimeter, and with the products themselves over serial or MIDI. These are used to create program & test scripts for each product (like this one).
I chose Python for all of this because I have a lot of experience with using Python for developer tooling. Beyond that, Python has long been seen as an ideal "glue" and tooling language thanks to its readability and large standard library. It's an ideal choice for use cases like this and there's literally tons of resources for writing tools like this in Python.
Looking towards the future, there's a few things I can do to make
wintertools easier to use for people who aren't me- things like documentation and tests. There's a few more features/tools I'd like to add as well, like unifying our firmware release & publishing process.
Some of our products don't use custom firmware; they instead use CircuitPython, an education-focused Python for microcontrollers. There's a lot of reasons why I picked CircuitPython:
- It makes it easy for our customers to customize the product's behavior. A CircuitPython device shows up as a little flash drive with a
code.pyfile that they can just edit!
- It makes developing the firmware/software for a product much easier (as long as it fits within the constraints of CircuitPython).
- It has an incredible community that's maintained by Adafruit.
For products like Big Honking Button and Sol, which are intended to be customized, CircuitPython is an incredibly powerful option. It's so easy to use and so approachable that it turns what would generally be a frustrating and confusing experience into a delightful one. It also helps with the longevity of my creations; CircuitPython supports these products directly and will continue to release new versions for them as long as there are people maintaining CircuitPython5.
Documentation & user guides
As hard as you might try, it's almost impossible to create a product that needs no instructions. Complicated things like synthesizers need to provide at least some guidance or folks will just be frustrated by trying to understand some inscrutable interface. Documentation is so important and I take it very seriously- take a look at Castor & Pollux's user guide for a look at how we approach documentation.
First, I chose Markdown because it's very easy to write and its also easy for other to contribute to. Fixing a few documentation issues is a common way that people start contributing to an open-source project, so I wanted to make that as easy as possible. In the past, I've preferred reStructuredText for writing technical documentation (especially API documentation), but this kind of documentation lends itself much better to Markdown.
Second, MkDocs is a relatively simple static site generator that works well for us. Using a static site generator simplifies hosting and places the source of truth into the project's repository - both very useful things for us. MkDocs is the right balance of small enough but with just enough flexibility for us. It's also written in Python which means that if I do need to dig into the internals I'll be well-equipped to do so.
Third, we use GitHub Pages as our host. Since MkDocs just generates a static website, hosting on a provider like GitHub Pages is incredibly easy. I chose GitHub Pages because we're already hosting our source code on GitHub and it's a free service that provides all of the features we need (custom domains, HTTPS, etc.) The beauty of using a static website is that if for some reason GitHub Pages becomes undesirable, it's easy to move.
Through MkDocs' theming engine our documentation sites all have the same look & feel, winterbloom-mkdocs-theme. This theme is built on the Bulma CSS framework and is optimized for readability and accessibility. We don't use any CSS compiler tools like Sass or LESS since I prefer to keep things simple.
In recent years, changes to the language have made it a lot easier to work with. However, the overall ecosystem continues to churn so rapidly that it's very difficult to keep up with current best practices - especially for a single developer with a lot of other responsibilities.
<forms>, and WebMIDI.
snake_case. I do this so that I'm consistent with our C and Python code which makes context switching much easier for me. Your style is probably different and that's totally okay!
See permacomputing, some strong thoughts by Viznut about how we are creating so much throw-away technology. Specifically the quote: "IC fabrication requires large amounts of energy ... the resulting microchips should be treasured like gems". ↩
A small example: a single peripheral interface can involve 8 different files and 3-4 abstractions! ↩
I'm currently one of the people that maintains CircuitPython :) ↩