├── .gitattributes ├── .gitignore ├── bluepill.jpeg ├── st-link.jpeg ├── .gitmodules ├── openocd.cfg ├── STM32F103RBTx_FLASH.ld ├── src ├── main.c └── main.c~ ├── toolchain.cmake ├── CMakeLists.txt ├── startup_stm32f103xb.c ├── README.org └── index.html /.gitattributes: -------------------------------------------------------------------------------- 1 | *.html linguist-documentation -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # temp emacs files 2 | *~ 3 | *# 4 | -------------------------------------------------------------------------------- /bluepill.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kxygk/bluepill/HEAD/bluepill.jpeg -------------------------------------------------------------------------------- /st-link.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kxygk/bluepill/HEAD/st-link.jpeg -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "stm32f1-ll"] 2 | path = stm32f1-ll 3 | url = https://github.com/geokon-gh/stm32f1-ll.git 4 | -------------------------------------------------------------------------------- /openocd.cfg: -------------------------------------------------------------------------------- 1 | 2 | source [find interface/stlink-v2.cfg] 3 | transport select hla_swd 4 | source [find target/stm32f1x_stlink.cfg] 5 | program blinky.elf verify reset exit 6 | -------------------------------------------------------------------------------- /STM32F103RBTx_FLASH.ld: -------------------------------------------------------------------------------- 1 | 2 | MEMORY 3 | { 4 | rom (rx) : ORIGIN = 0x08000000, LENGTH = 64K 5 | ram (rwx) : ORIGIN = 0x20000000, LENGTH = 20K 6 | } 7 | PROVIDE(_stack = ORIGIN(ram) + LENGTH(ram)); 8 | 9 | EXTERN(vector_table); 10 | ENTRY(reset_handler); 11 | 12 | SECTIONS 13 | { 14 | .text : { 15 | *(.vectors) 16 | *(.text*) 17 | . = ALIGN(4); 18 | } >rom 19 | } 20 | -------------------------------------------------------------------------------- /src/main.c: -------------------------------------------------------------------------------- 1 | 2 | #include "stm32f1xx_ll_bus.h" 3 | #include "stm32f1xx_ll_rcc.h" 4 | #include "stm32f1xx_ll_gpio.h" 5 | 6 | int main(void) 7 | { 8 | 9 | LL_APB2_GRP1_EnableClock(LL_APB2_GRP1_PERIPH_GPIOC); 10 | 11 | LL_GPIO_SetPinMode(GPIOC, LL_GPIO_PIN_13, LL_GPIO_MODE_OUTPUT); 12 | LL_GPIO_SetPinSpeed(GPIOC, LL_GPIO_PIN_13, LL_GPIO_SPEED_FREQ_HIGH); 13 | 14 | while (1) 15 | { 16 | LL_GPIO_TogglePin(GPIOC, LL_GPIO_PIN_13); 17 | int i = 200000;/* About 1/4 second delay */ 18 | while (i-- > 0) { 19 | asm("nop");/* This stops it optimising code out */ 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /toolchain.cmake: -------------------------------------------------------------------------------- 1 | 2 | set(CMAKE_SYSTEM_NAME Generic) # 'Generic' is used for embedded systems 3 | 4 | set(CMAKE_C_COMPILER arm-none-eabi-gcc) 5 | set(CMAKE_CXX_COMPILER arm-none-eabi-g++) 6 | set(CMAKE_ASM_COMPILER arm-none-eabi-gcc) 7 | 8 | # tells CMake not to try to link executables during its interal checks 9 | # things are not going to link properly without a linker script 10 | set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) 11 | 12 | set(CMAKE_OBJCOPY arm-none-eabi-objcopy) 13 | set(CMAKE_OBJDUMP arm-none-eabi-objdump) 14 | set(CMAKE_SIZE arm-none-eabi-size) 15 | set(CMAKE_DEBUGGER arm-none-eabi-gdb) 16 | set(CMAKE_DEBUGGER arm-none-eabi-gdb) 17 | set(CMAKE_CPPFILT arm-none-eabi-c++filt) 18 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | 2 | cmake_minimum_required(VERSION 3.0) 3 | project(blinky) 4 | 5 | set(ELF ${PROJECT_NAME}.elf) 6 | 7 | enable_language(ASM) 8 | set(STARTUP_FILE "startup_stm32f103xb.c") 9 | 10 | add_subdirectory(stm32f1-ll) 11 | 12 | add_executable(${ELF} ${STARTUP_FILE} 13 | src/main.c) 14 | target_include_directories(${ELF} PUBLIC inc) 15 | 16 | target_link_libraries(${ELF} ll ) 17 | 18 | target_include_directories(${ELF} PUBLIC inc) 19 | 20 | set_target_properties( 21 | ${ELF} 22 | PROPERTIES 23 | LINK_FLAGS 24 | "-T${PROJECT_SOURCE_DIR}/STM32F103RBTx_FLASH.ld \ 25 | -mthumb -mcpu=cortex-m3 \ 26 | -Wl,--gc-sections \ 27 | -Wl,-Map=${PROJECT_NAME}.map") 28 | 29 | target_compile_options(${ELF} PUBLIC 30 | -Wall 31 | -g 32 | -std=gnu99 33 | -Os 34 | -mthumb 35 | -mcpu=cortex-m3 36 | -mfloat-abi=soft 37 | -mlittle-endian 38 | -ffunction-sections 39 | -fdata-sections 40 | -Werror 41 | -Wstrict-prototypes 42 | -Warray-bounds 43 | -fno-strict-aliasing 44 | -Wno-unused-const-variable 45 | -specs=nano.specs 46 | -specs=nosys.specs) 47 | 48 | file(COPY 49 | openocd.cfg 50 | DESTINATION 51 | ${CMAKE_BINARY_DIR}) 52 | -------------------------------------------------------------------------------- /src/main.c~: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | 4 | #include "stm32f1xx.h" 5 | 6 | #include "stm32f1xx_ll_gpio.h" 7 | #include "stm32f1xx_ll_utils.h" 8 | #include "stm32f1xx_ll_rcc.h" 9 | //#include "stm32f1xx_conf.h" 10 | 11 | void SystemClock_Config(void){ 12 | 13 | /* Clock init stuff */ 14 | 15 | LL_UTILS_PLLInitTypeDef sUTILS_PLLInitStruct = {LL_RCC_PLL_MUL_3, LL_RCC_PLL_DIV_3}; 16 | LL_UTILS_ClkInitTypeDef sUTILS_ClkInitStruct = {LL_RCC_SYSCLK_DIV_1, LL_RCC_APB1_DIV_1, LL_RCC_APB2_DIV_1}; 17 | 18 | LL_PLL_ConfigSystemClock_HSI(&sUTILS_PLLInitStruct, &sUTILS_ClkInitStruct); 19 | 20 | LL_Init1msTick(SystemCoreClock); 21 | } 22 | 23 | int main(void){ 24 | 25 | /* Configure the system clock */ 26 | SystemClock_Config(); 27 | 28 | /* Let's pick a pin and toggle it */ 29 | 30 | /* Use a structure for this (usually for bulk init), you can also use LL functions */ 31 | LL_GPIO_InitTypeDef GPIO_InitStruct; 32 | 33 | /* Enable the GPIO clock for GPIOA*/ 34 | LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOA); 35 | 36 | /* Enable clock for SYSCFG */ 37 | LL_APB2_GRP1_EnableClock(LL_APB2_GRP1_PERIPH_SYSCFG); 38 | 39 | /* Set up port A parameters */ 40 | LL_GPIO_StructInit(&GPIO_InitStruct); // init the struct with some sensible defaults 41 | GPIO_InitStruct.Pin = LL_GPIO_PIN_5; // GPIO pin 5; on Nucleo there is an LED 42 | GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_LOW; // output speed 43 | GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT; // set as output 44 | GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL; // make it a push pull 45 | LL_GPIO_Init(GPIOA, &GPIO_InitStruct); // initialize PORT A 46 | 47 | /* Toggle forever */ 48 | while(1){ 49 | LL_mDelay(250); 50 | LL_GPIO_TogglePin(GPIOA, LL_GPIO_PIN_5); 51 | } 52 | 53 | return 0; 54 | } 55 | -------------------------------------------------------------------------------- /startup_stm32f103xb.c: -------------------------------------------------------------------------------- 1 | 2 | int main(void); 3 | void __attribute__ ((weak, naked)) reset_handler(void) { 4 | (*(volatile unsigned int *)(0x40021018)) |= (1 << 4); 5 | 6 | (*(volatile unsigned int *)(0x40011004)) |= (0x00 << (((13 - 8) * 4) + 2)); 7 | (*(volatile unsigned int *)(0x40011004)) |= (0x02 << ((13 - 8) * 4)); 8 | main(); 9 | } 10 | 11 | void blocking_handler(void) { while (1); } 12 | void null_handler(void) {} 13 | extern unsigned _stack; 14 | 15 | __attribute__ ((section(".vectors"))) 16 | struct { 17 | unsigned int *initial_sp_value; 18 | void (*reset)(void); 19 | void (*nmi)(void); 20 | void (*hard_fault)(void); 21 | void (*memory_manage_fault)(void); 22 | void (*bus_fault)(void); 23 | void (*usage_fault)(void); 24 | void (*reserved_x001c[4])(void); 25 | void (*sv_call)(void); 26 | void (*debug_monitor)(void); 27 | void (*reserved_x0034)(void); 28 | void (*pend_sv)(void); 29 | void (*systick)(void); 30 | void (*irq[68])(void); 31 | } vector_table = { 32 | .initial_sp_value = &_stack, 33 | .reset = reset_handler, 34 | .nmi = null_handler, 35 | .hard_fault = blocking_handler, 36 | 37 | .sv_call = null_handler, 38 | .pend_sv = null_handler, 39 | .systick = null_handler, 40 | .irq = { 41 | null_handler, 42 | null_handler, 43 | null_handler, 44 | null_handler, 45 | null_handler, 46 | null_handler, 47 | null_handler, 48 | null_handler, 49 | null_handler, 50 | null_handler, 51 | null_handler, 52 | null_handler, 53 | null_handler, 54 | null_handler, 55 | null_handler, 56 | null_handler, 57 | null_handler, 58 | null_handler, 59 | null_handler, 60 | null_handler, 61 | null_handler, 62 | null_handler, 63 | null_handler, 64 | null_handler, 65 | null_handler, 66 | null_handler, 67 | null_handler, 68 | null_handler, 69 | null_handler, 70 | null_handler, 71 | null_handler, 72 | null_handler, 73 | null_handler, 74 | null_handler, 75 | null_handler, 76 | null_handler, 77 | null_handler, 78 | null_handler, 79 | null_handler, 80 | null_handler, 81 | null_handler, 82 | null_handler, 83 | null_handler, 84 | null_handler, 85 | null_handler, 86 | null_handler, 87 | null_handler, 88 | null_handler, 89 | null_handler, 90 | null_handler, 91 | null_handler, 92 | null_handler, 93 | null_handler, 94 | null_handler, 95 | null_handler, 96 | null_handler, 97 | null_handler, 98 | null_handler, 99 | null_handler, 100 | null_handler, 101 | null_handler, 102 | null_handler, 103 | null_handler, 104 | null_handler, 105 | null_handler, 106 | null_handler, 107 | null_handler, 108 | null_handler, 109 | } 110 | }; 111 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | #+TITLE: Bluepill: a GCC/CMake build environment .. 2 | #+DESCRIPTION: A GCC/OpenOCD/CMake based setup for building code for the bluepill STM32F1 boards 3 | 4 | #+EXPORT_FILE_NAME: index.html 5 | #+HTML_DOCTYPE: html5 6 | #+HTML_LINK_UP: .. 7 | #+HTML_LINK_HOME: .. 8 | #+HTML_HEAD: 9 | #+HTML_HEAD_EXTRA: 10 | #+HTML_MATHJAX: path: "../MathJax/MathJax.js?config=TeX-AMS_CHTML" 11 | #+OPTIONS: html-style:nil 12 | #+OPTIONS: num:nil 13 | #+OPTIONS: html-scripts:nil 14 | 15 | * Intro 16 | #+BEGIN_QUOTE 17 | *Note:* Since I've written this there have been several efforts to build a complete CMake system. The most complete and impressive one I've seen is https://github.com/ObKo/stm32-cmake 18 | 19 | While this page will still have good info and is educational (esp concerning OpenOCD/GDB) - if you want to do a serious project please take a look at ObKo's template/framework. I've only used it a bit - but it seems very polished and is similar in principle to what I've done. It supports pretty much all STM32 chips so it's not quite as simple as what I've done here 20 | #+END_QUOTE 21 | 22 | In this guide we will setup a simple minimal blinky for the Bluepill - a very cheap board available from China with the =STM32F103C8T6= chip on it. They cost a smidge over one dollar each. 23 | 24 | I will be building a blinky using STM32's Low Level API (explained below). I noticed everyone seems to still be using Make so I've tried to change things up and write up a whole toolchain for this board in CMake so hopefully this guide will provide something new and interesting for some people. At the end I will show how CMake allows us to plug in our project into basically any modern IDE - and we'll get the whole thing running just using GCC and OpenOCD. I have virtually no experience with embedded development, so if there are mistakes or you just want to tell me I'm an idiot, please leave an issue on [[https://github.com/geokon-gh/bluepill][the github repo]]. Note that a big part of the guts of this project are in the submodule which has [[https://github.com/geokon-gh/stm32f1-ll/][its own repo]] 25 | 26 | [[file:bluepill.jpeg]] 27 | 28 | This file and webpage is an org-document and the code within is automatically tangled into the files in the project. I try to note any files I've manually copied from elsewhere 29 | 30 | ** A Quick start 31 | 32 | If you just want to try it out you just need to install ~cmake~ ~gcc-arm-none-eabi~ and ~openocd~. You will also need a bluepill and a *ST-Link V2* device. Attach those to your computer and run: 33 | #+BEGIN_SRC 34 | cd your/source/directory 35 | git clone --recurse-submodules https://github.com/geokon-gh/bluepill.git 36 | cd your/build/directory 37 | cmake -DCMAKE_TOOLCHAIN_FILE=your/source/directory/bluepill/toolchain.cmake your/source/directory/bluepill/ 38 | make 39 | opeocd 40 | #+END_SRC 41 | 42 | ** Prior Art 43 | Some (but not all) of the projects from which I took ideas, inspiration and suggestions: 44 | 45 | - =dwelch67= has some great extremely minimal setups for the blue pilll on [[https://github.com/dwelch67/stm32_samples/tree/master/STM32F103C8T6][his github]]. He describes them extensively in a [[https://electronics.stackexchange.com/questions/30736/stm32f2-makefile-linker-script-and-start-up-file-combination-without-commercia][few places on Stack Overflow]]. Basically here he rolls his own .. everything. It's very greybeard. All the code is in your face - no libraries. Straight writting to registers. Definitely take a look! 46 | 47 | - =PurpleAlien= has another setup - which is very close to what I'm doing. He described it [[https://www.purplealienplanet.com/node/69][on his website]] and the code is on the [[https://github.com/PurpleAlien/stm32-minimal][his github]]. It's a slightly different chip - but the steps should work almost identically for the =bluepill=. 48 | 49 | - =satoshinm= has [[https://satoshinm.github.io/blog/171212_stm32_blue_pill_arm_development_board_first_look_bare_metal_programming.html][a wonderful guide]] where he guides you through all the challenges he had setting up his bluepill. It's long but very informative and I recommend reading it. He also has an accompanying repository where we has code for the blinky working using bare metal, the STM HAL and another 3rd party library. His code (the bare metal part especially) was absolutely invaluable for getting my setup up and running. Check out [[https://github.com/satoshinm/pill_blink][his github]] project. 50 | 51 | * Anatomy of a build 52 | Unfortunately getting started and just getting a light blinking involved quite a lot of steps since we have no operating system to handle all the background stuff. I am writing all the piece out here in detail - but please do consider reusing some files from the templates provided by STM. Specifically look in the Cube package in =STM32Cube_FW_F1_V1.6.0/Projects/STM32F103RB-Nucleo/Examples_LL/GPIO/GPIO_InfiniteLedToggling=. Under =SW4STM32= you will find a startup script and a linker script. And in =Src= / =Inc= you will find additional code files. I will try to mention them in passing. 53 | 54 | ** CMake 55 | CMake is meta build automation tool. You define your project targets, how you want things linked and built and then it will generate build tools for you. Typically we just generate Make files but it can also coordinate system installation, generate installer wizards for Windows, generate Visual Studio project files and more. CMake has in effect standardized builds into a common set of easy to use terms - so while it's not as flexible as Make and you have to follow a certain layout/pattern, in the end everything ends up cleaner and more reusable and it's hard to shoot yourself in the foot. For us it will be our central tool for coordinating the build and we will define our whole project through a ~CMakeLists.txt~ file in our project root. We will see later that actually CMake will give us crucial project meta-information that Clang-tools and IDEs like CLion and KDevelop can hook into it to provide code highlighting/completion and other goodies. All of this is a serious quality-of-life improvement over vanilla Make. Because CMake has become the defacto standard in the C++ world the ecosystem of tools around it just keeps growing year to year. 56 | 57 | Starting our =CMakeLists.txt= we write out the usual CMake version and project name 58 | 59 | #+BEGIN_SRC cmake :tangle CMakeLists.txt 60 | cmake_minimum_required(VERSION 3.0) 61 | project(blinky) 62 | #+END_SRC 63 | Our end goal is to create an ELF file that we will flash onto the chip. I suggest adding the =.elf= extension explicitly to your target name. To keep the template generic enough I just name the elf the same as the project name. So if you fork this project and do your own thing - you can just change the project name and you'll be good to go 64 | #+BEGIN_SRC cmake :tangle CMakeLists.txt 65 | set(ELF ${PROJECT_NAME}.elf) 66 | #+END_SRC 67 | 68 | ** The Linker Script 69 | When an application normally runs on a desktop machine it's generally running using virtual memory in a virtual application-specific address space. From the applications point of view it can manipulate its own memory however it wants - and it's the operating system that then translates that into safe operations on the actual memory (for instance to insure that the applications don't touch any memory region they shouldn't) 70 | 71 | On a microcontroller by default there is no operating system to manage the memory and the memory is shared with other functionality - some addresses are reserved for peripherals, other addresses are for interrupts and reset bits, the stack and heap are allocated in some device-specific place and there is also a split between ROM and RAM. 72 | 73 | Because of these new limitations we can't just start executing code at address zero or drop in a ~main()~ function somewhere randomly and start there, we need to tell the linker what the code layout is though a custom *linker script*. First we tell it which parts correspond to ROM and RAM and what will be their respective sizes. ROM (Read Only Memory) is where the code and constants live, and RAM (Random Access Memory) is where the stack and heap live - the stuff that's dynamic. 74 | 75 | #+BEGIN_SRC c :tangle STM32F103RBTx_FLASH.ld 76 | MEMORY 77 | { 78 | rom (rx) : ORIGIN = 0x08000000, LENGTH = 64K 79 | ram (rwx) : ORIGIN = 0x20000000, LENGTH = 20K 80 | } 81 | PROVIDE(_stack = ORIGIN(ram) + LENGTH(ram)); 82 | #+END_SRC 83 | 84 | Even give that, code on a microcontroller doesn't by default start at the first address of ROM and go from there. On a desktop program you generally have an entry point (a ~main()~) and an exit point (ex: ~exit 0~). But a better way to think about the way a microcontroller works is it's as a machine that recieves interrupt signals from external inputs and the chip responds by running code and then returns to whatever it was doing before. These interrupting inputs can be a clock running out, a peripheral wanting attention, an attached debugger wanting to pause everything, etc. There is no real starting point nor an "exit" 85 | 86 | So instead of a ~main()~ or something, the first thing in ROM is a /vector table/ - a table of pointers to the different *interrupt handlers*. These handlers are the code that is run when each interrupt happens. The first interrupt handler in this table will be special and it's the one that is triggered when the system is powered on, the user presses the reset button, or the code runs out of things to do. It's appropriately called the *reset handeler* 87 | 88 | #+BEGIN_SRC c :tangle STM32F103RBTx_FLASH.ld 89 | EXTERN(vector_table); 90 | ENTRY(reset_handler); 91 | #+END_SRC 92 | These two are just symbols for the linker - and it will look for them later in the actual code it's linking. So we need to not forget to define them :) 93 | 94 | Lastly we need to tell the linker that we want those vectors first in the ROM and aligned at the byte level (b/c the micro reads things in byte sized chucks) 95 | #+BEGIN_SRC c :tangle STM32F103RBTx_FLASH.ld 96 | SECTIONS 97 | { 98 | .text : { 99 | *(.vectors) 100 | *(.text*) 101 | . = ALIGN(4); 102 | } >rom 103 | } 104 | 105 | #+END_SRC 106 | So the chip doesn't need to hunt for the vector table. It's always in the same spot at the start of the ROM - and the reset handler is as well. 107 | 108 | ** The Startup File (WIP) 109 | 110 | As I mentioned, the reset handler and vector table are just symbols to the linker at this point - so the next step is to write the code for them. The vector table will be set to some default values (and can be changed later) and we'll write the reset handler in a generic way that we can reuse between projects. It will do some initializations and then at the end call to ~main()~. Then when we start a new project we can copy over this file and simply start writing a ~main()~ skipping all that initialization. In so doing we've split off these the generic reusable stuff into a separate *startup file*. 111 | 112 | #+BEGIN_QUOTE 113 | *Note:* There is nothing special about this file for the compiler or linker - it's just another code file - and you are free to copy it over to your other code files if you want 114 | #+END_QUOTE 115 | 116 | The code from top to bottom: 117 | - We define a reset handler 118 | + We declare a =main()= (to be defined in our =main.c= later) 119 | + We initialize some stuff 120 | + We call main() 121 | - We define two dummy interrupt handlers. 122 | + One that spins for ever 123 | + One that does nothing and returns 124 | - We define our vector table in detail 125 | + The first element is the SP (Stack Pointer) 126 | + The second element is always the reset handler 127 | + After that are the remaining handlers which are being set to basically do nothing 128 | #+BEGIN_SRC c :tangle startup_stm32f103xb.c 129 | int main(void); 130 | void __attribute__ ((weak, naked)) reset_handler(void) { 131 | (*(volatile unsigned int *)(0x40021018)) |= (1 << 4); 132 | 133 | (*(volatile unsigned int *)(0x40011004)) |= (0x00 << (((13 - 8) * 4) + 2)); 134 | (*(volatile unsigned int *)(0x40011004)) |= (0x02 << ((13 - 8) * 4)); 135 | main(); 136 | } 137 | 138 | void blocking_handler(void) { while (1); } 139 | void null_handler(void) {} 140 | extern unsigned _stack; 141 | 142 | __attribute__ ((section(".vectors"))) 143 | struct { 144 | unsigned int *initial_sp_value; 145 | void (*reset)(void); 146 | void (*nmi)(void); 147 | void (*hard_fault)(void); 148 | void (*memory_manage_fault)(void); 149 | void (*bus_fault)(void); 150 | void (*usage_fault)(void); 151 | void (*reserved_x001c[4])(void); 152 | void (*sv_call)(void); 153 | void (*debug_monitor)(void); 154 | void (*reserved_x0034)(void); 155 | void (*pend_sv)(void); 156 | void (*systick)(void); 157 | void (*irq[68])(void); 158 | } vector_table = { 159 | .initial_sp_value = &_stack, 160 | .reset = reset_handler, 161 | .nmi = null_handler, 162 | .hard_fault = blocking_handler, 163 | 164 | .sv_call = null_handler, 165 | .pend_sv = null_handler, 166 | .systick = null_handler, 167 | .irq = { 168 | null_handler, 169 | null_handler, 170 | null_handler, 171 | null_handler, 172 | null_handler, 173 | null_handler, 174 | null_handler, 175 | null_handler, 176 | null_handler, 177 | null_handler, 178 | null_handler, 179 | null_handler, 180 | null_handler, 181 | null_handler, 182 | null_handler, 183 | null_handler, 184 | null_handler, 185 | null_handler, 186 | null_handler, 187 | null_handler, 188 | null_handler, 189 | null_handler, 190 | null_handler, 191 | null_handler, 192 | null_handler, 193 | null_handler, 194 | null_handler, 195 | null_handler, 196 | null_handler, 197 | null_handler, 198 | null_handler, 199 | null_handler, 200 | null_handler, 201 | null_handler, 202 | null_handler, 203 | null_handler, 204 | null_handler, 205 | null_handler, 206 | null_handler, 207 | null_handler, 208 | null_handler, 209 | null_handler, 210 | null_handler, 211 | null_handler, 212 | null_handler, 213 | null_handler, 214 | null_handler, 215 | null_handler, 216 | null_handler, 217 | null_handler, 218 | null_handler, 219 | null_handler, 220 | null_handler, 221 | null_handler, 222 | null_handler, 223 | null_handler, 224 | null_handler, 225 | null_handler, 226 | null_handler, 227 | null_handler, 228 | null_handler, 229 | null_handler, 230 | null_handler, 231 | null_handler, 232 | null_handler, 233 | null_handler, 234 | null_handler, 235 | null_handler, 236 | } 237 | }; 238 | #+END_SRC 239 | 240 | Now if you open up some startup files in the templates provided by STM you will see that they're all written in assembly and they do a little more than we are doing here. If you squint and look at the assembly you will see that the code is doing basically the same thing + some extra magic - but on a high level it's also defining a reset handler which then calls a main. The remaining interrupt handlers are actually defined in C in =stm32f1xx_it.h/c= which is next to the source files and headers. 241 | 242 | Once we have the file we can add it to CMake 243 | #+BEGIN_SRC cmake :tangle CMakeLists.txt 244 | enable_language(ASM) 245 | set(STARTUP_FILE "startup_stm32f103xb.c") 246 | #+END_SRC 247 | ** The STM Libraries 248 | 249 | Next we need actual libraries to write code with - otherwise we are kinda stuck poking at memory addresses with the datasheet. These are all provided in one bundle called *Cube* and it's on [[https://www.st.com/content/st_com/en/products/embedded-software/mcus-embedded-software/stm32-embedded-software/stm32cube-mcu-packages/stm32cubef1.html][the STM website]]. Here is a quick digest of what you get: 250 | 251 | - The *BSP* has board specific peripheral libraries.. since we aren't using a board from STM - this really doesn't concern us. 252 | 253 | - The *HAL* that comes from STM is the standard *Hardware Abstraction Layer*. It will be making some simplifying assumptions and do some stuff more automatically for you. I'm going to skip setting this up. Blinking a light should be pretty simple - so I'm shooting to get it working with simpler APIs 254 | 255 | - Hidden inside of the *HAL* folder you will see files that are names =stm32f1xx_ll_*.c/h=. These actually form a seperate sub-library of sorts called the *LL* API (for *Low Level*) 256 | 257 | - The *CMSIS* ( Cortex Microcontroller Software Interface Standard ) : This library comes from ARM (/not STM/). It's split into several semi-independent components and provides a common base for all ARM devices (independent of vendor). The *HAL* and *LL API* are built on top of the *CMSIS* 258 | 259 | Both the *HAL* and *CMSIS* need some chip-specific configuration - b/c while the API is standard, under the hood things will change from chip to chip (like memory addresses of things or clock information). I've bundled the *LL API* and the *CMSIS* together in a separate project [[https://geokon-gh.github.io/stm32f1-ll/index.html][stm32f1-ll]] ([[https://github.com/geokon-gh/stm32f1-ll/][github]]). It's also building with CMake so we can use it directly in our project (and you can skip registration and downloading the *Cube* thing). I recommend checking out that project's webpage for more details on how it works - but there is very little magic going on. 260 | 261 | The library bundle has been added as a submodule to this project, but if you forgot to clone recursively you can clone it right now into the project root with ~git clone https://github.com/geokon-gh/stm32f1-ll/~. Once we have it there we can just add it in 262 | 263 | #+BEGIN_SRC cmake :tangle CMakeLists.txt 264 | add_subdirectory(stm32f1-ll) 265 | #+END_SRC 266 | 267 | #+BEGIN_QUOTE 268 | *Note*:in ~STM32Cube_FW_F1_V1.6.0/Middlewares~ there are additional libraries that sorta live on top of all of this and do more complicated stuff like TCP/IP USB..stuff and Filesystem things. Basically things that are kinda complicated and that you probably want to avoid writing yourself. I'm completely skipping this :) 269 | #+END_QUOTE 270 | 271 | ** Our blinky code 272 | 273 | Now that we have a linker that can put the code in the right places, and the startup code to initialize things correctly for us we can finally write our blinky code in =/src/main.c= 274 | 275 | The libraries/modules in the LL-API are pretty fragmented/decoupled. So while we want to use the GPIO, we also need a few other libraries to get started. The GPIO peripheral for starters need to be powered on by the microcontroller (all the peripherals are powered off by default). The system responsible for that is called /Reset and Clock Control/, *RCC* for short. There is another module called *BUS* which seems to just have helper functions to turn on/off RCC sections. 276 | 277 | So first we include all three of these modules 278 | #+BEGIN_SRC c :tangle src/main.c 279 | #include "stm32f1xx_ll_bus.h" 280 | #include "stm32f1xx_ll_rcc.h" 281 | #include "stm32f1xx_ll_gpio.h" 282 | #+END_SRC 283 | Then we can start writing our =main()= 284 | #+BEGIN_SRC c :tangle src/main.c 285 | int main(void) 286 | { 287 | #+END_SRC 288 | We use a BUS module macro to enable the GPIO peripheral. The LED we want to blink on the board is labeled *PC13*. PC stands for Port C and it's number 13. So we enable GPIO port C. The peripherals controlled by the RCC system are split into two sections APB1 and APB2. GPIO is in the APB2 section. 289 | #+BEGIN_SRC c :tangle src/main.c 290 | LL_APB2_GRP1_EnableClock(LL_APB2_GRP1_PERIPH_GPIOC); 291 | #+END_SRC 292 | We then set this GPIO pin to be an ouput pin and we set it to be a high speed pin 293 | #+BEGIN_SRC c :tangle src/main.c 294 | LL_GPIO_SetPinMode(GPIOC, LL_GPIO_PIN_13, LL_GPIO_MODE_OUTPUT); 295 | LL_GPIO_SetPinSpeed(GPIOC, LL_GPIO_PIN_13, LL_GPIO_SPEED_FREQ_HIGH); 296 | #+END_SRC 297 | Now that we have the GPIO pin setup we just have an infinite loop that toggles the pin and spends some time sitting in a loop doing nothing 298 | #+BEGIN_SRC c :tangle src/main.c 299 | while (1) 300 | { 301 | LL_GPIO_TogglePin(GPIOC, LL_GPIO_PIN_13); 302 | int i = 200000;/* About 1/4 second delay */ 303 | while (i-- > 0) { 304 | asm("nop");/* This stops it optimising code out */ 305 | } 306 | } 307 | } 308 | #+END_SRC 309 | And that's it! 310 | 311 | If you look at the GPIO example that comes with the Cube package you will see it does the waiting/spinning in a smarter way using the clock system. But this requires setting up the clock properly and is a bit more complicated. You will also need the =system_stm32f1xx.h/c= files to configure the clock. For simplicity I've omitted this - but you probably want to add that back in if you're doing more complicated stuff. 312 | 313 | We can now return to CMake and declare our target elf file and link up our =main.c= with the startup file. 314 | #+BEGIN_SRC cmake :tangle CMakeLists.txt 315 | add_executable(${ELF} ${STARTUP_FILE} 316 | src/main.c) 317 | target_include_directories(${ELF} PUBLIC inc) 318 | #+END_SRC 319 | and then we link it to the LL-API library we are using 320 | #+BEGIN_SRC cmake :tangle CMakeLists.txt 321 | target_link_libraries(${ELF} ll ) 322 | #+END_SRC 323 | And I'm also adding in a include directory for future use (it's empty for now) 324 | #+BEGIN_SRC cmake :tangle CMakeLists.txt 325 | target_include_directories(${ELF} PUBLIC inc) 326 | #+END_SRC 327 | ** The toolchain 328 | 329 | Now that we have all the pieces we just need to tell CMake how we want everything compiled. I'm building using =gcc-arm-none-eabi= and its associated tools. I did this on a Debian system where this version of gcc can be installed from the repository (name =gcc-arm-none-eabi=) 330 | 331 | Canonically the compiler is specified in a separate file so that you can subsitute other possible toolchains (like for instance LLVM or custom versions of GCC). I'll just setup GCC as an example and write it out to a =toolchain.cmake= file in the source directory 332 | 333 | #+BEGIN_SRC cmake :tangle toolchain.cmake 334 | set(CMAKE_SYSTEM_NAME Generic) # 'Generic' is used for embedded systems 335 | 336 | set(CMAKE_C_COMPILER arm-none-eabi-gcc) 337 | set(CMAKE_CXX_COMPILER arm-none-eabi-g++) 338 | set(CMAKE_ASM_COMPILER arm-none-eabi-gcc) 339 | 340 | # tells CMake not to try to link executables during its interal checks 341 | # things are not going to link properly without a linker script 342 | set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) 343 | 344 | set(CMAKE_OBJCOPY arm-none-eabi-objcopy) 345 | set(CMAKE_OBJDUMP arm-none-eabi-objdump) 346 | set(CMAKE_SIZE arm-none-eabi-size) 347 | set(CMAKE_DEBUGGER arm-none-eabi-gdb) 348 | set(CMAKE_DEBUGGER arm-none-eabi-gdb) 349 | set(CMAKE_CPPFILT arm-none-eabi-c++filt) 350 | #+END_SRC 351 | If you skip writing a toolchain file then CMake will default to the system compiler and things will start to slowly go wrong for you (kinda unfortunately, it generally doesn't blow up into your face here) 352 | 353 | Next will also need to tell the linker which linker script to use (which is a bit ugly in CMake) 354 | 355 | #+BEGIN_SRC cmake :tangle CMakeLists.txt 356 | set_target_properties( 357 | ${ELF} 358 | PROPERTIES 359 | LINK_FLAGS 360 | "-T${PROJECT_SOURCE_DIR}/STM32F103RBTx_FLASH.ld \ 361 | -mthumb -mcpu=cortex-m3 \ 362 | -Wl,--gc-sections \ 363 | -Wl,-Map=${PROJECT_NAME}.map") 364 | #+END_SRC 365 | 366 | I'm appending this to the =CMakeLists.txt=, but it's something that maybe could be in the toolchain file. However b/c it does need the target name (${ELF}) it's more convenient to just append it to the ~CMakeLists.txt~ (something to maybe revisit in the future) 367 | 368 | You'll also notice I added some more linker options - the first two tell the linker it the chip type and the instruction set and the other two: 369 | 370 | #+BEGIN_QUOTE 371 | *Note:* At first I hadn't given the computer arch/intruction-set flags b/c they're being given to the compiler below and I ended up with a very very subtly big where ~__libc_init_array address~ was trying to jump to some addresses right outside of the ROM. It was very bizarre and took me a whole day to track down. 372 | #+END_QUOTE 373 | 374 | - =--gc-sections= this tells the linker to remove unused code/data from the final executable. There is a pesky ~_exit()~ function referrence that will often get slipped into your executable by the compiler. B/c we are running on a microcontroller the code never really exits (it can't quit and hand off executation to an OS after all!) so this exit needs to be removed by the linker. If you leave this off then the linker will get confused and start complaining you never defined an exit function. 375 | 376 | - =-Map= prints a link map: 377 | + Where object files and symbols are mapped into memory. 378 | + How common symbols are allocated. 379 | + All archive members included in the link, with a mention of the symbol which caused the archive member to be brought in. 380 | 381 | The link map is a high-level overview of how your code is placed in memory 382 | 383 | More linker options are explained in details here: https://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_node/ld_3.html 384 | 385 | We then also need to let the compiler know our target architecture and some compiler options (taken from [[https://github.com/PurpleAlien/stm32-minimal/blob/master/Makefile][PurpleAlien]]) 386 | 387 | #+BEGIN_SRC cmake :tangle CMakeLists.txt 388 | target_compile_options(${ELF} PUBLIC 389 | -Wall 390 | -g 391 | -std=gnu99 392 | -Os 393 | -mthumb 394 | -mcpu=cortex-m3 395 | -mfloat-abi=soft 396 | -mlittle-endian 397 | -ffunction-sections 398 | -fdata-sections 399 | -Werror 400 | -Wstrict-prototypes 401 | -Warray-bounds 402 | -fno-strict-aliasing 403 | -Wno-unused-const-variable 404 | -specs=nano.specs 405 | -specs=nosys.specs) 406 | #+END_SRC 407 | *TODO* Explain all of these... and again.. would be nice to have in the toolchain file 408 | 409 | * Getting the code on the chip 410 | ** Building 411 | 412 | At this point we have all the files we need to build the code, so just go to a new empty directory and run 413 | 414 | #+BEGIN_SRC 415 | cmake -DCMAKE_TOOLCHAIN_FILE=path/to/source/toolchain.cmake /path/to/source/ 416 | make 417 | #+END_SRC 418 | 419 | Now in the build directory you'll have some build garbage, the link map =blinky.map= and =blinky.elf= - which is the code/executable that we want to get onto the bluepill. 420 | 421 | ** OpenOCD 422 | The standard open source software for flashing the bluepill is *OpenOCD*. On educational/more-expensive boards there will be a secondary chip that helps you flash the microcontroller. But on cheaper and more practical chips this part is omitted (b/c in a sense it's a waste to have the same chip on every single board). So to flash the bluepill you will need something to do the flashing with. I'm using a knock off =ST-LINK v2= I purchased on Taobao. (note the wiring is in a different order on the board and programmer) 423 | 424 | [[file:st-link.jpeg]] 425 | 426 | OpenOCD will provide us with an abstraction layer. It will communicate over JTAG or SWD to the chip and we will communicate with OpenOCD and tell it what we need. 427 | 428 | OpenOCD's software setup is rather baroque - but the [[http://openocd.org/documentation/][documentation]] is very thorough. You start from the beginning and just read very carefully sequentially and it will all make sense. Fortunately for us - the hardware we're using is very standard so we can use some already provided templates. When I install OpenOCD on my Debian system through ~apt-get install openocd~ the templates are in =/usr/share/openocd/scripts/board/=. After browsing some similar boards (like the stm32f4disovery) you kinda get the picture of how the configuration file should look (*THESE VALUES CHANGE BETWEEN POINT RELEASE OF OPENOCD. DOUBLE CHECK THEM IF YOU HAVE ANY ISSUES*) 429 | 430 | The configuration is a bit finicky. For OpenOCD to automatically pick it up it needs to be called ~openocd.cfg~ and it needs to be in the directory where you run ~openocd~. 431 | #+BEGIN_SRC c :tangle openocd.cfg 432 | source [find interface/stlink-v2.cfg] 433 | transport select hla_swd 434 | source [find target/stm32f1x_stlink.cfg] 435 | program blinky.elf verify reset exit 436 | #+END_SRC 437 | The finaly file isn't too complicated. It sets the interface type (ie the ST-LINK flashing dongle thing), then it sets the flashing communication protocol for talking to the chip, then the actual chip type and lastly we tell it to program the chip with the ~.elf~ we just made. After flashing it will verify the code, reset the controller and then exit OpenOCD 438 | 439 | For convenience we should also tell CMake to copy this file over to the build directory 440 | #+BEGIN_SRC cmake :tangle CMakeLists.txt 441 | file(COPY 442 | openocd.cfg 443 | DESTINATION 444 | ${CMAKE_BINARY_DIR}) 445 | #+END_SRC 446 | 447 | So now in our build directory we simply run ~openocd~ and your program should magically upload to the chip and start running. The light should be flashing at this point :) 448 | 449 | If it's not, Some things to double check: 450 | - The version of OpenOCD you are running... I had weird issues with manually installed OpenOCDs, but the repo one worked great 451 | - Check the templates - if you have problems, try some of the other options available 452 | - try running OpenOCD as root! Maybe your user doesn't have the right USB permissions or something to that effect 453 | - I had weird connection issues that turned out to be due to faulty wires! Thanks to [[https://reddit.com/comments/9ba9n8/comment/e53aa2m?context=3][NeoMarxismIsEvil]] for catching that :) 454 | - In the next section about GDB I mention an =unlock= command.. I'm not 100% sure it's necessary - but try it if you're having issues 455 | 456 | * Intergrated Development 457 | One of the big bonuses of using CMake is that it will hook into existing tools very easily. 458 | 459 | ** GDB 460 | The first basic step is hookin' up a debugger. 461 | 462 | For some reason Debian (Testing) is missing a ~arm-none-eabi-gdb~, so I had to just download the whole GCC toolchain from [[https://developer.arm.com/open-source/gnu-toolchain/gnu-rm/downloads][ARM's website]] (this only works assuming you're running on a x64 machine). Just whatever you end up using, make sure you don't just run the system GDB! It won't throw you any errors and it will kinda work.. till it doesn't. 463 | 464 | Once we have the right version of *GDB* the next part becomes super easy b/c by default *OpenOCD* will provide us with a GDB server to which we can connect. We just need to disable the part where we flash the program and exit and replace it with a command to reset the chip and wait for GDB 465 | 466 | #+BEGIN_SRC c :tangle openocd_debug.cfg 467 | source [find interface/stlink-v2.cfg] 468 | transport select hla_swd 469 | source [find target/stm32f1x_stlink.cfg] 470 | reset_config srst_nogate 471 | #+END_SRC 472 | 473 | I honestly didn't entirely understand all the reset configuration options so if you're having issues I'd suggest looking at [[http://openocd.org/doc/html/Reset-Configuration.html][the documentation]] and trying several different settings. I've found the current one works for me. Again, we just run ~openocd~ but this time the program kinda hangs and sits and waits for a connection: 474 | 475 | #+BEGIN_QUOTE 476 | $ openocd 477 | Open On-Chip Debugger 0.10.0 478 | Licensed under GNU GPL v2 479 | For bug reports, read 480 | http://openocd.org/doc/doxygen/bugs.html 481 | WARNING: target/stm32f1x_stlink.cfg is deprecated, please switch to target/stm32f1x.cfg 482 | Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD 483 | adapter speed: 1000 kHz 484 | adapter_nsrst_delay: 100 485 | none separate 486 | none separate 487 | Info : Unable to match requested speed 1000 kHz, using 950 kHz 488 | Info : Unable to match requested speed 1000 kHz, using 950 kHz 489 | Info : clock speed 950 kHz 490 | Info : STLINK v2 JTAG v17 API v2 SWIM v4 VID 0x0483 PID 0x3748 491 | Info : using stlink api v2 492 | Info : Target voltage: 2.913562 493 | Info : stm32f1x.cpu: hardware has 6 breakpoints, 4 watchpoints 494 | #+END_QUOTE 495 | 496 | We open another terminal and run our ~arm-none-eabi-gdb~ to bring up the GDB "shell" The next few steps will connect to the OpenOCD server, stop the program running on it, unlock the chip, and load our new program 497 | 498 | #+BEGIN_SRC 499 | > target remote localhost:3333 500 | > monitor reset halt 501 | > monitor stm32f1x unlock 0 502 | > load blinky.elf 503 | #+END_SRC 504 | 505 | Now you can set breakpoint, run code, inspect the stack and variables, etc. etc. Look at the GDB manual for all the juicy details - and don't forget about the very handy [[https://ftp.gnu.org/old-gnu/Manuals/gdb-5.1.1/html_chapter/gdb_19.html][TUI Mode]]. Start it with ~C-x C-a~, then hit ~C-x 2~ to bring up the assembly. And type ~s~ or ~n~ to step one line of code at a time and ~si~ to step one assembly instruction at a time! 506 | 507 | ** KDevelop 508 | 509 | To demonstrate how flexible things get thanks to CMake, next I'll show you how to setup KDevelop to run everything for us. In principle this should work equally well with QtCreator or CLion or CQuery/Emacs. You can even hook up linters and other fancy Clang based tools now pretty easily. So this isn't an endorsement of KDevelop over the alternatives b/c after all it's sorta like Visual Studio - a big drop-box driven mess - but I'm just familiar with it and it's quick and easy to get up and running with a CMake project. We'll be able to jump around our code and refactor things in no time. The easiest way to get started is to just get the KDevelop AppImage from [[https://www.kdevelop.org/download][their website]]. Download it, make it executable with ~chmod +x $KDevelopAppImageFile~ and run! 510 | 511 | Next you click /Project/ > /Open - Import Project/ and the navigate to a copy of this repository where the =CMakeLists.txt= resides. It should automatically give you a window with the project name and with the CMake Project Manager. Just hit /Finish/ on the bottom row and you will get another window to set up your CMake configuration. Here you need: 512 | 513 | - Select a build directory 514 | + I typically don't go with the default (b/c my code resides on a USB drive) and I build somewhere else on my main disk. Always using a ~project_name/build~ directory encourages people to write sloppy build files that reach into the repository (b/c you can always go ~../~ from the ~/build/~ folder to get to the repository files). But you shouldnt' write code/configurations that assume their built location ;) 515 | - The installation prefix can be left blank 516 | + CMake is a bit weird in that it's not just a build tool, but it also has these unnecessary installation features that keep cropping up 517 | - Build Type 518 | + This part I don't 100% understand at the moment.. but I think you can go with *Release* here. GDB seems to somehow magically find the matching source code on its own even when you build with no symbols. But if you have issues with debugging don't hesitate to switch to *Debug* 519 | - Provide extra arguments to CMake 520 | + Here we need to tell CMake about our toolchain. Unfortunately a lot of people don't use toolchain files - as you always always should - and they just go with the random system defaults. KDevelop seems to encourage this further by not providing a field for the toolchain file.. so you need to add a =-DCMAKE_TOOLCHAIN_FILE=/path/to/your/project/directory/bluepill/toolchain.cmake= in the extra arguments area here (yeah.. this is a bit clunky..) 521 | 522 | Then just hit /Run/ and the wheels should start turning. It will load in your whole project and then index your code + LL/CMSIS libraries for a few minutes. At this point you can already hit /Build/ in the top left and make that =elf= file like we did from the command line. Infact, underthe hood KDevelop is doing exactly what we did before manually. If you ~cd~ to your build directory you can still run ~make~ by hand if you want 523 | 524 | But now we are also getting the benefits of CMake. You can now click on variables, jump around the code and get all the fancy syntax highlighting you expect in a desktop program 525 | 526 | 527 | *** Extras 528 | KDevelop unfortuantely has some very bizarre default working directories in their configurations... 529 | 530 | **** Execute 531 | To make the /Execute/ button flash the program to the chip go to /Run/ > /Configure Launches.../ and then hit /+ Add/ in the top left and select your target's name from the drop down menu (mine is called =blinky.elf=). In the new screen on the right side, you want to change the /Executable/ from *Project Target* to *Exectuable* and then put in /the full path/ to openocd (mine is ~/usr/bin/openocd~). We also need to set the /Working Directory/ to be the build directory so it can find the =openocd.cfg= file we made. So now when we hit *Execute* on the top bar it will just run =openocd= in the build directory. The way we've set things up, this should flash the chip! 532 | 533 | **** Debug (WIP) 534 | In that same window you will notice there is a *Debug* submenu on the left under our target executable. It's probably possible to get the =OpenOCD/GDB= setup running here as well - but unfortunately here things just got too ugly for me and I couldn't find a sane way to set this up (and I kept having issues where KDevelop wasn't cleaning up the OpenOCD processes correctly). If you find a clean way to get this working then please make an issue/PR and tell me about it :) 535 | 536 | 537 | #+BEGIN_QUOTE 538 | This webpage is generated from an org-document (at ~./index.org~) that also generates all the files described. 539 | 540 | Once opened in Emacs:\\ 541 | - ~C-c C-e h h~ generates the webpage \\ 542 | - ~C-c C-v C-t~ exports the code blocks into the appropriate files\\ 543 | #+END_QUOTE 544 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Bluepill: a GCC/CMake build environment .. 8 | 9 | 10 | 12 | 13 | 14 | 15 | 16 |
17 | UP 18 | | 19 | HOME 20 |
21 |

Bluepill: a GCC/CMake build environment ..

22 |
23 |

Table of Contents

24 |
25 | 59 |
60 |
61 | 62 |
63 |

Intro

64 |
65 |
66 |

67 | Note: Since I've written this there have been several efforts to build a complete CMake system. The most complete and impressive one I've seen is https://github.com/ObKo/stm32-cmake 68 |

69 | 70 |

71 | While this page will still have good info and is educational (esp concerning OpenOCD/GDB) - if you want to do a serious project please take a look at ObKo's template/framework. I've only used it a bit - but it seems very polished and is similar in principle to what I've done. It supports pretty much all STM32 chips so it's not quite as simple as what I've done here 72 |

73 |
74 | 75 |

76 | In this guide we will setup a simple minimal blinky for the Bluepill - a very cheap board available from China with the STM32F103C8T6 chip on it. They cost a smidge over one dollar each. 77 |

78 | 79 |

80 | I will be building a blinky using STM32's Low Level API (explained below). I noticed everyone seems to still be using Make so I've tried to change things up and write up a whole toolchain for this board in CMake so hopefully this guide will provide something new and interesting for some people. At the end I will show how CMake allows us to plug in our project into basically any modern IDE - and we'll get the whole thing running just using GCC and OpenOCD. I have virtually no experience with embedded development, so if there are mistakes or you just want to tell me I'm an idiot, please leave an issue on the github repo. Note that a big part of the guts of this project are in the submodule which has its own repo 81 |

82 | 83 | 84 |
85 |

bluepill.jpeg 86 |

87 |
88 | 89 |

90 | This file and webpage is an org-document and the code within is automatically tangled into the files in the project. I try to note any files I've manually copied from elsewhere 91 |

92 |
93 | 94 |
95 |

A Quick start

96 |
97 |

98 | If you just want to try it out you just need to install cmake gcc-arm-none-eabi and openocd. You will also need a bluepill and a ST-Link V2 device. Attach those to your computer and run: 99 |

100 |
101 | cd your/source/directory
102 | git clone --recurse-submodules https://github.com/geokon-gh/bluepill.git
103 | cd your/build/directory
104 | cmake -DCMAKE_TOOLCHAIN_FILE=your/source/directory/bluepill/toolchain.cmake your/source/directory/bluepill/
105 | make
106 | opeocd
107 | 
108 |
109 |
110 | 111 |
112 |

Prior Art

113 |
114 |

115 | Some (but not all) of the projects from which I took ideas, inspiration and suggestions: 116 |

117 | 118 |
    119 |
  • dwelch67 has some great extremely minimal setups for the blue pilll on his github. He describes them extensively in a few places on Stack Overflow. Basically here he rolls his own .. everything. It's very greybeard. All the code is in your face - no libraries. Straight writting to registers. Definitely take a look!
  • 120 | 121 |
  • PurpleAlien has another setup - which is very close to what I'm doing. He described it on his website and the code is on the his github. It's a slightly different chip - but the steps should work almost identically for the bluepill.
  • 122 | 123 |
  • satoshinm has a wonderful guide where he guides you through all the challenges he had setting up his bluepill. It's long but very informative and I recommend reading it. He also has an accompanying repository where we has code for the blinky working using bare metal, the STM HAL and another 3rd party library. His code (the bare metal part especially) was absolutely invaluable for getting my setup up and running. Check out his github project.
  • 124 |
125 |
126 |
127 |
128 | 129 |
130 |

Anatomy of a build

131 |
132 |

133 | Unfortunately getting started and just getting a light blinking involved quite a lot of steps since we have no operating system to handle all the background stuff. I am writing all the piece out here in detail - but please do consider reusing some files from the templates provided by STM. Specifically look in the Cube package in STM32Cube_FW_F1_V1.6.0/Projects/STM32F103RB-Nucleo/Examples_LL/GPIO/GPIO_InfiniteLedToggling. Under SW4STM32 you will find a startup script and a linker script. And in Src / Inc you will find additional code files. I will try to mention them in passing. 134 |

135 |
136 | 137 |
138 |

CMake

139 |
140 |

141 | CMake is meta build automation tool. You define your project targets, how you want things linked and built and then it will generate build tools for you. Typically we just generate Make files but it can also coordinate system installation, generate installer wizards for Windows, generate Visual Studio project files and more. CMake has in effect standardized builds into a common set of easy to use terms - so while it's not as flexible as Make and you have to follow a certain layout/pattern, in the end everything ends up cleaner and more reusable and it's hard to shoot yourself in the foot. For us it will be our central tool for coordinating the build and we will define our whole project through a CMakeLists.txt file in our project root. We will see later that actually CMake will give us crucial project meta-information that Clang-tools and IDEs like CLion and KDevelop can hook into it to provide code highlighting/completion and other goodies. All of this is a serious quality-of-life improvement over vanilla Make. Because CMake has become the defacto standard in the C++ world the ecosystem of tools around it just keeps growing year to year. 142 |

143 | 144 |

145 | Starting our CMakeLists.txt we write out the usual CMake version and project name 146 |

147 | 148 |
149 |
cmake_minimum_required(VERSION 3.0)
150 | project(blinky)
151 | 
152 |
153 |

154 | Our end goal is to create an ELF file that we will flash onto the chip. I suggest adding the .elf extension explicitly to your target name. To keep the template generic enough I just name the elf the same as the project name. So if you fork this project and do your own thing - you can just change the project name and you'll be good to go 155 |

156 |
157 |
set(ELF ${PROJECT_NAME}.elf)
158 | 
159 |
160 |
161 |
162 | 163 |
164 |

The Linker Script

165 |
166 |

167 | When an application normally runs on a desktop machine it's generally running using virtual memory in a virtual application-specific address space. From the applications point of view it can manipulate its own memory however it wants - and it's the operating system that then translates that into safe operations on the actual memory (for instance to insure that the applications don't touch any memory region they shouldn't) 168 |

169 | 170 |

171 | On a microcontroller by default there is no operating system to manage the memory and the memory is shared with other functionality - some addresses are reserved for peripherals, other addresses are for interrupts and reset bits, the stack and heap are allocated in some device-specific place and there is also a split between ROM and RAM. 172 |

173 | 174 |

175 | Because of these new limitations we can't just start executing code at address zero or drop in a main() function somewhere randomly and start there, we need to tell the linker what the code layout is though a custom linker script. First we tell it which parts correspond to ROM and RAM and what will be their respective sizes. ROM (Read Only Memory) is where the code and constants live, and RAM (Random Access Memory) is where the stack and heap live - the stuff that's dynamic. 176 |

177 | 178 |
179 |
MEMORY
180 | {
181 |     rom (rx) : ORIGIN = 0x08000000, LENGTH = 64K
182 |     ram (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
183 | }
184 | PROVIDE(_stack = ORIGIN(ram) + LENGTH(ram));
185 | 
186 |
187 | 188 |

189 | Even give that, code on a microcontroller doesn't by default start at the first address of ROM and go from there. On a desktop program you generally have an entry point (a main()) and an exit point (ex: exit 0). But a better way to think about the way a microcontroller works is it's as a machine that recieves interrupt signals from external inputs and the chip responds by running code and then returns to whatever it was doing before. These interrupting inputs can be a clock running out, a peripheral wanting attention, an attached debugger wanting to pause everything, etc. There is no real starting point nor an "exit" 190 |

191 | 192 |

193 | So instead of a main() or something, the first thing in ROM is a vector table - a table of pointers to the different interrupt handlers. These handlers are the code that is run when each interrupt happens. The first interrupt handler in this table will be special and it's the one that is triggered when the system is powered on, the user presses the reset button, or the code runs out of things to do. It's appropriately called the reset handeler 194 |

195 | 196 |
197 |
EXTERN(vector_table);
198 | ENTRY(reset_handler);
199 | 
200 |
201 |

202 | These two are just symbols for the linker - and it will look for them later in the actual code it's linking. So we need to not forget to define them :) 203 |

204 | 205 |

206 | Lastly we need to tell the linker that we want those vectors first in the ROM and aligned at the byte level (b/c the micro reads things in byte sized chucks) 207 |

208 |
209 |
SECTIONS
210 | {
211 |     .text : {
212 |         *(.vectors)
213 |         *(.text*)
214 |         . = ALIGN(4);
215 |     } >rom
216 | }
217 | 
218 | 
219 |
220 |

221 | So the chip doesn't need to hunt for the vector table. It's always in the same spot at the start of the ROM - and the reset handler is as well. 222 |

223 |
224 |
225 | 226 |
227 |

The Startup File (WIP)

228 |
229 |

230 | As I mentioned, the reset handler and vector table are just symbols to the linker at this point - so the next step is to write the code for them. The vector table will be set to some default values (and can be changed later) and we'll write the reset handler in a generic way that we can reuse between projects. It will do some initializations and then at the end call to main(). Then when we start a new project we can copy over this file and simply start writing a main() skipping all that initialization. In so doing we've split off these the generic reusable stuff into a separate startup file. 231 |

232 | 233 |
234 |

235 | Note: There is nothing special about this file for the compiler or linker - it's just another code file - and you are free to copy it over to your other code files if you want 236 |

237 |
238 | 239 |

240 | The code from top to bottom: 241 |

242 |
    243 |
  • We define a reset handler 244 |
      245 |
    • We declare a main() (to be defined in our main.c later)
    • 246 |
    • We initialize some stuff
    • 247 |
    • We call main()
    • 248 |
  • 249 |
  • We define two dummy interrupt handlers. 250 |
      251 |
    • One that spins for ever
    • 252 |
    • One that does nothing and returns
    • 253 |
  • 254 |
  • We define our vector table in detail 255 |
      256 |
    • The first element is the SP (Stack Pointer)
    • 257 |
    • The second element is always the reset handler
    • 258 |
    • After that are the remaining handlers which are being set to basically do nothing
    • 259 |
  • 260 |
261 |
262 |
  int main(void);
263 |   void __attribute__ ((weak, naked)) reset_handler(void) {
264 |       (*(volatile unsigned int *)(0x40021018)) |= (1 << 4);
265 | 
266 |       (*(volatile unsigned int *)(0x40011004)) |= (0x00 << (((13 - 8) * 4) + 2));
267 |       (*(volatile unsigned int *)(0x40011004)) |= (0x02 << ((13 - 8) * 4));
268 |       main();
269 |   }
270 | 
271 | void blocking_handler(void) { while (1); }
272 | void null_handler(void) {}
273 | extern unsigned _stack;
274 | 
275 | __attribute__ ((section(".vectors")))
276 | struct {
277 |     unsigned int *initial_sp_value;
278 |     void (*reset)(void);
279 |     void (*nmi)(void);
280 |     void (*hard_fault)(void);
281 |     void (*memory_manage_fault)(void);
282 |     void (*bus_fault)(void);
283 |     void (*usage_fault)(void);
284 |     void (*reserved_x001c[4])(void);
285 |     void (*sv_call)(void);
286 |     void (*debug_monitor)(void);
287 |     void (*reserved_x0034)(void);
288 |     void (*pend_sv)(void);
289 |     void (*systick)(void);
290 |     void (*irq[68])(void);
291 | } vector_table = {
292 |     .initial_sp_value = &_stack,
293 |     .reset = reset_handler,
294 |     .nmi = null_handler,
295 |     .hard_fault = blocking_handler,
296 | 
297 |     .sv_call = null_handler,
298 |     .pend_sv = null_handler,
299 |     .systick = null_handler,
300 |     .irq = {
301 |         null_handler,
302 |         null_handler,
303 |         null_handler,
304 |         null_handler,
305 |         null_handler,
306 |         null_handler,
307 |         null_handler,
308 |         null_handler,
309 |         null_handler,
310 |         null_handler,
311 |         null_handler,
312 |         null_handler,
313 |         null_handler,
314 |         null_handler,
315 |         null_handler,
316 |         null_handler,
317 |         null_handler,
318 |         null_handler,
319 |         null_handler,
320 |         null_handler,
321 |         null_handler,
322 |         null_handler,
323 |         null_handler,
324 |         null_handler,
325 |         null_handler,
326 |         null_handler,
327 |         null_handler,
328 |         null_handler,
329 |         null_handler,
330 |         null_handler,
331 |         null_handler,
332 |         null_handler,
333 |         null_handler,
334 |         null_handler,
335 |         null_handler,
336 |         null_handler,
337 |         null_handler,
338 |         null_handler,
339 |         null_handler,
340 |         null_handler,
341 |         null_handler,
342 |         null_handler,
343 |         null_handler,
344 |         null_handler,
345 |         null_handler,
346 |         null_handler,
347 |         null_handler,
348 |         null_handler,
349 |         null_handler,
350 |         null_handler,
351 |         null_handler,
352 |         null_handler,
353 |         null_handler,
354 |         null_handler,
355 |         null_handler,
356 |         null_handler,
357 |         null_handler,
358 |         null_handler,
359 |         null_handler,
360 |         null_handler,
361 |         null_handler,
362 |         null_handler,
363 |         null_handler,
364 |         null_handler,
365 |         null_handler,
366 |         null_handler,
367 |         null_handler,
368 |         null_handler,
369 |     }
370 | };
371 | 
372 |
373 | 374 |

375 | Now if you open up some startup files in the templates provided by STM you will see that they're all written in assembly and they do a little more than we are doing here. If you squint and look at the assembly you will see that the code is doing basically the same thing + some extra magic - but on a high level it's also defining a reset handler which then calls a main. The remaining interrupt handlers are actually defined in C in stm32f1xx_it.h/c which is next to the source files and headers. 376 |

377 | 378 |

379 | Once we have the file we can add it to CMake 380 |

381 |
382 |
enable_language(ASM)
383 | set(STARTUP_FILE "startup_stm32f103xb.c")
384 | 
385 |
386 |
387 |
388 |
389 |

The STM Libraries

390 |
391 |

392 | Next we need actual libraries to write code with - otherwise we are kinda stuck poking at memory addresses with the datasheet. These are all provided in one bundle called Cube and it's on the STM website. Here is a quick digest of what you get: 393 |

394 | 395 |
    396 |
  • The BSP has board specific peripheral libraries.. since we aren't using a board from STM - this really doesn't concern us.
  • 397 | 398 |
  • The HAL that comes from STM is the standard Hardware Abstraction Layer. It will be making some simplifying assumptions and do some stuff more automatically for you. I'm going to skip setting this up. Blinking a light should be pretty simple - so I'm shooting to get it working with simpler APIs
  • 399 | 400 |
  • Hidden inside of the HAL folder you will see files that are names stm32f1xx_ll_*.c/h. These actually form a seperate sub-library of sorts called the LL API (for Low Level)
  • 401 | 402 |
  • The CMSIS ( Cortex Microcontroller Software Interface Standard ) : This library comes from ARM (not STM). It's split into several semi-independent components and provides a common base for all ARM devices (independent of vendor). The HAL and LL API are built on top of the CMSIS
  • 403 |
404 | 405 |

406 | Both the HAL and CMSIS need some chip-specific configuration - b/c while the API is standard, under the hood things will change from chip to chip (like memory addresses of things or clock information). I've bundled the LL API and the CMSIS together in a separate project stm32f1-ll (github). It's also building with CMake so we can use it directly in our project (and you can skip registration and downloading the Cube thing). I recommend checking out that project's webpage for more details on how it works - but there is very little magic going on. 407 |

408 | 409 |

410 | The library bundle has been added as a submodule to this project, but if you forgot to clone recursively you can clone it right now into the project root with git clone https://github.com/geokon-gh/stm32f1-ll/. Once we have it there we can just add it in 411 |

412 | 413 |
414 |
add_subdirectory(stm32f1-ll)
415 | 
416 |
417 | 418 |
419 |

420 | Note:in STM32Cube_FW_F1_V1.6.0/Middlewares there are additional libraries that sorta live on top of all of this and do more complicated stuff like TCP/IP USB..stuff and Filesystem things. Basically things that are kinda complicated and that you probably want to avoid writing yourself. I'm completely skipping this :) 421 |

422 |
423 |
424 |
425 | 426 |
427 |

Our blinky code

428 |
429 |

430 | Now that we have a linker that can put the code in the right places, and the startup code to initialize things correctly for us we can finally write our blinky code in /src/main.c 431 |

432 | 433 |

434 | The libraries/modules in the LL-API are pretty fragmented/decoupled. So while we want to use the GPIO, we also need a few other libraries to get started. The GPIO peripheral for starters need to be powered on by the microcontroller (all the peripherals are powered off by default). The system responsible for that is called Reset and Clock Control, RCC for short. There is another module called BUS which seems to just have helper functions to turn on/off RCC sections. 435 |

436 | 437 |

438 | So first we include all three of these modules 439 |

440 |
441 |
#include "stm32f1xx_ll_bus.h"
442 | #include "stm32f1xx_ll_rcc.h"
443 | #include "stm32f1xx_ll_gpio.h"
444 | 
445 |
446 |

447 | Then we can start writing our main() 448 |

449 |
450 |
int main(void)
451 | {
452 | 
453 |
454 |

455 | We use a BUS module macro to enable the GPIO peripheral. The LED we want to blink on the board is labeled PC13. PC stands for Port C and it's number 13. So we enable GPIO port C. The peripherals controlled by the RCC system are split into two sections APB1 and APB2. GPIO is in the APB2 section. 456 |

457 |
458 |
LL_APB2_GRP1_EnableClock(LL_APB2_GRP1_PERIPH_GPIOC);
459 | 
460 |
461 |

462 | We then set this GPIO pin to be an ouput pin and we set it to be a high speed pin 463 |

464 |
465 |
LL_GPIO_SetPinMode(GPIOC, LL_GPIO_PIN_13, LL_GPIO_MODE_OUTPUT);
466 | LL_GPIO_SetPinSpeed(GPIOC, LL_GPIO_PIN_13, LL_GPIO_SPEED_FREQ_HIGH);
467 | 
468 |
469 |

470 | Now that we have the GPIO pin setup we just have an infinite loop that toggles the pin and spends some time sitting in a loop doing nothing 471 |

472 |
473 |
  while (1)
474 |   {
475 |     LL_GPIO_TogglePin(GPIOC, LL_GPIO_PIN_13);
476 |     int i = 200000;/* About 1/4 second delay */
477 |     while (i-- > 0) {
478 |         asm("nop");/* This stops it optimising code out */
479 |     }
480 |   }
481 | }
482 | 
483 |
484 |

485 | And that's it! 486 |

487 | 488 |

489 | If you look at the GPIO example that comes with the Cube package you will see it does the waiting/spinning in a smarter way using the clock system. But this requires setting up the clock properly and is a bit more complicated. You will also need the system_stm32f1xx.h/c files to configure the clock. For simplicity I've omitted this - but you probably want to add that back in if you're doing more complicated stuff. 490 |

491 | 492 |

493 | We can now return to CMake and declare our target elf file and link up our main.c with the startup file. 494 |

495 |
496 |
add_executable(${ELF} ${STARTUP_FILE}
497 |   src/main.c)
498 | target_include_directories(${ELF} PUBLIC inc)
499 | 
500 |
501 |

502 | and then we link it to the LL-API library we are using 503 |

504 |
505 |
target_link_libraries(${ELF} ll )
506 | 
507 |
508 |

509 | And I'm also adding in a include directory for future use (it's empty for now) 510 |

511 |
512 |
target_include_directories(${ELF} PUBLIC inc)
513 | 
514 |
515 |
516 |
517 |
518 |

The toolchain

519 |
520 |

521 | Now that we have all the pieces we just need to tell CMake how we want everything compiled. I'm building using gcc-arm-none-eabi and its associated tools. I did this on a Debian system where this version of gcc can be installed from the repository (name gcc-arm-none-eabi) 522 |

523 | 524 |

525 | Canonically the compiler is specified in a separate file so that you can subsitute other possible toolchains (like for instance LLVM or custom versions of GCC). I'll just setup GCC as an example and write it out to a toolchain.cmake file in the source directory 526 |

527 | 528 |
529 |
set(CMAKE_SYSTEM_NAME Generic) # 'Generic' is used for embedded systems
530 | 
531 | set(CMAKE_C_COMPILER arm-none-eabi-gcc)
532 | set(CMAKE_CXX_COMPILER arm-none-eabi-g++)
533 | set(CMAKE_ASM_COMPILER arm-none-eabi-gcc)
534 | 
535 | # tells CMake not to try to link executables during its interal checks
536 | # things are not going to link properly without a linker script
537 | set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)
538 | 
539 | set(CMAKE_OBJCOPY arm-none-eabi-objcopy)
540 | set(CMAKE_OBJDUMP arm-none-eabi-objdump)
541 | set(CMAKE_SIZE arm-none-eabi-size)
542 | set(CMAKE_DEBUGGER arm-none-eabi-gdb)
543 | set(CMAKE_DEBUGGER arm-none-eabi-gdb)
544 | set(CMAKE_CPPFILT arm-none-eabi-c++filt)
545 | 
546 |
547 |

548 | If you skip writing a toolchain file then CMake will default to the system compiler and things will start to slowly go wrong for you (kinda unfortunately, it generally doesn't blow up into your face here) 549 |

550 | 551 |

552 | Next will also need to tell the linker which linker script to use (which is a bit ugly in CMake) 553 |

554 | 555 |
556 |
set_target_properties(
557 |   ${ELF}
558 |   PROPERTIES
559 |   LINK_FLAGS
560 |   "-T${PROJECT_SOURCE_DIR}/STM32F103RBTx_FLASH.ld \
561 |    -mthumb -mcpu=cortex-m3 \
562 |    -Wl,--gc-sections \
563 |    -Wl,-Map=${PROJECT_NAME}.map")
564 | 
565 |
566 | 567 |

568 | I'm appending this to the CMakeLists.txt, but it's something that maybe could be in the toolchain file. However b/c it does need the target name (${ELF}) it's more convenient to just append it to the CMakeLists.txt (something to maybe revisit in the future) 569 |

570 | 571 |

572 | You'll also notice I added some more linker options - the first two tell the linker it the chip type and the instruction set and the other two: 573 |

574 | 575 |
576 |

577 | Note: At first I hadn't given the computer arch/intruction-set flags b/c they're being given to the compiler below and I ended up with a very very subtly big where __libc_init_array address was trying to jump to some addresses right outside of the ROM. It was very bizarre and took me a whole day to track down. 578 |

579 |
580 | 581 |
    582 |
  • --gc-sections this tells the linker to remove unused code/data from the final executable. There is a pesky _exit() function referrence that will often get slipped into your executable by the compiler. B/c we are running on a microcontroller the code never really exits (it can't quit and hand off executation to an OS after all!) so this exit needs to be removed by the linker. If you leave this off then the linker will get confused and start complaining you never defined an exit function.
  • 583 | 584 |
  • -Map prints a link map: 585 |
      586 |
    • Where object files and symbols are mapped into memory.
    • 587 |
    • How common symbols are allocated.
    • 588 |
    • All archive members included in the link, with a mention of the symbol which caused the archive member to be brought in.
    • 589 |
  • 590 |
591 | 592 |

593 | The link map is a high-level overview of how your code is placed in memory 594 |

595 | 596 |

597 | More linker options are explained in details here: https://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_node/ld_3.html 598 |

599 | 600 |

601 | We then also need to let the compiler know our target architecture and some compiler options (taken from PurpleAlien) 602 |

603 | 604 |
605 |
target_compile_options(${ELF} PUBLIC
606 |   -Wall 
607 |   -g 
608 |   -std=gnu99 
609 |   -Os
610 |   -mthumb
611 |   -mcpu=cortex-m3
612 |   -mfloat-abi=soft
613 |   -mlittle-endian
614 |   -ffunction-sections 
615 |   -fdata-sections
616 |   -Werror 
617 |   -Wstrict-prototypes 
618 |   -Warray-bounds 
619 |   -fno-strict-aliasing 
620 |   -Wno-unused-const-variable 
621 |   -specs=nano.specs 
622 |   -specs=nosys.specs)
623 | 
624 |
625 |

626 | TODO Explain all of these… and again.. would be nice to have in the toolchain file 627 |

628 |
629 |
630 |
631 | 632 |
633 |

Getting the code on the chip

634 |
635 |
636 |
637 |

Building

638 |
639 |

640 | At this point we have all the files we need to build the code, so just go to a new empty directory and run 641 |

642 | 643 |
644 | cmake -DCMAKE_TOOLCHAIN_FILE=path/to/source/toolchain.cmake /path/to/source/
645 | make
646 | 
647 | 648 |

649 | Now in the build directory you'll have some build garbage, the link map blinky.map and blinky.elf - which is the code/executable that we want to get onto the bluepill. 650 |

651 |
652 |
653 | 654 |
655 |

OpenOCD

656 |
657 |

658 | The standard open source software for flashing the bluepill is OpenOCD. On educational/more-expensive boards there will be a secondary chip that helps you flash the microcontroller. But on cheaper and more practical chips this part is omitted (b/c in a sense it's a waste to have the same chip on every single board). So to flash the bluepill you will need something to do the flashing with. I'm using a knock off ST-LINK v2 I purchased on Taobao. (note the wiring is in a different order on the board and programmer) 659 |

660 | 661 | 662 |
663 |

st-link.jpeg 664 |

665 |
666 | 667 |

668 | OpenOCD will provide us with an abstraction layer. It will communicate over JTAG or SWD to the chip and we will communicate with OpenOCD and tell it what we need. 669 |

670 | 671 |

672 | OpenOCD's software setup is rather baroque - but the documentation is very thorough. You start from the beginning and just read very carefully sequentially and it will all make sense. Fortunately for us - the hardware we're using is very standard so we can use some already provided templates. When I install OpenOCD on my Debian system through apt-get install openocd the templates are in /usr/share/openocd/scripts/board/. After browsing some similar boards (like the stm32f4disovery) you kinda get the picture of how the configuration file should look (THESE VALUES CHANGE BETWEEN POINT RELEASE OF OPENOCD. DOUBLE CHECK THEM IF YOU HAVE ANY ISSUES) 673 |

674 | 675 |

676 | The configuration is a bit finicky. For OpenOCD to automatically pick it up it needs to be called openocd.cfg and it needs to be in the directory where you run openocd. 677 |

678 |
679 |
source [find interface/stlink-v2.cfg]
680 | transport select hla_swd
681 | source [find target/stm32f1x_stlink.cfg]
682 | program blinky.elf verify reset exit
683 | 
684 |
685 |

686 | The finaly file isn't too complicated. It sets the interface type (ie the ST-LINK flashing dongle thing), then it sets the flashing communication protocol for talking to the chip, then the actual chip type and lastly we tell it to program the chip with the .elf we just made. After flashing it will verify the code, reset the controller and then exit OpenOCD 687 |

688 | 689 |

690 | For convenience we should also tell CMake to copy this file over to the build directory 691 |

692 |
693 |
file(COPY
694 |   openocd.cfg
695 |   DESTINATION
696 |   ${CMAKE_BINARY_DIR})
697 | 
698 |
699 | 700 |

701 | So now in our build directory we simply run openocd and your program should magically upload to the chip and start running. The light should be flashing at this point :) 702 |

703 | 704 |

705 | If it's not, Some things to double check: 706 |

707 |
    708 |
  • The version of OpenOCD you are running… I had weird issues with manually installed OpenOCDs, but the repo one worked great
  • 709 |
  • Check the templates - if you have problems, try some of the other options available
  • 710 |
  • try running OpenOCD as root! Maybe your user doesn't have the right USB permissions or something to that effect
  • 711 |
  • I had weird connection issues that turned out to be due to faulty wires! Thanks to NeoMarxismIsEvil for catching that :)
  • 712 |
  • In the next section about GDB I mention an unlock command.. I'm not 100% sure it's necessary - but try it if you're having issues
  • 713 |
714 |
715 |
716 |
717 | 718 |
719 |

Intergrated Development

720 |
721 |

722 | One of the big bonuses of using CMake is that it will hook into existing tools very easily. 723 |

724 |
725 | 726 |
727 |

GDB

728 |
729 |

730 | The first basic step is hookin' up a debugger. 731 |

732 | 733 |

734 | For some reason Debian (Testing) is missing a arm-none-eabi-gdb, so I had to just download the whole GCC toolchain from ARM's website (this only works assuming you're running on a x64 machine). Just whatever you end up using, make sure you don't just run the system GDB! It won't throw you any errors and it will kinda work.. till it doesn't. 735 |

736 | 737 |

738 | Once we have the right version of GDB the next part becomes super easy b/c by default OpenOCD will provide us with a GDB server to which we can connect. We just need to disable the part where we flash the program and exit and replace it with a command to reset the chip and wait for GDB 739 |

740 | 741 |
742 |
source [find interface/stlink-v2.cfg]
743 | transport select hla_swd
744 | source [find target/stm32f1x_stlink.cfg]
745 | reset_config srst_nogate
746 | 
747 |
748 | 749 |

750 | I honestly didn't entirely understand all the reset configuration options so if you're having issues I'd suggest looking at the documentation and trying several different settings. I've found the current one works for me. Again, we just run openocd but this time the program kinda hangs and sits and waits for a connection: 751 |

752 | 753 |
754 |

755 | $ openocd 756 | Open On-Chip Debugger 0.10.0 757 | Licensed under GNU GPL v2 758 | For bug reports, read 759 | http://openocd.org/doc/doxygen/bugs.html 760 | WARNING: target/stm32f1xstlink.cfg is deprecated, please switch to target/stm32f1x.cfg 761 | Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD 762 | adapter speed: 1000 kHz 763 | adapternsrstdelay: 100 764 | none separate 765 | none separate 766 | Info : Unable to match requested speed 1000 kHz, using 950 kHz 767 | Info : Unable to match requested speed 1000 kHz, using 950 kHz 768 | Info : clock speed 950 kHz 769 | Info : STLINK v2 JTAG v17 API v2 SWIM v4 VID 0x0483 PID 0x3748 770 | Info : using stlink api v2 771 | Info : Target voltage: 2.913562 772 | Info : stm32f1x.cpu: hardware has 6 breakpoints, 4 watchpoints 773 |

774 |
775 | 776 |

777 | We open another terminal and run our arm-none-eabi-gdb to bring up the GDB "shell" The next few steps will connect to the OpenOCD server, stop the program running on it, unlock the chip, and load our new program 778 |

779 | 780 |
781 | > target remote localhost:3333
782 | > monitor reset halt
783 | > monitor stm32f1x unlock 0
784 | > load blinky.elf
785 | 
786 | 787 |

788 | Now you can set breakpoint, run code, inspect the stack and variables, etc. etc. Look at the GDB manual for all the juicy details - and don't forget about the very handy TUI Mode. Start it with C-x C-a, then hit C-x 2 to bring up the assembly. And type s or n to step one line of code at a time and si to step one assembly instruction at a time! 789 |

790 |
791 |
792 | 793 |
794 |

KDevelop

795 |
796 |

797 | To demonstrate how flexible things get thanks to CMake, next I'll show you how to setup KDevelop to run everything for us. In principle this should work equally well with QtCreator or CLion or CQuery/Emacs. You can even hook up linters and other fancy Clang based tools now pretty easily. So this isn't an endorsement of KDevelop over the alternatives b/c after all it's sorta like Visual Studio - a big drop-box driven mess - but I'm just familiar with it and it's quick and easy to get up and running with a CMake project. We'll be able to jump around our code and refactor things in no time. The easiest way to get started is to just get the KDevelop AppImage from their website. Download it, make it executable with chmod +x $KDevelopAppImageFile and run! 798 |

799 | 800 |

801 | Next you click Project > Open - Import Project and the navigate to a copy of this repository where the CMakeLists.txt resides. It should automatically give you a window with the project name and with the CMake Project Manager. Just hit Finish on the bottom row and you will get another window to set up your CMake configuration. Here you need: 802 |

803 | 804 |
    805 |
  • Select a build directory 806 |
      807 |
    • I typically don't go with the default (b/c my code resides on a USB drive) and I build somewhere else on my main disk. Always using a project_name/build directory encourages people to write sloppy build files that reach into the repository (b/c you can always go ../ from the /build/ folder to get to the repository files). But you shouldnt' write code/configurations that assume their built location ;)
    • 808 |
  • 809 |
  • The installation prefix can be left blank 810 |
      811 |
    • CMake is a bit weird in that it's not just a build tool, but it also has these unnecessary installation features that keep cropping up
    • 812 |
  • 813 |
  • Build Type 814 |
      815 |
    • This part I don't 100% understand at the moment.. but I think you can go with Release here. GDB seems to somehow magically find the matching source code on its own even when you build with no symbols. But if you have issues with debugging don't hesitate to switch to Debug
    • 816 |
  • 817 |
  • Provide extra arguments to CMake 818 |
      819 |
    • Here we need to tell CMake about our toolchain. Unfortunately a lot of people don't use toolchain files - as you always always should - and they just go with the random system defaults. KDevelop seems to encourage this further by not providing a field for the toolchain file.. so you need to add a -DCMAKE_TOOLCHAIN_FILE=/path/to/your/project/directory/bluepill/toolchain.cmake in the extra arguments area here (yeah.. this is a bit clunky..)
    • 820 |
  • 821 |
822 | 823 |

824 | Then just hit Run and the wheels should start turning. It will load in your whole project and then index your code + LL/CMSIS libraries for a few minutes. At this point you can already hit Build in the top left and make that elf file like we did from the command line. Infact, underthe hood KDevelop is doing exactly what we did before manually. If you cd to your build directory you can still run make by hand if you want 825 |

826 | 827 |

828 | But now we are also getting the benefits of CMake. You can now click on variables, jump around the code and get all the fancy syntax highlighting you expect in a desktop program 829 |

830 |
831 | 832 | 833 |
834 |

Extras

835 |
836 |

837 | KDevelop unfortuantely has some very bizarre default working directories in their configurations… 838 |

839 |
840 | 841 |
    842 |
  • Execute
    843 |
    844 |

    845 | To make the Execute button flash the program to the chip go to Run > Configure Launches… and then hit + Add in the top left and select your target's name from the drop down menu (mine is called blinky.elf). In the new screen on the right side, you want to change the Executable from Project Target to Exectuable and then put in the full path to openocd (mine is /usr/bin/openocd). We also need to set the Working Directory to be the build directory so it can find the openocd.cfg file we made. So now when we hit Execute on the top bar it will just run openocd in the build directory. The way we've set things up, this should flash the chip! 846 |

    847 |
    848 |
  • 849 | 850 |
  • Debug (WIP)
    851 |
    852 |

    853 | In that same window you will notice there is a Debug submenu on the left under our target executable. It's probably possible to get the OpenOCD/GDB setup running here as well - but unfortunately here things just got too ugly for me and I couldn't find a sane way to set this up (and I kept having issues where KDevelop wasn't cleaning up the OpenOCD processes correctly). If you find a clean way to get this working then please make an issue/PR and tell me about it :) 854 |

    855 | 856 | 857 |
    858 |

    859 | This webpage is generated from an org-document (at ./index.org) that also generates all the files described. 860 |

    861 | 862 |

    863 | Once opened in Emacs:
    864 |

    865 |
      866 |
    • C-c C-e h h generates the webpage
    • 867 |
    • C-c C-v C-t exports the code blocks into the appropriate files
    • 868 |
    869 |
    870 |
    871 |
  • 872 |
873 |
874 |
875 |
876 |
877 |
878 |

Author: George Kontsevich

879 |

Created: 2021-04-13 Tue 15:54

880 |

Validate

881 |
882 | 883 | 884 | --------------------------------------------------------------------------------