STM32F103C8T6 без SPL, HAL и без IDE: Система тактирования RCC, таймер SysTick, UART передатчик, планировщик задач, SPI и I2C модули в режиме мастера

разделы: STM32 , дата: 19 октября 2018г.

Внимание! На данный момент(май-июнь 2025г.) статья находится в стадии редактирования. Первоначальную версию статьи можно открыть по следующей ссылке.

Когда я впервые начал знакомиться с микроконтроллерами семейства STM32, то обратил внимание на то, что прошивки даже с самыми простыми алгоритмами (например Blink), по меркам 8-битных микроконтроллеров, имеют огромный размер: от одного килобайта и больше. Поэтому целью этой статьи стала попытка написания прошивок для STM32 в стиле 8-битных микроконтроллеров, когда вы полностью контролируете процесс компиляции, используя лишь: компилятор, флешер и текстовый редактор. Соответственно в статье рассматриваются типовые на мой взгляд вопросы при переходе с 8-битников на 32-разрядную архитектуру: как помигать светодиодом, как настроить тактирование, как завести SPI и поднять I2C.

Если вам чего-то из этого не хватает, то вы легко сможете подтянуть "матчасть" по статьям на хабре: STM32F4: GNU AS: Программирование на ассемблере в семи частях, по методичке "Народная электроника" выпуск 2. А.В. Немоляев. GCC Cortex-M3. PDF, или по книге "Джозеф Ю. Ядро Cortex - МЗ компании ARM. Полное руководство", Козаченко В.Ф.(ред.): "Практический курс микропроцессорной техники на базе процессорных ядер ARM-Cortex-M3/M4/M4F" 2019 М.: МЭИ

Оборудование. В статье я буду использовать популярную плату "Blue Pill" на микроконтроллере STM32F103C8T6, программатор ST-LINK v2 (китайская реплика), USB-UART преобразователь FT232RL, 4-x разрядный семи-сегментный индикатор, на SPI интерфейсе и RTC DS3231 на I2C интерфейсе.

    Список используемой документации:
  1. Перевод на русский язык руководства по программированию STM32F10x и справочного руководства по STM32F105-107 альтернативная ссылка
  2. Cortex-M3 TRM Cortex-M3 Technical Reference Manual
  3. Cortex-M3: Руководство программиста (PM0056), для чипов серий: STM32F10xxx/20xxx/21xxx/L1xxxx.
  4. Справочное руководство (Reference Manual: RM0008), для чипов следующих серий: STM32F101xx, STM32F102xx, STM32F103xx, STM32F105xx and STM32F107xx advanced Arm.
  5. Datasheet на чипы: STM32F103x8/STM32F103xB.
  6. STM32F10xxx I2C optimized examples, Application note AN2824

Содержание:

    I. Создание базового проекта для STM32F103xx
  1. Вместо введения
  2. Минималистический Blink размером в 148 байт
  3. Небольшой ликбез по ассемблеру Cortex-M3
  4. Таблица векторов
  5. Настройка системы тактирования - RCC (Reset and Clock Control)
    II. Работа с периферией микроконтроллера STM32F103xx
  1. Функция задержки на ассемблерных инструкциях
  2. Функция задержки на прерывании таймера SysTick
  3. Настройка и передача данных по UART
  4. Пишем простой планировщик задач (RTOS)
  5. Драйвер 4-x разрядного семисегментного индикатора (программный SPI)
  6. Настройка аппаратного интерфейса SPI для драйвера 4-х разрядного семисегментного индикатора
  7. Регистры I2C интерфейса, делаем сканер I2C шины
  8. Однобайтный режим чтения по шине I2C
  9. Двухбайтный режим чтения по шине I2C
  10. Запись массива через шину I2C
  11. Чтение массива через шину I2C
  12. Отладка в консоли с использованием OpenOCD
    Продолжение цикла STM32F103C8 без HAL и SPL:
  1. Работа с SPI дисплеями Nokia_5110 и ST7735
  2. Работа с монохромными дисплеями STE2007 и SSD1306
  3. Вывод текста на дисплей ST7735

При написании примеров я использовал кросс-компилятор arm-none-eabi-gcc версии 10.3 скачанный с сайта ARM. Данная версия сейчас (май 2025г.) объявлена устаревшей (deprecated), однако у меня она работала стабильно, потому приведу альтернативные ссылки для скачивания: версия 10.3 тарбол для linux x86_64, zip-архив для win32. Актуальная версия компилятора - 14.2 тарбол для linux x86_64, и zip-архив для win64.

Посмотреть исходники, сборочные файлы, скачать скомпилированные прошивки, можно с портала GITLAB https://gitlab.com/flank1er/stm32_bare_metal

1) Вместо введения

В настоящее время (2025 год), доступно большое число микроконтролеров с архитектурой Cortex-M3. Это STM32F1xx, их реплики, клоны, и различные "мутанты" с непонятными характеристиками. Данные изделия предлагаются производителями со своими библиотеками и средами разработки. В текущей ситуации нужно уметь создать проект с нуля, имея на руках только даташит. Нужно уметь интегрировать этот проект в современную среду разработки, дабы не прыгать из одной программы в другую. Так или иначе, STM32F103xx сейчас является образцом, который копируют другие производители, и который следует знать.

Началось все с того, что меня несколько обескуражил размер прошивки минимального проекта в TrueStudio (после покупки компанией ST была переименованна в STM32CubeIDE) - 1572 байт:

В SW4STM32 получалась какая-то такая же цифра, при этом у меня в настройках проекта была выставлена опция: --gc-section, которая даёт команду компоновщику удалять неиспользуемый код:

Даже в Arduino-STM32 Blink "весил" в пределах одного килобайта.

Сначала я подумал, что все дело в используемой SPL и если переписать код на регистрах, то ситуация исправится. Давайте посмотрим так ли это.

Первая вызываемая функция имеет такой вид:

void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState)
{
  /* Check the parameters */
  assert_param(IS_RCC_APB2_PERIPH(RCC_APB2Periph));
  assert_param(IS_FUNCTIONAL_STATE(NewState));
  if (NewState != DISABLE)
  {
    RCC->APB2ENR |= RCC_APB2Periph;
  }
  else
  {
    RCC->APB2ENR &= ~RCC_APB2Periph;
  }
}

т.е. ее можно смело заменить строкой вида:

 RCC->APB2ENR |= RCC_APB2Periph_GPIOC;

Вызов функции инициализации порта GPIO оборачивается вызовом такой штуки:

void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct)
{
  uint32_t currentmode = 0x00, currentpin = 0x00, pinpos = 0x00, pos = 0x00;
  uint32_t tmpreg = 0x00, pinmask = 0x00;
  /* Check the parameters */
  assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
  assert_param(IS_GPIO_MODE(GPIO_InitStruct->GPIO_Mode));
  assert_param(IS_GPIO_PIN(GPIO_InitStruct->GPIO_Pin));

/*---------------------------- GPIO Mode Configuration -----------------------*/
  currentmode = ((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x0F);
  if ((((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x10)) != 0x00)
  {
    /* Check the parameters */
    assert_param(IS_GPIO_SPEED(GPIO_InitStruct->GPIO_Speed));
    /* Output mode */
    currentmode |= (uint32_t)GPIO_InitStruct->GPIO_Speed;
  }
/*---------------------------- GPIO CRL Configuration ------------------------*/
  /* Configure the eight low port pins */
  if (((uint32_t)GPIO_InitStruct->GPIO_Pin & ((uint32_t)0x00FF)) != 0x00)
  {
    tmpreg = GPIOx->CRL;
    for (pinpos = 0x00; pinpos < 0x08; pinpos++)
    {
      pos = ((uint32_t)0x01) << pinpos;
      /* Get the port pins position */
      currentpin = (GPIO_InitStruct->GPIO_Pin) & pos;
      if (currentpin == pos)
      {
        pos = pinpos << 2;
        /* Clear the corresponding low control register bits */
        pinmask = ((uint32_t)0x0F) << pos;
        tmpreg &= ~pinmask;
        /* Write the mode configuration in the corresponding bits */
        tmpreg |= (currentmode << pos);
        /* Reset the corresponding ODR bit */
        if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD)
        {
          GPIOx->BRR = (((uint32_t)0x01) << pinpos);
        }
        else
        {
          /* Set the corresponding ODR bit */
          if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU)
          {
            GPIOx->BSRR = (((uint32_t)0x01) << pinpos);
          }
        }
      }
    }
    GPIOx->CRL = tmpreg;
  }
/*---------------------------- GPIO CRH Configuration ------------------------*/
  /* Configure the eight high port pins */
  if (GPIO_InitStruct->GPIO_Pin > 0x00FF)
  {
    tmpreg = GPIOx->CRH;
    for (pinpos = 0x00; pinpos < 0x08; pinpos++)
    {
      pos = (((uint32_t)0x01) << (pinpos + 0x08));
      /* Get the port pins position */
      currentpin = ((GPIO_InitStruct->GPIO_Pin) & pos);
      if (currentpin == pos)
      {
        pos = pinpos << 2;
        /* Clear the corresponding high control register bits */
        pinmask = ((uint32_t)0x0F) << pos;
        tmpreg &= ~pinmask;
        /* Write the mode configuration in the corresponding bits */
        tmpreg |= (currentmode << pos);
        /* Reset the corresponding ODR bit */
        if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD)
        {
          GPIOx->BRR = (((uint32_t)0x01) << (pinpos + 0x08));
        }
        /* Set the corresponding ODR bit */
        if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU)
        {
          GPIOx->BSRR = (((uint32_t)0x01) << (pinpos + 0x08));
        }
      }
    }
    GPIOx->CRH = tmpreg;
  }
}

По большому счету, эта функция устанавливает значение регистров: GPIOx->CRL или/и GPIOx->CRH. Вспоминаем, что это за регистры:

Регистр GPIOx->CRL конфигурирует режим пинов от 0 до 7. Регистр GPIOx->CRH конфигурирует режим пинов от 8 до 15. Т.к. светодиод на Blue Pill подключён к PC13, то нам нужен регистр GPIOС->CRH. Чтобы его сконфигурировать. В поле MOD13[1:0] можно задать максимальную частоту переключения нужного пина в Output режиме. Полагаю 2МГц будет вполне достаточно, значит записываем в него значение 2. По умолчанию, в режиме выхода поле CNF13[1:0] конфигурирует пин в Push-Pull режим, что нас вполне устраивает, следовательно оставляем там по нулям.

Т.о. вызов функции GPIO_Init(GPIOC, &GPIO_InitStructure) можно заменить следующей парой строк:

    GPIOC->CRH &= ~(uint32_t)(0xf<<20);
    GPIOC->CRH |=  (uint32_t)(0x2<<20);

Переключение пина осуществляется через регистр GPIOx->ODR:

Т.е. вызов функции: "GPIO_SetBits(GPIOC,GPIO_Pin_13)" можно заменить на: "GPIOC->ODR |= GPIO_Pin_13".

В Cortex-M3 нет тех удобных битовых инструкций что были в STM8 (имеется ввиду bset,bres,bcpl). Поэтому для побитового изменения состояния регистра GPIOx->ODR, были специально введены дополнительные регистры: GPIOx->BSRR и GPIOx->BRR. Посмотрим на их описание:

Эти регистры управляют состоянием GPIOx->ODR путем записи единицы в их соответствующий разряд. Т.е. вместо GPIOC->ODR &= ~(GPIO_Pin_13); можно использовать GPIOC->BRR = GPIO_Pin_13. Такой вариант разложится компилятором в ОДНУ ассемблерную инструкцию, вместо трёх, если делать непосредственно через GPIOC->ODR регистр. Обратите внимание, что через BSRR/BRR регистры одной инструкцией можно менять состояние сразу нескольких пинов порта. При желании можно организовать параллельную шину через bit-bang.

В итоге, функция main() примет вид:

int main()
{
    // enable GPIOC port
    RCC->APB2ENR |= RCC_APB2Periph_GPIOC;
    // --- GPIO setup ----
    GPIOC->CRH &= ~(uint32_t)(0xf<<20);
    GPIOC->CRH |=  (uint32_t)(0x2<<20);

    for(;;){
        GPIOC->BSRR=GPIO_Pin_13;
        dummy_loop(600000);
        GPIOC->BRR=GPIO_Pin_13;
        dummy_loop(600000);
    }

}
Примечание от 2025г. Вместо использования непонятных цифр при конфигурации регистра ввода-вывода "GPIOC->CRH", можно было использовать именованные константы определенные в заголовочных файлах SPL:
    // Configure PC13 as push-pull output (max speed 2 MHz)
    GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13);
    GPIOC->CRH |= GPIO_CRH_MODE13_1;  // Output mode, max speed 2 MHz (0b10)

После компиляции размер прошивки сократился до 1084 байт. Уже лучше, но все-равно многовато. После дизассеблирования, становится ясно, к прошивке кроме нашего кода добавляется ещё стандартная библиотека Си: libc или newlib (облегчённый вариант стандартной библиотеки Cи), а также некоторые функции конфигурации микроконтроллера (startup files) включая таблицу прерываний. Все вместе это и занимает один килобайт. Ничего не имею против таблицы, но от всего остального (библиотеки libc и startup файлов) хотелось бы избавиться. В TrueStudio у нас нет полного контроля за Makefile'ом, поэтому предлагаю закрыть эту IDE и перейти в консоль.

2) Минималистичный Blink размером в 148 байт

Для начала давайте разберемся с форматом бинарной прошивки, т.е. с чего процессор начинает работу и как он находит программу. Потому-что, как вы наверно догадываетесь, прошивка в ARM начинается не с программного кода, и даже не с таблицы векторов.

Те, кто внимательно читали документацию, знают, что прошивка для Cortex-M3 обязательно должна начинаться со значения указателя стека, после чего должны идти адреса обработчиков прерываний: Reset, NMI, Hard Fault:

На адрес 0x00000000 происходит маппинг: или флеш-памяти или ОЗУ, в зависимости от конфигурации boot-пинов. Флеш-память начинается с адреса 0x08000000.

Т.о. минимальный вариант программы Blink для stm32f103 можно записать следующим образом:

#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"

#define WAIT 600000

asm(".word 0x20005000\n\t"      // stack pointer (adjust based on your device's RAM size)
    ".word main+1\n\t"          // reset handler (Thumb mode)
    ".word fault_irq+1\n\t"     // NMI
    ".word fault_irq+1\n\t"     // HardFault
    ".word fault_irq+1\n\t"     // MemManage
    ".word fault_irq+1\n\t"     // BusFault
    ".word fault_irq+1\n\t");   // UsageFault

void fault_irq() {
    while (1);
}

void dummy_loop(volatile uint32_t count) {
    while (--count);
}

int main() {
    // Enable GPIOC clock
    RCC->APB2ENR |= RCC_APB2Periph_GPIOC;

    // Configure PC13 as push-pull output (max speed 2 MHz)
    GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13);
    GPIOC->CRH |= GPIO_CRH_MODE13_1;  // Output mode, max speed 2 MHz (0b10)

    // Super loop to toggle PC13
    while (1) {
        GPIOC->BSRR = GPIO_Pin_13;    // Set PC13
        dummy_loop(WAIT);
        GPIOC->BRR = GPIO_Pin_13;     // Reset PC13
        dummy_loop(WAIT);
    }
}

Здесь, мы в начале программы пишем заголовок, в который включены: значение стека и адреса обязательных векторов прерываний. Среди векторов прерываний выделется Reset, который указывает на начало программы.

Так же как в AVR, где для включения имен регистров ввода-вывода и констант пишется:

#include <avr/io.h>

В STM32 используется аналогичные включения:

#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"

В файле "stm32f10x.h" содержатся физические адреса регистров, а в "stm32f10x_gpio.h" и "stm32f10x_rcc.h" описаны структуры, маски и константы, чтобы к регистрам ввода-вывода можно было обращаться в стиле "RCC->APB2ENR = какое-то значение". Данные заголовочные файлы были взяты из SPL для stm32f103xx, они не содержат код, только адреса и структуры.

Таблица векторов в Cortex-M3 это просто массив c адресами обработчиков прерываний. Таких инструкций как: INT и IRET, в Cortex-M3 не существует. Все адреса прерываний должны быть нечётными! Т.е. единица прибавляемая к адресу метки нужна для получения нечётного числа, чтобы указать, что инструкция в обработчике прерывания из набора Thumb/Thumb2, а не из 32-битных инструкций ARM. Если вектор будет указывать на чётный адрес, то переход по нему приведёт к срабатыванию прерывания Usage Fault.

Пока на данном этапе, структура проекта выглядит так:

$ tree .
.                                          
├── CMSIS
│   ├── core
│   │   ├── core_cm3.c
│   │   └── core_cm3.h
│   └── device
│       ├── stm32f10x.h
│       └── system_stm32f10x.h
├── SPL
│   └── inc
│       ├── stm32f10x_gpio.h
│       └── stm32f10x_rcc.h
└── main.c

5 directories, 7 files

Для компиляции прошивки под ARM-микроконтроллер потребуется скрипт компоновщика. В AVR он тоже требуется, но обычно входит в пакет компилятора, и никто о нем не думает. Что бы не кидаться сразу многостраничным скриптом компоновщика, про который нам в принципе нужно знать совем немного, я взял скрипт из шаблонного проекта SW4STM32 и сократил его настолько, что бы данный Blink просто напросто собрался:

/* Highest address of the user mode stack */
_estack = 0x20005000;    /* end of RAM */

/* Memories definition */
MEMORY
{
  RAM (xrw)     : ORIGIN = 0x20000000, LENGTH = 20K
  ROM (rx)      : ORIGIN = 0x8000000, LENGTH = 64K
}

/* Sections */
SECTIONS
{
  /* The program code and other data into ROM memory */
  .text :
  {
    . = ALIGN(4);
    *(.text)           /* .text sections (code) */
  } >ROM

  /* Constant data into ROM memory*/
  .rodata :
  {
    . = ALIGN(4);
    *(.rodata)         /* .rodata sections (constants, strings, etc.) */
  } >ROM

  /* Initialized data sections into RAM memory */
  .data :
  {
    . = ALIGN(4);
    *(.data)           /* .data sections */
  } >RAM AT> ROM


  /* Uninitialized data section into RAM memory */
  . = ALIGN(4);
  .bss :
  {
    *(.bss)
    *(COMMON)

    . = ALIGN(4);
    _ebss = .;         /* define a global symbol at bss end */
    __bss_end__ = _ebss;
  } >RAM

}

Назовем его "stm32f10xx8.ld". Теперь у нас есть все для компиляции, поэтому давайте сделаем это:

$ arm-none-eabi-gcc -mthumb -mcpu=cortex-m3 -O0 -c -g -DSTM32F10X_MD -I ./CMSIS/device -I ./CMSIS/core -I ./SPL/inc -o main.o ./main.c

Компоновка:

$ arm-none-eabi-ld   -Tstm32f10xx8.ld  main.o  -o firmware.elf

Тоже самое можно сделать одной командой:

$ arm-none-eabi-gcc -T stm32f10xx8.ld  -nostartfiles -mcpu=cortex-m3 -g -mthumb -DSTM32F10X_MD -I ./CMSIS/device -I ./CMSIS/core -I ./SPL/inc -o firmware.elf main.c

Смотрим на размер получившийся прошивки:

$ arm-none-eabi-size ./firmware.elf 
   text    data     bss     dec     hex filename
    148       0       0     148      94 ./firmware.elf

148 байт, как и было обещано. А почему-бы не добавить оптимизации? Добавим ключ "-Os":

$ arm-none-eabi-gcc -T stm32f10xx8.ld  -nostartfiles -mcpu=cortex-m3 -g -Os -mthumb -DSTM32F10X_MD -I ./CMSIS/device -I ./CMSIS/core -I ./SPL/inc -o firmware.elf main.c
$ arm-none-eabi-size ./firmware.elf 
   text    data     bss     dec     hex filename
    108       0       0     108      6c ./firmware.elf

108 байт - это уже близко к прошивке 8-битного микроконтроллера. Для ATtiny13A прошивка с Blink выходит на 68 байт. Но для 32-разрядного микроконтроллера программа всегда будет больше памяти занимать, т.к. физические адреса тут в два раза длиннее.

Прошиваем:

$ arm-none-eabi-objcopy -O binary firmware.elf ./firmware.bin
$ st-flash  write ./firmware.bin  0x08000000

После прошивки на плате должен начать мигать зеленый светодиод. На логическом анализаторе это выглядит следующим образом:

Сейчас микроконтроллер работает на дефолтных настройках, т.е. он тактируется от HSI с частотой 8 МГц.

Для удобства сборки можно добавить к проекту Makefile:

# Toolchain setup (adjust paths if needed)
CC = arm-none-eabi-gcc
OBJCOPY = arm-none-eabi-objcopy
SIZE = arm-none-eabi-size
RM = rm -f

# Target and output names
TARGET = firmware
BIN = $(TARGET).bin
HEX = $(TARGET).hex
ELF = $(TARGET).elf

# Include paths
INC  = -ICMSIS/device
INC += -ICMSIS/core
INC += -ISPL/inc

DEF = -DSTM32F10X_MD

# MCU and flags
MCU = cortex-m3
CFLAGS = -mcpu=$(MCU) -mthumb -Wall -Os -ffunction-sections -fdata-sections
LDFLAGS = -T stm32f10xx8.ld -nostartfiles

# Source files
SRCS = main.c

# Build rules
all: $(BIN) $(HEX) size

$(ELF): $(SRCS)
	$(CC) $(CFLAGS) $(DEF) $(INC) $(LDFLAGS) $^ -o $@

$(BIN): $(ELF)
	$(OBJCOPY) -O binary $< $@

$(HEX): $(ELF)
	$(OBJCOPY) -O ihex $< $@

size: $(ELF)
	$(SIZE) --format=berkeley $<

# Flash to STM32 via ST-Link
flash: $(BIN)
	st-flash write $< 0x08000000

# Clean
clean:
	$(RM) $(ELF) $(BIN) $(HEX)

Здесь я думаю, что все понятно.

Что еще можно сделать? Можно добавить к проекту CMakeLists.txt для сборки посредством CMake, его я спрятал под спойлер.

cmake_minimum_required(VERSION 3.12)
project(firmware LANGUAGES C)

# Toolchain setup
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR arm)
set(CMAKE_C_COMPILER arm-none-eabi-gcc)
set(CMAKE_OBJCOPY arm-none-eabi-objcopy)
set(CMAKE_SIZE arm-none-eabi-size)

# Target and output names
set(TARGET firmware)
set(ELF ${TARGET}.elf)
set(BIN ${TARGET}.bin)
set(HEX ${TARGET}.hex)

# Include paths
include_directories(
    CMSIS/device
    CMSIS/core
    SPL/inc
)

# Definitions
add_definitions(-DSTM32F10X_MD)

# MCU and flags
set(MCU cortex-m3)
set(CMAKE_C_FLAGS "-mcpu=${MCU} -mthumb -Wall -Os -ffunction-sections -fdata-sections")
set(CMAKE_EXE_LINKER_FLAGS "-T ${CMAKE_SOURCE_DIR}/stm32f10xx8.ld -nostartfiles")

# Source files
set(SRCS main.c)

# Build executable
add_executable(${ELF} ${SRCS})

# Generate binary and hex files
add_custom_command(
    TARGET ${ELF} POST_BUILD
    COMMAND ${CMAKE_OBJCOPY} -O binary ${ELF} ${BIN}
    COMMAND ${CMAKE_OBJCOPY} -O ihex ${ELF} ${HEX}
    COMMENT "Generating ${BIN} and ${HEX}"
)

# Print size information
add_custom_target(
    size
    COMMAND ${CMAKE_SIZE} --format=berkeley ${ELF}
    DEPENDS ${ELF}
)

# Flash target (requires ST-Link)
add_custom_target(
    flash
    COMMAND st-flash write ${BIN} 0x08000000
    DEPENDS ${BIN}
)

Использование:

$ mkdir build && cd $_
$ cmake -DCMAKE_BUILD_TYPE=Release ..
$ make && make size

Qbs-скрипт я здесь выкладывать не буду, он будет доступен на git. Скрипт для Qbs версии:

$ qbs --version
1.24.1

Подробно о связке STM32+Qbs+QtCreator я писал в: "Qbs-скрипт для STM32F103C8-проекта на CMSIS (пошаговая инструкция)"

Qbs-скрипт позволяет использовать IDE QtCreator для написании кода, и при этом контролировать процесс сборки не хуже Makefile'ов (Cmake все-равно какие-то свои флаги добавляет и прошивка получается толще):

QtCreator это нативная программа, а не Java как STM32CubeIDE, шустро ворочается, не требует регистрации, имеет статический линтер на Clang и возможность отладки:

На мой взгляд это самая недооцененная IDE для разработки.

3) Небольшой ликбез по ассемблеру Cortex-M3

С помощью команды:

$ arm-none-eabi-objdump -S ./firmware.elf

можно дизассемблировать прошивку, что бы убедиться, что заголовок с константами расположен именно по адресу 0x8000000:

Disassembly of section .text:

08000000 <.text>:
 8000000:   20005000    .word   0x20005000
 8000004:   08000031    .word   0x08000031
 8000008:   0800001d    .word   0x0800001d
 800000c:   0800001d    .word   0x0800001d
 8000010:   0800001d    .word   0x0800001d
 8000014:   0800001d    .word   0x0800001d
 8000018:   0800001d    .word   0x0800001d

Disassembly of section .text.fault_irq:

0800001c <fault_irq>:
    ".word fault_irq+1\n\t"     // MemManage
    ".word fault_irq+1\n\t"     // BusFault
    ".word fault_irq+1\n\t");   // UsageFault

void fault_irq() {
    while (1);
 800001c:   e7fe        b.n 800001c <fault_irq>

Disassembly of section .text.dummy_loop:

0800001e <dummy_loop>:
}

void dummy_loop(volatile uint32_t count) {
 800001e:   b082        sub sp, #8
 8000020:   9001        str r0, [sp, #4]
    while (--count);
 8000022:   9b01        ldr r3, [sp, #4]
 8000024:   3b01        subs    r3, #1
 8000026:   9301        str r3, [sp, #4]
 8000028:   2b00        cmp r3, #0
 800002a:   d1fa        bne.n   8000022 <dummy_loop+0x4>
}
 800002c:   b002        add sp, #8
 800002e:   4770        bx  lr

Disassembly of section .text.startup.main:

08000030 <main>:
    GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13);
    GPIOC->CRH |= GPIO_CRH_MODE13_1;  // Output mode, max speed 2 MHz (0b10)

    // Super loop to toggle PC13
    while (1) {
        GPIOC->BSRR = GPIO_Pin_13;    // Set PC13
 8000030:   f44f 5100   mov.w   r1, #8192   ; 0x2000
    RCC->APB2ENR |= RCC_APB2Periph_GPIOC;
 8000034:   4a0b        ldr r2, [pc, #44]   ; (8000064 <main+0x34>)
int main() {
 8000036:   b508        push    {r3, lr}
    RCC->APB2ENR |= RCC_APB2Periph_GPIOC;
 8000038:   6993        ldr r3, [r2, #24]
        dummy_loop(WAIT);
 800003a:   480b        ldr r0, [pc, #44]   ; (8000068 <main+0x38>)
    RCC->APB2ENR |= RCC_APB2Periph_GPIOC;
 800003c:   f043 0310   orr.w   r3, r3, #16
 8000040:   6193        str r3, [r2, #24]
    GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13);
 8000042:   f5a2 3280   sub.w   r2, r2, #65536  ; 0x10000
 8000046:   6853        ldr r3, [r2, #4]
 8000048:   f423 0370   bic.w   r3, r3, #15728640   ; 0xf00000
 800004c:   6053        str r3, [r2, #4]
    GPIOC->CRH |= GPIO_CRH_MODE13_1;  // Output mode, max speed 2 MHz (0b10)
 800004e:   6853        ldr r3, [r2, #4]
 8000050:   f443 1300   orr.w   r3, r3, #2097152    ; 0x200000
 8000054:   6053        str r3, [r2, #4]
        GPIOC->BSRR = GPIO_Pin_13;    // Set PC13
 8000056:   6111        str r1, [r2, #16]
        dummy_loop(WAIT);
 8000058:   f7ff ffe1   bl  800001e <dummy_loop>
        GPIOC->BRR = GPIO_Pin_13;     // Reset PC13
 800005c:   6151        str r1, [r2, #20]
        dummy_loop(WAIT);
 800005e:   f7ff ffde   bl  800001e <dummy_loop>
    while (1) {
 8000062:   e7f8        b.n 8000056 <main+0x26>
 8000064:   40021000    .word   0x40021000
 8000068:   000927c1    .word   0x000927c1

Здесь, возможно, потребуется небольшой ликбез.

1. Cortex-M3 был разработан двадцать лет назад, но до сих пор явлется популярным ядром для микроконтроллеров, несмотря на то, что впоследствии были выпущены более оптимизированные Cortex-M0/M0+ и более прогрессивные Cortex-M4, на смену которым в свою очередь выпущены Cortex-M23 и Cortex-M33. Все эти ядра занимают свою нишу в разработке электроники.

Coretx-M3 это совокупный набор инструкций Thumb и Thumb II. Наглядно на количество команд в том и другом наборе можно посмотреть на следующей картинке:

Thumb - это набор 16-битных инструкций. Исключением являются инструкция BL и барьерные инструкции (BL, DMB, DSB, ISB), которые имеют длину 4 байта. Thumb II - это набор 32-битных инструкций. Несмотря на то, что количество инструкций Thumb II в разы больше, чем у Thumb, многие (но не все) эти инструкции можно заменить связкой из пары инструкций Thumb I. Но зачастую, инструкции Thumb II будут работать быстрее, нежели такая связка.

Все инструкции в Cortex-M3 либо 16-битные, либо 32-битные. Одно-байтных, трех-байтных, пяти-байтных инструкций в данной архитектуре не существует.

2. В Cortex M3, чтобы выполнить какие-то действия над данными в ОЗУ, их нужно загрузить сначала в регистры, потом произвести над ними какие-то действия, и затем снова переслать их в ОЗУ. Эти операции перемещения занимают много времени, особенно когда данных много, например при обработке звука или изображения. Для того что бы сгладить этот недостаток, используется кеширование, SIMD инструкции и DMA.

3. В Cortex-M3 имеется 13 регистров общего назначения (РОН) R0-R12:

Все регистры делятся на нижние (R0-R7) и верхние (R8-R15). Инструкции из набора Thumb I могут адресовать только нижние регистры. Исключеним является инструкция MOV которая может адресовать в том числе и верхние регистры общего назначения. Инструкции из набора Thumb II могут адресовать все РОН, а так же регистры LR и SP. Регистр R15 имеет является счетчиком команд - PC, R14 - имеет обозначение LR - Link register, в него помещается адрес возврата при вызове подпрограммы. Регистр R13 - является указателем стека - SP, он "с двойным дном", т.е. в зависимости от того, выполняется ли обычная программа, либо прерывание (более строго будет говорить о выполнении кода с повышенными привилегиями, т.е. код который будет исполняться в первую очередь), указатель стека показывает разные значения. Кроме этих 16-регистров, имеется специальный статусный регистр xPSR. Он явлется составным из трех регистров: APSR + IPSR + EPSR.

4. В Cortex-M3 нет инструкций CALL/RET. Вместо них есть переход с сохранением адреса возврата в регистре R14/LR. Собственно: BL и BX. Если подпрограмма содержит в себе вызов другой подпрограммы, то содержимое R14/LR следует сохранить в стеке. Возврат тогда будет по инструкции: POP {PC}. например:

MySubroutine:
    PUSH {LR}       ; сохраняем в стеке значение LR
    BL AnotherSub    ; переход на другую подпрограмму, адрес возврата автоматически сохраняется в LR
    POP {PC}         ; возврат из подпрограммы

4. В Cortex-M3 нет переходов по абсолютному адресу, все переходы относительные. Это означает, что весь код являются перемещаемым.

5. Инструкций INC, DEC в Cortex-M3 так же нет, но вместо них можно использовать инструкции "SUB Reg,#1" и "ADD reg,#1". Они занимают два байта вместе с операндом. Если операнд больше одного байта, то тут потребуется 32-битная инструкция.

5. В Thumb II есть режим индексной адресации с автоинкрементом и автодекриментом для инструкций LDR и STR:

LDR R0, [R1, #4]!   ; пост инкремент : R1 += 4 после выполнения инструкции
STR R0, [R1, #-4]!  ; пост декремент : R1 -= 4 после выполнения инструкции

6. Указатель стека R13/SP выровнен по границе слова, т.е. его младшие два бита аппаратно сброшены в ноль. Указатель стека всегда указывает на последние сохраненные данные:

MOV SP, #0x20005000  ; инициализация SP 
PUSH {R0}            ; SP = 0x20004FFC, R0 сохранен по адресу  [0x20004FFC]
PUSH {R1}            ; SP = 0x20004FF8, R1 сохранен по адресу [0x20004FF8]
POP {R1}             ; R1 = [0x20004FF8], SP = 0x20004FFC

7. Многие инструкции могут работать с группой регистров (reglist). Причем такие операции выполняются одной инструкцией. Например - сохранение контекста:

PUSH {R0-R7, LR}  // сохраняем в стеке девять регистров

Или копирование блока памяти:

loop:
  LDMIA R0!, {R1-R4}
  STMIA R1!, {R1-R4}
  SUBS R2, #16
  BNE loop

8. С помощью суффиксов .b .h .w может указываться размер операнда: байт, полуслово или слово. Например:

LDRH R0, [R1]        // Загрузка полуслова (16-бит)
STRB.W R2, [R3, #4]  // Сохранение байта (8-бит) с  приведением к 32-битному виду

Но существуют инструкции с жестко прописанными размерами данных:

LDR = слово (32-бит)
LDRH = полуслово (16-бит)
LDRB = байт (8-битt)

9. С помощью условного выполнения, некоторые инструкция может быть выполнена или нет в зависимости от значений того или иного флага регистра состояний. В Thumb наборе инструкций, это применяется полько с инструкциями перехода, т.е. так образуются инструкции условного перехода. В наборе инструкций Thumb II с большинством логических и арифметических операций, суффиксы условного выполнения применяться не могут.

10. Добавление суффикса "S" (например ADDS, SUBS), указывает, что команда должна изменить флаги APSR: N, Z, C, V (разные инструкции влиют на разные флаги) на основе результата выполнения.

ADDS R0, R1, R2   ;  Изменяет APSR флаги (N, Z, C, V)
ADD  R0, R1, R2   ;  не изменяет  флаги

Суффикс "S" работает с арифметическими и логическими операторами. Не работает с LDR, STR, B, BX, MSR, MRS. ИНструкции сравнения: CMP, CMN, TST, TEQ, всегда изменяют флаги состояния. Также всегда влиают на них некоторые 16-битные инструкции из набора Thumb-I, например: ADD/SUB. Пример использования суффикса S:

SUBS R0, R0, #1    ; R0=R0-1
BNE  loop          ; переход по метке если  (R0 != 0)

11. В Thumb II наборе инструкций присутствует блок ветвления IT (If-Then), который призван реализовать небольшие ветвления, до 4-х инструкций, не прибегая к инструкциям ветвления и избегая сброса конвейера, т.к. каждый переход по метке влечет задержку в работе процессора на 2-3 машинных цикла. Он работает со статусными флагами: N (признак отрицательного числа), Z (признак нуля), C (перенос), V (переполнение) регистра APSR. Блок может выполняться с 16 суффиксами ветвления (EQ, NE, GT, LT, и др.).

Блок IT имеет следующий синтаксис:

IT{x{y{z}}} <условие>
где:
  x -  T(hen) или E(lse) для первой инструкции;
  y - тоже самое для второй инструкции;
  z - для третьей.
максимум - 4 инструкции.

Пример кода без IT блока

CMP R0, #10 ; выполняется за 1 машинный цикл
BNE skip      ; выполняется за 1 машинный цикл если R0 != 10, и выполняется за 3 машинных цикла, если осуществляется переход по метке.
MOV R1, #100  ; 1 цикл
skip:

Итого, данный код выполняется за 3 или 4 машинных цикла.

Тот же самый код с использованием IT блока:

CMP R0, #10 ; 1 цикл
IT EQ ; 1 цикл
MOVEQ R1, #100 ; 1 цикл, если R1 = 10

Итого: 2-3 машинных цикла.

12. Когда происходит прерывание или исключение, процессор автоматически сохраняет в стеке восемь регистров, процесс называется stacking " Coretx-M3 Technical Reference Manual - глава 5.5.1"

Сохраняются регистры: R0, R1, R2, R3, R12, LR, PC, xPSR. Регистр LR во время выполнения обработчика прерывания содержит адрес выхода из прерывания.

большие адреса
    | xPSR  |
    | PC    |
    | LR    |
    | R12   |
    | R3    |
    | R2    |
    | R1    |
    | R0    | <-- SP after stacking
меньшие адреса

Остальные регистры следует сохранять вручную, если это требуется:

ISR_Handler:
    PUSH {R4-R7, LR}      ; Manual save (R4-R7 and LR if nested calls exist)
    ...                   ; ISR code
    POP {R4-R7, PC}       ; Restore and return (PC replaces LR)
13. Ассемблер ARM поддерживает несколько режимов адресации.

a) Регистровая адресация, когда операндами являются исключительно регистры R0-R15. Пример:

ADD R0, R1, R2   ; R0 = R1 + R2 (все операнды являются регистрами)

б) Непосредственная адресация, когда операнд указан в самой инструкции. При таком типе адресации не требуется обращения к памяти. Например:

MOV R0, #0xAB    ; R0 = 0xAB (регистру R0 присваивается непосредственно само число стоящее в операнде)
ADD R3, #10      ; R3 += 10

в) Прямая (абсолютная) адресация (англ. direct (absolute) addressing) - это режим адресации, в котором адрес данных указан напрямую, без использования каких-либо косвенных указаний или расчетов. Например:

LDR R0, =0x40021000   ; Загрузка значения находящегося в оперативной памяти по адресу 0x40021000 (например RCC register)

г) Косвенно-регистровая адресация (Register Indirect Addressing) - это режим адресации, при котором адрес операнда (данных) хранится в регистре процессора, а не непосредственно в поле адреса инструкции. Например:

LDR R0, [R1]      ; регистру R0 присваивается значение хранящееся по адресу записаному в R1
STR R2, [R3]      ; сохранение значения регистра R2 по адресу записаному в R3

д) Косвенно-регистровая адресация со смещением - это режим адресации, при котором к адресу операнда (данных) прибавляется какое-то смещение. Например:

LDR R0, [R1, #8]    ; R0 = *(R1 + 8)
STR R2, [R3, R4]    ; Сохранение R2 по адресу (R3 + R4)

е) Пре-индексная адресация, когда индексный регистр предварительно увеличивается. Например:

LDR R0, [R1, #4]!   ; раскладывается  две операции: R1 += 4, и  R0 = *R1

ё) Пост-индексная адресация, когда после операции индексный регистр увеличивается. Например:

LDR R0, [R1], #4    ; раскладывается также в две операции: R0 = *R1 и R1 += 4

ж) Адресация относительно регистра PC. В этом случае PC выступает как индексный регистр. Например:

LDR R0, [PC, #0x100]  ; Загрузка в R0 значения по адресу  (PC + 0x100)

з) Адресация относительно регистра SP. В этом случае SP выступает как индексный регистр. Например:

LDR R0, [SP, #8]   ;Загрузка в R0 значения по адресу SP+8
STR  R1, [SP, #-4]!  ; Эквивалентно операции PUSH

Использование SP в качестве индексного довольно ограничено, если вы хотите что-то достать из стека, или изменить хранящиеся там данные, то следует SP скопировать в РОН и уже его использовать в качестве индексного. Например:

MOV  R4, SP         ; Копируем SP в R4
LDR  R0, [R4, R2]   ; Используем R4 как индексный

и) Адресация с блочной загрузкой-выгрузкой, когда в какой-то блок регистров загружается или выгружаются значения из памяти. Например:

LDMIA R0!, {R1-R4}   ; Загрузка в  R1-R4 значений из памяти, при этом R0 используется как  индексный регистр с автоинкрементом, после выполнения инструкции его значение увеличится на 16, т.е. R0 += 16
STMDB SP!, {R5-R7}   ; Аналог PUSH {R5-R7}

Если вам что-то было не понятно, вы можете почитать учебник Козаченко В.Ф.(ред.): "Практический курс микропроцессорной техники на базе процессорных ядер ARM-Cortex-M3/M4/M4F" 2019 М.: МЭИ, там очень дотошно всё разобрано.

4) Таблица векторов

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

Технически, можно отказаться как от использования ассемблера так и от размещения таблицы в "шапке" исходника. Для этого перепишем программу следующим образом:

#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"

#define WAIT 600000

void dummy_loop(volatile uint32_t count) {
    while (--count);
}

int main() {
    // Enable GPIOC clock
    RCC->APB2ENR |= RCC_APB2Periph_GPIOC;

    // Configure PC13 as push-pull output (max speed 2 MHz)
    GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13);
    GPIOC->CRH |= GPIO_CRH_MODE13_1;  // Output mode, max speed 2 MHz (0b10)

    // Super loop to toggle PC13
    while (1) {
        GPIOC->BSRR = GPIO_Pin_13;    // Set PC13
        dummy_loop(WAIT);
        GPIOC->BRR = GPIO_Pin_13;     // Reset PC13
        dummy_loop(WAIT);
    }
}

void fault_irq() {
    while (1);
}

// Vector table (placed at 0x08000000 via linker script)
__attribute__((section(".isr_vector")))
const void* vectors[] = {
    (void*)0x20005000,      // Stack pointer (adjust based on your device's RAM size)
    (void*)(main + 1),      // Reset handler (Thumb mode: address + 1)
    (void*)(fault_irq + 1), // NMI
    (void*)(fault_irq + 1), // HardFault
    (void*)(fault_irq + 1), // MemManage
    (void*)(fault_irq + 1), // BusFault
    (void*)(fault_irq + 1)  // UsageFault
};

Здесь заголовок прошивки был вынесен в отдельную секцию ".isr_vector". Теперь следует эту секцию указать в скрипте компоновщика. Для этого в блоке "SECTIONS", переж разделом ".text" разместить объявление новой секции:

    /* Vector table at the start of ROM memory */
    .isr_vector :
    {
        . = ALIGN(4);
        KEEP(*(.isr_vector))  /* IRQ vector table */
        . = ALIGN(4);
    } >ROM

Под спойлером размещен полный вариант скрипта компоновщика.

/* Highest address of the user mode stack */
_estack = 0x20005000;    /* end of RAM */

/* Memories definition */
MEMORY
{
  RAM (xrw)     : ORIGIN = 0x20000000, LENGTH = 20K
  ROM (rx)      : ORIGIN = 0x8000000, LENGTH = 64K
}

/* Sections */
SECTIONS
{
    /* Vector table at the start of ROM memory */
    .isr_vector :
    {
        . = ALIGN(4);
        KEEP(*(.isr_vector))  /* IRQ vector table */
        . = ALIGN(4);
    } >ROM

  /* The program code and other data into ROM memory */
  .text :
  {
    . = ALIGN(4);
    *(.text)           /* .text sections (code) */
  } >ROM

  /* Constant data into ROM memory*/
  .rodata :
  {
    . = ALIGN(4);
    *(.rodata)         /* .rodata sections (constants, strings, etc.) */
  } >ROM

  /* Initialized data sections into RAM memory */
  .data :
  {
    . = ALIGN(4);
    *(.data)           /* .data sections */
  } >RAM AT> ROM


  /* Uninitialized data section into RAM memory */
  . = ALIGN(4);
  .bss :
  {
    *(.bss)
    *(COMMON)

    . = ALIGN(4);
    _ebss = .;         /* define a global symbol at bss end */
    __bss_end__ = _ebss;
  } >RAM

}

Теперь, после компиляции, утилита "size" выдаст такое распределение кода:

$ arm-none-eabi-size --format=berkeley firmware.elf
   text    data     bss     dec     hex filename
     80      28       0     108      6c firmware.elf

Размер прошивки не изменился - 108 байт, но 28 байт заголовка перешли из секции "text" в секцию "data". Теперь, если дизассемблировать прошивку командой "arm-none-eabi-objdump -S", то таблицу даже не покажет, т.к. это не код. Но в том, что заголовок прошивки скомпоновался по адресу "0x8000000", можно убедиться с помощью команд:

$ arm-none-eabi-objdump -h firmware.elf |head 

firmware.elf:     file format elf32-littlearm

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .isr_vector   0000001c  08000000  08000000  00010000  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  1 .text         00000000  0800001c  0800001c  0001001c  2**1
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  2 .text.fault_irq 00000002  0800001c  0800001c  0001001c  2**1

или

$ arm-none-eabi-objdump -s -j .isr_vector firmware.elf

firmware.elf:     file format elf32-littlearm

Contents of section .isr_vector:
 8000000 00500020 31000008 1d000008 1d000008  .P. 1...........
 8000010 1d000008 1d000008 1d000008           ............    

или

$ xxd -g 4 firmware.bin | head
00000000: 00500020 31000008 1d000008 1d000008  .P. 1...........
00000010: 1d000008 1d000008 1d000008 fee782b0  ................
00000020: 0190019b 013b0193 002bfad1 02b07047  .....;...+....pG
00000030: 4ff40051 0b4a08b5 93690b48 43f01003  O..Q.J...i.HC...
00000040: 9361a2f5 80325368 23f47003 53605368  .a...2Sh#.p.S`Sh
00000050: 43f40013 53601161 fff7e1ff 5161fff7  C...S`.a....Qa..
00000060: defff8e7 00100240 c0270900           .......@.'..

Теперь можно вынести таблицу прерываний в отдельный модуль, и подключить его к проекту. Однако я предлагаю не изобретать велосипед, а привести проект, как принято говорить: "к стандартам принятым в индустрии".

В частности, я предлагаю подключить к проекту готовый "startup.s" - это ассемблерный файл, состоящий из полной таблицы прерываний STM32F103. С нашей стороны потребуется создание файлов с правилами сборки проекта Makefile, CMakeLists.txt и Qbs-скрипта.

В STM32F103 довольно обширная таблица прерываний, состоящая из 70-векторов обработчиков и еще шести зарезервированных (Reserved):

Итого 76 * 4 = 304 байта. Я взял готовый "startup.s" из шаблонного проекта SW4STM32, который содержит полную таблицу векторов и стартовый код:

/**
  ******************************************************************************
  * @file      startup_stm32.s dedicated to STM32F103C8Tx device
  * @author    Ac6
  * @version   V1.0.0
  * @date      2021-06-30
  ******************************************************************************
  */


.syntax unified
.cpu cortex-m3
.fpu softvfp
.thumb

.global g_pfnVectors
.global Default_Handler

/* start address for the initialization values of the .data section.
defined in linker script */
.word _sidata
/* start address for the .data section. defined in linker script */
.word _sdata
/* end address for the .data section. defined in linker script */
.word _edata
/* start address for the .bss section. defined in linker script */
.word _sbss
/* end address for the .bss section. defined in linker script */
.word _ebss

/**
 * @brief  This is the code that gets called when the processor first
 *          starts execution following a reset event. Only the absolutely
 *          necessary set is performed, after which the application
 *          supplied main() routine is called.
 * @param  None
 * @retval : None
*/

  .section .text.Reset_Handler
  .weak Reset_Handler
  .type Reset_Handler, %function
Reset_Handler:
  ldr   r0, =_estack
  mov   sp, r0          /* set stack pointer */

/* Copy the data segment initializers from flash to SRAM */
  ldr r0, =_sdata
  ldr r1, =_edata
  ldr r2, =_sidata
  movs r3, #0
  b LoopCopyDataInit

CopyDataInit:
  ldr r4, [r2, r3]
  str r4, [r0, r3]
  adds r3, r3, #4

LoopCopyDataInit:
  adds r4, r0, r3
  cmp r4, r1
  bcc CopyDataInit

/* Zero fill the bss segment. */
  ldr r2, =_sbss
  ldr r4, =_ebss
  movs r3, #0
  b LoopFillZerobss

FillZerobss:
  str  r3, [r2]
  adds r2, r2, #4

LoopFillZerobss:
  cmp r2, r4
  bcc FillZerobss

/* Call the clock system intitialization function.*/
//  bl  SystemInit
/* Call static constructors */
//  bl __libc_init_array
/* Call the application's entry point.*/
  bl main

LoopForever:
    b LoopForever


.size Reset_Handler, .-Reset_Handler

/**
 * @brief  This is the code that gets called when the processor receives an
 *         unexpected interrupt.  This simply enters an infinite loop, preserving
 *         the system state for examination by a debugger.
 *
 * @param  None
 * @retval : None
*/
    .section .text.Default_Handler,"ax",%progbits
Default_Handler:
Infinite_Loop:
  b Infinite_Loop
  .size Default_Handler, .-Default_Handler
/******************************************************************************
*
* The STM32F103C8Tx vector table.  Note that the proper constructs
* must be placed on this to ensure that it ends up at physical address
* 0x0000.0000.
*
******************************************************************************/
   .section .isr_vector,"a",%progbits
  .type g_pfnVectors, %object
  .size g_pfnVectors, .-g_pfnVectors


g_pfnVectors:
  .word _estack
  .word Reset_Handler
  .word NMI_Handler
  .word HardFault_Handler
  .word MemManage_Handler
  .word BusFault_Handler
  .word UsageFault_Handler
  .word 0
  .word 0
  .word 0
  .word 0
  .word SVC_Handler
  .word DebugMon_Handler
  .word 0
  .word PendSV_Handler
  .word SysTick_Handler
  .word WWDG_IRQHandler                     /* Window Watchdog interrupt                        */
  .word PVD_IRQHandler                      /* PVD through EXTI line detection interrupt        */
  .word TAMPER_IRQHandler                   /* Tamper interrupt                                 */
  .word RTC_IRQHandler                      /* RTC global interrupt                             */
  .word FLASH_IRQHandler                    /* Flash global interrupt                           */
  .word RCC_IRQHandler                      /* RCC global interrupt                             */
  .word EXTI0_IRQHandler                    /* EXTI Line0 interrupt                             */
  .word EXTI1_IRQHandler                    /* EXTI Line1 interrupt                             */
  .word EXTI2_IRQHandler                    /* EXTI Line2 interrupt                             */
  .word EXTI3_IRQHandler                    /* EXTI Line3 interrupt                             */
  .word EXTI4_IRQHandler                    /* EXTI Line4 interrupt                             */
  .word DMA1_Channel1_IRQHandler            /* DMA1 Channel1 global interrupt                   */
  .word DMA1_Channel2_IRQHandler            /* DMA1 Channel2 global interrupt                   */
  .word DMA1_Channel3_IRQHandler            /* DMA1 Channel3 global interrupt                   */
  .word DMA1_Channel4_IRQHandler            /* DMA1 Channel4 global interrupt                   */
  .word DMA1_Channel5_IRQHandler            /* DMA1 Channel5 global interrupt                   */
  .word DMA1_Channel6_IRQHandler            /* DMA1 Channel6 global interrupt                   */
  .word DMA1_Channel7_IRQHandler            /* DMA1 Channel7 global interrupt                   */
  .word ADC1_2_IRQHandler                   /* ADC1 and ADC2 global interrupt                   */
  .word USB_HP_CAN_TX_IRQHandler            /* USB High Priority or CAN TX interrupts           */
  .word USB_LP_CAN_RX0_IRQHandler           /* USB Low Priority or CAN RX0 interrupts           */
  .word CAN_RX1_IRQHandler                  /* CAN RX1 interrupt                                */
  .word CAN_SCE_IRQHandler                  /* CAN SCE interrupt                                */
  .word EXTI9_5_IRQHandler                  /* EXTI Line[9:5] interrupts                        */
  .word TIM1_BRK_IRQHandler                 /* TIM1 Break interrupt                             */
  .word TIM1_UP_IRQHandler                  /* TIM1 Update interrupt                            */
  .word TIM1_TRG_COM_IRQHandler             /* TIM1 Trigger and Commutation interrupts          */
  .word TIM1_CC_IRQHandler                  /* TIM1 Capture Compare interrupt                   */
  .word TIM2_IRQHandler                     /* TIM2 global interrupt                            */
  .word TIM3_IRQHandler                     /* TIM3 global interrupt                            */
  .word TIM4_IRQHandler                     /* TIM4 global interrupt                            */
  .word I2C1_EV_IRQHandler                  /* I2C1 event interrupt                             */
  .word I2C1_ER_IRQHandler                  /* I2C1 error interrupt                             */
  .word I2C2_EV_IRQHandler                  /* I2C2 event interrupt                             */
  .word I2C2_ER_IRQHandler                  /* I2C2 error interrupt                             */
  .word SPI1_IRQHandler                     /* SPI1 global interrupt                            */
  .word SPI2_IRQHandler                     /* SPI2 global interrupt                            */
  .word USART1_IRQHandler                   /* USART1 global interrupt                          */
  .word USART2_IRQHandler                   /* USART2 global interrupt                          */
  .word USART3_IRQHandler                   /* USART3 global interrupt                          */
  .word EXTI15_10_IRQHandler                /* EXTI Line[15:10] interrupts                      */
  .word RTCAlarm_IRQHandler                 /* RTC Alarms through EXTI line interrupt           */
  .word 0                                   /* Reserved                                         */
  .word TIM8_BRK_IRQHandler                 /* TIM8 Break interrupt                             */
  .word TIM8_UP_IRQHandler                  /* TIM8 Update interrupt                            */
  .word TIM8_TRG_COM_IRQHandler             /* TIM8 Trigger and Commutation interrupts          */
  .word TIM8_CC_IRQHandler                  /* TIM8 Capture Compare interrupt                   */
  .word ADC3_IRQHandler                     /* ADC3 global interrupt                            */
  .word FSMC_IRQHandler                     /* FSMC global interrupt                            */
  .word SDIO_IRQHandler                     /* SDIO global interrupt                            */
  .word TIM5_IRQHandler                     /* TIM5 global interrupt                            */
  .word SPI3_IRQHandler                     /* SPI3 global interrupt                            */
  .word UART4_IRQHandler                    /* UART4 global interrupt                           */
  .word UART5_IRQHandler                    /* UART5 global interrupt                           */
  .word TIM6_IRQHandler                     /* TIM6 global interrupt                            */
  .word TIM7_IRQHandler                     /* TIM7 global interrupt                            */
  .word DMA2_Channel1_IRQHandler            /* DMA2 Channel1 global interrupt                   */
  .word DMA2_Channel2_IRQHandler            /* DMA2 Channel2 global interrupt                   */
  .word DMA2_Channel3_IRQHandler            /* DMA2 Channel3 global interrupt                   */
  .word DMA2_Channel4_5_IRQHandler          /* DMA2 Channel4 and DMA2 Channel5 global interrupt */

/*******************************************************************************
*
* Provide weak aliases for each Exception handler to the Default_Handler.
* As they are weak aliases, any function with the same name will override
* this definition.
*
*******************************************************************************/

    .weak   NMI_Handler
    .thumb_set NMI_Handler,Default_Handler

    .weak   HardFault_Handler
    .thumb_set HardFault_Handler,Default_Handler

    .weak   MemManage_Handler
    .thumb_set MemManage_Handler,Default_Handler

    .weak   BusFault_Handler
    .thumb_set BusFault_Handler,Default_Handler

    .weak   UsageFault_Handler
    .thumb_set UsageFault_Handler,Default_Handler

    .weak   SVC_Handler
    .thumb_set SVC_Handler,Default_Handler

    .weak   DebugMon_Handler
    .thumb_set DebugMon_Handler,Default_Handler

    .weak   PendSV_Handler
    .thumb_set PendSV_Handler,Default_Handler

    .weak   SysTick_Handler
    .thumb_set SysTick_Handler,Default_Handler

    .weak   WWDG_IRQHandler
    .thumb_set WWDG_IRQHandler,Default_Handler

    .weak   PVD_IRQHandler
    .thumb_set PVD_IRQHandler,Default_Handler

    .weak   TAMPER_IRQHandler
    .thumb_set TAMPER_IRQHandler,Default_Handler

    .weak   RTC_IRQHandler
    .thumb_set RTC_IRQHandler,Default_Handler

    .weak   FLASH_IRQHandler
    .thumb_set FLASH_IRQHandler,Default_Handler

    .weak   RCC_IRQHandler
    .thumb_set RCC_IRQHandler,Default_Handler

    .weak   EXTI0_IRQHandler
    .thumb_set EXTI0_IRQHandler,Default_Handler

    .weak   EXTI1_IRQHandler
    .thumb_set EXTI1_IRQHandler,Default_Handler

    .weak   EXTI2_IRQHandler
    .thumb_set EXTI2_IRQHandler,Default_Handler

    .weak   EXTI3_IRQHandler
    .thumb_set EXTI3_IRQHandler,Default_Handler

    .weak   EXTI4_IRQHandler
    .thumb_set EXTI4_IRQHandler,Default_Handler

    .weak   DMA1_Channel1_IRQHandler
    .thumb_set DMA1_Channel1_IRQHandler,Default_Handler

    .weak   DMA1_Channel2_IRQHandler
    .thumb_set DMA1_Channel2_IRQHandler,Default_Handler

    .weak   DMA1_Channel3_IRQHandler
    .thumb_set DMA1_Channel3_IRQHandler,Default_Handler

    .weak   DMA1_Channel4_IRQHandler
    .thumb_set DMA1_Channel4_IRQHandler,Default_Handler

    .weak   DMA1_Channel5_IRQHandler
    .thumb_set DMA1_Channel5_IRQHandler,Default_Handler

    .weak   DMA1_Channel6_IRQHandler
    .thumb_set DMA1_Channel6_IRQHandler,Default_Handler

    .weak   DMA1_Channel7_IRQHandler
    .thumb_set DMA1_Channel7_IRQHandler,Default_Handler

    .weak   ADC1_2_IRQHandler
    .thumb_set ADC1_2_IRQHandler,Default_Handler

    .weak   USB_HP_CAN_TX_IRQHandler
    .thumb_set USB_HP_CAN_TX_IRQHandler,Default_Handler

    .weak   USB_LP_CAN_RX0_IRQHandler
    .thumb_set USB_LP_CAN_RX0_IRQHandler,Default_Handler

    .weak   CAN_RX1_IRQHandler
    .thumb_set CAN_RX1_IRQHandler,Default_Handler

    .weak   CAN_SCE_IRQHandler
    .thumb_set CAN_SCE_IRQHandler,Default_Handler

    .weak   EXTI9_5_IRQHandler
    .thumb_set EXTI9_5_IRQHandler,Default_Handler

    .weak   TIM1_BRK_IRQHandler
    .thumb_set TIM1_BRK_IRQHandler,Default_Handler

    .weak   TIM1_UP_IRQHandler
    .thumb_set TIM1_UP_IRQHandler,Default_Handler

    .weak   TIM1_TRG_COM_IRQHandler
    .thumb_set TIM1_TRG_COM_IRQHandler,Default_Handler

    .weak   TIM1_CC_IRQHandler
    .thumb_set TIM1_CC_IRQHandler,Default_Handler

    .weak   TIM2_IRQHandler
    .thumb_set TIM2_IRQHandler,Default_Handler

    .weak   TIM3_IRQHandler
    .thumb_set TIM3_IRQHandler,Default_Handler

    .weak   TIM4_IRQHandler
    .thumb_set TIM4_IRQHandler,Default_Handler

    .weak   I2C1_EV_IRQHandler
    .thumb_set I2C1_EV_IRQHandler,Default_Handler

    .weak   I2C1_ER_IRQHandler
    .thumb_set I2C1_ER_IRQHandler,Default_Handler

    .weak   I2C2_EV_IRQHandler
    .thumb_set I2C2_EV_IRQHandler,Default_Handler

    .weak   I2C2_ER_IRQHandler
    .thumb_set I2C2_ER_IRQHandler,Default_Handler

    .weak   SPI1_IRQHandler
    .thumb_set SPI1_IRQHandler,Default_Handler

    .weak   SPI2_IRQHandler
    .thumb_set SPI2_IRQHandler,Default_Handler

    .weak   USART1_IRQHandler
    .thumb_set USART1_IRQHandler,Default_Handler

    .weak   USART2_IRQHandler
    .thumb_set USART2_IRQHandler,Default_Handler

    .weak   USART3_IRQHandler
    .thumb_set USART3_IRQHandler,Default_Handler

    .weak   EXTI15_10_IRQHandler
    .thumb_set EXTI15_10_IRQHandler,Default_Handler

    .weak   RTCAlarm_IRQHandler
    .thumb_set RTCAlarm_IRQHandler,Default_Handler

    .weak   TIM8_BRK_IRQHandler
    .thumb_set TIM8_BRK_IRQHandler,Default_Handler

    .weak   TIM8_UP_IRQHandler
    .thumb_set TIM8_UP_IRQHandler,Default_Handler

    .weak   TIM8_TRG_COM_IRQHandler
    .thumb_set TIM8_TRG_COM_IRQHandler,Default_Handler

    .weak   TIM8_CC_IRQHandler
    .thumb_set TIM8_CC_IRQHandler,Default_Handler

    .weak   ADC3_IRQHandler
    .thumb_set ADC3_IRQHandler,Default_Handler

    .weak   FSMC_IRQHandler
    .thumb_set FSMC_IRQHandler,Default_Handler

    .weak   SDIO_IRQHandler
    .thumb_set SDIO_IRQHandler,Default_Handler

    .weak   TIM5_IRQHandler
    .thumb_set TIM5_IRQHandler,Default_Handler

    .weak   SPI3_IRQHandler
    .thumb_set SPI3_IRQHandler,Default_Handler

    .weak   UART4_IRQHandler
    .thumb_set UART4_IRQHandler,Default_Handler

    .weak   UART5_IRQHandler
    .thumb_set UART5_IRQHandler,Default_Handler

    .weak   TIM6_IRQHandler
    .thumb_set TIM6_IRQHandler,Default_Handler

    .weak   TIM7_IRQHandler
    .thumb_set TIM7_IRQHandler,Default_Handler

    .weak   DMA2_Channel1_IRQHandler
    .thumb_set DMA2_Channel1_IRQHandler,Default_Handler

    .weak   DMA2_Channel2_IRQHandler
    .thumb_set DMA2_Channel2_IRQHandler,Default_Handler

    .weak   DMA2_Channel3_IRQHandler
    .thumb_set DMA2_Channel3_IRQHandler,Default_Handler

    .weak   DMA2_Channel4_5_IRQHandler
    .thumb_set DMA2_Channel4_5_IRQHandler,Default_Handler

    .weak   SystemInit

/************************ (C) COPYRIGHT Ac5 *****END OF FILE****/

Кроме таблицы векторов здесь содержится "стартовый" код который содержится в обработчике прерывания "Reset":

Reset_Handler:
  ldr   r0, =_estack
  mov   sp, r0          /* set stack pointer */

/* Copy the data segment initializers from flash to SRAM */
  ldr r0, =_sdata
  ldr r1, =_edata
  ldr r2, =_sidata
  movs r3, #0
  b LoopCopyDataInit

CopyDataInit:
  ldr r4, [r2, r3]
  str r4, [r0, r3]
  adds r3, r3, #4

LoopCopyDataInit:
  adds r4, r0, r3
  cmp r4, r1
  bcc CopyDataInit

/* Zero fill the bss segment. */
  ldr r2, =_sbss
  ldr r4, =_ebss
  movs r3, #0
  b LoopFillZerobss

FillZerobss:
  str  r3, [r2]
  adds r2, r2, #4

LoopFillZerobss:
  cmp r2, r4
  bcc FillZerobss

Здесь два цикла. В первом цикле копируются данные из секции "data" в оперативную память, и тем самым, происходит инициализация глобальных переменных теми значениями, что были заданы в тексте программы. Второй цикл заполняет нулями bss секцию. В режиме отладки я запускал трассировку, и в случае данной программы, выполнение обоих циклов игнорировалось. После завершения стартового кода, происходит переход на функции SystemInit и __libc_init_array. Первая функция настраивает систему тактирования, которой пока в проекте нет, поэтому я ее закомментировал. Вторая функция находится в стандартной библиотеке, которой тоже нет, поэтому она тоже закомментирована. В финале, происходит переход на функцию "main".

Также я взял полный, неурезанный скрипт компоновщика:

/*
******************************************************************************
**
**  File        : LinkerScript.ld
**
**  Author      : Auto-generated by Ac6 System Workbench
**
**  Abstract    : Linker script for STM32F103C8Tx Device from STM32F1 series
**                20Kbytes RAM
**                64Kbytes ROM
**
**                Set heap size, stack size and stack location according
**                to application requirements.
**
**                Set memory bank area and size if external memory is used.
**
**  Target      : STMicroelectronics STM32
**
**  Distribution: The file is distributed �as is,� without any warranty
**                of any kind.
**
*****************************************************************************
** @attention
**
** <h2><center>&copy; COPYRIGHT(c) 2021 Ac6</center></h2>
**
** Redistribution and use in source and binary forms, with or without modification,
** are permitted provided that the following conditions are met:
**   1. Redistributions of source code must retain the above copyright notice,
**      this list of conditions and the following disclaimer.
**   2. Redistributions in binary form must reproduce the above copyright notice,
**      this list of conditions and the following disclaimer in the documentation
**      and/or other materials provided with the distribution.
**   3. Neither the name of Ac6 nor the names of its contributors
**      may be used to endorse or promote products derived from this software
**      without specific prior written permission.
**
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
** AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
** DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
** OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**
*****************************************************************************
*/

/* Entry Point */
ENTRY(Reset_Handler)

/* Highest address of the user mode stack */
_estack = 0x20005000;    /* end of 20K RAM */

_Min_Heap_Size = 0;      /* required amount of heap  */
_Min_Stack_Size = 0x100; /* required amount of stack */

/* Memories definition */
MEMORY
{
  RAM (xrw)     : ORIGIN = 0x20000000, LENGTH = 20K
  ROM (rx)      : ORIGIN = 0x8000000, LENGTH = 64K
}

/* Sections */
SECTIONS
{
  /* The startup code into ROM memory */
  .isr_vector :
  {
    . = ALIGN(4);
    KEEP(*(.isr_vector)) /* Startup code */
    . = ALIGN(4);
  } >ROM

  /* The program code and other data into ROM memory */
  .text :
  {
    . = ALIGN(4);
    *(.text)           /* .text sections (code) */
    *(.text*)          /* .text* sections (code) */
    *(.glue_7)         /* glue arm to thumb code */
    *(.glue_7t)        /* glue thumb to arm code */
    *(.eh_frame)

    KEEP (*(.init))
    KEEP (*(.fini))

    . = ALIGN(4);
    _etext = .;        /* define a global symbols at end of code */
  } >ROM

  /* Constant data into ROM memory*/
  .rodata :
  {
    . = ALIGN(4);
    *(.rodata)         /* .rodata sections (constants, strings, etc.) */
    *(.rodata*)        /* .rodata* sections (constants, strings, etc.) */
    . = ALIGN(4);
  } >ROM

  .ARM.extab   : {
    . = ALIGN(4);
    *(.ARM.extab* .gnu.linkonce.armextab.*)
    . = ALIGN(4);
  } >ROM

  .ARM : {
    . = ALIGN(4);
    __exidx_start = .;
    *(.ARM.exidx*)
    __exidx_end = .;
    . = ALIGN(4);
  } >ROM

  .preinit_array     :
  {
    . = ALIGN(4);
    PROVIDE_HIDDEN (__preinit_array_start = .);
    KEEP (*(.preinit_array*))
    PROVIDE_HIDDEN (__preinit_array_end = .);
    . = ALIGN(4);
  } >ROM

  .init_array :
  {
    . = ALIGN(4);
    PROVIDE_HIDDEN (__init_array_start = .);
    KEEP (*(SORT(.init_array.*)))
    KEEP (*(.init_array*))
    PROVIDE_HIDDEN (__init_array_end = .);
    . = ALIGN(4);
  } >ROM

  .fini_array :
  {
    . = ALIGN(4);
    PROVIDE_HIDDEN (__fini_array_start = .);
    KEEP (*(SORT(.fini_array.*)))
    KEEP (*(.fini_array*))
    PROVIDE_HIDDEN (__fini_array_end = .);
    . = ALIGN(4);
  } >ROM

  /* Used by the startup to initialize data */
  _sidata = LOADADDR(.data);

  /* Initialized data sections into RAM memory */
  .data :
  {
    . = ALIGN(4);
    _sdata = .;        /* create a global symbol at data start */
    *(.data)           /* .data sections */
    *(.data*)          /* .data* sections */

    . = ALIGN(4);
    _edata = .;        /* define a global symbol at data end */
  } >RAM AT> ROM


  /* Uninitialized data section into RAM memory */
  . = ALIGN(4);
  .bss :
  {
    /* This is used by the startup in order to initialize the .bss secion */
    _sbss = .;         /* define a global symbol at bss start */
    __bss_start__ = _sbss;
    *(.bss)
    *(.bss*)
    *(COMMON)

    . = ALIGN(4);
    _ebss = .;         /* define a global symbol at bss end */
    __bss_end__ = _ebss;
  } >RAM

  /* User_heap_stack section, used to check that there is enough RAM left */
  ._user_heap_stack :
  {
    . = ALIGN(8);
    PROVIDE ( end = . );
    PROVIDE ( _end = . );
    . = . + _Min_Heap_Size;
    . = . + _Min_Stack_Size;
    . = ALIGN(8);
  } >RAM



  /* Remove information from the compiler libraries*/

  /DISCARD/ :
  {
    libc.a ( * )
    libm.a ( * )
    libgcc.a ( * )
  }

  .ARM.attributes 0 : { *(.ARM.attributes) }
}

Модуль "main.c" теперь будет выглядеть так:

#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"

#define WAIT 600000

void dummy_loop(volatile uint32_t count) {
    while (--count);
}

int main() {
    // Enable GPIOC clock
    RCC->APB2ENR |= RCC_APB2Periph_GPIOC;

    // Configure PC13 as push-pull output (max speed 2 MHz)
    GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13);
    GPIOC->CRH |= GPIO_CRH_MODE13_1;  // Output mode, max speed 2 MHz (0b10)

    // Super loop to toggle PC13
    while (1) {
        GPIOC->BSRR = GPIO_Pin_13;    // Set PC13
        dummy_loop(WAIT);
        GPIOC->BRR = GPIO_Pin_13;     // Reset PC13
        dummy_loop(WAIT);
    }
}

Итак, имеются файлы: "main.c", "startup.s" и "stm32f10xx8.ld". Теперь это все вместе надо собрать в единую прошивку. Сделать это можно разными способами.

Одной командой это делается довольно просто:

$ arm-none-eabi-gcc -T stm32f10xx8.ld  -nostartfiles -mcpu=cortex-m3 -g -Os -mthumb -DSTM32F10X_MD -I ./CMSIS/device -I ./CMSIS/core -I ./SPL/inc -o firmware.elf main.c ./asm/startup.s
s arm-none-eabi-objcopy -O binary firmware.elf firmware.bin
$ arm-none-eabi-size ./firmware.elf
   text    data     bss     dec     hex filename
    480       0     256     736     2e0 ./firmware.elf

Прошивка весит 480 байт, bss область в прошивку не входит, она располагается в оперативной памяти. Проверяем таблицу векторов:

$ arm-none-eabi-objdump -s -j .isr_vector firmware.elf

firmware.elf:     file format elf32-littlearm

Contents of section .isr_vector:
 8000000 00500020 95010008 dd010008 dd010008  .P. ............
 8000010 dd010008 dd010008 dd010008 00000000  ................
 8000020 00000000 00000000 00000000 dd010008  ................
 8000030 dd010008 00000000 dd010008 dd010008  ................
 8000040 dd010008 dd010008 dd010008 dd010008  ................
 8000050 dd010008 dd010008 dd010008 dd010008  ................
 8000060 dd010008 dd010008 dd010008 dd010008  ................
 8000070 dd010008 dd010008 dd010008 dd010008  ................
 8000080 dd010008 dd010008 dd010008 dd010008  ................
 8000090 dd010008 dd010008 dd010008 dd010008  ................
 80000a0 dd010008 dd010008 dd010008 dd010008  ................
 80000b0 dd010008 dd010008 dd010008 dd010008  ................
 80000c0 dd010008 dd010008 dd010008 dd010008  ................
 80000d0 dd010008 dd010008 dd010008 dd010008  ................
 80000e0 dd010008 dd010008 00000000 dd010008  ................
 80000f0 dd010008 dd010008 dd010008 dd010008  ................
 8000100 dd010008 dd010008 dd010008 dd010008  ................
 8000110 dd010008 dd010008 dd010008 dd010008  ................
 8000120 dd010008 dd010008 dd010008 dd010008  ................
 

0x130 - это 304 байта, на этот раз полная таблица векторов.

Для того, чтобы составлять скрипты сборки проекта, сперва следует уточнить структуру каталогов проекта. Я привык использовать следующую структуру проекта:

project/
├── main.c                  # в корне всегда файл "main.c"
├── Makefile
├── stm32f10xx8.ld
├── inc/
│   └── (заголовочные файлы)
├── src/
│   └── (остальные Си-файлы проекта)
└── asm/
    └── (ассемблерные файлы проекта)

Самый простой Makefile для такого проекта у меня вышел следующим:

# Toolchain setup
CC = arm-none-eabi-gcc
AS = arm-none-eabi-gcc
OBJCOPY = arm-none-eabi-objcopy
SIZE = arm-none-eabi-size
RM = rm -f

# Target and output names
TARGET = firmware
BIN = $(TARGET).bin
HEX = $(TARGET).hex
ELF = $(TARGET).elf

# Directory structure
BUILD_DIR = build
SRC_DIR = src
ASM_DIR = asm
INC_DIR = inc

# Source files
SRCS_C = main.c
SRCS_C += $(wildcard $(SRC_DIR)/*.c)
SRCS_S = $(wildcard $(ASM_DIR)/*.s)
OBJS += $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.o,$(SRCS_C)) \
       $(patsubst $(ASM_DIR)/%.s,$(BUILD_DIR)/%.o,$(SRCS_S))

# Include paths
INC = -I$(INC_DIR) -ICMSIS/device -ICMSIS/core -ISPL/inc
DEF = -DSTM32F10X_MD

# MCU and flags
MCU = cortex-m3
CFLAGS = -mcpu=$(MCU) -mthumb -Wall -g -Os -ffunction-sections -fdata-sections $(INC) $(DEF)
ASFLAGS = -mcpu=$(MCU) -mthumb -g
LDFLAGS = -T stm32f10xx8.ld -nostartfiles -Wl,--gc-sections -specs=nano.specs

# Build rules
all: $(BUILD_DIR)/$(BIN) $(BUILD_DIR)/$(HEX) size

$(BUILD_DIR)/$(ELF): $(OBJS)
	@mkdir -p $(@D)
	$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)

# Rule for C files
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c
	@mkdir -p $(@D)
	$(CC) -c $(CFLAGS) $< -o $@

# Rule for assembly files
$(BUILD_DIR)/%.o: $(ASM_DIR)/%.s
	@mkdir -p $(@D)
	$(AS) -c $(ASFLAGS) $< -o $@

$(BUILD_DIR)/$(BIN): $(BUILD_DIR)/$(ELF)
	$(OBJCOPY) -O binary $< $@

$(BUILD_DIR)/$(HEX): $(BUILD_DIR)/$(ELF)
	$(OBJCOPY) -O ihex $< $@

size: $(BUILD_DIR)/$(ELF)
	$(SIZE) --format=berkeley $<

flash: $(BUILD_DIR)/$(BIN)
	st-flash write $< 0x08000000

clean:
	$(RM) -r $(BUILD_DIR)

.PHONY: all size flash clean

Сборка:

$ make 
arm-none-eabi-gcc -c -mcpu=cortex-m3 -mthumb -g asm/startup.s -o build/startup.o
arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -Wall -g -Os -ffunction-sections -fdata-sections -Iinc -ICMSIS/device -ICMSIS/core -ISPL/inc -DSTM32F10X_MD main.c build/startup.o -o build/firmware.elf -T stm32f10xx8.ld -nostartfiles -Wl,--gc-sections -specs=nano.specs
arm-none-eabi-objcopy -O binary build/firmware.elf build/firmware.bin
arm-none-eabi-objcopy -O ihex build/firmware.elf build/firmware.hex
arm-none-eabi-size --format=berkeley build/firmware.elf
   text    data     bss     dec     hex filename
    460       0     256     716     2cc build/firmware.elf

Здесь довольно странная конструкция получилась, когда смешана компиляция и компоновка. Я бы хотел, что бы компиляция и компоновка были разделены.

В конечном итоге, я остановился на следующем варианте Makefile:

# Toolchain setup
CC = arm-none-eabi-gcc
AS = arm-none-eabi-gcc
OBJCOPY = arm-none-eabi-objcopy
SIZE = arm-none-eabi-size
RM = rm -f

# Target and output names
TARGET = firmware
BIN = $(TARGET).bin
HEX = $(TARGET).hex
ELF = $(TARGET).elf

# Directory structure
BUILD_DIR = build
SRC_DIR = src
ASM_DIR = asm
INC_DIR = inc
PROJECT_ROOT = .

# Source files
SRCS_C = $(wildcard $(SRC_DIR)/*.c) $(wildcard $(PROJECT_ROOT)/main.c)
SRCS_S = $(wildcard $(ASM_DIR)/*.s)
OBJS = $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.o,$(filter $(SRC_DIR)/%,$(SRCS_C))) \
       $(patsubst $(PROJECT_ROOT)/%.c,$(BUILD_DIR)/%.o,$(filter $(PROJECT_ROOT)/%,$(SRCS_C))) \
       $(patsubst $(ASM_DIR)/%.s,$(BUILD_DIR)/%.o,$(SRCS_S))

# Include paths
INC = -I$(INC_DIR) -I$(PROJECT_ROOT) -ICMSIS/device -ICMSIS/core -ISPL/inc
DEF = -DSTM32F10X_MD

# MCU and flags
MCU = cortex-m3
CFLAGS = -mcpu=$(MCU) -mthumb -Wall -g -Os -ffunction-sections -fdata-sections $(INC) $(DEF)
ASFLAGS = -mcpu=$(MCU) -mthumb -g
LDFLAGS = -T stm32f10xx8.ld -nostartfiles -Wl,--gc-sections -specs=nano.specs

# Build rules
all: $(BUILD_DIR)/$(BIN) $(BUILD_DIR)/$(HEX) size

$(BUILD_DIR)/$(ELF): $(OBJS)
	@mkdir -p $(@D)
	$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)

# Rule for C files in src directory
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c
	@mkdir -p $(@D)
	$(CC) -c $(CFLAGS) $< -o $@

# Rule for main.c in project root
$(BUILD_DIR)/%.o: $(PROJECT_ROOT)/%.c
	@mkdir -p $(@D)
	$(CC) -c $(CFLAGS) $< -o $@

# Rule for assembly files
$(BUILD_DIR)/%.o: $(ASM_DIR)/%.s
	@mkdir -p $(@D)
	$(AS) -c $(ASFLAGS) $< -o $@

$(BUILD_DIR)/$(BIN): $(BUILD_DIR)/$(ELF)
	$(OBJCOPY) -O binary $< $@

$(BUILD_DIR)/$(HEX): $(BUILD_DIR)/$(ELF)
	$(OBJCOPY) -O ihex $< $@

size: $(BUILD_DIR)/$(ELF)
	$(SIZE) --format=berkeley $<

flash: $(BUILD_DIR)/$(BIN)
	st-flash write $< 0x08000000

clean:
	$(RM) -r $(BUILD_DIR)

.PHONY: all size flash clean

Makefile будет примерно в два раза меньше, если все исходники хранить в "src" каталоге, но как я говорил, я привык именно к такой структуре проекта:

project/
├── main.c                  # в корне всегда файл "main.c"
├── Makefile
├── stm32f10xx8.ld
├── inc/
│   └── (заголовочные файлы)
├── src/
│   └── (остальные Си-файлы проекта)
└── asm/
    └── (ассемблерные файлы проекта)

Сборка проекта:

$  make clean && make
rm -f -r build
arm-none-eabi-gcc -c -mcpu=cortex-m3 -mthumb -g asm/startup.s -o build/startup.o
arm-none-eabi-gcc -c -mcpu=cortex-m3 -mthumb -Wall -g -Os -ffunction-sections -fdata-sections -Iinc -I. -ICMSIS/device -ICMSIS/core -ISPL/inc -DSTM32F10X_MD main.c -o build/main.o
arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -Wall -g -Os -ffunction-sections -fdata-sections -Iinc -I. -ICMSIS/device -ICMSIS/core -ISPL/inc -DSTM32F10X_MD build/main.o build/startup.o -o build/firmware.elf -T stm32f10xx8.ld -nostartfiles -Wl,--gc-sections -specs=nano.specs
arm-none-eabi-objcopy -O binary build/firmware.elf build/firmware.bin
arm-none-eabi-objcopy -O ihex build/firmware.elf build/firmware.hex
arm-none-eabi-size --format=berkeley build/firmware.elf
   text    data     bss     dec     hex filename
    460       0     256     716     2cc build/firmware.elf

В этот раз все нормально, все модули собираются по отдельности, после чего происходит компоновка.

CMakeLists.txt для сборки проекта посредством CMake:

cmake_minimum_required(VERSION 3.12)
project(firmware LANGUAGES C ASM)

set(CMAKE_VERBOSE_MAKEFILE OFF)

# Toolchain setup
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR arm)
set(CMAKE_C_COMPILER arm-none-eabi-gcc)
set(CMAKE_ASM_COMPILER arm-none-eabi-gcc)
set(CMAKE_OBJCOPY arm-none-eabi-objcopy)
set(CMAKE_SIZE arm-none-eabi-size)

# Target and output names
set(TARGET firmware)
set(ELF ${TARGET}.elf)
set(BIN ${TARGET}.bin)
set(HEX ${TARGET}.hex)

# Directory structure
set(BUILD_DIR ${CMAKE_BINARY_DIR})
set(SRC_DIR src)
set(ASM_DIR asm)
set(INC_DIR inc)

# Source files
file(GLOB SRCS_C
    ${SRC_DIR}/*.c
    ${CMAKE_SOURCE_DIR}/main.c
)
file(GLOB SRCS_S ${ASM_DIR}/*.s)

# Include paths
set(INC
    ${INC_DIR}
    CMSIS/device
    CMSIS/core
    SPL/inc
)

# Compiler definitions
add_definitions(-DSTM32F10X_MD)

# MCU and flags
set(MCU cortex-m3)
set(CWARN "-Wall")
set(CMAKE_C_FLAGS "-mcpu=${MCU} -mthumb ${CWARN} -g -Os -ffunction-sections -fdata-sections")
set(CMAKE_ASM_FLAGS "-mcpu=${MCU} -mthumb -g")
set(LINKER_SCRIPT "${CMAKE_SOURCE_DIR}/stm32f10xx8.ld")
set(CMAKE_EXE_LINKER_FLAGS "-nostartfiles -specs=nano.specs -T${LINKER_SCRIPT} -Wl,--gc-sections")

# Create executable
add_executable(${ELF} ${SRCS_C} ${SRCS_S})
target_include_directories(${ELF} PRIVATE ${INC})

# Generate binary and hex files
add_custom_command(TARGET ${ELF} POST_BUILD
    COMMAND ${CMAKE_OBJCOPY} -O binary ${ELF} ${BIN}
    COMMAND ${CMAKE_OBJCOPY} -O ihex ${ELF} ${HEX}
    COMMENT "Generating ${BIN} and ${HEX}"
)

# Size target
add_custom_target(size
    COMMAND ${CMAKE_SIZE} --format=berkeley ${ELF}
    DEPENDS ${ELF}
)

# Flash target
add_custom_target(flash
    COMMAND st-flash write ${BIN} 0x08000000
    DEPENDS ${BIN}
)

# Install target (optional)
install(FILES ${BIN} DESTINATION bin)

Сборка проекта посредством CMake:

$ mkdir cbuild && cd $_
$ cmake ..
$ make && make size

Прошивка:

$ make flash

Под спойлером выкладываю скрипт для сборки проекта посредством Qbs:

import qbs

Product {
    type: ["application","flash"]
    Depends { name: "cpp" }
    name: "firmware"

    property string Home: sourceDirectory
    property string Inc: Home + "/inc"
    property string Asm: Home + "/asm"

    cpp.positionIndependentCode: false
    cpp.executableSuffix: ".elf"

    cpp.includePaths:[
        "CMSIS/device",
        "CMSIS/core",
        "SPL/inc",
        "inc",
    ]

    cpp.driverFlags: [
        "-nostartfiles",
        "-specs=nano.specs",
    ]

    cpp.linkerFlags:
    [
        "--gc-sections",
        "-T" + sourceDirectory + "/stm32f10xx8.ld",
    ]

    cpp.cFlags: [
        "-mthumb",
        "-mcpu=cortex-m3",
        "-Wall",
    ]

    cpp.defines: [
        "STM32F10X_MD",
    ]

    Properties
    {
        condition: qbs.buildVariant === "debug"
        cpp.defines: outer.concat(["DEBUG=1"])
        cpp.debugInformation: true
        cpp.optimization: "small"
        cpp.cFlags: outer.concat(["-ggdb"])
    }

    Properties
    {
        condition: qbs.buildVariant === "release"
        cpp.debugInformation: false
        cpp.optimization: "small"
    }

    Group {
        name: "Headers"
        files: [
            Inc + "/*.h",
        ]
        fileTags: ["h_src"]
    }

    Group {
        name: "Assembly"
        fileTags: ["asm"]
        files: [
            Asm  + "/*.s",
        ]
    }

   files: [
        "main.c",
    ]


    Rule
    {
        inputs: ["application"]
        Artifact
        {
            filePath: product.name + ".bin"
            fileTags: "flash"
        }
        prepare:
        {
            var size_name=input.filePath
            var argsObjcopy = ["-O", "binary", input.filePath, output.filePath]
            var cmd_bin=new Command("arm-none-eabi-objcopy",argsObjcopy)
            var cmd_size=new Command("arm-none-eabi-size",size_name)
            cmd_bin.description = "Generating binary: " + output.fileName
            cmd_size.description = "Size of " + output.fileName
            return [cmd_bin,cmd_size]
        }
    }
}

Сборка с помощь команды:

$ qbs build  config:release profile:arm-none-eabi-gcc-10_3 --command-echo-mode command-line
Build graph does not yet exist for configuration 'release'. Starting from scratch.
Resolving project for configuration release
WARNING: Could not detect target platform ('linux' given)
Setting up build graph for configuration release
Building for configuration release
/usr/local/bin/arm-none-eabi-as -I/home/flanker/qtcreator_projects/00_blink_148/CMSIS/device -I/home/flanker/qtcreator_projects/00_blink_148/CMSIS/core -I/home/flanker/qtcreator_projects/00_blink_148/SPL/inc -I/home/flanker/qtcreator_projects/00_blink_148/inc -o /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/2445be8241aabd34/startup.s.o /home/flanker/qtcreator_projects/00_blink_148/asm/startup.s
/usr/local/bin/arm-none-eabi-gcc -Os -Wall -Wextra -nostartfiles -specs=nano.specs -pipe -fvisibility=default -mthumb -mcpu=cortex-m3 -Wall -DNDEBUG -DSTM32F10X_MD -I/home/flanker/qtcreator_projects/00_blink_148/CMSIS/device -I/home/flanker/qtcreator_projects/00_blink_148/CMSIS/core -I/home/flanker/qtcreator_projects/00_blink_148/SPL/inc -I/home/flanker/qtcreator_projects/00_blink_148/inc -o /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/3a52ce780950d4d9/main.c.o -c /home/flanker/qtcreator_projects/00_blink_148/main.c
/usr/local/bin/arm-none-eabi-gcc -Wl,--gc-sections,-T/home/flanker/qtcreator_projects/00_blink_148/stm32f10xx8.ld -nostartfiles -specs=nano.specs -o /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/firmware.elf /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/2445be8241aabd34/startup.s.o /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/3a52ce780950d4d9/main.c.o
/usr/local/bin/arm-none-eabi-objcopy -O binary /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/firmware.elf /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/firmware.bin
/usr/local/bin/arm-none-eabi-size /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/firmware.elf
Build done for configuration release.
/usr/local/bin/arm-none-eabi-size /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/firmware.elf
   text    data     bss     dec     hex filename
    460       0     256     716     2cc /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/firmware.elf

5 Настройка системы тактирования - RCC (Reset and Clock Control)

После добавления таблицы прерываний необходимо настроить систему тактирования. Для микроконтроллера STM32F103C8 она выглядит следующим образом:

Вообще-то, в шаблонном проекте TrueStudio у нас уже была настроена система тактирования RCC, хоть мы к этому и не приложили ни капли усилий. С помощью добрых глаз и дизассемблера, мне удалось выяснить, что функции настройки RCC содержатся в файле system_stm32f10x.c:

Настройка системы тактирования начинается с вызова функции SystemInit(). В каталоге проекта "src", откроем новый файл system_init.c. В этот файл мы и скопируем функцию SystemInit():

#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"

void SystemInit (void)
{
  RCC->CR |= (uint32_t)0x00000001;          // enable HSI, 8 MHz

  /* Reset SW, HPRE, PPRE1, PPRE2, ADCPRE and MCO bits */
#ifndef STM32F10X_CL
  RCC->CFGR &= (uint32_t)0xF8FF0000;
#else
  RCC->CFGR &= (uint32_t)0xF0FF0000;
#endif /* STM32F10X_CL */

  /* Reset HSEON, CSSON and PLLON bits */
  RCC->CR &= (uint32_t)0xFEF6FFFF;

  /* Reset HSEBYP bit */
  RCC->CR &= (uint32_t)0xFFFBFFFF;

  /* Reset PLLSRC, PLLXTPRE, PLLMUL and USBPRE/OTGFSPRE bits */
  RCC->CFGR &= (uint32_t)0xFF80FFFF;

#ifdef STM32F10X_CL
  /* Reset PLL2ON and PLL3ON bits */
  RCC->CR &= (uint32_t)0xEBFFFFFF;

  /* Disable all interrupts and clear pending bits  */
  RCC->CIR = 0x00FF0000;

  /* Reset CFGR2 register */
  RCC->CFGR2 = 0x00000000;
#elif defined (STM32F10X_LD_VL) || defined (STM32F10X_MD_VL) || (defined STM32F10X_HD_VL)
  /* Disable all interrupts and clear pending bits  */
  RCC->CIR = 0x009F0000;

  /* Reset CFGR2 register */
  RCC->CFGR2 = 0x00000000;
#else
  /* Disable all interrupts and clear pending bits  */
  RCC->CIR = 0x009F0000;
#endif /* STM32F10X_CL */

#if defined (STM32F10X_HD) || (defined STM32F10X_XL) || (defined STM32F10X_HD_VL)
  #ifdef DATA_IN_ExtSRAM
    SystemInit_ExtMemCtl();
  #endif /* DATA_IN_ExtSRAM */
#endif

  /* Configure the System clock frequency, HCLK, PCLK2 and PCLK1 prescalers */
  /* Configure the Flash Latency cycles and enable prefetch buffer */
//  SetSysClock();
#ifdef SYSCLK_FREQ_HSE
    SetSysClockToHSE();
#elif defined SYSCLK_FREQ_24MHz
    SetSysClockTo24();
#elif defined SYSCLK_FREQ_36MHz
    SetSysClockTo36();
#elif defined SYSCLK_FREQ_48MHz
    SetSysClockTo48();
#elif defined SYSCLK_FREQ_56MHz
    SetSysClockTo56();
#elif defined SYSCLK_FREQ_72MHz
    SetSysClockTo72();
#endif

#ifdef VECT_TAB_SRAM
  SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM. */
#else
//  SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH. */
#endif
}

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

Мы будем использовать другую функцию: void SetSysClockTo72(). Эта функция сначала переключается на внешний кварц, затем запускает PLL-генератор, после его стабилизации устанавливается PLL-множитель на 9, в результате чего частота SYSCLK устанавливается в значение 72 MHz. Далее устанавливаются делители: для AHB и APB2 шин по единице, для APB1 шины равной двум, т.е. на 36МГц. Кроме того для флеш-памяти устанавливается WaitState равный двум, т.е. флеш-память работает на меньшей частоте чем процессор. Но давайте по порядку.

1. В начале запускается внешний кварц:

  /* SYSCLK, HCLK, PCLK2 and PCLK1 configuration ---------------------------*/
  /* Enable HSE */
  RCC->CR |= ((uint32_t)RCC_CR_HSEON);

  /* Wait till HSE is ready and if Time out is reached exit */
  do
  {
    HSEStatus = RCC->CR & RCC_CR_HSERDY;
    StartUpCounter++;
  } while((HSEStatus == 0) && (StartUpCounter != HSE_STARTUP_TIMEOUT));

Ниже приведено описание конфигурационного регистра RCC_CR:

Здесь HSION - включает/выключает внутренний 8 МГц генератор. Внутренний генератор невозможно выключить когда от него тактируется SYSCLK. Этот бит аппаратно устанавливается когда микроконтроллер выходит из Stop и Standby режимов или когда перестают поступать сигналы с внешнего генератора. HDIRDY - флаг готовности HSI генератора. HSITRIM и HSICAL - отвечают за подстройку частоты HSI.

HSEON - включает/выключает генератор работающий от внешнего кварца. Не может быть выключен когда от него тактируется SYSCLK. Аппаратно очищается при переходе микроконтроллера в Stop и Standby режимы. HSERDY - флаг готовности генератора. НSEBYP - разрешает работу от внешнего генератора (не путать с кварцем!). Внешний генератор должен быть в диапазоне 4-16 МГц. HSEBYP не может быть установлен если запущен HSE. HSEBYP должен устанавливаться вместе с HSEON битом.

СSSON - включает систему безопасности тактирования. PLLON - включает PLL генератор. Очищается аппаратно при переходе микроконтроллера в Stop и Standby режимы. PLLON не может быть очищен когда используется для тактирования SYSCLK. PLLRDY флаг готовности PLL-генератора.

Т.о. сначала включается HSE генератор, после чего некоторое время ожидается пока он стабилизируется.

Идем далее:

    /* Enable Prefetch Buffer */
    FLASH->ACR |= FLASH_ACR_PRFTBE;

    /* Flash 2 wait state */
    FLASH->ACR &= (uint32_t)((uint32_t)~FLASH_ACR_LATENCY);
    FLASH->ACR |= (uint32_t)FLASH_ACR_LATENCY_2;

Здесь включается буфер превыборки и устанавливается время обращения к флеш-памяти. Описание регистра FLASH_ACR приведено ниже:

Здесь Latency задает задержку при обращении к флеш-памяти. Для частоты SYSCLK=72MHz, следует ставить задержку в два такта. HLFCYA разрешает обращение к 16-битным данным, что помогает улучшить быстродействие при частотах 8 MHz и ниже. Его нельзя использовать совместно с PLL. PRFTBE включает буфер превыборки команд, PRFTBS - флаг буфера превыборки.

    /* HCLK = SYSCLK */
    RCC->CFGR |= (uint32_t)RCC_CFGR_HPRE_DIV1;

    /* PCLK2 = HCLK */
    RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE2_DIV1;

    /* PCLK1 = HCLK */
    RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE1_DIV2;

Здесь на шины AHB и APB2 устанавливаются предделители равные единице. На шину APB1 устанавливается предделитель равный двум.

Здесь SW переключает генератор тактирования SYSCLK. SWS - флаг завершения переключения на новый генератор SYSCLK. HPRE - устанавливает предделитель на AHB шину. PPRE1 устанавливает предделитель на шину APB1. PPRE2 устанавливает предделитель на шину APB2. ADCPRE устанавливает предделитель на АЦП. PLLSRC - выбирает источник опорного тактирования: HSI или HSE. PLLXTPRE - устанавливает предделитель на PLL. PLLMUL - устанавливает множитель на PLL. USBPRE - устанавливает предделитель на USB шину. MCO - выбирает источник выходного тактового сигнала.

Т.о. в следующем коде выбирается HSE в качестве опорного источника тактирования, и устанавливается множитель равный девяти. Т.к. на плате "Blue Pill" установлен кварц на 8 MHz, то умножив это число на 9, получим в итоге частоту SYSCLK = 72 MHz.

    /*  PLL configuration: PLLCLK = HSE * 9 = 72 MHz */
    RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_PLLSRC | RCC_CFGR_PLLXTPRE |
                                        RCC_CFGR_PLLMULL));
    RCC->CFGR |= (uint32_t)(RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMULL9);

Далее включается PLL и ожидается его готовность:

    /* Enable PLL */
    RCC->CR |= RCC_CR_PLLON;

    /* Wait till PLL is ready */
    while((RCC->CR & RCC_CR_PLLRDY) == 0)
    {
    }

В завершении, тактирование SYSCLK переключается на PLL:

    /* Select PLL as system clock source */
    RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_SW));
    RCC->CFGR |= (uint32_t)RCC_CFGR_SW_PLL;

    /* Wait till PLL is used as system clock source */
    while ((RCC->CFGR & (uint32_t)RCC_CFGR_SWS) != (uint32_t)0x08)
    {
    }

Во всем этом хозяйстве не хватает кода включения CSS и генератора низкочастотного генератора LSE, но для начала наверно сойдет. Полностью код файла startup.c можно посмотреть под спойлером, или в архиве в конце статьи.

#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"

#ifdef SYSCLK_FREQ_HSE
/**
  * @brief  Selects HSE as System clock source and configure HCLK, PCLK2
  *         and PCLK1 prescalers.
  * @note   This function should be used only after reset.
  * @param  None
  * @retval None
  */
static void SetSysClockToHSE(void)
{
  __IO uint32_t StartUpCounter = 0, HSEStatus = 0;

  /* SYSCLK, HCLK, PCLK2 and PCLK1 configuration ---------------------------*/
  /* Enable HSE */
  RCC->CR |= ((uint32_t)RCC_CR_HSEON);

  /* Wait till HSE is ready and if Time out is reached exit */
  do
  {
    HSEStatus = RCC->CR & RCC_CR_HSERDY;
    StartUpCounter++;
  } while((HSEStatus == 0) && (StartUpCounter != HSE_STARTUP_TIMEOUT));

  if ((RCC->CR & RCC_CR_HSERDY) != RESET)
  {
    HSEStatus = (uint32_t)0x01;
  }
  else
  {
    HSEStatus = (uint32_t)0x00;
  }

  if (HSEStatus == (uint32_t)0x01)
  {

#if !defined STM32F10X_LD_VL && !defined STM32F10X_MD_VL && !defined STM32F10X_HD_VL
    /* Enable Prefetch Buffer */
    FLASH->ACR |= FLASH_ACR_PRFTBE;

    /* Flash 0 wait state */
    FLASH->ACR &= (uint32_t)((uint32_t)~FLASH_ACR_LATENCY);

#ifndef STM32F10X_CL
    FLASH->ACR |= (uint32_t)FLASH_ACR_LATENCY_0;
#else
    if (HSE_VALUE <= 24000000)
    {
      FLASH->ACR |= (uint32_t)FLASH_ACR_LATENCY_0;
    }
    else
    {
      FLASH->ACR |= (uint32_t)FLASH_ACR_LATENCY_1;
    }
#endif /* STM32F10X_CL */
#endif

    /* HCLK = SYSCLK */
    RCC->CFGR |= (uint32_t)RCC_CFGR_HPRE_DIV1;

    /* PCLK2 = HCLK */
    RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE2_DIV1;

    /* PCLK1 = HCLK */
    RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE1_DIV1;

    /* Select HSE as system clock source */
    RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_SW));
    RCC->CFGR |= (uint32_t)RCC_CFGR_SW_HSE;

    /* Wait till HSE is used as system clock source */
    while ((RCC->CFGR & (uint32_t)RCC_CFGR_SWS) != (uint32_t)0x04)
    {
    }
  }
  else
  { /* If HSE fails to start-up, the application will have wrong clock 
         configuration. User can add here some code to deal with this error */
  }
}
#elif defined SYSCLK_FREQ_72MHz
/**
  * @brief  Sets System clock frequency to 72MHz and configure HCLK, PCLK2 
  *         and PCLK1 prescalers. 
  * @note   This function should be used only after reset.
  * @param  None
  * @retval None
  */
static void SetSysClockTo72(void)
{

    /* SYSCLK, HCLK, PCLK2 and PCLK1 configuration ---------------------------*/
    /* Enable HSE */
    RCC->CR |= ((uint32_t)RCC_CR_HSEON);

    /* Wait till HSE is ready and if Time out is reached exit */
    while (!(RCC->CR & RCC_CR_HSERDY))

    /* Enable Prefetch Buffer */
    FLASH->ACR |= FLASH_ACR_PRFTBE;

    /* Flash 2 wait state */
    FLASH->ACR &= (uint32_t)((uint32_t)~FLASH_ACR_LATENCY);
    FLASH->ACR |= (uint32_t)FLASH_ACR_LATENCY_2;


    /* HCLK = SYSCLK */
    RCC->CFGR |= (uint32_t)RCC_CFGR_HPRE_DIV1;

    /* PCLK2 = HCLK */
    RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE2_DIV1;

    /* PCLK1 = HCLK */
    RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE1_DIV2;

    /*  PLL configuration: PLLCLK = HSE * 9 = 72 MHz */
    RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_PLLSRC | RCC_CFGR_PLLXTPRE |
                                        RCC_CFGR_PLLMULL));
    RCC->CFGR |= (uint32_t)(RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMULL9);

    /* Enable PLL */
    RCC->CR |= RCC_CR_PLLON;

    /* Wait till PLL is ready */
    while(!(RCC->CR & RCC_CR_PLLRDY));

    /* Select PLL as system clock source */
    RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_SW));
    RCC->CFGR |= (uint32_t)RCC_CFGR_SW_PLL;

    /* Wait till PLL is used as system clock source */
    while ((RCC->CFGR & (uint32_t)RCC_CFGR_SWS) != (uint32_t)0x08);
}
#endif


void SystemInit (void)
{
  RCC->CR |= (uint32_t)0x00000001;          // enable HSI, 8 MHz

  /* Reset SW, HPRE, PPRE1, PPRE2, ADCPRE and MCO bits */
#ifndef STM32F10X_CL
  RCC->CFGR &= (uint32_t)0xF8FF0000;
#else
  RCC->CFGR &= (uint32_t)0xF0FF0000;
#endif /* STM32F10X_CL */

  /* Reset HSEON, CSSON and PLLON bits */
  RCC->CR &= (uint32_t)0xFEF6FFFF;

  /* Reset HSEBYP bit */
  RCC->CR &= (uint32_t)0xFFFBFFFF;

  /* Reset PLLSRC, PLLXTPRE, PLLMUL and USBPRE/OTGFSPRE bits */
  RCC->CFGR &= (uint32_t)0xFF80FFFF;

#ifdef STM32F10X_CL
  /* Reset PLL2ON and PLL3ON bits */
  RCC->CR &= (uint32_t)0xEBFFFFFF;

  /* Disable all interrupts and clear pending bits  */
  RCC->CIR = 0x00FF0000;

  /* Reset CFGR2 register */
  RCC->CFGR2 = 0x00000000;
#elif defined (STM32F10X_LD_VL) || defined (STM32F10X_MD_VL) || (defined STM32F10X_HD_VL)
  /* Disable all interrupts and clear pending bits  */
  RCC->CIR = 0x009F0000;

  /* Reset CFGR2 register */
  RCC->CFGR2 = 0x00000000;
#else
  /* Disable all interrupts and clear pending bits  */
  RCC->CIR = 0x009F0000;
#endif /* STM32F10X_CL */

#if defined (STM32F10X_HD) || (defined STM32F10X_XL) || (defined STM32F10X_HD_VL)
  #ifdef DATA_IN_ExtSRAM
    SystemInit_ExtMemCtl();
  #endif /* DATA_IN_ExtSRAM */
#endif

  /* Configure the System clock frequency, HCLK, PCLK2 and PCLK1 prescalers */
  /* Configure the Flash Latency cycles and enable prefetch buffer */
//  SetSysClock();
#ifdef SYSCLK_FREQ_HSE
    SetSysClockToHSE();
#elif defined SYSCLK_FREQ_24MHz
    SetSysClockTo24();
#elif defined SYSCLK_FREQ_36MHz
    SetSysClockTo36();
#elif defined SYSCLK_FREQ_48MHz
    SetSysClockTo48();
#elif defined SYSCLK_FREQ_56MHz
    SetSysClockTo56();
#elif defined SYSCLK_FREQ_72MHz
    SetSysClockTo72();
#endif

#ifdef VECT_TAB_SRAM
  SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM. */
#else
//  SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH. */
#endif
}

Данный файл следует разместить в каталоге проекта "src". В обработчике прерывания Reset (файл startup.s) следует раскомментировать вызов функции SystemInit:

  bl  SystemInit
В Makefile нужно добавить define:

DEF += -DSYSCLK_FREQ_72MHz

Тоже самое в "CMakeLists.txt"

add_definitions(-DSTM32F10X_MD -DSYSCLK_FREQ_72MHz)

Самые большие изменения в Qbs скрипте, там кроме дефйна нужно еще добавить группу для src файлов. Патч:

$ diff -Naur 00_blink_148.qbs ~/mydev/mcu/f103_25/02_blink_project/00_blink_148.qbs 
--- 00_blink_148.qbs    2025-05-29 09:37:37.000000000 +0400
+++ /home/flanker/mydev/mcu/f103_25/02_blink_project/00_blink_148.qbs   2025-05-27 16:08:04.000000000 +0400
@@ -7,7 +7,6 @@

     property string Home: sourceDirectory
     property string Inc: Home + "/inc"
-    property string Src: Home + "/src" 
     property string Asm: Home + "/asm"

     cpp.positionIndependentCode: false
@@ -39,7 +38,6 @@

        cpp.defines: [
                "STM32F10X_MD",
-        "SYSCLK_FREQ_72MHz",
        ]

     Properties
@@ -74,13 +72,6 @@
         ]
     }

-    Group {
-        name: "Src"
-        files: [
-            Src + "/*.c",
-        ]
-    }
-
    files: [
         "main.c",
     ]

Сборка:

$ make 
arm-none-eabi-gcc -c -mcpu=cortex-m3 -mthumb -Wall -g -Os -ffunction-sections -fdata-sections -Iinc -I. -ICMSIS/device -ICMSIS/core -ISPL/inc -DSTM32F10X_MD -DSYSCLK_FREQ_72MHz main.c -o build/main.o
arm-none-eabi-gcc -c -mcpu=cortex-m3 -mthumb -Wall -g -Os -ffunction-sections -fdata-sections -Iinc -I. -ICMSIS/device -ICMSIS/core -ISPL/inc -DSTM32F10X_MD -DSYSCLK_FREQ_72MHz src/system_init.c -o build/system_init.o
arm-none-eabi-gcc -c -mcpu=cortex-m3 -mthumb -g asm/startup.s -o build/startup.o
arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -Wall -g -Os -ffunction-sections -fdata-sections -Iinc -I. -ICMSIS/device -ICMSIS/core -ISPL/inc -DSTM32F10X_MD -DSYSCLK_FREQ_72MHz build/system_init.o build/main.o build/startup.o -o build/firmware.elf -T stm32f10xx8.ld -nostartfiles -Wl,--gc-sections -specs=nano.specs
arm-none-eabi-objcopy -O binary build/firmware.elf build/firmware.bin
arm-none-eabi-objcopy -O ihex build/firmware.elf build/firmware.hex
arm-none-eabi-size --format=berkeley build/firmware.elf
   text    data     bss     dec     hex filename
    648       0     256     904     388 build/firmware.elf

Сигнал на логическом анализаторе принял такую форму:

Итого, размер прошивки составил 648 байт, и это самая-самая минимальная программа (на мой взгляд) для микроконтроллера stm32f103xx.

Как известно, в Cortex-M3 нет поддержки чисел с плавающей запятой, она реализуется программно. Будет ли это работать в данном проекте? Чтобы ответить на этот вопрос, добавим операцию с вещественными числами в "main.c"

#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"

#define WAIT 600000

void dummy_loop(volatile uint32_t count) {
    while (--count);
}

int main() {
    // Enable GPIOC clock
    RCC->APB2ENR |= RCC_APB2Periph_GPIOC;
    float a=0.1f;
    // Configure PC13 as push-pull output (max speed 2 MHz)
    GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13);
    GPIOC->CRH |= GPIO_CRH_MODE13_1;  // Output mode, max speed 2 MHz (0b10)

    // Super loop to toggle PC13
    while (1) {
        a += 0.25;
        GPIOC->BSRR = GPIO_Pin_13;    // Set PC13
        dummy_loop(WAIT * a);
        GPIOC->BRR = GPIO_Pin_13;     // Reset PC13
        dummy_loop(WAIT);
    }
}

После сборки проекта, размер прошивки увеличится до 1656 байт:

$ arm-none-eabi-size --format=berkeley build/firmware.elf
   text    data     bss     dec     hex filename
   1656       0     256    1912     778 build/firmware.elf

Прошиваем и смотрим на частоту мигания светодиода:

Здесь при нижнем уровне светодиод загорается, а при верхнем - гаснет. И как видно по записи с логического анализатора, светодиод с течением времени загорается все реже и реже, что полностью соответствует алгоритму программы.

6) Функция задержки на ассемблерных инструкциях

Теперь, когда мы добрались до желанных до 72 MHz, хочется узнать, какова же реальная производительность такого микроконтроллера. Адепты STM32 при любом случае кричат, что 72 МHz это в несколько раза быстрее чем 16, но про влияние значения waitstate на производительность CPU, я упоминаний как-то не встречал.

Самым простым тестом производительности будет функция задержки на ассемблерных инструкциях. Маркером здесь будет служить число итераций которые микроконтроллер выполняет за 1 мс. В данном случае мы не учитываем тактовую частоту каждого микроконтроллера т.к. это необъективный показатель.

Итак, для реализации функции задержки добавим в ассемблерный файл asm/assembly.s следующий код:

.global delay
delay:
    push {r1}
l1: mov.w r1,#10285     @ 72000/7
lp: subs r1,#1
    bne lp
    subs r0,#1
    bne l1
    pop {r1}
    bx lr

Тогда main.c примет такой вид:

#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"

extern void delay(uint32_t ms);

int main()
{
    // enable GPIOC port
    RCC->APB2ENR |= RCC_APB2Periph_GPIOC;
    // --- GPIO setup ----
    GPIOC->CRH &= ~(uint32_t)(0xf<<20);
    GPIOC->CRH |=  (uint32_t)(0x2<<20);
    for(;;){
        GPIOC->ODR ^= GPIO_Pin_13;
        delay(1000);
    }

}

Как видно, цикл задержки проходит 10285 итераций за 1 мс, т.е. две инструкции выполняются вкупе за 7 тактов.

Так же функция на STM8 выполняется за 5333 итераций

.globl _delay
_delay:
    ldw x, (03,sp)
l0:
    ldw y, #5328      ; ->(16000/3 - 5)
l1:
    decw y
    jrne l1
    decw x
    jrne l0
    ret

Т.о. на первый взгляд реальная производительность STM32 "всего" в два раза выше восьмибитного STM8.

Далее добавлено в 2025г. Для вычисления производительности процессоров составляют сложные бенчмарки, и говорить о производительности на данном примере некорректно.

Вместо того, чтобы добавлять в проект ассемблерный модуль с одной единственной функцией, можно воспользоваться inline ассемблером, и тогда "main.c" примет следующий вид:

#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"

#define MS_1000 1000

void delay_ms(uint32_t milliseconds) {
    __asm volatile (
        "push {r0-r1}\n"        // Save r0,r1
        "1:\n"                  // Outer loop label
        "mov.w r1, #10285\n"    // 72000/7 = 10285.71410285
        "2:\n"                  // Inner loop label
        "subs r1, #1\n"
        "bne 2b\n"              // Branch back to inner loop (2:)
        "subs %[milliseconds], #1\n"
        "bne 1b\n"              // Branch back to outer loop (1:)
        "pop {r0-r1}\n"         // Restore r0,r1
        : /* No outputs */
        : [milliseconds] "r" (milliseconds)  // input
        : "r1", "cc"            // clobber list
    );
}

int main() {
    // Enable GPIOC clock
    RCC->APB2ENR |= RCC_APB2Periph_GPIOC;
    // Configure PC13 as push-pull output (max speed 2 MHz)
    GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13);
    GPIOC->CRH |= GPIO_CRH_MODE13_1;  // Output mode, max speed 2 MHz (0b10)

    // Super loop to toggle PC13
    while (1) {
        GPIOC->BSRR = GPIO_Pin_13;    // Set PC13
        delay_ms(MS_1000);
        GPIOC->BRR = GPIO_Pin_13;     // Reset PC13
        delay_ms(MS_1000);
    }
}

По логике вещей, компилятор должен сохранять все используемые регистры при вызове такой функции, и "Clobber List" это список регистров которые должны сохраняться компилятором в стеке, но если посмотреть ассемблерный код модуля "main.c", то будет видно, что там ничего подобного не происходит:

void delay_ms(uint32_t milliseconds) {
    __asm volatile (
   0:   b403        push    {r0, r1}
   2:   f642 012d   movw    r1, #10285  ; 0x282d
   6:   3901        subs    r1, #1
   8:   d1fd        bne.n   6 <delay_ms+0x6>
   a:   3801        subs    r0, #1
   c:   d1f9        bne.n   2 <delay_ms+0x2>
   e:   bc03        pop {r0, r1}
        "pop {r0-r1}\n"         // Restore r0,r1
        : /* No outputs */
        : [milliseconds] "r" (milliseconds)  // input
        : "r1", "cc"            // clobber list
    );
}
  10:   4770        bx  lr

Disassembly of section .text.startup.main:

00000000 <main>:

int main() {
    // Enable GPIOC clock
    RCC->APB2ENR |= RCC_APB2Periph_GPIOC;
   0:   4a0c        ldr r2, [pc, #48]   ; (34 <main+0x34>)
int main() {
   2:   b508        push    {r3, lr}
    RCC->APB2ENR |= RCC_APB2Periph_GPIOC;
   4:   6993        ldr r3, [r2, #24]
   6:   f043 0310   orr.w   r3, r3, #16
   a:   6193        str r3, [r2, #24]
    // Configure PC13 as push-pull output (max speed 2 MHz)
    GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13);
   c:   4b0a        ldr r3, [pc, #40]   ; (38 <main+0x38>)
   e:   685a        ldr r2, [r3, #4]
  10:   f422 0270   bic.w   r2, r2, #15728640   ; 0xf00000
  14:   605a        str r2, [r3, #4]
    GPIOC->CRH |= GPIO_CRH_MODE13_1;  // Output mode, max speed 2 MHz (0b10)
  16:   685a        ldr r2, [r3, #4]
  18:   f442 1200   orr.w   r2, r2, #2097152    ; 0x200000
  1c:   605a        str r2, [r3, #4]

    // Super loop to toggle PC13
    while (1) {
        GPIOC->BSRR = GPIO_Pin_13;    // Set PC13
  1e:   f44f 5200   mov.w   r2, #8192   ; 0x2000
        delay_ms(MS_1000);
  22:   f44f 707a   mov.w   r0, #1000   ; 0x3e8
        GPIOC->BSRR = GPIO_Pin_13;    // Set PC13
  26:   611a        str r2, [r3, #16]
        delay_ms(MS_1000);
  28:   f7ff fffe   bl  0 <main>   // вызов функции delay_ms
        GPIOC->BRR = GPIO_Pin_13;     // Reset PC13
  2c:   615a        str r2, [r3, #20]
        delay_ms(MS_1000);
  2e:   f7ff fffe   bl  0 <main>   // вызов функции delay_ms
    while (1) {
  32:   e7f6        b.n 22 <main+0x22>
  34:   40021000    .word   0x40021000
  38:   40011000    .word   0x40011000

В 22-й строке имеется инструкция " mov.w r0, #1000" которая заносит в r0 параметр функции, однако при втором вызове функции, этой операции нет. И поэтому обязанность сохранения регистров значение которых меняется во время выполнения функции, ложится на плечи программиста. Напомню, что в данном случае программы была компилирована с ключем оптимизации "-Os".

Давайте посмотрим, насколько точно функция формирует задержки.

Как можно видеть на логическом анализаторе, два вызова функции соответствуют 2000 мс, что очень точный результат.

Однако есть нюанс. Если отключить в обработчике прерывания Reset вызов функции "SystemInit", т.е. перевести микроконтроллер снова на дефолтную частоту 8 МГц, то микроконтроллер не будет работать в 9 раз медленнее. Посмотрите на лог:

Функция "delay_ms(1000)" на частоте 8 МГц, отрабатывает за 3.85 секунды, а не за 9 сек. Потому что на 8 МГц, у микроконтроллера флеш-память работает на частоте процессора, а когда микроконтроллер "разгоняют" до 72 МГц, флеш-память продолжает работать на той же самой низкой частоте, и получается, что "подтормаживает". Т.е. работа микроконтроллера на частоте 72 МГц не означает, что ваша программа будет работать в девять раз быстрее, нежели на дефолтных 8 МГц, всего лишь в четыре раза.

В микроконтроллерах других серий этот эффект постарались исправить, в частности, STM32F030 на своих 48 МГц работает на уровне STM32F103 c частотой 72 МГц. Если STM32F103 за 1мс выполняет 10285 итераций пустого цикла, то STM32F030 на 48МГц совершает 10000 итераций за одну миллисекунду. А микроконтроллер компании Artery AT32F403 выполняет 40000 итераций за 1 миллисекунду на рабочей частоте 200МГц. Техника идет вперед.

7) Функция задержки на таймере SysTick

Наиболее универсальным способом осуществления задержек, является использование таймера SysTick. Это простой 32-битный счетчик с одним прерыванием, который является частью ядра Cortex-M. Наиболее доходчиво про его конфигурацию можно почитать в статье - " ARM. Учебный Курс. SysTick — Системный таймер | Электроника для всех", либо в фирменной документации - "Cortex-M3 Technical Reference Manual", глава 8.2.2.

Чтобы добавить работу с таймером SysTick в проект, я предлагаю добавить в корневой каталог проекта три файла. Два заголовочных "inc/main.h", "inc/utils.h" и один с исходным кодом: "src/utils.c". В файлы сборки проекта ничего добавлять не надо, всё подхватится автоматически.

В "main.h" пока скинем скинем именованные константы:

#ifndef __MAIN_H
#define __MAIN_H

#define MS_1000 1000
#define MS_100  100


#endif  // __MAIN_H

В "utils.h" будет декларация функций модуля "utils.c":

#ifndef __UTILS_H
#define __UTILS_H
#include "stm32f10x.h"

void systick_init(void);
void delay_ms_asm(uint32_t milliseconds);
void delay_ms(uint32_t ms);

#endif // __UTILS_H

В "utils.c" перенесем функцию задержки из "main.c" и добавим код для работы с таймером SysTick:

#include "utils.h"

volatile uint32_t systick_counter = 0;

void SysTick_Handler(void) {
    systick_counter++;
}

void systick_init(void) {
    systick_counter=0;
    // Configure SysTick for 1ms interrupts
    if (SysTick_Config(SystemCoreClock / 1000)) {
        // Error handling
        while (1);
    }
}

uint32_t get_ticks(void) {
    return systick_counter;
}

void delay_ms(uint32_t ms) {
    uint32_t start = get_ticks();
    while ((get_ticks() - start) < ms) {
        __WFI();  // Wait for interrupt (power saving)
    }
}

void delay_ms_asm(uint32_t milliseconds) {
    __asm volatile (
        "push {r0-r1}\n"        // Save r0,r1
        "1:\n"                  // Outer loop label
        "mov.w r1, #10285\n"    // 72000/7 = 10285.71410285
        "2:\n"                  // Inner loop label
        "subs r1, #1\n"
        "bne 2b\n"              // Branch back to inner loop (2:)
        "subs %[milliseconds], #1\n"
        "bne 1b\n"              // Branch back to outer loop (1:)
        "pop {r0-r1}\n"         // Restore r0,r1
        : /* No outputs */
        : [milliseconds] "r" (milliseconds)  // input
        : "r1", "cc"            // clobber list
    );
}

Здесь "SysTick_Handler" - обработчик прерывания таймера, содержит инкрементный счетчик. "get_ticks()" возвращает значение счетчика, "systick_init()" настраивает таймер на частоту 1 кГц и запускает его, а "delay_ms()" - реализация задержки.

Работа таймер SysTick требует включения работы прерываний. Поэтому модуль "main.c" примет следующий вид:

#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
#include "main.h"
#include "utils.h"

uint32_t SystemCoreClock = 72000000;  // Clock STM32F103 at reset

int main() {
    // Enable GPIOC clock
    RCC->APB2ENR |= RCC_APB2Periph_GPIOC;
    // Configure PC13 as push-pull output (max speed 2 MHz)
    GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13);
    GPIOC->CRH |= GPIO_CRH_MODE13_1;  // Output mode, max speed 2 MHz (0b10)
    // setup SysTick
    systick_init();
    // Let's go..
    __enable_irq();

    // Super loop to toggle PC13
    while (1) {
        GPIOC->BSRR = GPIO_Pin_13;    // Set PC13
        delay_ms(MS_1000);
        GPIOC->BRR = GPIO_Pin_13;     // Reset PC13
        delay_ms(MS_1000);
    }
}

В отладочном модуле Cortex-M имеется 32-битный таймер который работает на частоте процессора. С его помощью можно реализовать задержку измеряемую в микросекундах. Он описан в TRM "Cortex-M3 Technical Reference Manual", глава 11.5.

В файл "utils.c" добавим две функции:

bool isDWT_Init=false;

void DWT_Init(void) {
    if ((CoreDebug->DEMCR & CoreDebug_DEMCR_TRCENA_Msk) == 0) {
        CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
        DWT_CYCCNT = 0;
        DWT_CTRL |= 1U;        // Enable CYCCNT directly (bit 0)
    }
    isDWT_Init=true;
}

void delay_us(uint32_t us) {
    if (!isDWT_Init) {
        DWT_Init();
    }
    uint32_t cycles = us * (SystemCoreClock/1000000);
    if (cycles < 12) cycles = 12;  // Minimum measurable delay
    DWT_CYCCNT = 0;
    while (DWT_CYCCNT < cycles);
}

Для проверки я выставил в суперцикле задержки на 1000 мкс и вот какой результат показал логический анализатор:

Т.е. работает довольно точно.

8) Настройка и передача данных по UART

Далее для вывода отладочной информации нам понадобится UART интерфейс. Работа с UART модулем в STM32 мало чем отличается от своего аналога в STM8, разве что только тем, здесь их три. При этом только USART1 тактируется от скоростной APB2 шины, остальные два тактируются от APB1.

Настройка UART через регистры подробнейшем образом разобрана в статье: ARM Учебный курс. USART | Электроника для всех. От STM8 процедура настройки отличается необходимостью включать альтернативный режим работы GPIO и немного другой формулой расчета регистра установки битрейта USART1->BRR.

Для реализации функций UART потребуется добавить модуль "uart" в проект (файлы автоматически подхватятся сборочными скриптами).

Настройку USART1 интерфейса STM32А103 я разместил в модуле "main.c":

#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
#include "stm32f10x_usart.h"
#include "main.h"
#include "utils.h"
#include "uart.h"

uint32_t SystemCoreClock = 72000000;  // Clock STM32F103 at reset

int main() {
    //  --- RCC configuration  -------
    RCC->APB2ENR |= RCC_APB2Periph_GPIOC;   // Enable GPIOC clock
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_USART1EN; // Enable GPIOA and USART1 clocks
    //  --- GPIO configuration ----
    // Configure PC13 as push-pull output (max speed 2 MHz)
    GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13);
    GPIOC->CRH |= GPIO_CRH_MODE13_1;  // Output mode, max speed 2 MHz (0b10)
    // Configure PA9 as alternate function push-pull (TX)
    GPIOA->CRH &= ~(GPIO_CRH_CNF9 | GPIO_CRH_MODE9);  // Clear bits
    GPIOA->CRH |= (GPIO_CRH_CNF9_1 | GPIO_CRH_MODE9); // Alternate function output, max speed 50MHz
    // Configure PA10 as input floating (RX)
    GPIOA->CRH &= ~(GPIO_CRH_CNF10 | GPIO_CRH_MODE10); // Clear bits
    GPIOA->CRH |= GPIO_CRH_CNF10_0; // Floating input
    //  --- UART configuration  ---
    //USART1->BRR  = 0x1d4c; // 9600 baud @ 72 MHz (APB2)
    USART1->BRR = 0x271; // 115200 baud @ 72MHz (APB2)
    USART1->CR1 = USART_CR1_TE | USART_CR1_RE | USART_CR1_UE; // Enable TX, RX and USART
    // Enable RX interrupt (Optional)
    // USART1->CR1 |= USART_CR1_RXNEIE;
    // NVIC_EnableIRQ(USART1_IRQn);
    // ---- SysTick Configuration ----
    systick_init();
    // Let's go..
    __enable_irq();
    // Super loop to toggle PC13
    while (1) {
        // Toggle PC13
        GPIOC->ODR ^= GPIO_Pin_13;

        // Send data via UART
        USART1->DR = 'A'; // Send character
        while (!(USART1->SR & USART_SR_TXE)); // Wait for transmission complete

        delay_ms(1000);
    }
}

В суперцикле небольшой отладочный код, для проверки работы интерфейса.

Нам понадобится заголовочный файл из SPL stm32f10x_usart.h в котором содержатся некоторые битовые маски регистров. К сожалению не все битовые маски содержались в заголовочном файле. Часть находилась в stm32f10x_usart.c. Их пришлось перенести в свой заголовочный файл uart.h:

#ifndef __UART_H__
#define __UART_H__

#include "stm32f10x.h"
#include "stm32f10x_usart.h"

#define USART_CR1_UE_Set                ((uint16_t)0x2000)  /*!< USART Enable Mask */
#define USART_CR1_UE_Reset              ((uint16_t)0xDFFF)  /*!< USART Disable Mask */

#define USART_CR1_WAKE_Mask             ((uint16_t)0xF7FF)  /*!< USART WakeUp Method Mask */

#define USART_CR1_RWU_Set               ((uint16_t)0x0002)  /*!< USART mute mode Enable Mask */
#define USART_CR1_RWU_Reset             ((uint16_t)0xFFFD)  /*!< USART mute mode Enable Mask */
#define USART_CR1_SBK_Set               ((uint16_t)0x0001)  /*!< USART Break Character send Mask */
#define USART_CR1_CLEAR_Mask            ((uint16_t)0xE9F3)  /*!< USART CR1 Mask */
#define USART_CR2_Address_Mask          ((uint16_t)0xFFF0)  /*!< USART address Mask */

#define USART_CR2_LINEN_Set              ((uint16_t)0x4000)  /*!< USART LIN Enable Mask */
#define USART_CR2_LINEN_Reset            ((uint16_t)0xBFFF)  /*!< USART LIN Disable Mask */

#define USART_CR2_LBDL_Mask             ((uint16_t)0xFFDF)  /*!< USART LIN Break detection Mask */
#define USART_CR2_STOP_CLEAR_Mask       ((uint16_t)0xCFFF)  /*!< USART CR2 STOP Bits Mask */
#define USART_CR2_CLOCK_CLEAR_Mask      ((uint16_t)0xF0FF)  /*!< USART CR2 Clock Mask */

#define USART_CR3_SCEN_Set              ((uint16_t)0x0020)  /*!< USART SC Enable Mask */
#define USART_CR3_SCEN_Reset            ((uint16_t)0xFFDF)  /*!< USART SC Disable Mask */

#define USART_CR3_NACK_Set              ((uint16_t)0x0010)  /*!< USART SC NACK Enable Mask */
#define USART_CR3_NACK_Reset            ((uint16_t)0xFFEF)  /*!< USART SC NACK Disable Mask */

#define USART_CR3_HDSEL_Set             ((uint16_t)0x0008)  /*!< USART Half-Duplex Enable Mask */
#define USART_CR3_HDSEL_Reset           ((uint16_t)0xFFF7)  /*!< USART Half-Duplex Disable Mask */

#define USART_CR3_IRLP_Mask             ((uint16_t)0xFFFB)  /*!< USART IrDA LowPower mode Mask */
#define USART_CR3_CLEAR_Mask            ((uint16_t)0xFCFF)  /*!< USART CR3 Mask */

#define USART_CR3_IREN_Set              ((uint16_t)0x0002)  /*!< USART IrDA Enable Mask */
#define USART_CR3_IREN_Reset            ((uint16_t)0xFFFD)  /*!< USART IrDA Disable Mask */
#define USART_GTPR_LSB_Mask             ((uint16_t)0x00FF)  /*!< Guard Time Register LSB Mask */
#define USART_GTPR_MSB_Mask             ((uint16_t)0xFF00)  /*!< Guard Time Register MSB Mask */
#define USART_IT_Mask                   ((uint16_t)0x001F)  /*!< USART Interrupt Mask */

/* USART OverSampling-8 Mask */
#define CR1_OVER8_Set             ((u16)0x8000)  /* USART OVER8 mode Enable Mask */
#define CR1_OVER8_Reset           ((u16)0x7FFF)  /* USART OVER8 mode Disable Mask */

/* USART One Bit Sampling Mask */
#define CR3_ONEBITE_Set           ((u16)0x0800)  /* USART ONEBITE mode Enable Mask */
#define CR3_ONEBITE_Reset         ((u16)0xF7FF)  /* USART ONEBITE mode Disable Mask */
#endif

Подключение USB-UART адаптера к Bluepill следующее:RX(адаптера) к PA9(bluebill), GND(адаптера) к GND(bluepill):

После компиляции и прошивки, на ножке PA9 появится UART сигнал с литерой "эй" (hex-код 0x41):

Для того, чтобы иметь возможность передавать через USART интерфейс строки и числа (предварительно прошедшию сериализацию), в модуле "uart.c" разместим следующий код:

#include "uart.h"
#include <stdarg.h>  // For variable arguments support

// Send single character
void usart1_send_char(uint8_t ch) {
    USART1->DR = ch;
    while(!(USART1->SR & USART_SR_TXE));  // Wait until transmit complete
}

// Send string (null-terminated)
void usart1_print_string(const char *str) {
    while(*str) {
        usart1_send_char(*str++);
    }
}

// Send number (32-bit unsigned)
void usart1_print_number(uint32_t num) {
    char buffer[10];  // Maximum 10 digits for 32-bit number
    char *p = buffer + sizeof(buffer) - 1;
    *p = '\0';  // Null terminator

    do {
        *(--p) = '0' + (num % 10);
        num /= 10;
    } while(num > 0);

    usart1_print_string(p);
}

// Extended version with format support (basic implementation)
void usart1_printf(const char *format, ...) {
    va_list args;
    va_start(args, format);

    while(*format) {
        if(*format == '%') {
            format++;
            switch(*format) {
                case 'd': {
                    int32_t num = va_arg(args, int32_t);
                    if(num < 0) {
                        usart1_send_char('-');
                        num = -num;
                    }
                    usart1_print_number((uint32_t)num);
                    break;
                }
                case 'u':
                    usart1_print_number(va_arg(args, uint32_t));
                    break;
                case 's':
                    usart1_print_string(va_arg(args, char*));
                    break;
                case 'c':
                    usart1_send_char(va_arg(args, int));
                    break;
                case '%':
                    usart1_send_char('%');
                    break;
            }
        } else {
            usart1_send_char(*format);
        }
        format++;
    }

    va_end(args);
}

Для проверки кода, суперцикл в "main.c" пусть будет таким:

    // Super loop to toggle PC13
    int i=0;
    while (1) {
        // Toggle PC13
        GPIOC->ODR ^= GPIO_Pin_13;

        // Send string
        usart1_print_string("Hello World\r\n");
        // Send number
        usart1_print_number(314159);
        // Formatted output
        usart1_printf("Temperature: %d°C, Status: %s\r\n", i++, "OK");

        delay_ms(1000);
    }

После компиляции, размер прошивки перевалит за первый килобайт:

arm-none-eabi-size --format=berkeley build/firmware.elf
   text    data     bss     dec     hex filename
   1116       4     260    1380     564 build/firmware.elf

К счастью, в микроконтроллере имеется еще 63 кБ флеш-памяти.

На логическом анализаторе, передача по UART будет выглядеть так:

В терминальной программе пойдет лог:

Простой бенчмарк на операции деления

Когда я тестировал свои STM8_Board, то мне нужен был простой тест, чтобы определить прирост производительности в 24MHz STM8S207 по сравнению с 16MHz STM8S105. Я приводил простой тест на операции деления и упоминал, что 105-й чип проходил тест со значением в ~40000 итераций, а 207-й работающий на частоте 24MHz - 59949 итераций.

В Cortex-M3 тоже есть операции деления, и согласно документации она должна выполнятся несколько быстрее чем в STM8. А именно: 16 тактов у STM8 и до 12 тактов у STM32.

При этом у Cortex-M3 есть также знаковое деление и 32 битное деление, но пока это нас интересовать не будет.

На базе проекта из предыдущей главы я составил аналогичный тест для STM32F103C8T6:

#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
#include "uart.h"

extern void delay(uint32_t ms);
//static __IO uint32_t s_timer;
volatile uint32_t s_timer;

uint32_t div_count;
uint32_t result;

void SysTick_Handler(void)
{
        if (s_timer)
            s_timer--;
}

void delay_ms(__IO uint32_t val) {
    // ------- SysTick CONFIG --------------
    if (SysTick_Config(72000)) // set 1ms
    {
        while(1); // error
    }

    s_timer=val;

    while(s_timer) {
        result=s_timer/(uint32_t)314;
        div_count++;
//      asm("wfi");
    };

    SysTick->LOAD &= ~(SysTick_CTRL_ENABLE_Msk);    // disable SysTick
}

int main()
{
    // enable GPIOC port
    RCC->APB2ENR |= RCC_APB2Periph_GPIOC;           // enable PORT_C
    RCC->APB2ENR |= RCC_APB2Periph_GPIOA;           // enable PORT_A
    RCC->APB2ENR |= RCC_APB2Periph_USART1;          // enable UART1
    // --- GPIO setup ----
    GPIOC->CRH &= ~(uint32_t)(0xf<<20);
    GPIOC->CRH |=  (uint32_t)(0x2<<20);
    GPIOA->CRH &= ~(uint32_t)(0xf<<4);
    GPIOA->CRH |=  (uint32_t)(0xa<<4);
    // --- UART setup ----

    //USART1->BRR  = 0x1d4c;                                // 9600 Baud, when APB2=72MHz
    USART1->BRR = 0x271;                                // 115200 Baud, when APB2=72MHz
    USART1->CR1 |= USART_CR1_UE_Set | USART_Mode_Tx;    // enable USART1, enable  TX mode
    // Let's go..
    __enable_irq();
    for(;;){
        div_count=0;
        GPIOC->ODR ^= GPIO_Pin_13;
        delay_ms(50);
        usart1_print_string("count: ");
        usart1_print_number(div_count);
        usart1_send_char('\n');
        delay(950);
    }

}

Для того чтобы он корректно работал, нужно в Makefile включить оптимизацию изменив ключ -O0 на -O2 в опциях компилятора. Тогда следующий цикл:

    while(s_timer) {
        result=s_timer/(uint32_t)314;
        div_count++;
    };

Будет раскладываться в следующую ассемблерную последовательность:

   while(s_timer) {
 8000166:       681a            ldr     r2, [r3, #0]
 8000168:       b182            cbz     r2, 800018c <delay_ms+0x48>
 800016a:       4d10            ldr     r5, [pc, #64]   ; (80001ac <delay_ms+0x68>)
 800016c:       682a            ldr     r2, [r5, #0]
 800016e:       3201            adds    r2, #1
 8000170:       e000            b.n     8000174 <delay_ms+0x30>
 8000172:       4602            mov     r2, r0
        result=s_timer/(uint32_t)314;
 8000174:       681c            ldr     r4, [r3, #0]
    while(s_timer) {
 8000176:       6819            ldr     r1, [r3, #0]
 8000178:       1c50            adds    r0, r2, #1
 800017a:       2900            cmp     r1, #0
 800017c:       d1f9            bne.n   8000172 <delay_ms+0x2e>
        result=s_timer/(uint32_t)314;
 800017e:       f44f 739d       mov.w   r3, #314        ; 0x13a
 8000182:       fbb4 f4f3       udiv    r4, r4, r3
 8000186:       4b0a            ldr     r3, [pc, #40]   ; (80001b0 <delay_ms+0x6c>)
 8000188:       602a            str     r2, [r5, #0]
 800018a:       601c            str     r4, [r3, #0]
        div_count++;
        };

Здесь можно видеть, что используется инструкция беззнакового деления: udiv, что нам и надо.

Ну и результат работы бенчмарка можно видеть на скриншоте:

Как видно, производительность STM32 в делении практически на порядок(!) выше STM8S105 работающего на 16 МHz. Впечатляет? Меня да.

Замечу, что аналогичный тест на Arduino приводил к результату приметно всего в две тысячи итераций. Но там конечно же нет аппаратного деления, так что сравнивать будет некорректно.

9 Пишем простой планировщик задач (RTOS)

На самом деле глупо меряться тем, насколько быстро выполняется операция деления, если все остальное время микроконтроллер пребывает в режиме ожидания. В таком случае и программная реализация вполне сойдет, спешить то некуда. По нормальному распределить и оценить нагрузку на CPU поможет планировщик задач или как его еще называют - RTOS.

Зачем оно надо? Планировщик поможет распределить задачи согласно приоритетам. Он сделает это автоматически, от вас не потребуется писать кода для переключения между задачами, а это экономия флеш-памяти. Кроме того планировщик может управлять задачами в процессе их выполнения, например завершение одной задачи может привести в возникновению другой, и т.д.

Если раньше мне надо было написать неблокирующее выполнение какой-либо задачи/функции, то я использовал алгоритм из примеров Arduino - "Blink without Delay":

void loop() {
  unsigned long currentMillis = millis();

  if (currentMillis - previousMillis >= interval) {
    previousMillis = currentMillis;

    if (ledState == LOW) {
      ledState = HIGH;
    } else {
      ledState = LOW;
    }

    digitalWrite(ledPin, ledState);
  }

Для одной или двух задач такой способ сносно работает, но когда у вас пять задач и больше, то ваш код превращается в туалетную бумагу (показывать не буду, кто плавал - знает). RTOS избавляет вас от необходимости писать такой код.


Иллюстрация Рины Зенюк

Как это работает? Представим, что в главном цикле "крутится" функция delay(1000). Delay() выполняется за одну секунду. Эта секунда - это "колбаса", ресурс т.е. "Колбаса" делится на тысячу маленьких кусочков (миллисекунды), которые нужно распределить между котиками (задачами). Если кому-то своего кусочка не хватает - это определённо плохо. Но еще хуже, если остаются несъеденные кусочки и их выбрасывают. Получается, что мы не используем ресурс микроконтроллера полностью. Распределение кусочков между котиками - это задача планировщика. Т.к. RTOS кооперативная, то мы не сможем помешать какому-то слишком толстому котику съесть кусочки других котиков. Планировщик в данном случае должен выдать остальным котикам оставшиеся свободными кусочки. Это всё, что нужно знать о планировщике RTOS.

Для STM32 уже существует достаточно мощная freeRTOS, но мне показалось, что часто бывает нужен простой планировщик. В качестве примера приведу свой планировщик, который я писал для STM8. Он мало что умеет, он "сырой" и тестировался всего на паре светодиодов. Но свою работу он делает: переключает задачи, показывает статистику, занимает мало места на флеше и не использует динамическую память.

В основе планировщика лежит следующая структура:

typedef struct TASK {
    uint32_t loop;
    uint32_t period;
    uint32_t counter;
    void (*handler)(void);
} TASK;

Здесь указатель на callback-функцию - это сама задача. Далее period - это интервал между выполнением задачи. Переменная counter - это убывающий счетчик, который показывает сколько еще осталось до выполнения задачи. loop - это флаг который указывает, будет ли следующее выполнение задачи последним или нет. Если следующее выполнение задачи последнее, то после её выполнения задача удаляется из таблицы планировщика.

Таблица планировщика это простой массив:

TASK task[TSK];

Для инициализации таблицы используется функция clear_task():

void clear_task(void) {
    for(char i=0;i<TSK;i++) {
        task[i].loop=ENABLE;
        task[i].period=0;
        task[i].counter=0;
        task[i].handler=NULL;
    }
    load_cpu=0;
    current_load=0;
    add_task(task_stat,TOP,LOOP,1000);
}

Сам планировщик реализован в обработчике прерывания таймера SysTick:

void SysTick_Handler(void) {
    for(char i=0; i<TSK; i++)
    {
        if (task[i].handler == NULL) break;

        if (!task[i].counter)
        {
            task[i].counter=task[i].period;
            task[i].handler();
            if (!task[i].loop)
                remove_task(i);
            break;
        } else
            task[i].counter--;
    }
    current_load +=(72000-SysTick->VAL);
}

Как можно видеть, он совсем небольшой. Обход таблицы начинается с нулевого значения, и следовательно задачи с меньшим номером имеют больший приоритет. Второй оператор break выделенный красным имеет принципиальное значение в работе планировщика. Когда планировщик "натыкается" на задачу которую следует запустить, он передаёт управление этой задаче, после чего завершает работу игнорируя обработку статусов других задач. На двух светодиодах запущенных с равным интервалом это очень четко видно. Сначала они мигают синхронно, а затем, начинается разсинхронизация. Каждый раз, когда планировщик запускает первый светодиод, время обработки второго сдвигается на один шаг. Впоследствии сдвиг накапливается и имеет место разсинхронизация. Если оператор break убрать, то светодиоды будут мигать всегда синхронно. Конечно, вместо тупого break можно использовать более хитрый алгоритм, но... нужно ли?

Планировщик имеет еще пару служебных функций и одну задачу. Функция void remove_task(uint8_t num) удаляет задачу из таблицы если ее флаг LOOP оказался равным нулю:

void remove_task(uint8_t num) {
    char i;
    for(i=num; i<(TSK-2);i++) {
        task[i]=task[i+1];
    }
    task[TSK-1].counter=0;
    task[TSK-1].period=0;
    task[TSK-1].loop=NOLOOP;
    task[TSK-1].handler=NULL;
}

Функция add_task(), напротив, добавляет задачу в таблицу:

void add_task(  void (*callback)(void),
                Task_Priority_TypeDef rank,
                Task_Loop_TypeDef loop,
                uint32_t period_ms)
{
    char i=TSK-1;
    do {
        if (task[i-1].handler != NULL) {
            task[i]=task[i-1];
        }
        i--;
    } while (i);
    task[i].loop=loop;
    task[i].period=period_ms;
    task[i].handler=callback;
    task[i].counter=period_ms;
}

В параметре функции можно задать приоритет задачи. Положить ее "на дно" или "под крышку" (BOTTOM/TOP).

Одна служебная задача void task_stat() подсчитывает статистику загруженности CPU. Работает это так. Допустим, что таймер SysTick настроен на интервал в 1 мс. Cчетчик таймера убывающий, и когда он доходит до нуля, то: a) во-первых вызывается прерывание, б) во-вторых в счетчик загружается значение инициализации, в нашем случае 72000. В то время пока работает обработчик прерывания, счетчик продолжает себе тихонько тикать. CPU для этого не используется. Когда обработчик прерывания заканчивает работу, он смотрит сколько натикал в этот раз таймер и суммирует это с общим значением current_load:

current_load +=(72000-SysTick->VAL);

И так тысячу раз. Служебная задача: add_task(task_stat,TOP,LOOP,1000) добавляемая при инициализации таблицы, вызывается один раз в секунду. Она обновляет статистическое значение переменной load_cpu и обнуляет счетчик current_load:

void task_stat() {
    load_cpu=current_load;
    current_load=0;
}

Полный код планировщика приведен под спойлерами:

#ifndef __TASK_H__
#define __TASK_H__
#include <stddef.h>
#include "stm32f10x.h"

#define TSK 5                   // count of task

typedef struct TASK {
    uint32_t loop;
    uint32_t period;
    uint32_t counter;
    void (*handler)(void);
} TASK;

typedef enum {
  TOP    = ((uint8_t) 0x00),
  BOTTOM = ((uint8_t) 0x01)
}Task_Priority_TypeDef;

typedef enum {
  NOLOOP    = ((uint8_t) 0x00),
  LOOP      = ((uint8_t) 0x01)
}Task_Loop_TypeDef;

uint32_t get_load_cpu();
void remove_task(uint8_t num);
void clear_task(void);
void add_task(void (*callback)(void), Task_Priority_TypeDef rank, Task_Loop_TypeDef loop, uint32_t period_ms);

#endif  // __TASK_H__

#include "task.h"

TASK task[TSK];

__IO uint32_t load_cpu;
__IO uint32_t current_load;

void task_stat();

void SysTick_Handler(void) {
    for(char i=0; i<TSK; i++)
    {
        if (task[i].handler == NULL) break;

        if (!task[i].counter)
        {
            task[i].counter=task[i].period;
            task[i].handler();
            if (!task[i].loop)
                remove_task(i);
            break;
        } else
            task[i].counter--;
    }
    current_load +=(72000-SysTick->VAL);
}

void remove_task(uint8_t num) {
    char i;
    for(i=num; i<(TSK-2);i++) {
        task[i]=task[i+1];
    }
    task[TSK-1].counter=0;
    task[TSK-1].period=0;
    task[TSK-1].loop=NOLOOP;
    task[TSK-1].handler=NULL;
}

void add_task(  void (*callback)(void),
                Task_Priority_TypeDef rank,
                Task_Loop_TypeDef loop,
                uint32_t period_ms)
{
    char i=TSK-1;
    do {
        if (task[i-1].handler != NULL) {
            task[i]=task[i-1];
        }
        i--;
    } while (i);
    task[i].loop=loop;
    task[i].period=period_ms;
    task[i].handler=callback;
    task[i].counter=period_ms;
}

void clear_task(void) {
    for(char i=0;i<TSK;i++) {
        task[i].loop=ENABLE;
        task[i].period=0;
        task[i].counter=0;
        task[i].handler=NULL;
    }
    load_cpu=0;
    current_load=0;
    add_task(task_stat,TOP,LOOP,1000);
}


uint32_t get_load_cpu() {
    return load_cpu;
}

void task_stat() {
    load_cpu=current_load;
    current_load=0;
}

Привожу тестовый пример main.c с миганием светодиода через планировщик:

#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
#include "uart.h"
#include "task.h"

extern void delay(uint32_t ms);
void toggle_led();

int main()
{
    // enable GPIOC port
    RCC->APB2ENR |= RCC_APB2Periph_GPIOC;           // enable PORT_C
    RCC->APB2ENR |= RCC_APB2Periph_GPIOA;           // enable PORT_A
    RCC->APB2ENR |= RCC_APB2Periph_USART1;          // enable UART1
    // --- GPIO setup ----
    GPIOC->CRH &= ~(uint32_t)(0xf<<20);
    GPIOC->CRH |=  (uint32_t)(0x2<<20);
    GPIOA->CRH &= ~(uint32_t)(0xf<<4);
    GPIOA->CRH |=  (uint32_t)(0xa<<4);
    // --- UART setup ----
    //USART1->BRR  = 0x1d4c;                                // 9600 Baud, when APB2=72MHz
    USART1->BRR = 0x271;                                // 115200 Baud, when APB2=72MHz
    USART1->CR1 |= USART_CR1_UE_Set | USART_Mode_Tx;    // enable USART1, enable  TX mode
    // Let's go..
    clear_task();
    add_task(toggle_led,TOP, LOOP, 1000);
    if (SysTick_Config(72000)) // set 1ms
    {
        while(1); // error
    }
    __enable_irq();
    for(;;){
        delay(1000);
        usart1_print_string("load cpu: ");
        usart1_print_number(get_load_cpu());
        usart1_send_char('\n');
    }
}

void toggle_led() {
    GPIOC->ODR ^= GPIO_Pin_13;
}

Я скомпилировал проект с опцией оптимизации -O2, и получил следующий результат загрузки CPU:

Т.к. в планировщике всего одна задача, данные цифры показывают затраты СPU на обслуживание работы самого планировщика. И эти затраты равны ~0.1%.

Если скомпилировать проект с опцией оптимизации -O0, то во-первых: размер прошивки увеличится примерно на треть, во-вторых: статистика покажет вдвое большую загруженность CPU. Как я говорил самом начале: больший размер кода влечет большую загруженность CPU.

10 Драйвер 4-x разрядного семисегментного индикатора (программный SPI)

Для тестирования планировщика возьмем для примера драйвер 4-x разрядного семисегментного индикатора. Он использует динамическую индикацию, что требует довольно высокой скорости обновления: от 5 мс и меньше. Посмотрим как планировщик будет с этим справляться.

Для начала будем использовать программную реализацию SPI протокола. За образец возьмём код драйвера для STM8 из статьи STM8S + SDCC: Программирование на связке языков Си и ассемблера. Немного усложним задачу и будем использовать для вывода все 4 сегмента, вместо трех, как было в оригинале.

Итак, добавляем в проект заголовочный файл драйвера: led.h

#ifndef __LED_H__
#define __LED_H__

#include "stm32f10x.h"
#include "stm32f10x_gpio.h"

#define SCLK GPIO_Pin_5
#define RCLK GPIO_Pin_4
#define DIO  GPIO_Pin_7

uint8_t reg;
__IO uint32_t led;

void show_led();

#endif

Файл с исходным код драйвера led.c

 #include "led.h"

const char digit[10] = {
      0b11000000, // 0
      0b11111001, // 1
      0b10100100, // 2
      0b10110000, // 3
      0b10011001, // 4
      0b10010010, // 5
      0b10000010, // 6
      0b11111000, // 7
      0b10000000, // 8
      0b10010000, // 9
};

void spi_transmit(uint8_t data) {
    for (char i=0; i<8; i++)
    {
        if (data & 0x80)
            GPIOA->BSRR=DIO;
        else
            GPIOA->BRR=DIO;

        data=(data<<1);
        GPIOA->BSRR = SCLK;
        GPIOA->BRR  = SCLK;
    }
}

void to_led(uint8_t value, uint8_t reg) {
    GPIOA->BRR = RCLK;
    spi_transmit(digit[value]);
    spi_transmit(reg);
    GPIOA->BSRR  = RCLK;
}

void show_led() {
    switch (reg) {
    case 0:
        to_led((uint8_t)(led%10),1);
        break;
    case 1:
        if (led>=10)
            to_led((uint8_t)((led%100)/10),2);
        break;
    case 2:
        if (led>=100)
            to_led((uint8_t)((led%1000)/100),4);
        break;
    case 3:
        if (led>=1000)
            to_led((uint8_t)(led/1000),8);
        break;
     }
    reg = (reg ==  3) ? 0 : reg+1;
}

main.c в таком случае будет выглядеть так:

#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
#include "uart.h"
#include "task.h"
#include "led.h"

extern void delay(uint32_t ms);
void toggle_led();

int main()
{
    // enable GPIOC port
    RCC->APB2ENR |= RCC_APB2Periph_GPIOC;           // enable PORT_C
    RCC->APB2ENR |= RCC_APB2Periph_GPIOA;           // enable PORT_A
    RCC->APB2ENR |= RCC_APB2Periph_USART1;          // enable UART1
    // --- GPIO setup ----
    GPIOC->CRH &= ~(uint32_t)(0xf<<20);
    GPIOC->CRH |=  (uint32_t)(0x2<<20);
    // Setup PA9
    GPIOA->CRH &= ~(uint32_t)(0xf<<4);
    GPIOA->CRH |=  (uint32_t)(0xa<<4);
    // Setup PA4, PA5, PA7
    GPIOA->CRL &= ~(uint32_t)(0xf<<16);             // clear mode for PA4
    GPIOA->CRL &= ~(uint32_t)(0xf<<20);             // clear mode for PA5
    GPIOA->CRL &= ~(uint32_t)(0xf<<28);             // clear mode for PA7
    GPIOA->CRL |= (uint32_t)(0x3<<16);              // for PA4 set  PushPull mode 50MHz
    GPIOA->CRL |= (uint32_t)(0x3<<20);              // for PA5 set  PushPull mode 50MHz
    GPIOA->CRL |= (uint32_t)(0x3<<28);              // for PA7 set  PushPull mode 50MHz
    // --- UART setup ----
    //USART1->BRR  = 0x1d4c;                                // 9600 Baud, when APB2=72MHz
    USART1->BRR = 0x271;                                // 115200 Baud, when APB2=72MHz
    USART1->CR1 |= USART_CR1_UE_Set | USART_Mode_Tx;    // enable USART1, enable  TX mode
    // Let's go..
    reg=0; led=0;
    clear_task();
    add_task(show_led,TOP,LOOP, 5);
    add_task(toggle_led,TOP,LOOP,1000);
    if (SysTick_Config(72000)) // set 1ms
    {
        while(1); // error
    }
    __enable_irq();
    for(;;){
        asm("wfi");
    }
}

void toggle_led() {
    GPIOC->ODR ^= GPIO_Pin_13;
    led++;
    usart1_print_number(get_load_cpu());
    usart1_send_char('\n');
}

Подключается индикатор к SPI1 порту, а именно: PA4 на RCLK, PA5 к SCLK, PA7 на DIO, земля к земле, на питание индикатора подается 3.3 Вольта:

После загрузки прошивки смотрим на загрузку CPU:

Как видно, добавление индикатора подняло загрузку CPU до 0.3%, т.е. мы все еще используем меньше одного процента от общего ресурса CPU.

11 Настройка аппаратного интерфейса SPI для драйвера 4-х разрядного семисегментного индикатора

Признаюсь, что с SPI модулем мне пришлось порядком повозиться. И хотя конфигурация производится всего одним регистром - SPI_CR1, тут имеются свои подводные камни. Первая загвоздка заключается в том, что максимальная скорость SPI интерфейса в STM32F103C8T6 согласно спецификации составляет 18MHz, при том, что на SPI1, который тактируется от скоростной шины APB2 можно выставить значение в 36MHz (как я конечно же пытался сделать). Это работать не будет!

Вторая загвоздка заключалась в флагах TXE и BSY. В сети есть множество примеров работы в SPI в STM32 где анализируется только флаг TXE. На самом же деле, прежде чем опустить защелку нужно ожидать сброса флага BSY, т.к. при этом флаге производится передача из сдвигового регистра на шину, и если раньше времени опустить защелку, передача прервется. Подробнее об этом можно почитать на хабре: STM32: SPI: LCD — Вы всё делаете не так [восклицательный знак].

В отличии от STM8, микроконтроллеры STM32 поддерживают 8-и и 16-и битные режимы. Т.к. в индикаторе стоит два сдвиговых регистра, то мы будем использовать 16-битный режим.

Подробно про SPI в STM32 можно почитать здесь: SPI (перевод из книги Mastering STM32)

Вкратце пробежимся по регистрам SPI_CR1 и SPI_SR.

По большому счету, в регистре SPI_CR1 мы видим все те-же флаги, что и в STM8. Но если там они были разбросаны по двум 8-битным регистрам: SPI_CR1 и SPI_CR2, то здесь они собраны в один регистр SPI_CR1.

Биты: BIDIMODE, BIDIOE, CRCEN, CRCNEXT, RXONLY нас сейчас не будут интересовать, они должны быть сброшены в ноль. Биты CPOL, CPHA устанавливают SPI режим. В нашем случае, для установки SPI режима "Mode 0" требуется чтобы CPOL и CPHA были сброшены в ноль. LSBFIRST устанавливает порядок передачи битов, в нашем случае данные передаются старшим битом вперед, следовательно LSBFIRST также должен быть сброшен в ноль.

Биты SSM и SSI разрешают программное управление защелкой, они должны быть установлены в "1". MSTR - включает режим мастера, должен быть установлен в "1". SPE - включает SPI модуль, должен быть установлен в "1". DFF - включает 16-битный режим, также должен быть установлен в "1". BR - устанавливает предделитель. Минимальный предделитель равен двум. Интерфейс SPI1 - тактируется от периферийной шины APB2, максимальная частота которой 72 МГц. Интерфейс SPI2 - тактируется от периферийной шины APB1, максимальная частота которой 36 МГц. Следовательно максимальная частота интерфейсов: SPI1 - 36 МГц, а SPI2 - 18 МГц. Но не забываем, что SPI не будет работать со скоростью 36 MHz, поэтому фактический минимальный делитель для SPI1, в случае STM32F103xx, равен четырём.

В регистре SPI_SR нас будут интересовать флаги BSY и TXE. Флаг TXE автоматически устанавливается при записи в регистр данных - SPI_DR, и сбрасывается когда значение из регистра данных уходит в сдвиговый регистр. Флаг BSY устанавливается в "1", когда сдвиговый регистр не пуст, т.е. идет передача на линию.

Для использования аппаратного SPI модуля потребуется добавить в проект заголовочный файл stm32f10x_spi.h из библиотеки SPL. Все заголовочные файлы из SPL добавляются проект без изменений, как есть. К сожалению, в stm32f10x_spi.h были не все битовые маски, поэтому именованные константы из stm32f10x_spi.с пришлось перенести в spi.h:

#ifndef __SPI_H__
#define __SPI_H__

#include "stm32f10x.h"
#include "stm32f10x_spi.h"

/* SPI SPE mask */
#define CR1_SPE_Set          ((uint16_t)0x0040)
#define CR1_SPE_Reset        ((uint16_t)0xFFBF)

/* I2S I2SE mask */
#define I2SCFGR_I2SE_Set     ((uint16_t)0x0400)
#define I2SCFGR_I2SE_Reset   ((uint16_t)0xFBFF)

/* SPI CRCNext mask */
#define CR1_CRCNext_Set      ((uint16_t)0x1000)

/* SPI CRCEN mask */
#define CR1_CRCEN_Set        ((uint16_t)0x2000)
#define CR1_CRCEN_Reset      ((uint16_t)0xDFFF)

/* SPI SSOE mask */
#define CR2_SSOE_Set         ((uint16_t)0x0004)
#define CR2_SSOE_Reset       ((uint16_t)0xFFFB)

/* SPI registers Masks */
#define CR1_CLEAR_Mask       ((uint16_t)0x3040)
#define I2SCFGR_CLEAR_Mask   ((uint16_t)0xF040)

/* SPI or I2S mode selection masks */
#define SPI_Mode_Select      ((uint16_t)0xF7FF)
#define I2S_Mode_Select      ((uint16_t)0x0800)

/* I2S clock source selection masks */
#define I2S2_CLOCK_SRC       ((uint32_t)(0x00020000))
#define I2S3_CLOCK_SRC       ((uint32_t)(0x00040000))
#define I2S_MUL_MASK         ((uint32_t)(0x0000F000))
#define I2S_DIV_MASK         ((uint32_t)(0x000000F0))

#endif  // __SPI_H__

Файл main.c получился таким:

#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
#include "uart.h"
#include "task.h"
#include "led.h"
#include "spi.h"

extern void delay(uint32_t ms);
void toggle_led();

int main()
{
    // enable GPIOC port
    RCC->APB2ENR |= RCC_APB2Periph_GPIOC;           // enable PORT_C
    RCC->APB2ENR |= RCC_APB2Periph_GPIOA;           // enable PORT_A
    RCC->APB2ENR |= RCC_APB2Periph_USART1;          // enable UART1
    RCC->APB2ENR |= RCC_APB2Periph_SPI1;            // enable SPI1
    // --- GPIO setup ----
    GPIOC->CRH &= ~(uint32_t)(0xf<<20);
    GPIOC->CRH |=  (uint32_t)(0x2<<20);
    // Setup PA9
    GPIOA->CRH &= ~(uint32_t)(0xf<<4);
    GPIOA->CRH |=  (uint32_t)(0xa<<4);
    // Setup PA4, PA5, PA7
    GPIOA->CRL &= ~(uint32_t)(0xf<<16);             // clear mode for PA4
    GPIOA->CRL &= ~(uint32_t)(0xf<<20);             // clear mode for PA5
    GPIOA->CRL &= ~(uint32_t)(0xf<<28);             // clear mode for PA7
    GPIOA->CRL |= (uint32_t)(0x3<<16);              // for PA4 set  PushPull mode, 50MHz
    GPIOA->CRL |= (uint32_t)(0xb<<20);              // for PA5 set  Alternative mode, 50MHz
    GPIOA->CRL |= (uint32_t)(0xb<<28);              // for PA7 set  Alternative mode, 50MHz
    // --- UART setup ----
    //USART1->BRR  = 0x1d4c;                                // 9600 Baud, when APB2=72MHz
    USART1->BRR = 0x271;                                // 115200 Baud, when APB2=72MHz
    USART1->CR1 |= USART_CR1_UE_Set | USART_Mode_Tx;    // enable USART1, enable  TX mode
    // --- SPI1 setup ----
    SPI1->CR1 = CR1_SPE_Set|SPI_Mode_Master|SPI_DataSize_16b|SPI_NSS_Soft|SPI_BaudRatePrescaler_4;;
    // Let's go..
    reg=0; led=0;
    clear_task();
    add_task(show_led,TOP,LOOP, 5);
    add_task(toggle_led,TOP,LOOP,1000);
    if (SysTick_Config(72000)) // set 1ms
    {
        while(1); // error
    }
    __enable_irq();

    for(;;){
        asm("wfi");
    }
}

void toggle_led() {
    GPIOC->ODR ^= GPIO_Pin_13;
    led++;
    usart1_print_number(get_load_cpu());
    usart1_send_char('\n');
}

Замечу, что константа SPI_Mode_Master включает в себя также установку бита SSI. Остальное не должно вызвать вопросов.

Некоторые изменения претерпел также файл led.c:

#include "led.h"
#include "spi.h"

const uint16_t  digit[10] = {
      0xC000, // 0
      0xF900, // 1
      0xA400, // 2
      0xB000, // 3
      0x9900, // 4
      0x9200, // 5
      0x8200, // 6
      0xF800, // 7
      0x8000, // 8
      0x9000, // 9
};

void to_led(uint8_t value, uint16_t reg) {
    GPIOA->BSRR = RCLK;
    SPI1->DR=(digit[value]|reg);
    while (!(SPI1->SR & SPI_I2S_FLAG_TXE) || (SPI1->SR & SPI_I2S_FLAG_BSY));
    GPIOA->BRR  = RCLK;
}

void show_led() {
    switch (reg) {
    case 0:
        to_led((uint8_t)(led%10),1);
        break;
    case 1:
        if (led>=10)
            to_led((uint8_t)((led%100)/10),2);
        break;
    case 2:
        if (led>=100)
            to_led((uint8_t)((led%1000)/100),4);
        break;
    case 3:
        if (led>=1000)
            to_led((uint8_t)(led/1000),8);
        break;
     }
    reg = (reg ==  3) ? 0 : reg+1;
}
Здесь я заменил таблицу digit[] на константы 16-битных чисел, чтобы избежать преобразования из 8-битного числа в 16-битное, и за ненадобностью удалил функцию void spi_transmit(uint8_t data).

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

Как можно видеть, сколь-либо значительного отличия в быстродействии от программного SPI нет. Подключение индикатора к BluePill такое же как в предыдущем случае: PA4 подключается к RCLK, PA5 к SCLK, PA7 к DIO.

12 Регистры I2C интерфейса, делаем сканер I2C шины

I2C интерфейс STM32 так же в чем то походит на свой аналог в STM8: здесь имеются standard и fast режимы, возможность работать с 7-битными и 10-битными I2C адресами, наличествуют режимы чтения по одному и по двум байтам, и т.д. Однако в деталях и в порядке работы с интерфейсом имеются существенные отличия. Давайте разбираться.

Для начала попробуем сделать сканер I2C шины. Для этого потребуется написать код инициализации I2C модуля STM32, и код инициализации I2C устройства, т.е. получения от него отклика ACK.

Для этого нам понадобится ознакомиться с регистрами I2C модуля. Ниже представлен первый конфигурационный регистр:

Здесь нам будет мешаться биты SMB шины, которая является разновидностью I2C, и в STM32 реализована на базе I2C модуля. Биты: ALERT, PEC, NO STRETCH, ENGC, ENPEC, ENARP, SMB TYPE, SMBUS - нас не будут интересовать т.к. они отвечают за работу SMB шины. Оставшиеся биты знакомы по STM8. PE - включает/выключает I2C модуль. Установка этого бита в ноль, сбрасывает I2C модуль и переводит его в состояние IDLE. SWRST - "отпускает" I2C шину. START - посылает сигнал START на линию, и переводит модуль в режим мастера. Если I2C модуль уже находился в режиме мастера, то повторный START посылает сигнал RESTART. Бит START устанавливается программно, а сбрасывается аппаратно. STOP - посылает одноимённый сигнал на линию, и так же сбрасывается аппаратно. Передача сигнала STOP переводит I2C модуль в режим слейва. ACK и POS конфигурируют режим чтения, в зависимости от комбинации этих битов, а также бита STOP, задают тот или иной режим чтения данных. Подробнее об этом ниже.

В настоящий момент нас пока будет волновать только бит PE отвечающий за включение I2C модуля.

Второй конфигурационный регистр:

Здесь нас будет интересовать только последнее поле FREQ, в котором следует указать частоту периферийной шины в мегагерцах, которая тактирует I2C модуль. В нашем случае это APB1 с частотой 36МНz. Т.о. в разделе инициализации I2C пишем:

    I2C1->CR2 &= I2C_CR2_FREQ_Reset;    //=0xffc0
    I2C1->CR2 |= 36;                    // set FREQ = APB1= 36MHz

Регистр I2C_CCR задает предделитель на I2C шину:

Здесь биты F/S и DUTY отвечают за fast режим I2C который работает на 400 kHz, они нас не будут интересовать, пока будем использовать самый простой standard режим 100 kHz.

Поле CRR вычисляется как отношение частоты APB шины, к частоте полупериода I2C шины. Т.е. в нашем случае: CCR= 36 * 10^6/(2 * 0.1 *10^6) = 36/0.2 = 36 * 5 = 180.

Таким образом, в раздел инициализации I2C добавляем:

   I2C1->CCR = 180; 

Последний регистр который участвует в инициализации I2C модуля - I2C_TRISE:

Насколько я понял, регистр задает время нарастания фронта I2C шины в тактах APB шины. Вычисляется: TRISE = количество МГц APB1-шины + 1. В нашем случае TRISE будет равняться 37.

При работе с I2C модулем мы будем использовать регистр данных I2C_DR:

Регистр 16-битный, но рабочие биты только младшие восемь.

Ну и наверное главный рабочий регистр при работе с I2C - это флаговый регистр I2C_SR1:

Здесь нас будут интересовать следующие флаги: SB - флаг сигнала START, ADDR - флаг успешной передачи адреса с получением ACK в ответе, BTF - флаг окончания передачи, TxE - флаг очистки регистра данных, когда байт из регистра данных уходит в сдвиговый регистр, RxNE - флаг поступления данных в регистр данных, AF - флаг получения NACK. Остальные флаги относятся либо к slave режиму либо в SMB, нас они интересовать не будут.

Итак, начинаем писать код. Нам понадобится заголовочный файл из stm32f10x_i2c.h в котором содержатся нужные нам для работы с I2C маски регистров. Т.к. часть масок содержится в файле stm32f10x_i2c.с их придётся скопировать в свой заголовочный файл.

Создадим файл inc/i2c.h следующего содержания:

#ifndef __I2C_H__
#define __I2C_H__

#include "stm32f10x.h"
#include "stm32f10x_i2c.h"

/* I2C SPE mask */
#define I2C_CR1_PE_Set              ((uint16_t)0x0001)
#define I2C_CR1_PE_Reset            ((uint16_t)0xFFFE)

/* I2C START mask */
#define I2C_CR1_START_Set           ((uint16_t)0x0100)
#define I2C_CR1_START_Reset         ((uint16_t)0xFEFF)

/* I2C STOP mask */
#define I2C_CR1_STOP_Set            ((uint16_t)0x0200)
#define I2C_CR1_STOP_Reset          ((uint16_t)0xFDFF)

/* I2C ACK mask */
#define I2C_CR1_ACK_Set             ((uint16_t)0x0400)
#define I2C_CR1_ACK_Reset           ((uint16_t)0xFBFF)

/* I2C ENGC mask */
#define I2C_CR1_ENGC_Set            ((uint16_t)0x0040)
#define I2C_CR1_ENGC_Reset          ((uint16_t)0xFFBF)

/* I2C SWRST mask */
#define I2C_CR1_SWRST_Set           ((uint16_t)0x8000)
#define I2C_CR1_SWRST_Reset         ((uint16_t)0x7FFF)

/* I2C PEC mask */
#define I2C_CR1_PEC_Set             ((uint16_t)0x1000)
#define I2C_CR1_PEC_Reset           ((uint16_t)0xEFFF)

/* I2C ENPEC mask */
#define I2C_CR1_ENPEC_Set           ((uint16_t)0x0020)
#define I2C_CR1_ENPEC_Reset         ((uint16_t)0xFFDF)

/* I2C ENARP mask */
#define I2C_CR1_ENARP_Set           ((uint16_t)0x0010)
#define I2C_CR1_ENARP_Reset         ((uint16_t)0xFFEF)

/* I2C NOSTRETCH mask */
#define I2C_CR1_NOSTRETCH_Set       ((uint16_t)0x0080)
#define I2C_CR1_NOSTRETCH_Reset     ((uint16_t)0xFF7F)

/* I2C registers Masks */
#define I2C_CR1_CLEAR_Mask          ((uint16_t)0xFBF5)

/* I2C DMAEN mask */
#define I2C_CR2_DMAEN_Set           ((uint16_t)0x0800)
#define I2C_CR2_DMAEN_Reset         ((uint16_t)0xF7FF)

/* I2C LAST mask */
#define I2C_CR2_LAST_Set            ((uint16_t)0x1000)
#define I2C_CR2_LAST_Reset          ((uint16_t)0xEFFF)

/* I2C FREQ mask */
#define I2C_CR2_FREQ_Reset          ((uint16_t)0xFFC0)

/* I2C ADD0 mask */
#define I2C_OAR1_ADD0_Set           ((uint16_t)0x0001)
#define I2C_OAR1_ADD0_Reset         ((uint16_t)0xFFFE)

/* I2C ENDUAL mask */
#define I2C_OAR2_ENDUAL_Set         ((uint16_t)0x0001)
#define I2C_OAR2_ENDUAL_Reset       ((uint16_t)0xFFFE)

/* I2C ADD2 mask */
#define I2C_OAR2_ADD2_Reset         ((uint16_t)0xFF01)

/* I2C F/S mask */
#define I2C_CCR_FS_Set              ((uint16_t)0x8000)

/* I2C CCR mask */
#define I2C_CCR_CCR_Set             ((uint16_t)0x0FFF)

/* I2C FLAG mask */
#define I2C_FLAG_Mask               ((uint32_t)0x00FFFFFF)

/* I2C Interrupt Enable mask */
#define I2C_ITEN_Mask               ((uint32_t)0x07000000)

#define DS3231_I2C_ADDR (0x68<<1)
#define DS3231_CONTROL_REG ((uint8_t)0x0E)
#define DS3231_STATUS_REG ((uint8_t)0x0F)

#define LAST    ((uint8_t)0x01)
#define NOLAST  ((uint8_t)0x00)

#define enable_i2c      I2C1->CR1 |= I2C_CR1_PE_Set;    // =1, disable I2C
#define disable_i2c     I2C1->CR1 &= I2C_CR1_PE_Reset;  // disable I2C
#define stop_i2c        I2C1->CR1 |= I2C_CR1_STOP_Set;  //=0x0200, send STOP

uint8_t init_i2c(uint8_t adr, uint8_t value, uint8_t last);

#endif

Здесь uint8_t init_i2c(uint8_t adr, uint8_t value, uint8_t last); - прототип функции инициализации I2C устройства. Через функцию вместе с инициализацией передаётся еще один байт. Из практики следует, что если мы "стучимся" на какое-то устройство, значит мы хотим передать ему какую-то команду. Флаги LAST/NOLAST указывают, будет ли этот байт единственным или за ним последуют еще байты.

Основной документ, который нам понадобится для работы с I2C модулем - это аппнот 2824: "STM32F10xxx I2C optimized examples, Application note AN2824"

Алгоритм работы функции инициализации представлен на следующей блок-схеме взятой из аппнота:

1) Вначале мы должны послать сигнал START и дождаться установки флага SB. 2) После сбрасываем флаг SB чтением регистра I2C_SR1. 3) Далее посылаем адрес и ждем установки флага ADDR. 4) Затем сбрасываем флаг ADDR и пишем наш байт данных в регистр I2C_DR и в зависимости от того, будет ли этот байт последним, ждем установки флага TxE или BTF. 5) Если байт был последним, то после установки флага BTF посылаем сигнал STOP, и ждем аппаратного сброса этого флага.

Создаем файл src/i2c.c и пишем в нем реализацию функции init_i2c():

#include "i2c.h"

uint8_t init_i2c(uint8_t adr, uint8_t value, uint8_t last) {
    I2C1->CR1 |= I2C_CR1_START_Set;             //=0x0100, START
    while (!(I2C1->SR1 & I2C_FLAG_SB));         // wait SB
    (void) I2C1->SR1;
    I2C1->DR=adr;                               // send ADDR
    while (!(I2C1->SR1 & I2C_FLAG_ADDR))        // wait ADDR
    {
        if(I2C1->SR1 & I2C_IT_AF)               // if NACK
            return 1;
    }
    (void) I2C1->SR1;                           // clear ADDR
    (void) I2C1->SR2;                           // clear ADDR
    I2C1->DR=value;
    if (last == LAST) {
        while(!(I2C1->SR1 & I2C_FLAG_BTF));     // wait BFT
        I2C1->CR1 |= I2C_CR1_STOP_Set;          // Program the STOP bit
        while (I2C1->CR1 & I2C_CR1_STOP_Set);   // Wait until STOP bit is cleared by hardware
    } else {
        while(!(I2C1->SR1 & I2C_FLAG_TXE));     // wait TXE bit
    }
    return 0;
}

От себя я добавил выход из функции в случае получения NACK при посылке I2C адреса.

Файл main.c будет выглядеть таким образом:

#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
#include "uart.h"
#include "task.h"
#include "led.h"
#include "spi.h"
#include "i2c.h"

extern void delay(uint32_t ms);
void toggle_led();

int main()
{
    // enable GPIOC port
    RCC->APB2ENR |= RCC_APB2Periph_GPIOA;           // enable PORT_A
    RCC->APB2ENR |= RCC_APB2Periph_GPIOB;           // enable PORT_B
    RCC->APB2ENR |= RCC_APB2Periph_GPIOC;           // enable PORT_C
    RCC->APB2ENR |= RCC_APB2Periph_USART1;          // enable UART1
    RCC->APB2ENR |= RCC_APB2Periph_SPI1;            // enable SPI1
    RCC->APB1ENR |= RCC_APB1Periph_I2C1;            // enable I2C1
    // --- GPIO setup ----
    GPIOC->CRH &= ~(uint32_t)(0xf<<20);
    GPIOC->CRH |=  (uint32_t)(0x2<<20);
    // Setup PA9
    GPIOA->CRH &= ~(uint32_t)(0xf<<4);
    GPIOA->CRH |=  (uint32_t)(0xa<<4);
    // Setup PA4, PA5, PA7
    GPIOA->CRL &= ~(uint32_t)(0xf<<16);             // clear mode for PA4
    GPIOA->CRL &= ~(uint32_t)(0xf<<20);             // clear mode for PA5
    GPIOA->CRL &= ~(uint32_t)(0xf<<28);             // clear mode for PA7
    GPIOA->CRL |= (uint32_t)(0x3<<16);              // for PA4 set  PushPull mode, 50MHz
    GPIOA->CRL |= (uint32_t)(0xb<<20);              // for PA5 set  Alternative mode, 50MHz
    GPIOA->CRL |= (uint32_t)(0xb<<28);              // for PA7 set  Alternative mode, 50MHz
    // Setup PB6, PB7
    GPIOB->CRL &= ~(uint32_t)(0xf<<24);             // clear mode for PB6
    GPIOB->CRL &= ~(uint32_t)(0xf<<28);             // clear mode for PB7
    GPIOB->CRL |= (uint32_t)(0xe<<24);              // for PB6 set  Alternative mode, 2MHz, OpenDrain
    GPIOB->CRL |= (uint32_t)(0xe<<28);              // for PB7 set  Alternative mode, 2MHz, OpenDrain

    // --- UART setup ----
    //USART1->BRR  = 0x1d4c;                                // 9600 Baud, when APB2=72MHz
    USART1->BRR = 0x271;                                // 115200 Baud, when APB2=72MHz
    USART1->CR1 |= USART_CR1_UE_Set | USART_Mode_Tx;    // enable USART1, enable  TX mode
    // --- SPI1 setup ----
    SPI1->CR1 = CR1_SPE_Set|SPI_Mode_Master|SPI_DataSize_16b|SPI_NSS_Soft|SPI_BaudRatePrescaler_4;;
    // --- I2C setup ----
    disable_i2c;
    I2C1->CR2 &= I2C_CR2_FREQ_Reset;    //=0xffc0
    I2C1->CR2 |= 36;                    // set FREQ = APB1= 36MHz
    I2C1->CCR = 180;                    // 100 KHz
    I2C1->TRISE = 37;

    // Let's go..
    reg=0; led=0;
    clear_task();
    add_task(show_led,TOP,LOOP, 5);
    add_task(toggle_led,TOP,LOOP,1000);
    if (SysTick_Config(72000)) // set 1ms
    {
        while(1); // error
    }
    __enable_irq();
    uint8_t adr;
    for(;;){
        delay(3000);
        for(adr=0;adr<128;adr++) {
            enable_i2c;
            if (init_i2c((adr<<1), 0x0, LAST) == 0) {
                usart1_print_string("Device was found: ");
                usart1_print_hex(adr);
                usart1_send_char('\n');
            } else
                stop_i2c;
            delay(20);
            disable_i2c;
        }

        GPIOC->ODR ^= GPIO_Pin_13;
    }
}

void toggle_led() {
    led++;
//  usart1_print_number(get_load_cpu());
//  usart1_send_char('\n');
}

Насколько понимаю, в данном случае работа I2C модуля не совсем корректна, т.к. модуль приходится постоянно отключать и включать заново. В дальнейшем, при нормальной работе это не потребуется.

Я тестировал код на китайском модуле RTC DS3231 + EEPROM AT24С32. Подключение: SCL на PB6, SDA на PB7. Результат работы сканера:

13 Однобайтный режим чтения по шине I2C

Однобайтное чтение используется когда нужно прочитать какой-либо регистр на I2C устройстве. Для примера, с помощью функции чтения одного байта мы прочитаем текущее время в DS3231.

Порядок работы с I2C модулем в режиме чтения одного байта продемонстрирован на следующей блок-схеме:

Вооружившись этим знанием, добавляем в src/i2c.c функцию чтения:

uint8_t read_byte(uint8_t adr){
    uint8_t ret;
    I2C1->CR1 |= I2C_CR1_START_Set;         // START, =0x0100
    while (!(I2C1->SR1 & I2C_FLAG_SB));     // wait SB, while(!(I2C1->SR1 & 0x0001));
    (void) I2C1->SR1;                       // clear SB
    I2C1->DR=adr;
    while (!(I2C1->SR1 & I2C_FLAG_ADDR));   // wait ADDR, while(!(I2C1->SR1 & 0x0002));
    I2C1->CR1 &= I2C_CR1_ACK_Reset;         // 1.NACK, =0xFBFF
    __disable_irq();
    (void) I2C1->SR1;                       // 2.reset ADDR
    (void) I2C1->SR2;                       // 2.reset ADDR
    I2C1->CR1 |= I2C_CR1_STOP_Set;          // 3.STOP, =0x0200
    __enable_irq();
    while(!(I2C1->SR1 & I2C_IT_RXNE));      // wait RxNE, while(!(I2C1->SR1 & 0x0040));
    ret=I2C1->DR;
    while (I2C1->CR1 & I2C_CR1_STOP_Set);   // wait clear STOP

    I2C1->CR1 |= I2C_CR1_ACK_Set;           // ACK for next byte
    return ret;

}

Для печати BCD числа через UART нам потребуется добавить следующую функцию к src/uart.c:

void usart1_print_bcd(uint8_t num) {
    USART1->DR=(num>>4) + 0x30;
    while(!(USART1->SR & USART_FLAG_TXE));
    USART1->DR=(num & 0x0f) + 0x30;
    while(!(USART1->SR & USART_FLAG_TXE));
}

Осталось только немного изменить main.c и дело в шляпе:

#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
#include "uart.h"
#include "task.h"
#include "led.h"
#include "spi.h"
#include "i2c.h"

extern void delay(uint32_t ms);
void toggle_led();

int main()
{
    // enable GPIOC port
    RCC->APB2ENR |= RCC_APB2Periph_GPIOA;           // enable PORT_A
    RCC->APB2ENR |= RCC_APB2Periph_GPIOB;           // enable PORT_B
    RCC->APB2ENR |= RCC_APB2Periph_GPIOC;           // enable PORT_C
    RCC->APB2ENR |= RCC_APB2Periph_USART1;          // enable UART1
    RCC->APB2ENR |= RCC_APB2Periph_SPI1;            // enable SPI1
    RCC->APB1ENR |= RCC_APB1Periph_I2C1;            // enable I2C1
    // --- GPIO setup ----
    GPIOC->CRH &= ~(uint32_t)(0xf<<20);
    GPIOC->CRH |=  (uint32_t)(0x2<<20);
    // Setup PA9
    GPIOA->CRH &= ~(uint32_t)(0xf<<4);
    GPIOA->CRH |=  (uint32_t)(0xa<<4);
    // Setup PA4, PA5, PA7
    GPIOA->CRL &= ~(uint32_t)(0xf<<16);             // clear mode for PA4
    GPIOA->CRL &= ~(uint32_t)(0xf<<20);             // clear mode for PA5
    GPIOA->CRL &= ~(uint32_t)(0xf<<28);             // clear mode for PA7
    GPIOA->CRL |= (uint32_t)(0x3<<16);              // for PA4 set  PushPull mode, 50MHz
    GPIOA->CRL |= (uint32_t)(0xb<<20);              // for PA5 set  Alternative mode, 50MHz
    GPIOA->CRL |= (uint32_t)(0xb<<28);              // for PA7 set  Alternative mode, 50MHz
    // Setup PB6, PB7
    GPIOB->CRL &= ~(uint32_t)(0xf<<24);             // clear mode for PB6
    GPIOB->CRL &= ~(uint32_t)(0xf<<28);             // clear mode for PB7
    GPIOB->CRL |= (uint32_t)(0xe<<24);              // for PB6 set  Alternative mode, 2MHz, OpenDrain
    GPIOB->CRL |= (uint32_t)(0xe<<28);              // for PB7 set  Alternative mode, 2MHz, OpenDrain

    // --- UART setup ----
    //USART1->BRR  = 0x1d4c;                                // 9600 Baud, when APB2=72MHz
    USART1->BRR = 0x271;                                // 115200 Baud, when APB2=72MHz
    USART1->CR1 |= USART_CR1_UE_Set | USART_Mode_Tx;    // enable USART1, enable  TX mode
    // --- SPI1 setup ----
    SPI1->CR1 = CR1_SPE_Set|SPI_Mode_Master|SPI_DataSize_16b|SPI_NSS_Soft|SPI_BaudRatePrescaler_4;;
    // --- I2C setup ----
    disable_i2c;
    I2C1->CR2 &= I2C_CR2_FREQ_Reset;    //=0xffc0
    I2C1->CR2 |= 36;                    // set FREQ = APB1= 36MHz
    I2C1->CCR = 180;                    // 100 KHz
    I2C1->TRISE = 37;
    enable_i2c;

    // Let's go..
    reg=0; led=0;
    clear_task();
    add_task(show_led,TOP,LOOP, 5);
    add_task(toggle_led,TOP,LOOP,1000);
    if (SysTick_Config(72000)) // set 1ms
    {
        while(1); // error
    }
    __enable_irq();
    uint8_t adr;
    for(;;){
        uint8_t min,sec,hours;
        delay(1000);
        if (init_i2c((DS3231_I2C_ADDR), 0x0,LAST) == 0) {
            sec=read_byte(DS3231_I2C_ADDR|0x01);
            min=read_byte(DS3231_I2C_ADDR|0x01);
            hours=read_byte(DS3231_I2C_ADDR|0x01);
            usart1_print_string("time: ");
            usart1_print_bcd(hours);
            usart1_send_char(':');
            usart1_print_bcd(min);
            usart1_send_char(':');
            usart1_print_bcd(sec);
            usart1_send_char('\n');
        } else
            stop_i2c;


        GPIOC->ODR ^= GPIO_Pin_13;
    }
}

void toggle_led() {
    led++;
//  usart1_print_number(get_load_cpu());
//  usart1_send_char('\n');
}

На скриншоте представлен результат работы программы:

14 Двухбайтный режим чтения по шине I2C

Двухбайтный режим чтения используется для чтения 16-битных регистров, например в RDA5807. В STM32 можно организовать и трехбайтное чтение, но мне трудно представить, где это может понадобится. А вот однобайтный и двухбайтные режимы, на мой взгляд, используются довольно часто.

Порядок работы с I2C модулем в режиме двухбайтного чтения продемонстрирован на следующей блок-схеме:

Реализация этого алгоритма на Си у меня получилась такой:

uint16_t read_two_byte(uint8_t adr){
    uint16_t ret=0;
    uint8_t vl;
    I2C1->CR1 |= I2C_CR1_START_Set;         // START, =0x0100
    while (!(I2C1->SR1 & I2C_FLAG_SB));     // wait SB, while(!(I2C1->SR1 & 0x0001));
    (void) I2C1->SR1;
    I2C1->DR=adr;
    while (!(I2C1->SR1 & I2C_FLAG_ADDR));    // wait ADDR, while(!(I2C1->SR1 & 0x0002));
    I2C1->CR1 |=I2C_PECPosition_Next;       // POS=1
    __disable_irq();
    (void) I2C1->SR1;                       // reset ADDR
    (void) I2C1->SR2;                       // reset ADDR
    I2C1->CR1 &= I2C_CR1_ACK_Reset;         // NACK, =0xFBFF
    __enable_irq();
    while(!(I2C1->SR1 & I2C_FLAG_BTF));     // wait  BFT
    __disable_irq();
    I2C1->CR1 |= I2C_CR1_STOP_Set; ;        // STOP, =0x0200
    vl=I2C1->DR;
    ret|=(uint16_t)vl;
    __enable_irq();
    vl=I2C1->DR;
    ret|=(uint16_t)(vl<<8);
    while (I2C1->CR1 & I2C_CR1_STOP_Set);
    I2C1->CR1 &= ~(I2C_PECPosition_Next);   // POS=0
    I2C1->CR1 |= I2C_CR1_ACK_Set;           // ACK=1

    return ret;
}

Чтение RTC из главного цикла будет осуществляться таким образом:

    for(;;){
        uint8_t min,sec,hours;
        delay(1000);
        if (init_i2c(DS3231_I2C_ADDR, 0x0,LAST) == 0) {
            uint16_t t=read_two_byte(DS3231_I2C_ADDR|0x01);
            min=(uint8_t)(t>>8);
            sec=(uint8_t)(t & 0x00ff);
            t=read_two_byte(DS3231_I2C_ADDR|0x01);
            hours=(uint8_t)(t & 0x00ff);
            usart1_print_string("time: ");
            usart1_print_bcd(hours);
            usart1_send_char(':');
            usart1_print_bcd(min);
            usart1_send_char(':');
            usart1_print_bcd(sec);
            usart1_send_char('\n');
        } else
            stop_i2c;

        GPIOC->ODR ^= GPIO_Pin_13;
    }

15 Запись массива через шину I2C

Алгоритм записи одного или нескольких байт через I2C шину в STM32 я уже приводил ранее:

Опираясь на эту блок-схему нетрудно написать функцию записи одного числа:

void ds3231_write_register(uint8_t reg, uint8_t value) {
    if (init_i2c(DS3231_I2C_ADDR, reg, NOLAST) == 0) {
        I2C1->DR=value;
        while(!(I2C1->SR1 & I2C_FLAG_BTF));     // wait BFT
        I2C1->CR1 |= I2C_CR1_STOP_Set;          // Program the STOP bit
        while (I2C1->CR1 & I2C_CR1_STOP_Set);   // Wait until STOP bit is cleared by hardware
    }  else
        stop_i2c;
}

и функцию записи массива:

void i2c_write(uint8_t adr, uint8_t reg, uint8_t count, uint8_t* data) {
    // "count" param MUST BE more than 0!
    if (init_i2c(adr, reg, NOLAST) == 0) {
        for(uint8_t i=1;i<=count;i++,data++) {
            I2C1->DR=*data;
            if (i == count) {
                while(!(I2C1->SR1 & I2C_FLAG_BTF));     // wait BFT
                I2C1->CR1 |= I2C_CR1_STOP_Set;          // Program the STOP bit
                while (I2C1->CR1 & I2C_CR1_STOP_Set);   // Wait until STOP bit is cleared by hardware
            } else
                while(!(I2C1->SR1 & I2C_FLAG_TXE));     // wait BFT
        }
    }  else
        stop_i2c;
}

Запись даты в RTC c последующим циклом чтения будет выглядеть так:

    uint8_t cal[]={0x0,0x37,0x11,0x7,0x14,0x10,0x18};
    i2c_write(DS3231_I2C_ADDR, 0x0, 0x7, cal);
    for(;;){
        uint8_t min,sec,hours;
        delay(1000);
        usart1_print_string("Control: ");
        usart1_print_hex(ds3231_read_register(DS3231_CONTROL_REG));
        usart1_print_string(" Status: ");
        usart1_print_hex(ds3231_read_register(DS3231_STATUS_REG));
        usart1_send_char('\n');
        if (init_i2c(DS3231_I2C_ADDR, 0x0,LAST) == 0) {
            uint16_t t=read_two_byte(DS3231_I2C_ADDR|0x01);
            min=(uint8_t)(t>>8);
            sec=(uint8_t)(t & 0x00ff);
            t=read_two_byte(DS3231_I2C_ADDR|0x01);
            hours=(uint8_t)(t & 0x00ff);
            usart1_print_string("time: ");
            usart1_print_bcd(hours);
            usart1_send_char(':');
            usart1_print_bcd(min);
            usart1_send_char(':');
            usart1_print_bcd(sec);
            usart1_send_char('\n');
        }

        GPIOC->ODR ^= GPIO_Pin_13;
    }

16 Чтение массива через шину I2C

Функцию чтения одного байта можно развить до функции чтения массива. Для этого придётся ее утяжелить циклом последовательного чтения байтов с шины I2C.

У меня это получилось так:

void i2c_read(uint8_t adr, uint8_t count,uint8_t* data){
    uint8_t ret;
    I2C1->CR1 |= I2C_CR1_START_Set;         // START, =0x0100
    while (!(I2C1->SR1 & I2C_FLAG_SB));     // wait SB, while(!(I2C1->SR1 & 0x0001));
    (void) I2C1->SR1;
    I2C1->DR=adr;
    while (!(I2C1->SR1 & I2C_FLAG_ADDR));   // wait ADDR, while(!(I2C1->SR1 & 0x0002));
    I2C1->CR1 |= I2C_CR1_ACK_Set;           // set ACK, 0x0400
    (void) I2C1->SR1;                       // reset ADDR
    (void) I2C1->SR2;                       // reset ADDR

    for(uint8_t i=1;i<=count;i++, data++) {
        if (i<count) {
           while(!(I2C1->SR1 & I2C_IT_RXNE));      // wait RxNE, while(!(I2C1->SR1 & 0x0040));
            *data=I2C1->DR;
        } else {
            I2C1->CR1 &= I2C_CR1_ACK_Reset;         // set NACK, =0xFBFF
            I2C1->CR1 |= I2C_CR1_STOP_Set;          // set STOP, =0x0200
            while(!(I2C1->SR1 & I2C_IT_RXNE));      // wait RxNE, while(!(I2C1->SR1 & 0x0040));
            *data=I2C1->DR;
            while (I2C1->CR1 & I2C_CR1_STOP_Set);   // white STOP, =0x0200
        }
    }

    I2C1->CR1 |= I2C_CR1_ACK_Set;           // set ACK, =0x0400;
    return;

}

Для проверки работы функции можно использовать такой главный цикл чтения времени и даты с RTC:

    uint8_t cal[7];
    for(;;){
        delay(1000);
        usart1_print_string("Control: ");
        usart1_print_hex(ds3231_read_register(DS3231_CONTROL_REG));
        usart1_print_string(" Status: ");
        usart1_print_hex(ds3231_read_register(DS3231_STATUS_REG));
        if (init_i2c(DS3231_I2C_ADDR, 0x0,LAST) == 0) {
            i2c_read(DS3231_I2C_ADDR|0x01, 7,cal);
            usart1_print_string(" Time: ");
            usart1_print_bcd(cal[2]);
            usart1_send_char(':');
            usart1_print_bcd(cal[1]);
            usart1_send_char(':');
            usart1_print_bcd(cal[0]);
            usart1_print_string(" Data: ");
            usart1_print_bcd(cal[3]);
            usart1_send_char(':');
            usart1_print_bcd(cal[4]);
            usart1_send_char(':');
            usart1_print_bcd(cal[5]);
            usart1_send_char(':');
            usart1_print_bcd(cal[6]);
            usart1_send_char('\n');
        }
        GPIOC->ODR ^= GPIO_Pin_13;
    }

Результат работы программы:

17 Отладка в консоли с использованием OpenOCD

Отладку STM32 c помощью связки st-util + stlink_v2 в консоли я уже рассматривал ранее, однако большинство IDE в качестве gdb-сервера используют OpenOCD. Прежде чем "прикручивать" OpenOCD к IDE, нужно научиться запускать его в консоли.

Моя версия OpenOCD:

$ openocd --version
Open On-Chip Debugger 0.10.0
Licensed under GNU GPL v2
For bug reports, read
        http://openocd.org/doc/doxygen/bugs.html

Посмотрим, к чему мы можем подключиться с помощью OpenOCD:

$ ls  /usr/share/openocd/scripts/target/

1986ве1т.cfg             at91sam9g45.cfg       icepick.cfg     lpc4350.cfg          stm32f1x.cfg
adsp-sc58x.cfg           at91sam9rl.cfg        imx.cfg         lpc4357.cfg          stm32f1x_stlink.cfg
aduc702x.cfg             at91samdXX.cfg        imx21.cfg       lpc4370.cfg          stm32f2x.cfg
aducm360.cfg             at91samg5x.cfg        imx25.cfg       lpc8xx.cfg           stm32f2x_stlink.cfg
alphascale_asm9260t.cfg  atheros_ar2313.cfg    imx27.cfg       mc13224v.cfg         stm32f3x.cfg
altera_fpgasoc.cfg       atheros_ar2315.cfg    imx28.cfg       mdr32f9q2i.cfg       stm32f3x_stlink.cfg
am335x.cfg               atheros_ar9331.cfg    imx31.cfg       nds32v2.cfg          stm32f4x.cfg
am437x.cfg               atmega128.cfg         imx35.cfg       nds32v3.cfg          stm32f4x_stlink.cfg
amdm37x.cfg              atsamv.cfg            imx51.cfg       nds32v3m.cfg         stm32f7x.cfg
ar71xx.cfg               avr32.cfg             imx53.cfg       nrf51.cfg            stm32l0.cfg
armada370.cfg            bcm281xx.cfg          imx6.cfg        nrf51_stlink.tcl     stm32l1.cfg
at32ap7000.cfg           bcm4706.cfg           is5114.cfg      nrf52.cfg            stm32l1x_dual_bank.cfg
at91r40008.cfg           bcm4718.cfg           ixp42x.cfg      nuc910.cfg           stm32l4x.cfg
at91rm9200.cfg           bcm47xx.cfg           k1921vk01t.cfg  numicro.cfg          stm32lx_stlink.cfg
at91sam3XXX.cfg          bcm5352e.cfg          k40.cfg         omap2420.cfg         stm32w108_stlink.cfg
at91sam3ax_4x.cfg        bcm6348.cfg           k60.cfg         omap3530.cfg         stm32w108xx.cfg
at91sam3ax_8x.cfg        c100.cfg              ke02.cfg        omap4430.cfg         stm32xl.cfg
at91sam3ax_xx.cfg        c100config.tcl        ke04.cfg        omap4460.cfg         str710.cfg
at91sam3nXX.cfg          c100helper.tcl        ke06.cfg        omap5912.cfg         str730.cfg
at91sam3sXX.cfg          c100regs.tcl          kex.cfg         omapl138.cfg         str750.cfg
at91sam3u1c.cfg          cc2538.cfg            kl25.cfg        or1k.cfg             str912.cfg
at91sam3u1e.cfg          cc26xx.cfg            kl25z_hla.cfg   pic32mx.cfg          swj-dp.tcl
at91sam3u2c.cfg          cc32xx.cfg            kl46.cfg        psoc4.cfg            test_reset_syntax_error.cfg
at91sam3u2e.cfg          cs351x.cfg            klx.cfg         psoc5lp.cfg          test_syntax_error.cfg
at91sam3u4c.cfg          davinci.cfg           ks869x.cfg      pxa255.cfg           ti-ar7.cfg
at91sam3u4e.cfg          dragonite.cfg         kx.cfg          pxa270.cfg           ti-cjtag.cfg
at91sam3uxx.cfg          dsp56321.cfg          lpc11xx.cfg     pxa3xx.cfg           ti_calypso.cfg
at91sam4XXX.cfg          dsp568013.cfg         lpc12xx.cfg     quark_d20xx.cfg      ti_dm355.cfg
at91sam4c32x.cfg         dsp568037.cfg         lpc13xx.cfg     quark_x10xx.cfg      ti_dm365.cfg
at91sam4cXXX.cfg         efm32.cfg             lpc17xx.cfg     readme.txt           ti_dm6446.cfg
at91sam4lXX.cfg          efm32_stlink.cfg      lpc1850.cfg     renesas_s7g2.cfg     ti_msp432p4xx.cfg
at91sam4sXX.cfg          em357.cfg             lpc1xxx.cfg     samsung_s3c2410.cfg  ti_rm4x.cfg
at91sam4sd32x.cfg        em358.cfg             lpc2103.cfg     samsung_s3c2440.cfg  ti_tms570.cfg
at91sam7a2.cfg           epc9301.cfg           lpc2124.cfg     samsung_s3c2450.cfg  ti_tms570ls20xxx.cfg
at91sam7se512.cfg        exynos5250.cfg        lpc2129.cfg     samsung_s3c4510.cfg  ti_tms570ls3137.cfg
at91sam7sx.cfg           faux.cfg              lpc2148.cfg     samsung_s3c6410.cfg  tmpa900.cfg
at91sam7x256.cfg         feroceon.cfg          lpc2294.cfg     sharp_lh79532.cfg    tmpa910.cfg
at91sam7x512.cfg         fm3.cfg               lpc2378.cfg     sim3x.cfg            u8500.cfg
at91sam9.cfg             fm4.cfg               lpc2460.cfg     smp8634.cfg          vybrid_vf6xx.cfg
at91sam9260.cfg          fm4_mb9bf.cfg         lpc2478.cfg     spear3xx.cfg         xmc1xxx.cfg
at91sam9260ext_flash.cfg fm4_s6e2cc.cfg        lpc2900.cfg     stellaris.cfg        xmc4xxx.cfg
at91sam9261.cfg          gp326xxxa.cfg         lpc2xxx.cfg     stellaris_icdi.cfg   xmos_xs1-xau8a-10_arm.cfg
at91sam9263.cfg          hilscher_netx10.cfg   lpc3131.cfg     stm32_stlink.cfg     zynq_7000.cfg
at91sam9g10.cfg          hilscher_netx50.cfg   lpc3250.cfg     stm32f0x.cfg         к1879xб1я.cfg
at91sam9g20.cfg          hilscher_netx500.cfg  lpc40xx.cfg     stm32f0x_stlink.cfg

Можно предположить, что нам подойдут таргеты stm32f1x_stlink.cfg или/и stm32f1x.cfg.

Пробуем подключиться с помощью скрипта stm32f1x_stlink.cfg:

$ openocd -f interface/stlink-v2.cfg -f target/stm32f1x_stlink.cfg -c "init" -c "reset halt"

Open On-Chip Debugger 0.10.0
Licensed under GNU GPL v2
For bug reports, read
        http://openocd.org/doc/doxygen/bugs.html
WARNING: target/stm32f1x_stlink.cfg is deprecated, please switch to target/stm32f1x.cfg
Info : auto-selecting first available session transport "hla_swd". To override use 'transport select <transport>'.
Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD
adapter speed: 1000 kHz
adapter_nsrst_delay: 100
none separate
Info : Unable to match requested speed 1000 kHz, using 950 kHz
Info : Unable to match requested speed 1000 kHz, using 950 kHz
Info : clock speed 950 kHz
Info : STLINK v2 JTAG v27 API v2 SWIM v6 VID 0x0483 PID 0x3748
Info : using stlink api v2
Info : Target voltage: 3.254076
Info : stm32f1x.cpu: hardware has 6 breakpoints, 4 watchpoints
target halted due to debug-request, current mode: Thread 
xPSR: 0x01000000 pc: 0x08000b4c msp: 0x20005000

Если верить этому сообщению: "WARNING: target/stm32f1x_stlink.cfg is deprecated, please switch to target/stm32f1x.cfg", то мы должны использовать таргет stm32f1x.cfg. Пока проигнорируем это.

В другом окне запускаем отладчик командой:

$ arm-none-eabi-gdb ./blink.elf --tui

Порт OpenOCD по умолчанию - 3333. Подключаемся к OpenOCD:

(gdb) target remote localhost:3333
Remote debugging using localhost:3333
Reset_Handler () at asm/init.s:20

В окне OpenOCD видим отклик:

 Info : accepting 'gdb' connection on tcp/3333
Info : device id = 0x20036410
Info : flash size = 64kbytes

Открываем окно с ассемблерным листингом: "layout asm", окно с регистрами: layout regs", и видим что находимся в обработчике прерывания Reset:

Что делать дальше - думаю понятно. Добавлю, что вместо таргета stm32f1x_stlink.cfg можно использовать stm32f1x.cfg, я разницы не заметил. Предыдущая версия OpenOCD 0.8 c с этим тагретом работать отказывалась.

Напоминаю, что посмотреть исходники, сборочные файлы, скачать скомпилированные прошивки, можно с портала GITLAB https://gitlab.com/flank1er/stm32_bare_metal