As I write this, the project is at commit c9b6b35. You can use this link to browse the code at that particular commit, in case it changes in the future and some of the information in this post no longer applies. The initial code I got from Tom had the motor start immediately after power up, with a speed set point which was hard coded. Statistics were continuously printed on the screen of the laptop using a simplified version of printf(), but there was nothing resembling scanf(), which would make life much simpler to make the control of the motor more interactive.

Using newlib-nano as C standard library

In the Makefile, option --specs=nano.specs is passed to the linker. This means my object code links to newlib-nano, a space-conscious version of newlib. Newlib is an implementation of the C standard library intended for embedded systems. Since both printf() and scanf() live in the standard library, I should be in business, right? Well, it’s not so simple. printf() needs to be told how to write in your particular setup (in this case I want it to write to the UART) and similarly scanf() needs low-level read support. Luckily, I found this post by François Baldassari very clearly explaining how to add this kind of support in a particular system. I needed to add code in usart.c for 7 low-level functions: _fstat(), _close(), _lseek(), _isatty(), _sbrk(), _read() and _write(). The first 4 are fairly trivial. I just copied/pasted his code.

_sbrk() is used by printf() to dynamically allocate memory. It receives as an argument the number of chars to allocate and internally manages a pointer to the top of the heap, which grows every time new memory is allocated. This means you need to know were your heap starts before you can write _sbrk().

An aside: linker scripts

In the STM32F401RE, there are 512KB of flash starting at address 0x08000000 and 96KB of RAM starting at 0x20000000. The linker script is where you tell the linker where each part of your program goes. It uses a relatively obscure syntax, but as luck would have it, François comes again to the rescue with another clear post. Space in RAM is used for the .data and .bss sections successively. The .data section, starting at address 0x20000000 in my case, contains all initialized data (i.e. variables I initialized after declaring them in the code). The .bss section contains all uninitialized data, which is cleared (i.e. set to zero) by the startup code written in ARM assembler. The rest of RAM is free to use. In my linker script, the heap starts at the end of the .bss section and grows upwards (i.e. towards increasing addresses) whenever new memory is dynamically allocated. The stack starts at the end of RAM (address 0x20017FFF) and grows downwards. I don’t think there is any rock-solid way to make sure your heap and stack don’t step on each other at some point, at least not at compile time. Your compiler cannot know in advance which paths your code will take, and how many times you will allocate dynamic memory (on the heap) or call functions which will need space for their arguments and automatic variables in the stack. In this particular application, the code is relatively simple and most of the 96KB is available for heap and stack, so we are safe.

The intricacies of _read() and _write()

The UART code is interrupt-based. This ensures you don’t overflow the UART TX path by writing another character to it before it has finished sending the previous one. Conversely, in the RX path, it is good to be awakened by an interrupt every time a character is received from the serial line so that it can be read before another incoming character overwrites the RX register.

On the read side, the UART generates an interrupt every time a character is received (from the laptop through USB in my case). The interrupt handler, USART2_IRQHandler(), reads the character and does two things with it: send it to the TX of the UART (so the user typing in the laptop can see the character (s)he typed) and push it to an RX FIFO. The _read() function just reads from the FIFO in a busy loop until the number of characters requested by scanf() is received. If a \r character is received, the busy waiting stops and the function returns with the number of characters read until then.

On the write side, the UART generates an interrupt when its TX buffer is empty. This means that, after it is enabled in the usart_send() routine, it will generate an interrupt. Then, in the USART2_IRQHandler() function, the character will be read from the TX FIFO and sent out to the serial line.

Status and plans

With printf() and scanf() fully working now, I have implemented three simple commands: start, stop and set speed XXXX. Next I plan to add some simple telemetry so that I can use the data to better tune the PI controller and study in detail the deceleration of the flywheel after external power is switched off.