Designing firmware from the inside out

I don't know about you, but a very natural way for me to think about designing embedded software is from the "outside in". I've been thinking about this a bit recently, and I'm not so sure that's the best approach.

By "outside in" I mean from the hardware drivers (at the outside) toward the "business logic" at the inside of the application.

Developing an application "outside in" starts with the outer hardware driver layer.

Developing an application "outside in" starts with the outer hardware driver layer.

On a typical project, it seems common to start by working on the hardware drivers and then build up layers on top of that to build the application. This focus on the hardware drivers seems natural for embedded developers, since these are the software components that make our software special.

The problem I see with this though, is that without a clear vision about how the application is going to use the hardware, it's not always clear how the hardware drivers should work or what sort of interface they should have.

If I'm writing the drivers myself, I could make it behave in any way that I could possibly imagine, so how do I choose?

Well, it seems to me that the alternative to this "outside in" approach is to design the software from the "inside out". When designing a system from the "inside out" you start with the core of the application logic first, and then build the hardware drivers to meet the needs of the application.

An "inside out" approach starts with the application core.

An "inside out" approach starts with the application core.

An example

Suppose I have a device which can receive data over a serial port. How should I build a driver for this? Where should I put the bytes when I receive them? Do I need to buffer the data internally or do I know that someone else will come to get each byte in time? Should I use interrupts, or will someone else be polling for the data?

I could answer these questions by building a driver with an interface that supports all of these options. But that is going to be a waste of time. Why build a whole bunch of features I'm never going to use -- features that won't get me any closer to shipping my code?

Alternatively, I could also attempt to consider all of these options up front and try pick the "best" one. However if I don't really now how the application wants to use the serial port driver, I won't have much basis for making the selection.

Instead, if I first develop the core of the application -- the consumer of the serial port driver interface -- the way in which I'll need the driver to behave will emerge and I'll know more definitively what I need to build.

What does my application do with data received from the serial port? Suppose I follow the "inside out" approach and implement a "command processor" which parses strings received over the serial port and executes specific commands. Maybe I have only a single interface function used for sending characters to the command processor: command_processor_put_char().

When it comes time to implement the serial port driver, I don't need to worry about all of the things that the driver could do -- I'll just focus on the few things that command processor needs it to do. In this case I could simply call command_processor_put_char() from my character received interrupt.

With this design for the command processor in place, the hardware driver implementation is obvious.

By working "inside out" I can focus on the core of the application without being distracted by the capabilities of the hardware drivers. Then when it is time to develop the drivers, I only need to implement the minimum features necessary to support my application.