If you want something to get done, make it easy.
On of the first things I do on any new project is work out what I need to do to reduce my own friction when performing any given task.
What is developer friction? I define it as anything that gives me a reason to be hesitant about performing a task. This hesitation may be rooted in several things:
- The task is time-consuming.
- The task is hard to perform correctly.
- The task is not hard, but it’s non-obvious and is not often performed.
- I just don’t want to do it right now because my attention span is approximately that of a racoon in the presence of many shiny baubles.
Anyway, what in particular are some things that can be done to reduce developer friction in embedded projects? I’ve chosen the following items because I’ve found that reducing friction in these areas makes it easier for me to do the right thing.
1. Scripted, Headless, Build/Flash/Test Scripts
It seems that on many embedded software projects, “performing a build” is akin to “pressing F5” and waiting for the IDE to build, load, and flash the software onto the target processor.
This, on the surface, is fine. But, at some point, it’s going to be important to ensure we can perform these tasks in combination with some other task. Perhaps there’s an external code generation tool or a static analysis tool that’s difficult or impossible to integrate into the IDE.
If we start by using a scripted build system (Make, Rake, Ceedling, etc), the amount of effort it takes to add something into the build system and the amount of mystery surrounding that addition seems lower.
2. Reducing Fear and Loathing via System Tests
Another form of friction that shows up in projects is a certain fear and loathing of changes to a project. Often times, this happens before a release when some external factors force a change in the software. A requirement changes, the faces of the developers sink, and everyone furiously rows away from the impending waterfall of delivery.
The easiest way I know of to reduce fear and loathing is to build a reliable set of fully-integrated system tests. This means that we use external means to instrument hardware as similar as possible to what will be delivered. In the end, we should have enough of the external factors verifiable by automated tests so that the team can ship the software with changes without being too concerned that core functionality has broken.
3. Scriptable Interfaces
Expose the full abilities of the board to the developers from the outside. Don’t wait around for a third party to build out a GUI or some other application.
I’ve often times found myself in a situation where having some sort of scriptable interface into my embedded target has been tremendously useful. This often just means that I’m able to interact with the target over a RS-232 port connected to a PC.
With a bit of planning, these sorts of scriptable interfaces can give the developer a window into the target allowing them to see and measure things normally well outside of the developer’s grasp.
4. Discovery as a Feature
There’s friction in assets. This often appears because what we’re generating is a binary blob. There’s a really simple thing to do: ensure that, as part of the build process, the compiler and linker output all the intermediate forms on the way from source code to binary. If you’re using C, this includes (at a minimum!): the preprocessed variation of the file(s), the assembly representation of each file, and the map files from the linker.
There are other things that can be done as well, easy things like using ASCII printable strings as stack canaries or initializing memory to something like the character ‘X’ instead of null.
5. On-Target CLI
There’s very little to say about this one. A simple command line interface can be incredibly handy for interacting with and testing your firmware. This item is closely related to building in a scriptable interface.
Expose the inner workings of the software and hardware easier to poke and developers will poke them more often.
6. Cross-Language/Host Data Marshaling
This one is a little tougher, but it’s one that I found to be immensely valuable on the most recent project I was involved in. I found this to be so useful that I released a system for generating code from a schema such that multiple languages on multiple hosts could interact with the same space-efficient binary representation. You can see more details about this open source library named Cauterize at its Github page.
A huge motivation for focusing on cross-language and cross-host data marshaling was the early recognition that our binary protocol would spend the majority of the project in flux. Building the code generator allowed us to ensure that all targets had an interface to the binary data in which all interfaces were generated from the same specification.
7. Code Generation
Sometimes, some complex, useful, but repetitive patterns can be expressed in terms of a script whose output is a C library. I’ve found code generation to be useful when attempting to keep two different systems written in different languages in synch. Generating code for the two different targets from the same model allows a developer to update the model and immediately find the changes propagated to both targets.
I should note that it’s important that the generated code still follow the normal guidelines one would apply when developing their own software by hand. Just because the code is generated by a script doesn’t mean good sense goes out the window in terms of formatting and safety.
Conclusion
In the end, anything to reduce the cognitive or mechanical load on a developer allows them to spend more time thinking about a solution and less time dealing with the tactical aspects of executing that solution.
What are some ways you reduce friction on projects?