Introduction

After some time sifting through code and making/breaking things, I decided to put stuff in writing.

Beginning was with an IDE. I migrated since to command line compiling and linking tools to better understand the background and how the things work.

Chip of choice is STM32F103C8 commonly known as the blue pill, named so after a size factor and PCB color.

Tools of choice: open source and widely used. GNU gcc for ARM microcontroller and GNU ld linker. For flashing the chip I use JLink for uploading the hex file.

Why does binary file format suck? When you need to put variables at exact memory address in address space, binary file format fills everything with blank space up until the declared variable, and that can grow to a quite big size. When I discovered this, I was perplexed why my binary file was cca 1 GB in size, instead of a few KBs. That’s why I switched to Intel hex format, or more popularly called just hex. JLink flash utility supports binary AND hex formats, amongst others.

A little background about the tools needed. Compiler transforms source code to object files, and linker pieces the bits together to produce one ouput file, which is then ready to be uploaded to the chip with a flash tool.

We’re going to start with the simplest possible example. Blinking the led inaccurately and with an uninitialized clock source, which defaults to 8 MHz internal RC clock.

What does it tell us? That the linker, compiler and uploader work correctly. With a proper stepping stone, we are always free to break and make things, for example “code refactoring”, fancy name for rewriting code because you don’t like it, think it’s ugly or it’s a depressing Saturday afternoon.

Entry to the main program

After compiler and linker together make a salad, where does it start? Does it start in a random chunk of memory, and if not, how can I tell them where to exactly start?

For that we have a global start function as an entry to the program. If a global directive is missing for a symbol, that symbol will not be placed in the object code’s export table, so linker has no way of knowing about the symbol. We also need to put the address of the stacktop right at the beginning of memory space. After that comes the reset address, where the microcontroller ALWAYS ends up first after resetting.

After we have defined global entry in the program (for the linker) and reset entry, we can add a path that leads to our main function (entry to our c part of the program).

.global _start
_start:
stacktop: .word 0x20001000
.word reset
.word hang

reset:
	bl notmain
	b hang
.thumb_func
hang:   b .

Recommendation

I can also recommend a really good github repository from dwelch67 that got me on the right track. dwelch67’s repository

Majority of code during this post will be explaining the code that can be found in the link above, with simplifications and modifications.

I am not going to say to copy all from somebody else. But for the beginning I recommend it because they made it work, and it should also work when compiled and flashed. After we have that working base example, then comes the creativity and design choices in question.

Functions for direct register access

We are going to need a function to directly access registers and also fill them with data. Functions used are assembler functions PUT32 and GET32. Assembler functions can be used directly in C program code and first four registers are filled with function paramaters. That means that the r0 registers will be filled with the first function parameter up until r3. Square brackets signify that the value in the register is meant as an address. .globl keyword signifies that the function is global and accessible anywhere.

.globl PUT32
PUT32:
	str r1,[r0]
	bx lr

.globl GET32
GET32:
	ldr r0,[r0]
	bx lr
	
.globl dummy
	dummy:
	bx lr

Getting the led up and running

Bluepill board has a led light connected to the PC13 pin. We need to enable clock for port C, initialize the pin as a push pull output, then toggle the pin.

void PUT32 ( unsigned int, unsigned int );
unsigned int GET32 ( unsigned int );
void dummy ( unsigned int );

//define address base for port C
#define GPIOCBASE 0x40011000
//define address base for clock control
#define RCCBASE 0x40021000

//our entry point into the program, defined in our assembler file
int notmain ( void )
{
	unsigned int ra;
	unsigned int rx;

	ra=GET32(RCCBASE+0x18);
	ra|=1<<4; //enable port c
	PUT32(RCCBASE+0x18,ra);
	//config
	ra=GET32(GPIOCBASE+0x04);
	ra&=~(3<<20);   //PC13
	ra|=1<<20;      //PC13
	ra&=~(3<<22);   //PC13
	ra|=0<<22;      //PC13
	PUT32(GPIOCBASE+0x04,ra);

	for(rx=0;;rx++)
	{
		PUT32(GPIOCBASE+0x10,1<<(13+0));
		for(ra=0;ra<200000;ra++) dummy(ra);
		PUT32(GPIOCBASE+0x10,1<<(13+16));
		for(ra=0;ra<200000;ra++) dummy(ra);
	}
	return(0);
}

Bits that interest us:

RCC_APB2ENR bit 4
0x018 offset IOPCEN

Taken from page 153 of the manual. We are putting 1 into the bit 4 of the APB2 clock enable register to turn on the clock for the port C peripherals.

Link for creating the tables: https://ozh.github.io/ascii-tables/

GPIO configuration registers are split into two registers: Low and High. Low register contain the pins from 0 to 7 and High contain pins from 8 to 15.

Bits that interest us:

GPIOx_CRH Bits 23-22 Bits 21-20
0x04 offset CNF13[1:0] MODE13[1:0]

Taken from page 188 of the manual. We are setting the values in the bits 20-21 for the pin mode and 22-23 for the configuration mode of the pin.

On the page 167 we may read about valid bit combos for those four bits.

CNFy[1:0] In input mode (MODE[1:0]=00) In output mode(MODE[1:0]>00)
for CRH 00: Analog mode 00: General purpose output Push-pull
y = 8…15 01: Floating input 01: General purpose output Open-drain
for CRL 10: Input with pull-up / pull-down 10: Alternate function output Push-pull
y = 0…7 11: Reserved state 11: Alternate function output Open-drain
MODEy[1:0]  
for CRH 00: Input mode (reset state)
y = 8…15 01: Output mode, max speed 10 MHz
for CRL 10: Output mode, max speed 2 MHz
y = 0…7 11: Output mode, max speed 50 MHz

For writing your own library, you will need to familiarize yourself with the datashet of the microcontroller, in this case STM32F103C8 RM0008 Reference Manual. The datasheet will be extensively used throughout the posts.

Up until now we have two files: startup.s and programentry.c. Now we need to setup an environment for compiling, linking and flashing.

Linker file

Linker file looks like this:

	MEMORY
	{
		rom : ORIGIN = 0x08000000, LENGTH = 0x1000
		ram : ORIGIN = 0x20000000, LENGTH = 0x1000
	}

	SECTIONS
	{
		.text : { *(.text*) } > rom
		.rodata : { *(.rodata*) } > rom
		.bss : { *(.bss*) } > ram
	}

MEMORY keyword tells us how the memory is organized and how big it is. SECTIONS keyword defines where the types of program data should go. In this example we tell the linker that .text and .rodata should go in ROM part of the memory, and .bss data in the RAM part of the memory. (.bss) => segment for uninitialized data. (.data) => segment for initialized data here by default. (.rodata) => segment for constant data.

I am using GNU ld linker, as a good open source choice widely used. I am not trying to use black magic or obscure tools.

Makefile

Minimum unix tools required are make and rm, both used in the makefile.

Let’s go through the makefile. Here, we are just declaring some variables to make the makefile more readable.

ARMGNU = arm-none-eabi

AOPS = --warn --fatal-warnings -mcpu=cortex-m3
COPS = -Wall -Werror -O2 -nostdlib -nostartfiles -ffreestanding -mcpu=cortex-m0

Here is the first “recipe” in linker terminology, that can be invoked with “make all” or just “make”. Invoking only “make” will execute the first found recipe.

	all : output.hex

Here is the second recipe which is used for a cleanup of produced files, leaving only the source files intact.

clean:
	rm -f *.bin
	rm -f *.o
	rm -f *.elf
	rm -f *.list
	rm -f *.bc
	rm -f *.opt.s
	rm -f *.norm.s
	rm -f *.hex

We are defining rules for compilation of our two source files. startup.o needs startup.s and that’s why we are declaring .s file on the right side as a dependency, and building the file with an assembler (-as, or full blown command arm-linux-gnueabi-as)

startup.o : startup.s
	$(ARMGNU)-as $(AOPS) startup.s -o startup.o

File programentry.c is being compiled here with a gcc compiler. It is also being specified that we are using thumb instruction set (-mthumb), cpu Cortex-M3 (-mcpu=cortex-m3), architecture ARMv7 (armv7-m), that the input file is programentry.c and output file should be programentry.o.

programentry.o : programentry.c	
	$(ARMGNU)-gcc $(COPS) -mthumb -mcpu=cortex-m3 -march=armv7-m -c programentry.c -o programentry.o

Here is the build of the final output file that is going to be flashed on the chip. Our final output file depends on three files. Linker file linker.ld and two object files startup.o and programentry.o compiled from the source files. the source files

	output.hex : linker.ld startup.o programentry.o

Here are the objects linked according to linker file linker.ld and output is an elf file.

		$(ARMGNU)-ld -o output.elf -T linker.ld startup.o programentry.o

Object dump (objdump) is used to dump the contents from an .elf file to the .list file.

		$(ARMGNU)-objdump -D output.elf > output.list

Finally, from the .elf file we get the final output file with objcopy.

		$(ARMGNU)-objcopy output.elf output.hex -O ihex

Hardware used for flashing the chip

I am using Segger’s J-link to flash the chip via the SWD (Single Wire Debugging) interface and am also using JLink v5.10u, an older version of the flashing software. If it works, there’s no reason to continuously update.

Hardware connections are as follows:

J-link Pin 1 = VCCref => Pin 1 SWD Connector
J-link Pin 7 = SWDIO => SWDIO Pin
J-link Pin 9 = SWCLK => SWCLK Pin
J-link Pin 13 = SWO => PB3 Pin
J-link Pin 19 = Target power supply => 3v3 pin of the microcontroller
J-link Pin 4 = Ground Pin => connect to any GND pin of the microcontroller

To enable the J-link to power the target, the following command must be input through the J-link command line: “power on perm”.

To connect to the microcontroller, I run the J-link flash program through the command line. Here is the output.

	Microsoft Windows [Version 6.1.7601]
	Copyright (c) 2009 Microsoft Corporation.  All rights reserved.

	C:\Users\Bunny>jlink
	SEGGER J-Link Commander V5.10u (Compiled Mar 17 2016 18:57:13)
	DLL version V5.10u, compiled Mar 17 2016 18:56:47

	Connecting to J-Link via USB...O.K.
	Firmware: J-Link ARM V8 compiled Nov 28 2014 13:44:46
	Hardware version: V8.00
	S/N: 58001139
	License(s): RDI,FlashDL,FlashBP,JFlash
	VTref = 4.801V


	Type "connect" to establish a target connection, '?' for help
	J-Link>connect
	Please specify device / core. <Default>: STM32F103C8
	Type '?' for selection dialog
	Device>
	Please specify target interface:
	  J) JTAG (Default)
	  S) SWD
	TIF>s
	Specify target interface speed [kHz]. <Default>: 4000 kHz
	Speed>
	Device "STM32F103C8" selected.


	Found SWD-DP with ID 0x1BA01477
	Found SWD-DP with ID 0x1BA01477
	Found Cortex-M3 r1p1, Little endian.
	FPUnit: 6 code (BP) slots and 2 literal slots
	CoreSight components:
	ROMTbl 0 @ E00FF000
	ROMTbl 0 [0]: FFF0F000, CID: B105E00D, PID: 001BB000 SCS
	ROMTbl 0 [1]: FFF02000, CID: B105E00D, PID: 001BB002 DWT
	ROMTbl 0 [2]: FFF03000, CID: B105E00D, PID: 000BB003 FPB
	ROMTbl 0 [3]: FFF01000, CID: B105E00D, PID: 001BB001 ITM
	ROMTbl 0 [4]: FFF41000, CID: B105900D, PID: 001BB923 TPIU-Lite
	Cortex-M3 identified.
	J-Link>power on perm
	J-Link>

We are using the “loadfile” command to load the hex file onto the chip. After flashing the chip, we issue “r” for reset and “g” for go command. The led should inaccurately, but happily blink.