Внимание! На данный момент(май-июнь 2025г.) статья находится в стадии редактирования. Первоначальную версию статьи можно открыть по следующей ссылке.
Когда я впервые начал знакомиться с микроконтроллерами семейства STM32, то обратил внимание на то, что прошивки даже с самыми простыми алгоритмами (например Blink), по меркам 8-битных микроконтроллеров, имеют огромный размер: от одного килобайта и больше. Поэтому целью этой статьи стала попытка написания прошивок для STM32 в стиле 8-битных микроконтроллеров, когда вы полностью контролируете процесс компиляции, используя лишь: компилятор, флешер и текстовый редактор. Соответственно в статье рассматриваются типовые на мой взгляд вопросы при переходе с 8-битников на 32-разрядную архитектуру: как помигать светодиодом, как настроить тактирование, как завести SPI и поднять I2C.
Если вам чего-то из этого не хватает, то вы легко сможете подтянуть "матчасть" по статьям на хабре: STM32F4: GNU AS: Программирование на ассемблере в семи частях, по методичке "Народная электроника" выпуск 2. А.В. Немоляев. GCC Cortex-M3. PDF, или по книге "Джозеф Ю. Ядро Cortex - МЗ компании ARM. Полное руководство", Козаченко В.Ф.(ред.): "Практический курс микропроцессорной техники на базе процессорных ядер ARM-Cortex-M3/M4/M4F" 2019 М.: МЭИ
Оборудование. В статье я буду использовать популярную плату "Blue Pill" на микроконтроллере STM32F103C8T6, программатор ST-LINK v2 (китайская реплика), USB-UART преобразователь FT232RL, 4-x разрядный семи-сегментный индикатор, на SPI интерфейсе и RTC DS3231 на I2C интерфейсе.
Содержание:
При написании примеров я использовал кросс-компилятор arm-none-eabi-gcc версии 10.3 скачанный с сайта ARM. Данная версия сейчас (май 2025г.) объявлена устаревшей (deprecated), однако у меня она работала стабильно, потому приведу альтернативные ссылки для скачивания: версия 10.3 тарбол для linux x86_64, zip-архив для win32. Актуальная версия компилятора - 14.2 тарбол для linux x86_64, и zip-архив для win64.
Посмотреть исходники, сборочные файлы, скачать скомпилированные прошивки, можно с портала GITLAB https://gitlab.com/flank1er/stm32_bare_metal
В настоящее время (2025 год), доступно большое число микроконтролеров с архитектурой Cortex-M3. Это STM32F1xx, их реплики, клоны, и различные "мутанты" с непонятными характеристиками. Данные изделия предлагаются производителями со своими библиотеками и средами разработки. В текущей ситуации нужно уметь создать проект с нуля, имея на руках только даташит. Нужно уметь интегрировать этот проект в современную среду разработки, дабы не прыгать из одной программы в другую. Так или иначе, STM32F103xx сейчас является образцом, который копируют другие производители, и который следует знать.
Началось все с того, что меня несколько обескуражил размер прошивки минимального проекта в TrueStudio (после покупки компанией ST была переименованна в STM32CubeIDE) - 1572 байт:
В SW4STM32 получалась какая-то такая же цифра, при этом у меня в настройках проекта была выставлена опция: --gc-section, которая даёт команду компоновщику удалять неиспользуемый код:
Даже в Arduino-STM32 Blink "весил" в пределах одного килобайта.
Сначала я подумал, что все дело в используемой SPL и если переписать код на регистрах, то ситуация исправится. Давайте посмотрим так ли это.
Первая вызываемая функция имеет такой вид:
void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState) { /* Check the parameters */ assert_param(IS_RCC_APB2_PERIPH(RCC_APB2Periph)); assert_param(IS_FUNCTIONAL_STATE(NewState)); if (NewState != DISABLE) { RCC->APB2ENR |= RCC_APB2Periph; } else { RCC->APB2ENR &= ~RCC_APB2Periph; } }
т.е. ее можно смело заменить строкой вида:
RCC->APB2ENR |= RCC_APB2Periph_GPIOC;
Вызов функции инициализации порта GPIO оборачивается вызовом такой штуки:
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct) { uint32_t currentmode = 0x00, currentpin = 0x00, pinpos = 0x00, pos = 0x00; uint32_t tmpreg = 0x00, pinmask = 0x00; /* Check the parameters */ assert_param(IS_GPIO_ALL_PERIPH(GPIOx)); assert_param(IS_GPIO_MODE(GPIO_InitStruct->GPIO_Mode)); assert_param(IS_GPIO_PIN(GPIO_InitStruct->GPIO_Pin)); /*---------------------------- GPIO Mode Configuration -----------------------*/ currentmode = ((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x0F); if ((((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x10)) != 0x00) { /* Check the parameters */ assert_param(IS_GPIO_SPEED(GPIO_InitStruct->GPIO_Speed)); /* Output mode */ currentmode |= (uint32_t)GPIO_InitStruct->GPIO_Speed; } /*---------------------------- GPIO CRL Configuration ------------------------*/ /* Configure the eight low port pins */ if (((uint32_t)GPIO_InitStruct->GPIO_Pin & ((uint32_t)0x00FF)) != 0x00) { tmpreg = GPIOx->CRL; for (pinpos = 0x00; pinpos < 0x08; pinpos++) { pos = ((uint32_t)0x01) << pinpos; /* Get the port pins position */ currentpin = (GPIO_InitStruct->GPIO_Pin) & pos; if (currentpin == pos) { pos = pinpos << 2; /* Clear the corresponding low control register bits */ pinmask = ((uint32_t)0x0F) << pos; tmpreg &= ~pinmask; /* Write the mode configuration in the corresponding bits */ tmpreg |= (currentmode << pos); /* Reset the corresponding ODR bit */ if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD) { GPIOx->BRR = (((uint32_t)0x01) << pinpos); } else { /* Set the corresponding ODR bit */ if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU) { GPIOx->BSRR = (((uint32_t)0x01) << pinpos); } } } } GPIOx->CRL = tmpreg; } /*---------------------------- GPIO CRH Configuration ------------------------*/ /* Configure the eight high port pins */ if (GPIO_InitStruct->GPIO_Pin > 0x00FF) { tmpreg = GPIOx->CRH; for (pinpos = 0x00; pinpos < 0x08; pinpos++) { pos = (((uint32_t)0x01) << (pinpos + 0x08)); /* Get the port pins position */ currentpin = ((GPIO_InitStruct->GPIO_Pin) & pos); if (currentpin == pos) { pos = pinpos << 2; /* Clear the corresponding high control register bits */ pinmask = ((uint32_t)0x0F) << pos; tmpreg &= ~pinmask; /* Write the mode configuration in the corresponding bits */ tmpreg |= (currentmode << pos); /* Reset the corresponding ODR bit */ if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD) { GPIOx->BRR = (((uint32_t)0x01) << (pinpos + 0x08)); } /* Set the corresponding ODR bit */ if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU) { GPIOx->BSRR = (((uint32_t)0x01) << (pinpos + 0x08)); } } } GPIOx->CRH = tmpreg; } }
По большому счету, эта функция устанавливает значение регистров: GPIOx->CRL или/и GPIOx->CRH. Вспоминаем, что это за регистры:
Регистр GPIOx->CRL конфигурирует режим пинов от 0 до 7. Регистр GPIOx->CRH конфигурирует режим пинов от 8 до 15. Т.к. светодиод на Blue Pill подключён к PC13, то нам нужен регистр GPIOС->CRH. Чтобы его сконфигурировать. В поле MOD13[1:0] можно задать максимальную частоту переключения нужного пина в Output режиме. Полагаю 2МГц будет вполне достаточно, значит записываем в него значение 2. По умолчанию, в режиме выхода поле CNF13[1:0] конфигурирует пин в Push-Pull режим, что нас вполне устраивает, следовательно оставляем там по нулям.
Т.о. вызов функции GPIO_Init(GPIOC, &GPIO_InitStructure) можно заменить следующей парой строк:
GPIOC->CRH &= ~(uint32_t)(0xf<<20); GPIOC->CRH |= (uint32_t)(0x2<<20);
Переключение пина осуществляется через регистр GPIOx->ODR:
Т.е. вызов функции: "GPIO_SetBits(GPIOC,GPIO_Pin_13)" можно заменить на: "GPIOC->ODR |= GPIO_Pin_13".
В Cortex-M3 нет тех удобных битовых инструкций что были в STM8 (имеется ввиду bset,bres,bcpl). Поэтому для побитового изменения состояния регистра GPIOx->ODR, были специально введены дополнительные регистры: GPIOx->BSRR и GPIOx->BRR. Посмотрим на их описание:
Эти регистры управляют состоянием GPIOx->ODR путем записи единицы в их соответствующий разряд. Т.е. вместо GPIOC->ODR &= ~(GPIO_Pin_13); можно использовать GPIOC->BRR = GPIO_Pin_13. Такой вариант разложится компилятором в ОДНУ ассемблерную инструкцию, вместо трёх, если делать непосредственно через GPIOC->ODR регистр. Обратите внимание, что через BSRR/BRR регистры одной инструкцией можно менять состояние сразу нескольких пинов порта. При желании можно организовать параллельную шину через bit-bang.
В итоге, функция main() примет вид:
int main() { // enable GPIOC port RCC->APB2ENR |= RCC_APB2Periph_GPIOC; // --- GPIO setup ---- GPIOC->CRH &= ~(uint32_t)(0xf<<20); GPIOC->CRH |= (uint32_t)(0x2<<20); for(;;){ GPIOC->BSRR=GPIO_Pin_13; dummy_loop(600000); GPIOC->BRR=GPIO_Pin_13; dummy_loop(600000); } }Примечание от 2025г. Вместо использования непонятных цифр при конфигурации регистра ввода-вывода "GPIOC->CRH", можно было использовать именованные константы определенные в заголовочных файлах SPL:
// Configure PC13 as push-pull output (max speed 2 MHz) GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13); GPIOC->CRH |= GPIO_CRH_MODE13_1; // Output mode, max speed 2 MHz (0b10)
После компиляции размер прошивки сократился до 1084 байт. Уже лучше, но все-равно многовато. После дизассеблирования, становится ясно, к прошивке кроме нашего кода добавляется ещё стандартная библиотека Си: libc или newlib (облегчённый вариант стандартной библиотеки Cи), а также некоторые функции конфигурации микроконтроллера (startup files) включая таблицу прерываний. Все вместе это и занимает один килобайт. Ничего не имею против таблицы, но от всего остального (библиотеки libc и startup файлов) хотелось бы избавиться. В TrueStudio у нас нет полного контроля за Makefile'ом, поэтому предлагаю закрыть эту IDE и перейти в консоль.
Для начала давайте разберемся с форматом бинарной прошивки, т.е. с чего процессор начинает работу и как он находит программу. Потому-что, как вы наверно догадываетесь, прошивка в ARM начинается не с программного кода, и даже не с таблицы векторов.
Те, кто внимательно читали документацию, знают, что прошивка для Cortex-M3 обязательно должна начинаться со значения указателя стека, после чего должны идти адреса обработчиков прерываний: Reset, NMI, Hard Fault:
На адрес 0x00000000 происходит маппинг: или флеш-памяти или ОЗУ, в зависимости от конфигурации boot-пинов. Флеш-память начинается с адреса 0x08000000.
Т.о. минимальный вариант программы Blink для stm32f103 можно записать следующим образом:
#include "stm32f10x.h" #include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h" #define WAIT 600000 asm(".word 0x20005000\n\t" // stack pointer (adjust based on your device's RAM size) ".word main+1\n\t" // reset handler (Thumb mode) ".word fault_irq+1\n\t" // NMI ".word fault_irq+1\n\t" // HardFault ".word fault_irq+1\n\t" // MemManage ".word fault_irq+1\n\t" // BusFault ".word fault_irq+1\n\t"); // UsageFault void fault_irq() { while (1); } void dummy_loop(volatile uint32_t count) { while (--count); } int main() { // Enable GPIOC clock RCC->APB2ENR |= RCC_APB2Periph_GPIOC; // Configure PC13 as push-pull output (max speed 2 MHz) GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13); GPIOC->CRH |= GPIO_CRH_MODE13_1; // Output mode, max speed 2 MHz (0b10) // Super loop to toggle PC13 while (1) { GPIOC->BSRR = GPIO_Pin_13; // Set PC13 dummy_loop(WAIT); GPIOC->BRR = GPIO_Pin_13; // Reset PC13 dummy_loop(WAIT); } }
Здесь, мы в начале программы пишем заголовок, в который включены: значение стека и адреса обязательных векторов прерываний. Среди векторов прерываний выделется Reset, который указывает на начало программы.
Так же как в AVR, где для включения имен регистров ввода-вывода и констант пишется:
#include <avr/io.h>
В STM32 используется аналогичные включения:
#include "stm32f10x.h" #include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h"
В файле "stm32f10x.h" содержатся физические адреса регистров, а в "stm32f10x_gpio.h" и "stm32f10x_rcc.h" описаны структуры, маски и константы, чтобы к регистрам ввода-вывода можно было обращаться в стиле "RCC->APB2ENR = какое-то значение". Данные заголовочные файлы были взяты из SPL для stm32f103xx, они не содержат код, только адреса и структуры.
Таблица векторов в Cortex-M3 это просто массив c адресами обработчиков прерываний. Таких инструкций как: INT и IRET, в Cortex-M3 не существует. Все адреса прерываний должны быть нечётными! Т.е. единица прибавляемая к адресу метки нужна для получения нечётного числа, чтобы указать, что инструкция в обработчике прерывания из набора Thumb/Thumb2, а не из 32-битных инструкций ARM. Если вектор будет указывать на чётный адрес, то переход по нему приведёт к срабатыванию прерывания Usage Fault.
Пока на данном этапе, структура проекта выглядит так:
$ tree . . ├── CMSIS │ ├── core │ │ ├── core_cm3.c │ │ └── core_cm3.h │ └── device │ ├── stm32f10x.h │ └── system_stm32f10x.h ├── SPL │ └── inc │ ├── stm32f10x_gpio.h │ └── stm32f10x_rcc.h └── main.c 5 directories, 7 files
Для компиляции прошивки под ARM-микроконтроллер потребуется скрипт компоновщика. В AVR он тоже требуется, но обычно входит в пакет компилятора, и никто о нем не думает. Что бы не кидаться сразу многостраничным скриптом компоновщика, про который нам в принципе нужно знать совем немного, я взял скрипт из шаблонного проекта SW4STM32 и сократил его настолько, что бы данный Blink просто напросто собрался:
/* Highest address of the user mode stack */ _estack = 0x20005000; /* end of RAM */ /* Memories definition */ MEMORY { RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K ROM (rx) : ORIGIN = 0x8000000, LENGTH = 64K } /* Sections */ SECTIONS { /* The program code and other data into ROM memory */ .text : { . = ALIGN(4); *(.text) /* .text sections (code) */ } >ROM /* Constant data into ROM memory*/ .rodata : { . = ALIGN(4); *(.rodata) /* .rodata sections (constants, strings, etc.) */ } >ROM /* Initialized data sections into RAM memory */ .data : { . = ALIGN(4); *(.data) /* .data sections */ } >RAM AT> ROM /* Uninitialized data section into RAM memory */ . = ALIGN(4); .bss : { *(.bss) *(COMMON) . = ALIGN(4); _ebss = .; /* define a global symbol at bss end */ __bss_end__ = _ebss; } >RAM }
Назовем его "stm32f10xx8.ld". Теперь у нас есть все для компиляции, поэтому давайте сделаем это:
$ arm-none-eabi-gcc -mthumb -mcpu=cortex-m3 -O0 -c -g -DSTM32F10X_MD -I ./CMSIS/device -I ./CMSIS/core -I ./SPL/inc -o main.o ./main.c
Компоновка:
$ arm-none-eabi-ld -Tstm32f10xx8.ld main.o -o firmware.elf
Тоже самое можно сделать одной командой:
$ arm-none-eabi-gcc -T stm32f10xx8.ld -nostartfiles -mcpu=cortex-m3 -g -mthumb -DSTM32F10X_MD -I ./CMSIS/device -I ./CMSIS/core -I ./SPL/inc -o firmware.elf main.c
Смотрим на размер получившийся прошивки:
$ arm-none-eabi-size ./firmware.elf
text data bss dec hex filename
148 0 0 148 94 ./firmware.elf
148 байт, как и было обещано. А почему-бы не добавить оптимизации? Добавим ключ "-Os":
$ arm-none-eabi-gcc -T stm32f10xx8.ld -nostartfiles -mcpu=cortex-m3 -g -Os -mthumb -DSTM32F10X_MD -I ./CMSIS/device -I ./CMSIS/core -I ./SPL/inc -o firmware.elf main.c $ arm-none-eabi-size ./firmware.elf text data bss dec hex filename 108 0 0 108 6c ./firmware.elf
108 байт - это уже близко к прошивке 8-битного микроконтроллера. Для ATtiny13A прошивка с Blink выходит на 68 байт. Но для 32-разрядного микроконтроллера программа всегда будет больше памяти занимать, т.к. физические адреса тут в два раза длиннее.
Прошиваем:
$ arm-none-eabi-objcopy -O binary firmware.elf ./firmware.bin $ st-flash write ./firmware.bin 0x08000000
После прошивки на плате должен начать мигать зеленый светодиод. На логическом анализаторе это выглядит следующим образом:
Сейчас микроконтроллер работает на дефолтных настройках, т.е. он тактируется от HSI с частотой 8 МГц.
Для удобства сборки можно добавить к проекту Makefile:
# Toolchain setup (adjust paths if needed) CC = arm-none-eabi-gcc OBJCOPY = arm-none-eabi-objcopy SIZE = arm-none-eabi-size RM = rm -f # Target and output names TARGET = firmware BIN = $(TARGET).bin HEX = $(TARGET).hex ELF = $(TARGET).elf # Include paths INC = -ICMSIS/device INC += -ICMSIS/core INC += -ISPL/inc DEF = -DSTM32F10X_MD # MCU and flags MCU = cortex-m3 CFLAGS = -mcpu=$(MCU) -mthumb -Wall -Os -ffunction-sections -fdata-sections LDFLAGS = -T stm32f10xx8.ld -nostartfiles # Source files SRCS = main.c # Build rules all: $(BIN) $(HEX) size $(ELF): $(SRCS) $(CC) $(CFLAGS) $(DEF) $(INC) $(LDFLAGS) $^ -o $@ $(BIN): $(ELF) $(OBJCOPY) -O binary $< $@ $(HEX): $(ELF) $(OBJCOPY) -O ihex $< $@ size: $(ELF) $(SIZE) --format=berkeley $< # Flash to STM32 via ST-Link flash: $(BIN) st-flash write $< 0x08000000 # Clean clean: $(RM) $(ELF) $(BIN) $(HEX)
Здесь я думаю, что все понятно.
Что еще можно сделать? Можно добавить к проекту CMakeLists.txt для сборки посредством CMake, его я спрятал под спойлер.
показать CMakeLists.txtcmake_minimum_required(VERSION 3.12) project(firmware LANGUAGES C) # Toolchain setup set(CMAKE_SYSTEM_NAME Generic) set(CMAKE_SYSTEM_PROCESSOR arm) set(CMAKE_C_COMPILER arm-none-eabi-gcc) set(CMAKE_OBJCOPY arm-none-eabi-objcopy) set(CMAKE_SIZE arm-none-eabi-size) # Target and output names set(TARGET firmware) set(ELF ${TARGET}.elf) set(BIN ${TARGET}.bin) set(HEX ${TARGET}.hex) # Include paths include_directories( CMSIS/device CMSIS/core SPL/inc ) # Definitions add_definitions(-DSTM32F10X_MD) # MCU and flags set(MCU cortex-m3) set(CMAKE_C_FLAGS "-mcpu=${MCU} -mthumb -Wall -Os -ffunction-sections -fdata-sections") set(CMAKE_EXE_LINKER_FLAGS "-T ${CMAKE_SOURCE_DIR}/stm32f10xx8.ld -nostartfiles") # Source files set(SRCS main.c) # Build executable add_executable(${ELF} ${SRCS}) # Generate binary and hex files add_custom_command( TARGET ${ELF} POST_BUILD COMMAND ${CMAKE_OBJCOPY} -O binary ${ELF} ${BIN} COMMAND ${CMAKE_OBJCOPY} -O ihex ${ELF} ${HEX} COMMENT "Generating ${BIN} and ${HEX}" ) # Print size information add_custom_target( size COMMAND ${CMAKE_SIZE} --format=berkeley ${ELF} DEPENDS ${ELF} ) # Flash target (requires ST-Link) add_custom_target( flash COMMAND st-flash write ${BIN} 0x08000000 DEPENDS ${BIN} )
Использование:
$ mkdir build && cd $_ $ cmake -DCMAKE_BUILD_TYPE=Release .. $ make && make size
Qbs-скрипт я здесь выкладывать не буду, он будет доступен на git. Скрипт для Qbs версии:
$ qbs --version
1.24.1
Подробно о связке STM32+Qbs+QtCreator я писал в: "Qbs-скрипт для STM32F103C8-проекта на CMSIS (пошаговая инструкция)"
Qbs-скрипт позволяет использовать IDE QtCreator для написании кода, и при этом контролировать процесс сборки не хуже Makefile'ов (Cmake все-равно какие-то свои флаги добавляет и прошивка получается толще):
QtCreator это нативная программа, а не Java как STM32CubeIDE, шустро ворочается, не требует регистрации, имеет статический линтер на Clang и возможность отладки:
На мой взгляд это самая недооцененная IDE для разработки.
С помощью команды:
$ arm-none-eabi-objdump -S ./firmware.elf
можно дизассемблировать прошивку, что бы убедиться, что заголовок с константами расположен именно по адресу 0x8000000:
Disassembly of section .text: 08000000 <.text>: 8000000: 20005000 .word 0x20005000 8000004: 08000031 .word 0x08000031 8000008: 0800001d .word 0x0800001d 800000c: 0800001d .word 0x0800001d 8000010: 0800001d .word 0x0800001d 8000014: 0800001d .word 0x0800001d 8000018: 0800001d .word 0x0800001d Disassembly of section .text.fault_irq: 0800001c <fault_irq>: ".word fault_irq+1\n\t" // MemManage ".word fault_irq+1\n\t" // BusFault ".word fault_irq+1\n\t"); // UsageFault void fault_irq() { while (1); 800001c: e7fe b.n 800001c <fault_irq> Disassembly of section .text.dummy_loop: 0800001e <dummy_loop>: } void dummy_loop(volatile uint32_t count) { 800001e: b082 sub sp, #8 8000020: 9001 str r0, [sp, #4] while (--count); 8000022: 9b01 ldr r3, [sp, #4] 8000024: 3b01 subs r3, #1 8000026: 9301 str r3, [sp, #4] 8000028: 2b00 cmp r3, #0 800002a: d1fa bne.n 8000022 <dummy_loop+0x4> } 800002c: b002 add sp, #8 800002e: 4770 bx lr Disassembly of section .text.startup.main: 08000030 <main>: GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13); GPIOC->CRH |= GPIO_CRH_MODE13_1; // Output mode, max speed 2 MHz (0b10) // Super loop to toggle PC13 while (1) { GPIOC->BSRR = GPIO_Pin_13; // Set PC13 8000030: f44f 5100 mov.w r1, #8192 ; 0x2000 RCC->APB2ENR |= RCC_APB2Periph_GPIOC; 8000034: 4a0b ldr r2, [pc, #44] ; (8000064 <main+0x34>) int main() { 8000036: b508 push {r3, lr} RCC->APB2ENR |= RCC_APB2Periph_GPIOC; 8000038: 6993 ldr r3, [r2, #24] dummy_loop(WAIT); 800003a: 480b ldr r0, [pc, #44] ; (8000068 <main+0x38>) RCC->APB2ENR |= RCC_APB2Periph_GPIOC; 800003c: f043 0310 orr.w r3, r3, #16 8000040: 6193 str r3, [r2, #24] GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13); 8000042: f5a2 3280 sub.w r2, r2, #65536 ; 0x10000 8000046: 6853 ldr r3, [r2, #4] 8000048: f423 0370 bic.w r3, r3, #15728640 ; 0xf00000 800004c: 6053 str r3, [r2, #4] GPIOC->CRH |= GPIO_CRH_MODE13_1; // Output mode, max speed 2 MHz (0b10) 800004e: 6853 ldr r3, [r2, #4] 8000050: f443 1300 orr.w r3, r3, #2097152 ; 0x200000 8000054: 6053 str r3, [r2, #4] GPIOC->BSRR = GPIO_Pin_13; // Set PC13 8000056: 6111 str r1, [r2, #16] dummy_loop(WAIT); 8000058: f7ff ffe1 bl 800001e <dummy_loop> GPIOC->BRR = GPIO_Pin_13; // Reset PC13 800005c: 6151 str r1, [r2, #20] dummy_loop(WAIT); 800005e: f7ff ffde bl 800001e <dummy_loop> while (1) { 8000062: e7f8 b.n 8000056 <main+0x26> 8000064: 40021000 .word 0x40021000 8000068: 000927c1 .word 0x000927c1
Здесь, возможно, потребуется небольшой ликбез.
1. Cortex-M3 был разработан двадцать лет назад, но до сих пор явлется популярным ядром для микроконтроллеров, несмотря на то, что впоследствии были выпущены более оптимизированные Cortex-M0/M0+ и более прогрессивные Cortex-M4, на смену которым в свою очередь выпущены Cortex-M23 и Cortex-M33. Все эти ядра занимают свою нишу в разработке электроники.
Coretx-M3 это совокупный набор инструкций Thumb и Thumb II. Наглядно на количество команд в том и другом наборе можно посмотреть на следующей картинке:
Thumb - это набор 16-битных инструкций. Исключением являются инструкция BL и барьерные инструкции (BL, DMB, DSB, ISB), которые имеют длину 4 байта. Thumb II - это набор 32-битных инструкций. Несмотря на то, что количество инструкций Thumb II в разы больше, чем у Thumb, многие (но не все) эти инструкции можно заменить связкой из пары инструкций Thumb I. Но зачастую, инструкции Thumb II будут работать быстрее, нежели такая связка.
Все инструкции в Cortex-M3 либо 16-битные, либо 32-битные. Одно-байтных, трех-байтных, пяти-байтных инструкций в данной архитектуре не существует.
2. В Cortex M3, чтобы выполнить какие-то действия над данными в ОЗУ, их нужно загрузить сначала в регистры, потом произвести над ними какие-то действия, и затем снова переслать их в ОЗУ. Эти операции перемещения занимают много времени, особенно когда данных много, например при обработке звука или изображения. Для того что бы сгладить этот недостаток, используется кеширование, SIMD инструкции и DMA.
3. В Cortex-M3 имеется 13 регистров общего назначения (РОН) R0-R12:
Все регистры делятся на нижние (R0-R7) и верхние (R8-R15). Инструкции из набора Thumb I могут адресовать только нижние регистры. Исключеним является инструкция MOV которая может адресовать в том числе и верхние регистры общего назначения. Инструкции из набора Thumb II могут адресовать все РОН, а так же регистры LR и SP. Регистр R15 имеет является счетчиком команд - PC, R14 - имеет обозначение LR - Link register, в него помещается адрес возврата при вызове подпрограммы. Регистр R13 - является указателем стека - SP, он "с двойным дном", т.е. в зависимости от того, выполняется ли обычная программа, либо прерывание (более строго будет говорить о выполнении кода с повышенными привилегиями, т.е. код который будет исполняться в первую очередь), указатель стека показывает разные значения. Кроме этих 16-регистров, имеется специальный статусный регистр xPSR. Он явлется составным из трех регистров: APSR + IPSR + EPSR.
4. В Cortex-M3 нет инструкций CALL/RET. Вместо них есть переход с сохранением адреса возврата в регистре R14/LR. Собственно: BL и BX. Если подпрограмма содержит в себе вызов другой подпрограммы, то содержимое R14/LR следует сохранить в стеке. Возврат тогда будет по инструкции: POP {PC}. например:
MySubroutine: PUSH {LR} ; сохраняем в стеке значение LR BL AnotherSub ; переход на другую подпрограмму, адрес возврата автоматически сохраняется в LR POP {PC} ; возврат из подпрограммы
4. В Cortex-M3 нет переходов по абсолютному адресу, все переходы относительные. Это означает, что весь код являются перемещаемым.
5. Инструкций INC, DEC в Cortex-M3 так же нет, но вместо них можно использовать инструкции "SUB Reg,#1" и "ADD reg,#1". Они занимают два байта вместе с операндом. Если операнд больше одного байта, то тут потребуется 32-битная инструкция.
5. В Thumb II есть режим индексной адресации с автоинкрементом и автодекриментом для инструкций LDR и STR:
LDR R0, [R1, #4]! ; пост инкремент : R1 += 4 после выполнения инструкции STR R0, [R1, #-4]! ; пост декремент : R1 -= 4 после выполнения инструкции
6. Указатель стека R13/SP выровнен по границе слова, т.е. его младшие два бита аппаратно сброшены в ноль. Указатель стека всегда указывает на последние сохраненные данные:
MOV SP, #0x20005000 ; инициализация SP PUSH {R0} ; SP = 0x20004FFC, R0 сохранен по адресу [0x20004FFC] PUSH {R1} ; SP = 0x20004FF8, R1 сохранен по адресу [0x20004FF8] POP {R1} ; R1 = [0x20004FF8], SP = 0x20004FFC
7. Многие инструкции могут работать с группой регистров (reglist). Причем такие операции выполняются одной инструкцией. Например - сохранение контекста:
PUSH {R0-R7, LR} // сохраняем в стеке девять регистров
Или копирование блока памяти:
loop: LDMIA R0!, {R1-R4} STMIA R1!, {R1-R4} SUBS R2, #16 BNE loop
8. С помощью суффиксов .b .h .w может указываться размер операнда: байт, полуслово или слово. Например:
LDRH R0, [R1] // Загрузка полуслова (16-бит) STRB.W R2, [R3, #4] // Сохранение байта (8-бит) с приведением к 32-битному виду
Но существуют инструкции с жестко прописанными размерами данных:
LDR = слово (32-бит) LDRH = полуслово (16-бит) LDRB = байт (8-битt)
9. С помощью условного выполнения, некоторые инструкция может быть выполнена или нет в зависимости от значений того или иного флага регистра состояний. В Thumb наборе инструкций, это применяется полько с инструкциями перехода, т.е. так образуются инструкции условного перехода. В наборе инструкций Thumb II с большинством логических и арифметических операций, суффиксы условного выполнения применяться не могут.
10. Добавление суффикса "S" (например ADDS, SUBS), указывает, что команда должна изменить флаги APSR: N, Z, C, V (разные инструкции влиют на разные флаги) на основе результата выполнения.
ADDS R0, R1, R2 ; Изменяет APSR флаги (N, Z, C, V) ADD R0, R1, R2 ; не изменяет флаги
Суффикс "S" работает с арифметическими и логическими операторами. Не работает с LDR, STR, B, BX, MSR, MRS. ИНструкции сравнения: CMP, CMN, TST, TEQ, всегда изменяют флаги состояния. Также всегда влиают на них некоторые 16-битные инструкции из набора Thumb-I, например: ADD/SUB. Пример использования суффикса S:
SUBS R0, R0, #1 ; R0=R0-1 BNE loop ; переход по метке если (R0 != 0)
11. В Thumb II наборе инструкций присутствует блок ветвления IT (If-Then), который призван реализовать небольшие ветвления, до 4-х инструкций, не прибегая к инструкциям ветвления и избегая сброса конвейера, т.к. каждый переход по метке влечет задержку в работе процессора на 2-3 машинных цикла. Он работает со статусными флагами: N (признак отрицательного числа), Z (признак нуля), C (перенос), V (переполнение) регистра APSR. Блок может выполняться с 16 суффиксами ветвления (EQ, NE, GT, LT, и др.).
Блок IT имеет следующий синтаксис:
IT{x{y{z}}} <условие> где: x - T(hen) или E(lse) для первой инструкции; y - тоже самое для второй инструкции; z - для третьей. максимум - 4 инструкции.
Пример кода без IT блока
CMP R0, #10 ; выполняется за 1 машинный цикл BNE skip ; выполняется за 1 машинный цикл если R0 != 10, и выполняется за 3 машинных цикла, если осуществляется переход по метке. MOV R1, #100 ; 1 цикл skip:
Итого, данный код выполняется за 3 или 4 машинных цикла.
Тот же самый код с использованием IT блока:
CMP R0, #10 ; 1 цикл IT EQ ; 1 цикл MOVEQ R1, #100 ; 1 цикл, если R1 = 10
Итого: 2-3 машинных цикла.
12. Когда происходит прерывание или исключение, процессор автоматически сохраняет в стеке восемь регистров, процесс называется stacking " Coretx-M3 Technical Reference Manual - глава 5.5.1"
Сохраняются регистры: R0, R1, R2, R3, R12, LR, PC, xPSR. Регистр LR во время выполнения обработчика прерывания содержит адрес выхода из прерывания.
большие адреса | xPSR | | PC | | LR | | R12 | | R3 | | R2 | | R1 | | R0 | <-- SP after stacking меньшие адреса
Остальные регистры следует сохранять вручную, если это требуется:
ISR_Handler: PUSH {R4-R7, LR} ; Manual save (R4-R7 and LR if nested calls exist) ... ; ISR code POP {R4-R7, PC} ; Restore and return (PC replaces LR)13. Ассемблер ARM поддерживает несколько режимов адресации.
a) Регистровая адресация, когда операндами являются исключительно регистры R0-R15. Пример:
ADD R0, R1, R2 ; R0 = R1 + R2 (все операнды являются регистрами)
б) Непосредственная адресация, когда операнд указан в самой инструкции. При таком типе адресации не требуется обращения к памяти. Например:
MOV R0, #0xAB ; R0 = 0xAB (регистру R0 присваивается непосредственно само число стоящее в операнде) ADD R3, #10 ; R3 += 10
в) Прямая (абсолютная) адресация (англ. direct (absolute) addressing) - это режим адресации, в котором адрес данных указан напрямую, без использования каких-либо косвенных указаний или расчетов. Например:
LDR R0, =0x40021000 ; Загрузка значения находящегося в оперативной памяти по адресу 0x40021000 (например RCC register)
г) Косвенно-регистровая адресация (Register Indirect Addressing) - это режим адресации, при котором адрес операнда (данных) хранится в регистре процессора, а не непосредственно в поле адреса инструкции. Например:
LDR R0, [R1] ; регистру R0 присваивается значение хранящееся по адресу записаному в R1 STR R2, [R3] ; сохранение значения регистра R2 по адресу записаному в R3
д) Косвенно-регистровая адресация со смещением - это режим адресации, при котором к адресу операнда (данных) прибавляется какое-то смещение. Например:
LDR R0, [R1, #8] ; R0 = *(R1 + 8) STR R2, [R3, R4] ; Сохранение R2 по адресу (R3 + R4)
е) Пре-индексная адресация, когда индексный регистр предварительно увеличивается. Например:
LDR R0, [R1, #4]! ; раскладывается две операции: R1 += 4, и R0 = *R1
ё) Пост-индексная адресация, когда после операции индексный регистр увеличивается. Например:
LDR R0, [R1], #4 ; раскладывается также в две операции: R0 = *R1 и R1 += 4
ж) Адресация относительно регистра PC. В этом случае PC выступает как индексный регистр. Например:
LDR R0, [PC, #0x100] ; Загрузка в R0 значения по адресу (PC + 0x100)
з) Адресация относительно регистра SP. В этом случае SP выступает как индексный регистр. Например:
LDR R0, [SP, #8] ;Загрузка в R0 значения по адресу SP+8 STR R1, [SP, #-4]! ; Эквивалентно операции PUSH
Использование SP в качестве индексного довольно ограничено, если вы хотите что-то достать из стека, или изменить хранящиеся там данные, то следует SP скопировать в РОН и уже его использовать в качестве индексного. Например:
MOV R4, SP ; Копируем SP в R4 LDR R0, [R4, R2] ; Используем R4 как индексный
и) Адресация с блочной загрузкой-выгрузкой, когда в какой-то блок регистров загружается или выгружаются значения из памяти. Например:
LDMIA R0!, {R1-R4} ; Загрузка в R1-R4 значений из памяти, при этом R0 используется как индексный регистр с автоинкрементом, после выполнения инструкции его значение увеличится на 16, т.е. R0 += 16 STMDB SP!, {R5-R7} ; Аналог PUSH {R5-R7}
Если вам что-то было не понятно, вы можете почитать учебник Козаченко В.Ф.(ред.): "Практический курс микропроцессорной техники на базе процессорных ядер ARM-Cortex-M3/M4/M4F" 2019 М.: МЭИ, там очень дотошно всё разобрано.
Первоначальная версия программы успешно собралась только потому, что таблица векторов была размещена в самом начале программы, а проект состоял лишь из одного файла с исходным кодом.
Технически, можно отказаться как от использования ассемблера так и от размещения таблицы в "шапке" исходника. Для этого перепишем программу следующим образом:
#include "stm32f10x.h" #include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h" #define WAIT 600000 void dummy_loop(volatile uint32_t count) { while (--count); } int main() { // Enable GPIOC clock RCC->APB2ENR |= RCC_APB2Periph_GPIOC; // Configure PC13 as push-pull output (max speed 2 MHz) GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13); GPIOC->CRH |= GPIO_CRH_MODE13_1; // Output mode, max speed 2 MHz (0b10) // Super loop to toggle PC13 while (1) { GPIOC->BSRR = GPIO_Pin_13; // Set PC13 dummy_loop(WAIT); GPIOC->BRR = GPIO_Pin_13; // Reset PC13 dummy_loop(WAIT); } } void fault_irq() { while (1); } // Vector table (placed at 0x08000000 via linker script) __attribute__((section(".isr_vector"))) const void* vectors[] = { (void*)0x20005000, // Stack pointer (adjust based on your device's RAM size) (void*)(main + 1), // Reset handler (Thumb mode: address + 1) (void*)(fault_irq + 1), // NMI (void*)(fault_irq + 1), // HardFault (void*)(fault_irq + 1), // MemManage (void*)(fault_irq + 1), // BusFault (void*)(fault_irq + 1) // UsageFault };
Здесь заголовок прошивки был вынесен в отдельную секцию ".isr_vector". Теперь следует эту секцию указать в скрипте компоновщика. Для этого в блоке "SECTIONS", переж разделом ".text" разместить объявление новой секции:
/* Vector table at the start of ROM memory */ .isr_vector : { . = ALIGN(4); KEEP(*(.isr_vector)) /* IRQ vector table */ . = ALIGN(4); } >ROM
Под спойлером размещен полный вариант скрипта компоновщика.
показать/* Highest address of the user mode stack */ _estack = 0x20005000; /* end of RAM */ /* Memories definition */ MEMORY { RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K ROM (rx) : ORIGIN = 0x8000000, LENGTH = 64K } /* Sections */ SECTIONS { /* Vector table at the start of ROM memory */ .isr_vector : { . = ALIGN(4); KEEP(*(.isr_vector)) /* IRQ vector table */ . = ALIGN(4); } >ROM /* The program code and other data into ROM memory */ .text : { . = ALIGN(4); *(.text) /* .text sections (code) */ } >ROM /* Constant data into ROM memory*/ .rodata : { . = ALIGN(4); *(.rodata) /* .rodata sections (constants, strings, etc.) */ } >ROM /* Initialized data sections into RAM memory */ .data : { . = ALIGN(4); *(.data) /* .data sections */ } >RAM AT> ROM /* Uninitialized data section into RAM memory */ . = ALIGN(4); .bss : { *(.bss) *(COMMON) . = ALIGN(4); _ebss = .; /* define a global symbol at bss end */ __bss_end__ = _ebss; } >RAM }
Теперь, после компиляции, утилита "size" выдаст такое распределение кода:
$ arm-none-eabi-size --format=berkeley firmware.elf
text data bss dec hex filename
80 28 0 108 6c firmware.elf
Размер прошивки не изменился - 108 байт, но 28 байт заголовка перешли из секции "text" в секцию "data". Теперь, если дизассемблировать прошивку командой "arm-none-eabi-objdump -S", то таблицу даже не покажет, т.к. это не код. Но в том, что заголовок прошивки скомпоновался по адресу "0x8000000", можно убедиться с помощью команд:
$ arm-none-eabi-objdump -h firmware.elf |head firmware.elf: file format elf32-littlearm Sections: Idx Name Size VMA LMA File off Algn 0 .isr_vector 0000001c 08000000 08000000 00010000 2**2 CONTENTS, ALLOC, LOAD, DATA 1 .text 00000000 0800001c 0800001c 0001001c 2**1 CONTENTS, ALLOC, LOAD, READONLY, CODE 2 .text.fault_irq 00000002 0800001c 0800001c 0001001c 2**1
или
$ arm-none-eabi-objdump -s -j .isr_vector firmware.elf
firmware.elf: file format elf32-littlearm
Contents of section .isr_vector:
8000000 00500020 31000008 1d000008 1d000008 .P. 1...........
8000010 1d000008 1d000008 1d000008 ............
или
$ xxd -g 4 firmware.bin | head
00000000: 00500020 31000008 1d000008 1d000008 .P. 1...........
00000010: 1d000008 1d000008 1d000008 fee782b0 ................
00000020: 0190019b 013b0193 002bfad1 02b07047 .....;...+....pG
00000030: 4ff40051 0b4a08b5 93690b48 43f01003 O..Q.J...i.HC...
00000040: 9361a2f5 80325368 23f47003 53605368 .a...2Sh#.p.S`Sh
00000050: 43f40013 53601161 fff7e1ff 5161fff7 C...S`.a....Qa..
00000060: defff8e7 00100240 c0270900 .......@.'..
Теперь можно вынести таблицу прерываний в отдельный модуль, и подключить его к проекту. Однако я предлагаю не изобретать велосипед, а привести проект, как принято говорить: "к стандартам принятым в индустрии".
В частности, я предлагаю подключить к проекту готовый "startup.s" - это ассемблерный файл, состоящий из полной таблицы прерываний STM32F103. С нашей стороны потребуется создание файлов с правилами сборки проекта Makefile, CMakeLists.txt и Qbs-скрипта.
В STM32F103 довольно обширная таблица прерываний, состоящая из 70-векторов обработчиков и еще шести зарезервированных (Reserved):
показатьИтого 76 * 4 = 304 байта. Я взял готовый "startup.s" из шаблонного проекта SW4STM32, который содержит полную таблицу векторов и стартовый код:
показать startup.s/** ****************************************************************************** * @file startup_stm32.s dedicated to STM32F103C8Tx device * @author Ac6 * @version V1.0.0 * @date 2021-06-30 ****************************************************************************** */ .syntax unified .cpu cortex-m3 .fpu softvfp .thumb .global g_pfnVectors .global Default_Handler /* start address for the initialization values of the .data section. defined in linker script */ .word _sidata /* start address for the .data section. defined in linker script */ .word _sdata /* end address for the .data section. defined in linker script */ .word _edata /* start address for the .bss section. defined in linker script */ .word _sbss /* end address for the .bss section. defined in linker script */ .word _ebss /** * @brief This is the code that gets called when the processor first * starts execution following a reset event. Only the absolutely * necessary set is performed, after which the application * supplied main() routine is called. * @param None * @retval : None */ .section .text.Reset_Handler .weak Reset_Handler .type Reset_Handler, %function Reset_Handler: ldr r0, =_estack mov sp, r0 /* set stack pointer */ /* Copy the data segment initializers from flash to SRAM */ ldr r0, =_sdata ldr r1, =_edata ldr r2, =_sidata movs r3, #0 b LoopCopyDataInit CopyDataInit: ldr r4, [r2, r3] str r4, [r0, r3] adds r3, r3, #4 LoopCopyDataInit: adds r4, r0, r3 cmp r4, r1 bcc CopyDataInit /* Zero fill the bss segment. */ ldr r2, =_sbss ldr r4, =_ebss movs r3, #0 b LoopFillZerobss FillZerobss: str r3, [r2] adds r2, r2, #4 LoopFillZerobss: cmp r2, r4 bcc FillZerobss /* Call the clock system intitialization function.*/ // bl SystemInit /* Call static constructors */ // bl __libc_init_array /* Call the application's entry point.*/ bl main LoopForever: b LoopForever .size Reset_Handler, .-Reset_Handler /** * @brief This is the code that gets called when the processor receives an * unexpected interrupt. This simply enters an infinite loop, preserving * the system state for examination by a debugger. * * @param None * @retval : None */ .section .text.Default_Handler,"ax",%progbits Default_Handler: Infinite_Loop: b Infinite_Loop .size Default_Handler, .-Default_Handler /****************************************************************************** * * The STM32F103C8Tx vector table. Note that the proper constructs * must be placed on this to ensure that it ends up at physical address * 0x0000.0000. * ******************************************************************************/ .section .isr_vector,"a",%progbits .type g_pfnVectors, %object .size g_pfnVectors, .-g_pfnVectors g_pfnVectors: .word _estack .word Reset_Handler .word NMI_Handler .word HardFault_Handler .word MemManage_Handler .word BusFault_Handler .word UsageFault_Handler .word 0 .word 0 .word 0 .word 0 .word SVC_Handler .word DebugMon_Handler .word 0 .word PendSV_Handler .word SysTick_Handler .word WWDG_IRQHandler /* Window Watchdog interrupt */ .word PVD_IRQHandler /* PVD through EXTI line detection interrupt */ .word TAMPER_IRQHandler /* Tamper interrupt */ .word RTC_IRQHandler /* RTC global interrupt */ .word FLASH_IRQHandler /* Flash global interrupt */ .word RCC_IRQHandler /* RCC global interrupt */ .word EXTI0_IRQHandler /* EXTI Line0 interrupt */ .word EXTI1_IRQHandler /* EXTI Line1 interrupt */ .word EXTI2_IRQHandler /* EXTI Line2 interrupt */ .word EXTI3_IRQHandler /* EXTI Line3 interrupt */ .word EXTI4_IRQHandler /* EXTI Line4 interrupt */ .word DMA1_Channel1_IRQHandler /* DMA1 Channel1 global interrupt */ .word DMA1_Channel2_IRQHandler /* DMA1 Channel2 global interrupt */ .word DMA1_Channel3_IRQHandler /* DMA1 Channel3 global interrupt */ .word DMA1_Channel4_IRQHandler /* DMA1 Channel4 global interrupt */ .word DMA1_Channel5_IRQHandler /* DMA1 Channel5 global interrupt */ .word DMA1_Channel6_IRQHandler /* DMA1 Channel6 global interrupt */ .word DMA1_Channel7_IRQHandler /* DMA1 Channel7 global interrupt */ .word ADC1_2_IRQHandler /* ADC1 and ADC2 global interrupt */ .word USB_HP_CAN_TX_IRQHandler /* USB High Priority or CAN TX interrupts */ .word USB_LP_CAN_RX0_IRQHandler /* USB Low Priority or CAN RX0 interrupts */ .word CAN_RX1_IRQHandler /* CAN RX1 interrupt */ .word CAN_SCE_IRQHandler /* CAN SCE interrupt */ .word EXTI9_5_IRQHandler /* EXTI Line[9:5] interrupts */ .word TIM1_BRK_IRQHandler /* TIM1 Break interrupt */ .word TIM1_UP_IRQHandler /* TIM1 Update interrupt */ .word TIM1_TRG_COM_IRQHandler /* TIM1 Trigger and Commutation interrupts */ .word TIM1_CC_IRQHandler /* TIM1 Capture Compare interrupt */ .word TIM2_IRQHandler /* TIM2 global interrupt */ .word TIM3_IRQHandler /* TIM3 global interrupt */ .word TIM4_IRQHandler /* TIM4 global interrupt */ .word I2C1_EV_IRQHandler /* I2C1 event interrupt */ .word I2C1_ER_IRQHandler /* I2C1 error interrupt */ .word I2C2_EV_IRQHandler /* I2C2 event interrupt */ .word I2C2_ER_IRQHandler /* I2C2 error interrupt */ .word SPI1_IRQHandler /* SPI1 global interrupt */ .word SPI2_IRQHandler /* SPI2 global interrupt */ .word USART1_IRQHandler /* USART1 global interrupt */ .word USART2_IRQHandler /* USART2 global interrupt */ .word USART3_IRQHandler /* USART3 global interrupt */ .word EXTI15_10_IRQHandler /* EXTI Line[15:10] interrupts */ .word RTCAlarm_IRQHandler /* RTC Alarms through EXTI line interrupt */ .word 0 /* Reserved */ .word TIM8_BRK_IRQHandler /* TIM8 Break interrupt */ .word TIM8_UP_IRQHandler /* TIM8 Update interrupt */ .word TIM8_TRG_COM_IRQHandler /* TIM8 Trigger and Commutation interrupts */ .word TIM8_CC_IRQHandler /* TIM8 Capture Compare interrupt */ .word ADC3_IRQHandler /* ADC3 global interrupt */ .word FSMC_IRQHandler /* FSMC global interrupt */ .word SDIO_IRQHandler /* SDIO global interrupt */ .word TIM5_IRQHandler /* TIM5 global interrupt */ .word SPI3_IRQHandler /* SPI3 global interrupt */ .word UART4_IRQHandler /* UART4 global interrupt */ .word UART5_IRQHandler /* UART5 global interrupt */ .word TIM6_IRQHandler /* TIM6 global interrupt */ .word TIM7_IRQHandler /* TIM7 global interrupt */ .word DMA2_Channel1_IRQHandler /* DMA2 Channel1 global interrupt */ .word DMA2_Channel2_IRQHandler /* DMA2 Channel2 global interrupt */ .word DMA2_Channel3_IRQHandler /* DMA2 Channel3 global interrupt */ .word DMA2_Channel4_5_IRQHandler /* DMA2 Channel4 and DMA2 Channel5 global interrupt */ /******************************************************************************* * * Provide weak aliases for each Exception handler to the Default_Handler. * As they are weak aliases, any function with the same name will override * this definition. * *******************************************************************************/ .weak NMI_Handler .thumb_set NMI_Handler,Default_Handler .weak HardFault_Handler .thumb_set HardFault_Handler,Default_Handler .weak MemManage_Handler .thumb_set MemManage_Handler,Default_Handler .weak BusFault_Handler .thumb_set BusFault_Handler,Default_Handler .weak UsageFault_Handler .thumb_set UsageFault_Handler,Default_Handler .weak SVC_Handler .thumb_set SVC_Handler,Default_Handler .weak DebugMon_Handler .thumb_set DebugMon_Handler,Default_Handler .weak PendSV_Handler .thumb_set PendSV_Handler,Default_Handler .weak SysTick_Handler .thumb_set SysTick_Handler,Default_Handler .weak WWDG_IRQHandler .thumb_set WWDG_IRQHandler,Default_Handler .weak PVD_IRQHandler .thumb_set PVD_IRQHandler,Default_Handler .weak TAMPER_IRQHandler .thumb_set TAMPER_IRQHandler,Default_Handler .weak RTC_IRQHandler .thumb_set RTC_IRQHandler,Default_Handler .weak FLASH_IRQHandler .thumb_set FLASH_IRQHandler,Default_Handler .weak RCC_IRQHandler .thumb_set RCC_IRQHandler,Default_Handler .weak EXTI0_IRQHandler .thumb_set EXTI0_IRQHandler,Default_Handler .weak EXTI1_IRQHandler .thumb_set EXTI1_IRQHandler,Default_Handler .weak EXTI2_IRQHandler .thumb_set EXTI2_IRQHandler,Default_Handler .weak EXTI3_IRQHandler .thumb_set EXTI3_IRQHandler,Default_Handler .weak EXTI4_IRQHandler .thumb_set EXTI4_IRQHandler,Default_Handler .weak DMA1_Channel1_IRQHandler .thumb_set DMA1_Channel1_IRQHandler,Default_Handler .weak DMA1_Channel2_IRQHandler .thumb_set DMA1_Channel2_IRQHandler,Default_Handler .weak DMA1_Channel3_IRQHandler .thumb_set DMA1_Channel3_IRQHandler,Default_Handler .weak DMA1_Channel4_IRQHandler .thumb_set DMA1_Channel4_IRQHandler,Default_Handler .weak DMA1_Channel5_IRQHandler .thumb_set DMA1_Channel5_IRQHandler,Default_Handler .weak DMA1_Channel6_IRQHandler .thumb_set DMA1_Channel6_IRQHandler,Default_Handler .weak DMA1_Channel7_IRQHandler .thumb_set DMA1_Channel7_IRQHandler,Default_Handler .weak ADC1_2_IRQHandler .thumb_set ADC1_2_IRQHandler,Default_Handler .weak USB_HP_CAN_TX_IRQHandler .thumb_set USB_HP_CAN_TX_IRQHandler,Default_Handler .weak USB_LP_CAN_RX0_IRQHandler .thumb_set USB_LP_CAN_RX0_IRQHandler,Default_Handler .weak CAN_RX1_IRQHandler .thumb_set CAN_RX1_IRQHandler,Default_Handler .weak CAN_SCE_IRQHandler .thumb_set CAN_SCE_IRQHandler,Default_Handler .weak EXTI9_5_IRQHandler .thumb_set EXTI9_5_IRQHandler,Default_Handler .weak TIM1_BRK_IRQHandler .thumb_set TIM1_BRK_IRQHandler,Default_Handler .weak TIM1_UP_IRQHandler .thumb_set TIM1_UP_IRQHandler,Default_Handler .weak TIM1_TRG_COM_IRQHandler .thumb_set TIM1_TRG_COM_IRQHandler,Default_Handler .weak TIM1_CC_IRQHandler .thumb_set TIM1_CC_IRQHandler,Default_Handler .weak TIM2_IRQHandler .thumb_set TIM2_IRQHandler,Default_Handler .weak TIM3_IRQHandler .thumb_set TIM3_IRQHandler,Default_Handler .weak TIM4_IRQHandler .thumb_set TIM4_IRQHandler,Default_Handler .weak I2C1_EV_IRQHandler .thumb_set I2C1_EV_IRQHandler,Default_Handler .weak I2C1_ER_IRQHandler .thumb_set I2C1_ER_IRQHandler,Default_Handler .weak I2C2_EV_IRQHandler .thumb_set I2C2_EV_IRQHandler,Default_Handler .weak I2C2_ER_IRQHandler .thumb_set I2C2_ER_IRQHandler,Default_Handler .weak SPI1_IRQHandler .thumb_set SPI1_IRQHandler,Default_Handler .weak SPI2_IRQHandler .thumb_set SPI2_IRQHandler,Default_Handler .weak USART1_IRQHandler .thumb_set USART1_IRQHandler,Default_Handler .weak USART2_IRQHandler .thumb_set USART2_IRQHandler,Default_Handler .weak USART3_IRQHandler .thumb_set USART3_IRQHandler,Default_Handler .weak EXTI15_10_IRQHandler .thumb_set EXTI15_10_IRQHandler,Default_Handler .weak RTCAlarm_IRQHandler .thumb_set RTCAlarm_IRQHandler,Default_Handler .weak TIM8_BRK_IRQHandler .thumb_set TIM8_BRK_IRQHandler,Default_Handler .weak TIM8_UP_IRQHandler .thumb_set TIM8_UP_IRQHandler,Default_Handler .weak TIM8_TRG_COM_IRQHandler .thumb_set TIM8_TRG_COM_IRQHandler,Default_Handler .weak TIM8_CC_IRQHandler .thumb_set TIM8_CC_IRQHandler,Default_Handler .weak ADC3_IRQHandler .thumb_set ADC3_IRQHandler,Default_Handler .weak FSMC_IRQHandler .thumb_set FSMC_IRQHandler,Default_Handler .weak SDIO_IRQHandler .thumb_set SDIO_IRQHandler,Default_Handler .weak TIM5_IRQHandler .thumb_set TIM5_IRQHandler,Default_Handler .weak SPI3_IRQHandler .thumb_set SPI3_IRQHandler,Default_Handler .weak UART4_IRQHandler .thumb_set UART4_IRQHandler,Default_Handler .weak UART5_IRQHandler .thumb_set UART5_IRQHandler,Default_Handler .weak TIM6_IRQHandler .thumb_set TIM6_IRQHandler,Default_Handler .weak TIM7_IRQHandler .thumb_set TIM7_IRQHandler,Default_Handler .weak DMA2_Channel1_IRQHandler .thumb_set DMA2_Channel1_IRQHandler,Default_Handler .weak DMA2_Channel2_IRQHandler .thumb_set DMA2_Channel2_IRQHandler,Default_Handler .weak DMA2_Channel3_IRQHandler .thumb_set DMA2_Channel3_IRQHandler,Default_Handler .weak DMA2_Channel4_5_IRQHandler .thumb_set DMA2_Channel4_5_IRQHandler,Default_Handler .weak SystemInit /************************ (C) COPYRIGHT Ac5 *****END OF FILE****/
Кроме таблицы векторов здесь содержится "стартовый" код который содержится в обработчике прерывания "Reset":
Reset_Handler: ldr r0, =_estack mov sp, r0 /* set stack pointer */ /* Copy the data segment initializers from flash to SRAM */ ldr r0, =_sdata ldr r1, =_edata ldr r2, =_sidata movs r3, #0 b LoopCopyDataInit CopyDataInit: ldr r4, [r2, r3] str r4, [r0, r3] adds r3, r3, #4 LoopCopyDataInit: adds r4, r0, r3 cmp r4, r1 bcc CopyDataInit /* Zero fill the bss segment. */ ldr r2, =_sbss ldr r4, =_ebss movs r3, #0 b LoopFillZerobss FillZerobss: str r3, [r2] adds r2, r2, #4 LoopFillZerobss: cmp r2, r4 bcc FillZerobss
Здесь два цикла. В первом цикле копируются данные из секции "data" в оперативную память, и тем самым, происходит инициализация глобальных переменных теми значениями, что были заданы в тексте программы. Второй цикл заполняет нулями bss секцию. В режиме отладки я запускал трассировку, и в случае данной программы, выполнение обоих циклов игнорировалось. После завершения стартового кода, происходит переход на функции SystemInit и __libc_init_array. Первая функция настраивает систему тактирования, которой пока в проекте нет, поэтому я ее закомментировал. Вторая функция находится в стандартной библиотеке, которой тоже нет, поэтому она тоже закомментирована. В финале, происходит переход на функцию "main".
Также я взял полный, неурезанный скрипт компоновщика:
показать stm32f10xx8.ld/* ****************************************************************************** ** ** File : LinkerScript.ld ** ** Author : Auto-generated by Ac6 System Workbench ** ** Abstract : Linker script for STM32F103C8Tx Device from STM32F1 series ** 20Kbytes RAM ** 64Kbytes ROM ** ** Set heap size, stack size and stack location according ** to application requirements. ** ** Set memory bank area and size if external memory is used. ** ** Target : STMicroelectronics STM32 ** ** Distribution: The file is distributed �as is,� without any warranty ** of any kind. ** ***************************************************************************** ** @attention ** ** <h2><center>© COPYRIGHT(c) 2021 Ac6</center></h2> ** ** Redistribution and use in source and binary forms, with or without modification, ** are permitted provided that the following conditions are met: ** 1. Redistributions of source code must retain the above copyright notice, ** this list of conditions and the following disclaimer. ** 2. Redistributions in binary form must reproduce the above copyright notice, ** this list of conditions and the following disclaimer in the documentation ** and/or other materials provided with the distribution. ** 3. Neither the name of Ac6 nor the names of its contributors ** may be used to endorse or promote products derived from this software ** without specific prior written permission. ** ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" ** AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE ** IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE ** DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE ** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL ** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR ** SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER ** CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, ** OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ** ***************************************************************************** */ /* Entry Point */ ENTRY(Reset_Handler) /* Highest address of the user mode stack */ _estack = 0x20005000; /* end of 20K RAM */ _Min_Heap_Size = 0; /* required amount of heap */ _Min_Stack_Size = 0x100; /* required amount of stack */ /* Memories definition */ MEMORY { RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K ROM (rx) : ORIGIN = 0x8000000, LENGTH = 64K } /* Sections */ SECTIONS { /* The startup code into ROM memory */ .isr_vector : { . = ALIGN(4); KEEP(*(.isr_vector)) /* Startup code */ . = ALIGN(4); } >ROM /* The program code and other data into ROM memory */ .text : { . = ALIGN(4); *(.text) /* .text sections (code) */ *(.text*) /* .text* sections (code) */ *(.glue_7) /* glue arm to thumb code */ *(.glue_7t) /* glue thumb to arm code */ *(.eh_frame) KEEP (*(.init)) KEEP (*(.fini)) . = ALIGN(4); _etext = .; /* define a global symbols at end of code */ } >ROM /* Constant data into ROM memory*/ .rodata : { . = ALIGN(4); *(.rodata) /* .rodata sections (constants, strings, etc.) */ *(.rodata*) /* .rodata* sections (constants, strings, etc.) */ . = ALIGN(4); } >ROM .ARM.extab : { . = ALIGN(4); *(.ARM.extab* .gnu.linkonce.armextab.*) . = ALIGN(4); } >ROM .ARM : { . = ALIGN(4); __exidx_start = .; *(.ARM.exidx*) __exidx_end = .; . = ALIGN(4); } >ROM .preinit_array : { . = ALIGN(4); PROVIDE_HIDDEN (__preinit_array_start = .); KEEP (*(.preinit_array*)) PROVIDE_HIDDEN (__preinit_array_end = .); . = ALIGN(4); } >ROM .init_array : { . = ALIGN(4); PROVIDE_HIDDEN (__init_array_start = .); KEEP (*(SORT(.init_array.*))) KEEP (*(.init_array*)) PROVIDE_HIDDEN (__init_array_end = .); . = ALIGN(4); } >ROM .fini_array : { . = ALIGN(4); PROVIDE_HIDDEN (__fini_array_start = .); KEEP (*(SORT(.fini_array.*))) KEEP (*(.fini_array*)) PROVIDE_HIDDEN (__fini_array_end = .); . = ALIGN(4); } >ROM /* Used by the startup to initialize data */ _sidata = LOADADDR(.data); /* Initialized data sections into RAM memory */ .data : { . = ALIGN(4); _sdata = .; /* create a global symbol at data start */ *(.data) /* .data sections */ *(.data*) /* .data* sections */ . = ALIGN(4); _edata = .; /* define a global symbol at data end */ } >RAM AT> ROM /* Uninitialized data section into RAM memory */ . = ALIGN(4); .bss : { /* This is used by the startup in order to initialize the .bss secion */ _sbss = .; /* define a global symbol at bss start */ __bss_start__ = _sbss; *(.bss) *(.bss*) *(COMMON) . = ALIGN(4); _ebss = .; /* define a global symbol at bss end */ __bss_end__ = _ebss; } >RAM /* User_heap_stack section, used to check that there is enough RAM left */ ._user_heap_stack : { . = ALIGN(8); PROVIDE ( end = . ); PROVIDE ( _end = . ); . = . + _Min_Heap_Size; . = . + _Min_Stack_Size; . = ALIGN(8); } >RAM /* Remove information from the compiler libraries*/ /DISCARD/ : { libc.a ( * ) libm.a ( * ) libgcc.a ( * ) } .ARM.attributes 0 : { *(.ARM.attributes) } }
Модуль "main.c" теперь будет выглядеть так:
#include "stm32f10x.h" #include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h" #define WAIT 600000 void dummy_loop(volatile uint32_t count) { while (--count); } int main() { // Enable GPIOC clock RCC->APB2ENR |= RCC_APB2Periph_GPIOC; // Configure PC13 as push-pull output (max speed 2 MHz) GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13); GPIOC->CRH |= GPIO_CRH_MODE13_1; // Output mode, max speed 2 MHz (0b10) // Super loop to toggle PC13 while (1) { GPIOC->BSRR = GPIO_Pin_13; // Set PC13 dummy_loop(WAIT); GPIOC->BRR = GPIO_Pin_13; // Reset PC13 dummy_loop(WAIT); } }
Итак, имеются файлы: "main.c", "startup.s" и "stm32f10xx8.ld". Теперь это все вместе надо собрать в единую прошивку. Сделать это можно разными способами.
Одной командой это делается довольно просто:
$ arm-none-eabi-gcc -T stm32f10xx8.ld -nostartfiles -mcpu=cortex-m3 -g -Os -mthumb -DSTM32F10X_MD -I ./CMSIS/device -I ./CMSIS/core -I ./SPL/inc -o firmware.elf main.c ./asm/startup.s s arm-none-eabi-objcopy -O binary firmware.elf firmware.bin $ arm-none-eabi-size ./firmware.elf text data bss dec hex filename 480 0 256 736 2e0 ./firmware.elf
Прошивка весит 480 байт, bss область в прошивку не входит, она располагается в оперативной памяти. Проверяем таблицу векторов:
$ arm-none-eabi-objdump -s -j .isr_vector firmware.elf
firmware.elf: file format elf32-littlearm
Contents of section .isr_vector:
8000000 00500020 95010008 dd010008 dd010008 .P. ............
8000010 dd010008 dd010008 dd010008 00000000 ................
8000020 00000000 00000000 00000000 dd010008 ................
8000030 dd010008 00000000 dd010008 dd010008 ................
8000040 dd010008 dd010008 dd010008 dd010008 ................
8000050 dd010008 dd010008 dd010008 dd010008 ................
8000060 dd010008 dd010008 dd010008 dd010008 ................
8000070 dd010008 dd010008 dd010008 dd010008 ................
8000080 dd010008 dd010008 dd010008 dd010008 ................
8000090 dd010008 dd010008 dd010008 dd010008 ................
80000a0 dd010008 dd010008 dd010008 dd010008 ................
80000b0 dd010008 dd010008 dd010008 dd010008 ................
80000c0 dd010008 dd010008 dd010008 dd010008 ................
80000d0 dd010008 dd010008 dd010008 dd010008 ................
80000e0 dd010008 dd010008 00000000 dd010008 ................
80000f0 dd010008 dd010008 dd010008 dd010008 ................
8000100 dd010008 dd010008 dd010008 dd010008 ................
8000110 dd010008 dd010008 dd010008 dd010008 ................
8000120 dd010008 dd010008 dd010008 dd010008 ................
0x130 - это 304 байта, на этот раз полная таблица векторов.
Для того, чтобы составлять скрипты сборки проекта, сперва следует уточнить структуру каталогов проекта. Я привык использовать следующую структуру проекта:
project/ ├── main.c # в корне всегда файл "main.c" ├── Makefile ├── stm32f10xx8.ld ├── inc/ │ └── (заголовочные файлы) ├── src/ │ └── (остальные Си-файлы проекта) └── asm/ └── (ассемблерные файлы проекта)
Самый простой Makefile для такого проекта у меня вышел следующим:
# Toolchain setup CC = arm-none-eabi-gcc AS = arm-none-eabi-gcc OBJCOPY = arm-none-eabi-objcopy SIZE = arm-none-eabi-size RM = rm -f # Target and output names TARGET = firmware BIN = $(TARGET).bin HEX = $(TARGET).hex ELF = $(TARGET).elf # Directory structure BUILD_DIR = build SRC_DIR = src ASM_DIR = asm INC_DIR = inc # Source files SRCS_C = main.c SRCS_C += $(wildcard $(SRC_DIR)/*.c) SRCS_S = $(wildcard $(ASM_DIR)/*.s) OBJS += $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.o,$(SRCS_C)) \ $(patsubst $(ASM_DIR)/%.s,$(BUILD_DIR)/%.o,$(SRCS_S)) # Include paths INC = -I$(INC_DIR) -ICMSIS/device -ICMSIS/core -ISPL/inc DEF = -DSTM32F10X_MD # MCU and flags MCU = cortex-m3 CFLAGS = -mcpu=$(MCU) -mthumb -Wall -g -Os -ffunction-sections -fdata-sections $(INC) $(DEF) ASFLAGS = -mcpu=$(MCU) -mthumb -g LDFLAGS = -T stm32f10xx8.ld -nostartfiles -Wl,--gc-sections -specs=nano.specs # Build rules all: $(BUILD_DIR)/$(BIN) $(BUILD_DIR)/$(HEX) size $(BUILD_DIR)/$(ELF): $(OBJS) @mkdir -p $(@D) $(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS) # Rule for C files $(BUILD_DIR)/%.o: $(SRC_DIR)/%.c @mkdir -p $(@D) $(CC) -c $(CFLAGS) $< -o $@ # Rule for assembly files $(BUILD_DIR)/%.o: $(ASM_DIR)/%.s @mkdir -p $(@D) $(AS) -c $(ASFLAGS) $< -o $@ $(BUILD_DIR)/$(BIN): $(BUILD_DIR)/$(ELF) $(OBJCOPY) -O binary $< $@ $(BUILD_DIR)/$(HEX): $(BUILD_DIR)/$(ELF) $(OBJCOPY) -O ihex $< $@ size: $(BUILD_DIR)/$(ELF) $(SIZE) --format=berkeley $< flash: $(BUILD_DIR)/$(BIN) st-flash write $< 0x08000000 clean: $(RM) -r $(BUILD_DIR) .PHONY: all size flash clean
Сборка:
$ make arm-none-eabi-gcc -c -mcpu=cortex-m3 -mthumb -g asm/startup.s -o build/startup.o arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -Wall -g -Os -ffunction-sections -fdata-sections -Iinc -ICMSIS/device -ICMSIS/core -ISPL/inc -DSTM32F10X_MD main.c build/startup.o -o build/firmware.elf -T stm32f10xx8.ld -nostartfiles -Wl,--gc-sections -specs=nano.specs arm-none-eabi-objcopy -O binary build/firmware.elf build/firmware.bin arm-none-eabi-objcopy -O ihex build/firmware.elf build/firmware.hex arm-none-eabi-size --format=berkeley build/firmware.elf text data bss dec hex filename 460 0 256 716 2cc build/firmware.elf
Здесь довольно странная конструкция получилась, когда смешана компиляция и компоновка. Я бы хотел, что бы компиляция и компоновка были разделены.
В конечном итоге, я остановился на следующем варианте Makefile:
# Toolchain setup CC = arm-none-eabi-gcc AS = arm-none-eabi-gcc OBJCOPY = arm-none-eabi-objcopy SIZE = arm-none-eabi-size RM = rm -f # Target and output names TARGET = firmware BIN = $(TARGET).bin HEX = $(TARGET).hex ELF = $(TARGET).elf # Directory structure BUILD_DIR = build SRC_DIR = src ASM_DIR = asm INC_DIR = inc PROJECT_ROOT = . # Source files SRCS_C = $(wildcard $(SRC_DIR)/*.c) $(wildcard $(PROJECT_ROOT)/main.c) SRCS_S = $(wildcard $(ASM_DIR)/*.s) OBJS = $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.o,$(filter $(SRC_DIR)/%,$(SRCS_C))) \ $(patsubst $(PROJECT_ROOT)/%.c,$(BUILD_DIR)/%.o,$(filter $(PROJECT_ROOT)/%,$(SRCS_C))) \ $(patsubst $(ASM_DIR)/%.s,$(BUILD_DIR)/%.o,$(SRCS_S)) # Include paths INC = -I$(INC_DIR) -I$(PROJECT_ROOT) -ICMSIS/device -ICMSIS/core -ISPL/inc DEF = -DSTM32F10X_MD # MCU and flags MCU = cortex-m3 CFLAGS = -mcpu=$(MCU) -mthumb -Wall -g -Os -ffunction-sections -fdata-sections $(INC) $(DEF) ASFLAGS = -mcpu=$(MCU) -mthumb -g LDFLAGS = -T stm32f10xx8.ld -nostartfiles -Wl,--gc-sections -specs=nano.specs # Build rules all: $(BUILD_DIR)/$(BIN) $(BUILD_DIR)/$(HEX) size $(BUILD_DIR)/$(ELF): $(OBJS) @mkdir -p $(@D) $(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS) # Rule for C files in src directory $(BUILD_DIR)/%.o: $(SRC_DIR)/%.c @mkdir -p $(@D) $(CC) -c $(CFLAGS) $< -o $@ # Rule for main.c in project root $(BUILD_DIR)/%.o: $(PROJECT_ROOT)/%.c @mkdir -p $(@D) $(CC) -c $(CFLAGS) $< -o $@ # Rule for assembly files $(BUILD_DIR)/%.o: $(ASM_DIR)/%.s @mkdir -p $(@D) $(AS) -c $(ASFLAGS) $< -o $@ $(BUILD_DIR)/$(BIN): $(BUILD_DIR)/$(ELF) $(OBJCOPY) -O binary $< $@ $(BUILD_DIR)/$(HEX): $(BUILD_DIR)/$(ELF) $(OBJCOPY) -O ihex $< $@ size: $(BUILD_DIR)/$(ELF) $(SIZE) --format=berkeley $< flash: $(BUILD_DIR)/$(BIN) st-flash write $< 0x08000000 clean: $(RM) -r $(BUILD_DIR) .PHONY: all size flash clean
Makefile будет примерно в два раза меньше, если все исходники хранить в "src" каталоге, но как я говорил, я привык именно к такой структуре проекта:
project/ ├── main.c # в корне всегда файл "main.c" ├── Makefile ├── stm32f10xx8.ld ├── inc/ │ └── (заголовочные файлы) ├── src/ │ └── (остальные Си-файлы проекта) └── asm/ └── (ассемблерные файлы проекта)
Сборка проекта:
$ make clean && make
rm -f -r build
arm-none-eabi-gcc -c -mcpu=cortex-m3 -mthumb -g asm/startup.s -o build/startup.o
arm-none-eabi-gcc -c -mcpu=cortex-m3 -mthumb -Wall -g -Os -ffunction-sections -fdata-sections -Iinc -I. -ICMSIS/device -ICMSIS/core -ISPL/inc -DSTM32F10X_MD main.c -o build/main.o
arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -Wall -g -Os -ffunction-sections -fdata-sections -Iinc -I. -ICMSIS/device -ICMSIS/core -ISPL/inc -DSTM32F10X_MD build/main.o build/startup.o -o build/firmware.elf -T stm32f10xx8.ld -nostartfiles -Wl,--gc-sections -specs=nano.specs
arm-none-eabi-objcopy -O binary build/firmware.elf build/firmware.bin
arm-none-eabi-objcopy -O ihex build/firmware.elf build/firmware.hex
arm-none-eabi-size --format=berkeley build/firmware.elf
text data bss dec hex filename
460 0 256 716 2cc build/firmware.elf
В этот раз все нормально, все модули собираются по отдельности, после чего происходит компоновка.
CMakeLists.txt для сборки проекта посредством CMake:
cmake_minimum_required(VERSION 3.12) project(firmware LANGUAGES C ASM) set(CMAKE_VERBOSE_MAKEFILE OFF) # Toolchain setup set(CMAKE_SYSTEM_NAME Generic) set(CMAKE_SYSTEM_PROCESSOR arm) set(CMAKE_C_COMPILER arm-none-eabi-gcc) set(CMAKE_ASM_COMPILER arm-none-eabi-gcc) set(CMAKE_OBJCOPY arm-none-eabi-objcopy) set(CMAKE_SIZE arm-none-eabi-size) # Target and output names set(TARGET firmware) set(ELF ${TARGET}.elf) set(BIN ${TARGET}.bin) set(HEX ${TARGET}.hex) # Directory structure set(BUILD_DIR ${CMAKE_BINARY_DIR}) set(SRC_DIR src) set(ASM_DIR asm) set(INC_DIR inc) # Source files file(GLOB SRCS_C ${SRC_DIR}/*.c ${CMAKE_SOURCE_DIR}/main.c ) file(GLOB SRCS_S ${ASM_DIR}/*.s) # Include paths set(INC ${INC_DIR} CMSIS/device CMSIS/core SPL/inc ) # Compiler definitions add_definitions(-DSTM32F10X_MD) # MCU and flags set(MCU cortex-m3) set(CWARN "-Wall") set(CMAKE_C_FLAGS "-mcpu=${MCU} -mthumb ${CWARN} -g -Os -ffunction-sections -fdata-sections") set(CMAKE_ASM_FLAGS "-mcpu=${MCU} -mthumb -g") set(LINKER_SCRIPT "${CMAKE_SOURCE_DIR}/stm32f10xx8.ld") set(CMAKE_EXE_LINKER_FLAGS "-nostartfiles -specs=nano.specs -T${LINKER_SCRIPT} -Wl,--gc-sections") # Create executable add_executable(${ELF} ${SRCS_C} ${SRCS_S}) target_include_directories(${ELF} PRIVATE ${INC}) # Generate binary and hex files add_custom_command(TARGET ${ELF} POST_BUILD COMMAND ${CMAKE_OBJCOPY} -O binary ${ELF} ${BIN} COMMAND ${CMAKE_OBJCOPY} -O ihex ${ELF} ${HEX} COMMENT "Generating ${BIN} and ${HEX}" ) # Size target add_custom_target(size COMMAND ${CMAKE_SIZE} --format=berkeley ${ELF} DEPENDS ${ELF} ) # Flash target add_custom_target(flash COMMAND st-flash write ${BIN} 0x08000000 DEPENDS ${BIN} ) # Install target (optional) install(FILES ${BIN} DESTINATION bin)
Сборка проекта посредством CMake:
$ mkdir cbuild && cd $_ $ cmake .. $ make && make size
Прошивка:
$ make flash
Под спойлером выкладываю скрипт для сборки проекта посредством Qbs:
показать Qbs-скриптimport qbs Product { type: ["application","flash"] Depends { name: "cpp" } name: "firmware" property string Home: sourceDirectory property string Inc: Home + "/inc" property string Asm: Home + "/asm" cpp.positionIndependentCode: false cpp.executableSuffix: ".elf" cpp.includePaths:[ "CMSIS/device", "CMSIS/core", "SPL/inc", "inc", ] cpp.driverFlags: [ "-nostartfiles", "-specs=nano.specs", ] cpp.linkerFlags: [ "--gc-sections", "-T" + sourceDirectory + "/stm32f10xx8.ld", ] cpp.cFlags: [ "-mthumb", "-mcpu=cortex-m3", "-Wall", ] cpp.defines: [ "STM32F10X_MD", ] Properties { condition: qbs.buildVariant === "debug" cpp.defines: outer.concat(["DEBUG=1"]) cpp.debugInformation: true cpp.optimization: "small" cpp.cFlags: outer.concat(["-ggdb"]) } Properties { condition: qbs.buildVariant === "release" cpp.debugInformation: false cpp.optimization: "small" } Group { name: "Headers" files: [ Inc + "/*.h", ] fileTags: ["h_src"] } Group { name: "Assembly" fileTags: ["asm"] files: [ Asm + "/*.s", ] } files: [ "main.c", ] Rule { inputs: ["application"] Artifact { filePath: product.name + ".bin" fileTags: "flash" } prepare: { var size_name=input.filePath var argsObjcopy = ["-O", "binary", input.filePath, output.filePath] var cmd_bin=new Command("arm-none-eabi-objcopy",argsObjcopy) var cmd_size=new Command("arm-none-eabi-size",size_name) cmd_bin.description = "Generating binary: " + output.fileName cmd_size.description = "Size of " + output.fileName return [cmd_bin,cmd_size] } } }
Сборка с помощь команды:
$ qbs build config:release profile:arm-none-eabi-gcc-10_3 --command-echo-mode command-line
Build graph does not yet exist for configuration 'release'. Starting from scratch.
Resolving project for configuration release
WARNING: Could not detect target platform ('linux' given)
Setting up build graph for configuration release
Building for configuration release
/usr/local/bin/arm-none-eabi-as -I/home/flanker/qtcreator_projects/00_blink_148/CMSIS/device -I/home/flanker/qtcreator_projects/00_blink_148/CMSIS/core -I/home/flanker/qtcreator_projects/00_blink_148/SPL/inc -I/home/flanker/qtcreator_projects/00_blink_148/inc -o /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/2445be8241aabd34/startup.s.o /home/flanker/qtcreator_projects/00_blink_148/asm/startup.s
/usr/local/bin/arm-none-eabi-gcc -Os -Wall -Wextra -nostartfiles -specs=nano.specs -pipe -fvisibility=default -mthumb -mcpu=cortex-m3 -Wall -DNDEBUG -DSTM32F10X_MD -I/home/flanker/qtcreator_projects/00_blink_148/CMSIS/device -I/home/flanker/qtcreator_projects/00_blink_148/CMSIS/core -I/home/flanker/qtcreator_projects/00_blink_148/SPL/inc -I/home/flanker/qtcreator_projects/00_blink_148/inc -o /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/3a52ce780950d4d9/main.c.o -c /home/flanker/qtcreator_projects/00_blink_148/main.c
/usr/local/bin/arm-none-eabi-gcc -Wl,--gc-sections,-T/home/flanker/qtcreator_projects/00_blink_148/stm32f10xx8.ld -nostartfiles -specs=nano.specs -o /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/firmware.elf /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/2445be8241aabd34/startup.s.o /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/3a52ce780950d4d9/main.c.o
/usr/local/bin/arm-none-eabi-objcopy -O binary /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/firmware.elf /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/firmware.bin
/usr/local/bin/arm-none-eabi-size /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/firmware.elf
Build done for configuration release.
/usr/local/bin/arm-none-eabi-size /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/firmware.elf
text data bss dec hex filename
460 0 256 716 2cc /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/firmware.elf
После добавления таблицы прерываний необходимо настроить систему тактирования. Для микроконтроллера STM32F103C8 она выглядит следующим образом:
Вообще-то, в шаблонном проекте TrueStudio у нас уже была настроена система тактирования RCC, хоть мы к этому и не приложили ни капли усилий. С помощью добрых глаз и дизассемблера, мне удалось выяснить, что функции настройки RCC содержатся в файле system_stm32f10x.c:
Настройка системы тактирования начинается с вызова функции SystemInit(). В каталоге проекта "src", откроем новый файл system_init.c. В этот файл мы и скопируем функцию SystemInit():
#include "stm32f10x.h" #include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h" void SystemInit (void) { RCC->CR |= (uint32_t)0x00000001; // enable HSI, 8 MHz /* Reset SW, HPRE, PPRE1, PPRE2, ADCPRE and MCO bits */ #ifndef STM32F10X_CL RCC->CFGR &= (uint32_t)0xF8FF0000; #else RCC->CFGR &= (uint32_t)0xF0FF0000; #endif /* STM32F10X_CL */ /* Reset HSEON, CSSON and PLLON bits */ RCC->CR &= (uint32_t)0xFEF6FFFF; /* Reset HSEBYP bit */ RCC->CR &= (uint32_t)0xFFFBFFFF; /* Reset PLLSRC, PLLXTPRE, PLLMUL and USBPRE/OTGFSPRE bits */ RCC->CFGR &= (uint32_t)0xFF80FFFF; #ifdef STM32F10X_CL /* Reset PLL2ON and PLL3ON bits */ RCC->CR &= (uint32_t)0xEBFFFFFF; /* Disable all interrupts and clear pending bits */ RCC->CIR = 0x00FF0000; /* Reset CFGR2 register */ RCC->CFGR2 = 0x00000000; #elif defined (STM32F10X_LD_VL) || defined (STM32F10X_MD_VL) || (defined STM32F10X_HD_VL) /* Disable all interrupts and clear pending bits */ RCC->CIR = 0x009F0000; /* Reset CFGR2 register */ RCC->CFGR2 = 0x00000000; #else /* Disable all interrupts and clear pending bits */ RCC->CIR = 0x009F0000; #endif /* STM32F10X_CL */ #if defined (STM32F10X_HD) || (defined STM32F10X_XL) || (defined STM32F10X_HD_VL) #ifdef DATA_IN_ExtSRAM SystemInit_ExtMemCtl(); #endif /* DATA_IN_ExtSRAM */ #endif /* Configure the System clock frequency, HCLK, PCLK2 and PCLK1 prescalers */ /* Configure the Flash Latency cycles and enable prefetch buffer */ // SetSysClock(); #ifdef SYSCLK_FREQ_HSE SetSysClockToHSE(); #elif defined SYSCLK_FREQ_24MHz SetSysClockTo24(); #elif defined SYSCLK_FREQ_36MHz SetSysClockTo36(); #elif defined SYSCLK_FREQ_48MHz SetSysClockTo48(); #elif defined SYSCLK_FREQ_56MHz SetSysClockTo56(); #elif defined SYSCLK_FREQ_72MHz SetSysClockTo72(); #endif #ifdef VECT_TAB_SRAM SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM. */ #else // SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH. */ #endif }
Данная функция сбрасывает систему тактирования, т.е. делает то, что и так должно происходить после RESET. В завершении работы, в зависимости от флагов компиляции, включается вызов функций для настройки тактирования.
Мы будем использовать другую функцию: void SetSysClockTo72(). Эта функция сначала переключается на внешний кварц, затем запускает PLL-генератор, после его стабилизации устанавливается PLL-множитель на 9, в результате чего частота SYSCLK устанавливается в значение 72 MHz. Далее устанавливаются делители: для AHB и APB2 шин по единице, для APB1 шины равной двум, т.е. на 36МГц. Кроме того для флеш-памяти устанавливается WaitState равный двум, т.е. флеш-память работает на меньшей частоте чем процессор. Но давайте по порядку.
1. В начале запускается внешний кварц:
/* SYSCLK, HCLK, PCLK2 and PCLK1 configuration ---------------------------*/ /* Enable HSE */ RCC->CR |= ((uint32_t)RCC_CR_HSEON); /* Wait till HSE is ready and if Time out is reached exit */ do { HSEStatus = RCC->CR & RCC_CR_HSERDY; StartUpCounter++; } while((HSEStatus == 0) && (StartUpCounter != HSE_STARTUP_TIMEOUT));
Ниже приведено описание конфигурационного регистра RCC_CR:
Здесь HSION - включает/выключает внутренний 8 МГц генератор. Внутренний генератор невозможно выключить когда от него тактируется SYSCLK. Этот бит аппаратно устанавливается когда микроконтроллер выходит из Stop и Standby режимов или когда перестают поступать сигналы с внешнего генератора. HDIRDY - флаг готовности HSI генератора. HSITRIM и HSICAL - отвечают за подстройку частоты HSI.
HSEON - включает/выключает генератор работающий от внешнего кварца. Не может быть выключен когда от него тактируется SYSCLK. Аппаратно очищается при переходе микроконтроллера в Stop и Standby режимы. HSERDY - флаг готовности генератора. НSEBYP - разрешает работу от внешнего генератора (не путать с кварцем!). Внешний генератор должен быть в диапазоне 4-16 МГц. HSEBYP не может быть установлен если запущен HSE. HSEBYP должен устанавливаться вместе с HSEON битом.
СSSON - включает систему безопасности тактирования. PLLON - включает PLL генератор. Очищается аппаратно при переходе микроконтроллера в Stop и Standby режимы. PLLON не может быть очищен когда используется для тактирования SYSCLK. PLLRDY флаг готовности PLL-генератора.
Т.о. сначала включается HSE генератор, после чего некоторое время ожидается пока он стабилизируется.
Идем далее:
/* Enable Prefetch Buffer */ FLASH->ACR |= FLASH_ACR_PRFTBE; /* Flash 2 wait state */ FLASH->ACR &= (uint32_t)((uint32_t)~FLASH_ACR_LATENCY); FLASH->ACR |= (uint32_t)FLASH_ACR_LATENCY_2;
Здесь включается буфер превыборки и устанавливается время обращения к флеш-памяти. Описание регистра FLASH_ACR приведено ниже:
Здесь Latency задает задержку при обращении к флеш-памяти. Для частоты SYSCLK=72MHz, следует ставить задержку в два такта. HLFCYA разрешает обращение к 16-битным данным, что помогает улучшить быстродействие при частотах 8 MHz и ниже. Его нельзя использовать совместно с PLL. PRFTBE включает буфер превыборки команд, PRFTBS - флаг буфера превыборки.
/* HCLK = SYSCLK */ RCC->CFGR |= (uint32_t)RCC_CFGR_HPRE_DIV1; /* PCLK2 = HCLK */ RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE2_DIV1; /* PCLK1 = HCLK */ RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE1_DIV2;
Здесь на шины AHB и APB2 устанавливаются предделители равные единице. На шину APB1 устанавливается предделитель равный двум.
Здесь SW переключает генератор тактирования SYSCLK. SWS - флаг завершения переключения на новый генератор SYSCLK. HPRE - устанавливает предделитель на AHB шину. PPRE1 устанавливает предделитель на шину APB1. PPRE2 устанавливает предделитель на шину APB2. ADCPRE устанавливает предделитель на АЦП. PLLSRC - выбирает источник опорного тактирования: HSI или HSE. PLLXTPRE - устанавливает предделитель на PLL. PLLMUL - устанавливает множитель на PLL. USBPRE - устанавливает предделитель на USB шину. MCO - выбирает источник выходного тактового сигнала.
Т.о. в следующем коде выбирается HSE в качестве опорного источника тактирования, и устанавливается множитель равный девяти. Т.к. на плате "Blue Pill" установлен кварц на 8 MHz, то умножив это число на 9, получим в итоге частоту SYSCLK = 72 MHz.
/* PLL configuration: PLLCLK = HSE * 9 = 72 MHz */ RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_PLLSRC | RCC_CFGR_PLLXTPRE | RCC_CFGR_PLLMULL)); RCC->CFGR |= (uint32_t)(RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMULL9);
Далее включается PLL и ожидается его готовность:
/* Enable PLL */ RCC->CR |= RCC_CR_PLLON; /* Wait till PLL is ready */ while((RCC->CR & RCC_CR_PLLRDY) == 0) { }
В завершении, тактирование SYSCLK переключается на PLL:
/* Select PLL as system clock source */ RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_SW)); RCC->CFGR |= (uint32_t)RCC_CFGR_SW_PLL; /* Wait till PLL is used as system clock source */ while ((RCC->CFGR & (uint32_t)RCC_CFGR_SWS) != (uint32_t)0x08) { }
Во всем этом хозяйстве не хватает кода включения CSS и генератора низкочастотного генератора LSE, но для начала наверно сойдет. Полностью код файла startup.c можно посмотреть под спойлером, или в архиве в конце статьи.
показать startup.c#include "stm32f10x.h" #include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h" #ifdef SYSCLK_FREQ_HSE /** * @brief Selects HSE as System clock source and configure HCLK, PCLK2 * and PCLK1 prescalers. * @note This function should be used only after reset. * @param None * @retval None */ static void SetSysClockToHSE(void) { __IO uint32_t StartUpCounter = 0, HSEStatus = 0; /* SYSCLK, HCLK, PCLK2 and PCLK1 configuration ---------------------------*/ /* Enable HSE */ RCC->CR |= ((uint32_t)RCC_CR_HSEON); /* Wait till HSE is ready and if Time out is reached exit */ do { HSEStatus = RCC->CR & RCC_CR_HSERDY; StartUpCounter++; } while((HSEStatus == 0) && (StartUpCounter != HSE_STARTUP_TIMEOUT)); if ((RCC->CR & RCC_CR_HSERDY) != RESET) { HSEStatus = (uint32_t)0x01; } else { HSEStatus = (uint32_t)0x00; } if (HSEStatus == (uint32_t)0x01) { #if !defined STM32F10X_LD_VL && !defined STM32F10X_MD_VL && !defined STM32F10X_HD_VL /* Enable Prefetch Buffer */ FLASH->ACR |= FLASH_ACR_PRFTBE; /* Flash 0 wait state */ FLASH->ACR &= (uint32_t)((uint32_t)~FLASH_ACR_LATENCY); #ifndef STM32F10X_CL FLASH->ACR |= (uint32_t)FLASH_ACR_LATENCY_0; #else if (HSE_VALUE <= 24000000) { FLASH->ACR |= (uint32_t)FLASH_ACR_LATENCY_0; } else { FLASH->ACR |= (uint32_t)FLASH_ACR_LATENCY_1; } #endif /* STM32F10X_CL */ #endif /* HCLK = SYSCLK */ RCC->CFGR |= (uint32_t)RCC_CFGR_HPRE_DIV1; /* PCLK2 = HCLK */ RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE2_DIV1; /* PCLK1 = HCLK */ RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE1_DIV1; /* Select HSE as system clock source */ RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_SW)); RCC->CFGR |= (uint32_t)RCC_CFGR_SW_HSE; /* Wait till HSE is used as system clock source */ while ((RCC->CFGR & (uint32_t)RCC_CFGR_SWS) != (uint32_t)0x04) { } } else { /* If HSE fails to start-up, the application will have wrong clock configuration. User can add here some code to deal with this error */ } } #elif defined SYSCLK_FREQ_72MHz /** * @brief Sets System clock frequency to 72MHz and configure HCLK, PCLK2 * and PCLK1 prescalers. * @note This function should be used only after reset. * @param None * @retval None */ static void SetSysClockTo72(void) { /* SYSCLK, HCLK, PCLK2 and PCLK1 configuration ---------------------------*/ /* Enable HSE */ RCC->CR |= ((uint32_t)RCC_CR_HSEON); /* Wait till HSE is ready and if Time out is reached exit */ while (!(RCC->CR & RCC_CR_HSERDY)) /* Enable Prefetch Buffer */ FLASH->ACR |= FLASH_ACR_PRFTBE; /* Flash 2 wait state */ FLASH->ACR &= (uint32_t)((uint32_t)~FLASH_ACR_LATENCY); FLASH->ACR |= (uint32_t)FLASH_ACR_LATENCY_2; /* HCLK = SYSCLK */ RCC->CFGR |= (uint32_t)RCC_CFGR_HPRE_DIV1; /* PCLK2 = HCLK */ RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE2_DIV1; /* PCLK1 = HCLK */ RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE1_DIV2; /* PLL configuration: PLLCLK = HSE * 9 = 72 MHz */ RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_PLLSRC | RCC_CFGR_PLLXTPRE | RCC_CFGR_PLLMULL)); RCC->CFGR |= (uint32_t)(RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMULL9); /* Enable PLL */ RCC->CR |= RCC_CR_PLLON; /* Wait till PLL is ready */ while(!(RCC->CR & RCC_CR_PLLRDY)); /* Select PLL as system clock source */ RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_SW)); RCC->CFGR |= (uint32_t)RCC_CFGR_SW_PLL; /* Wait till PLL is used as system clock source */ while ((RCC->CFGR & (uint32_t)RCC_CFGR_SWS) != (uint32_t)0x08); } #endif void SystemInit (void) { RCC->CR |= (uint32_t)0x00000001; // enable HSI, 8 MHz /* Reset SW, HPRE, PPRE1, PPRE2, ADCPRE and MCO bits */ #ifndef STM32F10X_CL RCC->CFGR &= (uint32_t)0xF8FF0000; #else RCC->CFGR &= (uint32_t)0xF0FF0000; #endif /* STM32F10X_CL */ /* Reset HSEON, CSSON and PLLON bits */ RCC->CR &= (uint32_t)0xFEF6FFFF; /* Reset HSEBYP bit */ RCC->CR &= (uint32_t)0xFFFBFFFF; /* Reset PLLSRC, PLLXTPRE, PLLMUL and USBPRE/OTGFSPRE bits */ RCC->CFGR &= (uint32_t)0xFF80FFFF; #ifdef STM32F10X_CL /* Reset PLL2ON and PLL3ON bits */ RCC->CR &= (uint32_t)0xEBFFFFFF; /* Disable all interrupts and clear pending bits */ RCC->CIR = 0x00FF0000; /* Reset CFGR2 register */ RCC->CFGR2 = 0x00000000; #elif defined (STM32F10X_LD_VL) || defined (STM32F10X_MD_VL) || (defined STM32F10X_HD_VL) /* Disable all interrupts and clear pending bits */ RCC->CIR = 0x009F0000; /* Reset CFGR2 register */ RCC->CFGR2 = 0x00000000; #else /* Disable all interrupts and clear pending bits */ RCC->CIR = 0x009F0000; #endif /* STM32F10X_CL */ #if defined (STM32F10X_HD) || (defined STM32F10X_XL) || (defined STM32F10X_HD_VL) #ifdef DATA_IN_ExtSRAM SystemInit_ExtMemCtl(); #endif /* DATA_IN_ExtSRAM */ #endif /* Configure the System clock frequency, HCLK, PCLK2 and PCLK1 prescalers */ /* Configure the Flash Latency cycles and enable prefetch buffer */ // SetSysClock(); #ifdef SYSCLK_FREQ_HSE SetSysClockToHSE(); #elif defined SYSCLK_FREQ_24MHz SetSysClockTo24(); #elif defined SYSCLK_FREQ_36MHz SetSysClockTo36(); #elif defined SYSCLK_FREQ_48MHz SetSysClockTo48(); #elif defined SYSCLK_FREQ_56MHz SetSysClockTo56(); #elif defined SYSCLK_FREQ_72MHz SetSysClockTo72(); #endif #ifdef VECT_TAB_SRAM SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM. */ #else // SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH. */ #endif }
Данный файл следует разместить в каталоге проекта "src". В обработчике прерывания Reset (файл startup.s) следует раскомментировать вызов функции SystemInit:
bl SystemInitВ Makefile нужно добавить define:
DEF += -DSYSCLK_FREQ_72MHz
Тоже самое в "CMakeLists.txt"
add_definitions(-DSTM32F10X_MD -DSYSCLK_FREQ_72MHz)
Самые большие изменения в Qbs скрипте, там кроме дефйна нужно еще добавить группу для src файлов. Патч:
$ diff -Naur 00_blink_148.qbs ~/mydev/mcu/f103_25/02_blink_project/00_blink_148.qbs --- 00_blink_148.qbs 2025-05-29 09:37:37.000000000 +0400 +++ /home/flanker/mydev/mcu/f103_25/02_blink_project/00_blink_148.qbs 2025-05-27 16:08:04.000000000 +0400 @@ -7,7 +7,6 @@ property string Home: sourceDirectory property string Inc: Home + "/inc" - property string Src: Home + "/src" property string Asm: Home + "/asm" cpp.positionIndependentCode: false @@ -39,7 +38,6 @@ cpp.defines: [ "STM32F10X_MD", - "SYSCLK_FREQ_72MHz", ] Properties @@ -74,13 +72,6 @@ ] } - Group { - name: "Src" - files: [ - Src + "/*.c", - ] - } - files: [ "main.c", ]
Сборка:
$ make
arm-none-eabi-gcc -c -mcpu=cortex-m3 -mthumb -Wall -g -Os -ffunction-sections -fdata-sections -Iinc -I. -ICMSIS/device -ICMSIS/core -ISPL/inc -DSTM32F10X_MD -DSYSCLK_FREQ_72MHz main.c -o build/main.o
arm-none-eabi-gcc -c -mcpu=cortex-m3 -mthumb -Wall -g -Os -ffunction-sections -fdata-sections -Iinc -I. -ICMSIS/device -ICMSIS/core -ISPL/inc -DSTM32F10X_MD -DSYSCLK_FREQ_72MHz src/system_init.c -o build/system_init.o
arm-none-eabi-gcc -c -mcpu=cortex-m3 -mthumb -g asm/startup.s -o build/startup.o
arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -Wall -g -Os -ffunction-sections -fdata-sections -Iinc -I. -ICMSIS/device -ICMSIS/core -ISPL/inc -DSTM32F10X_MD -DSYSCLK_FREQ_72MHz build/system_init.o build/main.o build/startup.o -o build/firmware.elf -T stm32f10xx8.ld -nostartfiles -Wl,--gc-sections -specs=nano.specs
arm-none-eabi-objcopy -O binary build/firmware.elf build/firmware.bin
arm-none-eabi-objcopy -O ihex build/firmware.elf build/firmware.hex
arm-none-eabi-size --format=berkeley build/firmware.elf
text data bss dec hex filename
648 0 256 904 388 build/firmware.elf
Сигнал на логическом анализаторе принял такую форму:
Итого, размер прошивки составил 648 байт, и это самая-самая минимальная программа (на мой взгляд) для микроконтроллера stm32f103xx.
Как известно, в Cortex-M3 нет поддержки чисел с плавающей запятой, она реализуется программно. Будет ли это работать в данном проекте? Чтобы ответить на этот вопрос, добавим операцию с вещественными числами в "main.c"
#include "stm32f10x.h" #include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h" #define WAIT 600000 void dummy_loop(volatile uint32_t count) { while (--count); } int main() { // Enable GPIOC clock RCC->APB2ENR |= RCC_APB2Periph_GPIOC; float a=0.1f; // Configure PC13 as push-pull output (max speed 2 MHz) GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13); GPIOC->CRH |= GPIO_CRH_MODE13_1; // Output mode, max speed 2 MHz (0b10) // Super loop to toggle PC13 while (1) { a += 0.25; GPIOC->BSRR = GPIO_Pin_13; // Set PC13 dummy_loop(WAIT * a); GPIOC->BRR = GPIO_Pin_13; // Reset PC13 dummy_loop(WAIT); } }
После сборки проекта, размер прошивки увеличится до 1656 байт:
$ arm-none-eabi-size --format=berkeley build/firmware.elf
text data bss dec hex filename
1656 0 256 1912 778 build/firmware.elf
Прошиваем и смотрим на частоту мигания светодиода:
Здесь при нижнем уровне светодиод загорается, а при верхнем - гаснет. И как видно по записи с логического анализатора, светодиод с течением времени загорается все реже и реже, что полностью соответствует алгоритму программы.
Теперь, когда мы добрались до желанных до 72 MHz, хочется узнать, какова же реальная производительность такого микроконтроллера. Адепты STM32 при любом случае кричат, что 72 МHz это в несколько раза быстрее чем 16, но про влияние значения waitstate на производительность CPU, я упоминаний как-то не встречал.
Самым простым тестом производительности будет функция задержки на ассемблерных инструкциях. Маркером здесь будет служить число итераций которые микроконтроллер выполняет за 1 мс. В данном случае мы не учитываем тактовую частоту каждого микроконтроллера т.к. это необъективный показатель.
Итак, для реализации функции задержки добавим в ассемблерный файл asm/assembly.s следующий код:
.global delay delay: push {r1} l1: mov.w r1,#10285 @ 72000/7 lp: subs r1,#1 bne lp subs r0,#1 bne l1 pop {r1} bx lr
Тогда main.c примет такой вид:
#include "stm32f10x.h" #include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h" extern void delay(uint32_t ms); int main() { // enable GPIOC port RCC->APB2ENR |= RCC_APB2Periph_GPIOC; // --- GPIO setup ---- GPIOC->CRH &= ~(uint32_t)(0xf<<20); GPIOC->CRH |= (uint32_t)(0x2<<20); for(;;){ GPIOC->ODR ^= GPIO_Pin_13; delay(1000); } }
Как видно, цикл задержки проходит 10285 итераций за 1 мс, т.е. две инструкции выполняются вкупе за 7 тактов.
Так же функция на STM8 выполняется за 5333 итераций
.globl _delay _delay: ldw x, (03,sp) l0: ldw y, #5328 ; ->(16000/3 - 5) l1: decw y jrne l1 decw x jrne l0 ret
Т.о. на первый взгляд реальная производительность STM32 "всего" в два раза выше восьмибитного STM8.
Далее добавлено в 2025г. Для вычисления производительности процессоров составляют сложные бенчмарки, и говорить о производительности на данном примере некорректно.
Вместо того, чтобы добавлять в проект ассемблерный модуль с одной единственной функцией, можно воспользоваться inline ассемблером, и тогда "main.c" примет следующий вид:
#include "stm32f10x.h" #include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h" #define MS_1000 1000 void delay_ms(uint32_t milliseconds) { __asm volatile ( "push {r0-r1}\n" // Save r0,r1 "1:\n" // Outer loop label "mov.w r1, #10285\n" // 72000/7 = 10285.714 ≈ 10285 "2:\n" // Inner loop label "subs r1, #1\n" "bne 2b\n" // Branch back to inner loop (2:) "subs %[milliseconds], #1\n" "bne 1b\n" // Branch back to outer loop (1:) "pop {r0-r1}\n" // Restore r0,r1 : /* No outputs */ : [milliseconds] "r" (milliseconds) // input : "r1", "cc" // clobber list ); } int main() { // Enable GPIOC clock RCC->APB2ENR |= RCC_APB2Periph_GPIOC; // Configure PC13 as push-pull output (max speed 2 MHz) GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13); GPIOC->CRH |= GPIO_CRH_MODE13_1; // Output mode, max speed 2 MHz (0b10) // Super loop to toggle PC13 while (1) { GPIOC->BSRR = GPIO_Pin_13; // Set PC13 delay_ms(MS_1000); GPIOC->BRR = GPIO_Pin_13; // Reset PC13 delay_ms(MS_1000); } }
По логике вещей, компилятор должен сохранять все используемые регистры при вызове такой функции, и "Clobber List" это список регистров которые должны сохраняться компилятором в стеке, но если посмотреть ассемблерный код модуля "main.c", то будет видно, что там ничего подобного не происходит:
void delay_ms(uint32_t milliseconds) { __asm volatile ( 0: b403 push {r0, r1} 2: f642 012d movw r1, #10285 ; 0x282d 6: 3901 subs r1, #1 8: d1fd bne.n 6 <delay_ms+0x6> a: 3801 subs r0, #1 c: d1f9 bne.n 2 <delay_ms+0x2> e: bc03 pop {r0, r1} "pop {r0-r1}\n" // Restore r0,r1 : /* No outputs */ : [milliseconds] "r" (milliseconds) // input : "r1", "cc" // clobber list ); } 10: 4770 bx lr Disassembly of section .text.startup.main: 00000000 <main>: int main() { // Enable GPIOC clock RCC->APB2ENR |= RCC_APB2Periph_GPIOC; 0: 4a0c ldr r2, [pc, #48] ; (34 <main+0x34>) int main() { 2: b508 push {r3, lr} RCC->APB2ENR |= RCC_APB2Periph_GPIOC; 4: 6993 ldr r3, [r2, #24] 6: f043 0310 orr.w r3, r3, #16 a: 6193 str r3, [r2, #24] // Configure PC13 as push-pull output (max speed 2 MHz) GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13); c: 4b0a ldr r3, [pc, #40] ; (38 <main+0x38>) e: 685a ldr r2, [r3, #4] 10: f422 0270 bic.w r2, r2, #15728640 ; 0xf00000 14: 605a str r2, [r3, #4] GPIOC->CRH |= GPIO_CRH_MODE13_1; // Output mode, max speed 2 MHz (0b10) 16: 685a ldr r2, [r3, #4] 18: f442 1200 orr.w r2, r2, #2097152 ; 0x200000 1c: 605a str r2, [r3, #4] // Super loop to toggle PC13 while (1) { GPIOC->BSRR = GPIO_Pin_13; // Set PC13 1e: f44f 5200 mov.w r2, #8192 ; 0x2000 delay_ms(MS_1000); 22: f44f 707a mov.w r0, #1000 ; 0x3e8 GPIOC->BSRR = GPIO_Pin_13; // Set PC13 26: 611a str r2, [r3, #16] delay_ms(MS_1000); 28: f7ff fffe bl 0 <main> // вызов функции delay_ms GPIOC->BRR = GPIO_Pin_13; // Reset PC13 2c: 615a str r2, [r3, #20] delay_ms(MS_1000); 2e: f7ff fffe bl 0 <main> // вызов функции delay_ms while (1) { 32: e7f6 b.n 22 <main+0x22> 34: 40021000 .word 0x40021000 38: 40011000 .word 0x40011000
В 22-й строке имеется инструкция " mov.w r0, #1000" которая заносит в r0 параметр функции, однако при втором вызове функции, этой операции нет. И поэтому обязанность сохранения регистров значение которых меняется во время выполнения функции, ложится на плечи программиста. Напомню, что в данном случае программы была компилирована с ключем оптимизации "-Os".
Давайте посмотрим, насколько точно функция формирует задержки.
Как можно видеть на логическом анализаторе, два вызова функции соответствуют 2000 мс, что очень точный результат.
Однако есть нюанс. Если отключить в обработчике прерывания Reset вызов функции "SystemInit", т.е. перевести микроконтроллер снова на дефолтную частоту 8 МГц, то микроконтроллер не будет работать в 9 раз медленнее. Посмотрите на лог:
Функция "delay_ms(1000)" на частоте 8 МГц, отрабатывает за 3.85 секунды, а не за 9 сек. Потому что на 8 МГц, у микроконтроллера флеш-память работает на частоте процессора, а когда микроконтроллер "разгоняют" до 72 МГц, флеш-память продолжает работать на той же самой низкой частоте, и получается, что "подтормаживает". Т.е. работа микроконтроллера на частоте 72 МГц не означает, что ваша программа будет работать в девять раз быстрее, нежели на дефолтных 8 МГц, всего лишь в четыре раза.
В микроконтроллерах других серий этот эффект постарались исправить, в частности, STM32F030 на своих 48 МГц работает на уровне STM32F103 c частотой 72 МГц. Если STM32F103 за 1мс выполняет 10285 итераций пустого цикла, то STM32F030 на 48МГц совершает 10000 итераций за одну миллисекунду. А микроконтроллер компании Artery AT32F403 выполняет 40000 итераций за 1 миллисекунду на рабочей частоте 200МГц. Техника идет вперед.
Наиболее универсальным способом осуществления задержек, является использование таймера SysTick. Это простой 32-битный счетчик с одним прерыванием, который является частью ядра Cortex-M. Наиболее доходчиво про его конфигурацию можно почитать в статье - " ARM. Учебный Курс. SysTick — Системный таймер | Электроника для всех", либо в фирменной документации - "Cortex-M3 Technical Reference Manual", глава 8.2.2.
Чтобы добавить работу с таймером SysTick в проект, я предлагаю добавить в корневой каталог проекта три файла. Два заголовочных "inc/main.h", "inc/utils.h" и один с исходным кодом: "src/utils.c". В файлы сборки проекта ничего добавлять не надо, всё подхватится автоматически.
В "main.h" пока скинем скинем именованные константы:
#ifndef __MAIN_H #define __MAIN_H #define MS_1000 1000 #define MS_100 100 #endif // __MAIN_H
В "utils.h" будет декларация функций модуля "utils.c":
#ifndef __UTILS_H #define __UTILS_H #include "stm32f10x.h" void systick_init(void); void delay_ms_asm(uint32_t milliseconds); void delay_ms(uint32_t ms); #endif // __UTILS_H
В "utils.c" перенесем функцию задержки из "main.c" и добавим код для работы с таймером SysTick:
#include "utils.h" volatile uint32_t systick_counter = 0; void SysTick_Handler(void) { systick_counter++; } void systick_init(void) { systick_counter=0; // Configure SysTick for 1ms interrupts if (SysTick_Config(SystemCoreClock / 1000)) { // Error handling while (1); } } uint32_t get_ticks(void) { return systick_counter; } void delay_ms(uint32_t ms) { uint32_t start = get_ticks(); while ((get_ticks() - start) < ms) { __WFI(); // Wait for interrupt (power saving) } } void delay_ms_asm(uint32_t milliseconds) { __asm volatile ( "push {r0-r1}\n" // Save r0,r1 "1:\n" // Outer loop label "mov.w r1, #10285\n" // 72000/7 = 10285.714 ≈ 10285 "2:\n" // Inner loop label "subs r1, #1\n" "bne 2b\n" // Branch back to inner loop (2:) "subs %[milliseconds], #1\n" "bne 1b\n" // Branch back to outer loop (1:) "pop {r0-r1}\n" // Restore r0,r1 : /* No outputs */ : [milliseconds] "r" (milliseconds) // input : "r1", "cc" // clobber list ); }
Здесь "SysTick_Handler" - обработчик прерывания таймера, содержит инкрементный счетчик. "get_ticks()" возвращает значение счетчика, "systick_init()" настраивает таймер на частоту 1 кГц и запускает его, а "delay_ms()" - реализация задержки.
Работа таймер SysTick требует включения работы прерываний. Поэтому модуль "main.c" примет следующий вид:
#include "stm32f10x.h" #include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h" #include "main.h" #include "utils.h" uint32_t SystemCoreClock = 72000000; // Clock STM32F103 at reset int main() { // Enable GPIOC clock RCC->APB2ENR |= RCC_APB2Periph_GPIOC; // Configure PC13 as push-pull output (max speed 2 MHz) GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13); GPIOC->CRH |= GPIO_CRH_MODE13_1; // Output mode, max speed 2 MHz (0b10) // setup SysTick systick_init(); // Let's go.. __enable_irq(); // Super loop to toggle PC13 while (1) { GPIOC->BSRR = GPIO_Pin_13; // Set PC13 delay_ms(MS_1000); GPIOC->BRR = GPIO_Pin_13; // Reset PC13 delay_ms(MS_1000); } }
В отладочном модуле Cortex-M имеется 32-битный таймер который работает на частоте процессора. С его помощью можно реализовать задержку измеряемую в микросекундах. Он описан в TRM "Cortex-M3 Technical Reference Manual", глава 11.5.
В файл "utils.c" добавим две функции:
bool isDWT_Init=false; void DWT_Init(void) { if ((CoreDebug->DEMCR & CoreDebug_DEMCR_TRCENA_Msk) == 0) { CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT_CYCCNT = 0; DWT_CTRL |= 1U; // Enable CYCCNT directly (bit 0) } isDWT_Init=true; } void delay_us(uint32_t us) { if (!isDWT_Init) { DWT_Init(); } uint32_t cycles = us * (SystemCoreClock/1000000); if (cycles < 12) cycles = 12; // Minimum measurable delay DWT_CYCCNT = 0; while (DWT_CYCCNT < cycles); }
Для проверки я выставил в суперцикле задержки на 1000 мкс и вот какой результат показал логический анализатор:
Т.е. работает довольно точно.
Далее для вывода отладочной информации нам понадобится UART интерфейс. Работа с UART модулем в STM32 мало чем отличается от своего аналога в STM8, разве что только тем, здесь их три. При этом только USART1 тактируется от скоростной APB2 шины, остальные два тактируются от APB1.
Настройка UART через регистры подробнейшем образом разобрана в статье: ARM Учебный курс. USART | Электроника для всех. От STM8 процедура настройки отличается необходимостью включать альтернативный режим работы GPIO и немного другой формулой расчета регистра установки битрейта USART1->BRR.
Для реализации функций UART потребуется добавить модуль "uart" в проект (файлы автоматически подхватятся сборочными скриптами).
Настройку USART1 интерфейса STM32А103 я разместил в модуле "main.c":
#include "stm32f10x.h" #include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h" #include "stm32f10x_usart.h" #include "main.h" #include "utils.h" #include "uart.h" uint32_t SystemCoreClock = 72000000; // Clock STM32F103 at reset int main() { // --- RCC configuration ------- RCC->APB2ENR |= RCC_APB2Periph_GPIOC; // Enable GPIOC clock RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_USART1EN; // Enable GPIOA and USART1 clocks // --- GPIO configuration ---- // Configure PC13 as push-pull output (max speed 2 MHz) GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13); GPIOC->CRH |= GPIO_CRH_MODE13_1; // Output mode, max speed 2 MHz (0b10) // Configure PA9 as alternate function push-pull (TX) GPIOA->CRH &= ~(GPIO_CRH_CNF9 | GPIO_CRH_MODE9); // Clear bits GPIOA->CRH |= (GPIO_CRH_CNF9_1 | GPIO_CRH_MODE9); // Alternate function output, max speed 50MHz // Configure PA10 as input floating (RX) GPIOA->CRH &= ~(GPIO_CRH_CNF10 | GPIO_CRH_MODE10); // Clear bits GPIOA->CRH |= GPIO_CRH_CNF10_0; // Floating input // --- UART configuration --- //USART1->BRR = 0x1d4c; // 9600 baud @ 72 MHz (APB2) USART1->BRR = 0x271; // 115200 baud @ 72MHz (APB2) USART1->CR1 = USART_CR1_TE | USART_CR1_RE | USART_CR1_UE; // Enable TX, RX and USART // Enable RX interrupt (Optional) // USART1->CR1 |= USART_CR1_RXNEIE; // NVIC_EnableIRQ(USART1_IRQn); // ---- SysTick Configuration ---- systick_init(); // Let's go.. __enable_irq(); // Super loop to toggle PC13 while (1) { // Toggle PC13 GPIOC->ODR ^= GPIO_Pin_13; // Send data via UART USART1->DR = 'A'; // Send character while (!(USART1->SR & USART_SR_TXE)); // Wait for transmission complete delay_ms(1000); } }
В суперцикле небольшой отладочный код, для проверки работы интерфейса.
Нам понадобится заголовочный файл из SPL stm32f10x_usart.h в котором содержатся некоторые битовые маски регистров. К сожалению не все битовые маски содержались в заголовочном файле. Часть находилась в stm32f10x_usart.c. Их пришлось перенести в свой заголовочный файл uart.h:
показать#ifndef __UART_H__ #define __UART_H__ #include "stm32f10x.h" #include "stm32f10x_usart.h" #define USART_CR1_UE_Set ((uint16_t)0x2000) /*!< USART Enable Mask */ #define USART_CR1_UE_Reset ((uint16_t)0xDFFF) /*!< USART Disable Mask */ #define USART_CR1_WAKE_Mask ((uint16_t)0xF7FF) /*!< USART WakeUp Method Mask */ #define USART_CR1_RWU_Set ((uint16_t)0x0002) /*!< USART mute mode Enable Mask */ #define USART_CR1_RWU_Reset ((uint16_t)0xFFFD) /*!< USART mute mode Enable Mask */ #define USART_CR1_SBK_Set ((uint16_t)0x0001) /*!< USART Break Character send Mask */ #define USART_CR1_CLEAR_Mask ((uint16_t)0xE9F3) /*!< USART CR1 Mask */ #define USART_CR2_Address_Mask ((uint16_t)0xFFF0) /*!< USART address Mask */ #define USART_CR2_LINEN_Set ((uint16_t)0x4000) /*!< USART LIN Enable Mask */ #define USART_CR2_LINEN_Reset ((uint16_t)0xBFFF) /*!< USART LIN Disable Mask */ #define USART_CR2_LBDL_Mask ((uint16_t)0xFFDF) /*!< USART LIN Break detection Mask */ #define USART_CR2_STOP_CLEAR_Mask ((uint16_t)0xCFFF) /*!< USART CR2 STOP Bits Mask */ #define USART_CR2_CLOCK_CLEAR_Mask ((uint16_t)0xF0FF) /*!< USART CR2 Clock Mask */ #define USART_CR3_SCEN_Set ((uint16_t)0x0020) /*!< USART SC Enable Mask */ #define USART_CR3_SCEN_Reset ((uint16_t)0xFFDF) /*!< USART SC Disable Mask */ #define USART_CR3_NACK_Set ((uint16_t)0x0010) /*!< USART SC NACK Enable Mask */ #define USART_CR3_NACK_Reset ((uint16_t)0xFFEF) /*!< USART SC NACK Disable Mask */ #define USART_CR3_HDSEL_Set ((uint16_t)0x0008) /*!< USART Half-Duplex Enable Mask */ #define USART_CR3_HDSEL_Reset ((uint16_t)0xFFF7) /*!< USART Half-Duplex Disable Mask */ #define USART_CR3_IRLP_Mask ((uint16_t)0xFFFB) /*!< USART IrDA LowPower mode Mask */ #define USART_CR3_CLEAR_Mask ((uint16_t)0xFCFF) /*!< USART CR3 Mask */ #define USART_CR3_IREN_Set ((uint16_t)0x0002) /*!< USART IrDA Enable Mask */ #define USART_CR3_IREN_Reset ((uint16_t)0xFFFD) /*!< USART IrDA Disable Mask */ #define USART_GTPR_LSB_Mask ((uint16_t)0x00FF) /*!< Guard Time Register LSB Mask */ #define USART_GTPR_MSB_Mask ((uint16_t)0xFF00) /*!< Guard Time Register MSB Mask */ #define USART_IT_Mask ((uint16_t)0x001F) /*!< USART Interrupt Mask */ /* USART OverSampling-8 Mask */ #define CR1_OVER8_Set ((u16)0x8000) /* USART OVER8 mode Enable Mask */ #define CR1_OVER8_Reset ((u16)0x7FFF) /* USART OVER8 mode Disable Mask */ /* USART One Bit Sampling Mask */ #define CR3_ONEBITE_Set ((u16)0x0800) /* USART ONEBITE mode Enable Mask */ #define CR3_ONEBITE_Reset ((u16)0xF7FF) /* USART ONEBITE mode Disable Mask */ #endif
Подключение USB-UART адаптера к Bluepill следующее:RX(адаптера) к PA9(bluebill), GND(адаптера) к GND(bluepill):
После компиляции и прошивки, на ножке PA9 появится UART сигнал с литерой "эй" (hex-код 0x41):
Для того, чтобы иметь возможность передавать через USART интерфейс строки и числа (предварительно прошедшию сериализацию), в модуле "uart.c" разместим следующий код:
#include "uart.h" #include <stdarg.h> // For variable arguments support // Send single character void usart1_send_char(uint8_t ch) { USART1->DR = ch; while(!(USART1->SR & USART_SR_TXE)); // Wait until transmit complete } // Send string (null-terminated) void usart1_print_string(const char *str) { while(*str) { usart1_send_char(*str++); } } // Send number (32-bit unsigned) void usart1_print_number(uint32_t num) { char buffer[10]; // Maximum 10 digits for 32-bit number char *p = buffer + sizeof(buffer) - 1; *p = '\0'; // Null terminator do { *(--p) = '0' + (num % 10); num /= 10; } while(num > 0); usart1_print_string(p); } // Extended version with format support (basic implementation) void usart1_printf(const char *format, ...) { va_list args; va_start(args, format); while(*format) { if(*format == '%') { format++; switch(*format) { case 'd': { int32_t num = va_arg(args, int32_t); if(num < 0) { usart1_send_char('-'); num = -num; } usart1_print_number((uint32_t)num); break; } case 'u': usart1_print_number(va_arg(args, uint32_t)); break; case 's': usart1_print_string(va_arg(args, char*)); break; case 'c': usart1_send_char(va_arg(args, int)); break; case '%': usart1_send_char('%'); break; } } else { usart1_send_char(*format); } format++; } va_end(args); }
Для проверки кода, суперцикл в "main.c" пусть будет таким:
// Super loop to toggle PC13 int i=0; while (1) { // Toggle PC13 GPIOC->ODR ^= GPIO_Pin_13; // Send string usart1_print_string("Hello World\r\n"); // Send number usart1_print_number(314159); // Formatted output usart1_printf("Temperature: %d°C, Status: %s\r\n", i++, "OK"); delay_ms(1000); }
После компиляции, размер прошивки перевалит за первый килобайт:
arm-none-eabi-size --format=berkeley build/firmware.elf text data bss dec hex filename 1116 4 260 1380 564 build/firmware.elf
К счастью, в микроконтроллере имеется еще 63 кБ флеш-памяти.
На логическом анализаторе, передача по UART будет выглядеть так:
В терминальной программе пойдет лог:
Когда я тестировал свои STM8_Board, то мне нужен был простой тест, чтобы определить прирост производительности в 24MHz STM8S207 по сравнению с 16MHz STM8S105. Я приводил простой тест на операции деления и упоминал, что 105-й чип проходил тест со значением в ~40000 итераций, а 207-й работающий на частоте 24MHz - 59949 итераций.
В Cortex-M3 тоже есть операции деления, и согласно документации она должна выполнятся несколько быстрее чем в STM8. А именно: 16 тактов у STM8 и до 12 тактов у STM32.
При этом у Cortex-M3 есть также знаковое деление и 32 битное деление, но пока это нас интересовать не будет.
На базе проекта из предыдущей главы я составил аналогичный тест для STM32F103C8T6:
#include "stm32f10x.h" #include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h" #include "uart.h" extern void delay(uint32_t ms); //static __IO uint32_t s_timer; volatile uint32_t s_timer; uint32_t div_count; uint32_t result; void SysTick_Handler(void) { if (s_timer) s_timer--; } void delay_ms(__IO uint32_t val) { // ------- SysTick CONFIG -------------- if (SysTick_Config(72000)) // set 1ms { while(1); // error } s_timer=val; while(s_timer) { result=s_timer/(uint32_t)314; div_count++; // asm("wfi"); }; SysTick->LOAD &= ~(SysTick_CTRL_ENABLE_Msk); // disable SysTick } int main() { // enable GPIOC port RCC->APB2ENR |= RCC_APB2Periph_GPIOC; // enable PORT_C RCC->APB2ENR |= RCC_APB2Periph_GPIOA; // enable PORT_A RCC->APB2ENR |= RCC_APB2Periph_USART1; // enable UART1 // --- GPIO setup ---- GPIOC->CRH &= ~(uint32_t)(0xf<<20); GPIOC->CRH |= (uint32_t)(0x2<<20); GPIOA->CRH &= ~(uint32_t)(0xf<<4); GPIOA->CRH |= (uint32_t)(0xa<<4); // --- UART setup ---- //USART1->BRR = 0x1d4c; // 9600 Baud, when APB2=72MHz USART1->BRR = 0x271; // 115200 Baud, when APB2=72MHz USART1->CR1 |= USART_CR1_UE_Set | USART_Mode_Tx; // enable USART1, enable TX mode // Let's go.. __enable_irq(); for(;;){ div_count=0; GPIOC->ODR ^= GPIO_Pin_13; delay_ms(50); usart1_print_string("count: "); usart1_print_number(div_count); usart1_send_char('\n'); delay(950); } }
Для того чтобы он корректно работал, нужно в Makefile включить оптимизацию изменив ключ -O0 на -O2 в опциях компилятора. Тогда следующий цикл:
while(s_timer) { result=s_timer/(uint32_t)314; div_count++; };
Будет раскладываться в следующую ассемблерную последовательность:
while(s_timer) { 8000166: 681a ldr r2, [r3, #0] 8000168: b182 cbz r2, 800018c <delay_ms+0x48> 800016a: 4d10 ldr r5, [pc, #64] ; (80001ac <delay_ms+0x68>) 800016c: 682a ldr r2, [r5, #0] 800016e: 3201 adds r2, #1 8000170: e000 b.n 8000174 <delay_ms+0x30> 8000172: 4602 mov r2, r0 result=s_timer/(uint32_t)314; 8000174: 681c ldr r4, [r3, #0] while(s_timer) { 8000176: 6819 ldr r1, [r3, #0] 8000178: 1c50 adds r0, r2, #1 800017a: 2900 cmp r1, #0 800017c: d1f9 bne.n 8000172 <delay_ms+0x2e> result=s_timer/(uint32_t)314; 800017e: f44f 739d mov.w r3, #314 ; 0x13a 8000182: fbb4 f4f3 udiv r4, r4, r3 8000186: 4b0a ldr r3, [pc, #40] ; (80001b0 <delay_ms+0x6c>) 8000188: 602a str r2, [r5, #0] 800018a: 601c str r4, [r3, #0] div_count++; };
Здесь можно видеть, что используется инструкция беззнакового деления: udiv, что нам и надо.
Ну и результат работы бенчмарка можно видеть на скриншоте:
Как видно, производительность STM32 в делении практически на порядок(!) выше STM8S105 работающего на 16 МHz. Впечатляет? Меня да.
Замечу, что аналогичный тест на Arduino приводил к результату приметно всего в две тысячи итераций. Но там конечно же нет аппаратного деления, так что сравнивать будет некорректно.
На самом деле глупо меряться тем, насколько быстро выполняется операция деления, если все остальное время микроконтроллер пребывает в режиме ожидания. В таком случае и программная реализация вполне сойдет, спешить то некуда. По нормальному распределить и оценить нагрузку на 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; }
Полный код планировщика приведен под спойлерами:
показать полный код task.h#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.
Для тестирования планировщика возьмем для примера драйвер 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.
Признаюсь, что с 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.
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. Результат работы сканера:
Однобайтное чтение используется когда нужно прочитать какой-либо регистр на 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'); }
На скриншоте представлен результат работы программы:
Двухбайтный режим чтения используется для чтения 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; }
Алгоритм записи одного или нескольких байт через 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; }
Функцию чтения одного байта можно развить до функции чтения массива. Для этого придётся ее утяжелить циклом последовательного чтения байтов с шины 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; }
Результат работы программы:
Отладку STM32 c помощью связки st-util + stlink_v2 в консоли я уже рассматривал ранее, однако большинство IDE в качестве gdb-сервера используют OpenOCD. Прежде чем "прикручивать" OpenOCD к IDE, нужно научиться запускать его в консоли.
Моя версия OpenOCD:
$ openocd --version Open On-Chip Debugger 0.10.0 Licensed under GNU GPL v2 For bug reports, read http://openocd.org/doc/doxygen/bugs.html
Посмотрим, к чему мы можем подключиться с помощью OpenOCD:
$ ls /usr/share/openocd/scripts/target/ 1986ве1т.cfg at91sam9g45.cfg icepick.cfg lpc4350.cfg stm32f1x.cfg adsp-sc58x.cfg at91sam9rl.cfg imx.cfg lpc4357.cfg stm32f1x_stlink.cfg aduc702x.cfg at91samdXX.cfg imx21.cfg lpc4370.cfg stm32f2x.cfg aducm360.cfg at91samg5x.cfg imx25.cfg lpc8xx.cfg stm32f2x_stlink.cfg alphascale_asm9260t.cfg atheros_ar2313.cfg imx27.cfg mc13224v.cfg stm32f3x.cfg altera_fpgasoc.cfg atheros_ar2315.cfg imx28.cfg mdr32f9q2i.cfg stm32f3x_stlink.cfg am335x.cfg atheros_ar9331.cfg imx31.cfg nds32v2.cfg stm32f4x.cfg am437x.cfg atmega128.cfg imx35.cfg nds32v3.cfg stm32f4x_stlink.cfg amdm37x.cfg atsamv.cfg imx51.cfg nds32v3m.cfg stm32f7x.cfg ar71xx.cfg avr32.cfg imx53.cfg nrf51.cfg stm32l0.cfg armada370.cfg bcm281xx.cfg imx6.cfg nrf51_stlink.tcl stm32l1.cfg at32ap7000.cfg bcm4706.cfg is5114.cfg nrf52.cfg stm32l1x_dual_bank.cfg at91r40008.cfg bcm4718.cfg ixp42x.cfg nuc910.cfg stm32l4x.cfg at91rm9200.cfg bcm47xx.cfg k1921vk01t.cfg numicro.cfg stm32lx_stlink.cfg at91sam3XXX.cfg bcm5352e.cfg k40.cfg omap2420.cfg stm32w108_stlink.cfg at91sam3ax_4x.cfg bcm6348.cfg k60.cfg omap3530.cfg stm32w108xx.cfg at91sam3ax_8x.cfg c100.cfg ke02.cfg omap4430.cfg stm32xl.cfg at91sam3ax_xx.cfg c100config.tcl ke04.cfg omap4460.cfg str710.cfg at91sam3nXX.cfg c100helper.tcl ke06.cfg omap5912.cfg str730.cfg at91sam3sXX.cfg c100regs.tcl kex.cfg omapl138.cfg str750.cfg at91sam3u1c.cfg cc2538.cfg kl25.cfg or1k.cfg str912.cfg at91sam3u1e.cfg cc26xx.cfg kl25z_hla.cfg pic32mx.cfg swj-dp.tcl at91sam3u2c.cfg cc32xx.cfg kl46.cfg psoc4.cfg test_reset_syntax_error.cfg at91sam3u2e.cfg cs351x.cfg klx.cfg psoc5lp.cfg test_syntax_error.cfg at91sam3u4c.cfg davinci.cfg ks869x.cfg pxa255.cfg ti-ar7.cfg at91sam3u4e.cfg dragonite.cfg kx.cfg pxa270.cfg ti-cjtag.cfg at91sam3uxx.cfg dsp56321.cfg lpc11xx.cfg pxa3xx.cfg ti_calypso.cfg at91sam4XXX.cfg dsp568013.cfg lpc12xx.cfg quark_d20xx.cfg ti_dm355.cfg at91sam4c32x.cfg dsp568037.cfg lpc13xx.cfg quark_x10xx.cfg ti_dm365.cfg at91sam4cXXX.cfg efm32.cfg lpc17xx.cfg readme.txt ti_dm6446.cfg at91sam4lXX.cfg efm32_stlink.cfg lpc1850.cfg renesas_s7g2.cfg ti_msp432p4xx.cfg at91sam4sXX.cfg em357.cfg lpc1xxx.cfg samsung_s3c2410.cfg ti_rm4x.cfg at91sam4sd32x.cfg em358.cfg lpc2103.cfg samsung_s3c2440.cfg ti_tms570.cfg at91sam7a2.cfg epc9301.cfg lpc2124.cfg samsung_s3c2450.cfg ti_tms570ls20xxx.cfg at91sam7se512.cfg exynos5250.cfg lpc2129.cfg samsung_s3c4510.cfg ti_tms570ls3137.cfg at91sam7sx.cfg faux.cfg lpc2148.cfg samsung_s3c6410.cfg tmpa900.cfg at91sam7x256.cfg feroceon.cfg lpc2294.cfg sharp_lh79532.cfg tmpa910.cfg at91sam7x512.cfg fm3.cfg lpc2378.cfg sim3x.cfg u8500.cfg at91sam9.cfg fm4.cfg lpc2460.cfg smp8634.cfg vybrid_vf6xx.cfg at91sam9260.cfg fm4_mb9bf.cfg lpc2478.cfg spear3xx.cfg xmc1xxx.cfg at91sam9260ext_flash.cfg fm4_s6e2cc.cfg lpc2900.cfg stellaris.cfg xmc4xxx.cfg at91sam9261.cfg gp326xxxa.cfg lpc2xxx.cfg stellaris_icdi.cfg xmos_xs1-xau8a-10_arm.cfg at91sam9263.cfg hilscher_netx10.cfg lpc3131.cfg stm32_stlink.cfg zynq_7000.cfg at91sam9g10.cfg hilscher_netx50.cfg lpc3250.cfg stm32f0x.cfg к1879xб1я.cfg at91sam9g20.cfg hilscher_netx500.cfg lpc40xx.cfg stm32f0x_stlink.cfg
Можно предположить, что нам подойдут таргеты stm32f1x_stlink.cfg или/и stm32f1x.cfg.
Пробуем подключиться с помощью скрипта stm32f1x_stlink.cfg:
$ openocd -f interface/stlink-v2.cfg -f target/stm32f1x_stlink.cfg -c "init" -c "reset halt" Open On-Chip Debugger 0.10.0 Licensed under GNU GPL v2 For bug reports, read http://openocd.org/doc/doxygen/bugs.html WARNING: target/stm32f1x_stlink.cfg is deprecated, please switch to target/stm32f1x.cfg Info : auto-selecting first available session transport "hla_swd". To override use 'transport select <transport>'. Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD adapter speed: 1000 kHz adapter_nsrst_delay: 100 none separate Info : Unable to match requested speed 1000 kHz, using 950 kHz Info : Unable to match requested speed 1000 kHz, using 950 kHz Info : clock speed 950 kHz Info : STLINK v2 JTAG v27 API v2 SWIM v6 VID 0x0483 PID 0x3748 Info : using stlink api v2 Info : Target voltage: 3.254076 Info : stm32f1x.cpu: hardware has 6 breakpoints, 4 watchpoints target halted due to debug-request, current mode: Thread xPSR: 0x01000000 pc: 0x08000b4c msp: 0x20005000
Если верить этому сообщению: "WARNING: target/stm32f1x_stlink.cfg is deprecated, please switch to target/stm32f1x.cfg", то мы должны использовать таргет stm32f1x.cfg. Пока проигнорируем это.
В другом окне запускаем отладчик командой:
$ arm-none-eabi-gdb ./blink.elf --tui
Порт OpenOCD по умолчанию - 3333. Подключаемся к OpenOCD:
(gdb) target remote localhost:3333 Remote debugging using localhost:3333 Reset_Handler () at asm/init.s:20
В окне OpenOCD видим отклик:
Info : accepting 'gdb' connection on tcp/3333 Info : device id = 0x20036410 Info : flash size = 64kbytes
Открываем окно с ассемблерным листингом: "layout asm", окно с регистрами: layout regs", и видим что находимся в обработчике прерывания Reset:
Что делать дальше - думаю понятно. Добавлю, что вместо таргета stm32f1x_stlink.cfg можно использовать stm32f1x.cfg, я разницы не заметил. Предыдущая версия OpenOCD 0.8 c с этим тагретом работать отказывалась.
Напоминаю, что посмотреть исходники, сборочные файлы, скачать скомпилированные прошивки, можно с портала GITLAB https://gitlab.com/flank1er/stm32_bare_metal