Apache NuttX and small systems – CAN node example

Klenance
15 Min Read

The previous posts in this series provided useful information about small systems
and NuttX, but the code examples lacked practical value. Now, we move on to more
practical, real-life applications that actually do something useful.

We begin with a simple CAN Bus node, which can be used to test CAN network setups
or drivers, making it a handy tool for developers. Additionally, it can serve as
a ready-to-use foundation for more complex applications.

Our goal here is to check and compare the resource usage of two CAN implementations
available in NuttX: one based on the CAN character device and the other using
the SocketCAN network interface. This comparison may be useful for developers
deciding which solution to choose when working with CAN Bus on NuttX.

The entire test setup is built using accessible development kits, and thanks
to NuttX’s architecture, the implementation can be easily portable across
different hardware.

Simple CAN Node

A ready-to-compile code is availalbe in the railab NuttX examples repository.

The application is designed to be POSIX-compliant and hardware-independent,
and provides the following features:

  1. Sends a periodic heartbeat every second, including the device system time in
    the message data.

  2. Reads the button state and transmits it over CAN, notifying only when
    the button state changes.

  3. Sets the LED state according to requests received for the node on CAN.

This implementation omits CAN error handling and additional diagnostic features
for the device. Support for CAN extended IDs and CAN FD is also not included.

The application uses a simple protocol, where the CAN ID identifies both
the device node and the message type:

CAN ID = Node ID (mask : 0x700) + Message ID (mask: 0x0ff)

The Message ID specifies the type of message on the bus and can be one
of the following:

Table 1: CAN Node message type

ID

Direction

Message

Data

1

Out

Heartbeat

8B POSIX timestamp

2

Out

Button state

1B button state

3

In

LED state

1B LED state

The Node ID is used to identify the device on the bus and can be configurable
via the Kconfig CONFIG_RAILAB_MINIMAL_CANNODE_NODEID option. Standard CAN ID
is 11 bits and only the 3 most significant bits are used as Node ID, so this
value must fit into the 0x700 mask.

As per the purpose of this post, the application can be compiled in two variants:
one that uses the character device approach, and the other that uses the network
interface. Depending on the configuration options, a given variant is used:

config RAILAB_MINIMAL_CANNODE_CHARDEV
  bool "CAN character device"
  depends on CAN

config RAILAB_MINIMAL_CANNODE_SOCKET
  bool "CAN SocketCAN interface"
  depends on CAN && NET_CAN

config RAILAB_MINIMAL_CANNODE_INVAL
  bool "CAN interface not selected"

endchoice # CAN stack used

The application uses a simple generalized CAN interface:

/* Common CAN message format for chardev and SocketCAN */

struct canmsg_s
  {
    uint32_t id;                    /* CAN message ID */
    uint8_t  len;                   /* CAN message data length */
    uint8_t  data[CAN_DATA_MAX];    /* CAN message data */
  };

/* Initialize CAN interface */

int can_init(void);

/* Get data from CAN interface (blocking) */

int can_read(int fd, FAR struct canmsg_s *msg);

/* Write data to CAN interface */

int can_send(int fd, FAR struct canmsg_s *msg);

The implementation of this interface can be found in:

All application features are identical in both variants, so we can easily compare
the costs of both solutions.

Each node feature is provided in separate thread:

  1. main() after initializing other components becomes responsible for
    periodic heartbeat transmition.

  2. thread_rx() read messages from CAN Bus and handle LED requests.

  3. thread_button() waits for button state change and send reports about
    button state on CAN Bus.

All read operations are blocking, so a thread is only woken up when there is
something to process.

For implementation details, please visit application sources on Github.

Console support

Console support in the final application is often a waste of resources for small
systems, but it’s extremely helpful during development. For this, it’s always good
to keep logging features controlable with Kconfig option.
This can be done with a simple macros:

/* Debug prints */

#ifdef CONFIG_SERIAL
#  define PRINTF(format, ...) printf(format, ##__VA_ARGS__)
#else
#  define PRINTF(...)
#endif

During development, if possible, we can use an MCU with more resources available,
so we are not limited in debug capabilities. After everything works as expected,
we disable all excessive features so the application fits into our target chip.

We can maintain multiple configurations with various debug levels enabled.
When we use NuttX with the CMake build system, working simultaneously with many
out-of-tree builds is straightforward. This is a big advantage of CMake which
is relatively new in NuttX, but drastically changes the developer experience
and productivity.

Configuration

We use the same board as in previous posts in this series – NUCLEO-F302R8
based on the STM32F302R8.

Complete configurations can be found at:

The most important parts of the configuration are presented below:

  1. LED control from user-space is enabled with:

    # CONFIG_ARCH_LEDS is not set
    CONFIG_USERLED=y
    CONFIG_USERLED_LOWER=y
    
  2. Button support with interrupt notifications is enabled with:

    CONFIG_ARCH_BUTTONS=y
    CONFIG_ARCH_IRQBUTTONS=y
    CONFIG_INPUT=y
    CONFIG_INPUT_BUTTONS=y
    CONFIG_INPUT_BUTTONS_DEBOUNCE_DELAY=10
    CONFIG_INPUT_BUTTONS_LOWER=y
    

    Additionally CONFIG_STM32_SYSCFG must be set to support GPIO interrupts.

    Without this option, GPIO interrupts just won’t work without any obvious
    errors reported, which can be difficult to debug for users. By default,
    this option is enabled in NuttX, but during optimization in earlier parts
    of this series, we disabled it to save some FLASH memory.

  3. CAN support for STM32 requires:

    CONFIG_STM32_CAN1=y
    CONFIG_STM32_CAN_TSEG1=15
    CONFIG_STM32_CAN_TSEG2=2
    

    CAN Bit timings are optimized to work on a given MCU with 250 kbit/s bitrate,
    thanks to bittiming.can-wiki.info
    for help.

    At default STM32 port supports CAN character device, SocketCAN interface is
    enabled with an CONFIG_STM32_CAN_SOCKET=y option.

    CAN character device configuration is simple:

    CONFIG_CAN=y
    CONFIG_CAN_TXFIFOSIZE=16
    

    It’s possible that there are two pending messages on the CAN TX FIFO, so we
    have to increase the default FIFO size from 8 to 16.

    In the case of SocketCAN, configuration is more complex, because we have to
    configure network stack. The minimum working configuration is:

    # CONFIG_NET_ETHERNET is not set
    # CONFIG_NET_IPv4 is not set
    CONFIG_IOB_BUFSIZE=64
    CONFIG_IOB_NBUFFERS=5
    CONFIG_NET=y
    CONFIG_NETDEV_LATEINIT=y
    CONFIG_NET_CAN=y
    CONFIG_NET_PREALLOC_DEVIF_CALLBACKS=2
    CONFIG_SCHED_LPWORK=y
    

    The IOB buffer could be configured more efficiently to save a few bytes of
    SRAM. However, finding the optimal value is insignificant here, as we’re more
    interested in FLASH consumption than SRAM.

CAN Bus Demo

The test setup used to verify if the code works correctly is shown below:

Our simple CAN node is connected to two devices with SocketCAN utilities onboard.
One device is used to send LED requests using the cansend tool, while the
other is used to capture CAN traffic with the candump tool. The boards used
in this demo are the NUCLEO-F302R8 for the CAN node and the NUCLEO-G431RB next
to the B-G431B-ESC1 for SocketCAN utilities.

The Nucleo boards are equipped with the Waveshare CAN Shield, which is based on
the SN65HVD230 CAN transceiver, while the B-G431B-ESC1 has an onboard TCAN330DCNT
CAN transceiver.

LED requests are sent with these commands:

  • cansend can0 103#01 which turn LED on,

  • cansend can0 103#00 which turn LED off.

An example of CAN network traffic with an explanation is shown below:

nsh> candump can0
  can0  101   [8]  00 00 00 00 00 00 00 00   | node reset
  can0  102   [1]  00                        | button state
  can0  101   [8]  50 69 0F 00 00 00 00 00
  can0  101   [8]  A0 D2 1E 00 00 00 00 00
  can0  101   [8]  F0 3B 2E 00 00 00 00 00
  can0  101   [8]  40 A5 3D 00 00 00 00 00
  can0  102   [1]  01                        | button press
  can0  102   [1]  00                        | button release
  can0  101   [8]  90 0E 4D 00 00 00 00 00
  can0  101   [8]  E0 77 5C 00 00 00 00 00
  can0  101   [8]  30 E1 6B 00 00 00 00 00
  can0  101   [8]  80 4A 7B 00 00 00 00 00
  can0  102   [1]  01                        | button press
  can0  101   [8]  D0 B3 8A 00 00 00 00 00
  can0  101   [8]  20 1D 9A 00 00 00 00 00
  can0  101   [8]  70 86 A9 00 00 00 00 00
  can0  101   [8]  C0 EF B8 00 00 00 00 00
  can0  102   [1]  00                        | button release
  can0  101   [8]  10 59 C8 00 00 00 00 00
  can0  101   [8]  60 C2 D7 00 00 00 00 00
  can0  101   [8]  B0 2B E7 00 00 00 00 00
  can0  103   [1]  01                        | set LED
  can0  101   [8]  00 95 F6 00 00 00 00 00
  can0  101   [8]  50 FE 05 01 00 00 00 00
  can0  101   [8]  A0 67 15 01 00 00 00 00
  can0  103   [1]  00                        | reset LED
  can0  101   [8]  F0 D0 24 01 00 00 00 00
  can0  101   [8]  40 3A 34 01 00 00 00 00
  can0  101   [8]  90 A3 43 01 00 00 00 00
  can0  103   [1]  01                        | set LED
  can0  101   [8]  E0 0C 53 01 00 00 00 00
  can0  101   [8]  30 76 62 01 00 00 00 00
  can0  101   [8]  80 DF 71 01 00 00 00 00
  can0  101   [8]  D0 48 81 01 00 00 00 00
  can0  101   [8]  20 B2 90 01 00 00 00 00

Results

Memory report for the complete application with the CAN character device version:

Memory region         Used Size  Region Size  %age Used
           flash:       26280 B        64 KB     40.10%
            sram:        2324 B        16 KB     14.18%

and for the heartbeat-only feature:

Memory region         Used Size  Region Size  %age Used
           flash:       22196 B        64 KB     33.87%
            sram:        2172 B        16 KB     13.26%

Memory report for the complete application with the SocketCAN version:

Memory region         Used Size  Region Size  %age Used
           flash:       30056 B        64 KB     45.86%
            sram:        3264 B        16 KB     19.92%

and for the heartbeat-only feature:

Memory region         Used Size  Region Size  %age Used
           flash:       24992 B        64 KB     38.13%
            sram:        3128 B        16 KB     19.09%

The functionality of both applications is identical, but SocketCAN consumes
an additional 3776 bytes of FLASH and 940 bytes of SRAM for a complete applicaton.

Additionally, the application consumes 512 bytes of SRAM for each thread in
the system:

  1. IDLE thread,

  2. main(),

  3. thread_rx() when LED support is enabled,

  4. thread_button() when button support is enabled,

  5. and for SocketCAN only: the worker thread enabled with CONFIG_SCHED_LPWORK.

Summary

In both cases, the application remains under 32KB of FLASH, leaving some room to
add more useful features. Scaling the application to support additional buttons
or LEDs should require minimal resources, as most of the necessary logic is
already embedded in the firmware.

The CAN interface based on the character driver is slightly more
resource-efficient compared to the SocketCAN version. Implementing user code
with the character driver is also simpler, as it eliminates the need to
work with sockets.

For projects with strict resource constraints, it’s advisable to avoid
SocketCAN unless absolutely necessary. Unless you are porting existing
SocketCAN—based code or require another network interface in your
application—making the cost of network abstraction irrelevant—the character
driver is the preferred choice for small systems.

Source link

TAGGED: , , , ,
Share This Article
Leave a comment

Leave a Reply

Your email address will not be published. Required fields are marked *