There are good examples to see in well-written device drivers. I don't have specific code at hand, but there are bits and pieces that stuck to me.

For example, there was this driver that could do pattern-matching on button presses, written by an ex-colleague of mine. The way this is done by most developers is to hardcode some state machines (there's a limited set of patterns that have to be matched anyway) and populate an array of callbacks for each. This one did something rather different (hopefully, I can remember what; it's been a few years since I've seen that code).

The patterns it had to recognize were arbitrary combinations of short and long button press or hold sequence, double presses and triple presses. Instead of having the state machines hardcoded, the programmer would define a state machine by defining an array of press patterns (e.g. button_event_t events[] = {BTN_CLICK, BTN_HOLD, BTN_DBL_CLICK, BTN_SEQ_END}) and register it with the driver, which added it into a list of patterns to match against. Each pattern could also have as many callbacks to be launched as needed.

The button driver itself had absolutely no idea about all this pattern crap, all it did was receive interrupts from GPIO ports and place them in a queue. An "event" matcher would munch on that queue, interpret elementary events (like button was pressed, button was depressed, button was held etc.) and put these in another queue that was pushed to the "action" matcher. Each time there was an interaction with it, it would start a new pattern matching drill; it would walk the list of events it knew about, "add" those whose first step matched the current one to a temporarily-used list (it was only adding pointers, of course, not copying the actual event) and "remove" the first press pattern (it would actually increment an index counter so as not to start copying bytes around). If nothing happened before a certain amount of time passed, the list would be disposed of. If it did, however, the temporary list would be walked again, removing those elements whose new first step didn't match the current one, and so on until the list would be brought down to one element. The callbacks would then be scheduled to run (these were actually delayed jobs, not blocking function calls), the one-element list disposed of and so on.

This was beautifully structured in every possible way:

1. The button driver, that did the GPIO magic, only worked with GPIO interrupts.

2. Recognizing specific user presses was done by another subsystem, and

3. Recognizing specific sequences of user presses was done by another subsystem

None of these had to do anything foreign to them: the button driver (which encompassed roughly #1 and #2 with nice separation between them) only did button stuff. Matching sequences of button actions superficially sounds like "button stuff" which is why it's routinely mashed in the same soup, with horrible results, but this one wasn't.

It had the further advantage that now automatic testing of anything was blatantly trivial (background: each sequence of presses would trigger actions that could potentially take up to one hour), since events like BUTTON_PRESSED or BUTTON_HOLD didn't have to come from actual interaction, they could be pushed in there by a test routine communicating with a PC through a serial port. Extending the range of possible actions or redefining them was trivial, since all that had to be done was edit arrays. It was also trivial to allow customization of these sequences by end-users.

But this is also aesthetically pleasing: there's no conflict of abstraction between "software"and "hardware". There is one module that abstracts only what is relevantly abstracted in hardware, and produces purely software-abstractions, that are meaningful only to humans, not silicon, such as BUTTON_HOLD events (silicon only knows about signals it translates into a bit flip which we like to call an interrupt). It didn't push non-silicon stuff into software that spoke to silicon, nor did it push silicon stuff high up there where there should only be software stuff.

Oh yeah: this was on a microcontroller that had 4K of RAM, and the software also included a communication stack for some weird-ass protocol, a flash driver, a temperature sensor, energy metering, LED signaling, toggling an array of relays, driving a buzzer, a firmware upgrade mechanism, a sort of application settings manager, a clock (the kind that shows the hour) with multiple alarms and the rest of the OS, pretty much all of it written by us. This guy was a guru though. The parts written by me sucked.