Formatting Strings with chprintf in ChibiOS

Introduction

In this article, we will explore how to import and use a printf-like function for embedded systems provided by ChibiOS/HAL. This function, known as chprintf, is not included by default in projects and must be manually added to the makefile for use. The chprintf library is designed to work with an abstract interface called BaseSequentialStream, making it versatile for use with any driver that implements this interface. Two common drivers compatible with chprintf are the Serial Driver and the SerialOverUSB driver. The former uses UART, while the latter utilizes USB.

This article will guide you through including the chprintf library in a project and using it with the Serial Driver, providing examples. This assumes you have a basic understanding of UART and know how to set up and use ChibiOS/HAL’s Serial Driver. If you are unfamiliar with these concepts, we recommend reading ChibiOS/HAL’s Serial Driver Explained for a comprehensive guide on using the Serial Driver. For a deeper understanding of the underlying hardware mechanisms, UART 101: the blueprint of Serial Communications might be helpful.

From printf to chprintf

The chprintf is a function available in the ChibiOS/HAL library, designed specifically for embedded systems programming. It provides functionality similar to the standard printf function found in C standard libraries, allowing developers to output formatted strings.

printf

The value of the printf is its ability to print formatted strings. In simpler words it means that this function allows for dynamic generation of text output based on variable values within our program. To provide some context let’s look at the following code:

int myvar = 42;
printf("The value of myvar is %d\r\n", myvar);

The printf uses a formatted string which includes text and placeholders for variables. Specifically, %d is a placeholder for an integer. When printf executes, it replaces %d with the value of the variable myvar. So, in this case, printf will output the string “The value of myvar is 42” to the standard output, effectively substituting %d with the integer value 42 assigned to myvar.

The printf, as we know, allows for different types of placeholders and can print multiple values at a time. This capability is due to it being a variadic function, meaning it can accept an indefinite number of arguments after the format string. This design allows developers to pass various data types to printf, with each placeholder in the format string corresponding to an argument based on its type (%d for integers, %f for floating-point numbers, %s for strings, etc.). The function then processes each argument according to its respective placeholder in the format string, enabling the dynamic creation of output based on the values and types of the provided arguments.

An important thing to notice is the concept of standard output (often abbreviated as stdout). It refers to the default data stream for output from a program or process: it’s where a program writes its output data. In C programs running in an OS such Windows or Linux this is often the console, however it can be easily redirected to other destination such as a file or the input of another program.

chprintf

The chprintf is a function available in the ChibiOS/HAL library, designed specifically for embedded systems programming. It provides functionality similar to the standard printf function found in C standard libraries, allowing developers to output formatted strings. However, chprintf is tailored to the constraints and requirements of embedded systems, where resources are limited, and efficiency is crucial.

For this reason, chprintf has several key differences from printf. Primarily, applications written by the user are executed on a microcontroller, where the concept of standard output does not exist as it does on a PC. Instead, chprintf utilizes an abstract interface known as BaseSequentialStream. This means the function outputs to BaseSequentialStream, and any driver implementing this interface can be used with chprintf, allowing for formatted output across various communication interfaces like UART (Serial Driver in ChibiOS/HAL) or USB (Serial over USB in ChibiOS/HAL).

This significant distinction is evident when examining the prototype of chprintf. Unlike the traditional printf, chprintf includes an additional parameter at the beginning: a pointer to the BaseSequentialStream to be used as the output stream.

/**
 * @brief   System formatted output function.
 * @details This function implements a minimal @p printf() like functionality
 *          with output on a @p BaseSequentialStream.
 *          The general parameters format is: %[-][width|*][.precision|*][l|L]p, with
 *          parameter types (p) including hexadecimal, octal, decimal integers,
 *          characters, and strings.
 *
 * @param[in] chp       pointer to a @p BaseSequentialStream implementing object
 * @param[in] fmt       formatting string
 * @return              The number of bytes that would have been
 *                      written to @p chp if no stream error occurs
 *
 * @api
 */
int chprintf(BaseSequentialStream *chp, const char *fmt, ...)

To illustrate, here’s how we might print a variable using the Serial Driver:

int myvar = 42;
chprintf((BaseSequentialStream*)&SD5, "The value of myvar is %d\r\n", myvar);

And similarly, for the Serial over USB Driver:

int myvar = 42;
chprintf((BaseSequentialStream*)&SDU1, "The value of myvar is %d\r\n", myvar);

For those seeking a more in-depth explanation, the underlying principle of this programming technique is detailed in Appendix A. However, in simpler terms, BaseSequentialStream acts as an abstract interface defining four common methods: write, read, put, and get. Both SerialDriver and SerialDriver over USB adhere to this interface, extending it, which enables their use in any context requiring a BaseSequentialStream.

Diagram illustrating chprintf’s interaction with UART and USB-specific implementations of BaseSequentialStream, highlighting the common API and device-specific extensions.

This design facilitates flexible driver interchangeability and allows for higher level abstraction.

Using the chprintf

Unlike standard library functions that are included automatically, chprintf must be explicitly integrated into your project. This typically requires adding specific files and configuring the makefile, which helps minimize the compiled code size by including only what is necessary. This approach ensures that if chprintf is not needed, the codebase remains lean without unnecessary additions.

Including the library

This function is included in the similarly named library located in [ChibiOS root]\os\hal\lib\streams. To use this library, add streams.mk to the makefile. This is achieved by updating the Project, Target, Sources, and Paths section of the makefile with the following line:

include $(CHIBIOS)/os/hal/lib/streams/streams.mk

As example the makefile of a project for the SDP-K1 would look as follows

##############################################################################
# Project, target, sources and paths
#
# Define project name here
PROJECT = ch
# Target settings.
MCU  = cortex-m4
# Imported source files and paths.
CHIBIOS  := ../../chibios2111
CONFDIR  := ./cfg
BUILDDIR := ./build
DEPDIR   := ./.dep
# Licensing files.
include $(CHIBIOS)/os/license/license.mk
# Startup files.
include $(CHIBIOS)/os/common/startup/ARMCMx/compilers/GCC/mk/startup_stm32f4xx.mk
# HAL-OSAL files (optional).
include $(CHIBIOS)/os/hal/hal.mk
include $(CHIBIOS)/os/hal/ports/STM32/STM32F4xx/platform.mk
include $(CHIBIOS)/os/hal/boards/ADI_EVAL_SDP_CK1Z/board.mk
include $(CHIBIOS)/os/hal/osal/rt-nil/osal.mk
# RTOS files (optional).
include $(CHIBIOS)/os/rt/rt.mk
include $(CHIBIOS)/os/common/ports/ARMv7-M/compilers/GCC/mk/port.mk
# Auto-build files in ./source recursively.
include $(CHIBIOS)/tools/mk/autobuild.mk
# Other files (optional).
include $(CHIBIOS)/os/hal/lib/streams/streams.mk

At this point it is possible to include the chprintf and use it in our applications.

Example of use

As example if we would use the debugger port of the SDP-K1 to print out we could use the following example

#include "ch.h"
#include "hal.h"
#include "chprintf.h"
/* Pointer to BaseSequentialStream for output */
static BaseSequentialStream* chp = (BaseSequentialStream*) &SD5;
/* 
 * This thread periodically toggles an LED.
 */
static THD_WORKING_AREA(waThread1, 128);
static THD_FUNCTION(Thread1, arg) {
  (void)arg;
  
  chRegSetThreadName("blinker");
  while (true) {
    palToggleLine(LINE_LED_GREEN);
    chThdSleepMilliseconds(250);
  }
}
/* Counter variable */
static int counter;
/* 
 * Main function - entry point of the application.
 */
int main(void) {
  /* System initializations:
   * - HAL initialization, including device drivers and board-specific init.
   * - Kernel initialization, turning main() function into a thread.
   */
  halInit();
  chSysInit();
  /* Activates Serial Driver 5 with default configuration. */
  sdStart(&SD5, NULL);
  /* Creates the LED blinker thread. */
  chThdCreateStatic(waThread1, sizeof(waThread1), NORMALPRIO + 1, Thread1, NULL);
  counter = 0; /* Initialize counter */
  /* Main loop - prints the value of counter every second. */
  while (true) {
    chprintf(chp, "Counter is %d\r\n", counter);
    counter++;
    chThdSleepMilliseconds(1000); /* Sleep for 1 second */
  }
}

And if we were to open the Debugger COM port we would see this output in the terminal

Counter is 0
Counter is 1
Counter is 2
Counter is 3
Counter is 4

An important thing to notice here is that we declared a pointer to a BaseSequentialStream and assigned the pointer to the Serial Driver in use to it with a cast. This extra variable could have been avoided rewriting the chprintf as follow

chprintf((BaseSequentialStream*) &SD5, "Counter is %d\r\n", counter);

However, in case the printf is used multiple times in our code, having a static variable pointing to the serial in use has a couple of advantages:

  1. The code of the chprintf is going to be more neat as we can avoid to write the long casting everytime
  2. In case we decide to change serial in use we need modify the code only in one spot.

Handling floating point number

Another significant distinction of the chprintf is its lightweight design. It is engineered to be more resource-efficient than the traditional printf functions, a critical feature for embedded systems where memory and processing power are scarce. For the same reason the support of floating-point number handling is not enabled by default, as printing floating-point numbers is less common in embedded systems and would reduce efficiency. However, this feature can be enabled by the user if needed.

This can be done by changing a switch that is defined in chprintf.h

/**
 * @brief   Float type support.
 */
#if !defined(CHPRINTF_USE_FLOAT) || defined(__DOXYGEN__)
#define CHPRINTF_USE_FLOAT          FALSE
#endif

However changing this configuration directly in this file, would impact all the projects that are using the module. Therefore we can overwrite this switch using the makefile of our project. Indeed the makefile allows for user define populating the variable UDEFS. The following syntax example shows how to redefine CHPRINTF_USE_FLOAT to be 1.

##############################################################################
# Start of user section
#
# List all user C define here, like -D_DEBUG=1
UDEFS = -DCHPRINTF_USE_FLOAT=1

Conclusion

In conclusion, the exploration of chprintf within the ChibiOS/HAL environment underscores its significant utility and efficiency for embedded systems development, particularly for tasks such as debugging over serial interfaces and logging system events. Its value shines in environments where memory and processing power are at a premium, offering a lightweight alternative to the more resource-intensive standard library functions like printf. The forthcoming article will highlight into how chprintf is instrumental in implementing CLI using ChibiOS/Shell, further highlighting its critical role in efficient embedded system design.

Appendix A: Casting mechanism of a SerialDriver to BaseSequentialStream in ChibiOS

In our discussion, we have noted that chprintf requires a pointer to BaseSequentialStream as its initial parameter. Furthermore, we have repeatedly cast a SerialDriver* to a BaseSequentialStream* without extensively exploring the underlying mechanics.

To comprehend this operation, it is essential to examine the concept of BaseSequentialStream. This entity acts as an abstract interface within ChibiOS/HAL, as defined in the header file hal_streams.h. The extract from this file sheds light on its definition and usage.

/**
 * @brief   BaseSequentialStream specific methods.
 */
#define _base_sequential_stream_methods                                     \
  _base_object_methods                                                      \
  /* Stream write buffer method.*/                                          \
  size_t (*write)(void *instance, const uint8_t *bp, size_t n);             \
  /* Stream read buffer method.*/                                           \
  size_t (*read)(void *instance, uint8_t *bp, size_t n);                    \
  /* Channel put method, blocking.*/                                        \
  msg_t (*put)(void *instance, uint8_t b);                                  \
  /* Channel get method, blocking.*/                                        \
  msg_t (*get)(void *instance);
/**
 * @brief   @p BaseSequentialStream specific data.
 * @note    It is empty because @p BaseSequentialStream is only an interface
 *          without implementation.
 */
#define _base_sequential_stream_data                                        \
  _base_object_data
/**
 * @brief   @p BaseSequentialStream virtual methods table.
 */
struct BaseSequentialStreamVMT {
  _base_sequential_stream_methods
};
/**
 * @extends BaseObject
 *
 * @brief   Base stream class.
 * @details This class represents a generic blocking unbuffered sequential
 *          data stream.
 */
typedef struct {
  /** @brief Virtual Methods Table.*/
  const struct BaseSequentialStreamVMT *vmt;
  _base_sequential_stream_data
} BaseSequentialStream;

The definition of BaseSequentialStream incorporates references to _base_object_methods and _base_object_data, indicating it builds upon the foundational structures defined in the hal_objects.h header of ChibiOS/HAL. These foundational structures, which include the virtual methods table for BaseObject, establish a generic object framework.

/**
 * @brief   @p BaseObject specific methods.
 * @note    This object defines no methods.
 */
#define _base_object_methods                                                \
  /* Instance offset, used for multiple inheritance, normally zero. It
     represents the offset between the current object and the container
     object*/                                                               \
  size_t instance_offset;
/**
 * @brief   @p BaseObject specific data.
 * @note    This object defines no data.
 */
#define _base_object_data
/**
 * @brief   @p BaseObject virtual methods table.
 */
struct BaseObjectVMT {
  _base_object_methods
};
/**
 * @brief   Base object class.
 * @details This class represents a generic object including a virtual
 *          methods table (VMT).
 */
typedef struct {
  /** @brief Virtual Methods Table.*/
  const struct BaseObjectVMT *vmt;
  _base_object_data
} BaseObject;

At first glance, the code defining BaseSequentialStream may seem intricate. However, if we simplify it by expanding the macros as a pre-processor would, the structure becomes more straightforward. The BaseSequentialStream essentially combines the basic properties of a BaseObject (such as the instance offset for supporting multiple inheritances) with specific methods tailored for sequential data stream operations like write, read, put, and get.

/**
 * @brief   @p BaseSequentialStream virtual methods table.
 */
struct BaseSequentialStreamVMT {
  /* Base Object method. */
  size_t instance_offset;
  /* + BaseSequentialStream specific methods. */
  size_t (*write)(void *instance, const uint8_t *bp, size_t n);
  size_t (*read)(void *instance, uint8_t *bp, size_t n);
  msg_t (*put)(void *instance, uint8_t b);
  msg_t (*get)(void *instance);
};
/**
 * @extends BaseObject
 *
 * @brief   Base stream class.
 * @details This class represents a generic blocking unbuffered sequential
 *          data stream.
 */
typedef struct {
  /** @brief Virtual Methods Table.*/
  const struct BaseSequentialStreamVMT *vmt;
} BaseSequentialStream;

In summary BaseSequentialStream is a structure where the initial element is a pointer to another structure containing defined methods. Upon examining hal_serial.h, it becomes evident that SerialDriver is an object that extends BaseSequentialStream. To specify, SerialDriver extends BaseAsynchronousChannel, which extends BaseChannel, which extends BaseSequentialStream. This hierarchical relationship demonstrates how SerialDriver integrates multiple levels of abstraction. But at the end of the show, by processing some pre-processor tasks, SerialDriver is effectively revealed to embody the following structure.

/**
 * @extends BaseAsynchronousChannel VMT
 *
 * @brief   @p SerialDriver virtual methods table.
 */
struct SerialDriverVMT {
  /* Base Object method. */
  size_t instance_offset;
  /* + BaseSequentialStream specific methods. */
  size_t (*write)(void *instance, const uint8_t *bp, size_t n);
  size_t (*read)(void *instance, uint8_t *bp, size_t n);
  msg_t (*put)(void *instance, uint8_t b);
  msg_t (*get)(void *instance);
  /* + BaseChannel specific methods. */
  /* + BaseAsynchronousChannel specific methods. */
  /* + SerialDriver specific methods. */
};
/**
 * @extends BaseAsynchronousChannel
 *
 * @brief   Full duplex serial driver class.
 * @details This class extends @p BaseAsynchronousChannel by adding physical
 *          I/O queues.
 */
typedef struct {
  /** @brief Virtual Methods Table.*/
  const struct SerialDriverVMT *vmt;
} SerialDriver;

Upon closely comparing BaseSequentialStream and SerialDriver, it becomes apparent that both structures position the pointer to their respective Virtual Method Tables identically. If we then compare BaseSequentialStreamVMT and SerialDriverVMT we can notice that also the common methods between the classes are accessible at the same offsets. This alignment is critical for polymorphism, allowing objects of SerialDriver to be used wherever BaseSequentialStream is expected, facilitating seamless integration and interoperability within the ChibiOS/HAL ecosystem.

Schematic comparison of the memory structure and method offsets for SerialDriver and BaseSequentialStream, showcasing their VMT alignment for polymorphism.

This is because in C, accessing a field within a structure involves using the base address of the structure combined with the field’s offset as defined in the structure’s prototype.

Therefore, if there is an instance of SerialDriver named, for example, SD1, executing the code SD1.vmt->get(); is functionally equivalent to the following:

BaseSequentialStream* chp = &SD1;
chp->vmt->get();

To illustrate the interoperability between SerialDriver and BaseSequentialStream, consider the SerialDriverVMT structure of SD1, located at address 0x20004000. During initialization, halInit assigns this address to SD1.vmt. Given that for both SerialDriver and BaseSequentialStream, the vmt field is at offset 0, the follow expressions are equivalent

/* Expression 1: Accessing the vmt field directly from SD1. 
   The compiler will try to access &SD1 + 0 (offset of the field vmt in a SerialDriver). */
SD1.vmt
/* Expression 2: After assigning SD1's address to chp, accessing the vmt field via chp. 
   The compiler will try to access &SD1 + 0 (offset of the field vmt in a BaseSequentialStream). */
BaseSequentialStream* chp = &SD1;
chp->vmt

Since the get function is at the same offset in both SerialDriverVMT and BaseSequentialStreamVMT, the following method calls are equivalent, further proving that a SerialDriver can be cast to a BaseSequentialStream for using shared interface methods:

BaseSequentialStream* chp = &SD1;
char tkn;
/* Directly invoking get from SD1's VMT. */
tkn = SD1.vmt->get();
/* Invoking get through a BaseSequentialStream pointer. */
tkn = chp->vmt->get();

Be the first to reply at Formatting Strings with chprintf in ChibiOS

Leave a Reply