Realtime Fake Drivers for Microcontrollers – Develop Embedded Code on PC and Hardware Seamlessly

Project Overview

Developing embedded projects usually requires all physical hardware (displays, sensors, buttons) to test code. This increases costs, slows down prototyping, and often creates messy test setups with multiple wires and breadboards.

This project proposes a small open-source library of Fake Drivers that allows developers to:

  • Run the same microcontroller code on a PC using simulated hardware interfaces.

  • Seamlessly switch to real hardware if connected, without modifying the code.

  • Test and prototype embedded applications without cluttered testboards or unnecessary wiring.

The main goal is to speed up development, simplify debugging, and reduce hardware dependency during early prototyping.


Core Concept

  • Fake Drivers provide the same API and function calls as the original hardware libraries, so developers do not need to rewrite code.

  • Each Fake Device appears on the PC as a visual interface:

    • Display → opens a window representing the real screen.

    • Button → appears as a clickable button on the GUI that triggers events like a real button press.

  • When real hardware is connected, the Fake Drivers automatically bypass simulation and interact with the actual devices.

  • This allows testing realtime user interactions and UI logic without building a full physical prototype.


Example Usage

#include <FakeDrivers.h>   // Fake Drivers library
#include <DisplayLib.h>    // Original display library
#include <ButtonLib.h>     // Original button library

// Define devices the same way for PC or MCU
Display display;
Button button;

void setup() {
    // Initialize the display window and the button on PC
    display.begin();   // Opens a window representing the display
    button.begin();    // Shows a clickable button on the GUI
}

void loop() {
    // Read the button state (real or fake)
    bool pressed = button.isPressed();  // On PC: true if user clicks the button

    // Draw on the display
    display.clear();
    if (pressed) {
        display.drawText("Button Pressed!");
    } else {
        display.drawText("Waiting...");
    }

    delay(500);  // Keep the loop simple for demonstration
}

How it works on a PC:

  1. display.begin() → opens a GUI window that simulates the screen.

  2. button.begin() → shows a clickable button that can be pressed with the mouse.

  3. button.isPressed() → returns true if the user clicks the GUI button.

  4. display.drawText() → renders text or graphics in the display window.

  5. On real hardware, the Fake Drivers automatically pass calls to the real devices, no code changes required.


Key Benefits

  • Run and test embedded code on a PC before using real hardware.

  • Interact with fake devices through GUI interfaces, simulating screens, buttons, and sensors.

  • Avoid messy testboards and spaghetti wiring, making prototyping cleaner and safer.

  • Enable faster debugging, UI testing, and prototyping without purchasing every component.

  • Provides a foundation for an open-source library that can eventually support multiple MCUs and devices.


Call for Contributors

We are looking for embedded developers, students, and IoT enthusiasts to help conceptualize, design, and develop this project:

  • Share ideas for designing Fake Drivers interfaces.

  • Contribute support for additional types of devices and libraries.

  • Provide examples, demos, or GUI improvements to accelerate prototyping.

Join the discussion and help build a library that allows seamless testing of embedded code on PC and real hardware, saving time, reducing costs, and avoiding messy test setups.

What is the advantage over Wokwi / Wokwi for VSCode, which doesn’t require any additional headers?

Hi, thanks for your question!

You’re right that tools like Wokwi are great for simulating hardware without extra headers. What I’m proposing is slightly different:

The idea is to run the same application code on both a PC (using Fake Drivers) and the MCU, without modifying your code.
Fake Drivers are integrated into the project itself, so they are part of your workflow, not an external simulator.
This also allows runtime interaction, like pressing buttons or updating pins via a PC GUI, and even connecting to real hardware for testing.
It supports complex setups, including FreeRTOS tasks, which are usually difficult to fully simulate in conventional simulators.

To make it clearer, I’m planning to create a working prototype first, and I’ll share it with you so you can see exactly how the system works and how it differs from standard simulators like Wokwi.

I really appreciate your feedback once you’ve had a chance to try it!

1 Like

This is an interesting concept, although I’m not sure adding yet another driver abstraction is the right way to go. Ideally, developers should not need to change the code they write, so all existing examples and libraries work as is - build-time tooling could handle the practical differences.

Production applications sometimes achieve cross-platform support by abstracting the platform layer so the application isn’t coded directly to the hardware. However, this puts a lot of the burden on the developer (although it’s arguably a good architectural direction despite the high barrier to entry.)

Dependency injection is also a useful tool, although this can be at odds with the “simple” approach of just accessing a global instance. Unit/integration/acceptance tests use injection with a varying blend of mocks vs real hardware according to the test needs, with unit tests typically using 100% mocks, integration tests using a mix, while acceptance tests typically culminating in testing on 100% hardware, broadly speaking.

The ecosystem does support hardware independence in some cases. For example, the Espressif platform has emerging support for cross-compiling to posix, while some libraries are implemented in a hardware-agnostic way, for example, with a pluggable communications transport (the Sparkfun GNSS library, Bosch IMU drivers, Blues’ note-c library for talking with Notecard to name a few.). Some platforms, such as Particle, already have hardware independence via a platform abstraction. When I added a platform layer to Particle’s firmware in 2015, I gave a demo running their firmware on the desktop to demonstrate platform-neutrality - it almost seemed “impossible” to some.

My take on this effort would be to first envisage what the developer experience looks like in the ideal world, with all these gaps filled, tooling support provided etc.., and then the look at the gaps that currently exist in practice, and focus on filling gaps that offer the biggest wins. Also survey what currently exists and what could be leveraged.

It’s fairly easy to build something that works for one project, with known, limited hardware, but much harder to provide something genuinely useful across the board for the majority of projects. There are lots of moving parts, thousands of libraries, dozens of platforms - many gaps to fill. The project management aspect will be significant, as well as encouraging library maintainers to add any required changes.

A major source of friction is that SDK/platforms typically have a hardware-centric view. This is natural, since there is a learning aspect often included here, and real hardware gives real feedback. Typically, developers initially don’t care about running their code on anything but the target hardware, so, adding any additional complexity would already starting off on the wrong foot. Of course, experience shows that testing both on- and off-device is eventually necessary for any significant project.

Effortlessly targeting application firmware to the desktop is very desirable, but it’s a huge mountain to climb! I hope not to dissuade anyone from this effort, but show the potential scope from the outset.

Cheers,

mat.

Hi Mat,

Thank you so much for the detailed and insightful feedback. Your experience at Particle really helps put the scope of this challenge into perspective, and I truly appreciate you taking the time to guide me.

I’ve decided to pivot the approach to a Remote Register Mapping model to keep the application code 100% untouched.

How it works technically: Instead of rewriting libraries, I am targeting the Data Source. The application firmware remains 100% identical to the production version. I “intercept” the hardware access at the register level:

  • The Interception: When the firmware calls for a register read (e.g., Read_Register(0x48)), the call is routed via a lightweight Serial protocol to a PC-side GUI.

  • The Injection: The PC GUI (acting as the “Silicon”) responds with the user-defined value.

  • The Result: The firmware processes this “injected” data as if it came from real hardware.

The Main Challenge: As this is an Open-Source Library focused on early-stage prototyping, my only real challenge right now is ensuring a Reliable Handshake between the MCU and the PC Tooling. I am not targeting real-time performance or solving latency issues at this stage; the goal is strictly Functional Logic Verification.

I am working on the MVP to prove this “PC-as-a-Peripheral” concept and will share the repo as soon as it’s ready. Thank you again for your encouragement and for helping me refine this direction.

Cheers,

ishakismaili

Creating reliable host <> embedded comms can be tricky. I’ve been working on a project embedded-bridge which aims to solve this problem. GitHub - m-mcgowan/embedded-bridge: Lightweight host–device bridge library for embedded testing (C++ and Python) · GitHub .

It’s not yet released but I have been using it in several projects. I expect to cut a release in the next week or so.

Emulating hardware over serial to the host could work in theory, but I wonder if the increased latency would create an issue with existing library code expecting a response within a given duration. Of course, you could always replace the library code too to sidestep that issue.

I would aim for a solution where the mock hardware could also be compiled into the firmware itself to reduce latency. No code changes other than a conditional define. Since you’re already hooking the primary transport (I2C/SPI/Serial) or using linker replacement/wrapping to stub functions, this ought to be achievable. If the code is written in portable C/C++ then you can run on the host or the device depending on other constraints - such as hardware latency requirements, available device memory etc.

But also step back - to make this work at scale, you’d need library authors to embrace whatever new system you develop - there’s just too many different libraries and hardware to undertake that work alone. Do you see that as the best path forward, compared to (say) describing how library authors can support mocking and useful abstractions to make this easier? Creating a full emulation of all hardware is going to be challenging.

If I’m misunderstanding your goal (full hardware emulation on the host), please set me right.

Thanks again for your detailed feedback, it really helped me clarify and refine the direction of the project.

I want to also mention that the architecture has slightly evolved compared to my earlier description. After analyzing the problem more carefully, I realized that a full hardware emulation approach would introduce unnecessary complexity and scalability issues for my current goals.

Instead, I shifted toward a simpler and more modular design focused on input injection and register-level simulation, which is more practical for early-stage embedded development and easier to extend later if needed.

This approach keeps the system lightweight while still achieving the main objective: speeding up development, simplifying debugging, and reducing dependency on physical hardware during prototyping. It also preserves the MCU as the single source of execution, which avoids timing and latency issues.

The PC side is strictly limited to generating simulated inputs (via GUI or scripts), while the MCU handles all logic execution. The Fake Drivers layer only validates, maps, and forwards values into the MCU registers without affecting the firmware execution flow.

The current simplified architecture is as follows:

PC (GUI inputs)
      ↓
Serial communication
      ↓
Fake Drivers (validation + mapping)
      ↓
MCU registers
      ↓
Firmware execution

This design is intentionally kept simple at the core level, but structured in a way that allows future expansion (for example adding more device types or interfaces) without changing the firmware logic.

Overall, this direction came from re-evaluating the trade-offs between full emulation complexity and practical embedded testing needs, and aiming for a solution that is both realistic and scalable in the long term.

I really appreciate your insights and I’ll also study the embedded-bridge project more closely as it seems closely related to the communication layer challenges in this space.