STM32 Returns – System Workbench, STM32Cube, C++, FreeRTOS, MAX7219 and Conway’s Game of Life
Introduction
Last year, I had written about my experience of getting started with STM32 microcontrollers. There was (and still is) a bewildering number of choices when it comes to ARM programming. After some research, I had settled on using ARM GCC, Eclipse, and Standard Peripheral Library to program an STM32F103RB chip. Recently, since I am working again with STM32 for a product, I decided to revisit development options for this chip family.
What I finally settled on is the following:
- System Workbench (SW4STM32)
- STM32Cube
- FreeRTOS
- C++
A few thoughts on the above choices:
System Workbench (SW4STM32)
You can program SMT32 on a variety of toolchains and IDEs. I wanted to use ARM GCC based solution (not interested in spending $$$ on Keil, IAR, etc.) which was easy to set up and get going. System Workbench, which is built on top of Eclipse is a great choice, especially as it is supported by STMicroelectronics. (Note that you need to register and login before you see a download link on the SW4STM32 website.)
STM32Cube
STM32Cube is an initiative by STMicroelectronics consisting of a software platform – HAL (Hardware Abstraction Layer), LL (Low-layer API), and middleware components – plus STM32CubeMX, a graphical tool for generating initialization code for your project – clock configuration, GPIOs, etc. 32 bit ARM chips are beasts compared to their petite 8 bit ancestors, and everything is complicated – whether you are setting up the clock or configuring a peripheral. Yes, ultimately it’s a matter of setting the right values in the right registers and you can code it up by hand, but the amount of time you will save with STM32Cube is quite significant. Also having a graphical tool like STM32CubeMX has many advantages. When you have a complex project, it’s very convenient to have a visual tool that helps you allocate resources of your chip without conflicts. As you will see below, it can also generate an SW4STM32 project for you directly.
The other compelling factor for moving away from the Standard Peripheral Library is that STMicro themselves seems to have ditched it. There are other options like libopencm3, but I feel it’s better to stick to something supported by the chip manufacturer, especially when you are working on a product.
FreeRTOS
The boilerplate for any microcontroller project consists of a while loop and a bunch of interrupt routines – assuming that you aren’t coding “Arduino-style” by peppering your code with delay() calls. As your projects increase in complexity and you start torturing the chip, you will soon find yourself prioritising interrupts, managing communication between routines, and synchronising data access. Unknowingly, you are writing your own RTOS (Real Time Operating System). So why not just use one designed for the purpose? FreeRTOS seems to fit the bill, and does not have restrictions for commercial use. As you will see below, STM32CubeMX can generate a FreeRTOS project for you. You don’t have to download on install anything separately.
C++
The most common coding language for microcontrollers is C, and that’s what I stick to, most of the time. But having worked on complex software projects for many years, sometimes I miss a higher level of abstaction in the code. Although the first instinct might be to reject C++ as too heavy for microcontrollers, some research will show that C++ use is quite feasible – especially for 32 bit ARM chips with significant Flash/RAM. Personally, the ability to use classes is reason enought for me to use C++ – the code is way more organized than a C project.
I understand that the above choices won’t always fit the bill, especially if you are trying to heavily optimise Flash/RAM usage on the chip. Now, on to the project.
Objective
In this project, we will use an STMicroelectronics NUCLEO-F103RB board (with the STM32F103RB chip) to run Conway’s Game Of Life simulation on an 8×8 LED grid that uses the MAX7219 driver. The project will use the framework discussed above.
Hardware Hookup
For the 8×8 LED grid, I used an assembled kit from ebay with the MAX7219 driver chip.

Here’s what the pinout of the Nucleo F103RB board looks like, from the datasheet.

The connections are as follows:
Nucleo | Max7219 |
---|---|
PB13 | CLK |
PB14 | CS |
PB15 | DIN |
GND | GND |
The Max7219 board is powered separately using a 5V power supply.
Now let’s get started with the software part.
Software Setup
Fire up SMT32CubeMX, start a new project, and select the NUCLEO-F103RB board. The pinout tab look like this:

Above, you can see that we have done the following:
- Enabled FreeRTOS
- Enabled SYS/TIM1 (For the RTOS)
- Enabled SPI2 and assigned pins PB13, PB14 and PB15
- Enabled USART2 and assigned PA2 (USART2_TX)
The clock configuration tab will let you set the (complex) clock system in a visual manner. (In this case we just choose the default.)
Now, go to Project->Settings:

You can see above that we have chosen SW4STM32 as the toolchain and set up the project directory. Now go to Project->Generate Code. When you do this the first time, it will download some resources (like FreeRTOS files) and at the end, it will give you an option to open this project directly inside System Workbench.
Once the project loads in System Workbench, the first thing you need to do is right-click on the project and choose the “Convert to C++” option. Once that is done, rename the main.c file as main.cpp.
To upload the code to the Nucleo board, go to Run->Debug Configurations” and you will see an *Ac6 STM32 Debugging option. You just need to add your project to this section and you are all set for uploading code and debugging it under System Workbench.
The above procedure was for creating a project from scratch. But now let’s look at the code details for this project.
The Code
If you download the code for this project, you will see the following structure:
$ tree -L 2
.
├── Debug
├── Drivers
│ ├── CMSIS
│ └── STM32F1xx_HAL_Driver
├── Inc
│ ├── FreeRTOSConfig.h
│ ├── main.h
│ ├── stm32f1xx_hal_conf.h
│ └── stm32f1xx_it.h
├── Middlewares
│ └── Third_Party
├── NUCLEO-F103RB.xml
├── STM32F103RBTx_FLASH.ld
├── Src
│ ├── BitBuf88.h
│ ├── Conway64.cpp
│ ├── Conway64.h
│ ├── MAX7219.cpp
│ ├── MAX7219.h
│ ├── freertos.c
│ ├── main.cpp
│ ├── stm32f1xx_hal_msp.c
│ ├── stm32f1xx_hal_timebase_TIM.c
│ ├── stm32f1xx_it.c
│ └── system_stm32f1xx.c
├── startup
│ └── startup_stm32f103xb.s
├── stm32f103rb-max7219\ Debug.cfg
└── stm32f103rb-max7219.ioc
So STM32CubeMX has downloaded and set up all the initial setup code, required HAL files, as well as FreeRTOS files.
Now let’s look at the main() function in main.cpp:
Conway64 conway(&hspi2);
int main(void)
{
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* Configure the system clock */
SystemClock_Config();
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_SPI2_Init();
MX_USART2_UART_Init();
/* Create the thread(s) */
/* definition and creation of defaultTask */
osThreadDef(defaultTask, StartDefaultTask, osPriorityNormal, 0, 128);
defaultTaskHandle = osThreadCreate(osThread(defaultTask), NULL);
/* USER CODE BEGIN RTOS_THREADS */
/* add threads, ... */
char str[] = "STM32 Returns...\n";
HAL_UART_Transmit(&huart2, (uint8_t*)str, strlen(str), 100);
conway.init();
/* USER CODE END RTOS_THREADS */
/* Start scheduler */
osKernelStart();
while (1)
{
}
}
In the code above, after HAL and clock configuration, we initialise the GPIOs, SPI2 and the USART2 peripherals. Next, the default FreeRTOS task StartDefaultTask is created. Then we test out the UART using HAL_UART_Transmit(). (You can see the output by connecting your Nucelo board via USB to your computer and setting up a serial terminal software.) Then we call osKernelStart() and FreeRTOS takes over. From that point, tasks or threads take care of the program execution.
Also, notice above that we are using functions like osKernelStart rather than vTaskStartScheduler for FreeRTOS. This is because we are using the CMSIS abstraction for an RTOS, and these functions are defined in cmsis_os.h/cmsis_os.c. This is a good thing, because if you switch to a different RTOS, you won’t need to change all your calling code.
Here’s the task/thread function for the default task:
/* StartDefaultTask function */
void StartDefaultTask(void const * argument)
{
/* USER CODE BEGIN 5 */
/* Infinite loop */
for(;;)
{
HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin);
osDelay(250);
HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin);
osDelay(250);
// signal thread
conway.signal();
osDelay(100);
}
/* USER CODE END 5 */
}
In the thread, we’re toggling the LED on the board and signaling the conway object, with a small delay in between.
To implement Conway’s Game Of Life on the 8×8 LED grid, we will make use of three classes – Conway64, MAX7219 and BitBuf88.
The BitBuf88 is a simple struct (header only) that abstracts an 8×8 1-bit buffer for LED display.
struct BitBuf88
{
// constr
BitBuf88() {
clearAll();
}
// destr
virtual ~BitBuf88() {}
// set (i, j)
void set(uint8_t i, uint8_t j) {
_vals[i] |= (1 << j);
}
// clear (i, j)
void clr(uint8_t i, uint8_t j) {
_vals[i] &= ~(1 << j);
}
// get (i, j)
bool get(uint8_t i, uint8_t j) {
return _vals[i] & (1 << j) ? true : false;
}
// clear all bits
void clearAll() {
for(uint8_t i = 0; i < 8; i++) {
_vals[i] = 0;
}
}
// vals[i] represents digit (column) and bits vals[i][0:7] the rows
uint8_t _vals[8];
};
Since each LED is either ON or OFF, we need only 64 bits or 8 x uint8_t data to represent the grid in an efficient way.
The MAX7219 is the driver class for the chip, which is an LED driver. The chip uses a simple serial protocol to control the LEDs.

(Above image is from the MAX7219 datasheet.)
The above protocol can be easily implemented using the SPI peripheral of the STM32F103RB.
// send 16 bit data packet
void MAX7219::sendPacket(MAX7129_REG reg, uint8_t data)
{
// CS
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_RESET);
//uint16_t packet = (reg << 8) | data;
uint8_t packet[2];
packet[0] = reg;
packet[1] = data;
HAL_SPI_Transmit(const_cast<SPI_HandleTypeDef*>(_hSPI), (uint8_t*)&packet, 2, 100);
// CS
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_SET);
}
// set the whole display with an 8x8 buffer
void MAX7219::setBuffer(const BitBuf88& buf)
{
for (int j = 0 ; j < 8; j++) {
sendPacket(static_cast<MAX7129_REG>(j+1), buf._vals[j]);
}
}
Above, you can see in sendPacket() how SPI is used to send a two-byte packet consisting of register and data. The setBuffer() method uses this to send data for the whole LED 8×8 grid.
Now that we know how to turn LEDs ON/OFF in the grid, let’s see how to implement Conway’s Game Of Life on it. Here’s the Conway64 class header.
class Conway64 {
public:
Conway64(SPI_HandleTypeDef* hSPI);
virtual ~Conway64();
// initialize
void init();
// add glider with top left cell at (i, j)
void addGlider(int i, int j);
void addBlinker(int i, int j);
// update simulation
void update();
// render to LED grid
void render();
// signal to thread
void signal();
// test init SPI
void testInit();
// test SPI
void test();
private:
BitBuf88 _grid;
MAX7219* _max7219;
osThreadId _conwayTaskHandle;
};
The Conway64 class holds a grid, a thread handle and a reference to the MAX7219 driver. Here’s how a Conway64 object is initialised:
// initialize
void Conway64::init()
{
// set up display
_max7219->power(true);
_max7219->setIntensity(5);
_max7219->setScanLimit(7);
_max7219->clear();
// create Conway task
osThreadDef(conwayTask, conwayTaskFunc, osPriorityNormal, 0, 128);
_conwayTaskHandle = osThreadCreate(osThread(conwayTask), this);
}
Above, we set up the correct registers on the MAX7219 driver (or the LEDs won’t light up), and start the conway task. Here’s the task function:
// The Conway Task function
void conwayTaskFunc(void const * arg)
{
// The Joy of C++
Conway64* conway = const_cast<Conway64*>(static_cast<const Conway64*>(arg));
// enable for testing only
//conway->testInit();
// add a glider object
conway->addGlider(0, 0);
// draw it
conway->render();
// task loop
for(;;)
{
// wait for signal
osSignalWait (0x0001, osWaitForever);
// draw
conway->update();
// slight delay
osDelay(1);
}
}
Above we add a glider to the grid, and in the for loop, we call the osSignalWait() function. This is an example of an inter-thread communication mechanism – another reason to use a full-fledged RTOS. So this code will block till it get the signal that it’s expecting. Once it gets the signal, it will call the update() method which updates the simulation by one time step.
You may recall that we called conway.signal() in the main task. Here’s what it does:
void Conway64::signal()
{
osSignalSet (_conwayTaskHandle, 0x0001);
}
This is the call that wakes up the blocked execution in the conway task.
And now for the core of the simulation – the update function:
void Conway64::update()
{
// From my book Python Playground
// https://www.nostarch.com/pythonplayground
// copy grid since we require 8 neighbors for calculation
// and we go line by line
BitBuf88 _newGrid = _grid;
uint8_t N = 8;
// compute 8-neighbour sum
// using toroidal boundary conditions - x and y wrap around
// so that the simulaton takes place on a toroidal surface.
for(int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
int total = 0;
// count
if(_grid.get(i, (uint8_t)(j-1)%N))
total++;
if(_grid.get(i, (j+1)%N))
total++;
if(_grid.get((uint8_t)(i-1)%N, j))
total++;
if(_grid.get((i+1)%N, j))
total++;
if(_grid.get((uint8_t)(i-1)%N, (uint8_t)(j-1)%N))
total++;
if(_grid.get((uint8_t)(i-1)%N, (j+1)%N))
total++;
if(_grid.get((i+1)%N, (uint8_t)(j-1)%N))
total++;
if(_grid.get((i+1)%N, (j+1)%N))
total++;
// apply Conway's rules
if (_grid.get(i, j)) {
if ((total < 2) || (total > 3)) {
_newGrid.clr(i, j);
}
}
else {
if (total == 3) {
_newGrid.set(i, j);
}
}
}
}
// render
_max7219->setBuffer(_newGrid);
// update data
_grid = _newGrid;
}
I won’t go into the details of Game Of Life here. I’ve written an entire chapter on it in my book Python Playground. The above code updates the grid according to the rules of the game and the MAX7219 object is used to send this updated grid to the 8×8 LED grid.
In Action
You can see the project in action here:
Conclusion
I think System Workbench + STM32CubeMX + FreeRTOS + C++ gives you a good framework for building complex application on STM32 microcontrollers.
Downloads
You can download the code for this project from the git repo below: