Arming with an OS [Hackaday]

View Article on Hackaday

We see tons of projects with the infamous “Blue Pill” STM32 boards. They are cheap and plentiful and have a lot of great features, or at least they were before the chip shortage. I recently picked up a “Black Pill”, which is very similar but has an even more powerful processor. For a few bucks, you get an ARM CPU that can run at 100 MHz (but with USB, probably 96 MHz). There’s 512 kB of flash and 128 kB of RAM. There’s a USB type C port, and even a button and an LED onboard. The thing fits on a breadboard and you can program it with a cheap STLink dongle which costs about $10.

The Blackpill module on a breadboard.

Of course, you then have to consider the software. The STM32Cube stuff is a lot to set up and learn but it does let you do just about anything you can imagine. Then there is the STM32Duino plug-in that lets you use it as a beefy Arduino. That works and is easy enough to set up. However, there’s also Mbed. The only problem is that Mbed doesn’t work right out of the box. Turns out, though, it isn’t that hard to set up. I’ll show you how easy it is to get things going and, next time, I’ll show you a practical example of a USB peripheral that uses the mBed RTOS features.

First Steps

Obviously, you are going to need a Black Pill. There are at least two choices but for as cheap as they are there is little reason not to get the STM32F411 version that has more memory. The DIP form factor will fit in whatever breadboard you happen to have and a USB C cable will power the board so unless you are driving a lot of external circuitry, you probably don’t need an external supply.

Unlike some boards, though, the USB port won’t help you for programming unless you burn in a bootloader. In theory, the board can do DFU if you hold down the BOOT0 button during reset. However, my board would only enter DFU mode on occasion. Reading the Internet that sounds like a common problem. The trick seems to be to unplug the USB and replug it instead of using the reset button. Press BOOT0 while plugging in the board. After a few seconds, release the BOOT0 button. That makes it a little more reliable. Then you need a DFU flash program to program a .bin file to write to alt 0 of the DFU device at location 0x08000000. All that and you still don’t get debugging.

Luckily, an STLink v2 dongle that goes from USB to a 10-pin connector is very inexpensive — $10 or less. This will let you do programming and debugging. However, there is also a trace feature that these dongles don’t support.

You can get along fine without tracing, but it is handy that you can do “printf-style” output via a feature called SWO (serial wire output) and not have to use the USB port as a serial port. If you want SWO, you’ll either need a more expensive version of STLink (with the bigger connector) or there is, of course, a hack. You can also use other similar probes like the Blackmagic probe. In some cases, you can also use semihosting, but it is better to either get the right probe, hack the SWO output, or just use the debugger. Of course, if you are setting up the USB serial port — easy enough to do — then you can use it with no problems.

The STLink dongle I used has 10 pins. Unfortunately, not all of the clones have the same pinout. The four pins on the Blackpill, starting at the left while looking at the connector are: 3.3 V, SWDIO, SWCLK, and Ground. If you don’t mind getting power from the USB port, you only need the last three pins.

Usually, these clone adapters have a pinout on the case. For mine, the three wires I needed were on pins 6, 7, and 8. If you do draw 3.3 V from the device, be careful. Drawing too much current or shorting the power line will kill the dongle.

Another issue with the clones is that they often have out-of-date firmware loaded. You can get the official STM32CubeProgrammer that knows how to update them. Just remember to unplug the device after you update and refresh the list of programmers after you plug it back in.

Software

As I mentioned, there are lots of options and plenty of toolsets. I generally like the Mbed tools. When we first looked at Mbed, it was sort of an Arduino-like ecosystem. The IDE was online, but you could go offline with a few options. However, since then it has grown and now has a variety of tools and even a full-blown operating system if you want it. There is a way to select “bare metal” mode if you are trying to build something simple, but with something like the Black Pill, you have plenty of memory and so pushing a lot of code to it isn’t really a problem. Even without bare metal mode, the linker tries to remove things you aren’t using, so it isn’t so bad.

The bad news, though, is that the ecosystem doesn’t directly support the Black Pill. It does, however, support a Nucleo board with the same CPU. That will work and for simple tests, it isn’t bad. Sure, the pin names are wrong but you can just specify the real names like PC_13 instead of LED1. However, the real bad news is that target board has no USB support. If you try to use USB drivers, you will simply halt because the system assumes you don’t have the hardware to handle it.

There’s good news and bad news. The good news is that there is a user-contributed setup for the Black Pill. It has a more stable clock reference, allows USB, and has correct pin definitions. The not-so-great news is that it only seems to work on the Mbed IDE running locally. That’s not so bad since I wanted to use something local anyway. If you use a different toolset, you might find yourself on your own to get the targets defined. PlatformIO also seems to work as it did for the Blue Pill.

Let’s Go!

Once you have everything assembled, it is pretty easy to get a program running. Next time, I’ll show you some more fun with USB, but for now, let’s do a simple LED flasher with some output on a USB serial port. For example, you can run the “Blinky” sample in an online emulator. The problem is that printf doesn’t go anywhere useful on our board.

No problem:

#include "mbed.h"
#include "USBSerial.h"

DigitalOut led(LED1);
USBSerial usbSerial(false); // don't wait for connection

int main() {
   usbSerial.connect();  // set up serial port
   while (1) {
     led = !led;
    usbSerial.printf("Blink! LED is now %dn", led.read());
    ThisThread::sleep_for(500ms);
    }
}

It is that simple. Just set up a project as described in the Black Pill configuration. That is:

  • Create a new project in Mbed IDE.
  • Right-click on the program’s root folder and in the popup window select Add library…
  • Enter https://os.mbed.com/users/hudakz/code/BLACKPILL_Custom_Target and click on the Next button.
  • Open the drop-list and select default then click Finish.
  • Open the BLACKPILL_Custom_Target folder and drag the TARGET_BLACKPILL_F411CE folder to the project root folder.
  • Drag custom_targets.json from the BLACKPILL_Custom_Target folder into the root folder.
  • Delete the BLACKPILL_Custom_Target folder from your project.
  • Open the Target drop-list and click on the button with a “chip” icon on it.
  • Open the USB device drop-list and select your STM32 ST-Link programmer (or the DFU device if you are going that route).
  • Select BLACKPILL_F411CE as the target.
  • Click on the Save All button.

If you are using the STLink, you can simply click the run button or the debug button to get started. If you selected DFU, the IDE will tell you where it left the .bin file. That’s what you’ll need to feed your DFU programmer. If you use Linux, the dfu-util line will look like:

dfu-util -d 0483:df11 -a 0 -s 0x80000000:leave -D blackpill-program.bin

About the Example

The default constructor for USBSerial causes the program to hang until you actually open the serial port which may or may not be a behavior you want. However, with the Black Pill and my setup, it didn’t reliably enumerate the serial port using the default constructor anyway.

In the simulator, they use the wait_ms function to pause, but I change it to the more modern ThisThread::sleep_for. The truth is, the above program has a full-blown RTOS with scheduled prioritized tasks and a variety of synchronization methods ranging from mailboxes to mutexes. There are also drivers for USB devices of all kinds, CAN bus, file systems, and lots more. We aren’t using any of those things right now, but they’re there. We still have a single thread and can work with it using methods like sleep_for.

Plain Old Printf

If you prefer to just have printf and the other console I/O go to the USB port by default, you can add the following code, assuming you also have the usbSerial object visible to this code.


namespace mbed
{
   FileHandle *mbed_override_console(int fd)
   {
   return &usbSerial;
   }
}

Then you can use printf instead of usbSerial.printf and the result will be the same. There’s only one catch. If you use the default USBSerial constructor, your program will hang until the USB port connects. But if you set the first constructor argument to false, that won’t happen. What will happen is when your program attempts to write output and nothing is there the stdio library will helpfully decide you don’t need to try any further so nothing will appear. To solve that, you need to periodically call clearerr(stdout) or on any other handle that you might use. There are other alternatives. For example, you can avoid calling things like printf when usbSerial::connected() returns false. Or, call clearerr when you detect the port going from not connected to connected. If you are interested, I’ve left another example on GitHub to get you started.

Next Steps

The Mbed IDE in Debugger Mode

You can easily run the code through the debugger, which is quite nice. Of course, there are plenty of other debugging options since, at the core, it is basically a gdb server talking to hardware over the STLink connection.

Next time, though, we’ll turn the Black Pill into a fake USB keyboard and use it to send volume control commands to a PC that correspond to motion on a potentiometer connected to an analog input. Along the way, we’ll create some threads and make them cooperate.