Calculating..Register to attend

Embedded Firmware Consulting

Find out more about getting the assistance you need in building powerful realtime embedded systems and hardware support for your products.

Embedded firmware consulting

book call

Embedded firmware consulting is the fastest way for you to resolve issues and gain guidance on your path towards developing high quality software for your embedded products.

When you become a consulting client, you embark on a journey of investing into a platform that helps you to stay agile by providing you the means to develop highly portable and verifiable embedded applications.

There are several benefits of becoming a Swedish Embedded Group client and partner:

  • Simplifies your application development: by providing you with a robust kernel and libraries that adhere to the highest coding standards.
  • Assists you in overcoming embedded firmware challenges: by working with you to identify and mitigate current and future issues with your software architecture. When done in time, you can avoid many challenges altogether.
  • Provides independent, unbiased code and infrastructure review: so that your team can develop better embedded firmware.
  • Helps you design robust and portable system architecture: by taking advantage of proven sorftware design patterns that make common problems easier to identify in time.
  • System level consulting: embedded systems are complex and a consultant is absolutely required to understand everything from application level down to the interrupt and hardware interaction level. We cover the whole range top to bottom.
  • Involvement in design decision making of the platform: taking into account you as a client, we develop our platform and SDK in direct response to your requirements and challenges. In a highly democratic fashion you are able to influence design decisions each step of the way.
  • Weekly Q & A calls: so that you (and your team) can gain in depth understanding of how to build bigger and better software.

It is easy to get stuck and overwhelmed with the amount of data required to fully understand what needs to be done to build your firmware application on time. We are here to assist you with source code and know-how precisely in the area of embedded firmware development.

Partner with Swedish Embedded Consulting Group to make sure that you and your team are on the right track.

Simplify your firmware application development

By providing you with a complete toolbox of software libraries, utilities, device drivers and simulation support, we are able to considerably simplify your firmware development.

firmware development

You no longer have to worry about the following:

  • Device driver support: we maintain our platform to always stay up to date with latest device support and develop extra drivers for you.
  • Simulation of your latest hardware: we can work with you (included in application development package) to develop highly customized simulation scenarios that will help your developers write software for your products before your product has been manufactured.

Together with Swedish Embedded Group you can get assistance with topics such as:

  • RTOS: writing software for complex hardware devices often means that having full control over executing of the software is very important. RTOS makes it possible for us to implement a clean software architecture where complexity is split up into smaller easily understandable chunks.
  • Control systems: control systems design on embedded systems requires thorough understanding of mathematics combined with understanding of realtime systems in order to implement efficient and fast algorithms on the available hardware. By utilizing control theory we can develop mathematically correct control algorithms for your embedded hardware device and make them run with high level of precision.
  • Communication protocols: modern IoT systems rely on several standard protocols including CANOpen, LwM2M, Profinet and others to communicate with other embedded systems in the deployment environment. By using standard protocols and open source protocol stacks we can make your device communicate with other devices in the field.
  • Hardware simulation: starting from a PCB working with instruction set simulation to simulate and verify your embedded firmware.

Overcome and eliminate your firmware challenges

Due to the nature of firmware development that places it very close to physical hardware, it is often hard to debug software problems and timing issues.

Through the use of several tools in the Swedish Embedded Platform SDK software debugging can be simplified considerably.

This includes:

  • Cross platform compilation: some software bugs can be identified merely by cross compiling a program to run on 64 bit linux. Good C code should work exactly the same and by looking at the differences in behavior, we can identify problem areas quickly.
  • Unit and integration testing: our platform SDK contains integration with two powerful open source unit testing frameworks (CMock and Unity) which enable us to write unit tests for ANY piece of code inside your firmware code base. We are not limited to only testing higher level algorithms - we can help you test it all down to the lowest level device drivers.
  • Debugging and tracing: many timing issues can be solved with debugging and tracing. However it is not always clear exactly how to trace and what to trace (and how to do it without negatively impacting application behavior). Using our tools we can build and simulate your firmware on variety of platforms and extract much more detailed information about what is going on inside your code.
  • Automated simulation: this involves simulating your firmware. Simulation enables you to test scenarios involving multiple devices and to automate this testing process. Multi-device simulation has traditionally been hard to implement due to complicated coding skills being required. Swedish Embedded Platform SDK integrates simulation and testing into the main build process so that it becomes part of the normal development workflow.

Independent code and infrastructure review

Even the best developers can easily miss potential problems with their code. This is not due to not being attentive, it is simply due to being "too close" to ones own code.

Independent review solves this issue by having somebody from outside come in and review merge/pull requests of your team, giving valuable feedback, finding bugs and suggesting improvements. It also helps you with:

  • Designing better architecture: an architecture that does its function well AND has been documented in a clear fashion is a good architecture.
  • Use of design patterns improves code quality: some of these design patterns may not even be familiar to your programmers but they are very useful.
  • Better workflow and infrastructure: to make reviews possible, it is necessary to have infrastructure in place for effective code review. If you don’t have it then you probably don’t have code reviews right now either. Which means you are letting many issues accumulate into a massive technical debt each and every day.

The less familiar the person who is doing the review is with your code, the more bugs they will find.

Achieve robust and portable system architecture

platform sdk

Portability is an increasingly important factor in firmware development because the market for hardware components can change extremely fast (as we have seen during 2020). This means that at any given time you may find yourself having to adopt your software to a completely new hardware.

If you do not address portability each and every day through automatic cross compilation and simulation, you are very likely to find yourself in a situation where you must quickly redo a lot of code. If this happens, you will find that it will be very difficult to avoid making mistakes and your project will very likely become delayed because of this.

We help you achieve portability for your embedded firmware in several ways:

  • Native posix unit and integration testing: by mandating that majority of firmware code must be buildable for posix (which is absolutely necessary for unit tests anyway), we ensure that dependencies of your code are kept to a minimum. Native compilation does not guarantee clean code, but it does ensure that programmers run into hard blocks when they try to use obscure processor specific constructs and libraries - and this is a good thing.
  • Extensive board support: it is no secret that we draw a lot of our lowest level hardware support from the Zephyr RTOS open source ecosystem. Zephyr hardware support is not always 100% perfect and very often drivers must be fixed and extended - but Zephyr is also a massive project with support for 400+ hardware configurations and numerous architectures - so the fact that some features may be missing is always expected. The upside is that you avoid having to write software support for 400+ boards yourself - and this is a good thing. By using existing board support it is possible to cross compile your application for many different boards - which makes prototyping and debugging very easy.
  • Single board and multi-board simulation support: modern connected IoT systems rarely exist in a vacuum. Often multiple boards need to communicate with each other. This complexity leads to test scenarios that are unique to embedded systems. Traditional simulation tools have been very good at simulating one CPU - but simulation multiple boards including all peripherals has traditionally been hard. Swedish Embedded Platform SDK once again solves this problem by leveraging open source technologies - but what we also do is integrate simulation into the build process itself and define templates and patterns for quickly prototyping your firmware. This makes it possible for us to easily define multiboard setups running different binary applications and test these complex user interaction scenarios in a fully automated way leading to better software quality and better developer experience for your team.

System level consulting

Building an SDK like the Swedish Embedded Platform SDK is not an effort in duplicating what has already been done. Meaning that a lot of system components are used and reused to make the development experience as smooth as possible.

A consultant can not just focus on one layer and expect good results. A firmware consultant that is only good at application development will quickly discover that lack of understanding of kernel level code will become a bottleneck and a pain. Thus we we cover the whole range of technologies from top to bottom:

  • Zephyr RTOS Consulting: linux has revolutionized the way we build modern web services. Open source ecosystem of reusable libraries and tools has been instrumental to this. To develop high quality embedded systems it is good to take what is already available and adopt it to a specific application. Zephyr RTOS, being fully embraced by the Linux foundation is a fantastic alternative to the linux kernel on resource constrained devices (32KB FLASH). Open source nature of these tools means that development often proceeds with a blazing speed that is never achievable by any single entity on their own. Keeping up to these changes can be a challenges. Getting technical help when you need it is absolutely essential.
  • Renode simulation consulting: renode is another very powerful platform used by Swedish Embedded Platform SDK which takes care of emulation. Renode provides the infrastructure necessary for defining hardware configurations and interacting with the simulation which itself runs on a highly optimized emulator (based on QEMU). This in itself is a massive piece of infrastructure with support for over 50 hardware configurations and ability to define any combination of peripherals quite easily. Understanding how to leverage this to your best advantage is of utmost importance. We provide you with knowledge and experience in integrating these systems efficiently into your workflow. Swedish Embedded SDK provides a baseline for this integration.
  • Swedish Embedded Platform SDK: the platform SDK itself provides a platform for integrating many different tools and allows us to deliver progressively better service to you as a consulting client. It ensures that we never need to solve the same problems twice. Meaning that you get a lot of development work done for you free of charge. However the most valuable work is the work that directly affects your product performance and for this it is necessary to do work directly related to your products. This is what a large portion of our consulting is about - helping you extract value out of what we already have and make any necessary changes to make this process of extracting value faster.

Involvement in design and decision making process

Suppose you need to have support added for new hardware. This support requires wide reaching changes to other parts of the system. These changes also affect other existing use cases outside of your company that you haven’t even thought about - but which may be relevant to you in the future.

By being a client with us you are directly involved in the process of this decision making. This ensures that design decisions are always made while taking your use cases into account and you can always influence these decisions (provided you do so constructively).

Consider the opposite scenario: you are using open source software and you are not a consulting client. Design decisions are being made without prioritizing your use cases. Now you are forced to chase latest and greatest source code and port your changes over and over again to enable your product to take advantage of latest features. This is an unworkable alternative.

Weekly Q & A calls

Back in the early days of Swedish Embedded, I used to conduct free public workshops. These workshops were wildly successful and I had no trouble filling these zoom calls with 50+ people.

I stopped doing these calls because it was prohibitively expensive. I had to spend several hours preparing the material ahead of the call, iron out slides, prepare the presentation and I got absolutely nothing in return for it.

The model for doing these calls was simply broken at the time.

The new model is to include these calls for you as part of the consulting service. Making them part of the consulting services also makes them far easier to justify in terms of effort required to make them worthwhile.

Thus, as a consulting client you get access to two weekly 1h calls for the duration of your subscription.

book call

Embedded firmware simulation and testing

High precision machine level simulation

When building embedded systems simulation is extremely important. As the number of devices in the field grows, small faults can have major cost impact. The firmware must be perfect.

As firmware complexity grows, manual testing becomes completely unfeasible.

Swedish Embedded Platform SDK therefore integrates Renode simulation environment into the build process to make simulations not just simple to setup but also easy to execute as part of the normal firmware development cycle.

Through the use of simulation our test scripts can affect every aspect of the virtual hardware. We can:

  • Run the processor for precise number of microseconds: so that we can verify state at precise points in time.
  • Trace every function call and peripheral access: all in software so that it is easy to run in docker based CI.
  • Simulate any on board and off board peripheral: you are not limited to just simulating one board - you can simulate several quite easily through setup of simulation environments that are used solely for automated testing.

In testing embedded firmware it is extremely important to test the unmodified final binary. This is only possible with an accurate simulation environment. By keeping your simulation environments always up to date with your hardware, you always have means of testing any corner case and hardware fault handling easily.

Build firmware before hardware is ready

Building hardware is a costly process. Prototyping typically requires multiple revisions of the hardware built. This is even more expensive.

Swedish Embedded Consulting can help you learn how to prototype ideas in software before you pay a skilled hardware engineer to build the hardware.

This is often much faster and more fun to do because it is easy to iron out firmware related interfaces and gain insight on how your future hardware should function.

demo
  • Start with and idea on what hardware you want to use: find data sheets and other information.
  • Setup a basic board: pick a chip you want to use - ARM? RiscV? Sparc?
  • Write support for peripherals: implement what is missing in simulation.
  • Write your firmware and prototype it: and make adjustments along the way. Setup a graphical visualization.
  • Hire a hardware designer and give them your hardware spec: this will make it easy for them to connect all peripherals.
  • Verify physical hardware: you can use the same binary you used for simulation to verify that your new hardware works - you’re now done!

This is prototyping made easy!

Better than software in the loop

Traditional software in the loop where you build your firmware application to run natively on Posix and mock parts of the system (such as physics) using software is already much much better than what most companies have as part of their development process.

However it is not enough. The main problem with conventional software in the loop setup is that you are not running the same code in simulation as you do in production.

This makes it impossible to say with certainty that you are testing actual behavior of the software.

Simulation based software in the loop completely solves this issue.

With careful attention dedicated to timing it is possible to make the software behave exactly like it would on the physical device.

The fact that you are also testing production binary means that you are always using production build of the software without any modifications introduced by testing. This is a major benefit to simulation based testing.

Eliminates repetitive manual labor in testing

Hardware in-the-loop (HiL) testing is very valuable way of testing your product. However, for every day build and debug cycle, hardware testing is sub-optimal.

The biggest problem with physical hardware testing is that you can not easily script your repeatable actions and run them faster (of course to a degree you can do it with complicated robotics but I assume you don’t want to go there yet - if at all).

Not having the ability to script your testing actions means that you end up spending a lot of time on executing the test actions over and over again - or you simply hope that it all works.

Hoping is not good enough. We want precise, repeatable scripts that verify our software/hardware interaction.

Having simulation as part of the development cycle makes this task very simple.

We can code simulation actions in a high level test automation language such as "Cucumber" and run these on a simulated setup, verifying our software in the same way every single time!

The benefits of this include:

  • Time saved: you spend a little bit of time setting up your simulation and save a lot when you run your tests every day as part of your commit cycle.
  • Direct mapping to user stories: not only does cucumber help us express test scenarios in plain English, using it together with hardware simulation means that we can express actual use case scenarios of users interacting with our hardware products. Plus we can automate it as well. This is very powerful.

Provides means to run hardware tests after each change

Hardware simulation and testing truly shines only when you integrate it into the continuous integration (CI) pipeline.

It is not very useful to simply see a graphical visualization of your firmware (even if it’s cool), the true power is in being able to quickly execute vast numbers of simulation scenarios on demand and automatically for every single source commit into your main GIT repository.

You simply can not achieve this kind of verification coverage every single day without using automation!

Simulation (which does not require physical hardware at all) makes this kind of automation possible.

Provides ability to easily create custom visualizations

Even if scripted testing is extremely valuable, sometimes it is nice to be able to demo something visual.

Swedish Embedded SDK includes a visualization framework that enables development of customized visual representations of simulation state.

You can use these visualizations to create beautiful demos of your product to show to your customers and also use it in development for quick visual feedback.

Using the visualization extensions you can:

  • Use widely available browser frameworks: like jQuery, bootstrap, chart.js etc. to create arbitrarily detailed visualization of simulation state.
  • Visualize networks of devices easily: since you can define multiple boards to be simulated at the same time.
  • Export silkscreen and fab layer of your PCB: and use it for visualization of simulation.
  • Experiment with mock-ups of your products: including ui and multiple view angles.

Unit and integration testing included

Testing puts in writing what usually only exists in the programmer’s head: namely the logical paths that processor takes through the code. This is very valuable when it can be tracked through version control.

We separate testing into two parts:

  • Unit testing: which is concerned with testing only a single file at a time and testing logic of the each function in isolation. The metric for unit testing is extent of logic tested (and always 100% code and branch coverage).
  • Integration testing: is concerned with testing behavior of the code when integrated with other modules, drivers or objects as part of a test application. The metric for these tests is behaviors tested.

In addition to this we of course have the system testing which is done on simulated hardware. Unit and integration testing however is typically done by compiling each of your C files to run on your computer and then linking it with test code.

It is very important that all of your development environment is setup in such a way that testing must always be done. CI pipeline should automatically reject code unless sufficient tests have been written for it and committed at the same time. This requires proper CI setup, scripts and testing frameworks to be integrated with your firmware code.

Swedish Embedded Consulting can help you integrate testing into your daily workflow.

book call

Embedded Platform SDK

The Swedish Embedded Platform SDK is a complete firmware development framework for developing, simulating and testing embedded firmware.

It consists of two parts:

Docker image includes all the compilation tools you need in order to start developing embedded software and for running your build process. You can see exactly what packages are included here and here.

The second link is to a docker configuration file that adds tools for writing code as well. You can of course use any editor you want (including VSCode or Eclipse). There is no rule forcing you to use Emacs.

The docker image together with the SDK (which in turn links to Zephyr, Renode and other tools) provides you with a powerful workflow where you can:

  • Build your application on a powerful RTOS platform: support for Linux Foundation Zephyr RTOS is integrated into this SDK - so you get existing cross compilation support for over 400 different boards.
  • Simulate and debug your application without physical hardware: we provide full simulation capability with ability to model networks of devices and running automated test scenarios as part of your CI workflow (provided by this SDK).

We combine the above with embedded firmware consulting where we help your team get up to speed quickly with simulating your hardware and provide support in the form of platform development and training so you don’t have to do it all on your own.

Who this SDK is for?

This SDK is for you if:

  • You are building a hardware product and you need somebody to build the firmware. Maybe you have considered hiring a freelancer but have found that good embedded programmers are very hard to find.
  • You want 100% certainty in that your product works and you have found that achieving this certainty without automated, scripted verification is notoriously hard - specially in cases where mesh networks of IoT devices are the norm.
  • You want stable and future proof environment for development of your embedded firmware with simulation integrated into the workflow.

What hardware is supported?

The SDK leverages open source technology (notably Zephyr) which is under constant and very active development. This means that list of hardware for which at least some support has been added, steadily grows.

Here are only some of the hardware platforms supported:

boards

Problem Of Embedded Firmware Testing

Modern web developers have a fantastic tool ecosystem available to them (such as selenium) that make UI testing easy. Embedded firmware development has been much slower at developing similar tools.

Firmware testing usually is either non-existent, or very limited to only testing high level application concepts and hoping for the best with the rest of the software.

There are numerous reasons for testing being problematic and here are just some of the reasons that we are going to address on this page:

  • Embedded software runs directly on hardware: thus we can not really test it fully without some form of hardware to run our code.
  • Embedded software almost always requires C: even if you are running a virtual machine, you still need to code it in C and it needs to be tested.
  • Modern embedded devices exist as networks: you often need to connect multiple devices together in order to test them properly.

The very fact that hardware is needed in order to test device compliance with user requirements means that testing has traditionally been a slow process.

Integrated simulation, visualization and testing

Programmers often believe that if they are good enough then they can write code that is infallible. Unfortunately, in practice this is almost never the case.

The very volume of programming decisions that are taken each day while writing code quickly result in a bigger picture that is very difficult to fully comprehend.

In addition, a fast pace of development means that functionality that may have worked when it was written can easily become unintentionally broken unless it’s behavior is always being monitored automatically.

Try looking at your existing code and answer the following:

  • What features can you guarantee are working at any given time? - Can you list them with mathematical precision? Can you easily describe how they are being verified?
  • Can you currently be fully sure that what you are developing works after each and every change is made? - when you merge a change each day, can you be fully sure that it did not break anything else? Is your software always in a deliverable state?
  • Can you verify multi-device interactions automatically? - Is it possible to do this today using your current development workflow for each and every commit each and every day?

This highlights only some potential problems.

The biggest of these problems is that tracing and debugging multiple embedded applications simultaneously as they interact with each other can quickly become a very complicated task.

We are here to simplify all of this - and more.

Provides built in simulation and testing

Swedish Embedded Platform SDK makes development, simulation and testing of embedded firmware a single straight process.

Combining multiple verified technologies into a workable workflow and automating it fully is a very easy way to ensure that your embedded firmware development organization is fast and agile.

The SDK provides answers to problems observed during years of experience in developing embedded software.

Here are just some of the benefits that Platform SDK offers:

  • Easy to build your firmware for many platforms: adding new platforms and cross compiling your application easily is very important.
  • Easy to add simulation and visualization: by using instruction set simulation and providing an easy and standardized way to add custom behavior into the automated build process means that simulating complex multi-node systems and verifying them automatically has become extremely simple.
  • Easy to adopt to existing system: the SDK is an extended build system that integrates simulation and testing as part of the build process. Existing code and libraries can be added quite easily.
  • Open and collaborative: we strongly believe in open source and a collaborative way of developing software. The SDK delivers on this as well.

Extensive Hardware Support

After the chip shortages following 2020, it is more important than ever to design embedded software that can be easily ported to new hardware without having to rework a lot of hardware support.

Swedish Embedded Platform SDK leverages the Zephyr RTOS ecosystem to provide you with open source support for more than 400 different boards using multitude of different architectures.

Porting your application to a new hardware becomes a matter of implementing a handful lines of code instead of reworking your whole application. The overall build infrastructure then ensures that you must follow precise patterns of implementation which make porting your application very easy.

From Products To A Platform

Traditionally, chip manufacturers were focusing on providing proprietary SDKs which often contained binary components which were not open source. This has often led customers to face difficulties when bad design in the software was not easily changeable.

Swedish Embedded Platform SDK is open source, eliminating this class of issues entirely. Using Swedish Embedded Platform SDK you can develop your own business specific SDK and provide a platform for you to build on.

This doesn’t necessarily mean that everything about the SDK needs to be publicly open source - but as a consulting client you always have full access to all code delivered to you - and you have right to modify the platform to your own needs if you want.

You can even offer up the SDK as part of the platform to your own customers thus providing them with more than just your hardware product - an application platform for innovation.

Simulating Hardware And Software

The SDK is designed in such a way that you can develop embedded software entirely without requiring access to physical hardware. This is not just excellent from a development perspective - it also means that you are not limited at all when testing features as part of your CI process.

Hundreds of devices and boards can be easily simulated using existing peripheral support and new peripherals can easily be added using scripting.

Swedish Embedded Platform SDK is a complete simulation and development environment for building embedded firmware. Even if you already have existing code, the SDK helps you keep that code up to strict coding standards where you will have to make sure that everything has been properly reviewed and tested. With simulation driven testing we can for the first time truly achieve arbitrarily thorough test coverage.

Cross Platform Verification Made Easy

Managing multiple board revisions and multiple target architectures can be a daunting task. As new product versions are added, the complexity usually increases exponentially because now you must test on many more permutations of configuration options than before.

To keep the complexity at bay, one needs automation. This includes CI automation and test automation.

Swedish Embedded Platform SDK makes powerful open source automation tools available to you in your firmware development with emphasis on ability to simulate your production firmware in full. This is accomplished by extending open source Zephyr RTOS kernel and extending the powerful Renode hardware emulator to make sure that these tools can be used together seamlessly as part of your development process.

Adding on top of that a visualization framework that runs in your browser truly makes for a complete embedded firmware development and debugging environment.

Built on 15+ Years of Embedded Software Development

The Platform SDK builds on years of experience in building embedded software and the problems and challenges of doing it. Many problems have been solved through relatively recent systems such as Zephyr. This is a good thing. But many new challenges have emerged as well.

With the recent ability of the Renode simulation framework to simulate multinode testing scenarios means that all limitations in simulation and testing of embedded systems is now a thing of the past. We can extend and build on these findings to create bigger and more robust networks of connected devices.

What is included

Besides the resources that are already available to you: When you sight up for consulting with us, you also get everything you need in order to build top quality firmware for your products.

This includes:

  • Access to digital training content: that helps you not just make the most of the SDK but also build better software.
  • Access to Swedish Embedded Platform SDK Planning Sessions: so you can influence development in the way that creates most benefit for your business case.
  • Two in depth, 2h live calls every week: for as along as you are subscribed where we dive deep into the topics that are most important for you and your team to understand.
  • Access to development discussions and early access to new features: so you can influence their implementation and get the most value from the SDK.

Importance of starting right now

If you are thinking that you can wait until later to get simulation and testing in place. It is indeed a common thought so you are not alone.

The downside is that issues in code tend to compound the more code you have. A simple lack of proper design pattern multiplied by the number of files in your project can quickly become a huge problem.

A workflow without possibility to run user acceptance tests for each commit is a ticking technical debt where problems accumulate without you knowing.

Each time your developer needs to manually perform testing procedures on your physical hardware is time that could have been better spent on software.

There are no good reasons to not talk to us and get started right away.

book call

Case Study: Peripheral Simulation

What if you could develop and test your embedded firmware entirely on your PC even before your hardware has been manufactured?

demo

This case study is for you if:

  • You have products in the field that use embedded software: simulation your existing hardware gives you a very useful toolbox where you can setup use cases and verify customer requirements.
  • You are developing a new product and you don’t have the hardware yet. In this case you will be able to setup a development environment where you can interact with your product before hardware is ready.
  • You want to implement a solid CI workflow for your team: in which case simulation is absolutely necessary because traditional hardware in the loop testing requires tremendous amount of physical maintenance that you just don’t need with a simulated environment.

In this case study I’m going to show you how to simulate a board peripheral from scratch.

We are going to implement simulation for a GPIO expander chip connected to our CPU over SPI bus. The chip we are going to simulate is a very common GPIO expander MCP23S17 (link to datasheet).

The result is that we are going to be able to connect virtual buttons to the pins of the GPIO expander and use it in our firmware:

block diagram.drawio

Once our simulation is setup, we are going to be able to interact with our firmware in two ways: (1) over uart serial console which we’ll enable on the firmware side and (2) the simulated buttons which we are going to be controlled through emulation monitor console.

The final result of this case study looks like this:

What you need in order to simulate a chip from scratch

We will be using the Swedish Embedded Platform SDK to make the process easier. The tools we will be working with the most is Zephyr RTOS and Renode - which are integrated into the SDK. The SDK provides also additional resources that make development easier.

You can install the SDK and even run the example as follows:

  • Install the SDK
  • Build the sample
  • Run the sample

Installing and running the SDK

For convenience, the SDK including all toolchains and configuration can be downloaded as a docker image:

$ docker pull swedishembedded/develop:latest
$ docker run -ti swedishembedded/develop:latest
SDK /build> cd platform/sdk
SDK /build/platform/sdk> git pull origin main

Building and running the example

You can now build and execute the example as follows:

SDK /build/platform/sdk> west build -b custom_board -s \
	samples/drivers/gpio/mcp23s17
SDK /build/platform/sdk> west build -t run_sim
...
(machine-0)

This is our simulation console and here we can now interact with peripherals (ie our buttons) directly. Firmware will respond to the interrupts and update its internal state.

Let’s press a button:

(machine-0) sysbus.spi1.gpioExpander.A1 Press

Now let’s connect to the firmware uart and query the gpio state:

(machine-0) uart_connect sysbus.usart2
Redirecting the input to sysbus.usart2, press <ESC> to quit...
uart:~$ gpio get GPIO_E0 1
Reading GPIO_E0 pin 1
Value 1
uart:~$ gpio get GPIO_E0 2
Reading GPIO_E0 pin 2
Value 0

Press kbd:[ESC] to exit the console and type "quit" in the monitor to return to the docker shell.

Renode provides multiple ways to interact with the console. If you are running renode locally (outside of docker) you can have a graphical window with the console. Another method is to instruct renode to create a pseudoterminal (pty) device and connect it to the sysbus.usart2 peripheral. This allows you to connect to the serial port using picocom.

Renode is also highly extendable and the platform SDK provides a seamless integration of renode with kernel, CI and the whole development process. This means we can develop our own plugins for visualizing the state of our peripherals and have them integrated with the build process so that they are only loaded when we build for a particular target board.

If you would like to have a more detailed overview of the SDK and how you can work with it within your company then booking a demo is the fastest way to get exactly what you need.

Let’s jump right into it and implement simulation and driver for MCP23S17.

Simulation of a peripheral: how it’s done

In order to simulate our firmware, we are going to have to implement the simulation side which shall behave exactly like the real chip. We will then run our firmware on a virtual STM32F4 and communicate with our simulation from both sides (firmware uses SPI and GPIO interrupts while we can also control the simulation through the monitor console).

The simulation itself is running in the Renode simulator which is integrated into the SDK. Renode is an open source instruction set simulator and is similar to QEmu (even uses the same emulation engine), but is much easier to extend than QEmu due to being scripted in C# and Python. With renode we can easily write simulation models in a variety of languages and even simulate Verilog models.

For this case study, we are only going to focus on C# peripheral simulation. For this we are going to implement an SPI device model that behaves like MCP23S17.

simulation files.drawio

Implementing the C# model

The C# model is available in the SDK as follows:

SDK /build/platform/sdk> emacs -nw renode/drivers/gpio/Mcp23S17.cs

The first thing we do is create a new class for the new peripheral:

public class MCP23S17 : BaseGPIOPort, ISPIPeripheral, IGPIOReceiver, IBytePeripheral {
  20 | public MCP23S17(Machine machine)
  21 | | : base(machine, NumberOfPins)
  22 | {
  23 | | CS = new GPIO();
  24 | | A0 = new GPIO();
  25 | | A1 = new GPIO();
  26 | | A2 = new GPIO();
  27 | | RESET = new GPIO();
  28 | | INTB = new GPIO();
  29 | | INTA = new GPIO();
  30
  31 | | registers = CreateRegisters();
  32 | | Reset();
  33 | }

This will be our simulated device. It needs to extend a few interfaces:

  • BaseGPIOPort: this gives it a list of "connections" which we can then bind to other peripherals (in our case the buttons).
  • ISPIPeripheral: this is the interface that all spi peripherals need to implement. It includes prototype for "Transmit" and "FinishTransmission" which allow us to receive SPI data and CS pin function from the firmware.
  • IGPIOReceiver: this provides interface for "OnGPIO" function which allows us to receive interrupts (we need to receive interrupts from buttons and pass them out to the STM32 as INTA and INTB signals).
  • IBytePeripheral: this means that our peripheral is byte addressable. This basically communicates that we are going to address our internal registers as bytes. We could have gone with "IWordPeripheral" as well and used 16 bit integers, but bytes make things more clear and easier to map to the device datasheet.

Next we have a few public GPIO pins that we define in the simulation:

  35 | public GPIO CS { get; }
  36 | public GPIO A0 { get; }
  37 | public GPIO A1 { get; }
  38 | public GPIO A2 { get; }
  39 | public GPIO RESET { get; }
  40 | public GPIO INTB { get; }
  41 | public GPIO INTA { get; }

We are going to use the INTA and INTB labels here to bind these to GPIO pins on our STM32 simulation later.

Next we implement the SPI interface for our simulated driver:

  49 | public byte Transmit(byte data)
  50 | {
  51 | | if(CS.IsSet)
  52 | | | return 0;
  53 | | byte value = 0;
  54 | | switch(byteId){
  55 | | | case 0:
  56 | | | | isRead = ((data & 0x01) == 1)?1:0;
  57 | | | | break;
  58 | | | case 1:
  59 | | | | memOffset = data;
  60 | | | | break;
  61 | | | default:
  62 | | | | if(isRead == 1){
  63 | | | | | value = registers.Read(memOffset);
  64 | | | | | if(memOffset == (uint)Registers.INTFA) {
  65 | | | | | | registers.Write(memOffset, 0);
  66 | | | | | | INTA.Set();
  67 | | | | | } else if(memOffset == (uint)Registers.INTFB){
  68 | | | | | | registers.Write(memOffset, 0);
  69 | | | | | | INTB.Set();
  70 | | | | | }
  71 | | | | } else {
  72 | | | | | registers.Write(memOffset, data);
  73 | | | | }
  74 | | | | memOffset++;
  75 | | | | break;
  76 | | }
  77 | | this.DebugLog("SPI byte {0:X}: rx: {1:X} (tx: {2:X})", byteId, data, value);
  78 | | byteId++;
  79 | | return value;
  80 | }

This method parses the SPI messages which are sent to our simulated device.

The "FinishTransmission" method is used to reset the byte counter:

  43 | public void FinishTransmission()
  44 | {
  45 | | byteId = 0;
  46 | | memOffset = 0;
  47 | }

The "OnGPIO" method is called when state changes on any of the GPIO pins of the simulated MCP23S17:

 102 | public override void OnGPIO(int number, bool value)
 103 | {
 104 | | if(number >= 0 && number < NumberOfPins){
 105 | | | base.OnGPIO(number, value);
 106 | | | Connections[number].Set(value);
 107 | | | if(number < 8){
 108 | | | | byte INTEN = (byte)registers.Read((long)Registers.GPINTENA);
 109 | | | | if((INTEN & (byte)(1 << number)) != 0){
 110 | | | | | registers.Write((long)Registers.INTFA,
     (byte)(registers.Read((long)Registers.INTFA) | (byte)(1 << number)));
 111 | | | | | INTA.Unset();
 112 | | | | } else {
 113 | | | | | this.DebugLog("Interrupt on pin {0} is not enabled in GPINTENA", number);
 114 | | | | }
 115 | | | } else {
 116 | | | | byte INTEN = (byte)registers.Read((long)Registers.GPINTENB);
 117 | | | | if((INTEN & (byte)(1 << (number - 8))) != 0){
 118 | | | | | registers.Write((long)Registers.INTFB,
     (byte)(registers.Read((long)Registers.INTFB) | (byte)(1 << (number - 8))));
 119 | | | | | INTB.Unset();
 120 | | | | } else {
 121 | | | | | this.DebugLog("Interrupt on pin {0} is not enabled in GPINTENB", number);
 122 | | | | }
 123 | | | }
 124 | | } else if(number == (int)GPIOPin.CS){
 125 | | | this.DebugLog("CS: {0}", value);
 126 | | | CS.Set(value);
 127 | | | FinishTransmission();
 128 | | }
 129 | }

In the "CreateRegisters" function we setup our register map. We could of course simply directly write to an array, but the register dictionary allows us to define callbacks for reads and writes which makes our implementation more flexible:

 131 | private ByteRegisterCollection CreateRegisters()
 132 | {
 133 | | var registersMap = new Dictionary<long, ByteRegister>
 134 | | {
 135 | | | {(long)Registers.IODIRA, new ByteRegister(this, 0xff)
 136 | | |  .WithValueField(0, 8, out IODIRA, name: "IODIRA")
 137 | | | },
 138 | | | {(long)Registers.IODIRB, new ByteRegister(this, 0xff)
 139 | | |  .WithValueField(0, 8, out IODIRB, name: "IODIRB")
 140 | | | },
 141 | | | {(long)Registers.IPOLA, new ByteRegister(this, 0)
 142 | | |  .WithValueField(0, 8, out IPOLA, name: "IPOLA")
 143 | | | },
 144 | | | {(long)Registers.IPOLB, new ByteRegister(this, 0)
 145 | | |  .WithValueField(0, 8, out IPOLB, name: "IPOLB")
 ....
 189 | | | {(long)Registers.GPIOA, new ByteRegister(this)
 190 | | |  .WithValueField(0, 8, name: "GPIOA", writeCallback: (_, val) => WriteOutputs(0, val),
 191 | | | | | | | | | | |  valueProviderCallback: _ => ReadInputs(0))
 192 | | | },
 193 | | | {(long)Registers.GPIOB, new ByteRegister(this)
 194 | | |  .WithValueField(0, 8, name: "GPIOB", writeCallback: (_, val) => WriteOutputs(1, val),
 195 | | | | | | | | | | |  valueProviderCallback: _ => ReadInputs(1))
 196 | | | },

Finally we implement the callbacks that are referenced in the register definition:

 207 | private uint ReadInputs(byte port){
 208 | | uint values = BitHelper.GetValueFromBitsArray(State);
 209 | | this.DebugLog("ReadInputs port {0}: {1}", port, values);
 210 | | return (port == 0)?(values & 0xff):(values >> 8);
 211 | }
 212
 213 | private void WriteOutputs(byte port, uint value){
 214 | | if(port == 0){
 215 | | | OLATA.Value = (byte)value;
 216 | | | for(var i = 0; i < 8; i++){
 217 | | | | if(((value >> i) & 1) != 0){
 218 | | | | | State[i] = true;
 219 | | | | | Connections[i].Set();
 220 | | | | } else {
 221 | | | | | State[i] = false;
 222 | | | | | Connections[i].Unset();
 223 | | | | }
 224 | | | }
 225 | | } else {
 226 | | | OLATB.Value = (byte)value;
 227 | | | for(var i = 0; i < 8; i++){
 228 | | | | if(((value >> i) & 1) != 0){
 229 | | | | | State[8 + i] = true;
 230 | | | | | Connections[8 + i].Set();
 231 | | | | } else {
 232 | | | | | State[8 + i] = false;
 233 | | | | | Connections[8 + i].Unset();
 234 | | | | }
 235 | | | }
 236 | | }
 237 | }

We can test the model without firmware driver through the renode monitor. But first let’s setup a simulation…​

Setting up the simulation

Renode uses two kinds of files for the simulation:

  • resc: this is a script of monitor commands to run inside the monitor console in order to load the ELF file and start the machine.
  • repl: this is the platform description of peripherals and their wiring.

There is also a third type of file which is the RobotFramework emulation. But this basically works with the existing resc files and runs additional monitor commands to verify behavior of the firmware running inside the simulation.

We start with our repl file:

SDK /build/platform/sdk> emacs -nw \
	samples/drivers/gpio/mcp23s17/boards/custom_board.repl

We need to define a platform where buttons are connected to our simulation. For this we are going to define 16 buttons and two interrupts:

using "platforms/cpus/stm32f4.repl"

// chip select
gpioPortB:
    6 -> gpioExpander@16

// buttons
A0: Miscellaneous.Button @ gpioExpander
    -> gpioExpander@0
A1: Miscellaneous.Button @ gpioExpander
    -> gpioExpander@1
A2: Miscellaneous.Button @ gpioExpander
    -> gpioExpander@2
A3: Miscellaneous.Button @ gpioExpander
    -> gpioExpander@3
A4: Miscellaneous.Button @ gpioExpander
    -> gpioExpander@4
A5: Miscellaneous.Button @ gpioExpander
    -> gpioExpander@5
A6: Miscellaneous.Button @ gpioExpander
    -> gpioExpander@6
A7: Miscellaneous.Button @ gpioExpander
    -> gpioExpander@7
B0: Miscellaneous.Button @ gpioExpander
    -> gpioExpander@8
B1: Miscellaneous.Button @ gpioExpander
    -> gpioExpander@9
B2: Miscellaneous.Button @ gpioExpander
    -> gpioExpander@10
B3: Miscellaneous.Button @ gpioExpander
    -> gpioExpander@11
B4: Miscellaneous.Button @ gpioExpander
    -> gpioExpander@12
B5: Miscellaneous.Button @ gpioExpander
    -> gpioExpander@13
B6: Miscellaneous.Button @ gpioExpander
    -> gpioExpander@14
B7: Miscellaneous.Button @ gpioExpander
    -> gpioExpander@15

// interrupts back to stm32
gpioExpander: GPIOPort.MCP23S17 @ spi1
    INTA -> gpioPortB@8
    INTB -> gpioPortB@9

If you remember the "OnGPIO" callback, then you can see above which GPIO indices are going to be sent to our simulation by renode.

We wire pins from STM32 GPIO and from simulated buttons to our GPIO expander and then we define our GPIO expander as "GPIOPort.MCP23S17" which points to our C# file. We place this under spi1 in the sysbus tree.

Then we wire the INTA and INTB pins to gpioPortB (defined in the STM32 repl file) so that we can register GPIO interrupt callbacks in our firmware and respond to the interrupts reported by the gpio expander.

This gives our system the ability to respond to individual GPIO interrupts occurring on the expander GPIO pins by reading the expander interrupt flag registers to get the interrupt status.

Next we implement the Renode script that loads our firmware (when we run "west build -t run_sim" command):

SDK /build/platform/sdk> emacs -nw \
	samples/drivers/gpio/mcp23s17/boards/custom_board.resc
:name: MCP23S17 Sample
:description: Custom MCP23S17 Board

using sysbus

i $PROJECT_BASE/renode/drivers/gpio/Mcp23S17.cs

mach create
machine LoadPlatformDescription $ORIGIN/custom_board.repl

# Setup pty terminal
#emulation CreateUartPtyTerminal "usart2" $APPLICATION_BINARY_DIR/usart2
#connector Connect sysbus.usart2 usart2

cpu PerformanceInMips 168

logLevel 3
sysbus LogPeripheralAccess sysbus.spi1.gpioExpander

showAnalyzer sysbus.usart2

macro reset
"""
    sysbus LoadELF $bin
"""

runMacro $reset

This script uses several predefined variables that are automatically passed to it by the SDK. These are named the same way as they are defined in the CMake context. So we can just use them directly in our script. The "bin" variable will point to our firmware elf bin file.

The RESC file can be written such that it uses absolute path to the ELF file or even uses an HTTP location. Thus the RESC file can be made completely standalone and you can test production firmware with the SDK without actually invoking the build step.

However, in this case we are integrating the simulation with the build process for the ability to directly run our firmware more easily. Thus we rely on the build process to pass the firmware bin file path to us and we run the most recently built elf file in this case.

Implementation of the firmware driver

The next thing we need to do is implement our firmware driver.

SDK /build/platform/sdk> emacs -nw \
	drivers/gpio/mcp23s17.c

This is the main SPI driver that we are going to expose to our application. We are going to expose it as a generic GPIO device so that our application does not need to worry about the fact that this is an SPI gpio device at all. Instead, our application will treat it exactly the same way as built in STM32 gpio device and we will be able to register interrupt callbacks for each of the expansion gpio pins in exactly the same way as we do it for the on chip GPIO pins.

The first thing directly at the top of the source file is the device tree compatible id:

   1 #define DT_DRV_COMPAT microchip_mcp23s17v2

This will be used to map our C driver to a device tree definition which contains all of the settings for the SPI device.

Next we have the main data structure that will be instantiated for every instance of MCP23S17 on our PCB:

  39 /** Driver private data */
  40 struct mcp23s17 {
  41 | /** Driver data needs to be first */
  42 | struct gpio_driver_data data;
  43 | /** Device pointer (used for gpio callbacks) */
  44 | const struct device *dev;
  45 | /** Device lock */
  46 | struct k_mutex lock;
  47 | /** Callback list */
  48 | sys_slist_t cb;
  49 | /** Interrupt A callback */
  50 | struct gpio_callback inta_cb;
  51 | /** Interrupt B callback */
  52 | struct gpio_callback intb_cb;
  53 | /** Work for handling IRQs */
  54 | struct k_work irq_work;
  55 | /** Register cache */
  56 | struct {
          ....
 101 | } __packed regs;
 102 };

The driver needs to use a mutex because we are very likely to share the same GPIO chip across multiple threads. Using a mutex guarantees that we can queue threads waiting for the device to become available and that we can do it with priority inheritance (semaphores don’t support priority inheritance).

We also keep a cache of the device registers locally because we want to avoid having to read registers before writing them. Instead, we assume that our "true" state of the chip is always managed by the driver and that we simply upload individual registers to the chip when their values change.

Next we define the configuration structure for the driver:

 104 /** Driver configuration */
 105 struct mcp23s17_config {
 106 | /** Device address */
 107 | uint8_t addr;
 108 | /** SPI bus */
 109 | struct spi_dt_spec spi;
 110 | /** Interrupts */
 111 | struct gpio_dt_spec inta_gpio, intb_gpio;
 112 };

This structure is populated directly from the device tree definition in the same C file and contains configuration for the GPIO pins that we would like to use as interrupt inputs.

Note that these are generic gpio pins, so theoretically these pins themselves could potentially reside on another instance of the MCP23S17 chip itself. We don’t care how they are implemented. This is extremely flexible approach of defining portable embedded drivers.

Next we implement two functions for reading and writing values to the chip:

 125 static int _mcp23s17_read_regs(const struct device *dev, uint8_t reg, uint8_t *buf, size_t size)
 126 {
 127 | if (!dev || !buf) {
 128 | | return -EINVAL;
 129 | }
 130
 131 | const struct mcp23s17_config *config = dev->config;
 132 | int ret;
 133 | uint8_t addr = config->addr | 1;
 134 | uint8_t buffer_tx[] = { 0x40 | addr, reg };
 135
 136 | const struct spi_buf tx_buf[] = {
 137 | | {
 138 | | | .buf = buffer_tx,
 139 | | | .len = sizeof(buffer_tx),
 140 | | },
 141 | };
 142 | const struct spi_buf_set tx = {
 143 | | .buffers = tx_buf,
 144 | | .count = ARRAY_SIZE(tx_buf),
 145 | };
 146 | const struct spi_buf rx_buf[2] = {
 147 | | {
 148 | | | .buf = NULL,
 149 | | | .len = sizeof(buffer_tx),
 150 | | },
 151 | | {
 152 | | | .buf = (uint8_t *)buf,
 153 | | | .len = size,
 154 | | },
 155 | };
 156 | const struct spi_buf_set rx = {
 157 | | .buffers = rx_buf,
 158 | | .count = ARRAY_SIZE(rx_buf),
 159 | };
 160
 161 | ret = spi_transceive_dt(&config->spi, &tx, &rx);
 162 | if (ret) {
 163 | | LOG_DBG("spi_transceive FAIL %d\n", ret);
 164 | | return -EIO;
 165 | }
 166
 167 | return 0;
 168 }
 182 static int _mcp23s17_write_regs(const struct device *dev, uint8_t reg, uint8_t *buf, size_t size)
 183 {
 184 | const struct mcp23s17_config *config = dev->config;
 185
 186 | uint8_t buffer_tx[2] = { 0x40 | config->addr, reg };
 187
 188 | const struct spi_buf tx_buf[2] = {
 189 | | {
 190 | | | .buf = buffer_tx,
 191 | | | .len = 2,
 192 | | },
 193 | | {
 194 | | | .buf = buf,
 195 | | | .len = (uint8_t)size,
 196 | | },
 197 | };
 198
 199 | const struct spi_buf_set tx = {
 200 | | .buffers = tx_buf,
 201 | | .count = ARRAY_SIZE(tx_buf),
 202 | };
 203
 204 | int ret = spi_write_dt(&config->spi, &tx);
 205
 206 | if (ret != 0) {
 207 | | LOG_ERR("spi_write FAIL: %d\n", ret);
 208 | | return -EIO;
 209 | }
 210
 211 | return 0;
 212 }

The rest of the C driver is concerned with implementing the generic GPIO device interface which will allow us to use this driver in a portable fashion (both within other drivers and within our application):

 535 static const struct gpio_driver_api _mcp23s17_api = {
 536 | .pin_configure = _mcp23s17_pin_configure,
 537 | .port_get_raw = _mcp23s17_port_get_raw,
 538 | .port_set_masked_raw = _mcp23s17_port_set_masked_raw,
 539 | .port_set_bits_raw = _mcp23s17_port_set_bits_raw,
 540 | .port_clear_bits_raw = _mcp23s17_port_clear_bits_raw,
 541 | .port_toggle_bits = _mcp23s17_port_toggle_bits,
 542 | .pin_interrupt_configure = _mcp23s17_pin_interrupt_configure,
 543 | .manage_callback = _mcp23s17_manage_callback,
 544 | .get_pending_int = _mcp23s17_get_pending_int,
 545 };

We implement our interrupts by registering two callbacks which are called when a state change occurs on the correponding interrupt GPIO pins:

 570 static void _mcp23s17_irqa_callback(const struct device *dev, struct gpio_callback *gpio_cb,
 571 | | | |     uint32_t pins)
 572 {
 573 | struct mcp23s17 *self = CONTAINER_OF(gpio_cb, struct mcp23s17, inta_cb);
 574
 575 | ARG_UNUSED(pins);
 576 | k_work_submit(&self->irq_work);
 577 }
 578
 579 static void _mcp23s17_irqb_callback(const struct device *dev, struct gpio_callback *gpio_cb,
 580 | | | |     uint32_t pins)
 581 {
 582 | struct mcp23s17 *self = CONTAINER_OF(gpio_cb, struct mcp23s17, intb_cb);
 583
 584 | ARG_UNUSED(pins);
 585 | k_work_submit(&self->irq_work);
 586 }

Both of these GPIO callbacks defer the action of reading the MCP23S17 interrupt registers to the system work queue. This is important, because for the on chip GPIO pins, the interrupt callback will be called in interrupt context where we can not execute SPI operations. One optimization that we could do in this case is call "k_is_in_isr()" kernel function and either do the work directly or defer it if we are in the ISR. But in this case we defer it regardless.

The work item in turn calls the actual work handler in the context of the system work queue thread that has priority -1 (higher than all user threads):

 547 static void _mcp23s17_irq_work(struct k_work *work)
 548 {
 549 | struct mcp23s17 *self = CONTAINER_OF(work, struct mcp23s17, irq_work);
 550 | uint8_t buf[2];
 551
 552 | k_mutex_lock(&self->lock, K_FOREVER);
 553
 554 | if (_mcp23s17_read_regs(self->dev, REG_INTFA, buf, sizeof(buf)) != 0) {
 555 | | LOG_ERR("Failed to read interrupt flag registers");
 556 | | goto done;
 557 | }
 558
 559 | uint32_t intf = ((uint32_t)buf[1] << 8) | buf[0];
 560
 561 | self->regs.INTFA = buf[0];
 562 | self->regs.INTFB = buf[1];
 563
 564 | gpio_fire_callbacks(&self->cb, self->dev, intf);
 565
 566 done:
 567 | k_mutex_unlock(&self->lock);
 568 }

Inside this function we read the interrupt status register of the MCP23S17 and then fire the GPIO callbacks that user has registered with our driver. In this way, we can call external gpio callbacks that have been registered for the pins provided by our GPIO expander.

If you are very very attentive to the above code, you will see that it has a a little problem. We are trying to acquire a mutex inside a function that is executed in the system work queue context. While this will work just fine in most cases, it also results in other work queue items potentially being delayed. The work queue is a scarce resource where we must do things as fast as possible. Since acquiring a lock can take time if it is busy, it would actually be better to acquire it with zero timeout and if the acquisition fails, to reschedule the work item again. In this way, the work queue can always be responsive to new work being pushed to it.

Finally, we instantiate our driver for all instances compatible with the MCP23S17 in our device tree:

 655 #define MCP23S17_INIT_PRIORITY 75
 656 #define MCP23S17_INIT(n)                                                                           \
 657 | static const struct mcp23s17_config _mcp23s17_##n##_config = {                             \
 658 | | .spi = SPI_DT_SPEC_INST_GET(                                                       \
 659 | | | n, SPI_OP_MODE_MASTER | SPI_MODE_CPOL | SPI_MODE_CPHA | SPI_WORD_SET(8),   \
 660 | | | 0),                                                                        \
 661 | | .inta_gpio = GPIO_DT_SPEC_INST_GET_OR(n, inta_gpios, { 0 }),                       \
 662 | | .intb_gpio = GPIO_DT_SPEC_INST_GET_OR(n, intb_gpios, { 0 }),                       \
 663 | };                                                                                         \
 664 | static struct mcp23s17 _mcp23s17_##n##_data = { 0 };                                       \
 665 | DEVICE_DT_INST_DEFINE(n, _mcp23s17_init, NULL, &_mcp23s17_##n##_data,                      \
 666 | | |       &_mcp23s17_##n##_config, POST_KERNEL, MCP23S17_INIT_PRIORITY,        \
 667 | | |       &_mcp23s17_api)
 668
 669 DT_INST_FOREACH_STATUS_OKAY(MCP23S17_INIT);

This concludes our overview of the driver implementation.

Next, let’s look at how we define this driver inside the device tree.

Device tree definition

In order to use our driver in the devicetree and be able to query variables from the device tree definition as we did above, we need to define device tree bindings for our driver:

SDK /build/platform/sdk > emacs -nw dts/bindings/gpio/microchip,mcp23s17.yaml

Inside this file we define the 'schema' for the device tree definition:

   4 description: |
   5     This is a representation of the Microchip MCP23S17 SPI Gpio Expander.
   6
   7 compatible: "microchip,mcp23s17v2"
   8
   9 include: [gpio-controller.yaml, spi-device.yaml]
  10
  11 properties:
  12     label:
  13       required: true
  14     "#gpio-cells":
  15       const: 2
  16
  17     ngpios:
  18       type: int
  19       required: true
  20       const: 16
  21       description: Number of gpios supported
  22     inta-gpios:
  23       type: phandle-array
  24       required: false
  25       description: Interrupt for PORTA
  26     intb-gpios:
  27       type: phandle-array
  28       required: false
  29       description: Interrupt for PORTB
  30
  31 gpio-cells:
  32   - pin
  33   - flags

Once this has been done, we can now instantiate our device in any board definition. This provides an incredibly flexible way to support any number of boards. You can define device tree definitions both at application level (using overlays) and at board level by placing them directly into your board dts file.

Let’s place our MCP23S17 on the SPI bus as an overlay for our sample:

SDK /build/platform/sdk > emacs -nw \
	samples/drivers/gpio/mcp23s17/boards/custom_board.overlay

This overlay will only be used when we build our application for "custom_board" target.

&spi1 {
| pinctrl-0 = <&spi1_sck_pa5 &spi1_miso_pa6 &spi1_mosi_pa7>;
| pinctrl-names = "default";
| cs-gpios = <
| | &gpiob 6 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)
| >;
| status = "okay";

| mcp23s17: mcp23s17@0 {
| | compatible = "microchip,mcp23s17v2";
| | label = "GPIO_E0";
| | spi-max-frequency = <1000000>;
| | reg = <0>;
| | gpio-controller;
| | #gpio-cells = <2>;
| | ngpios = <16>;
| | inta-gpios = <
| | | &gpiob 8 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)
| | >;
| | intb-gpios = <
| | | &gpiob 9 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)
| | >;
| | status = "okay";
| };
};

This is where we define for the firmware the fact that our MCP23S17 is connected to SPI1 bus of STM32 and also has two interrupts wired to PB8 and PB9 on the STM32.

Our driver can then query this data and use it to register GPIO interrupt callbacks.

Automated testing

Once we have both simulation and firmware ready, we can now script all of the emulator commands using robot framework. So let’s do just that:

SDK /build/platform/sdk > emacs -nw samples/drivers/gpio/mcp23s17/sample.robot

This script contains robot framework simulation which adds an extra automation layer on top of our platform description and renode script. This allows us to test actual usage scenarios and automate the whole process so that it runs automatically in CI:

*** Settings ***
Resource                      ${RENODEKEYWORDS}
Suite Setup                   Setup
Suite Teardown                Teardown
Test Setup                    Reset Emulation
Test Teardown                 Test Teardown

*** Variables ***
${PROJECT_BASE}               %{PROJECT_BASE}
${APPLICATION_BINARY_DIR}     %{APPLICATION_BINARY_DIR}
${APPLICATION_SOURCE_DIR}     %{APPLICATION_SOURCE_DIR}
${BOARD}                      %{BOARD}
${UART}                       sysbus.usart2

*** Test Cases ***

GPA pins are connected to gpio register
| Set Test Variable  ${GPIO}  GPIO_E0
| Boot
| FOR  ${PIN}  IN RANGE  0  7
| | Write Line To Uart  gpio get ${GPIO} ${PIN}
| | Wait For Line On Uart  Value 0  timeout=2

| | Execute Command  sysbus.spi1.gpioExpander.A${PIN} Press
| | Write Line To Uart  gpio get ${GPIO} ${PIN}
| | Wait For Line On Uart  Value 1  timeout=2

| | Execute Command  sysbus.spi1.gpioExpander.A${PIN} Release
| | Write Line To Uart  gpio get ${GPIO} ${PIN}
| | Wait For Line On Uart  Value 0  timeout=2
| END
| Check gpio io works 0x12 0x14 A0 0x01
| Check gpio io works 0x12 0x14 A1 0x02
| Check gpio io works 0x12 0x14 A2 0x04
| Check gpio io works 0x12 0x14 A3 0x08
| Check gpio io works 0x12 0x14 A4 0x10
| Check gpio io works 0x12 0x14 A5 0x20
| Check gpio io works 0x12 0x14 A6 0x40
| Check gpio io works 0x12 0x14 A7 0x80

GPB pins are connected to gpio register
| Set Test Variable  ${GPIO}  GPIO_E0
| Boot
| FOR  ${PIN}  IN RANGE  8  15
| | ${idx}=  Evaluate  ${PIN} - 8
| | Write Line To Uart  gpio get ${GPIO} ${PIN}
| | Wait For Line On Uart  Value 0  timeout=2

| | Execute Command  sysbus.spi1.gpioExpander.B${idx} Press
| | Write Line To Uart  gpio get ${GPIO} ${PIN}
| | Wait For Line On Uart  Value 1  timeout=2

| | Execute Command  sysbus.spi1.gpioExpander.B${idx} Release
| | Write Line To Uart  gpio get ${GPIO} ${PIN}
| | Wait For Line On Uart  Value 0  timeout=2
| END
| Check gpio io works 0x13 0x15 B0 0x01
| Check gpio io works 0x13 0x15 B1 0x02
| Check gpio io works 0x13 0x15 B2 0x04
| Check gpio io works 0x13 0x15 B3 0x08
| Check gpio io works 0x13 0x15 B4 0x10
| Check gpio io works 0x13 0x15 B5 0x20
| Check gpio io works 0x13 0x15 B6 0x40
| Check gpio io works 0x13 0x15 B7 0x80

*** Keywords ***

Boot
| Execute Command           set bin @${APPLICATION_BINARY_DIR}/zephyr/zephyr.elf
| Execute Command           set APPLICATION_BINARY_DIR
@${APPLICATION_BINARY_DIR}
| Execute Command           include
@${APPLICATION_SOURCE_DIR}/boards/${BOARD}.resc
| Create Terminal Tester    ${UART}
| Start Emulation

Check gpio io works ${gpio} ${olat} ${name} ${exp}
| Check gpio input works ${gpio} ${olat} ${name} ${exp}
| Check gpio output works ${gpio} ${olat} ${name} ${exp}

Check gpio input works ${gpio} ${olat} ${name} ${exp}
| ${value}=  Execute Command  sysbus.spi1.gpioExpander ReadByte ${gpio}
| Should Contain  ${value}  0x00

| Execute Command  sysbus.spi1.gpioExpander.${name} Press
| Sleep  50ms
| ${value}=  Execute Command  sysbus.spi1.gpioExpander ReadByte ${gpio}
| Should Contain  ${value}  ${exp}
| # OLAT should be zero here
| ${value}=  Execute Command  sysbus.spi1.gpioExpander ReadByte ${olat}
| Should Contain  ${value}  0x00

| Execute Command  sysbus.spi1.gpioExpander.${name} Release
| Sleep  50ms
| ${value}=  Execute Command  sysbus.spi1.gpioExpander ReadByte ${gpio}
| Should Contain  ${value}  0x00

Check gpio output works ${gpio} ${olat} ${name} ${exp}
| ${value}=  Execute Command  sysbus.spi1.gpioExpander ReadByte ${gpio}
| Should Contain  ${value}  0x00

| Execute Command  sysbus.spi1.gpioExpander WriteByte ${gpio} ${exp}
| ${value}=  Execute Command  sysbus.spi1.gpioExpander ReadByte ${gpio}
| Should Contain  ${value}  ${exp}
| # OLAT should contain output value as well
| ${value}=  Execute Command  sysbus.spi1.gpioExpander ReadByte ${olat}
| Should Contain  ${value}  ${exp}

| Execute Command  sysbus.spi1.gpioExpander WriteByte ${gpio} 0x00
| Execute Command  sysbus.spi1.gpioExpander WriteByte ${olat} 0x00

Having done this, we can now run the automation using support provided by the Swedish Embedded Platform SDK:

SDK /build/platform/sdk > west build -t run_robot
Preparing suites
Started Renode instance on port 9999; pid 1892
Starting suites
Running /build/platform/sdk/samples/drivers/gpio/mcp23s17/sample.robot
13:24:28.5074 [INFO] Loaded monitor commands from: /opt/renode/scripts/monitor.py
13:24:28.5258 [INFO] Robot Framework remote server is listening on port 9999
+++++ Starting test 'sample.GPA pins are connected to gpio register'
13:24:29.2085 [INFO] Including script: /build/platform/sdk/samples/drivers/gpio/mcp23s17/boards/custom_board.resc
13:24:30.5936 [INFO] System bus created.
13:24:32.6584 [INFO] sysbus: Loaded SVD: /tmp/renode-1892/85cf5df8-43c9-41a0-a674-585432a9a1d0.tmp. Name: STM32F40x. Description: STM32F40x.
13:24:35.3736 [INFO] machine-0: Machine paused.
13:24:35.4073 [INFO] machine-0: Disposed.
+++++ Finished test 'sample.GPA pins are connected to gpio register' in 6.27 seconds with status OK
+++++ Starting test 'sample.GPB pins are connected to gpio register'
13:24:35.4547 [INFO] Including script: /build/platform/sdk/samples/drivers/gpio/mcp23s17/boards/custom_board.resc
13:24:35.4585 [INFO] System bus created.
13:24:35.7963 [INFO] sysbus: Loaded SVD: /tmp/renode-1892/7f453e48-8011-4c69-a31c-2a7a78aa4124.tmp. Name: STM32F40x. Description: STM32F40x.
13:24:37.8962 [INFO] machine-0: Machine paused.
13:24:37.9013 [INFO] machine-0: Disposed.
+++++ Finished test 'sample.GPB pins are connected to gpio register' in 2.47 seconds with status OK
Cleaning up suites
Closing Renode pid 1892
Aggregating all robot results
Output:  /build/platform/sdk/build/robot_output.xml
Log:     /build/platform/sdk/build/log.html
Report:  /build/platform/sdk/build/report.html
Tests finished successfully :)

The resulting test report is available under build/log.html.

This concludes our case study and I hope you learned something.

Why this case study is important

It is in this way that a complete peripheral can be simulated and exposed to our firmware application in a highly portable and flexible way.

We do not need to stop at simulating only digital peripherals. Analog peripherals can be easily simulated as well. Since firmware always acts on digital samples of the real signal, we can easily convert any analog signal into corresponding ADC samples and feed them into our firmware as well.

The SDK provides you with a fully integrated build environment for developing and simulating your embedded applications.

Pricing and consulting packages

price list

Ready to get started?

book call