How-To's Tips & Tricks

Arduino Development With CLion: From Hobby to Professional Project. Part II

Read this post in other languages:

In the first post of this series, we built a simple prototype project using an Arduino board. This second stage involves constructing a full-featured device that can display the current temperature and pressure with an atmospheric pressure chart, and can run off a single battery for several weeks. The source code is available on GitHub, and readers can follow along with the text in this post commit-by-commit.

Hardware

We’ll need a real-time clock (RTC), a serial peripheral interface (SPI) interface for the screen, an I2C interface for the weather sensor, and at least 16 KB of RAM, and 64 KB of ROM, all preferably with the lowest possible power consumption. I am going to use an ST Nucleo-L432KC board based on the STM32L432 MCU, just because I have one lying around, but there are many different boards that fit the requirements, and the project can be built around any STM32L4-series microcontroller(MCU).

This is what is included in the board and its MCU:

  • A 72 MHz ARM Cortex-M4 kernel.
  • 64 KB of RAM.
  • 256 KB of flash memory.
  • Hardware SPI and I2C interfaces.
  • Analog-digital convertor (ADC) to monitor the battery charge.
  • A user-controllable LED.
  • An on-chip RTC with a 32.768 kHz quartz oscillator.
  • On-board ST-LINK/V2 debugger/programmer, which allows us to flash our code to the MCU and debug it right on the chip.
  • A hardware serial port (UART) connected to a USB-UART bridge, which can be used for diagnostics output.
  • Plus lots of other stuff that we won’t need for this project.

We’ll use the same sensor and screen we used in Part 1:

  • A Bosch BMP085 air pressure sensor. We’ll use a GY-65 module, though a BMP180 (GY-68) would do the job just as well.
  • A Waveshare 4.2 inch e-Paper screen.

The sensor and the screen can both be powered by either 5V (Arduino) or 3.3V (STM32).

A single-cell LiPo or Li-ion battery is a good solution to power the device. I’m going to use a 1000 mAh battery from an action camera. Any battery would be suitable, for example, an old phone battery or an 18650 cell. If you do use an 18650, make sure to use one with a built-in protection circuit. Unprotected lithium cells can cause fire or explosion if they are short-circuited.
Let’s use the simplest charger possible – a module based on a TC4056A chip. They are readily available on eBay and in most electronics and hobby stores. I have this one:

Board

It contains its own charge/discharge protection circuit, which is always nice to have.

Nucleo board modding

Since I’m aiming for a battery-powered application, I need to disconnect everything extra that consumes electricity from the battery. This includes the power LED and the ST-LINK interface. According to the documentation, the onboard MCU can be powered via a 3V3 pin if the solder bridges SB9 and SB14 are removed. I’ll also remove SB16 and SB18, as they only provide pin-to-pin compatibility with Arduino but they block the usage of two pins. Below is the board layout from the documentation and the solder bridge map.

Board layout

Now that the on-board power supply is disconnected from the MCU we need to provide an alternative power supply. I use the LDO that comes with the BMP085 module. I solder an extra wire to the 3.3V pin of the sensor chip and fix the wire to the board with hot glue to prevent stress on the solder connection.

Pin

Full circuit assembly

With these modifications done, I can finally put the circuit together:

Circuit

The device can be powered and charged by either of two USB ports – one is on the Nucleo board, the other is on the charger – but debugging and flashing can only be done via the Nucleo USB. Note also that the IN+ <-> 5V wire can be removed when we’re finished building and we no longer need to power the ST-LINK interface.

I mounted the whole setup on an IKEA tablet stand:

Construction

Firmware

All the stages from this point on are related to source code, and I make a git commit after each stage so that you can follow along with each change step by step. It is worth mentioning that all git manipulations were made right from CLion.

Git integration

Configuring peripherals

In this stage, we write the actual firmware for the device. I use STM32CubeMX, the GCC toolchain for ARM and CLion. First of all, let’s create a project in STM32CubeMX, select the Nucleo-L432KC board, and then configure all the peripherals:

Peripherals

  • I2C1 – standard mode, 100 kHz speed, 7-bit address
  • USART2 – 115200 bps, 8 bit, no parity
  • ADC1 – software-triggered Vrefint channel enabled
  • SPI1 – transmit-only master mode, Motorola format, 8 bit max speed
  • Additional GPIO for the display PA0 as input; PA1, PA3, and PA4 as GPIO output
  • The RTC must be clocked by an LSE (i.e. quartz oscillator) with internal wake-up enabled

Note that the toolchain must be set to STM32CubeIDE on the Project Manager tab.

STM32CubeIDE

After setting all that up, I select the project folder and generate the skeleton of the project by clicking “Generate Code”.

Writing code

First of all, the IDE must be configured for embedded development. Check out CLion’s webhelp for details on how to do this.

Now it’s time to start CLion and open the project. CLion creates a CMake structure for the project and then we can write the code. The project is now compilable but it does not work as intended. First of all, I need to write error-reporting routines, and call them from the error handler or fault handlers. I am able to use all the on-chip debugging features available in CLion for this. That means stepping, starting, and stopping the firmware, watching variables and even peripheral registers and bits. Follow bit 3 in the GPIOB ORD register in the animation:

Debugging in CLion

I also redirect stdout to the UART2 serial port with the function:

__unused int _write(__unused int handle, char *data, int size )
{
    HAL_UART_Transmit(&huart2, (uint8_t*)data, size, size);
    return size;
}

Note that the project contains a lot of auto-generated files, and I can only change those that are between pseudo comments / USER CODE BEGIN whatever / and / USER CODE END whatever /. Otherwise we may lose all our changes when the project is regenerated in STM32CubeMX.

Traditionally, most embedded source code is written in C and not C++. Chip vendors, including ST, used to provide both startup code and hardware-access libraries also in C. Nowadays C++ has had a lot of improvements that make it way more error-proof and readable, and I prefer to use C++17. Unfortunately, STM32CubeMX generates C code, and I need to call the C library functions anyway. The only way to use C++ and the vendor codebase is to make one or more separate files (cppMain.cpp in my case) and write most of the code there. There is a function declared as external C void cppMain(), and it’s called from the global main() function when all of the automatically-generated initialization is executed. Note that the C++ standard is set via the set(CMAKE_CXX_STANDARD 17) clause in CMakeLists.txt Commit.

Now let’s get the pressure sensor working. I am borrowing the code from the same Adafruit library that we used in Part I, and I’ll modify it to use the STM32’s I2C interface. The measurement result is printed to the standard output,, meaning it is sent via the UART port which is connected to a virtual COM port on my PC. I use the PuTTY utility to watch the port and check the results. Commit.

Since the device is battery-powered, the firmware should keep an eye on the battery voltage and warn us if the battery is running low. We have an on-chip ADC for that. To consume as little power as possible, the MCU must be put into sleep mode as soon as all the screen update tasks are done, and the RTC must help us to wake the device back up.

Single-cell LiPo batteries normally provide voltage in the range 3.0–4.2 V, and our power supply circuit limits that to roughly 3.3 V max. Without a voltage divider we can’t measure battery voltage above 3.3 V, but when the battery voltage drops below 3.3V, the 3.3 V line also drops to a lower voltage, which we can measure. The firmware must measure the voltage of an internal voltage reference relative to the supply voltage, and then calculate the actual voltage using a pre-programmed calibration value stored in the MCU’s non-volatile memory. There is a macro __LL_ADC_CALC_VREFANALOG_VOLTAGE() for that.
Commit.

The next three steps are about getting the display working. I have ported the same WaveShare library for the 4.2” e-paper display as I used in Part I to the STM32 platform. The library was written in classic C++, but I prefer the later standards, starting from at least C++17. CLion constantly performs code inspections and marks suspicious code with yellow warnings. This significantly reduces the number of typos and small mistakes. But, at the same time, the existing code usually sets off lots of warnings, both reasonable and false-positive. I always check these warnings, as some of them will indicate real bugs or performance issues. Some attributes and keywords like maybe_unused, noexcept, noreturn, __unused help to mitigate false-positives, and together, that helps to keep the code cleaner and more error-proof. I add the Doctor Jekyll NF font, which I personally like much more than the fonts provided with the library. The font was converted from TrueType format to bitmap in the form of a C file with this font generator script.
Commit.

Now I can go further and output real data, but for now without graphics. Also, I can draw my own symbols as a bitmap font. These symbols include “Battery low”, “Battery empty” and four pictograms to show the rises and falls in atmospheric pressure. My drawing abilities are way below my coding skills, so you’ll have to forgive me for that. I define “Battery low” and “Battery empty” as 3.0 V and 2.9 V at the 3.3V line respectively.
Commit.

The next stage is to draw the historical pressure data as a chart. I use the MCU low-power mode, named “shutdown”, between measurements. In this mode, the RAM content is lost, and I cannot store the data there. Fortunately, there is a special block in the MCU named “backup domain”. It’s thirty-two 32-bit registers that keep running in shutdown mode along with the RTC, while everything else is switched off. Twenty-four registers are used to keep historical data as forty-eight 16-bit values, one value per hour, for two days in total. The RTC wakes up the MCU from time to time to update both the history and the screen.
Commit.

Power consumption

Now we have a working device. It displays weather data and plots a pressure chart. Let’s check how long it works with my battery on a single charge. I’ve made a simple power meter, which is also based on the STM32 MCU.

Power

I will describe how I made the power meter in a separate blog post. I don’t believe it’s the most precise meter in the world, but I think it gives a reasonable indication of the power consumption.

Power details

As you can see in the picture, the metered average consumption over 42 minutes is about 25 mA. That means that it will last less than two days on one 1000 mAh battery, which is definitely not enough. Let’s do some optimizations.

MCU configuration changes to optimize power consumption

We can decrease the MCU clock from 72 MHz to 24 MHz, connect the CPU kernel to the MSI generator, and switch off extra clocking units (HSI, HSI48, PLL, and LSI). 24MHz is definitely enough for this application. That can be done via STM32CubeMX on the Clock Configuration tab with a few mouse clicks and three lines of code.
The screen update requires several arrays of data, each of which is 15 KB. The library does this in poll mode, as it’s done for 8-bit AVR. All the STM32 MCUs support Direct Memory Access, that makes SPI data transfer faster and lets the CPU kernel go into Sleep mode while the transfer is being done, and so it consumes less power. This requires a few clicks in STM32CubeMX, and then changing a couple of lines in the display library code.

Firmware changes to optimize power consumption

The MCU spends quite a lot of time waiting for screen updates, sensor initialization and other operations with time delays. We can optimize this by rewriting the standard HAL_Delay function (see the main.c file). The only change there is a __WFI (wait for interrupt) call in the delay loop. It puts the MCU into Sleep mode between timer interrupt requests.
The last change, and the most important, is to bring Shutdown mode into use. In this mode, most of the MCU except the backup domain is shut down, and the power consumption drops considerably. When the MCU wakes from that mode, it resets and starts the execution of the firmware from the very beginning. Thus we lose the global infinite loop in the code, because the execution never continues after shutdown. I have left an empty loop in the code just to let the compiler know that the execution does not proceed beyond the HAL_PWREx_EnterSHUTDOWNMode() call. Finally, I set the RTC wakeup period to once per hour. Commit.

Let’s see how this affects our power consumption.

Power consumption optimization

Here is the data after 24 hours – 0.03 mA in Shutdown mode between refreshes, with 0.07 mA average consumption. This gives us more than a year of battery life, even taking into account battery self-discharge and allowing for some reasonable current measurement error. This is a bit longer than the two days we had before!

Conclusion

While implementing this project I’ve built a functional device out of a simple Arduino-based prototype. Of course, the device isn’t perfect, the code could still be optimized, and I could use better hardware. I’ve been using quite a lot of modern features such as automatic code generation, version control system, on-chip debugging, and CLion’s smart editor. Of course, the project could be done without all of this but the development process would take much longer. For now, I’ll say “see you later”, and get my toolbox out, it’s time for me to make a case for my weather station and hang it on the wall.

image description