Часть 1. Настройка окружения для работы с libopencm3
Часть 2. Работа с GPIO, SPI, отладка проекта при помощи GDB
Часть 3. Работа с USART, прерываниями, I2C и таймерами

В первой части мы подготовили среду для легковесной разработки под STM32. Пора приступить к экспериментам.

В процессе разработки вам будет полезны следующие документы:

Также не лишними будут эти 2 информативные картинки с платами:

STM32F103 Blue Pill

STM32F411 BlackPill

Начнем.

Первый проект: моргаем светодиодом (GPIO & DWT)

Как и на многих курсах по программированию микроконтроллеров, мы начнем с элементарных вещей, а именно, с управления GPIO.

Цель: реализовать моргание светодиодом на отладочной плате, подключенной к ножке PC13.

Переместимся в каталог отладочной платы (BluePill или BlackPill) и воспользуемся скриптом инициализации проекта:

$ ./make_project.sh ex1_gpio_blink

Переместимся в каталог проекта ex1_gpio_blink и изучим сгенерированный шаблон. Откроем файл ex1_gpio_blink.c:

STM32F1
#include <libopencm3/stm32/rcc.h>
#include "ex1_gpio_blink.h"
 
static void clock_setup(void)
{
        rcc_clock_setup_pll(&rcc_hse_configs[RCC_CLOCK_HSE8_72MHZ]);
        rcc_periph_clock_enable(RCC_GPIOA);
}
 
int main(void)
{
        clock_setup()
        while(1);
}

STM32F4
#include <libopencm3/stm32/rcc.h>
#include "ex1_gpio_blink.h"
 
static void clock_setup(void)
{
        rcc_clock_setup_pll(&rcc_hse_25mhz_3v3[RCC_CLOCK_3V3_96MHZ]);
        rcc_periph_clock_enable(RCC_GPIOA);
}
 
int main(void)
{
        clock_setup();
        while(1);
}

В начале мы подключаем заголовочный файл для работы c подсистемой RCC (Reset and Clock Control), которая отвечает за тактирование других подсистем микроконтроллера.

Инициализация тактирования возложена на функцию clock_setup, в которой происходит настройка глобального источника тактирования и запускается тактирование порта GPIOA. Изучим библиотечную функцию rcc_clock_setup_pll внимательнее.

В качестве параметра данная функция принимает структуру rcc_clock_scale (описание для F1 и F4), содержащую значения регистров, задающих коэффициенты деления/умножения схемы тактирования.

libopencm3 уже содержит набор популярных конфигураций схем тактирования. Конкретные значения регистров можно найти в исходном коде (для F1 здесь и для F4 здесь).

В большинстве случаев нет необходимости менять что-то в тактировании, и мы можем воспользоваться готовыми конфигурациями. В случае STM32F1 это будет тактирование от внешнего кварца 8 Мгц и генерация частоты шины AHB 72 Мгц, а для STM32F4 – тактирование от внешнего кварца 25 Мгц и генерация частоты шины AHB 96 Мгц. В любом случае, внимательно проверьте, какой кварц установлен на вашей плате, и используйте подходящую конфигурацию.

Теперь подключаем заголовочные файлы для работы с GPIO, затем изменяем добавляемое в заготовку проекта по умолчанию включение тактирования для порта GPIOA на порт GPIOC. Далее вызываем функции конфигурации пинов микроконтроллера. Для нашего случая задаем ножке микроконтроллера PC13 следующую конфигурацию: работа на выход, режим «push-pull».

STM32F1
#include <libopencm3/stm32/rcc.h>
#include <libopencm3/stm32/gpio.h>
#include "ex1_gpio_blink.h"
 
static void clock_setup(void)
{
        rcc_clock_setup_pll(&rcc_hse_configs[RCC_CLOCK_HSE8_72MHZ]);
        rcc_periph_clock_enable(RCC_GPIOC);
}
 
int main(void)
{
        clock_setup();
        gpio_set_mode(GPIOC, GPIO_MODE_OUTPUT_50_MHZ,  GPIO_CNF_OUTPUT_PUSHPULL, GPIO13);
        while(1);
 
}

STM32F4
#include <libopencm3/stm32/rcc.h>
#include <libopencm3/stm32/gpio.h>
#include "ex1_gpio_blink.h"
 
static void clock_setup(void)
{
        rcc_clock_setup_pll(&rcc_hse_25mhz_3v3[RCC_CLOCK_3V3_96MHZ]);
        rcc_periph_clock_enable(RCC_GPIOC);
}
 
int main(void)
{
        clock_setup();
        gpio_mode_setup(GPIOC, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO13);
        while(1);
}

Следующим шагом нам нужно в бесконечном цикле менять состояние ножки микроконтроллера после небольшой задержки. Библиотека libopencm3 предлагает нам следующие функции для оперирования состоянием портов:

  • gpio_set() - Установить вывод в «1»

  • gpio_clear() - Установить вывод в «0»

  • gpio_toggle() - Инвертировать текущее состояние вывода

  • gpio_port_write() - Установить состояние всех выводов порта аналогично битам переданного 16-битного значения

Используем gpio_toggle():

STM32F1
#include <libopencm3/stm32/rcc.h>
#include <libopencm3/stm32/gpio.h>
#include "ex1_gpio_blink.h"
 
static void clock_setup(void)
{
        rcc_clock_setup_pll(&rcc_hse_configs[RCC_CLOCK_HSE8_72MHZ]);
        rcc_periph_clock_enable(RCC_GPIOC);
}
 
int main(void)
{
        clock_setup();
        gpio_set_mode(GPIOC, GPIO_MODE_OUTPUT_50_MHZ,  GPIO_CNF_OUTPUT_PUSHPULL, GPIO13);
        while(1){
                for (i = 0; i < 1000000; i++) {
                        __asm__("nop");
                }
                gpio_toggle(GPIOC, GPIO13);
        }
}

STM32F4
#include <libopencm3/stm32/rcc.h>
#include <libopencm3/stm32/gpio.h>
#include "ex1_gpio_blink.h"
 
static void clock_setup(void)
{
        rcc_clock_setup_pll(&rcc_hse_25mhz_3v3[RCC_CLOCK_3V3_96MHZ]);
        rcc_periph_clock_enable(RCC_GPIOC);
}       
 
int main(void)
{
        clock_setup();
        gpio_mode_setup(GPIOC, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO13);
        while(1){
                for (i = 0; i < 1000000; i++) {
                        __asm__("nop");
                }
                gpio_toggle(GPIOC, GPIO13);
        }
}

Внимание! Если вы хотите собрать проект, который будет загружаться через программный бутлоадер, важно не забыть поменять опции линкера. Откройте Makefile в каталоге проекта и измените переменную LDSCRIPT на:

$ LDSCRIPT = ../bluepill-bootloader.ld

Собираем наш проект. Вы можете указать make-файлу генерировать различные типы выходного файла (bin, hex или elf). Я соберу в bin.

$ make clean && make bin

Теперь прошьем файл. Я буду прошивать через StLinkV2. Make файлы, которые мы взяли из libopencm3-examples, уже имеют готовые опции для прошивки через st-util и BMP. Если желаете дополнить их другими вариантами (к примеру, прошивка через USART с помощью stm32-flash или через DFU), то взгляните на файл rules.mk. Он находится в корневом каталоге нашего окружения. Там все довольно очевидно.

Итак, прошиваем через STLinkV2:

$ make ex1_gpio_blink.stlink-flash

После прошивки мы увидим моргающий светодиод на отладочной плате.

Если вы попробовали прошить данный код и на BluePill, и на BlackPill, то заметили, что скорость моргания светодиодов на платах различается. Это вызвано тем, что наша задержка между переключениями светодиода полагается на скорость выполнения микроконтроллером 1 миллиона пустых операций. Она будет меняться в зависимости от тактовой частоты. Чтобы избежать этого, можно воспользоваться такой подсистемой STM32 как DWT (Data Watchpoint Trigger), которая имеет различные счетчики для проведения профилирования производительности. Нам потребуется DWT счетчик «Clock cycle». После его активации при каждом «тике» процессора счетчик будет увеличивать свое значение на единицу. Зная частоту процессора, мы можем определить, через сколько «тиков» истечет нужный нам временной промежуток.

Для начала активируем подсистему DWT, создав для этого отдельную функцию. В ней мы активируем счетчик библиотечной функцией dwt_enable_cycle_counter(). Далее выполним пару инструкций и убедимся, что счетчик не равен нулю, т.е. происходит его инкремент.

STM32F1 & STM32F4
static uint32_t dwt_setup(void)
{
        dwt_enable_cycle_counter();
        __asm volatile ("nop");                                      
        __asm volatile ("nop");            
        __asm volatile ("nop");
 
     if(dwt_read_cycle_counter())
     {                
       return 0;                        
     }           
     else                                      
  {                                    
    return 1;
  }                                        
}   

Теперь реализуем сами функции, реализующие задержку:

STM32F1 & STM32F4
static void dwt_delay_us(uint32_t microseconds)                                                                          
{                                                                                                                                      
        uint32_t initial_ticks = dwt_read_cycle_counter();                                                                        
        uint32_t us_count_tics = microseconds * (rcc_ahb_frequency / 1000000);                                                                           
        while ((dwt_read_cycle_counter() - initial_ticks) < us_count_tics);                                        
}                                                                                                                                      
 
static void dwt_delay_ms(uint32_t milliseconds)                                                                                 
{                                                                                                                                      
        uint32_t initial_ticks = dwt_read_cycle_counter();                                                                        
        uint32_t ms_count_tics = milliseconds * (rcc_ahb_frequency / 1000);                                                                              
        while ((dwt_read_cycle_counter() - initial_ticks) < ms_count_tics);
}

Используем наши новые функции, не забыв подключить заголовочный файл dwt.h:

STM32F1
#include <libopencm3/stm32/rcc.h>
#include <libopencm3/stm32/gpio.h>
#include <libopencm3/cm3/dwt.h>
#include "ex1_gpio_blink.h"
 
static void clock_setup(void)
{
        rcc_clock_setup_pll(&rcc_hse_configs[RCC_CLOCK_HSE8_72MHZ]);
        rcc_periph_clock_enable(RCC_GPIOC);
}
 
static uint32_t dwt_setup(void) {
        dwt_enable_cycle_counter();
        __asm volatile ("nop");
        __asm volatile ("nop");
        __asm volatile ("nop");
 
     if(dwt_read_cycle_counter())
     {
       return 0;
     }
     else
  {
    return 1;
  }
}
 
static void dwt_delay_us(uint32_t microseconds){
        uint32_t initial_ticks = dwt_read_cycle_counter();
        uint32_t us_count_tics = microseconds * (rcc_ahb_frequency / 1000000);
        while ((dwt_read_cycle_counter() - initial_ticks) < us_count_tics);
}
 
static void dwt_delay_ms(uint32_t milliseconds){
        uint32_t initial_ticks = dwt_read_cycle_counter();
        uint32_t ms_count_tics = milliseconds * (rcc_ahb_frequency / 1000);
        while ((dwt_read_cycle_counter() - initial_ticks) < ms_count_tics);
}
 
 
int main(void)
{
        clock_setup();
        dwt_setup();
        gpio_set_mode(GPIOC, GPIO_MODE_OUTPUT_50_MHZ,  GPIO_CNF_OUTPUT_PUSHPULL, GPIO13);
        while(1){
                dwt_delay_ms(1000);
                gpio_toggle(GPIOC, GPIO13);
        }
}

STM32F4
#include <libopencm3/stm32/rcc.h>
#include <libopencm3/stm32/gpio.h>
#include <libopencm3/cm3/dwt.h>
#include "ex1_gpio_blink.h"
 
static void clock_setup(void)
{
        rcc_clock_setup_pll(&rcc_hse_25mhz_3v3[RCC_CLOCK_3V3_96MHZ]);
        rcc_periph_clock_enable(RCC_GPIOC);
}
 
static uint32_t dwt_setup(void) {
        dwt_enable_cycle_counter();
        __asm volatile ("nop");
        __asm volatile ("nop");
        __asm volatile ("nop");
 
     if(dwt_read_cycle_counter())
     {
       return 0;
     }
     else
  {
    return 1;
  }
}
 
static void dwt_delay_us(uint32_t microseconds){
        uint32_t initial_ticks = dwt_read_cycle_counter();
        uint32_t us_count_tics = microseconds * (rcc_ahb_frequency / 1000000);
        while ((dwt_read_cycle_counter() - initial_ticks) < us_count_tics);
}
 
static void dwt_delay_ms(uint32_t milliseconds){
        uint32_t initial_ticks = dwt_read_cycle_counter();
        uint32_t ms_count_tics = milliseconds * (rcc_ahb_frequency / 1000);
        while ((dwt_read_cycle_counter() - initial_ticks) < ms_count_tics);
}
 
 
int main(void)
{
        clock_setup();
        dwt_setup();
        gpio_mode_setup(GPIOC, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO13);
        while(1){
                dwt_delay_ms(1000);
                gpio_toggle(GPIOC, GPIO13);
        }
}

Прошиваем вновь, и теперь светодиод должен изменять свое состояние точно раз в секунду.

Наш первый проект прошит и работает. Двигаемся далее.

Второй проект: Взаимодействие через SPI

Протокол SPI является простейшим протоколом взаимодействия с периферийными устройствами.

Модуль SPI в микроконтроллерах STM32 поддерживает режимы передачи как 8 бит, так и 16 бит. Сама по себе настройка SPI довольно тривиальна и не вызывает проблем у тех, кто имел дело с этим протоколом на других микроконтроллерах. Единственный нюанс, который может путать начинающего – это правильная настройка пина, отвечающего за линию CS (chip select).

Цель: вывести на четырехразрядный семисегментный индикатор строку «1234», работая с ним через драйвер MAX7219. Микроконтроллер будет использовать аппаратный SPI2 и работать в режиме мастера.

Сгенерируем проект в каталоге платы и перейдем в него:

$ ./make_project.sh ex2_spi
$ cd ex2_spi

Добавим в нашу заготовку подключение заголовочных файлов для GPIO и SPI, включим тактирование SPI2, и изменим порт тактирования с GPIOA на GPIOB (сверившись с картинкой в начале, можно увидеть, что интерфейс SPI2 использует пины PB12 – PB15):

STM32F1
#include <libopencm3/stm32/rcc.h> 
#include <libopencm3/stm32/gpio.h>
#include <libopencm3/stm32/spi.h>
#include "ex2_spi.h" 
 
static void clock_setup(void)
{       
        rcc_clock_setup_pll(&rcc_hse_configs[RCC_CLOCK_HSE8_72MHZ]);
        rcc_periph_clock_enable(RCC_GPIOB);
        rcc_periph_clock_enable(RCC_SPI2);
}
 
int main(void)
{
        clock_setup();
        while(1);
}

STM32F4
#include <libopencm3/stm32/rcc.h>
#include <libopencm3/stm32/gpio.h>
#include <libopencm3/stm32/spi.h>
#include "ex2_spi.h"
 
static void clock_setup(void)
{
        rcc_clock_setup_pll(&rcc_hse_25mhz_3v3[RCC_CLOCK_3V3_96MHZ]);
        rcc_periph_clock_enable(RCC_GPIOB);
        rcc_periph_clock_enable(RCC_SPI2);
}
 
int main(void)
{
        clock_setup();
        while(1);
}

Теперь создадим функцию, которая выполнит конфигурирование SPI. В ней мы сначала активируем альтернативные функции выводов микроконтроллера, а затем вызовем процедуру базовой настройки SPI co следующими параметрами:

  • Частота SPI Clock будет равна тактовой частоте, поделенной на 128

  • В неактивном режиме пин CLK в состоянии low

  • Считывание бита происходит на восходящем фронте тактового сигнала

  • Длина одной транзакции равна 16 битам

  • Старший бит передается первым

Здесь мы должны сделать небольшое отступление. Если вы уже имели дело с протоколом SPI, то знаете: этот протокол использует линию CS (chip select) для сигнализации ведомому устройству, что к нему сейчас обратится мастер-устройство. Мастер-устройство может управлять линией CS как аппаратно, так и программно. Я приведу оба примера настройки и передачи данных как для аппаратного управления, так и для программного.

SPI с аппаратным управлением ножкой CS

В первую очередь настроим альтернативные функции ножек микроконтроллера.

В конфигурации SPI мы отключаем «software slave management» и включаем «slave select output». Теперь внимание! Как только мы включим наш модуль SPI, ножка GPIO12 автоматически перейдет в низкое состояние, сигнализируя ведомому устройству о начале передачи. Зная это, мы создаем функцию передачи, состоящую из 3 шагов: включить SPI, передать данные, выключить SPI. Иначе говоря, функция включения SPI возьмет на себя и функцию сигнализирования ведомому устройству о требовании готовности к приему данных.

Также хочу обратить внимание, что libopencm3 содержит 3 разные функции, которыми можно осуществить отправку данных в SPI:

  • spi_write() - производит простое копирование данных в регистр SPI

  • spi_send() - ждет окончания предыдущей передачи (если она была), и затем записывает данные в SPI

  • spi_xfer() - копирует данные в регистр SPI для отправки, ожидает окончания приема и возвращает принятые данные

Внутри функции, реализующей отправку данных, я решил воспользоваться библиотечной функцией spi_xfer(), чтобы быть уверенным, что данные приняты микросхемой драйвера индикатора.

STM32F1
static void spi_setup(void){
        gpio_set_mode(GPIOB, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_ALTFN_PUSHPULL, GPIO12 | GPIO13 | GPIO15 ); //CS, SCK, MOSI
        gpio_set_mode(GPIOB, GPIO_MODE_INPUT, GPIO_CNF_INPUT_FLOAT, GPIO14); //MISO
 
        spi_disable(SPI2);
        spi_init_master(SPI2, SPI_CR1_BAUDRATE_FPCLK_DIV_128, SPI_CR1_CPOL_CLK_TO_0_WHEN_IDLE, SPI_CR1_CPHA_CLK_TRANSITION_1, SPI_CR1_DFF_16BIT, SPI_CR1_MSBFIRST);
        spi_disable_software_slave_management(SPI2);
        spi_enable_ss_output(SPI2);
  }
 
  static void spi_transmit(uint16_t data){
        spi_enable(SPI2);
        spi_xfer(SPI2, data);
        spi_disable(SPI2);
}

STM32F4
static void spi_setup(void){                                                                                                           
        gpio_mode_setup(GPIOB, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO12 | GPIO13 | GPIO14 | GPIO15 ); //CS, SCK, MISO, MOSI                
        gpio_set_af(GPIOB, GPIO_AF5, GPIO12 | GPIO13 | GPIO14 | GPIO15);                                                               
        gpio_set_output_options(GPIOB, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO12 | GPIO13  | GPIO15);                                   
 
        spi_disable(SPI2);                                                                                                             
        spi_init_master(SPI2, SPI_CR1_BAUDRATE_FPCLK_DIV_128, SPI_CR1_CPOL_CLK_TO_0_WHEN_IDLE, SPI_CR1_CPHA_CLK_TRANSITION_1, SPI_CR1_DFF_16BIT, SPI_CR1_MSBFIRST);
        spi_disable_software_slave_management(SPI2);                                                                                   
        spi_enable_ss_output(SPI2);
}
 
  static void spi_transmit(uint16_t data){                                                                                             
        spi_enable(SPI2);
        spi_xfer(SPI2, data);                                                                                                          
        spi_disable(SPI2);
}

SPI с программным управлением ножкой CS

Разобравшись с аппаратным управлением ножкой CS, рассмотрим программное управление. Оно дает большую гибкость. Есть возможность управлять несколькими ведомыми устройствами, выделяя каждому персональную линию CS, и возможность использовать любой GPIO порт как CS.

Конфигурация SPI отличается по следующим моментам:

  • Ножка GPIO12, которая будет CS, сконфигурирована в режиме push-pull и переведена в высокий уровень

  • Включено программное управление CS

  • Необходимо вручную установить бит SSI (slave select input) при помощи библиотечной функции spi_set_nss_high()

Внутри функции передачи данных мы вручную устанавливаем ножке низкий уровень, а после передачи вновь возвращаем ей высокий уровень.

STM32F1
static void spi_setup(void){
        gpio_set_mode(GPIOB, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, GPIO12); //CS
        gpio_set(GPIOB, GPIO12);
        gpio_set_mode(GPIOB, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_ALTFN_PUSHPULL, GPIO13 | GPIO15 ); //SCK, MOSI
 
        spi_disable(SPI2);
        spi_init_master(SPI2, SPI_CR1_BAUDRATE_FPCLK_DIV_128, SPI_CR1_CPOL_CLK_TO_0_WHEN_IDLE, SPI_CR1_CPHA_CLK_TRANSITION_1, SPI_CR1_DFF_16BIT, SPI_CR1_MSBFIRST);
        spi_enable_software_slave_management(SPI2);
        spi_set_nss_high(SPI2);
        spi_enable(SPI2);
  }
 
static void spi_transmit(uint16_t data){
        gpio_clear(GPIOB, GPIO12);
        spi_xfer(SPI2, data);
        gpio_set(GPIOB, GPIO12);      
}

STM32F4
static void spi_setup(void){
        gpio_mode_setup(GPIOB, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO12); //CS
        gpio_set(GPIOB, GPIO12);
        gpio_mode_setup(GPIOB, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO13 | GPIO14 | GPIO15 ); //SCK, MISO, MOSI
        gpio_set_af(GPIOB, GPIO_AF5, GPIO13 | GPIO14 | GPIO15);
        gpio_set_output_options(GPIOB, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO13  | GPIO15);
 
        spi_disable(SPI2);
        spi_init_master(SPI2, SPI_CR1_BAUDRATE_FPCLK_DIV_128, SPI_CR1_CPOL_CLK_TO_0_WHEN_IDLE, SPI_CR1_CPHA_CLK_TRANSITION_1, SPI_CR1_DFF_16BIT, SPI_CR1_MSBFIRST);
        spi_enable_software_slave_management(SPI2);
        spi_set_nss_high(SPI2);
        spi_enable(SPI2);
}
 
static void spi_transmit(uint16_t data){
        gpio_clear(GPIOB, GPIO12);
        spi_xfer(SPI2, data);
        gpio_set(GPIOB, GPIO12);
}

Наш модуль SPI отконфигурирован, и создана вспомогательная функция передачи. Можно заняться взаимодействием с микросхемой MAX7219. Она очень проста. Мы берем байт команды и байт данных, затем объединяем их в 16-битной переменной и отправляем по SPI:

static void max7219_send(uint8_t cmd, uint8_t data){
        spi_transmit(((uint16_t)cmd << 8) | data);
}

Изучаем даташит на MAX7219 и набрасываем инициализацию:

STM32F1
#include <libopencm3/stm32/rcc.h>
#include <libopencm3/stm32/gpio.h>
#include <libopencm3/stm32/spi.h>
#include "ex2_spi.h"
 
#define MAX7219_MODE_DECODE       0x09
#define MAX7219_MODE_INTENSITY    0x0A
#define MAX7219_MODE_SCAN_LIMIT   0x0B
#define MAX7219_MODE_POWER        0x0C
#define MAX7219_MODE_TEST         0x0F
#define MAX7219_MODE_NOOP         0x00
 
static void clock_setup(void)
{
        rcc_clock_setup_pll(&rcc_hse_configs[RCC_CLOCK_HSE8_72MHZ]);
        rcc_periph_clock_enable(RCC_GPIOB);
        rcc_periph_clock_enable(RCC_SPI2);
}
 
static void spi_setup(void){
        gpio_set_mode(GPIOB, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, GPIO12); //CS
        gpio_set(GPIOB, GPIO12);
        gpio_set_mode(GPIOB, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_ALTFN_PUSHPULL, GPIO13 | GPIO15 ); //SCK, MOSI
 
        spi_disable(SPI2);
        spi_init_master(SPI2, SPI_CR1_BAUDRATE_FPCLK_DIV_256, SPI_CR1_CPOL_CLK_TO_0_WHEN_IDLE, SPI_CR1_CPHA_CLK_TRANSITION_1, SPI_CR1_DFF_16BIT, SPI_CR1_MSBFIRST);
        spi_enable_software_slave_management(SPI2);
        spi_set_nss_high(SPI2);
        spi_enable(SPI2);
  }
 
static void spi_transmit(uint16_t data){
        gpio_clear(GPIOB, GPIO12);
        spi_xfer(SPI2, data);
        gpio_set(GPIOB, GPIO12);     
}
 
static void max7219_send(uint8_t cmd, uint8_t data){
       spi_transmit(((uint16_t)cmd << 8) | data);
}
 
int main(void)
{
        clock_setup();
        spi_setup();
        max7219_send(MAX7219_MODE_DECODE, 0xFF);
        max7219_send(MAX7219_MODE_SCAN_LIMIT, 3);
        max7219_send(MAX7219_MODE_INTENSITY, 2);
        max7219_send(MAX7219_MODE_POWER, 1);
        max7219_send(0x1, 4);
        max7219_send(0x2, 3);
        max7219_send(0x3, 2);
        max7219_send(0x4, 1);
 
        while(1){
        }
}

STM32F4
#include <libopencm3/stm32/rcc.h>
#include <libopencm3/stm32/gpio.h>
#include <libopencm3/stm32/spi.h>
#include "ex2_spi.h"
 
#define MAX7219_MODE_DECODE       0x09
#define MAX7219_MODE_INTENSITY    0x0A
#define MAX7219_MODE_SCAN_LIMIT   0x0B
#define MAX7219_MODE_POWER        0x0C
#define MAX7219_MODE_TEST         0x0F
#define MAX7219_MODE_NOOP         0x00
 
static void clock_setup(void)
{
        rcc_clock_setup_pll(&rcc_hse_25mhz_3v3[RCC_CLOCK_3V3_96MHZ]);
        rcc_periph_clock_enable(RCC_GPIOB);
        rcc_periph_clock_enable(RCC_SPI2);
}
 
static void spi_setup(void){
        gpio_mode_setup(GPIOB, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO12); //CS
        gpio_set(GPIOB, GPIO12);
        gpio_mode_setup(GPIOB, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO13 | GPIO14 | GPIO15 ); //SCK, MISO, MOSI
        gpio_set_af(GPIOB, GPIO_AF5, GPIO13 | GPIO14 | GPIO15);
        gpio_set_output_options(GPIOB, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO13  | GPIO15);
 
        spi_disable(SPI2);
        spi_init_master(SPI2, SPI_CR1_BAUDRATE_FPCLK_DIV_128, SPI_CR1_CPOL_CLK_TO_0_WHEN_IDLE, SPI_CR1_CPHA_CLK_TRANSITION_1, SPI_CR1_DFF_16BIT, SPI_CR1_MSBFIRST);
        spi_enable_software_slave_management(SPI2);
        spi_set_nss_high(SPI2);
        spi_enable(SPI2);
}
 
static void spi_transmit(uint16_t data){
        gpio_clear(GPIOB, GPIO12);
        spi_xfer(SPI2, data);
        gpio_set(GPIOB, GPIO12);
}
 
static void max7219_send(uint8_t cmd, uint8_t data){
        spi_transmit(((uint16_t)cmd << 8) | data);
}
 
int main(void)
{
        clock_setup();
        spi_setup();
        max7219_send(MAX7219_MODE_DECODE, 0xFF);
        max7219_send(MAX7219_MODE_SCAN_LIMIT, 3);
        max7219_send(MAX7219_MODE_INTENSITY, 2);
        max7219_send(MAX7219_MODE_POWER, 1);
        max7219_send(0x1, 4);
        max7219_send(0x2, 3);
        max7219_send(0x3, 2);
        max7219_send(0x4, 1);
 
        while(1){
        }
}

Подключаем.

Подключение линий данных MAX7219 к микроконтроллеру на примере BluePill. Обвязка 7219 и подключенный к ней индикатор опущена.
Подключение линий данных MAX7219 к микроконтроллеру на примере BluePill. Обвязка 7219 и подключенный к ней индикатор опущена.

Собираем, прошиваем

$ make clean && make bin
$ make ex2_spi.stlink-flash

Мы должны увидеть на индикаторе "1234".

Отладка программ на микроконтроллере при помощи GDB

Немного отвлечемся от работы с libopencm3 и подумаем о том, как мы будем производить отладку и поиск ошибок в наших программах. Ответ на это – GNU Debugger. Взглянем на картинку.

Непосредственно GDB, что располагается у вас на компьютере, будет являться клиентом к удаленному GDB серверу. А уже GDB сервер будет взаимодействовать с оборудованием, используя специфичные для устройства интерфейсы и протоколы отладки. Подключение к GDB серверу может выполняться как при помощи TCP/IP, так и через символьное устройство.

В прошлой статье мы рассмотрели прошивку через SWD при помощи stlink-utils и Black Magic Probe. Но эти вещи несут в себе и сервера GDB. Утилита st-util реализует TCP сервер, а Black Magic Probe – аппаратный, взаимодействуя с ним через символьное устройство /dev/ttyACM0.

Поэкспериментируем! Подключим Blue Pill к ST-Link V2 программатору и запустим st-util.

$ st-util 
st-util
2023-04-17T23:56:08 WARN common.c: NRST is not connected
2023-04-17T23:56:08 INFO common.c: F1xx Medium-density: 20 KiB SRAM, 128 KiB flash in at least 1 KiB pages.
2023-04-17T23:56:08 INFO gdb-server.c: Listening at *:4242...

st-util запустил TCP сервер на порту 4242. Попробуем подключиться к нему. Запустим в другом окне терминала GDB:

$ arm-none-eabi-gdb -tui

Подключимся к серверу:

(gdb) target remote localhost:4242
Remote debugging using localhost:4242
warning: No executable has been specified and target does not support
determining executable automatically.  Try using the "file" command.
0x080008c0 in ?? ()
(gdb) 

Подключение прошло успешно. Теперь приостановимся, отдадим команду quit и попробуем подключиться к аппаратному GDB серверу.

Подключим наш Black Magic Probe к USB, к BMP присоединим нашу плату и вновь запустим GDB. Теперь последовательность команд будет следующая:

(gdb) target extended-remote /dev/ttyACM0
Remote debugging using /dev/ttyACM0
(gdb) monitor swdp_scan
Available Targets:
No. Att Driver
 1      STM32F1 medium density M3
(gdb) attach 1
Attaching to Remote target
warning: No executable has been specified and target does not support
determining executable automatically.  Try using the "file" command.
0x080008bc in ?? ()
(gdb) 

Успех! Теперь попытаемся отладить наш проект с SPI. Соберем его при помощи команды "make hex", это сгенерирует не только HEX файл, но и ELF, содержащий отладочную информацию. По итогу мы должны иметь файл "ex2_spi.elf". Скажем GDB его загрузить:

(gdb) file ex2_spi.elf 
A program is being debugged already.
Are you sure you want to change the file? (y or n) yReading symbols from ex2_spi.elf...
(gdb) 

Загрузим файл в память микроконтроллера:

(gdb) load
Loading section .text, size 0x708 lma 0x8000000
Loading section .data, size 0xc lma 0x8000708
Start address 0x080005d4, load size 1812
Transfer rate: 9 KB/sec, 604 bytes/write.
(gdb) 

Поставим прерывание на функцию main():

(gdb) break main
Breakpoint 1 at 0x8000180: file ex2_spi.c, line 15.
Note: automatically using hardware breakpoints for read-only addresses.
(gdb) 

Запускаем выполнение:

(gdb) continue
Continuing.

Breakpoint 1, main () at ex2_spi.c:15
(gdb) 

Микроконтроллер начнет исполнение и остановится на первой команде из main(). В окне сверху отобразится код.

Теперь быстрый cheatsheet по командам GDB:

  • "s" - "step into"

  • "c" - "step over"

  • "p <variable name>" - показать содержимое переменной

  • "b <line number | function name>" - установить брейкпойнт на указанный номер строки или функцию

  • "layout split" - разделить область кода на текст программы и ассемблерные инструкции

  • "ni" - "next instruction", выполнить следующую ассемблерную инструкцию

  • "i r r3" - показать содержимое регистра R3

Более подробное описание команд GDB вы можете найти в этом кратком документе, или полной официальной документации.

На этом шаге мы завершим данную часть вводного курса по libopencm3. Надеюсь, что вся изложенная информация будет полезна тем, кто хочет ознакомиться с альтернативой библиотеке HAL от STMicroelectronics.
Код всех примеров, приведенных в статье, доступен на GitHub.

Комментарии (2)


  1. teap0t
    22.09.2023 18:48

    Горизонт завален.


  1. devprodest
    22.09.2023 18:48

    Вы уверены что стоит изучать и libopencm3? имхо эта либа больше мертва чем жива