📜 ⬆️ ⬇️

Bildim under stm32duino using CMake (and scavenging from the linker)

image

Hello! How often do you think how a code written in a beautiful IDE turns into a set of bytes, digestible for a processor or microcontroller? Here I personally do not often. Works well and well. I clicked the Add File button - the IDE itself added the source code to the project, it itself calls the compiler and linker. Write yourself the code and do not think about the little things.

But sometimes all these implicit things get out on the surface and start dreaming in nightmares. It turns out even in the simplest Hello World under the hood there are a lot of all sorts of different and very interesting gears that provide smooth and comfortable movement. The world of microcontrollers is no exception.
')
Today it will be about replacing the build system for my project . For various reasons, I was closely with Arduino and had to look for something where you can turn around. Under the cat, I described my experience of switching from the Arduino system build to the firmware assembly under the STM32 microcontroller and the stm32duino framework using CMake.

Build Arduino system


I like Arduino because it allows you to smoothly enter the world of microcontrollers. But, as many point out, this is all for beginners and for a more or less serious project, the Arduino is not suitable. I do not want to be categorical and declare that Arduino sucks and all that jazz. Let's better sort everything out.

So, I would single out the following things in Arduino:


Each of these parts is independent of the rest. So in ArduinoIDE, you can write under other microcontrollers (for example, under ESP8266), or vice versa, you can abandon ArduinoIDE and learn all the charms of Arduino somewhere in Atmel Studio, Eclipse or even vim. Some boards are not quite similar to the Arduino boards (for example, flight controllers for quadrocopters), but you can easily infuse Arduino sketches into them. Conversely, under the Arduino board, you can program in bare C or assembly. As for the Arduino framework, it can be noted that it is ported to many microcontrollers ( STM32 , TI ), which lowers the threshold of entry into this world to an acceptable level.

image
In the middle of the board on stm32f103c6 (in fact stm32f103cb), to the right is arduino nano. Photos from here ( here stm32duino is touched on a little more deeply)

In my project, I almost immediately abandoned the Arduino IDE in favor of the more familiar Atmel Studio. I also refused the Arduino boards in favor of the STM32 as a more powerful platform with a little more peripherals and memory. I really would not like to give up the Arduino framework - it is quite beautiful, comfortable and provides everything you need. I use stm32duino - Arduino port under STM32 microcontrollers. But the SPL (Orthodox abstraction over the registers and the controller's stuffing from the ST itself) didn’t go down - the code is too bulky and ugly.

But there is another very implicit part of the Arduino - build system . This implicitness is a big plus for small projects, when you do not want to think and understand how it all works. Just add files and libraries and it collects everything. For beginners, the most it. But when the project grows at least a little, several libraries are added and a fine adjustment of the compiler is required, the Arduino build system begins to interfere. Here is a list of things that interfered with my project.


We study the patient


image

So, the problems of the Arduino build system are clear. Clearly you need to come up with something better. But before moving on to another build system, you first need to figure out how the Arduino itself builds its projects. And it works very interesting.

First of all, the build system tries to knock a sketch file (.ino) with the -o nul key (we do not write anything, we only collect compilation errors). If the compiler does not find any kind of header, the buildsystem will look for whether this header is there in the installed libraries. If it is, it adds the -I key (additional include path) and tries to knock it one more time. If the build system does not find another header, the operation is repeated. The path string for the -I switch is accumulated and applied to the next file in the project.

In addition to the trick with the -I key, the build system will also build the library itself. For this, it tries to compile both all * .c and * .cpp files in the library directory. Library dependencies for libraries are solved by the method already described. The library is compiled entirely, even if not all files from it are used. And well, if the linker then throws out too much. And if not?

So, if a global object is declared in the library (for example, a Serial or SPI class), then the code for this object always gets into the firmware, even if this code is not actually used. Those. Inadvertently added #include directive can add a few extra kilos to the firmware.

But that is not all. When all the dependencies are compiled, the compilation process is launched once more, already with the correct keys for compilation, code generation, optimization, and so on. In general, each file is compiled at least 2 times, and especially unsuccessful files (such as .ino) can be compiled as many times as many libraries are included in the project.

Any system pieces (interrupt vectors, basic board initialization) are also compiled along with the project. Fortunately, some of the files that are compiled are then compiled into .a static libraries. But this, in fact, shamanism from the creators of stm32duino - for other libraries, everything is going to be file-based.

I think for a small Arduinov project with 1-2 libraries, such an approach to assembly does not create too large an overhead. But my project from 25 cpp files and 5 libraries began to compile for almost a minute. Do you know how many times the compiler, linker and other programs from toolchain are run? 240 times !!!

Nevertheless, there is nothing military in the assembly itself. I was afraid of some hidden assembly mechanisms that could not be repeated without the Arduino. In fact, everything is assembled by a sequential call to the compiler and linker with a specific set of keys. So all the same can be repeated in another build system.

It is worth noting that I investigated the work of the build system when building under STM32 and the stm32duino framework. But when building a classic Arduino on an ATMega controller, everything is almost the same (only a bit simpler)

We try CMake


image

A colleague in the workshop recommended to look at CMake - there are already ready tulchens for assembling Arduino projects. So, I installed CMake, took the minimal project from the examples, filed a bit CMakeList.txt. But when I launched CMake, I got a crash in ld.exe at the configuration stage (when compilers, linkers and similar things are checked). Attempting to understand exactly what was happening did not succeed - the same command line launched separately was run without problems. How to go around it, I did not understand.

In search of a solution, I carefully studied the tulchein files and realized that I was not digging there at all. While the real Arduino has a plug-in system to which you can add any boards with compilers (not necessarily AVR), one of the tulcheins I have studied is sharpened exclusively for AVR. And only 2-3 types of standard boards are available.

The second toolchain looked better. He parsed the Arduin-type files of the boards.txt and searched through the directories of the variants, collecting all the available options for assembly, boards and processors. Nevertheless, something confused me. It seemed that at the exit I would again get a monolith in the style of arduino. It was not very clear whether the ARM compiler would work instead of AVR. Moreover, it is still not clear how to control libraries.

In general, nothing bad to say about this tulchayn. Simply, without defeating crash when generating make files, I decided to look for a solution elsewhere.

CooCox


I love to write code in a good IDE. The most popular IDEs for STM32 are often mentioned on the Internet - Keil, IAR and CooCox. The first 2 paid and with unflattering reviews on the topic of usability. But CooCox is highly praised. The fact that it was built on the basis of Eclipse also bribed me, and I have long wanted to meet him. It seemed to me that this allows us to fine-tune the assembly process. Looking ahead, I say that, alas, I was wrong.

Having installed CooCox I was quite surprised that it goes without a compiler. In general, this is understandable - the IDE is separate and the compiler is separate. It's just as unusual after many years in Visual Studio. Well, okay, this is simply solved by installing the compiler as a separate installer , although at first I took the compiler from Atmel Studio.

image

And then the problems started. I spent an hour trying to start a blinker on the LED. Found on the Internet about ten options of a different degree of compilability. It would seem, where there can be mistaken? But no. Compile, fill - does not work. I recreated the project several times - it did not help. As a result, I went by the very Nuba instructions, clearly and literally following all the instructions. Firmware flooded through the UART and the system bootloader and light bulb blinked happily.

I would like to blame the stm32duino / libmaple USB bootloader, which was the cause of the problem, but it is more reasonable to acknowledge my misunderstanding of the initialization process on the STM32 controllers. Having understood the question, I described my “discoveries” in a separate article . In a nutshell, the problem was in the incorrect starting address of the firmware. When using a bootloader, the firmware needs to be assembled with a different starting address. This parameter, in fact, is not in the classic Arduino, but it is very important for STM32 microcontrollers.

Thoughts on STM32 bootloaders
Of course, you can stitch through the UART, but this one is still a hemorrhoid. You need to constantly switch Boot jumpers and click a lot in ST Flash Demonstrator. I have a Chinese ST-Link, it would be possible to connect it, sew through it and debug. Probably, I'll get to this before, when I really need in-circuit debugging.

But at the moment I consider the firmware via USB as the most convenient for several reasons. I still have the USB cable for power and the USB serial connected. I'm also going to actively pick USB Mass Storage soon. In the end, in the finished device will also be displayed USB. So why use additional components instead of flashing through USB?

Returning to the topic of configuration management, and more specifically where and how to store libraries. I’m carrying all my burdens with me. All library changes (including their configuration) I would like to version in my repository. Of course there are classic unix patches, but it looks like the last century. But what to use in return? I would like a solution in the style of “pulled the source code with one team, knocked it out, poured it into the MK”.

For a long time I tried to deal with submodules and subtrees in the gita. I, as a person who has never worked closely with a git, do not understand anything. I even read the first few chapters of Git Book, but I got even more confused. I asked for help from more experienced colleagues in these cases, but they bent their fingers even more and it became even less clear. Decided to do it through subtrees . I still can not imagine what it threatens me.

I liked the subtrees because I got the files I needed right in my repository. And with the ability to edit them. Submodule, nevertheless, implies storing changes somewhere in another repository. If it is reasonable for bug fixes (you can easily fork it and then push the fixes into the main repository), then it is at least strange to store the library settings. Versioning changes for one project in different repositories would not be desirable.

So I pulled the stm32duino for STM32F1xx series controllers into my repo. Tightened entirely, with examples and libraries. To be honest, I don’t like the structure, and I don’t need everything from there. But I do not know how to do it differently, and how it will be then. I compensated for the fact that I laid out all the files as I like in the CooCox project.

As a result, I pulled all the necessary files into the project, set the same defaults as Arduino, after which the linking process began. Went by mistake. If there was no symbol I was looking for in which source code it was declared and added only this source code. It was a very interesting experience, because I understood what was going on and where I was (I mean stm32duino / libmaple) and how it works there.

image

As I said, the build under ARM (which is STM32) is more complicated than the build under AVR. The thing is that the compiler is more general there and allows you to configure yourself very finely. On the one hand, this is good and you can tyunit the assembly process, on the other hand, it complicates the configuration. So, the linker scripts are additionally used here - special files that describe which sections should be in the resulting binary, where these sections are located and how much space they occupy.

It turned out that low-level code can refer to characters that are declared in linker scripts and vice versa. If the linker is called with standard settings, then these characters are not found. To solve this problem on the linker settings page, I unchecked the 'Use Memory Layout from Memory Window ”checkbox - the scatter file field opened. There I registered the linker script that was used in the Arduino build ( STM32F1 / variants / generic_gd32f103c / ld / bootloader_20.ld ). After that, everything immediately linked.

image

But then an unpleasant surprise was waiting for me - the assembled firmware took 115kb at once (it was 56k for the Arduino). Moreover, all the characters were mixed there, and the firmware contained a lot of C ++ runtime - rtti, manglers, abi, exceptions and a lot of unnecessary things. I had to compare the linker commands that arduino and CooCox make and for each key to smoke documentation.

It turned out in CooCox you cannot choose between C and C ++ compilers. Because of this, it is impossible to configure each of the compilers separately. So C ++ code is compiled using gcc (and not g ++) and by default generates a ton of code, references to rtti, ABI and something else. And it seems that it is not so easy to zakostilyat. There is a field for additional compiler flags, but if you add something like -fno-rtti there, then gcc starts swearing right away, saying that you give me flags from g ++?

image

On the Internet, people suggested CooCox for the second version - it already has more settings, can distinguish between C and C ++ code and calls the correct compiler for each of the file types. But at the same time, it is still impossible to set up separate keys for each of the compilers. So the -fno-rtti key can still not be added only for g ++ - it is also transmitted to gcc, which it angrily curses at it.

In general, the second CooCox as it did not go - too much glamorous UI, at the same time to the detriment of convenience. The first, by the way, is also not super in terms of UI - a million settings, and the font in the build console cannot be changed (despite the fact that you can do it in the full Eclipse).

Again CMake


Since such well-known tools as CooCox do not allow to make a trivial setting of compilers, then well, it’s nafig. We will go down to the lowest level and write everything by hand. Well, like hands ... With makefiles, of course.

The last time I wrote naked makefiles was almost 10 years ago. I have already forgotten all the subtleties, but I remember for sure that this is an extremely ungrateful thing. Wacky syntax and very easy to make a mistake. And it is extremely not portable. I decided to try CMake one more time, but not with arduino tulchein, but with STM32. What does all the rules: separately installed eclipse, compiler, CMake, MinGW32 for make.

I didn’t look at the tulchains much. I came across this , from where I got a general idea, but the toolchain itself took it from here . I decided not to install it in the general pile, but to carry it with me. Besides, I don’t need everything there, but only 2 files - gcc_stm32.cmake where the common variables and procedures are declared, and gcc_stm32f1.cmake where the parameters of my controller are described.

I have all the libraries in directories, which (theoretically) I will synchronize with the main repositories (when I figure out how :)). Therefore, adding CMakeList.txt to each library was like a noncomme. I decided to make one common CMakeList.txt in the directory with libraries and describe the build of all libraries in it. Each library is collected in an archive (static library) and then all linked into a binary.

CMake script to build libraries (one script for all libraries)
# Not pushing this file down to libraries in order to keep source tree as is (not populating with extra files, such as CMakeList.txt) # # Below each section represents a library with its own settings ################### # NeoGPS ################### SET(NEOGPS_SRC NeoGPS/DMS.cpp NeoGPS/GPSTime.cpp NeoGPS/Location.cpp NeoGPS/NeoTime.cpp NeoGPS/NMEAGPS.cpp ) ADD_LIBRARY(NeoGPS STATIC ${NEOGPS_SRC}) ################### # FreeRTOS ################### SET(FREERTOS_SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR}/STM32duino/libraries/FreeRTOS821) SET(FREERTOS_SRC ${FREERTOS_SRC_DIR}/MapleFreeRTOS821.cpp ${FREERTOS_SRC_DIR}/utility/heap_1.c ${FREERTOS_SRC_DIR}/utility/list.c ${FREERTOS_SRC_DIR}/utility/port.c ${FREERTOS_SRC_DIR}/utility/queue.c ${FREERTOS_SRC_DIR}/utility/tasks.c ) ADD_LIBRARY(FreeRTOS STATIC ${FREERTOS_SRC}) ################### # Adafruit GFX library ################### ADD_LIBRARY(AdafruitGFX STATIC AdafruitGFX/Adafruit_GFX.cpp ) ################### # Adafruit SSD1306 library ################### ADD_LIBRARY(AdafruitSSD1306 STATIC STM32duino/libraries/Adafruit_SSD1306/Adafruit_SSD1306_STM32.cpp ) TARGET_INCLUDE_DIRECTORIES(AdafruitSSD1306 PRIVATE STM32duino/libraries/Wire STM32duino/libraries/SPI/src #In fact it should not depend on it AdafruitGFX ) 


In the assembly of the main part there are several non-trivial things that I described below. For now, I’ll bring CMakeLists.txt in its entirety.

Script assembly of the main part of the project
 # Build rules for GPS logger target. # App specific compiler/linker settings are also defined here SET(SOURCE_FILES # Screens and screen management stuff Screens/AltitudeScreen.cpp Screens/AltitudeScreen.h Screens/CurrentPositionScreen.cpp Screens/CurrentPositionScreen.h Screens/CurrentTimeScreen.cpp Screens/CurrentTimeScreen.h Screens/DebugScreen.cpp Screens/DebugScreen.h Screens/OdometerActionScreen.cpp Screens/OdometerActionScreen.h Screens/OdometerScreen.cpp Screens/OdometerScreen.h Screens/ParentScreen.cpp Screens/ParentScreen.h Screens/SatellitesScreen.cpp Screens/SatellitesScreen.h Screens/Screen.cpp Screens/Screen.h Screens/ScreenManager.cpp Screens/ScreenManager.h Screens/SelectorScreen.cpp Screens/SelectorScreen.h Screens/SettingsGroupScreen.cpp Screens/SettingsGroupScreen.h Screens/SpeedScreen.cpp Screens/SpeedScreen.h Screens/TimeZoneScreen.cpp Screens/TimeZoneScreen.h 8x12Font.cpp Buttons.cpp FontTest.cpp GPSDataModel.cpp GPSLogger.cpp GPSOdometer.cpp GPSSatellitesData.cpp GPSThread.cpp IdleThread.cpp TimeFont.cpp Utils.cpp ) INCLUDE_DIRECTORIES( . ${GPSLOGGER_LIBS_DIR}/AdafruitGFX ${GPSLOGGER_LIBS_DIR}/NeoGPS ${GPSLOGGER_LIBS_DIR}/STM32duino/libraries/Adafruit_SSD1306 ${GPSLOGGER_LIBS_DIR}/STM32duino/libraries/SPI/src ${GPSLOGGER_LIBS_DIR}/STM32duino/libraries/FreeRTOS821 ) # Do not link to libc or newlib-nano - we are not using anything from that SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --specs=nosys.specs") ADD_EXECUTABLE(GPSLogger ${SOURCE_FILES}) TARGET_LINK_LIBRARIES(GPSLogger NeoGPS FreeRTOS AdafruitGFX AdafruitSSD1306 ArduinoLibs STM32duino ) STM32_SET_TARGET_PROPERTIES(GPSLogger) STM32_PRINT_SIZE_OF_TARGETS(GPSLogger) # Additional handy targets STM32_ADD_HEX_BIN_TARGETS(GPSLogger) STM32_ADD_DUMP_TARGET(GPSLogger) 


As for the parameters of the CMake. In the examples, stm32-cmake suggests specifying which toolchain file to use. This is done by a separate key during a CMake call to generate makefiles. But I do not plan to build a project for different platforms and compilers. Therefore, I simply registered a link to the desired toolchain file in the main CMakeLists.txt.

 # Load the toolchain file that uses vars above SET(CMAKE_TOOLCHAIN_FILE cmake/gcc_stm32.cmake) 

But the compiler does not automatically guess. More precisely, I would have guessed on Unix (it is searched by the default path to / usr), but it must be explicitly indicated on Windows. In general, my command line (do not forget to change Windows slashes to Unix - CMake doesn't like them):

 cmake -G "MinGW Makefiles" "-DTOOLCHAIN_PREFIX=C:/Program Files (x86)/GNU Tools ARM Embedded/5.4 2016q3" . 

Makefiles are generated and you can start building the project. There was no problem with compiling. But then the fight began with a strong linker magic, where I stuck for a week. Below is a list of individual problems that I solved along the way.

Problem: cross dependencies between libraries


The stm32duino framework is quite large. Even at the stage of creating CMakeList.txt for libraries, I tried to divide it into 2 separate libraries - one part that emulates the Arduino API (actually stm32duino) and libmaple, which is a Hardware Abstraction Layer (HAL) and hides low-level microcontroller pieces. In any case, it seemed logical to me. But it turned out to be problematic to link. I thought that stm32duino is built on top of libmaple, but in the second there were also calls to the top layer.

The joke is that the linker collects the libraries one by one and does not return to the previous ones. If there is a call to the first library in the second library, then the linker does not understand that you need to link the first library again. Therefore, unresolved characters occur. It was necessary in the project to combine the two libraries into one.

Then I, however, learned that the linker has special keys -Wl, - start-group / --end-group that change the behavior of the linker. They just make it go through the libraries several times. I have not tried it yet.

CMake script to build stm32duino and libmaple
 ################### # STM32duino, libmaple and system layer # Has to be as a single library, otherwise linker does not resolve all crossreferences # Unused files are commented on the list ################### SET(LIBMAPLE_SRC STM32duino/variants/generic_stm32f103c/board.cpp STM32duino/variants/generic_stm32f103c/wirish/boards.cpp STM32duino/variants/generic_stm32f103c/wirish/boards_setup.cpp STM32duino/variants/generic_stm32f103c/wirish/start.S STM32duino/variants/generic_stm32f103c/wirish/start_c.c STM32duino/variants/generic_stm32f103c/wirish/syscalls.c STM32duino/cores/maple/cxxabi-compat.cpp # STM32duino/cores/maple/ext_interrupts.cpp STM32duino/cores/maple/HardwareSerial.cpp # STM32duino/cores/maple/HardwareTimer.cpp # STM32duino/cores/maple/IPAddress.cpp STM32duino/cores/maple/main.cpp # STM32duino/cores/maple/new.cpp STM32duino/cores/maple/Print.cpp # STM32duino/cores/maple/pwm.cpp # STM32duino/cores/maple/Stream.cpp # STM32duino/cores/maple/tone.cpp STM32duino/cores/maple/usb_serial.cpp # STM32duino/cores/maple/wirish_analog.cpp STM32duino/cores/maple/wirish_digital.cpp # STM32duino/cores/maple/wirish_math.cpp # STM32duino/cores/maple/wirish_shift.cpp STM32duino/cores/maple/wirish_time.cpp # STM32duino/cores/maple/WString.cpp # STM32duino/cores/maple/hooks.c STM32duino/cores/maple/itoa.c # STM32duino/cores/maple/stm32f1/util_hooks.c # STM32duino/cores/maple/stm32f1/wiring_pulse_f1.cpp STM32duino/cores/maple/stm32f1/wirish_debug.cpp STM32duino/cores/maple/stm32f1/wirish_digital_f1.cpp STM32duino/cores/maple/libmaple/adc.c STM32duino/cores/maple/libmaple/adc_f1.c # STM32duino/cores/maple/libmaple/bkp_f1.c # STM32duino/cores/maple/libmaple/dac.c # STM32duino/cores/maple/libmaple/dma.c # STM32duino/cores/maple/libmaple/dma_f1.c # STM32duino/cores/maple/libmaple/exc.S # STM32duino/cores/maple/libmaple/exti.c # STM32duino/cores/maple/libmaple/exti_f1.c STM32duino/cores/maple/libmaple/flash.c # STM32duino/cores/maple/libmaple/fsmc_f1.c STM32duino/cores/maple/libmaple/gpio.c STM32duino/cores/maple/libmaple/gpio_f1.c STM32duino/cores/maple/libmaple/i2c.c STM32duino/cores/maple/libmaple/i2c_f1.c STM32duino/cores/maple/libmaple/iwdg.c STM32duino/cores/maple/libmaple/nvic.c # STM32duino/cores/maple/libmaple/pwr.c STM32duino/cores/maple/libmaple/rcc.c STM32duino/cores/maple/libmaple/rcc_f1.c # STM32duino/cores/maple/libmaple/spi.c # STM32duino/cores/maple/libmaple/spi_f1.c STM32duino/cores/maple/libmaple/systick.c STM32duino/cores/maple/libmaple/timer.c # STM32duino/cores/maple/libmaple/timer_f1.c STM32duino/cores/maple/libmaple/usart.c STM32duino/cores/maple/libmaple/usart_f1.c STM32duino/cores/maple/libmaple/usart_private.c STM32duino/cores/maple/libmaple/util.c STM32duino/cores/maple/libmaple/stm32f1/performance/isrs.S STM32duino/cores/maple/libmaple/stm32f1/performance/vector_table.S STM32duino/cores/maple/libmaple/usb/stm32f1/usb.c STM32duino/cores/maple/libmaple/usb/stm32f1/usb_cdcacm.c STM32duino/cores/maple/libmaple/usb/stm32f1/usb_reg_map.c STM32duino/cores/maple/libmaple/usb/usb_lib/usb_core.c STM32duino/cores/maple/libmaple/usb/usb_lib/usb_init.c STM32duino/cores/maple/libmaple/usb/usb_lib/usb_mem.c STM32duino/cores/maple/libmaple/usb/usb_lib/usb_regs.c ) ADD_LIBRARY(STM32duino STATIC ${LIBMAPLE_SRC}) TARGET_INCLUDE_DIRECTORIES(STM32duino PRIVATE STM32duino/system/libmaple/usb/stm32f1 STM32duino/system/libmaple/usb/usb_lib ) TARGET_COMPILE_DEFINITIONS(STM32duino PRIVATE -DVECT_TAB_ADDR=${VECT_TAB_ADDR} -DGENERIC_BOOTLOADER -DBOARD_maple ) 


Problem: system calls are not linked


Then it was impossible to link normally with the standard library - there was a lack of _sbrk (), _open () / _ close (), _read () / _ write () and some others, which for some reason stick out of the standard library.

In fact, they have a trivial implementation in STM32duino \ variants \ generic_stm32f103c \ wirish \ syscalls.c , but it was not linked for the same reason: at the moment of stm32duino linking, these functions (they are declared as weak) are not needed by anyone and are discarded. The standard library connects implicitly at the very end of the linking process and begins to require these symbols, and the linker does not return back. You can, of course, link the syscalls.c file separately after all the others, but from a purely sports interest, I began to figure out where it comes from.

Trivial implementation of some system calls
 __weak int _open(const char *path, int flags, ...) { return 1; } __weak int _close(int fd) { return 0; } __weak int _fstat(int fd, struct stat *st) { st->st_mode = S_IFCHR; return 0; } __weak int _isatty(int fd) { return 1; } __weak int isatty(int fd) { return 1; } __weak int _lseek(int fd, off_t pos, int whence) { return -1; } __weak unsigned char getch(void) { return 0; } __weak int _read(int fd, char *buf, size_t cnt) { *buf = getch(); return 1; } __weak void putch(unsigned char c) { } 

As I understand it, the arm-gcc compiler is quite powerful and can compile for a good half of existing microcontrollers and processors. But since it can be small microcontrollers with a small amount of memory, and thick “stones” on which Linux can be twisted, then it is necessary to collect, taking into account the capabilities of the platform. Therefore, bundled with the linker there is a set of * .specs files that describe what exactly needs to be linked for this or that platform (this is in addition to the linker scripts).

So by default a standard library is linked to the project, in this case it is newlib-nano . Only if libc on large computers relies on system calls and the kernel, then in the case of newlib-nano, the system calls must be provided by the user. Therefore, the linker requires declaring these _sbrk (), _open () / _ close (), _read () / _ write ().

The problem was solved by adding the key --specs = nosys.specs to the linker settings. This key points the linker to the specs file where linking of the standard library is disabled.

 # Do not link to libc or newlib-nano - we are not using anything from that SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --specs=nosys.specs") 

Problem: initialization code is not linked


At some point, there was a problem with the _init () function. From newlib-nano there is a call to this function, but this code is not declared anywhere in the code. I think the design idea here is as follows.


As I understand it, the _init () function should be substituted according to the chosen platform, for example, its implementation is here: <ARM Toolchain> \ lib \ gcc \ arm-none-eabi \ 5.4.1 \ armv7-m \ crti.o. Arduino somehow implicitly connects this object, but this file did not pull up with me. Most likely this is because I turned off part of the standard library with the key --specs = nosys.specs. As recommended here, simply added an empty implementation to the libmaple code.

 void __attribute__ ((weak)) _init(void) {} 

On good, this function should not be empty. It should do what premain () does — initialize the board. But the authors of stm32duino decided differently.

In general, everything was linked, but the firmware was still very oily - more than 120kb. It was necessary to understand what was there too much. To do this, you had to carefully study the scripts of the toolchain and disassemble what has already been gathered.

Problem: code sections incorrectly defined


The first thing that caught my eye is the starting address. The starting address was either 0x00000000, or 0x00008000, but not exactly as needed - 0x08002000. After a careful study of the STM32 CMake toolchain, I realized that the necessary parameters are set in the linker script, which also comes with the toolchain . Only this script is not used by default, but is enabled by a separate STM32_SET_TARGET_PROPERTIES () command. The starting address was fixed, and the firmware even lost weight to 100k.

 # Flash offset due to bootloader SET(VECT_TAB_ADDR "0x08002000") SET(STM32_FLASH_ORIGIN "0x08002000") ADD_EXECUTABLE(GPSLogger ${SOURCE_FILES}) STM32_SET_TARGET_PROPERTIES(GPSLogger) 

Now in the code there was no interrupt vector table at the beginning of the firmware. Judging by the description of the sections in the file, the table is located in a separate section .isr_vector, only the size of it for some reason is zero.

I spent an hour trying to figure out the linker scripts. But there is some very strong low-level witchcraft: some sections are defined, some kind of code reference is made. In particular, as I understand it, the section with the interrupt vector table should be described in a certain way somewhere in the code (most likely in CMSIS , which I don’t have). Those. My mistake was that I tried to use the generic linker script with the initialization code from libmaple, and their code and data sections are named and arranged differently.

The solution was to explicitly specify the libmaple link script ( STM32duino / variants / generic_stm32f103c / ld / bootloader_20.ld )

 # Using Maple's linker script that corresponds maple/stm32duino code SET(STM32_LINKER_SCRIPT ${GPSLOGGER_LIBS_DIR}/STM32duino/variants/generic_stm32f103c/ld/bootloader_20.ld) LINK_DIRECTORIES(${GPSLOGGER_LIBS_DIR}/STM32duino/variants/generic_stm32f103c/ld) 

Problem: incorrect optimization settings


But here's a bummer. The linker said that he could not fit the generated code into my flash drive. Apart from the fact that a bunch of unnecessary nasty things get into the firmware, it turned out that CMake collects in the default configuration by default. And in the debug configuration, -O2 optimization (speed optimization), while in release -Os (optimization by size). I switched to the release configuration, but it still didn’t work out with it: the toolchain exposes the -flto (Link Time Optimization) flag, with which half of stm32duino is not built.

In general, I took all the best from two configurations. From the release configuration, I took the -Os key. From the debug, I took -g, which add debug information to the binary. This information still does not go to the firmware, and it is much more convenient to disassemble.

 # TODO: It would be nice to use -flto for all of these 3 settings (except for asm one) SET(CMAKE_C_FLAGS_RELEASE "-Os -g" CACHE INTERNAL "c compiler flags release") SET(CMAKE_CXX_FLAGS_RELEASE "-Os -g" CACHE INTERNAL "cxx compiler flags release") SET(CMAKE_ASM_FLAGS_RELEASE "-g" CACHE INTERNAL "asm compiler flags release") SET(CMAKE_EXE_LINKER_FLAGS_RELEASE "" CACHE INTERNAL "linker flags release") 

A further reduction in the size of the firmware lay in the selection of the correct keys for compilation and code generation, as well as the repetition of all the necessary defines that the original build system set. I once again compared the entire list of keys and decided to add a couple to my project:


Here are my keys, which I, in the end, pass to the compiler:

 SET(CMAKE_C_FLAGS "-mthumb -fno-builtin -mcpu=cortex-m3 -Wall -std=gnu99 -ffunction-sections -fdata-sections -fomit-frame-pointer -mabi=aapcs -fno-unroll-loops -ffast-math -ftree-vectorize -nostdlib -march=armv7-m --param max-inline-insns-single=500" CACHE INTERNAL "c compiler flags") SET(CMAKE_CXX_FLAGS "-mthumb -fno-builtin -mcpu=cortex-m3 -Wall -std=c++11 -ffunction-sections -fdata-sections -fomit-frame-pointer -mabi=aapcs -fno-unroll-loops -ffast-math -ftree-vectorize -fno-rtti -fno-exceptions -nostdlib -fno-use-cxa-atexit -march=armv7-m --param max-inline-insns-single=500" CACHE INTERNAL "cxx compiler flags") SET(CMAKE_ASM_FLAGS "-mthumb -mcpu=cortex-m3 -x assembler-with-cpp" CACHE INTERNAL "asm compiler flags") SET(CMAKE_EXE_LINKER_FLAGS "-Wl,--gc-sections -mthumb -mcpu=cortex-m3 -march=armv7-m -mabi=aapcs -Wl,--warn-common -Wl,--warn-section-align" CACHE INTERNAL "executable linker flags") 

Victory over the assembly system


The result - 48 kilobytes for the firmware. In the Arduino version was 56kb. The difference is due to the absence of malloc / free and related functions that I still did not use. A complete reassembly takes 17s per minute against the Arduino. Incremental build only 1-2 seconds.

The moment of truth and try to fill what happened in the chip. To be honest, I was also surprised that after such a long dance in a company with a tambourine, Google and CMake, the firmware started up right away. I was expecting another week of hard debugging in the black box mode in an attempt to understand why this piece of iron doesn't even blink with a light bulb.

For beauty, I added a call to STM32_PRINT_SIZE_OF_TARGETS () - now after assembling, memory statistics are written to the console. Immediately you can see if the memory consumption skipped.

image

He also added a target for disassembling the firmware (at the moment he is already logged into the main branch of stm32-cmake ). It is very convenient to build and immediately check what the compiler has done there, that the firmware has dramatically grown stout.

 FUNCTION(STM32_ADD_DUMP_TARGET TARGET) IF(EXECUTABLE_OUTPUT_PATH) SET(FILENAME "${EXECUTABLE_OUTPUT_PATH}/${TARGET}") ELSE() SET(FILENAME "${TARGET}") ENDIF() ADD_CUSTOM_TARGET(${TARGET}.dump DEPENDS ${TARGET} COMMAND ${CMAKE_OBJDUMP} -x -D -S -s ${FILENAME} | ${CMAKE_CPPFILT} > ${FILENAME}.dump) ENDFUNCTION() 

QtCreator


I did all of the above build process just in the console. Now it's time to connect the IDE. I already mentioned that I would like to get acquainted with Eclipse, but when I installed it, it seemed to me terribly monstrous. But most importantly, I didn’t understand the concept of workspaces. In my understanding, if I open a project, I immediately see all the files from this project. In Eclipse, I had to do a lot of unnecessary gestures. In short, he just did not go, but I plan to definitely return to it another time.

I remembered that I once programmed in Qt Creator and that seemed to be nice. Having installed Qt Creator 4.2.2, I began to google how to connect a CMake project. According to the instructions on the Internet, it was suggested to simply open the CMakeLists.txt file and follow the instructions in the wizard. The first thing he suggested was putting down the toolkit (kits). It is quite reasonable, given the terrible custom build.

image

Qt Creator swears at this kit, they say the compiler is not set. But if you set it up, it still swears - the compiler that CMake itself exposes (arm-gcc) does not match the one selected in the corresponding field (even if it is the same). However, all the build it is normal.

When setting up a project, one very nontrivial moment arose. When importing a CMake project, only CMakeLists.txt was imported. No source code was added to the project. I studied the Internet a few evenings - to no avail. Everything works for everyone, but I don’t. I thought the problem was in a custom toolchain, but the simplest hello world under MinGW32 was not imported either.

The solution was suggested by QtCreator itself. Vorning in the tooltip on the kit I created said that I chose the wrong type of CMake generator. You need to choose Code Blocks - without this, Qt Creator cannot parse projects. After installing the correct generator all the files and subdirectories appeared in the project window.

image

Now you can conveniently navigate through the files, but autocomplete has not worked yet. In fact, strange as it may sound, but Qt Creator simply ignored the passes specified in CMakeLists.txt. Those.the build goes fine, and in the editor most # include'ov highlights as an error (No such file). Work only from the same directory. I edited all the settings, googled for several hours, but did not find the answer.

UPDATE : in the comments suggested that autocomplete works through the compiler. Therefore, it is important to set the correct compiler in the kit settings. Qt Creator, however, still swears, but autocomplete works.

One of the most convenient pieces that make a text editor a full-fledged IDE - automatic filling and launching firmware. I did it like this. The main build calls the target GPSLogger.bin, not the classic all. This target makes from an .elf file .bin, which can already be inserted into the controller. Deployment procedure starts the “flooded” firmware.

image

Note the ping trick trick. Its task is to provide a 1 second pause between the fill and the launch. Otherwise, the microcontroller does not have time to reboot and initialize USB.

QtCreator Oddities
, QtCreator. powershell, timeout .

As a “starter” (Run) it turned out to be convenient to use a terminal emulator. This piece allows you to catch the debug output in serial. Those.firmware after downloading, in fact, runs itself. We only choose whether we want to watch what is written in the series or not.

But about debugging today will not. First of all, it has been written about this many times, I don’t think that I will invent something new. Secondly, I do not use debuggers - in this project I do not use heaped peripherals, which require in-circuit debugging. I only use a pair of UARTs and I2Cs. So you can debug prints in the log. Thirdly, I haven’t written code for a month - I’ve been messing around with build systems and IDE. I would not want to spend another week or two to dive into the debuggers. Who needs - google. There are many articles about screwing debuggers to different IDEs.

Conclusion


I did not set myself a goal to write an assembly tutorial. Rather, it describes my pain and suffering in trying to find a good build system for my project. In this article, I told my experience of switching to CMake to build my project on the STM32 controller.

I use the stm32duino framework, which is based on libmaple. This is not a very standard framework, and therefore I had a number of difficulties and non-trivial moments. In the course of the work, I figured out some of the subtleties of the linker: how the board is initialized, the libc structure and what it depends on, who distributes the memory in the code sections and how. Usually these things are hidden in the depths of the CRT, compiler and IDE, but in this case I had to explicitly pick these nuances.

It is worth noting that most of the shamanism described in this article refers to an assembly under STM32. In the classic Arduino and ATMega assembly, everything is simpler. But many ideas remain the same regardless of the platform. I think a thoughtful reader will also find a lot of useful information in my article.

Probably, some things I did not fully disclose or erroneous judgments. Well, I do not exclude the possibility of errors. In any case, I am open to constructive criticism.

Source: https://habr.com/ru/post/328010/


All Articles