Bus Sensors and Actuators

2023 12 05 head

Your embedded system has a set of sensors and actors.

Some sensors and actors are connected to a bus. Typical buses are CAN bus, Serial Peripherical Interface, Ethernet, Bluetooth, or I2C standards.

The bus is a shared medium used to communicate with a set of external active or passive components.

Which design rules shall you follow when designing sensors and actors using a bus architecture?

Do you need to consider threading design?

Should you use synchronous or asynchronous communication?

Should you provide queues for sending and receiving messages?

Synchronous Sending

Your application can always send messages to a bus synchronously. These messages are in general commands to physical devices connected through the bus.

Synchronous Unbuffered Sending

The send function is a blocking operation until the message is transformed into a frame and sent over the bus. You should select this approach only if the frame creation and the bus frame transmission is fast.

Beware that bus contention can slow down the frame transmission.

I do not recommend this approach if you send burst of messages or messages with bigger payloads.

The approach works well for transmitting irregular short frames and is simple to implement.

Analyze the application call stack for all synchronous sending operations.

If some driver functions use blocking calls such as polling waits, the thread, the process and in the worst case, the processor core can be blocked until the call returns.

If the instruction is a slow initialization request, the system freezes for tens of seconds.

Asynchronous Buffered Sending

Modern bus drivers are able to buffer messages and return immediately. Often CAN bus drivers are implemented with a transmission queue. This approach provides asynchronous processing of a synchronous transmission request. The caller still has to handle the case when the queue is full.

Beware that transmission errors are not reported back synchronously to the caller. An asynchronous error can be reported through a callback function.

This approach requires a thread to process the queue. The processing thread is either part of the bus driver or part of the application.

Microcontrollers sometimes implement the thread as an interrupt service routine. I would try to avoid the interrupt routine approach if you have a RTOS available.

This approach scales well for hundreds of devices and thousands of messages per second. The sole limitation is the bus bandwidth.

Asynchronous Receiving

Please never use a synchronous receiving operation approach. If you do so, you will block the thread until a message is received. The waiting time is unpredictable. If the sending function call graph also handles the sending of the message, you will end up in a deadlock.

This is a major design rule. You must at least once break the synchronous application calling sequence of sending and receiving frames. Therefore, you need at least two threads in your solution. One application thread and one bus driver thread.

Asynchronous Callbacks

The bus driver can call a callback function when a message is received. The asynchronous called function is executed in the context of the bus driver. Therefore, the callback function shall delegate further processing to another thread.

lely CANOpen asynchronous callback when a PDO is written in the memory of the device representation.

The documentation clearly states the user is responsible to provide needed threads for processing.

The design of the lely CANOpen is an example of shared memory implementation. The device state is replicated in the local application. The bus receives frames and updates the local device state objects.

Changes are propagated to the application with registered asynchronous callbacks.

CANopenNode is another example of a CAN bus driver. All code of the CANopenNode driver is non-blocking.

The callback shall not block the bus driver thread. It should hand over the payload to another thread for further processing. The handover is easily done by sending a message to the processing thread.

This approach is implemented through the actor model. All realtime operating systems support this approach through tasks and message queues.


Use a synchronous calling sequence for all sending operations only if the bus frame transmission is fast.

Break a synchronization calling sequence for all receiving operations.

The actor model is an ideal solution for asynchronous message processing.

2023 12 05 can

The lely CANOpen design is based on a set of clever decisions.

The implementation is completely passive. The library does not perform any I/O besides maybe reading some files from the disk.

It does not create threads nor does it access the system clock. Instead, it relies on the user to send and receive bus frames and update the clock. This allows the library to be easily embedded in a wide variety of applications. This implies that the user shall provide at least one thread to run the library.

The library is also asynchronous. Issuing a request is always a non-blocking operation. If the request is confirmed, the API accepts a callback function which is invoked once the request completes with success or failure. This allows the stack to run in a single thread, even when processing dozens of simultaneous requests. This configuration is not uncommon for a master node.

Threading is delegated to the user of the driver. The calling application is responsible for creating threads and protecting shared resources accessed through the callbacks.


The operation sendMsg simply adds a message to the driver mailbox. The operation callback similarly adds a message to the motor actor mailbox. These operations are fast and non-blocking. Shared resources are the mailboxes. The RTOS explicitly protects these objects against concurrent modifications.

The callback mechanism shall implement a selector. The callback function shall dispatch the message to the correct actor mailbox.

The selector is a simple mapping between the device identifier and the actor object if exactly one actor is responsible for the device. Otherwise, a more complex registration mechanism mapping a list of actors to a device identifier is required.

Both approaches are implemented with a few tens of source code lines. The C++ or Java standard libraries provide the required data structures and algorithms.

The activation blocks show when a thread is active in a single core processor. If multiple cores are available to the application, the threads can run in parallel when messages are delivered in actor mailboxes.

In both cases, most of the time, actors are suspended waiting for the next message.

Lessons Learnt

Do not ignore threading design when designing sensors and actors using a bus architecture. Bus architectures are common in embedded systems. Classical buses are CAN bus, Serial Peripherical Interface, Ethernet, I2C standards.

Future Tesla cars will use a Ethernet bus to connect all sensors and actors. CAN bus is discarded as a legacy bus.

Subsystems will be connected through a Ethernet switch and have local processing capabilities. The latency is below 1 millisecond and is suitable for control by wire approaches.

Power is provided through a Ethernet cable and Power over Ethernet. The standard voltage for the whole car is 48V.

The approach tremendously simplifies the wiring of the car.

Multiple devices connected to a bus are per nature asynchronous. You cannot predict when a device will send a message. Therefore, the reception and processing of a received message must be asynchronous and run in a separate thread.

Always provide send and receive queues for sending and receiving messages for sophisticated devices. The queues are used to decouple the bus driver from the application. All receive and transmit operations are asynchronous. This design implies asynchronous handling of protocol error codes.

Small embedded systems shall use the RTOS provided primitives for communication between the driver and the application.

Do not use abstraction layers to isolate the used realtime kernel. It makes the code more complex and less efficient. Modern development environments with powerful refactoring tools make the change of the RTOS simple.

Be honest. You seldom replace the selected RTOS with another one in your device family.

If you have a complex realtime system, evaluate using a linux based operating system. Use linux design patterns to implement your application. Training will be easier. Numerous external suppliers can provide support.