How to flash and write a program on a nucleo F401RE using libopencm3
- Install git and clone this repository (see git conference/tutorial)
- You need a ssh key on your account to clone libopencm3
git clone --recurse-submodules URL_OF_THE_REPO- We will now assume your working directory is this folder ! (
cdinto it please)
- Install the necessary tools to cross compile and flash your code
sudo pacman -S arm-none-eabi-gcc arm-none-eabi-newlib openocdsudo apt install gcc-arm-none-eabi libnewlib-arm-none-eabi openocd
- We now have to build the library that reads and writes in the STM registers for us : libopencm3!
cd hal_common/libopencm3makecd -
- Give yourself the rights to access the port to talk to the card
make install_udev
- Now we can build and flash the program with a simple:
make
The magic of everything that just happened will kindly be explained by a robotronik member :)
Gives you some hints about what file does what to know where to start your exploration of the project.
Makefilecontains the recipe to build our project and flash it also specifies where our files are hiddencompile_command.jsonfile contains the location of all files for your modern text editor/IDEREADME.mdis where you actually areinstall_udev.shis a script to give access to the port used in the Step by stepdoxygenanddoxygenConfare used to generate documentation for the project (see Doxygen part for more information)hal_commoncontains thelinker_scrpitsand alllibopencm3fileslowlevelcontains the code for your peripheral as Clock, GPIOs, Timers with the .h inlowlevel/includemainTest.cis the main program
Step by step explanation of the code. At any moment you may refer to the documentation (see Doxygen part)
Only mainTest.c, lowlevel/*.c and lowlevel/include/*.h will be edited
For all example it is necessary to use the libopencm3 f4 documentation (I recommend to look up every function used to become Familiar with the definition and parameters in libopencm3)
Documentation libopencm3 STM32F4
You will first find a header with a brief description of the peripheral the date and the author, feel free to contact them if you encounter any problem
A section with include from std, libopencm3, lowlevel/include
A section with definition where all peripheral information are chosen/edited and all constant
Then a section with all prototypes for function with a brief description of the function and description of all parameters
Including all needed .h files (in lowlevel/include)
Function starting with _ are not to be called in the main program.
The first function is normally a setup used to initialize the peripheral
Including all peripheral .h
In main we start with all setup and initialization
The main code that most probably loop on itself
In all libopencm3 projects you start with the clock. It is normally a very simple module for the system_clock and implementing delay.
Be aware of each μC specific architecture the function may vary from a μC to another (e. g. F3 to F4)
In setup, it is important to know the core frequency of your μC (84 MHz for the STM32F4)
It is almost the same for all projects. You can probably copy paste it and only change the frequency.
It implements all temporal functions of the uC and as to be setup first and enabled for all needed peripheral. We also implement a delay for the uC.
GPIO are all the input/ output of the μC.
They can be used normally in 4 possible mode:
- Digital Input
- Digital Output
- Alternate Function
- Analog Pin
They are 3 setup methods that cover all purposes.
The simplest one is to use a pin as a digital I/O, the setup is then:
- Enable Clock on Port (definition as RCC_GPIOX with X the port)
- Setup mode as an input or output with a pull-up, pull-down or neither
- Finally, only for output, configuration of the output (GPIO output type, GPIO pin speed)
Pins on a μC are limited therefor multiple function are used on any pin, the function are for example timer controlled pin, communication pin (uart, spi, etc.)
All Information are found in the alternate function mapping in the datasheet (see Hardware Documentation)
Same setup as Digital I/O with the mode AF then set alternate function to correct AF (number in the mapping)
We will use clock and GPIO, if you run into an issue or want to debug the code further see Debug with uart
Example is already done on the master branch. On your local branch you can delete lowlevel/gpio.c, lowlevel/led.c, lowlevel/include/led.h.
rm lowlevel/gpio.c lowlevel/led.c lowlevel/include/led.h
You also need to empty the mainTest.c file. Please replace the content to:
#include "clock.h"
#include "uart.h"
#include "led.h"
int main() {
//setup
clock_setup();
//Your code here
}
-
Let's setup the clock in main
clock_setup() -
Create or Edit gpio.c
touch lowlevel/gpio.c -
For GPIO we only need a setup function
_gpio_setup_pin- Include
gpio.hingpio.cand copy your prototypes
in
gpio.cwe edit_gpio_setup_pin:-
Parameters are rcc_clken, port, pin, mode
-
Clock enable is done via
rcc_periph_clock_enablercc_periph_clock_enable(rcc_clken) -
Setup mode via
gpio_mode_setupgpio_mode_setup(port,mode,GPIO_PUPD_NONE,pin)- GPIO_PUPD_NONE stands for: neither Pull up or Pull down
-
If we want to setup an output, we need to configure output via
gpio_set_output_options-
In the documentation we find a definition of possible mode: GPIO_MODE_INPUT, GPIO_MODE_OUTPUT, GPIO_MODE_AF, GPIO_MODE_ANALOG
-
We want to check for an output and then setup output
gpio_set_output_options(port, GPIO_OTYPE_PP,GPIO_OSPEED_50MHZ, pin)-
Type is push-pull or open drain, we normally use GPIO_OTYPE_PP
-
Speed is normally GPIO_OSPEED_50MHZ
-
- Include
-
We want a LED module, let's create it
touch lowlevel/led.ctouch lowlevel/include/led.h
We want to setup the led and have an user function to blink it
-
We will use all we wrote in
gpioso we have to include it#include "gpio.h" -
Prototypes in
led.hvoid led_setup()void led_blink()
We want to use the onboard LED PA5
-
Definitions
-
port is A
#define LED_PORT GPIOA -
pin is 5
#define LED_PIN GPIO5 -
clken should be RCC for our port (A)
#define LED_GPIO_RCC RCC_GPIOA
-
-
Write
led_setupinled.cusing_gpio_setup_pinPA5 should be an output to control LED
_gpio_setup_pin(LED_GPIO_RCC, LED_PORT, LED_PIN, GPIO_MODE_OUTPUT) -
Write
led_blinkusing libopencm3In libopencm3 operation on digital I/O are: clear, set, toggle, get.
We want to toggle LED state by calling the function
gpio_toggle(LED_PORT, LED_PIN) -
Write a program in main (
mainTest.c)For example a blinking led at a given frequency/delay
led_setup(); while(1){ led_blink(); delay_ms(1000); } -
Party
A timer is a sequential circuit that counts by increasing or decreasing a register. It resets and generates a event, often used as an interrupt, when said register exceeds or falls behind a predefined value.
We can then use said event to control an output channel (for example a pin as an alternate function).
So there are 3 functions we want in our timer module:
- Setup/configure timer
- Start timer
- Setup/configure output channel
A STM32F4 as a lot of timer with different possibilities. For illustration purpose we will only use Timer 1 and 2, because they are the most potent one.
The most important parameters for a timer are the prescaler and the period.
- The prescaler transforms the clock frequency to a lower frequency easier to apprehend for the user. Most of the time we for example divide the clock to make a 1 us tick
- The period is then the temporal distance between two equal values
- As the timer is also a peripheral we need to enable the clock
- Choose a mode for the Timer (it will not be explained in details as it is in most case not changed from example 2)
- For advanced Timer, you need to allow breaks
- Set the prescaler
- Choose the starting value
- Enable preload
- Choose if the timer should run continuously
- Set the period
After the setup we need to start the timer.
- Generate a update event
- Enable counter
A given Timer as most of the time multiple possible output channels. Therefore it is important in the choice of the timer to check the timer, but the output channel too.
The most important parameters for the output channel are its purpose ( mode ) and its value
- The mode are defined use in the μC as PWM (see timer_set_oc_mode in libopencm3 documentation)
- The value defines the value of the timer on which the event takes place
We first have to setup the GPIO in alternate_function mode
-
See GPIO for the 4 needed steps
It is important to check the AF number for your chosen timer
After the GPIO is fully setup we can setup the output channel
-
Disable the output channel to not generate unforeseen event
-
Choose the mode for your output channel
-
Enable preload
-
Choose the value
-
Enable the output channel
We will use clock, gpio and timer, if you run in an issue or want to debug the code further see Debug with uart
Example is already done on master branch. On your local branch you can delete lowlevel/timer.c, lowlevel/pwm.c, lowlevel/include/pwm.h.
You can also use gpio from the previous example
rm lowlevel/timer.c lowlevel/pwm.c lowlevel/include/pwm.h
You also need to empty the mainTest.c file. Please replace the content to:
#include "clock.h"
#include "uart.h"
#include "led.h"
#include "pwm.h"
int main() {
//setup
clock_setup();
//Your code here
}
-
Let's setup the clock in main
clock_setup() -
Create or Edit
timer.ctouch lowlevel/timer.c -
For Timer we first need a setup function
_timer_setupReminder: include your header in .c file and copy prototypes
in
timer.cin_timer_setup:-
Parameters are rcc_clken, timer_peripheral, prescaler, period
-
Clock enable is done via
rcc_periph_clock_enablercc_periph_clock_enable(rcc_clken) -
Set timer mode via
timer_set_modetimer_set_mode(timer_peripheral, TIM_CR1_CKD_CK_INT, TIM_CR1_CMS_EDGE, TIM_CR1_DIR_UP)- TIM_CR1_CKD_CK_INT means no division ratio for the prescaled timer
- TIM_CR1_CMS_EDGE means aligned on edge
- TIM_CR1_DIR_UP means the timer is counting up
Following command are really easy to use with the documentation, do it yourself
-
For advanced timer, enable break via
timer_enable_break_main_output -
Set prescaler via
timer_set_prescaler -
Set the starting value either as a parameter or 0 via
timer_set_repetition_counter -
Enable preload via
timer_enable_preload -
We choose to count in continuous mode via
timer_continuous_mode -
Set period via
timer_set_period
-
-
Then we need a start timer function
_timer_start-
Generate the update event via
timer_generate_event(timer_peripheral, TIM_EGR_UG)- TIM_EGR_UG is the update event
-
Enable counter via
timer_enable_counter
-
-
Function to setup output channel
_timer_setup_output_cThey can be multiple channel active for one timer so this function could be called more than one time per timeroc_id, oc_mode, oc_value are specific to the function of your timer
Because we are using a pin we need to disable it first and enable it afterward
-
Disable oc via
timer_disable_oc_output -
Set mode via
timer_set_oc_mode -
Enable preload via
timer_enable_oc_preload -
Set oc value, value that create an event via
timer_set_oc_value -
Enable oc via
timer_enable_oc_output
-
-
Add a function to
gpioto setup alternate functionin
_gpio_setup_pin_afcopy_gpio_setup_pinbut changegpio_mode_setupto this:gpio_mode_setup(gpio_port,GPIO_MODE_AF,GPIO_PUPD_NONE,gpio_pin)and at the end add
gpio_set_afwith the new parameter gpio_altfungpio_set_af(gpio_port, gpio_altfun, gpio_pin) -
We want a PWM module, let's create it
touch lowlevel/pwm.ctouch lowlevel/include/pwm.h
We want to setup the PWM and have an user function to change the pulse width
Reminder: we use gpio and timer in pwm so include it
-
Prototypes in
pwm.hvoid pwm_setup()void pwm_set_pulse_width(uint32_t pulse_width)
Looking at the alternate function mapping we choose TIM1 and PA10. PA10 is the 3rd channel of TIM1 ( TIM_OC3 ). And OC from TIM1 is the alternate function 1 from PA10 ( GPIO_AF1 ).
We choose a prescaler of 84 to get tick in us and a period of 20 000 us = 20 ms <=> 50 Hz. Imagine for example the driving of a servo.
We want to make a PWM and gladly there are already PWM mode on STM32F4: TIM_OCM_PWM1 (High then Low) and TIM_OCM_PWM2 (Low then High)
-
Definitions
#define PWM_PRESCALE (84) #define PWM_PERIOD (20000) #define PWM_TIM TIM1 #define PWM_TIM_RCC RCC_TIM1 #define PWM_GPIO_RCC_EN RCC_GPIOA #define PWM_PORT_EN GPIOA #define PWM_PIN_EN GPIO10 #define PWM_AF GPIO_AF1 #define PWM_OC_ID TIM_OC3 #define PWM_OC_MODE TIM_OCM_PWM1 -
Write
pwm_setupinpwm.cusinggpioandtimerReminder: include your header in .c file and copy prototypes
-
Setup the timer
_timer_setup(PWM_TIM_RCC, PWM_TIM, PWM_PRESCALE, PWM_PERIOD) -
Setup the output pin
_gpio_setup_pin_af(PWM_GPIO_RCC_EN, PWM_PORT_EN, PWM_PIN_EN, PWM_AF) -
Setup the output channel with a default value of 0
_timer_setup_output_c(PWM_TIM, PWM_OC_ID, PWM_OC_MODE, 0) -
Start the timer
_timer_start(PWM_TIM)
-
-
Write
pwm_set_pulse_widthchanging the oc_valuetimer_set_oc_value(PWM_TIM, PWM_OC_ID, pulse_width) -
Write a program in main
Varying the pulse width in time
pwm_setup(); uint32_t pw = 10; while (1) { pw = (pw+100)%20000; pwm_set_pulse_width(pw); delay_ms(50); }Check the result on an oscilloscope looking at pin PA10 (D2)
-
Party Harder
Bonus: Using a timer enables to do anything in the main, parallel to the timer. We can for example imagine a modification of example 1 using a timer so we can execute code while the LED is blinking.
- You first need to enable the clock SYSCFG which will handle the EXTI(for external interrupt). The idea is that your GPIO input will change state and when this change happens it will raise a flag that is a signal for the CPU to say hey something happened there. And that is the EXTI peripheral that handles this signal. We have to link the exti line with our GPIO.
- Write
exti_setup(usercc_periph_clock_enable)
- The function
_limit_switch_initwill be our universal function to initialize limit switches (or any exti interrupt from GPIO).
-
Disable requests to avoid false activation during setup with
exti_disable_request -
Select which GPIO port will connect to exti number something. All pins of the same number are multiplexed into one exti so for exemple GPIO A1;B1;C1 all all connect to EXTI_1
exti_select_source -
Now we can choose which type of event on the exti line will actually raise the flag to notify an event happened, it could be a rising edge, a falling one or maybe we want both to trigger the flag.
exti_set_trigger -
Now we have to enable the EXTI interrupt
exti_enable_requestand enable the entry that will point and tell the µC what code (the interrupt routine= a function) should be executed when it sees the risen flag(this information is contained in the NVIC for nested vector interrupt control)nvic_enable_irq.
- We will now write a function to initialize the interrupts coming from the
blue button (on PC13). It is the function
button_switch_initWe have to initialize the GPIO as an input as we did before.
- Start the clock of the port
rcc_periph_clock_enable - Setup the port as input
gpio_mode_setup - Use
_limit_switch_initto plug the exti line with the GPIO - Choose the priority of interrupts with
nvic_set_priority
I hope you understood everything we have done before because we will need it in this example. The difference between this example and the previous one (EXTI) is that exti are interrupt from a given GPIO and this time we want an interruption only based on a timer.
The scenario we are proposing in this example is as follow: We want to call a function named routine (for example the acquisition procedure of a sensor) every 500 ms. Therefore we want to link a timer to an ISR (Interrupt Service Routine). In our routine we will print something on the debug UART and toggle a LED.
We will assume your main is empty and we will write in intime.c and intime.h.
-
Let's setup all needed peripheral in
main-
in
main()clock_setup(); -
add a test
void test_timer_interrupt() -
we want to write on the debug uart and toggle a led in the routine, so setup uart and the LED PIN (PA5)
void test_timer_interrupt(){ uart_setup(); _gpio_setup_pin(RCC_GPIOA,GPIOA,GPIO5,GPIO_MODE_OUTPUT); timer_setup_interrupt(); while (1){ } } -
call the test in
main()
-
-
Create or edit
intime.candintime.htouch lowlevel/intime.c lowlevel/include/intime.h -
In
intime.hall definitions:-
include
#include <libopencm3/stm32/timer.h> #include <libopencm3/cm3/nvic.h> #include "timer.h" #include "uart.h" //only needed to print on uart in the routine -
We want a period of 500ms
#define TIM_PRESCALE (84) #define TIM_PERIOD (500000) -
We will use timer 4 and output channel 1
#define TIM_RCC RCC_TIM4 #define TIM TIM4 #define TIM_OC_ID TIM_OC1
We need some special parameter for the OC as follow
-
The OC mode is frozen because we don't have any connection to any GPIO
#define TIM_OC_MODE TIM_OCM_FROZEN -
We want to make interruption on the timer 4 so we have to enable the interrupt controller NVIC (Nested Vector Interrupt Controller).
#define TIM_NVIC NVIC_TIM4_IRQ -
For timers we need to set other register to enable interruption on the channel 1. in the DIER (DMA/Interrupt Enable Register) we need to to enable interruption (IE) for the type CC (Capture/Compare) =>
DIER_CC1ER.#define TIM_DIER_CCIE TIM_DIER_CC1IE -
Now we need to identify with a status register (SR) the given interruption over the interrupt flag (IF) of type CC and for channel 1 =>
SR_CC1IF.#define TIM_SR_CCIF TIM_SR_CC1IF
-
-
We will need two functions for this example a setup function
timer_setup_interruptand the ISR with exactly this nametim4_isrwith the following prototype:void timer_setup_interrupt(); void tim4_isr(); -
In
intime.cthe setup function-
Setup the timer and oc using the functions written in
timer_timer_setup(TIM_RCC,TIM,TIM_PRESCALE,TIM_PERIOD); _timer_setup_output_c(TIM,TIM_OC_ID,TIM_OC_MODE,0); -
Enable interrupt request (IRQ) on a NVIC level
nvic_enable_irq(TIM_NVIC); -
Enable IRQ on a Timer level for OC1
timer_enable_irq(TIM, TIM_DIER_CCIE); -
Start the timer
_timer_start(TIM);
-
-
the ISR in
tim4_isr()-
Check the exact interruption over the flag
if(timer_get_flag(TIM, TIM_SR_CCIF)) -
Write the routine (Here write on the debug uart and toggle led, can be anything you want)
//do anything fprintf(stderr,"interrupt from timer\n"); gpio_toggle(GPIOA,GPIO5); -
Clear the flag to reset status
timer_clear_flag(TIM, TIM_SR_CCIF);
-
-
You are now intimate with timer, Party Very Hard
Bonus: Using only one timer we can only have one period but we can have multiple routine called per period. Using multiple OC and modifying the oc_value of each channel, we can schedule some routine every period.
Setup different OC in timer_setup_interrupt() and check the different flags in the ISR.
Reminder: To clone the submodule in the same time, use git clone --recurse-submodules, then don't forget to build libopencm3. You need to execute make install_udev one time to add the permission to flash.
If you forgot to clone with submodule just run git submodule update --init --recursive
To compile and flash you need gcc-arm-none-eabi, st-link and openocd
To build: make mainTest.elf
To flash: make mainTest.flash
To clean: make clean
You can access the documentation generated from the code with doxygen (see the doxygen paragraph to generate it) in the doxygen/html or latex.
μC used: STM32F401RE on a Nucleo-64 board, main doc:
- Reference Manual STM32F401 line, doc RM0368, 847 pages
- STM32F401xD/xE datasheet, 135 pages
- Alternate function mapping, p45, table 9
- Reference STM Nucleo-64 Board, doc UM1724, 69 pages
Generating the documentation with doxygen: --> install doxygen on your system
--> run doxygen doxygenConf from the project directory
--> the documentation can then be read from doxygen/html/index.html in a browser
--> to generate the pdf for the github (or for yourself !) go into the latex directory cd doxygen/latex and run make.
Note: You must have a latex distribution on your computer that has pdflatex command.
--> to update the compile_command.json you can use bear (available in the AUR)
--> run make clean
--> run bear -- /compilation command/
--> install picocom
--> find your card ls /dev . It should be /dev/ttyACM0
--> run picocom with picocom -b 9600 /dev/ttyACM0
--> in main setup uart (uart_setp())
--> in code use fprintf(stderr,message) to debug
-
Tabs are spaces = 4
-
Column = 80
-
Brackets :
def peripheral_action_subjectofaction(params){ code... some more code... } -
Function naming
- _ is the separator
- peripheral_action_subjectofaction
- function starting with _ are private and should not be called in high level code
-
Variables
- my_var
-
Documentation using doxygen
- comment the function interface in the .h files (javadoc like)
- detail the function in .c files
-
We envision three levels for the code :
- lowlevel functions that must be as general as possible to setup the hardware config (ex: timer functions)
- lowlevel modules with the functions called by the user (ex: motor module, with setup and speed/dir functions)
- rolling unit level (ex: control engineering)