Refactoring a Device Driver: A Case Study in Hardware Obsolescence

Newsletter

Table of content

The Challenge of Multi-Generation Hardware Support

In embedded systems, we know that part obsolescence is inevitable. Witekio was recently tasked with modifying an existing device driver to support a replacement component. The project came with firm requirements that the driver had to:

  • support the new device;
  • maintain full backward compatibility with the original device; and
  • preserve the existing interface.

 

Analysis: Evaluating the Legacy Architecture

First, we needed to understand the nature and extent of the changes required. This analysis was broken down into two parts:

  • Hardware Comparison: Comparing data sheets revealed that while both were I2C slave devices, they had fundamentally different methods of operation.
  • Software Audit: The existing driver was implemented in a single file of approximately five hundred lines:
Device driver architecture diagram

Structural Refactoring

From both parts of the analysis, it became clear that the driver would have to be substantially modified to deal with the new device.

Adding more code to the driver in its current state was undesirable. Introducing conditional device-specific code would clutter the main execution flow, increasing complexity making the driver more difficult to maintain.

Therefore, the decision was made to re-architect the driver, breaking it down into components, each with a specific role:

Device driver architecture with components

A key feature of the new modular architecture was the Separation of Concerns. Each module has a dedicated role:

  • I2C Layer: Providing a simpler I2C interface to the device driver;
  • Device Driver: Essentially an abstraction of the two devices (old and new); and
  • Specific Device Code: Implementations of the abstract device for the two actual devices.

The existing code (for device A) was refactored to conform with the above architecture, minus the device B specific code. The refactored code was tested to ensure nothing had broken during this stage. At this stage the device driver module made direct calls into functions provided in the device A specific code module.

 

Solving Hardware Divergences

The next stage was to introduce support for the new device. This was done by temporarily isolating the Device A specific code and then developing specific code for Device B. This phase revealed two issues that would need to be addressed:

  • Different I2C Addresses: Boards with differing devices had been designed such that the old and new devices had different I2C addresses. This was a problem because the operating system expected a single common address to be configured;
  • Interrupt Mechanism Disparity: The interrupt mechanism for the two devices behaved very differently.

As a temporary measure, new device’s I2C address was hardwired into the software, allowing development to proceed. At the same time, it was decided to introduce an Interrupt Layer to the architecture in order to abstract away the differences in interrupt handling:

This was implemented and the driver was soon working correctly with Device B. Throughout this phase, the primary engineering aim was to evolve an identical API between the driver and the specific code layers for both devices. This was achieved fairly easily.

 

Bringing it All Together: Dynamic Device Detection

The final stage was to re-introduce the device A specific code and add runtime “intelligence” to the device driver to identify which device it was dealing with. This enabled it to call the appropriate device specific software routines.

In theory, this was fairly straightforward: the driver would probe for the devices using their two different I2C addresses. Whichever address gave a response would allow the driver to select the correct specific code to use. Having done that, the driver could set up a set of function pointers allowing the correct specific code to be called.

However, the operating system’s API did not allow the I2C address of a device to be changed dynamically and so a workaround had to be introduced in the I2C layer that provided the required functionality. This was not easy and required detailed knowledge of the operating system’s handling of I2C addresses.

Once the I2C address could be switched, a probe routine was developed and the driver able to detect which of the two devices it was dealing with and select the correct set of functions to use:

Driver architecture after refactoring

Conclusion: Beyond the Initial Requirements

The completed driver now supports two different devices seamlessly. While the primary goal was hardware compatibility, the refactoring process achieved much more:

  • Code Sanitization: During the process, several issues were resolved and unnecessary software was discovered and removed;
  • Operational Efficiency: The new architecture is more robust, efficient, and easier to understand; and
  • Scalability: Adding further new devices will now be a much easier.
Paul Lee
Paul Lee
Senior Software Engineer/Software Technical Lead

DISCOVER OUR LATEST ARTICLES

DevOps Lifecycle in Off-Highway Industry
The DevOps playbook for off-highway
06/17/2026
GUI on MCU Hero Banner
GUI on MCU: what a drawing app taught us about MCU/MPU trade-offs
06/12/2026
remote device hero
Maximizing efficiency with Remote Device Management (RDM)
06/05/2026

Newsletters
Signup