Something more about multi-threading
One of most important feature of ChibiOS is multi-threading. Oversimplifying, a thread could be imagined like a sequence of instructions (with associated a priority and a working area) and multi-threading means kernel can manage more than a thread independently, executing them in a parallel fashion even if there is a single core.
Achieving this requires Kernel must plan operation sequence: this task is called scheduling. We could act indirectly on this operation though priority levels. Priority follows a simple rule:
among all the threads ready for execution, the one with the highest priority is the one being executed, no exceptions to this rule.
There is a nice article on chibios.org about how to create threads.Threads must always have a chThdSleep() or some suspending function inside their loop. In this way they will yield the CPU ownership and the Kernel will perform a context switch resuming the thread in ready state having the highest priority. Typically a thread is resumed on a certain event, performs its operations and than is suspended.
The best way to maximize usefulness of multi-threading is to divide our application in more independent parts and make them cooperate to achieve our purpose. Every part will represent one of ours threads.
Consider a scenario in which a LED changes its state on button pressed: we can consider this like a kind of action and reaction. So we have two task: checking the button state and changing the LED state. We can assign these tasks to two different threads, in this way code related to every task is independent from other tasks and if a task changes we don’t have to edit the whole software.
In this tutorial we are going to use the green LED and the User Button of our STM32 Nucleo-64 F401RE to explain how to use multi-threading and to introduce the PAL driver. In the following step, we will modify the LED related task without editing the one button related.
STM32 General Purpose I/Os
The simplest way to interact with a MCU is sampling voltages from its pins or force a signal on them. Every MCU manages its pins in different way and typically pins or pads (inspired by contact pads) are grouped by ports.
On STM32 the I/O takes the name of GPIO (General Purpose Input Output). The GPIO ports are named using alphabet letters and every port manages 16 pins (from 0 to 15), so we could have a pins named as PB13 (Port B pin 13). On these MCUs we can configure each pin independently with different configuration:
- Input floating, the pin is set as input but is floating:that means its voltage depends entirely on external circuit.
- Input pull-up, the pin is set as digital input (uses a Schmitt trigger) with a weak pull-up resistor. That means its voltage depends on external circuit but is shifted on VDD (3.3V in this case) when external source is disconnected or doesn’t supply any voltage.
- Input pull-down, as input pull-up but voltage is shifted on VSS (GND in this case) when external source is disconnected or doesn’t supply any voltage.
- Analog, the pin is set as analog input (Schmitt trigger is removed) and weak resistor are disabled.
- Output open-drain with pull-up or pull-down capability, pin is set as output using only a pull-down MOSFET. In this way it is possible to obtain a Wired-AND connecting more than a open-drain pin together. It is possible to add weak pull-up, weak pull-down resistors or both.
- Output push-pull with pull-up or pull-down capability, pin is set as output with using a FCMOS. It is possible to add weak pull-up, weak pull-down resistors or both.
- Alternate function push-pull or open-drain with pull-up or pull-down capability. STM32 has internally a lot of peripherals and pins are not enough to interconnect all of them with the external world. Because of that GPIO has internally a 4-bit multiplexer for each pin. In this way it is possible to connect up to 16 different internal peripherals I/O to each pin (once at time, of course).
Even more, it is possible to chose output speed for each pin.
About PAL driver
PAL set pad mode
Of course every MCU could manage I/Os in a different way from STM32’s GPIO and PAL must be abstract to support different MCUs. PAL allows to select pin mode through the API palSetPadMode() that receives the port identifier, the pin identifier and the pad mode. There are some generic modes that could be suitable for every MCU:
other are MCU-dependent like STM32 PAL_MODE_STM32_ALTERNATE(n) whereas n indicate the number of the alternate function.This function allow to select output speed as well.
As example consider the case in which we want to set PC0 as output push-pull at the highest speed as we want to drive a LED, the function call should be something like that
Note that PAL_MODE_OUTPUT_PUSHPULL | PAL_STM32_OSPEED_HIGHEST is a bitwise operation typically used operating on registers with bit-masks.
Enable/disable PAL driver in halconf.h
In almost every official demo PAL driver comes already enabled anyway there is a driver switch in halconf.h that allows user to disable this driver.
board.h and board.c files
At HAL start-up (that occurs on halInit()) every pin is configured according to board.h and board.c files. These files could be automatically generated, so if we need specific configuration for more than a pin we can use custom board file instead of using palSetPadMode() more than once (for more info please read this article).
Note that PAL driver offers APIs to set mode for the whole port or for a group of pins from the same port.
Read/Write a pin
PAL allows to read pin whereas this is set as input through the API palReadPad(). This function reads pin status returning PAL_LOW or PAL_HIGH. Of course, sampling a signal requires a pin set as analog mode and internally connected to a channel of an ADC (we will talk about this when we will introduce ADC driver).
Similarly, it is possible to write an output using the API palWritePad() to set output as PAL_LOW or PAL_HIGH. To do this pin must be set as output push-pull or open-drain. There are additional function that act on pin configured in this way:
- palSetPad(), that writes output value to PAL_HIGH;
- palClearPad(), that writes output value to PAL_LOW
- palTogglePad(), that inverts output value.
Similarly to sampling, set pin voltage to an arbitrary value requires the use of a driver: the DAC. PAL drivers offers APIs to read/write for the whole port or for a group of pins from the same port too.
How to use chprintf
chprintf() is a version of well known printf() specific of ChibiOS. It prints strings on a kind of abstract stream named BaseSequentialStream.
Idea behind BaseSequentialStream
Basically a stream is a sequence of data elements made available over time. Normal functions cannot operate on streams as a whole, as they have potentially unlimited data.In ChibiOS a BaseSequentialStream is just an object containing a table of virtual methods and a virtual data stream. The idea is pretty simple:defining an abstract stream we can implement function that uses it and its methods without know anything about method implementation.
As example ChibiOS/HAL Serial Driver (SD) is a good candidate to be a BaseSequentialStream but how methods are structured depends on driver implementation. Of course it is required that every driver eligible to be a BaseSequentialStream and that implements every methods contained into the table of virtual methods.
This approach is very powerful because we can make additional driver compatible with BaseSequentialStreamacting only on the driver implementation without edit functions based on this abstract stream type such chprintf().
Where are printed strings?
Consider this code:
We are passing chp as argument 1 of chprintf(). This is a pointer to a BaseSequentialStream that actually points to the Serial Driver 2 (Cast is necessary to avoid a warning). So, string “Hello World” is printed on Serial Driver 2 that actually is connected to ST Link V2-1 and is figured in Device Manager as ST Virtual COM port. Connecting to this COM port using a terminal we can capture this string.
How to include chprintf()
Using chprintf() requires the inclusion in main.c and furthermore some new entries in the Makefile. The Makefile syntax depends on GNU compiler, anyway what you need to know is: we had to specify path to folder containing the headers and full path of sources.
As chprintf() requires chprintf.c, memstreams.c and related headers, next quotes of Makefile would be exhaustive:
About Serial Driver
To use Serial Driver we need to enable the serial driver switch in halConf.h
Than we have to enable Serial Driver 2 in the mcuconf.h
Concluding sdStart() starts Serial Driver 2 with a default configuration. In ChibiOS/HAL every driver must be started before to use it so we will often find similar function (spiStart(), i2cStart(), adcStart() and so on). For further readings consider this article.
Project used in this tutorial
Demos here proposed are a clear example of everything we have just explained. Note that the first one doesn’t use chprintf(). Anyway, the code of these demos is over commented in order to help user understanding every part.
- Something more about multi-threading
- STM32 General Purpose I/Os
- About PAL driver
- How to use chprintf
- About Serial Driver
- Project used in this tutorial