Electronics, Embedded Systems, and Software are my breakfast, lunch, and dinner.
A couple years ago I wrote a post about writing a bare metal USB driver for the Teensy 3.1, which uses Freescale Kinetis K20 microcontroller. Over the past couple years I've switched over to instead using the STM32 series of microcontrollers since they are cheaper to program the "right" way (the dirt-cheap STLink v2 enables that). I almost always prefer to use the microcontroller IC by itself, rather than building around a development kit since I find that to be much more interesting.
One of my recent (or not so recent) projects was an LED Wristwatch which utilized an STM32L052. This microcontroller is optimized for low power, but contains a USB peripheral which I used for talking to the wristwatch from my PC, both for setting the time and for reflashing the firmware. This was one of my first hobby projects where I designed something without any prior breadboarding (beyond the battery charger circuit). The USB and such was all rather "cross your fingers and hope it works" and it just so happened to work without a problem.
In this post I'm going to only cover a small portion of what I learned from the USB portion of the watch. There will be a further followup on making the watch show up as a HID Device and writing a USB bootloader.
Example code for this post can be found here:
**https://github.com/kcuzner/led-watch**
(mainly in common/src/usb.c and common/include/usb.h)
My objective here is to walk quickly through the operation of the USB Peripheral, specifically the Packet Memory Area, then talk a bit about how the USB Peripheral does transfers, and move on to how I structured my code to abstract the USB packetizing logic away from the application.
I am working on a project that involves a Li-Ion battery charger. I've never built one of these circuits before and I wanted to test the battery over its entire charge-discharge cycle to make sure it wasn't going to burst into flame because I set some resistor wrong. The battery itself is very tiny (100mAH, 2.5mm thick) and is going to be powering an extremely low-power circuit, hopefully over the course of many weeks between charges.
After about 2 days of taking meter measurements every 6 hours or so to see what the voltage level had dropped to, I decided to try to automate this process. I had my trusty Teensy 3.1 lying around, so I thought that it should be pretty simple to turn it into a simple data logger, measuring the voltage at a very slow rate (maybe 1 measurement per 5 seconds). Thus was born the EZDAQ.
All code for this project is located in the repository at `https://github.com/kcuzner/ezdaq <https://github.com/kcuzner/ezdaq>`__
I've never used the ADC before on the Teensy 3.1. I don't use the Teensy Cores HAL/Arduino Library because I find it more fun to twiddle the bits and write the makefiles myself. Of course, this also means that I don't always get a project working within 30 minutes.
The ADC on the Teensy 3.1 (or the Kinetis MK20DX256) is capable of doing 16-bit conversions at 400-ish ksps. It is also quite complex and can do conversions in many different ways. It is one of the larger and more configurable peripherals on the device, probably rivaled only by the USB module. The module does not come pre-calibrated and requires a calibration cycle to be performed before its accuracy will match that specified in the datasheet. My initialization code is as follows:
1//Enable ADC0 module
2SIM_SCGC6 |= SIM_SCGC6_ADC0_MASK;
3
4//Set up conversion precision and clock speed for calibration
5ADC0_CFG1 = ADC_CFG1_MODE(0x1) | ADC_CFG1_ADIV(0x1) | ADC_CFG1_ADICLK(0x3); //12 bit conversion, adc async clock, div by 2 (<3MHz)
6ADC0_CFG2 = ADC_CFG2_ADACKEN_MASK; //enable async clock
7
8//Enable hardware averaging and set up for calibration
9ADC0_SC3 = ADC_SC3_CAL_MASK | ADC_SC3_AVGS(0x3);
10while (ADC0_SC3 & ADC_SC3_CAL_MASK) { }
11if (ADC0_SC3 & ADC_SC3_CALF_MASK) //calibration failed. Quit now while we're ahead.
12 return;
13temp = ADC0_CLP0 + ADC0_CLP1 + ADC0_CLP2 + ADC0_CLP3 + ADC0_CLP4 + ADC0_CLPS;
14temp /= 2;
15temp |= 0x1000;
16ADC0_PG = temp;
17temp = ADC0_CLM0 + ADC0_CLM1 + ADC0_CLM2 + ADC0_CLM3 + ADC0_CLM4 + ADC0_CLMS;
18temp /= 2;
19temp |= 0x1000;
20ADC0_MG = temp;
21
22//Set clock speed for measurements (no division)
23ADC0_CFG1 = ADC_CFG1_MODE(0x1) | ADC_CFG1_ADICLK(0x3); //12 bit conversion, adc async clock, no divide
Following the recommendations in the datasheet, I selected a clock that would bring the ADC clock speed down to <4MHz and turned on hardware averaging before starting the calibration. The calibration is initiated by setting a flag in ADC0_SC3 and when completed, the calibration results will be found in the several ADC0_CL* registers. I'm not 100% certain how this calibration works, but I believe what it is doing is computing some values which will trim some value in the SAR logic (probably something in the internal DAC) in order to shift the converted values into spec.
One thing to note is that I did not end up using the 16-bit conversion capability. I was a little rushed and was put off by the fact that I could not get it to use the full 0-65535 dynamic range of a 16-bit result variable. It was more like 0-10000. This made figuring out my "volts-per-value" value a little difficult. However, the 12-bit mode gave me 0-4095 with no problems whatsoever. Perhaps I'll read a little further and figure out what is wrong with the way I was doing the 16-bit conversions, but for now 12 bits is more than sufficient. I'm just measuring some voltages.
Since I planned to measure the voltages coming off a Li-Ion battery, I needed to make sure I could handle the range of 3.0V-4.2V. Most of this is outside the Teensy's ADC range (max is 3.3V), so I had to make myself a resistor divider attenuator (with a parallel capacitor for added stability). It might have been better to use some sort of active circuit, but this is supposed to be a quick and dirty DAQ. I'll talk a little more about handling issues spawning from the use of this resistor divider in the section about the host software.
For this project I used my device-side USB driver software that I wrote in this project. Since we are gathering data quite slowly, I figured that a simple control transfer should be enough to handle the requisite bandwidth.
1static uint8_t tx_buffer[256];
2
3/**
4 * Endpoint 0 setup handler
5 */
6static void usb_endp0_handle_setup(setup_t* packet)
7{
8 const descriptor_entry_t* entry;
9 const uint8_t* data = NULL;
10 uint8_t data_length = 0;
11 uint32_t size = 0;
12 uint16_t *arryBuf = (uint16_t*)tx_buffer;
13 uint8_t i = 0;
14
15 switch(packet->wRequestAndType)
16 {
17...USB Protocol Stuff...
18 case 0x01c0: //get adc channel value (wIndex)
19 *((uint16_t*)tx_buffer) = adc_get_value(packet->wIndex);
20 data = tx_buffer;
21 data_length = 2;
22 break;
23 default:
24 goto stall;
25 }
26
27 //if we are sent here, we need to send some data
28 send:
29...Send Logic...
30
31 //if we make it here, we are not able to send data and have stalled
32 stall:
33...Stall logic...
34}
I added a control request (0x01) which uses the wIndex (not to be confused with the cleaning product) value to select a channel to read. The host software can now issue a vendor control request 0x01, setting the wIndex value accordingly, and get the raw value last read from a particular analog channel. In order to keep things easy, I labeled the analog channels using the same format as the standard Teensy 3.1 layout. Thus, wIndex 0 corresponds to A0, wIndex 1 corresponds to A1, and so forth. The adc_get_value function reads the last read ADC value for a particular channel. Sampling is done by the ADC continuously, so the USB read doesn't initiate a conversion or anything like that. It just reads what happened on the channel during the most recent conversion.
Since libusb is easy to use with Python, via PyUSB, I decided to write out the whole thing in Python. Originally I planned on some sort of fancy gui until I realized that it would far simpler just to output a CSV and use MATLAB or Excel to process the data. The software is simple enough that I can just put the entire thing here:
1#!/usr/bin/env python3
2
3# Python Host for EZDAQ
4# Kevin Cuzner
5#
6# Requires PyUSB
7
8import usb.core, usb.util
9import argparse, time, struct
10
11idVendor = 0x16c0
12idProduct = 0x05dc
13sManufacturer = 'kevincuzner.com'
14sProduct = 'EZDAQ'
15
16VOLTS_PER = 3.3/4096 # 3.3V reference is being used
17
18def find_device():
19 for dev in usb.core.find(find_all=True, idVendor=idVendor, idProduct=idProduct):
20 if usb.util.get_string(dev, dev.iManufacturer) == sManufacturer and \
21 usb.util.get_string(dev, dev.iProduct) == sProduct:
22 return dev
23
24def get_value(dev, channel):
25 rt = usb.util.build_request_type(usb.util.CTRL_IN, usb.util.CTRL_TYPE_VENDOR, usb.util.CTRL_RECIPIENT_DEVICE)
26 raw_data = dev.ctrl_transfer(rt, 0x01, wIndex=channel, data_or_wLength=256)
27 data = struct.unpack('H', raw_data)
28 return data[0] * VOLTS_PER;
29
30def get_values(dev, channels):
31 return [get_value(dev, ch) for ch in channels]
32
33def main():
34 # Parse arguments
35 parser = argparse.ArgumentParser(description='EZDAQ host software writing values to stdout in CSV format')
36 parser.add_argument('-t', '--time', help='Set time between samples', type=float, default=0.5)
37 parser.add_argument('-a', '--attenuation', help='Set channel attentuation level', type=float, nargs=2, default=[], action='append', metavar=('CHANNEL', 'ATTENUATION'))
38 parser.add_argument('channels', help='Channel number to record', type=int, nargs='+', choices=range(0, 10))
39 args = parser.parse_args()
40
41 # Set up attentuation dictionary
42 att = args.attenuation if len(args.attenuation) else [[ch, 1] for ch in args.channels]
43 att = dict([(l[0], l[1]) for l in att])
44 for ch in args.channels:
45 if ch not in att:
46 att[ch] = 1
47
48 # Perform data logging
49 dev = find_device()
50 if dev is None:
51 raise ValueError('No EZDAQ Found')
52 dev.set_configuration()
53 print(','.join(['Time']+['Channel ' + str(ch) for ch in args.channels]))
54 while True:
55 values = get_values(dev, args.channels)
56 print(','.join([str(time.time())] + [str(v[1] * (1/att[v[0]])) for v in zip(args.channels, values)]))
57 time.sleep(args.time)
58
59if __name__ == '__main__':
60 main()
Basically, I just use the argparse module to take some command line inputs, find the device using PyUSB, and spit out the requested channel values in a CSV format to stdout every so often.
In addition to simply displaying the data, the program also processes the raw ADC values into some useful voltage values. I contemplated doing this on the device, but it was simpler to configure if I didn't have to reflash it every time I wanted to make an adjustment. One thing this lets me do is a sort of calibration using the "attenuation" values that I put into the host. The idea with these values is to compensate for a voltage divider in front of the analog input in order so that I can measure higher voltages, even though the Teensy 3.1 only supports voltages up to 3.3V.
For example, if I plugged my 50%-ish resistor divider on channel A0 into 3.3V, I would run the following command:
1$ ./ezdaq 0
2Time,Channel 0
31467771464.9665403,1.7990478515625
4...
We now have 1.799 for the "voltage" seen at the pin with an attenuation factor of 1. If we divide 1.799 by 3.3 we get 0.545 for our attenuation value. Now we run the following to get our newly calibrated value:
1$ ./ezdaq -a 0 0.545 0
2Time,Channel 0
31467771571.2447994,3.301005232
4...
This process highlights an issue with using standard resistors. Unless the resistors are precision resistors, the values will not ever really match up very well. I used 4 1meg resistors to make two voltage dividers. One of them had about a 46% division and the other was close to 48%. Sure, those seem close, but in this circuit I needed to be accurate to at least 50mV. The difference between 46% and 48% is enough to throw this off. So, when doing something like this with trying to derive an input voltage after using an imprecise voltage divider, some form of calibration is definitely needed.
After hooking everything up and getting everything to run, it was fairly simple for me to take some two-channel measurements:
1$ ./ezdaq -t 5 -a 0 0.465 -a 1 0.477 0 1 > ~/Projects/AVR/the-project/test/charge.csv
This will dump the output of my program into the charge.csv file (which is measuring the charge cycle on the battery). I will get samples every 5 seconds. Later, I can use this data to make sure my circuit is working properly and observe its behavior over long periods of time. While crude, this quick and dirty DAQ solution works quite well for my purposes.
About two years ago I started working with the Teensy 3.1 (which uses a Freescale Kinetis ARM-Cortex microcontroller) and I was super impressed with the ARM processor, both for its power and relative simplicity (it is not simple...its just relatively simple for the amount of power you get for the cost IMO). Almost all my projects before that point had consisted of AVRs and PICs (I'm in the AVR camp now), but now ARM-based microcontrollers had become serious contenders for something that I could go to instead. I soon began working on a small development board project also involving some Freescale Kinetis microcontrollers since those are what I have become the most familiar with. Sadly, I have had little success since I have been trying to make a programmer myself (the official one is a minimum of $200). During the course of this project I came across a LOT of STM32 stuff and it seemed that it was actually quite easy to set up. Lots of the projects used the STM32 Discovery and similar dev boards, which are a great tools and provide an easy introduction to ARM microcontrollers. However, my interest is more towards doing very bare metal development. Like soldering the chip to a board and hooking it up to a programmer. Who needs any of that dev board stuff? For some reason I just find doing embedded development without a development board absolutely fascinating. Some people might interpret doing things this way as a form of masochism. Perhaps I should start seeing a doctor...
Having seen how common the STM32 family was (both in dev boards and in commercial products) and noting that they were similarly priced to the Freescale Kinetis series, I looked in to exactly what I would need to program these, saw that the stuff was cheap, and bought it. After receiving my parts and soldering everything up, I plugged everything into my computer and had a program running on the STM32 in a matter of hours. Contrast that to a year spent trying to program a Kinetis KL26 with only partial success.
This post is a complete step-by-step tutorial on getting an STM32 microcontroller up and running without using a single dev board (breakout boards don't count as dev boards for the purposes of this tutorial). I'm writing this because I could not find a standalone tutorial for doing this with an ARM microcontroller and I ended up having to piece it together bit by bit with a lot of googling. My objective is to walk you through the process of purchasing the appropriate parts, writing a small program, and uploading it to the microcontroller.
I make the following assumptions:
All code, makefiles, and configuration stuff can be found in the project repository on github. Project Repository: `https://github.com/kcuzner/stm32f103c8-blink <https://github.com/kcuzner/stm32f103c8-blink>`__
You will require the following materials:
I was able to acquire all of these parts for less than $20. Now, I did have stuff like the capacitors, led, resistor, and wires lying around in parts boxes, but those are quite cheap anyway.
Side note: Here is an excellent video by the EE guru Dave Jones on surface mount soldering if the prospect is less than palatable to you: https://www.youtube.com/watch?v=b9FC9fAlfQE
Above we decided to use the STM32F103C8 ARM Cortex-M3 microcontroller in a TQFP-48 package. This microcontroller has so many peripherals its no wonder its the one all over eBay. I could see this microcontroller easily satisfying the requirements for all of my projects. Among other things it has:
All this for ~$1.20/part no less! Of course, its like $6 on digikey, but for my purposes having an eBay-sourced part is just fine.
Ok, so when messing with any microcontroller we need to look at its datasheet to know where to plug stuff in. For almost all ARM Microcontrollers there will be no less than 2 datasheet-like documents you will need: The part datasheet and the family reference manual . The datasheet contains information such as the specific pinouts and electrical characteristics and the family reference manual contains the detailed information on how the microcontroller works (core and peripherals). These are both extremely important and will be indispensable for doing anything at all with one of these microcontrollers bare metal.
Find the STM32F103C8 datasheet and family reference manual here (datasheet is at the top of the page, reference manual is at the bottom): http://www.st.com/en/microcontrollers/stm32f103c8.html. They are also found in the "ref" folder of the repository.
After getting the datasheet we need to solder the microcontroller down to the breakout board so that we can start working with it on a standard breadboard. If you prefer to go build your own PCB and all that (I usually do actually) then do that instead of this. However, you will still need to know which pins to hook up.
On the pin diagram posted here you will find the highlighted pins of interest for hooking this thing up. We need the following pins at a minimum:
Below you will find a picture of my breakout board. I soldered a couple extra pins since I want to experiment with USB.
Very important: You may notice that I have some little tiny capacitors (0.1uF) soldered between the power pins (the one on the top is the most visible in the picture). You need to mount your capacitors between each pair of VDD/VSS pins (including AVDD/AVSS) . How you do this is completely up to you, but it must be done and *they should be rather close to the microcontroller itself* . If you don't it is entirely possible that when the microcontroller first turns on and powers up (specifically at the first falling edge of the internal clock cycle), the inductance created by the flying power wires we have will create a voltage spike that will either cause a malfunction or damage. I've broken microcontrollers by forgetting the decoupling caps and I'm not eager to do it again.
Don't do this with the programmer plugged in.
On the right you will see my STLinkV2 clone which I will use for this project. Barely visible is the pinout. We will need the following pins connected from the programmer onto our breadboard. These come off the header on the non-USB end of the programmer. Pinouts may vary. Double check your programmer!
You may notice in the above picture that I have an IDC cable coming off my programmer rather than the dupont wires. I borrowed the cable from my AVR USBASP programmer since it was more available at the time rather than finding the dupont cables that came with the STLinkV2.
Next, we need to connect the following pins on the breadboard:
Here is my breadboard setup:
Project Repository: `https://github.com/kcuzner/stm32f103c8-blink <https://github.com/kcuzner/stm32f103c8-blink>`__
Since we are going to write a program, we need the headers. These are part of the STM32CubeF1 library found here.
Visit the page and download the STM32CubeF1 zip file. It will ask for an email address. If you really don't want to give them your email address, the necessary headers can be found in the project github repository.
Alternately, just clone the repository. You'll miss all the fun of poking around the zip file, but sometimes doing less work is better.
The STM32CubeF1 zip file contains several components which are designed to help people get started quickly when programming STM32s. This is one thing that ST definitely does better than Freescale. It was so difficult to find the headers for the Kinetis microcontrollers that almost gave up at that point. Anyway, inside the zip file we are only interested in the following:
I copied all the files referenced above to various places in my project structure so they could be compiled into the final program. Please visit the repository for the exact locations and such. My objective with this tutorial isn't really to talk too much about project structure, and so I think that's best left as an exercise for the reader.
We need to be able to compile the program and flash the resulting binary file to the microcontroller. In order to do this, we will require the following programs to be installed:
Once you have installed all of the above programs, you should be good to go for ARM development. As for an editor or IDE, I use vim. You can use whatever. It doesn't matter really.
Ok, so we need to write a program for this microcontroller. We are going to simply toggle on and off a GPIO pin (PB0). After reset, the processor uses the internal RC oscillator as its system clock and so it runs at a reasonable 8MHz or so I believe. There are a few steps that we need to go through in order to actually write to the GPIO, however:
Here is my super-simple main program that does all of the above:
1/**
2 * STM32F103C8 Blink Demonstration
3 *
4 * Kevin Cuzner
5 */
6
7#include "stm32f1xx.h"
8
9int main(void)
10{
11 //Step 1: Enable the clock to PORT B
12 RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
13
14 //Step 2: Change PB0's mode to 0x3 (output) and cfg to 0x0 (push-pull)
15 GPIOB->CRL = GPIO_CRL_MODE0_0 | GPIO_CRL_MODE0_1;
16
17 while (1)
18 {
19 //Step 3: Set PB0 high
20 GPIOB->BSRR = GPIO_BSRR_BS0;
21 for (uint16_t i = 0; i != 0xffff; i++) { }
22 //Step 4: Reset PB0 low
23 GPIOB->BSRR = GPIO_BSRR_BR0;
24 for (uint16_t i = 0; i != 0xffff; i++) { }
25 }
26
27 return 0;
28}
If we turn to our trusty family reference manual, we will see that the clock gating functionality is located in the Reset and Clock Control (RCC) module (section 7 of the manual). The gates to the various peripherals are sorted by the exact data bus they are connected to and have appropriately named registers. The PORTB module is located on the APB2 bus, and so we use the RCC->APB2ENR to turn on the clock for port B (section 7.3.7 of the manual).
The GPIO block is documented in section 9. We first talk to the low control register (CRL) which controls pins 0-7 of the 16-pin port. There are 4 bits per pin which describe the configuration grouped in to two 2-bit (see how many "2" sounding words I had there?) sections: The Mode and Configuration. The Mode sets the analog/input/output state and the Configuration handles the specifics of the particular mode. We have chosen output (Mode is 0b11) and the 50MHZ-capable output mode (Cfg is 0b00). I'm not fully sure what the 50MHz refers to yet, so I just kept it at 50MHz because that was the default value.
After talking to the CRL, we get to talk to the BSRR register. This register allows us to write a "1" to a bit in the register in order to either set or reset the pin's output value. We start by writing to the BS0 bit to set PB0 high and then writing to the BR0 bit to reset PB0 low. Pretty straightfoward.
It's not a complicated program. Half the battle is knowing where all the pieces fit. The STM32F1Cube zip file contains some examples which could prove quite revealing into the specifics on using the various peripherals on the device. In fact, it includes an entire hardware abstraction layer (HAL) which you could compile into your program if you wanted to. However, I have heard some bad things about it from a software engineering perspective (apparently it's badly written and quite ugly). I'm sure it works, though.
So, the next step is to compile the program. See the makefile in the repository. Basically what we are going to do is first compile the main source file, the assembly file we pulled in from the STM32Cube library, and the C file we pulled in from the STM32Cube library. We will then link them using the linker script from the STM32Cube and then dump the output into a binary file.
1# Makefile for the STM32F103C8 blink program
2#
3# Kevin Cuzner
4#
5
6PROJECT = blink
7
8# Project Structure
9SRCDIR = src
10COMDIR = common
11BINDIR = bin
12OBJDIR = obj
13INCDIR = include
14
15# Project target
16CPU = cortex-m3
17
18# Sources
19SRC = $(wildcard $(SRCDIR)/*.c) $(wildcard $(COMDIR)/*.c)
20ASM = $(wildcard $(SRCDIR)/*.s) $(wildcard $(COMDIR)/*.s)
21
22# Include directories
23INCLUDE = -I$(INCDIR) -Icmsis
24
25# Linker
26LSCRIPT = STM32F103X8_FLASH.ld
27
28# C Flags
29GCFLAGS = -Wall -fno-common -mthumb -mcpu=$(CPU) -DSTM32F103xB --specs=nosys.specs -g -Wa,-ahlms=$(addprefix $(OBJDIR)/,$(notdir $(<:.c=.lst)))
30GCFLAGS += $(INCLUDE)
31LDFLAGS += -T$(LSCRIPT) -mthumb -mcpu=$(CPU) --specs=nosys.specs
32ASFLAGS += -mcpu=$(CPU)
33
34# Flashing
35OCDFLAGS = -f /usr/share/openocd/scripts/interface/stlink-v2.cfg \
36 -f /usr/share/openocd/scripts/target/stm32f1x.cfg \
37 -f openocd.cfg
38
39# Tools
40CC = arm-none-eabi-gcc
41AS = arm-none-eabi-as
42AR = arm-none-eabi-ar
43LD = arm-none-eabi-ld
44OBJCOPY = arm-none-eabi-objcopy
45SIZE = arm-none-eabi-size
46OBJDUMP = arm-none-eabi-objdump
47OCD = openocd
48
49RM = rm -rf
50
51## Build process
52
53OBJ := $(addprefix $(OBJDIR)/,$(notdir $(SRC:.c=.o)))
54OBJ += $(addprefix $(OBJDIR)/,$(notdir $(ASM:.s=.o)))
55
56
57all:: $(BINDIR)/$(PROJECT).bin
58
59Build: $(BINDIR)/$(PROJECT).bin
60
61install: $(BINDIR)/$(PROJECT).bin
62 $(OCD) $(OCDFLAGS)
63
64$(BINDIR)/$(PROJECT).hex: $(BINDIR)/$(PROJECT).elf
65 $(OBJCOPY) -R .stack -O ihex $(BINDIR)/$(PROJECT).elf $(BINDIR)/$(PROJECT).hex
66
67$(BINDIR)/$(PROJECT).bin: $(BINDIR)/$(PROJECT).elf
68 $(OBJCOPY) -R .stack -O binary $(BINDIR)/$(PROJECT).elf $(BINDIR)/$(PROJECT).bin
69
70$(BINDIR)/$(PROJECT).elf: $(OBJ)
71 @mkdir -p $(dir $@)
72 $(CC) $(OBJ) $(LDFLAGS) -o $(BINDIR)/$(PROJECT).elf
73 $(OBJDUMP) -D $(BINDIR)/$(PROJECT).elf > $(BINDIR)/$(PROJECT).lst
74 $(SIZE) $(BINDIR)/$(PROJECT).elf
75
76macros:
77 $(CC) $(GCFLAGS) -dM -E - < /dev/null
78
79cleanBuild: clean
80
81clean:
82 $(RM) $(BINDIR)
83 $(RM) $(OBJDIR)
84
85# Compilation
86$(OBJDIR)/%.o: $(SRCDIR)/%.c
87 @mkdir -p $(dir $@)
88 $(CC) $(GCFLAGS) -c $< -o $@
89
90$(OBJDIR)/%.o: $(SRCDIR)/%.s
91 @mkdir -p $(dir $@)
92 $(AS) $(ASFLAGS) -o $@ $<
93
94
95$(OBJDIR)/%.o: $(COMDIR)/%.c
96 @mkdir -p $(dir $@)
97 $(CC) $(GCFLAGS) -c $< -o $@
98
99$(OBJDIR)/%.o: $(COMDIR)/%.s
100 @mkdir -p $(dir $@)
101 $(AS) $(ASFLAGS) -o $@ $<
The result of this makefile is that it will create a file called "bin/blink.bin" which contains our compiled program. We can then flash this to our microcontroller using openocd.
Source for this step: https://github.com/rogerclarkmelbourne/Arduino_STM32/wiki/Programming-an-STM32F103XXX-with-a-generic-%22ST-Link-V2%22-programmer-from-Linux
This is the very last step. We get to do some openocd configuration. Firstly, we need to write a small configuration script that will tell openocd how to flash our program. Here it is:
1# Configuration for flashing the blink program
2init
3reset halt
4flash write_image erase bin/blink.bin 0x08000000
5reset run
6shutdown
Firstly, we init and halt the processor (reset halt). When the processor is first powered up, it is going to be running whatever program was previously flashed onto the microcontroller. We want to stop this execution before we overwrite the flash. Next we execute "flash write_image erase" which will first erase the flash memory (if needed) and then write our program to it. After writing the program, we then tell the processor to execute the program we just flashed (reset run) and we shutdown openocd.
Now, openocd requires knowledge of a few things. It first needs to know what programmer to use. Next, it needs to know what device is attached to the programmer. Both of these requirements must be satisfied before we can run our script above. We know that we have an stlinkv2 for a programmer and an stm32f1xx attached on the other end. It turns out that openocd actually comes with configuration files for these. On my installation these are located at "/usr/share/openocd/scripts/interface/stlink-v2.cfg" and "/usr/share/openocd/scripts/target/stm32f1x.cfg", respectively. We can execute all three files (stlink, stm32f1, and our flashing routine (which I have named "openocd.cfg")) with openocd as follows:
1openocd -f /usr/share/openocd/scripts/interface/stlink-v2.cfg \
2 -f /usr/share/openocd/scripts/target/stm32f1x.cfg \
3 -f openocd.cfg
So, small sidenote: If we left off the "shutdown" command, openocd would actually continue running in "daemon" mode, listening for connections to it. If you wanted to use gdb to interact with the program running on the microcontroller, that is what you would use to do it. You would tell gdb that there is a "remote target" at port 3333 (or something like that). Openocd will be listening at that port and so when gdb starts talking to it and trying to issue debug commands, openocd will translate those through the STLinkV2 and send back the translated responses from the microcontroller. Isn't that sick?
In the makefile earlier, I actually made this the "install" target, so running "sudo make install" will actually flash the microcontroller. Here is my output from that command for your reference:
1kcuzner@kcuzner-laptop:~/Projects/ARM/stm32f103-blink$ sudo make install
2arm-none-eabi-gcc -Wall -fno-common -mthumb -mcpu=cortex-m3 -DSTM32F103xB --specs=nosys.specs -g -Wa,-ahlms=obj/system_stm32f1xx.lst -Iinclude -Icmsis -c src/system_stm32f1xx.c -o obj/system_stm32f1xx.o
3arm-none-eabi-gcc -Wall -fno-common -mthumb -mcpu=cortex-m3 -DSTM32F103xB --specs=nosys.specs -g -Wa,-ahlms=obj/main.lst -Iinclude -Icmsis -c src/main.c -o obj/main.o
4arm-none-eabi-as -mcpu=cortex-m3 -o obj/startup_stm32f103x6.o src/startup_stm32f103x6.s
5arm-none-eabi-gcc obj/system_stm32f1xx.o obj/main.o obj/startup_stm32f103x6.o -TSTM32F103X8_FLASH.ld -mthumb -mcpu=cortex-m3 --specs=nosys.specs -o bin/blink.elf
6arm-none-eabi-objdump -D bin/blink.elf > bin/blink.lst
7arm-none-eabi-size bin/blink.elf
8 text data bss dec hex filename
9 1756 1092 1564 4412 113c bin/blink.elf
10arm-none-eabi-objcopy -R .stack -O binary bin/blink.elf bin/blink.bin
11openocd -f /usr/share/openocd/scripts/interface/stlink-v2.cfg -f /usr/share/openocd/scripts/target/stm32f1x.cfg -f openocd.cfg
12Open On-Chip Debugger 0.9.0 (2016-04-27-23:18)
13Licensed under GNU GPL v2
14For bug reports, read
15 http://openocd.org/doc/doxygen/bugs.html
16Info : auto-selecting first available session transport "hla_swd". To override use 'transport select <transport>'.
17Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD
18adapter speed: 1000 kHz
19adapter_nsrst_delay: 100
20none separate
21Info : Unable to match requested speed 1000 kHz, using 950 kHz
22Info : Unable to match requested speed 1000 kHz, using 950 kHz
23Info : clock speed 950 kHz
24Info : STLINK v2 JTAG v17 API v2 SWIM v4 VID 0x0483 PID 0x3748
25Info : using stlink api v2
26Info : Target voltage: 3.335870
27Info : stm32f1x.cpu: hardware has 6 breakpoints, 4 watchpoints
28target state: halted
29target halted due to debug-request, current mode: Thread
30xPSR: 0x01000000 pc: 0x08000380 msp: 0x20004ffc
31auto erase enabled
32Info : device id = 0x20036410
33Info : flash size = 64kbytes
34target state: halted
35target halted due to breakpoint, current mode: Thread
36xPSR: 0x61000000 pc: 0x2000003a msp: 0x20004ffc
37wrote 3072 bytes from file bin/blink.bin in 0.249272s (12.035 KiB/s)
38shutdown command invoked
39kcuzner@kcuzner-laptop:~/Projects/ARM/stm32f103-blink$
After doing that I saw the following awesomeness:
Wooo!!! The LED blinks! At this point, you have successfully flashed an ARM Cortex-M3 microcontroller with little more than a cheap programmer from eBay, a breakout board, and a few stray wires. Feel happy about yourself.
For me, this marks the end of one journey and the beginning of another. I can now feel free to experiment with ARM microcontrollers without having to worry about ruining a nice shiny development board. I can buy a obscenely powerful $1 STM32 microcontroller from eBay and put it into any project I want. If I were to try to do that with AVRs, I would be stuck with the ultra-low-end 8-pin ATTiny13A since that's about it for ~$1 AVR eBay offerings (don't worry...I've got plenty of ATMega328PB's...though they weren't $1). I sincerely hope that you found this tutorial useful and that it might serve as a springboard for doing your own dev board-free ARM development.
If you have any questions or comments (or want to let me know about any errors I may have made), let me know in the comments section here. I will try my best to help you out, although I can't always find the time to address every issue.