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

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

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

Данная статья не рассчитана на новичков, я часто буду сравнивать STM32 c STM8, и по ходу повествования буду опускать многие элементарные, на мой взгляд, вещи. Статья предполагает, что вы уже знаете Cи, имеете опыт работы в консоли Linux или CYGWIN Windows. Также будет весьма кстати, если у вас уже есть опыт программирования в "Bare Metal" хотя бы на уровне микроконтроллеров STM8.

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

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

    Список используемой документации:
  1. Cortex-M3: Руководство программиста (PM0056), для чипов серий: STM32F10xxx/20xxx/21xxx/L1xxxx.
  2. Справочное руководство (Reference Manual: RM0008), для чипов следующих серий: STM32F101xx, STM32F102xx, STM32F103xx, STM32F105xx and STM32F107xx advanced Arm.
  3. Datasheet на чипы: STM32F103x8/STM32F103xB.
  4. STM32F10xxx I2C optimized examples, Application note AN2824
  5. Шпаргалка по набору инструкций 16-битных инструкций Thumb.
  6. Шпаргалка по набору инструкций Thumb2.
  7. ARM. Учебный Курс. SysTick — Системный таймер | Электроника для всех
  8. ARM Учебный курс. USART | Электроника для всех
  9. Статья на хабре: STM32: SPI: LCD — Вы всё делаете не так [восклицательный знак]
  10. SPI (перевод из книги Mastering STM32) – Radiotech
  11. Статья на хабре: Бюджетный отладчик к ESP-32 и его настройка
  12. Статья на хабре: FT232H, MPSSE и SPI-программатор за 15 евр
  13. Статья на хабре: FT232H и почти универсальный USB<->JTAG-адаптер за 15 евро

Содержание:

    I. Программирование и отладка STM32 в консоли
  1. Работа с GPIO на регистрах, без использования SPL или HAL
  2. Минималистичный Blink размером в 148 байт
  3. Добавляем к проекту таблицу векторов и Makefile
  4. Настройка системы тактирования - RCC (Reset and Clock Control)
  5. Функция задержки на ассемблерных инструкциях
  6. Функция задержки на прерывании таймера SysTick
  7. Настройка UART интерфейса в режиме передатчика
  8. Простой бенчмарк на операции деления
  9. Пишем простой планировщик задач (RTOS)
  10. Драйвер 4-x разрядного семисегментного индикатора (программный SPI)
  11. Настройка аппаратного интерфейса SPI для драйвера 4-х разрядного семисегментного индикатора
  12. Регистры I2C интерфейса, делаем сканер I2C шины
  13. Однобайтный режим чтения по шине I2C
  14. Двухбайтный режим чтения по шине I2C
  15. Запись массива через шину I2C
  16. Чтение массива через шину I2C
  17. Отладка в консоли с использованием OpenOCD
  18. Отладка с помощью JTAG адаптера на чипе FT232H

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

1 Работа с GPIO на регистрах, без использования SPL или HAL

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

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

Этот STM32 попахивал какой-то очередной Arduino, но даже там 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. Поэтому для побитового изменения состояния регистра 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);
    }

}

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

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

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

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

С учётом вышесказанного, минимальная программа Blink для stm32f103c8 у меня получилась такой:

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

asm(".word 0x20005000\n\t"
".word main+1\n\t"
".word fault_irq+1\n\t"
".word fault_irq+1\n\t"
".word fault_irq+1\n\t"
".word fault_irq+1\n\t"
".word fault_irq+1\n\t");

void  dummy_loop(uint32_t count){

  while(--count);
}

void fault_irq() {
    while(1);
}

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);
    }

}

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

Т.к. в программе используются имена регистров и константы из CMSIS и SPL, то потребуются заголовочные файлы этих библиотек. Я их брал из шаблонного проекта SW4STM32, но SPL для своей модели микроконтроллера, конечно же можно скачать с сайта st.com. Например для STMF103C8 отсюда: STSW-STM32054 - STM32F10x standard peripheral library - STMicroelectronics. Структура проекта пока выглядит так:

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

5 directories, 11 files

Для сборки также понадобится скрипт компоновщика: "script.ld", я его взял также из шаблонного проекта SW4STM32 и несколько сократил под свои нужды:

/* 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

}

Компилируем:

$ 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   -Tscript.ld  main.o  -o main.elf

Проверяем:

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

./main.elf:     file format elf32-littlearm


Disassembly of section .text:

08000000 <dummy_loop-0x1c>:
 8000000:   20005000    .word   0x20005000
 8000004:   08000041    .word   0x08000041
 8000008:   0800003b    .word   0x0800003b
 800000c:   0800003b    .word   0x0800003b
 8000010:   0800003b    .word   0x0800003b
 8000014:   0800003b    .word   0x0800003b
 8000018:   0800003b    .word   0x0800003b

0800001c <dummy_loop>:
".word fault_irq+1\n\t"
".word fault_irq+1\n\t"
".word fault_irq+1\n\t"
".word fault_irq+1\n\t");

void  dummy_loop(uint32_t count){
 800001c:   b480        push    {r7}
 800001e:   b083        sub sp, #12
 8000020:   af00        add r7, sp, #0
 8000022:   6078        str r0, [r7, #4]

  while(--count);
 8000024:   687b        ldr r3, [r7, #4]
 8000026:   3b01        subs    r3, #1
 8000028:   607b        str r3, [r7, #4]
 800002a:   687b        ldr r3, [r7, #4]
 800002c:   2b00        cmp r3, #0
 800002e:   d1f9        bne.n   8000024 <dummy_loop+0x8>
}
 8000030:   bf00        nop
 8000032:   370c        adds    r7, #12
 8000034:   46bd        mov sp, r7
 8000036:   bc80        pop {r7}
 8000038:   4770        bx  lr

0800003a <fault_irq>:

void fault_irq() {
 800003a:   b480        push    {r7}
 800003c:   af00        add r7, sp, #0
    while(1);
 800003e:   e7fe        b.n 800003e <fault_irq+0x4>

08000040 <main>:
}

int main()
{
 8000040:   b580        push    {r7, lr}
 8000042:   af00        add r7, sp, #0
    // enable GPIOC port
    RCC->APB2ENR |= RCC_APB2Periph_GPIOC;
 8000044:   4b10        ldr r3, [pc, #64]   ; (8000088 <main+0x48>)
 8000046:   699b        ldr r3, [r3, #24]
 8000048:   4a0f        ldr r2, [pc, #60]   ; (8000088 <main+0x48>)
 800004a:   f043 0310   orr.w   r3, r3, #16
 800004e:   6193        str r3, [r2, #24]
    // --- GPIO setup ----
    GPIOC->CRH &= ~(uint32_t)(0xf<<20);
 8000050:   4b0e        ldr r3, [pc, #56]   ; (800008c <main+0x4c>)
 8000052:   685b        ldr r3, [r3, #4]
 8000054:   4a0d        ldr r2, [pc, #52]   ; (800008c <main+0x4c>)
 8000056:   f423 0370   bic.w   r3, r3, #15728640   ; 0xf00000
 800005a:   6053        str r3, [r2, #4]
    GPIOC->CRH |=  (uint32_t)(0x2<<20);
 800005c:   4b0b        ldr r3, [pc, #44]   ; (800008c <main+0x4c>)
 800005e:   685b        ldr r3, [r3, #4]
 8000060:   4a0a        ldr r2, [pc, #40]   ; (800008c <main+0x4c>)
 8000062:   f443 1300   orr.w   r3, r3, #2097152    ; 0x200000
 8000066:   6053        str r3, [r2, #4]

    for(;;){
        GPIOC->BSRR=GPIO_Pin_13;
 8000068:   4b08        ldr r3, [pc, #32]   ; (800008c <main+0x4c>)
 800006a:   f44f 5200   mov.w   r2, #8192   ; 0x2000
 800006e:   611a        str r2, [r3, #16]
        dummy_loop(600000);
 8000070:   4807        ldr r0, [pc, #28]   ; (8000090 <main+0x50>)
 8000072:   f7ff ffd3   bl  800001c <dummy_loop>
        GPIOC->BRR=GPIO_Pin_13;
 8000076:   4b05        ldr r3, [pc, #20]   ; (800008c <main+0x4c>)
 8000078:   f44f 5200   mov.w   r2, #8192   ; 0x2000
 800007c:   615a        str r2, [r3, #20]
        dummy_loop(600000);
 800007e:   4804        ldr r0, [pc, #16]   ; (8000090 <main+0x50>)
 8000080:   f7ff ffcc   bl  800001c <dummy_loop>
        GPIOC->BSRR=GPIO_Pin_13;
 8000084:   e7f0        b.n 8000068 <main+0x28>
 8000086:   bf00        nop
 8000088:   40021000    .word   0x40021000
 800008c:   40011000    .word   0x40011000
 8000090:   000927c0    .word   0x000927c0

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

В ассемблере ARM/Cortex-M3 есть много подкупающих вещей, которые заставят вас влюбиться в архитектуру STM32. Например операции знакового деления, 32-битного умножения, операции с насыщением, операции параллельной обработки данных SIMD (Single Instruction Multiple Data), операции на битовыми полями, и т.д. Если раньше вам не хватало именно этих операций, то ARM - это ваша архитектура.

В начале по адресу 0x08000000 записано значение указателя стека, далее идут нечётные указатели на обработчики прерываний. При этом обычные команды ветвления или вызова подпрограммы используют обычные чётные адреса. Это видно по инструкции вызова функции задержки:

   bl  800001c <dummy_loop>

Правда тут есть немного лукавства. Все переходы в Cortex-M3 относительные. То, что дизассемблер показал конкретный адрес, не означает, что конкретно этот адрес записан в операнде. Если сама инструкция начинается с НЕчетного адреса, то смещение до другой НЕчетной инструкции будет четным. Из этого следует, что к меткам в программе единицу прибавлять не следует, они и так уже выровнены по нечетным значениям. И учтите, что в счётчике команд PC/R15 при отладке, вы никогда не увидите нечетный адрес, т.к. младший бит PC/R15 аппаратно сброшен в ноль. Такая вот у этой архитектуры фича.

Т.к. счетчик команд PC/R15 всегда выровнен по границе слова или полуслова, вследствие этого, все инструкции в Cortex-M3 занимают либо два (чаще всего), либо четыре байта (инструкции Thumb2). Для того, что бы занести в регистр 32-битную константу имеется два способа. Первый способ заключается в использовании инструкции mov.w дважды, когда начала заносится младшее полуслово, затем старшее. Или наоборот. Второй способ заключается в использовании псевдо инструкции LDR, когда число записывается константой со смещением относительно регистра PC. Инструкция LDR загружает эту константу в регистр используя индексную адресацию. Оба способа занимают 8 байт на флеш-памяти.

В Cortex-M3 используется 3-х уровневый конвейер. Имеется две шины работающие с флеш-памятью: шина для выборки инструкций i-code и шина для выборки данных d-code. Каждая шина свой имеет 64-битный буфер. Т.о. Cortex-M3 имеет буфер превыборки 2х64 бит. Учитывая, что средняя инструкция занимает 16-бит, это должно нивелировать тот факт, что флеш-память работает на меньшей частоте чем ядро.

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

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

В ARM нет понятия сегментов, как и сегментных регистров. Инструкции могут обращаться ко всему адресному пространству через индексную или косвенную адресацию. Зато в ARM есть понятие региона. Например: регион флеш-памяти, регион bit-banging и т.д.

В Cortex-M3 есть еще такое понятие, как маппинг региона, этакое переназначение адресов. Например в зависимости от конфигурации BOOT-пинов на адрес 0х00000000 маппится или регион ОЗУ с адресом 0х20000000 или флеш-памяти с адресом 0х08000000.

Инструкций INC, DEC в Cortex-M3 так же нет, зато есть режим индексной адресации с автоинкрементом и автодекриментом (этакий реверанс в сторону архитектуры PDP-11). Инструкции sub reg,#1 и add reg,#1 могут служить заменой dec и inc. Они занимают всего два байта вместе с операндом. Если операнд больше одного байта, то такая инструкция будет занимать уже 4 байта.

Указатель стека R13/SP выровнен по границе слова, т.е. его младшие два бита аппаратно сброшены в ноль. Указатель стека всегда указывает на младший байт последнего положенного в стек слова. В микроконтроллере STM32F103C8 20КБ ОЗУ или 0х5000. При инициализации стека мы записываем в SP значение 0х20005000, зная что по этому адресу уже нет ОЗУ. При помещении в стек нового значения, из указателя стека сначала вычитается четыре, и только затем в адреса: sp, sp+1, sp+2, sp+3 заносится слово. Т.е. максимальная используемая ячейка ОЗУ равна 0x20004fff, в ячейку 0х20005000 ничего не заносится.

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

        push {r0, r5-r7}
 8000202:       b4e1            push    {r0, r5, r6, r7}

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

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

С помощью добавления или убирания суффикса "S", действие инструкции может изменять содержимое регистра состояния или нет.

В качестве примера ассемблера Cortex-M3, можно посмотреть как раскладывается строка на Си:
GPIOC->BSRR=GPIO_Pin_13;

    ldr r3, [pc, #32]
    mov.w   r2, #8192
    str r2, [r3, #16]

    .word   0x40011000

Первая инструкция LDR с помощью индексной адресации загружает в регистр R3 константу 0x40011000. Данная константа является базой для GPIO блока периферийных регистров. Следующая инструкция MOV.W из набора инструкций Thumb2 является 32-битной. Она загружает в R2 нужный номер пина 0x2000. Инструкция STR переносит этот номер в BSRR регистр, вычисляя его значение как сумму базы 0x40011000 и числа 16 с помощью, опять же, индексной адресации.

Возвращаемся к нашей программе. Прошиваем микроконтроллер:

$ st-flash write ./main.bin  0x08000000 
st-flash 1.4.0-50-g7fafee2
2018-09-28T18:16:01 INFO common.c: Loading device parameters....
2018-09-28T18:16:01 INFO common.c: Device connected is: F1 Medium-density device, id 0x20036410
2018-09-28T18:16:01 INFO common.c: SRAM size: 0x5000 bytes (20 KiB), Flash: 0x10000 bytes (64 KiB) in pages of 1024 bytes
2018-09-28T18:16:01 INFO common.c: Attempting to write 148 (0x94) bytes to stm32 address: 134217728 (0x8000000)
Flash page at addr: 0x08000000 erased
2018-09-28T18:16:01 INFO common.c: Finished erasing 1 pages of 1024 (0x400) bytes
2018-09-28T18:16:01 INFO common.c: Starting Flash write for VL/F0/F3/F1_XL core id
2018-09-28T18:16:01 INFO flash_loader.c: Successfully loaded flash loader in sram
  1/1 pages written
2018-09-28T18:16:01 INFO common.c: Starting verification of write complete
2018-09-28T18:16:01 INFO common.c: Flash written and verified! jolly good!

Если все было сделано правильно, то светодиод начнет мигать в несколько раз медленнее по сравнению с Bkink'ом в ТrueStudio. Это из-за того, что у нас не настроена система тактирования и микроконтроллер работает сейчас от встроенного HSI генератора с частотой 8 МГц. Однако, получить минимальную прошивку в полторы сотни байт на STM32 вполне реально.

К сожалению, у такого минимализма есть своя цена. Кроме того, что микроконтроллер работает на 10% от своего максимального быстродействия, мы лишаемся функций стандартной библиотеки Си: printf(), scanf(), malloc(), функций работы со строками, математических функций, поддержки чисел с плавающей точкой и т.д. Вполне возможно, что в каком-нибудь простом проекте для Cortex-M0, это все действительно будет лишним.

3 Добавляем к проекту таблицу векторов и Makefile

Следующим логическим шагом будет вынесение ассемблерной части программы в отдельный ассемблерный файл и добавление полной таблицы векторов. Эта таблица займет у нас какое-то место на флеше.

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

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

заменим на:

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

Файл с Си программой примет вид:

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

void  dummy_loop(uint32_t count){

  while(--count);
}

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);
    }

}

Таблица прерываний STM32F103 довольно обширная:

Всего 70 прерываний, плюс шесть зарезервированных. Т.е. размер таблицы будет 76 * 4 = 304 байта. Готовую таблицу векторов я попросту скопировал из шаблонного проекта SW4STM32, в итоге ассемблерный файл получился настолько внушительных размеров, что я его спрятал под спойлер:

.syntax unified
.cpu cortex-m3
.thumb

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

    .section .text.Default_Handler,"ax",%progbits
Default_Handler:
Infinite_Loop:
  b Infinite_Loop
  .size Default_Handler, .-Default_Handler

  .section .vectors
  .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

Здесь два обработчика прерывания: Reset и Default_Handler, а ко всем адресам автоматически прибавляется единица.

Осталось добавить файл сборки проекта: Makefile. Пусть к примеру он будет таким:

MCU=cortex-m3
OBJCOPY=arm-none-eabi-objcopy
CC=arm-none-eabi-gcc
LD=arm-none-eabi-ld
SIZE=arm-none-eabi-size
INC  = -ICMSIS/device
INC += -ICMSIS/core
INC += -ISPL/inc
DEF = -DSTM32F10X_MD
CFLAGS=-mthumb -mcpu=$(MCU) -g -O0 -Wall $(DEF) $(INC)
ASFLAGS=-mthumb -mcpu=$(MCU) -g -Wall
LDFLAGS=-Tscript.ld
OBJ=main.o  init.o
TARGET=blink
.PHONY: all clean

%.o:	%.c
	$(CC) -c -o $@ $< $(CFLAGS)
%.o:	asm/%.s
	$(CC) -c -o $@ $< $(ASFLAGS)
all:	$(OBJ)
	$(LD) $(LDFLAGS) -g  -o $(TARGET).elf  $(OBJ)
	$(OBJCOPY) -O binary $(TARGET).elf $(TARGET).bin
	$(SIZE)  $(TARGET).elf
install:
	st-flash  write $(TARGET).bin 0x08000000
clean:
	@rm -v $(TARGET).elf $(TARGET).bin $(OBJ)

Пытаемся скомпилировать:

$ make all
arm-none-eabi-gcc -c -o main.o main.c -mthumb -mcpu=cortex-m3 -g -O0 -Wall -DSTM32F10X_MD -ICMSIS/device -ICMSIS/core -ISPL/inc
arm-none-eabi-gcc -c -o init.o asm/init.s -mthumb -mcpu=cortex-m3 -g -Wall
arm-none-eabi-ld -Tscript.ld -g  -o blink.elf  main.o  init.o
arm-none-eabi-objcopy -O binary blink.elf blink.bin
arm-none-eabi-size  blink.elf
   text    data     bss     dec     hex filename
    430       0       2     432     1b0 blink.elf

Чтож... С таблицей прерываний размер проекта приближается к полукилобайту. Можно конечно удалить неиспользуемые, если не хватает места, но я так делать не буду.

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

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

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

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

Настройка системы тактирования начинается с вызова функции SystemInit(). Создадим в каталоге проекта новый подкаталог: "src" в котором откроем новый файл startup.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. В завершении работы, в зависимости от флагов компиляции, включается вызов функций для настройки тактирования.

Мне показалась интересной функция SetSysClockToHSE() - которая осуществляет переключение на внешний кварц.

#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 */
  }
}

Её я комментировать не буду, т.к. мы будем использовать другую функцию: 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
}

Теперь вызов функции SystemInit() следует добавить в обработчик прерывания Reset:

    .section .text.Reset_Handler
    .weak Reset_Handler
    .type Reset_Handler, %function
Reset_Handler:
    ldr   r0, =_estack
    mov   sp, r0            /* set stack pointer */
    cpsid i                 @ disable irq
    ldr   r0, =SystemInit
    blx   r0
    b    main

Осталось обновить Makefile:

MCU=cortex-m3
OBJCOPY=arm-none-eabi-objcopy
CC=arm-none-eabi-gcc
LD=arm-none-eabi-ld
SIZE=arm-none-eabi-size
INC  = -ICMSIS/device
INC += -ICMSIS/core
INC += -ISPL/inc
DEF = -DSTM32F10X_MD
DEF +=-DSYSCLK_FREQ_72MHz
CFLAGS=-mthumb -mcpu=$(MCU) -g -O0  $(DEF) $(INC)
ASFLAGS=-mthumb -mcpu=$(MCU) -g 
LDFLAGS=-Tscript.ld
OBJ=main.o  init.o startup.o
TARGET=blink
.PHONY: all clean

%.o:	%.c
	$(CC) -c -o $@ $< $(CFLAGS)
%.o:	src/%.c
	$(CC) -c -o $@ $< $(CFLAGS)
%.o:	asm/%.s
	$(CC) -c -o $@ $< $(ASFLAGS)
all:	$(OBJ)
	$(LD) $(LDFLAGS) -g  -o $(TARGET).elf  $(OBJ)
	$(OBJCOPY) -O binary $(TARGET).elf $(TARGET).bin
	$(SIZE)  $(TARGET).elf
install:
	st-flash  write $(TARGET).bin 0x08000000
clean:
	@rm -v $(TARGET).elf $(TARGET).bin $(OBJ)

После сборки проекта, вес прошивки увеличивается до ~700 байт.

Если все было сделано правильно, то после прошивки светодиод должен начать мигать в девять раз быстрее, что означает, что микроконтроллер работает на частоте 72 МНz.

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

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

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

Итак, для реализации функции задержки добавим в ассемблерный файл asm/init.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. Это конечно не объективная оценка. Объективную оценку мы получим когда запустим RTOS и посмотрим загруженность системы.

6 Функция задержки на прерывании таймера SysTick

Задержка на инструкциях все-таки специфическая вещь, счетчик цикла итераций на самом деле это не бенчмарк никакой. Для более точного определения производительности микроконтроллера нам понадобится функция задержки на системном таймере SysTick

Про регистры таймера SysTick как и про него самого можно почитать здесь: ARM. Учебный Курс. SysTick — Системный таймер | Электроника для всех

Реализация функция задержки на таймере SysTick у меня полностью поместилось в файле main.c:

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

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

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) {
        asm("wfi");
    };

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

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

}

Реализация используемой функции SysTick_Config() находится в CMSIS, так что никаких дополнительных файлов и библиотек подключать не надо. После сборки прошивка весит уже 990 байт. Таблицу прерываний в asm/init.s трогать не нужно. После компиляции удостоверьтесь, что адрес обработчика прерывания SysTick указывает именно на функцию SysTick_Handler().

7 Настройка UART интерфейса в режиме передатчика

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

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

Настройку USART1 интерфейса STM32 в режиме передатчика я поместил в файл main.c:

#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;

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) {
        asm("wfi");
    };

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

int main()
{
    // enable GPIOC & GPIOA 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);             // enable PushPull mode
    GPIOA->CRH &= ~(uint32_t)(0xf<<4);              // enable Alterentive mode
    GPIOA->CRH |=  (uint32_t)(0xa<<4);              // for PA9 = USART1_TX
    // --- 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();
    uint32_t i=0;
    for(;;){
        GPIOC->ODR ^= GPIO_Pin_13;
        delay_ms(1000);
        usart1_print_string("count: ");
        usart1_print_number(i++);
        usart1_send_char('\n');
    }

}

Нам понадобится заголовочный файл из 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 */

void usart1_send_char(uint32_t ch);
void usart1_print_string(char *str);
void usart1_print_number(uint32_t num);

#endif

Модуль uart.c практически не отличается от своего аналога для STM8:

#include "uart.h"
#define len 8

void usart1_print_number(uint32_t num){
    uint8_t n[len];
    char *s=n+(len-1);
    *s=0;           // EOL
    do {
        *(--s)=(uint32_t)(num%10 + 0x30);
        num=num/10;
    } while (num>0);

    usart1_print_string(s);
}

void usart1_send_char(uint32_t ch) {

    USART1->DR=ch;
    while(!(USART1->SR & USART_FLAG_TXE));
}

void usart1_print_string(char *str) {
    while (*str)
    {
        usart1_send_char((uint32_t)*str++);
    }
}

В связи с добавлением новых файлов: "uart.c", "uart.h" и директории "inc" в проект, Makefile также немного поменялся:

MCU=cortex-m3
OBJCOPY=arm-none-eabi-objcopy
CC=arm-none-eabi-gcc
LD=arm-none-eabi-ld
SIZE=arm-none-eabi-size
INC  = -ICMSIS/device
INC += -ICMSIS/core
INC += -ISPL/inc
INC += -Iinc
DEF = -DSTM32F10X_MD
DEF +=-DSYSCLK_FREQ_72MHz
CFLAGS=-mthumb -mcpu=$(MCU) -g -O0  $(DEF) $(INC)
ASFLAGS=-mthumb -mcpu=$(MCU) -g 
LDFLAGS=-Tscript.ld
OBJ=main.o  init.o startup.o uart.o
TARGET=blink
.PHONY: all clean

%.o:	%.c
	$(CC) -c -o $@ $< $(CFLAGS)
%.o:	src/%.c
	$(CC) -c -o $@ $< $(CFLAGS)
%.o:	asm/%.s
	$(CC) -c -o $@ $< $(ASFLAGS)
all:	$(OBJ)
	$(LD) $(LDFLAGS) -g  -o $(TARGET).elf  $(OBJ)
	$(OBJCOPY) -O binary $(TARGET).elf $(TARGET).bin
	$(SIZE)  $(TARGET).elf
install:
	st-flash  write $(TARGET).bin 0x08000000
clean:
	@rm -v $(TARGET).elf $(TARGET).bin $(OBJ)

Общая структура проекта теперь выглядит так:

$ tree .
.
├── CMSIS
│   ├── core
│   │   ├── core_cm3.c
│   │   └── core_cm3.h
│   └── device
│       ├── stm32f10x.h
│       └── system_stm32f10x.h
├── Makefile
├── SPL
│   └── inc
│       ├── stm32f10x_gpio.h
│       ├── stm32f10x_rcc.h
│       └── stm32f10x_usart.h
├── asm
│   └── init.s
├── blink.bin
├── blink.elf
├── inc
│   └── uart.h
├── init.o
├── main.c
├── main.o
├── script.ld
├── src
│   ├── startup.c
│   └── uart.c
├── startup.o
└── uart.o

8 directories, 20 files

После компиляции прошивка теперь "весит" 1298 байт:

$ make all
arm-none-eabi-gcc -c -o main.o main.c -mthumb -mcpu=cortex-m3 -g -O0  -DSTM32F10X_MD -DSYSCLK_FREQ_72MHz -ICMSIS/device -ICMSIS/core -ISPL/inc -Iinc
arm-none-eabi-gcc -c -o init.o asm/init.s -mthumb -mcpu=cortex-m3 -g 
arm-none-eabi-gcc -c -o startup.o src/startup.c -mthumb -mcpu=cortex-m3 -g -O0  -DSTM32F10X_MD -DSYSCLK_FREQ_72MHz -ICMSIS/device -ICMSIS/core -ISPL/inc -Iinc
arm-none-eabi-gcc -c -o uart.o src/uart.c -mthumb -mcpu=cortex-m3 -g -O0  -DSTM32F10X_MD -DSYSCLK_FREQ_72MHz -ICMSIS/device -ICMSIS/core -ISPL/inc -Iinc
arm-none-eabi-ld -Tscript.ld -g  -o blink.elf  main.o  init.o startup.o uart.o
arm-none-eabi-objcopy -O binary blink.elf blink.bin
arm-none-eabi-size  blink.elf
   text    data     bss     dec     hex filename
   1298       0       4    1302     516 blink.elf

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

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

Когда я тестировал свои 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 с этим тагретом работать отказывалась.

18 Отладка с помощью JTAG адаптера на чипе FT232H


FT232H Board

JTAG - интерфейс для тестирования печатных плат. Процесс тестирования называется - граничным сканированием (boundary scan). Данная технология позволяет определять непропай/замыкание дорожек или контактов без физического доступа к этим самым контактам. Это классная штука когда вам нужно протестировать BGA чип или чип в безвыводном корпусе, или вы запускаете микро/мелко/гига -серийное производство и вам нужно выявлять брак прямо на конвейере. На youtube есть ролики с процессом тестирования, посмотрите, не ленитесь. Нас же пока будет интересовать "побочная" сторона JTAG - возможность прошивки и отладки микроконтроллера.

Для этого я буду использовать плату с чипом FT232H. Этот чип - является преобразователем интерфейсов USB в UART, JTAG, SPI, I2C, bit-bang и т.е. Т.е. это такой универсальный "швейцарский нож", который будет вам UART преобразователем, JTAG - флешером, отладчиком, программатором SPI флешек и т.к. В какой-то мере его можно сравнить с микроконтроллером, но вся его управляющая программа находится на стороне компьютера. Нас будет интересовать возможность чипа работать в качестве JTAG - адаптера. В отличии от ST-LINK c протоколом SWD, JTAG'ом можно "подцепиться" к очень широкому спектру устройств. Собственно об плате я узнал из статьи: Бюджетный отладчик к ESP-32 и его настройка где речь шла об отладке ESP32. Мы же будем тренироваться на кошках на STM32.

Почитать об FT232H можно на хабре: FT232H, MPSSE и SPI-программатор за 15 евр, или здесь: FT232H и почти универсальный USB<->JTAG-адаптер за 15 евро

Большинство аппаратных отладчиков сделаны на чипе FT2232H, он имеет два канала, т.е. чип может служить и JTAG-адаптером и UART-конвертером, но для наших целей сгодится и одноканальный FT232H.

Я лично плату покупал на али за 500р с копейками, но хочу обратить внимание, что Adafruit выпускает аналогичную плату: Adafruit FT232H Breakout - General Purpose USB to GPIO+SPI+I2C . У них есть любопытный туториал по работе с платой на Python. Платы полностью совместимы друг с другом, так что не стесняйтесь, и выполните хотя бы несколько упражнений для знакомства с железкой.

В Linux, для работы с чипом нам понадобится библиотека libftdi1. В комплекте OpenOCD имеется файл "/usr/share/openocd/contrib/60-openocd.rules" c правилами для udev, в котором имеется правило и для FT232H:

# Original FT232H VID:PID
ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6014", MODE="660", GROUP="plugdev", TAG+="uaccess"

Для доступа к железке из под непривилегированного пользователя, этот файл следует скопировать в /lib/udev/rules.d после чего нужно будет перезагрузить правила командой:

# udevadm control --reload-rules && udevadm trigger

Плата имеет следующую распиновку:

В руководстве на чип ищем JTAG пины: TMS, TCK, TDO, TDI:

Согласно руководству на чип stm32f103c8, JTAG пины TMS и TCL разделяют те же пины, что и SWDIO и SWCLK, куда мы подключали ST-LINK. JTDI это PA15, а JTDO это PB3.

Т.о. подключение FT232H к STM32F103C8 будет следующим: 3.3V к VCC, GND к GND, AD0 к SWCLK, AD3 к SWDIO, AD1 к PA15, AD2 к PB3.

Подключаем плату с FT232H к компьютеру, и видим такой лог:

[363652.166692] usb 1-2.2: new high-speed USB device number 81 using ehci-pci
[363652.252481] usb 1-2.2: New USB device found, idVendor=0403, idProduct=6014
[363652.252492] usb 1-2.2: New USB device strings: Mfr=1, Product=2, SerialNumber=0
[363652.252500] usb 1-2.2: Product: Single RS232-HS
[363652.252506] usb 1-2.2: Manufacturer: FTDI
[363652.253036] ftdi_sio 1-2.2:1.0: FTDI USB Serial Device converter detected
[363652.253110] usb 1-2.2: Detected FT232H
[363652.253474] usb 1-2.2: FTDI USB Serial Device converter now attached to ttyUSB0

Создаем файл с описанием интерфейса - interface.cfg, следующего содержания:

interface ftdi
ftdi_vid_pid 0x0403 0x6014
ftdi_layout_init 0x0c08 0x0f1b

Подключаемся:

$ openocd -f ./interface.cfg  -f target/stm32f1x.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
Info : auto-selecting first available session transport "jtag". To override use 'transport select <transport>'.
adapter speed: 1000 kHz
adapter_nsrst_delay: 100
jtag_ntrst_delay: 100
none separate
cortex_m reset_config sysresetreq
Info : clock speed 1000 kHz
Info : JTAG tap: stm32f1x.cpu tap/device found: 0x3ba00477 (mfg: 0x23b (ARM Ltd.), part: 0xba00, ver: 0x3)
Info : JTAG tap: stm32f1x.bs tap/device found: 0x16410041 (mfg: 0x020 (STMicroelectronics), part: 0x6410, ver: 0x1)
Info : stm32f1x.cpu: hardware has 6 breakpoints, 4 watchpoints
Info : JTAG tap: stm32f1x.cpu tap/device found: 0x3ba00477 (mfg: 0x23b (ARM Ltd.), part: 0xba00, ver: 0x3)
Info : JTAG tap: stm32f1x.bs tap/device found: 0x16410041 (mfg: 0x020 (STMicroelectronics), part: 0x6410, ver: 0x1)
target halted due to debug-request, current mode: Thread 
xPSR: 0x01000000 pc: 0x08000b4c msp: 0x20005000

Из лога видно, что было намдено два целевых TAP-контроллера. Далее все действия аналогичны отладке через ST-LINK.

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

поделиться: