Когда пару лет назад я писал вводную статью по STM32, то больше всего мне тогда запомнилось, что прошивки даже с самыми простыми алгоритмами (например Blink) - имеют огромный размер: от одного килобайта и больше. Поэтому целью этой статьи стала попытка написания прошивок для STM32 в стиле 8-битных микроконтроллеров, когда ты полностью контролируешь процесс компиляции, используя лишь: компилятор, флешер и текстовый редактор. Соответственно в статье рассматриваются типовые на мой взгляд вопросы при переходе с 8-битников на 32-разрядную архитектуру: как помигать светодиодом, как настроить тактирование, как завести SPI и поднять I2C.
Данная статья не рассчитана на новичков, я часто буду сравнивать STM32 c STM8, и по ходу повествования буду опускать многие элементарные, на мой взгляд, вещи. Статья предполагает, что вы уже знаете Cи, имеете опыт работы в консоли Linux или CYGWIN Windows. Также будет весьма кстати, если у вас уже есть опыт программирования в "Bare Metal" хотя бы на уровне микроконтроллеров STM8.
Если вам чего-то из этого не хватает, то вы легко сможете подтянуть "матчасть" по статьям на хабре: STM32F4: GNU AS: Программирование на ассемблере в семи частях, по методичке "Народная электроника" выпуск 2. А.В. Немоляев. GCC Cortex-M3. PDF, или по книге "Джозеф Ю. Ядро Cortex - МЗ компании ARM. Полное руководство". Также, в какой-то мере, могут быть полезны материалы данного сайта.
Оборудование. В статье я буду использовать популярную плату "Blue Pill" на микроконтроллере STM32F103C8T6, программатор ST-LINK v2 (китайская реплика), USB-UART преобразователь FT232RL, 4-x разрядный семисегментный индикатор, на SPI интерфейсе и RTC DS3231 на I2C интерфейсе.
Содержание:
Посмотреть исходники, сборочные файлы, скачать скомпилированные прошивки, можно с портала GITLAB https://gitlab.com/flank1er/stm32_bare_metal
Как я уже говорил, началось все с того, что меня несколько обескуражил размер прошивки минимального проекта в TrueStudio - 1572 байт:
В SW4STM32 получается какая-то такая же цифра, при этом у меня в настройках проекта выставлена опция: --gc-section, которая даёт команду компоновщику удалять неиспользуемый код:
Этот STM32 попахивал какой-то очередной Arduino, но даже там Blink "весил" в пределах одного килобайта.
Сначала я подумал, что все дело в используемой SPL и если переписать код на регистрах, то ситуация исправится. Давайте посмотрим так ли это.
Первая вызываемая функция имеет такой вид:
void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState) { /* Check the parameters */ assert_param(IS_RCC_APB2_PERIPH(RCC_APB2Periph)); assert_param(IS_FUNCTIONAL_STATE(NewState)); if (NewState != DISABLE) { RCC->APB2ENR |= RCC_APB2Periph; } else { RCC->APB2ENR &= ~RCC_APB2Periph; } }
т.е. ее можно смело заменить строкой вида:
RCC->APB2ENR |= RCC_APB2Periph_GPIOC;
Вызов функции инициализации порта GPIO оборачивается вызовом такой штуки:
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct) { uint32_t currentmode = 0x00, currentpin = 0x00, pinpos = 0x00, pos = 0x00; uint32_t tmpreg = 0x00, pinmask = 0x00; /* Check the parameters */ assert_param(IS_GPIO_ALL_PERIPH(GPIOx)); assert_param(IS_GPIO_MODE(GPIO_InitStruct->GPIO_Mode)); assert_param(IS_GPIO_PIN(GPIO_InitStruct->GPIO_Pin)); /*---------------------------- GPIO Mode Configuration -----------------------*/ currentmode = ((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x0F); if ((((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x10)) != 0x00) { /* Check the parameters */ assert_param(IS_GPIO_SPEED(GPIO_InitStruct->GPIO_Speed)); /* Output mode */ currentmode |= (uint32_t)GPIO_InitStruct->GPIO_Speed; } /*---------------------------- GPIO CRL Configuration ------------------------*/ /* Configure the eight low port pins */ if (((uint32_t)GPIO_InitStruct->GPIO_Pin & ((uint32_t)0x00FF)) != 0x00) { tmpreg = GPIOx->CRL; for (pinpos = 0x00; pinpos < 0x08; pinpos++) { pos = ((uint32_t)0x01) << pinpos; /* Get the port pins position */ currentpin = (GPIO_InitStruct->GPIO_Pin) & pos; if (currentpin == pos) { pos = pinpos << 2; /* Clear the corresponding low control register bits */ pinmask = ((uint32_t)0x0F) << pos; tmpreg &= ~pinmask; /* Write the mode configuration in the corresponding bits */ tmpreg |= (currentmode << pos); /* Reset the corresponding ODR bit */ if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD) { GPIOx->BRR = (((uint32_t)0x01) << pinpos); } else { /* Set the corresponding ODR bit */ if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU) { GPIOx->BSRR = (((uint32_t)0x01) << pinpos); } } } } GPIOx->CRL = tmpreg; } /*---------------------------- GPIO CRH Configuration ------------------------*/ /* Configure the eight high port pins */ if (GPIO_InitStruct->GPIO_Pin > 0x00FF) { tmpreg = GPIOx->CRH; for (pinpos = 0x00; pinpos < 0x08; pinpos++) { pos = (((uint32_t)0x01) << (pinpos + 0x08)); /* Get the port pins position */ currentpin = ((GPIO_InitStruct->GPIO_Pin) & pos); if (currentpin == pos) { pos = pinpos << 2; /* Clear the corresponding high control register bits */ pinmask = ((uint32_t)0x0F) << pos; tmpreg &= ~pinmask; /* Write the mode configuration in the corresponding bits */ tmpreg |= (currentmode << pos); /* Reset the corresponding ODR bit */ if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD) { GPIOx->BRR = (((uint32_t)0x01) << (pinpos + 0x08)); } /* Set the corresponding ODR bit */ if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU) { GPIOx->BSRR = (((uint32_t)0x01) << (pinpos + 0x08)); } } } GPIOx->CRH = tmpreg; } }
По большому счету, эта функция устанавливает значение регистров: GPIOx->CRL или/и GPIOx->CRH. Вспоминаем, что это за регистры:
Регистр GPIOx->CRL конфигурирует режим пинов от 0 до 7. Регистр GPIOx->CRH конфигурирует режим пинов от 8 до 15. Т.к. светодиод на Blue Pill подключён к PC13, то нам нужен регистр GPIOС->CRH. Чтобы его сконфигурировать. В поле MOD13[1:0] можно задать максимальную частоту переключения нужного пина в Output режиме. Полагаю 2МГц будет вполне достаточно, значит записываем в него значение 2. По умолчанию, в режиме выхода поле CNF13[1:0] конфигурирует пин в Push-Pull режим, что нас вполне устраивает, следовательно оставляем там по нулям.
Т.о. вызов функции GPIO_Init(GPIOC, &GPIO_InitStructure) можно заменить следующей парой строк:
GPIOC->CRH &= ~(uint32_t)(0xf<<20); GPIOC->CRH |= (uint32_t)(0x2<<20);
Переключение пина осуществляется через регистр GPIOx->ODR:
Т.е. вызов функции: "GPIO_SetBits(GPIOC,GPIO_Pin_13)" можно заменить на: "GPIOC->ODR |= GPIO_Pin_13".
В Cortex-M3 нет тех удобных битовых инструкций что были в STM8. Поэтому для побитового изменения состояния регистра GPIOx->ODR, были специально введены дополнительные регистры: GPIOx->BSRR и GPIOx->BRR. Посмотрим на их описание:
Эти регистры управляют состоянием GPIOx->ODR путем записи единицы в их соответствующий разряд. Т.е. вместо GPIOC->ODR &= ~(GPIO_Pin_13); можно использовать GPIOC->BRR = GPIO_Pin_13. Такой вариант разложится компилятором в ОДНУ ассемблерную инструкцию, вместо трёх, если делать непосредственно через GPIOC->ODR регистр. Обратите внимание, что через BSRR/BRR регистры одной инструкцией можно менять состояние сразу нескольких пинов порта. При желании можно организовать параллельную шину через bit-bang.
В итоге, функция main() примет вид:
int main() { // enable GPIOC port RCC->APB2ENR |= RCC_APB2Periph_GPIOC; // --- GPIO setup ---- GPIOC->CRH &= ~(uint32_t)(0xf<<20); GPIOC->CRH |= (uint32_t)(0x2<<20); for(;;){ GPIOC->BSRR=GPIO_Pin_13; dummy_loop(600000); GPIOC->BRR=GPIO_Pin_13; dummy_loop(600000); } }
После компиляции размер прошивки сократился до 1084 байт. Уже лучше, но все-равно многовато. После дизассеблирования, становится ясно, к прошивке кроме нашего кода добавляется ещё стандартная библиотека Си: libc или newlib (облегчённый вариант стандартной библиотеки Cи), а также некоторые функции конфигурации микроконтроллера (startup files) включая таблицу прерываний. Все вместе это и занимает один килобайт. Ничего не имею против таблицы, но от всего остального хотелось бы избавиться. В TrueStudio у нас нет полного контроля за Makefile'ом, поэтому предлагаю закрыть эту IDE и перейти в консоль.
Те, кто внимательно читали документацию, знают, что прошивка для Cortex-M3 должна начинаться со значения указателя стека, после чего должны идти адреса обработчиков прерываний: Reset, NMI, Hard Fault:
На адрес 0x00000000 происходит маппинг: или флеш-памяти или ОЗУ, в зависимости от конфигурации boot-пинов. Флеш-память начинается с адреса 0x08000000.
С учётом вышесказанного, минимальная программа Blink для stm32f103c8 у меня получилась такой:
#include "stm32f10x.h" #include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h" asm(".word 0x20005000\n\t" ".word main+1\n\t" ".word fault_irq+1\n\t" ".word fault_irq+1\n\t" ".word fault_irq+1\n\t" ".word fault_irq+1\n\t" ".word fault_irq+1\n\t"); void dummy_loop(uint32_t count){ while(--count); } void fault_irq() { while(1); } int main() { // enable GPIOC port RCC->APB2ENR |= RCC_APB2Periph_GPIOC; // --- GPIO setup ---- GPIOC->CRH &= ~(uint32_t)(0xf<<20); GPIOC->CRH |= (uint32_t)(0x2<<20); for(;;){ GPIOC->BSRR=GPIO_Pin_13; dummy_loop(600000); GPIOC->BRR=GPIO_Pin_13; dummy_loop(600000); } }
Таблица векторов в Cortex-M3 это просто массив c адресами обработчиков прерываний. Инструкции INT, как и IRET в Cortex-M3 не существует. Все адреса прерываний должны быть нечётными! Т.е. единица прибавляемая к адресу метки нужна для получения нечётного числа, чтобы указать, что инструкция в обработчике прерывания из набора Thumb/Thumb2, а не из 32-битных инструкций ARM. Если вектор будет указывать на чётный адрес, то переход по нему приведёт срабатыванию прерывания Usage Fault.
Т.к. в программе используются имена регистров и константы из CMSIS и SPL, то потребуются заголовочные файлы этих библиотек. Я их брал из шаблонного проекта SW4STM32, но SPL для своей модели микроконтроллера, конечно же можно скачать с сайта st.com. Например для STMF103C8 отсюда: STSW-STM32054 - STM32F10x standard peripheral library - STMicroelectronics. Структура проекта пока выглядит так:
$ tree . . ├── CMSIS │ ├── core │ │ ├── core_cm3.c │ │ └── core_cm3.h │ └── device │ ├── stm32f10x.h │ └── system_stm32f10x.h ├── SPL │ └── inc │ ├── stm32f10x_gpio.h │ └── stm32f10x_rcc.h ├── main.bin ├── main.c ├── main.elf ├── main.o └── script.ld 5 directories, 11 files
Для сборки также понадобится скрипт компоновщика: "script.ld", я его взял также из шаблонного проекта SW4STM32 и несколько сократил под свои нужды:
/* Highest address of the user mode stack */ _estack = 0x20005000; /* end of RAM */ /* Memories definition */ MEMORY { RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K ROM (rx) : ORIGIN = 0x8000000, LENGTH = 64K } /* Sections */ SECTIONS { /* The program code and other data into ROM memory */ .text : { . = ALIGN(4); *(.text) /* .text sections (code) */ } >ROM /* Constant data into ROM memory*/ .rodata : { . = ALIGN(4); *(.rodata) /* .rodata sections (constants, strings, etc.) */ } >ROM /* Initialized data sections into RAM memory */ .data : { . = ALIGN(4); *(.data) /* .data sections */ } >RAM AT> ROM /* Uninitialized data section into RAM memory */ . = ALIGN(4); .bss : { *(.bss) *(COMMON) . = ALIGN(4); _ebss = .; /* define a global symbol at bss end */ __bss_end__ = _ebss; } >RAM }
Компилируем:
$ arm-none-eabi-gcc -mthumb -mcpu=cortex-m3 -O0 -c -g -DSTM32F10X_MD -I ./CMSIS/device -I ./CMSIS/core -I ./SPL/inc -o main.o ./main.c
Линкуем:
$ arm-none-eabi-ld -Tscript.ld main.o -o main.elf
Проверяем:
$ arm-none-eabi-objdump -S ./main.elf ./main.elf: file format elf32-littlearm Disassembly of section .text: 08000000 <dummy_loop-0x1c>: 8000000: 20005000 .word 0x20005000 8000004: 08000041 .word 0x08000041 8000008: 0800003b .word 0x0800003b 800000c: 0800003b .word 0x0800003b 8000010: 0800003b .word 0x0800003b 8000014: 0800003b .word 0x0800003b 8000018: 0800003b .word 0x0800003b 0800001c <dummy_loop>: ".word fault_irq+1\n\t" ".word fault_irq+1\n\t" ".word fault_irq+1\n\t" ".word fault_irq+1\n\t"); void dummy_loop(uint32_t count){ 800001c: b480 push {r7} 800001e: b083 sub sp, #12 8000020: af00 add r7, sp, #0 8000022: 6078 str r0, [r7, #4] while(--count); 8000024: 687b ldr r3, [r7, #4] 8000026: 3b01 subs r3, #1 8000028: 607b str r3, [r7, #4] 800002a: 687b ldr r3, [r7, #4] 800002c: 2b00 cmp r3, #0 800002e: d1f9 bne.n 8000024 <dummy_loop+0x8> } 8000030: bf00 nop 8000032: 370c adds r7, #12 8000034: 46bd mov sp, r7 8000036: bc80 pop {r7} 8000038: 4770 bx lr 0800003a <fault_irq>: void fault_irq() { 800003a: b480 push {r7} 800003c: af00 add r7, sp, #0 while(1); 800003e: e7fe b.n 800003e <fault_irq+0x4> 08000040 <main>: } int main() { 8000040: b580 push {r7, lr} 8000042: af00 add r7, sp, #0 // enable GPIOC port RCC->APB2ENR |= RCC_APB2Periph_GPIOC; 8000044: 4b10 ldr r3, [pc, #64] ; (8000088 <main+0x48>) 8000046: 699b ldr r3, [r3, #24] 8000048: 4a0f ldr r2, [pc, #60] ; (8000088 <main+0x48>) 800004a: f043 0310 orr.w r3, r3, #16 800004e: 6193 str r3, [r2, #24] // --- GPIO setup ---- GPIOC->CRH &= ~(uint32_t)(0xf<<20); 8000050: 4b0e ldr r3, [pc, #56] ; (800008c <main+0x4c>) 8000052: 685b ldr r3, [r3, #4] 8000054: 4a0d ldr r2, [pc, #52] ; (800008c <main+0x4c>) 8000056: f423 0370 bic.w r3, r3, #15728640 ; 0xf00000 800005a: 6053 str r3, [r2, #4] GPIOC->CRH |= (uint32_t)(0x2<<20); 800005c: 4b0b ldr r3, [pc, #44] ; (800008c <main+0x4c>) 800005e: 685b ldr r3, [r3, #4] 8000060: 4a0a ldr r2, [pc, #40] ; (800008c <main+0x4c>) 8000062: f443 1300 orr.w r3, r3, #2097152 ; 0x200000 8000066: 6053 str r3, [r2, #4] for(;;){ GPIOC->BSRR=GPIO_Pin_13; 8000068: 4b08 ldr r3, [pc, #32] ; (800008c <main+0x4c>) 800006a: f44f 5200 mov.w r2, #8192 ; 0x2000 800006e: 611a str r2, [r3, #16] dummy_loop(600000); 8000070: 4807 ldr r0, [pc, #28] ; (8000090 <main+0x50>) 8000072: f7ff ffd3 bl 800001c <dummy_loop> GPIOC->BRR=GPIO_Pin_13; 8000076: 4b05 ldr r3, [pc, #20] ; (800008c <main+0x4c>) 8000078: f44f 5200 mov.w r2, #8192 ; 0x2000 800007c: 615a str r2, [r3, #20] dummy_loop(600000); 800007e: 4804 ldr r0, [pc, #16] ; (8000090 <main+0x50>) 8000080: f7ff ffcc bl 800001c <dummy_loop> GPIOC->BSRR=GPIO_Pin_13; 8000084: e7f0 b.n 8000068 <main+0x28> 8000086: bf00 nop 8000088: 40021000 .word 0x40021000 800008c: 40011000 .word 0x40011000 8000090: 000927c0 .word 0x000927c0
В ассемблере ARM/Cortex-M3 есть много подкупающих вещей, которые заставят вас влюбиться в архитектуру STM32. Например операции знакового деления, 32-битного умножения, операции с насыщением, операции параллельной обработки данных SIMD (Single Instruction Multiple Data), операции на битовыми полями, и т.д. Если раньше вам не хватало именно этих операций, то ARM - это ваша архитектура.
В начале по адресу 0x08000000 записано значение указателя стека, далее идут нечётные указатели на обработчики прерываний. При этом обычные команды ветвления или вызова подпрограммы используют обычные чётные адреса. Это видно по инструкции вызова функции задержки:
bl 800001c <dummy_loop>
Правда тут есть немного лукавства. Все переходы в Cortex-M3 относительные. То, что дизассемблер показал конкретный адрес, не означает, что конкретно этот адрес записан в операнде. Если сама инструкция начинается с НЕчетного адреса, то смещение до другой НЕчетной инструкции будет четным. Из этого следует, что к меткам в программе единицу прибавлять не следует, они и так уже выровнены по нечетным значениям. И учтите, что в счётчике команд PC/R15 при отладке, вы никогда не увидите нечетный адрес, т.к. младший бит PC/R15 аппаратно сброшен в ноль. Такая вот у этой архитектуры фича.
Т.к. счетчик команд PC/R15 всегда выровнен по границе слова или полуслова, вследствие этого, все инструкции в Cortex-M3 занимают либо два (чаще всего), либо четыре байта (инструкции Thumb2). Для того, что бы занести в регистр 32-битную константу имеется два способа. Первый способ заключается в использовании инструкции mov.w дважды, когда начала заносится младшее полуслово, затем старшее. Или наоборот. Второй способ заключается в использовании псевдо инструкции LDR, когда число записывается константой со смещением относительно регистра PC. Инструкция LDR загружает эту константу в регистр используя индексную адресацию. Оба способа занимают 8 байт на флеш-памяти.
В Cortex-M3 используется 3-х уровневый конвейер. Имеется две шины работающие с флеш-памятью: шина для выборки инструкций i-code и шина для выборки данных d-code. Каждая шина свой имеет 64-битный буфер. Т.о. Cortex-M3 имеет буфер превыборки 2х64 бит. Учитывая, что средняя инструкция занимает 16-бит, это должно нивелировать тот факт, что флеш-память работает на меньшей частоте чем ядро.
В Cortex-M3 нет инструкций CALL/RET. Вместо них есть переход с сохранением адреса возврата в регистре R14/LR. Собственно: BL и BX. Если подпрограмма содержит в себе вызов другой подпрограммы, то содержимое R14/LR следует сохранить в стеке. Возврат тогда будет по инструкции: POP PC.
В Cortex-M3 нет переходов по абсолютному адресу, все переходы относительные. Это означает, что весь код являются перемещаемым.
В ARM нет понятия сегментов, как и сегментных регистров. Инструкции могут обращаться ко всему адресному пространству через индексную или косвенную адресацию. Зато в ARM есть понятие региона. Например: регион флеш-памяти, регион bit-banging и т.д.
В Cortex-M3 есть еще такое понятие, как маппинг региона, этакое переназначение адресов. Например в зависимости от конфигурации BOOT-пинов на адрес 0х00000000 маппится или регион ОЗУ с адресом 0х20000000 или флеш-памяти с адресом 0х08000000.
Инструкций INC, DEC в Cortex-M3 так же нет, зато есть режим индексной адресации с автоинкрементом и автодекриментом (этакий реверанс в сторону архитектуры PDP-11). Инструкции sub reg,#1 и add reg,#1 могут служить заменой dec и inc. Они занимают всего два байта вместе с операндом. Если операнд больше одного байта, то такая инструкция будет занимать уже 4 байта.
Указатель стека R13/SP выровнен по границе слова, т.е. его младшие два бита аппаратно сброшены в ноль. Указатель стека всегда указывает на младший байт последнего положенного в стек слова. В микроконтроллере STM32F103C8 20КБ ОЗУ или 0х5000. При инициализации стека мы записываем в SP значение 0х20005000, зная что по этому адресу уже нет ОЗУ. При помещении в стек нового значения, из указателя стека сначала вычитается четыре, и только затем в адреса: sp, sp+1, sp+2, sp+3 заносится слово. Т.е. максимальная используемая ячейка ОЗУ равна 0x20004fff, в ячейку 0х20005000 ничего не заносится.
Многие инструкции могут работать с группой регистров (reglist). Причем такие операции выполняются одной инструкцией, а не разбиваются по операндам, как можно было бы подумать. Например:
push {r0, r5-r7} 8000202: b4e1 push {r0, r5, r6, r7}
С помощью суффиксов .w .h .d может указываться размер операнда: слово, полуслово или двойное слово.
С помощью следующих суффиксов условного выполнения, инструкция может быть выполнена или нет в зависимости от значений того или иного флага регистра состояний. Применяется чаще всего с инструкциями перехода.
С помощью добавления или убирания суффикса "S", действие инструкции может изменять содержимое регистра состояния или нет.
В качестве примера ассемблера Cortex-M3, можно посмотреть как раскладывается строка на Си:
GPIOC->BSRR=GPIO_Pin_13;
ldr r3, [pc, #32] mov.w r2, #8192 str r2, [r3, #16] .word 0x40011000
Первая инструкция LDR с помощью индексной адресации загружает в регистр R3 константу 0x40011000. Данная константа является базой для GPIO блока периферийных регистров. Следующая инструкция MOV.W из набора инструкций Thumb2 является 32-битной. Она загружает в R2 нужный номер пина 0x2000. Инструкция STR переносит этот номер в BSRR регистр, вычисляя его значение как сумму базы 0x40011000 и числа 16 с помощью, опять же, индексной адресации.
Возвращаемся к нашей программе. Прошиваем микроконтроллер:
$ st-flash write ./main.bin 0x08000000 st-flash 1.4.0-50-g7fafee2 2018-09-28T18:16:01 INFO common.c: Loading device parameters.... 2018-09-28T18:16:01 INFO common.c: Device connected is: F1 Medium-density device, id 0x20036410 2018-09-28T18:16:01 INFO common.c: SRAM size: 0x5000 bytes (20 KiB), Flash: 0x10000 bytes (64 KiB) in pages of 1024 bytes 2018-09-28T18:16:01 INFO common.c: Attempting to write 148 (0x94) bytes to stm32 address: 134217728 (0x8000000) Flash page at addr: 0x08000000 erased 2018-09-28T18:16:01 INFO common.c: Finished erasing 1 pages of 1024 (0x400) bytes 2018-09-28T18:16:01 INFO common.c: Starting Flash write for VL/F0/F3/F1_XL core id 2018-09-28T18:16:01 INFO flash_loader.c: Successfully loaded flash loader in sram 1/1 pages written 2018-09-28T18:16:01 INFO common.c: Starting verification of write complete 2018-09-28T18:16:01 INFO common.c: Flash written and verified! jolly good!
Если все было сделано правильно, то светодиод начнет мигать в несколько раз медленнее по сравнению с Bkink'ом в ТrueStudio. Это из-за того, что у нас не настроена система тактирования и микроконтроллер работает сейчас от встроенного HSI генератора с частотой 8 МГц. Однако, получить минимальную прошивку в полторы сотни байт на STM32 вполне реально.
К сожалению, у такого минимализма есть своя цена. Кроме того, что микроконтроллер работает на 10% от своего максимального быстродействия, мы лишаемся функций стандартной библиотеки Си: printf(), scanf(), malloc(), функций работы со строками, математических функций, поддержки чисел с плавающей точкой и т.д. Вполне возможно, что в каком-нибудь простом проекте для Cortex-M0, это все действительно будет лишним.
Следующим логическим шагом будет вынесение ассемблерной части программы в отдельный ассемблерный файл и добавление полной таблицы векторов. Эта таблица займет у нас какое-то место на флеше.
Первым делом, нам нужно будет добавить новую секцию в скрипт компоновщика. Для этого, следующий фрагмент:
/* The program code and other data into ROM memory */ .text : { . = ALIGN(4); *(.text) /* .text sections (code) */ } >ROM
заменим на:
/* The program code and other data into ROM memory */ .text : { . = ALIGN(4); *(.vectors) KEEP(*(.vectors)) *(.text) /* .text sections (code) */ } >ROM
Файл с Си программой примет вид:
#include "stm32f10x.h" #include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h" void dummy_loop(uint32_t count){ while(--count); } int main() { // enable GPIOC port RCC->APB2ENR |= RCC_APB2Periph_GPIOC; // --- GPIO setup ---- GPIOC->CRH &= ~(uint32_t)(0xf<<20); GPIOC->CRH |= (uint32_t)(0x2<<20); for(;;){ GPIOC->BSRR=GPIO_Pin_13; dummy_loop(600000); GPIOC->BRR=GPIO_Pin_13; dummy_loop(600000); } }
Таблица прерываний STM32F103 довольно обширная:
Всего 70 прерываний, плюс шесть зарезервированных. Т.е. размер таблицы будет 76 * 4 = 304 байта. Готовую таблицу векторов я попросту скопировал из шаблонного проекта SW4STM32, в итоге ассемблерный файл получился настолько внушительных размеров, что я его спрятал под спойлер:
показать ассемблерный листинг.syntax unified .cpu cortex-m3 .thumb .section .text.Reset_Handler .weak Reset_Handler .type Reset_Handler, %function Reset_Handler: ldr r0, =_estack mov sp, r0 /* set stack pointer */ b main .section .text.Default_Handler,"ax",%progbits Default_Handler: Infinite_Loop: b Infinite_Loop .size Default_Handler, .-Default_Handler .section .vectors .word _estack .word Reset_Handler .word NMI_Handler .word HardFault_Handler .word MemManage_Handler .word BusFault_Handler .word UsageFault_Handler .word 0 .word 0 .word 0 .word 0 .word SVC_Handler .word DebugMon_Handler .word 0 .word PendSV_Handler .word SysTick_Handler .word WWDG_IRQHandler /* Window Watchdog interrupt */ .word PVD_IRQHandler /* PVD through EXTI line detection interrupt */ .word TAMPER_IRQHandler /* Tamper interrupt */ .word RTC_IRQHandler /* RTC global interrupt */ .word FLASH_IRQHandler /* Flash global interrupt */ .word RCC_IRQHandler /* RCC global interrupt */ .word EXTI0_IRQHandler /* EXTI Line0 interrupt */ .word EXTI1_IRQHandler /* EXTI Line1 interrupt */ .word EXTI2_IRQHandler /* EXTI Line2 interrupt */ .word EXTI3_IRQHandler /* EXTI Line3 interrupt */ .word EXTI4_IRQHandler /* EXTI Line4 interrupt */ .word DMA1_Channel1_IRQHandler /* DMA1 Channel1 global interrupt */ .word DMA1_Channel2_IRQHandler /* DMA1 Channel2 global interrupt */ .word DMA1_Channel3_IRQHandler /* DMA1 Channel3 global interrupt */ .word DMA1_Channel4_IRQHandler /* DMA1 Channel4 global interrupt */ .word DMA1_Channel5_IRQHandler /* DMA1 Channel5 global interrupt */ .word DMA1_Channel6_IRQHandler /* DMA1 Channel6 global interrupt */ .word DMA1_Channel7_IRQHandler /* DMA1 Channel7 global interrupt */ .word ADC1_2_IRQHandler /* ADC1 and ADC2 global interrupt */ .word USB_HP_CAN_TX_IRQHandler /* USB High Priority or CAN TX interrupts */ .word USB_LP_CAN_RX0_IRQHandler /* USB Low Priority or CAN RX0 interrupts */ .word CAN_RX1_IRQHandler /* CAN RX1 interrupt */ .word CAN_SCE_IRQHandler /* CAN SCE interrupt */ .word EXTI9_5_IRQHandler /* EXTI Line[9:5] interrupts */ .word TIM1_BRK_IRQHandler /* TIM1 Break interrupt */ .word TIM1_UP_IRQHandler /* TIM1 Update interrupt */ .word TIM1_TRG_COM_IRQHandler /* TIM1 Trigger and Commutation interrupts */ .word TIM1_CC_IRQHandler /* TIM1 Capture Compare interrupt */ .word TIM2_IRQHandler /* TIM2 global interrupt */ .word TIM3_IRQHandler /* TIM3 global interrupt */ .word TIM4_IRQHandler /* TIM4 global interrupt */ .word I2C1_EV_IRQHandler /* I2C1 event interrupt */ .word I2C1_ER_IRQHandler /* I2C1 error interrupt */ .word I2C2_EV_IRQHandler /* I2C2 event interrupt */ .word I2C2_ER_IRQHandler /* I2C2 error interrupt */ .word SPI1_IRQHandler /* SPI1 global interrupt */ .word SPI2_IRQHandler /* SPI2 global interrupt */ .word USART1_IRQHandler /* USART1 global interrupt */ .word USART2_IRQHandler /* USART2 global interrupt */ .word USART3_IRQHandler /* USART3 global interrupt */ .word EXTI15_10_IRQHandler /* EXTI Line[15:10] interrupts */ .word RTCAlarm_IRQHandler /* RTC Alarms through EXTI line interrupt */ .word 0 /* Reserved */ .word TIM8_BRK_IRQHandler /* TIM8 Break interrupt */ .word TIM8_UP_IRQHandler /* TIM8 Update interrupt */ .word TIM8_TRG_COM_IRQHandler /* TIM8 Trigger and Commutation interrupts */ .word TIM8_CC_IRQHandler /* TIM8 Capture Compare interrupt */ .word ADC3_IRQHandler /* ADC3 global interrupt */ .word FSMC_IRQHandler /* FSMC global interrupt */ .word SDIO_IRQHandler /* SDIO global interrupt */ .word TIM5_IRQHandler /* TIM5 global interrupt */ .word SPI3_IRQHandler /* SPI3 global interrupt */ .word UART4_IRQHandler /* UART4 global interrupt */ .word UART5_IRQHandler /* UART5 global interrupt */ .word TIM6_IRQHandler /* TIM6 global interrupt */ .word TIM7_IRQHandler /* TIM7 global interrupt */ .word DMA2_Channel1_IRQHandler /* DMA2 Channel1 global interrupt */ .word DMA2_Channel2_IRQHandler /* DMA2 Channel2 global interrupt */ .word DMA2_Channel3_IRQHandler /* DMA2 Channel3 global interrupt */ .word DMA2_Channel4_5_IRQHandler /* DMA2 Channel4 and DMA2 Channel5 global interrupt */ /******************************************************************************* * * Provide weak aliases for each Exception handler to the Default_Handler. * As they are weak aliases, any function with the same name will override * this definition. * *******************************************************************************/ .weak NMI_Handler .thumb_set NMI_Handler,Default_Handler .weak HardFault_Handler .thumb_set HardFault_Handler,Default_Handler .weak MemManage_Handler .thumb_set MemManage_Handler,Default_Handler .weak BusFault_Handler .thumb_set BusFault_Handler,Default_Handler .weak UsageFault_Handler .thumb_set UsageFault_Handler,Default_Handler .weak SVC_Handler .thumb_set SVC_Handler,Default_Handler .weak DebugMon_Handler .thumb_set DebugMon_Handler,Default_Handler .weak PendSV_Handler .thumb_set PendSV_Handler,Default_Handler .weak SysTick_Handler .thumb_set SysTick_Handler,Default_Handler .weak WWDG_IRQHandler .thumb_set WWDG_IRQHandler,Default_Handler .weak PVD_IRQHandler .thumb_set PVD_IRQHandler,Default_Handler .weak TAMPER_IRQHandler .thumb_set TAMPER_IRQHandler,Default_Handler .weak RTC_IRQHandler .thumb_set RTC_IRQHandler,Default_Handler .weak FLASH_IRQHandler .thumb_set FLASH_IRQHandler,Default_Handler .weak RCC_IRQHandler .thumb_set RCC_IRQHandler,Default_Handler .weak EXTI0_IRQHandler .thumb_set EXTI0_IRQHandler,Default_Handler .weak EXTI1_IRQHandler .thumb_set EXTI1_IRQHandler,Default_Handler .weak EXTI2_IRQHandler .thumb_set EXTI2_IRQHandler,Default_Handler .weak EXTI3_IRQHandler .thumb_set EXTI3_IRQHandler,Default_Handler .weak EXTI4_IRQHandler .thumb_set EXTI4_IRQHandler,Default_Handler .weak DMA1_Channel1_IRQHandler .thumb_set DMA1_Channel1_IRQHandler,Default_Handler .weak DMA1_Channel2_IRQHandler .thumb_set DMA1_Channel2_IRQHandler,Default_Handler .weak DMA1_Channel3_IRQHandler .thumb_set DMA1_Channel3_IRQHandler,Default_Handler .weak DMA1_Channel4_IRQHandler .thumb_set DMA1_Channel4_IRQHandler,Default_Handler .weak DMA1_Channel5_IRQHandler .thumb_set DMA1_Channel5_IRQHandler,Default_Handler .weak DMA1_Channel6_IRQHandler .thumb_set DMA1_Channel6_IRQHandler,Default_Handler .weak DMA1_Channel7_IRQHandler .thumb_set DMA1_Channel7_IRQHandler,Default_Handler .weak ADC1_2_IRQHandler .thumb_set ADC1_2_IRQHandler,Default_Handler .weak USB_HP_CAN_TX_IRQHandler .thumb_set USB_HP_CAN_TX_IRQHandler,Default_Handler .weak USB_LP_CAN_RX0_IRQHandler .thumb_set USB_LP_CAN_RX0_IRQHandler,Default_Handler .weak CAN_RX1_IRQHandler .thumb_set CAN_RX1_IRQHandler,Default_Handler .weak CAN_SCE_IRQHandler .thumb_set CAN_SCE_IRQHandler,Default_Handler .weak EXTI9_5_IRQHandler .thumb_set EXTI9_5_IRQHandler,Default_Handler .weak TIM1_BRK_IRQHandler .thumb_set TIM1_BRK_IRQHandler,Default_Handler .weak TIM1_UP_IRQHandler .thumb_set TIM1_UP_IRQHandler,Default_Handler .weak TIM1_TRG_COM_IRQHandler .thumb_set TIM1_TRG_COM_IRQHandler,Default_Handler .weak TIM1_CC_IRQHandler .thumb_set TIM1_CC_IRQHandler,Default_Handler .weak TIM2_IRQHandler .thumb_set TIM2_IRQHandler,Default_Handler .weak TIM3_IRQHandler .thumb_set TIM3_IRQHandler,Default_Handler .weak TIM4_IRQHandler .thumb_set TIM4_IRQHandler,Default_Handler .weak I2C1_EV_IRQHandler .thumb_set I2C1_EV_IRQHandler,Default_Handler .weak I2C1_ER_IRQHandler .thumb_set I2C1_ER_IRQHandler,Default_Handler .weak I2C2_EV_IRQHandler .thumb_set I2C2_EV_IRQHandler,Default_Handler .weak I2C2_ER_IRQHandler .thumb_set I2C2_ER_IRQHandler,Default_Handler .weak SPI1_IRQHandler .thumb_set SPI1_IRQHandler,Default_Handler .weak SPI2_IRQHandler .thumb_set SPI2_IRQHandler,Default_Handler .weak USART1_IRQHandler .thumb_set USART1_IRQHandler,Default_Handler .weak USART2_IRQHandler .thumb_set USART2_IRQHandler,Default_Handler .weak USART3_IRQHandler .thumb_set USART3_IRQHandler,Default_Handler .weak EXTI15_10_IRQHandler .thumb_set EXTI15_10_IRQHandler,Default_Handler .weak RTCAlarm_IRQHandler .thumb_set RTCAlarm_IRQHandler,Default_Handler .weak TIM8_BRK_IRQHandler .thumb_set TIM8_BRK_IRQHandler,Default_Handler .weak TIM8_UP_IRQHandler .thumb_set TIM8_UP_IRQHandler,Default_Handler .weak TIM8_TRG_COM_IRQHandler .thumb_set TIM8_TRG_COM_IRQHandler,Default_Handler .weak TIM8_CC_IRQHandler .thumb_set TIM8_CC_IRQHandler,Default_Handler .weak ADC3_IRQHandler .thumb_set ADC3_IRQHandler,Default_Handler .weak FSMC_IRQHandler .thumb_set FSMC_IRQHandler,Default_Handler .weak SDIO_IRQHandler .thumb_set SDIO_IRQHandler,Default_Handler .weak TIM5_IRQHandler .thumb_set TIM5_IRQHandler,Default_Handler .weak SPI3_IRQHandler .thumb_set SPI3_IRQHandler,Default_Handler .weak UART4_IRQHandler .thumb_set UART4_IRQHandler,Default_Handler .weak UART5_IRQHandler .thumb_set UART5_IRQHandler,Default_Handler .weak TIM6_IRQHandler .thumb_set TIM6_IRQHandler,Default_Handler .weak TIM7_IRQHandler .thumb_set TIM7_IRQHandler,Default_Handler .weak DMA2_Channel1_IRQHandler .thumb_set DMA2_Channel1_IRQHandler,Default_Handler .weak DMA2_Channel2_IRQHandler .thumb_set DMA2_Channel2_IRQHandler,Default_Handler .weak DMA2_Channel3_IRQHandler .thumb_set DMA2_Channel3_IRQHandler,Default_Handler .weak DMA2_Channel4_5_IRQHandler .thumb_set DMA2_Channel4_5_IRQHandler,Default_Handler .weak SystemInit
Здесь два обработчика прерывания: Reset и Default_Handler, а ко всем адресам автоматически прибавляется единица.
Осталось добавить файл сборки проекта: Makefile. Пусть к примеру он будет таким:
MCU=cortex-m3 OBJCOPY=arm-none-eabi-objcopy CC=arm-none-eabi-gcc LD=arm-none-eabi-ld SIZE=arm-none-eabi-size INC = -ICMSIS/device INC += -ICMSIS/core INC += -ISPL/inc DEF = -DSTM32F10X_MD CFLAGS=-mthumb -mcpu=$(MCU) -g -O0 -Wall $(DEF) $(INC) ASFLAGS=-mthumb -mcpu=$(MCU) -g -Wall LDFLAGS=-Tscript.ld OBJ=main.o init.o TARGET=blink .PHONY: all clean %.o: %.c $(CC) -c -o $@ $< $(CFLAGS) %.o: asm/%.s $(CC) -c -o $@ $< $(ASFLAGS) all: $(OBJ) $(LD) $(LDFLAGS) -g -o $(TARGET).elf $(OBJ) $(OBJCOPY) -O binary $(TARGET).elf $(TARGET).bin $(SIZE) $(TARGET).elf install: st-flash write $(TARGET).bin 0x08000000 clean: @rm -v $(TARGET).elf $(TARGET).bin $(OBJ)
Пытаемся скомпилировать:
$ make all arm-none-eabi-gcc -c -o main.o main.c -mthumb -mcpu=cortex-m3 -g -O0 -Wall -DSTM32F10X_MD -ICMSIS/device -ICMSIS/core -ISPL/inc arm-none-eabi-gcc -c -o init.o asm/init.s -mthumb -mcpu=cortex-m3 -g -Wall arm-none-eabi-ld -Tscript.ld -g -o blink.elf main.o init.o arm-none-eabi-objcopy -O binary blink.elf blink.bin arm-none-eabi-size blink.elf text data bss dec hex filename 430 0 2 432 1b0 blink.elf
Чтож... С таблицей прерываний размер проекта приближается к полукилобайту. Можно конечно удалить неиспользуемые, если не хватает места, но я так делать не буду.
После компиляции обязательно проверьте, чтобы адреса прерываний были нечетными, а в начале прошивки стоял адрес указателя стека 0х200500, после чего должен идти адрес обработчика прерывания Reset.
После добавления таблицы прерываний необходимо будет настроить систему тактирования. Для микроконтроллера STM32F103C8 она выглядит следующим образом:
Вообще-то, в шаблонном проекте TrueStudio у нас уже была настроена система тактирования RCC, хоть мы к этому и не приложили ни капли усилий. С помощью добрых глаз и дизассемблера, мне удалось выяснить, что функции настройки RCC содержатся в файле system_stm32f10x.c:
Настройка системы тактирования начинается с вызова функции SystemInit(). Создадим в каталоге проекта новый подкаталог: "src" в котором откроем новый файл startup.c. В этот файл мы и скопируем функцию SystemInit():
#include "stm32f10x.h" #include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h" void SystemInit (void) { RCC->CR |= (uint32_t)0x00000001; // enable HSI, 8 MHz /* Reset SW, HPRE, PPRE1, PPRE2, ADCPRE and MCO bits */ #ifndef STM32F10X_CL RCC->CFGR &= (uint32_t)0xF8FF0000; #else RCC->CFGR &= (uint32_t)0xF0FF0000; #endif /* STM32F10X_CL */ /* Reset HSEON, CSSON and PLLON bits */ RCC->CR &= (uint32_t)0xFEF6FFFF; /* Reset HSEBYP bit */ RCC->CR &= (uint32_t)0xFFFBFFFF; /* Reset PLLSRC, PLLXTPRE, PLLMUL and USBPRE/OTGFSPRE bits */ RCC->CFGR &= (uint32_t)0xFF80FFFF; #ifdef STM32F10X_CL /* Reset PLL2ON and PLL3ON bits */ RCC->CR &= (uint32_t)0xEBFFFFFF; /* Disable all interrupts and clear pending bits */ RCC->CIR = 0x00FF0000; /* Reset CFGR2 register */ RCC->CFGR2 = 0x00000000; #elif defined (STM32F10X_LD_VL) || defined (STM32F10X_MD_VL) || (defined STM32F10X_HD_VL) /* Disable all interrupts and clear pending bits */ RCC->CIR = 0x009F0000; /* Reset CFGR2 register */ RCC->CFGR2 = 0x00000000; #else /* Disable all interrupts and clear pending bits */ RCC->CIR = 0x009F0000; #endif /* STM32F10X_CL */ #if defined (STM32F10X_HD) || (defined STM32F10X_XL) || (defined STM32F10X_HD_VL) #ifdef DATA_IN_ExtSRAM SystemInit_ExtMemCtl(); #endif /* DATA_IN_ExtSRAM */ #endif /* Configure the System clock frequency, HCLK, PCLK2 and PCLK1 prescalers */ /* Configure the Flash Latency cycles and enable prefetch buffer */ // SetSysClock(); #ifdef SYSCLK_FREQ_HSE SetSysClockToHSE(); #elif defined SYSCLK_FREQ_24MHz SetSysClockTo24(); #elif defined SYSCLK_FREQ_36MHz SetSysClockTo36(); #elif defined SYSCLK_FREQ_48MHz SetSysClockTo48(); #elif defined SYSCLK_FREQ_56MHz SetSysClockTo56(); #elif defined SYSCLK_FREQ_72MHz SetSysClockTo72(); #endif #ifdef VECT_TAB_SRAM SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM. */ #else // SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH. */ #endif }
Данная функция сбрасывает систему тактирования, т.е. делает то, что и так должно происходить после RESET. В завершении работы, в зависимости от флагов компиляции, включается вызов функций для настройки тактирования.
Мне показалась интересной функция SetSysClockToHSE() - которая осуществляет переключение на внешний кварц.
#ifdef SYSCLK_FREQ_HSE /** * @brief Selects HSE as System clock source and configure HCLK, PCLK2 * and PCLK1 prescalers. * @note This function should be used only after reset. * @param None * @retval None */ static void SetSysClockToHSE(void) { __IO uint32_t StartUpCounter = 0, HSEStatus = 0; /* SYSCLK, HCLK, PCLK2 and PCLK1 configuration ---------------------------*/ /* Enable HSE */ RCC->CR |= ((uint32_t)RCC_CR_HSEON); /* Wait till HSE is ready and if Time out is reached exit */ do { HSEStatus = RCC->CR & RCC_CR_HSERDY; StartUpCounter++; } while((HSEStatus == 0) && (StartUpCounter != HSE_STARTUP_TIMEOUT)); if ((RCC->CR & RCC_CR_HSERDY) != RESET) { HSEStatus = (uint32_t)0x01; } else { HSEStatus = (uint32_t)0x00; } if (HSEStatus == (uint32_t)0x01) { #if !defined STM32F10X_LD_VL && !defined STM32F10X_MD_VL && !defined STM32F10X_HD_VL /* Enable Prefetch Buffer */ FLASH->ACR |= FLASH_ACR_PRFTBE; /* Flash 0 wait state */ FLASH->ACR &= (uint32_t)((uint32_t)~FLASH_ACR_LATENCY); #ifndef STM32F10X_CL FLASH->ACR |= (uint32_t)FLASH_ACR_LATENCY_0; #else if (HSE_VALUE <= 24000000) { FLASH->ACR |= (uint32_t)FLASH_ACR_LATENCY_0; } else { FLASH->ACR |= (uint32_t)FLASH_ACR_LATENCY_1; } #endif /* STM32F10X_CL */ #endif /* HCLK = SYSCLK */ RCC->CFGR |= (uint32_t)RCC_CFGR_HPRE_DIV1; /* PCLK2 = HCLK */ RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE2_DIV1; /* PCLK1 = HCLK */ RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE1_DIV1; /* Select HSE as system clock source */ RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_SW)); RCC->CFGR |= (uint32_t)RCC_CFGR_SW_HSE; /* Wait till HSE is used as system clock source */ while ((RCC->CFGR & (uint32_t)RCC_CFGR_SWS) != (uint32_t)0x04) { } } else { /* If HSE fails to start-up, the application will have wrong clock configuration. User can add here some code to deal with this error */ } }
Её я комментировать не буду, т.к. мы будем использовать другую функцию: void SetSysClockTo72(). Эта функция сначала переключается на внешний кварц, затем запускает PLL-генератор, после его стабилизации устанавливается PLL-множитель на 9, в результате чего частота SYSCLK устанавливается в значение 72 MHz. Далее устанавливаются делители: для AHB и APB2 шин по единице, для APB1 шины равной двум, т.е. на 36МГц. Кроме того для флеш-памяти устанавливается WaitState равный двум, т.е. флеш-память работает на меньшей частоте чем процессор. Но давайте по порядку.
1. В начале запускается внешний кварц:
/* SYSCLK, HCLK, PCLK2 and PCLK1 configuration ---------------------------*/ /* Enable HSE */ RCC->CR |= ((uint32_t)RCC_CR_HSEON); /* Wait till HSE is ready and if Time out is reached exit */ do { HSEStatus = RCC->CR & RCC_CR_HSERDY; StartUpCounter++; } while((HSEStatus == 0) && (StartUpCounter != HSE_STARTUP_TIMEOUT));
Ниже приведено описание конфигурационного регистра RCC_CR:
Здесь HSION - включает/выключает внутренний 8 МГц генератор. Внутренний генератор невозможно выключить когда от него тактируется SYSCLK. Этот бит аппаратно устанавливается когда микроконтроллер выходит из Stop и Standby режимов или когда перестают поступать сигналы с внешнего генератора. HDIRDY - флаг готовности HSI генератора. HSITRIM и HSICAL - отвечают за подстройку частоты HSI.
HSEON - включает/выключает генератор работающий от внешнего кварца. Не может быть выключен когда от него тактируется SYSCLK. Аппаратно очищается при переходе микроконтроллера в Stop и Standby режимы. HSERDY - флаг готовности генератора. НSEBYP - разрешает работу от внешнего генератора (не путать с кварцем!). Внешний генератор должен быть в диапазоне 4-16 МГц. HSEBYP не может быть установлен если запущен HSE. HSEBYP должен устанавливаться вместе с HSEON битом.
СSSON - включает систему безопасности тактирования. PLLON - включает PLL генератор. Очищается аппаратно при переходе микроконтроллера в Stop и Standby режимы. PLLON не может быть очищен когда используется для тактирования SYSCLK. PLLRDY флаг готовности PLL-генератора.
Т.о. сначала включается HSE генератор, после чего некоторое время ожидается пока он стабилизируется.
Идем далее:
/* Enable Prefetch Buffer */ FLASH->ACR |= FLASH_ACR_PRFTBE; /* Flash 2 wait state */ FLASH->ACR &= (uint32_t)((uint32_t)~FLASH_ACR_LATENCY); FLASH->ACR |= (uint32_t)FLASH_ACR_LATENCY_2;
Здесь включается буфер превыборки и устанавливается время обращения к флеш-памяти. Описание регистра FLASH_ACR приведено ниже:
Здесь Latency задает задержку при обращении к флеш-памяти. Для частоты SYSCLK=72MHz, следует ставить задержку в два такта. HLFCYA разрешает обращение к 16-битным данным, что помогает улучшить быстродействие при частотах 8 MHz и ниже. Его нельзя использовать совместно с PLL. PRFTBE включает буфер превыборки команд, PRFTBS - флаг буфера превыборки.
/* HCLK = SYSCLK */ RCC->CFGR |= (uint32_t)RCC_CFGR_HPRE_DIV1; /* PCLK2 = HCLK */ RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE2_DIV1; /* PCLK1 = HCLK */ RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE1_DIV2;
Здесь на шины AHB и APB2 устанавливаются предделители равные единице. На шину APB1 устанавливается предделитель равный двум.
Здесь SW переключает генератор тактирования SYSCLK. SWS - флаг завершения переключения на новый генератор SYSCLK. HPRE - устанавливает предделитель на AHB шину. PPRE1 устанавливает предделитель на шину APB1. PPRE2 устанавливает предделитель на шину APB2. ADCPRE устанавливает предделитель на АЦП. PLLSRC - выбирает источник опорного тактирования: HSI или HSE. PLLXTPRE - устанавливает предделитель на PLL. PLLMUL - устанавливает множитель на PLL. USBPRE - устанавливает предделитель на USB шину. MCO - выбирает источник выходного тактового сигнала.
Т.о. в следующем коде выбирается HSE в качестве опорного источника тактирования, и устанавливается множитель равный девяти. Т.к. на плате "Blue Pill" установлен кварц на 8 MHz, то умножив это число на 9, получим в итоге частоту SYSCLK = 72 MHz.
/* PLL configuration: PLLCLK = HSE * 9 = 72 MHz */ RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_PLLSRC | RCC_CFGR_PLLXTPRE | RCC_CFGR_PLLMULL)); RCC->CFGR |= (uint32_t)(RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMULL9);
Далее включается PLL и ожидается его готовность:
/* Enable PLL */ RCC->CR |= RCC_CR_PLLON; /* Wait till PLL is ready */ while((RCC->CR & RCC_CR_PLLRDY) == 0) { }
В завершении, тактирование SYSCLK переключается на PLL:
/* Select PLL as system clock source */ RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_SW)); RCC->CFGR |= (uint32_t)RCC_CFGR_SW_PLL; /* Wait till PLL is used as system clock source */ while ((RCC->CFGR & (uint32_t)RCC_CFGR_SWS) != (uint32_t)0x08) { }
Во всем этом хозяйстве не хватает кода включения CSS и генератора низкочастотного генератора LSE, но для начала наверно сойдет. Полностью код файла startup.c можно посмотреть под спойлером, или в архиве в конце статьи.
показать startup.c#include "stm32f10x.h" #include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h" #ifdef SYSCLK_FREQ_HSE /** * @brief Selects HSE as System clock source and configure HCLK, PCLK2 * and PCLK1 prescalers. * @note This function should be used only after reset. * @param None * @retval None */ static void SetSysClockToHSE(void) { __IO uint32_t StartUpCounter = 0, HSEStatus = 0; /* SYSCLK, HCLK, PCLK2 and PCLK1 configuration ---------------------------*/ /* Enable HSE */ RCC->CR |= ((uint32_t)RCC_CR_HSEON); /* Wait till HSE is ready and if Time out is reached exit */ do { HSEStatus = RCC->CR & RCC_CR_HSERDY; StartUpCounter++; } while((HSEStatus == 0) && (StartUpCounter != HSE_STARTUP_TIMEOUT)); if ((RCC->CR & RCC_CR_HSERDY) != RESET) { HSEStatus = (uint32_t)0x01; } else { HSEStatus = (uint32_t)0x00; } if (HSEStatus == (uint32_t)0x01) { #if !defined STM32F10X_LD_VL && !defined STM32F10X_MD_VL && !defined STM32F10X_HD_VL /* Enable Prefetch Buffer */ FLASH->ACR |= FLASH_ACR_PRFTBE; /* Flash 0 wait state */ FLASH->ACR &= (uint32_t)((uint32_t)~FLASH_ACR_LATENCY); #ifndef STM32F10X_CL FLASH->ACR |= (uint32_t)FLASH_ACR_LATENCY_0; #else if (HSE_VALUE <= 24000000) { FLASH->ACR |= (uint32_t)FLASH_ACR_LATENCY_0; } else { FLASH->ACR |= (uint32_t)FLASH_ACR_LATENCY_1; } #endif /* STM32F10X_CL */ #endif /* HCLK = SYSCLK */ RCC->CFGR |= (uint32_t)RCC_CFGR_HPRE_DIV1; /* PCLK2 = HCLK */ RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE2_DIV1; /* PCLK1 = HCLK */ RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE1_DIV1; /* Select HSE as system clock source */ RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_SW)); RCC->CFGR |= (uint32_t)RCC_CFGR_SW_HSE; /* Wait till HSE is used as system clock source */ while ((RCC->CFGR & (uint32_t)RCC_CFGR_SWS) != (uint32_t)0x04) { } } else { /* If HSE fails to start-up, the application will have wrong clock configuration. User can add here some code to deal with this error */ } } #elif defined SYSCLK_FREQ_72MHz /** * @brief Sets System clock frequency to 72MHz and configure HCLK, PCLK2 * and PCLK1 prescalers. * @note This function should be used only after reset. * @param None * @retval None */ static void SetSysClockTo72(void) { /* SYSCLK, HCLK, PCLK2 and PCLK1 configuration ---------------------------*/ /* Enable HSE */ RCC->CR |= ((uint32_t)RCC_CR_HSEON); /* Wait till HSE is ready and if Time out is reached exit */ while (!(RCC->CR & RCC_CR_HSERDY)) /* Enable Prefetch Buffer */ FLASH->ACR |= FLASH_ACR_PRFTBE; /* Flash 2 wait state */ FLASH->ACR &= (uint32_t)((uint32_t)~FLASH_ACR_LATENCY); FLASH->ACR |= (uint32_t)FLASH_ACR_LATENCY_2; /* HCLK = SYSCLK */ RCC->CFGR |= (uint32_t)RCC_CFGR_HPRE_DIV1; /* PCLK2 = HCLK */ RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE2_DIV1; /* PCLK1 = HCLK */ RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE1_DIV2; /* PLL configuration: PLLCLK = HSE * 9 = 72 MHz */ RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_PLLSRC | RCC_CFGR_PLLXTPRE | RCC_CFGR_PLLMULL)); RCC->CFGR |= (uint32_t)(RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMULL9); /* Enable PLL */ RCC->CR |= RCC_CR_PLLON; /* Wait till PLL is ready */ while(!(RCC->CR & RCC_CR_PLLRDY)); /* Select PLL as system clock source */ RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_SW)); RCC->CFGR |= (uint32_t)RCC_CFGR_SW_PLL; /* Wait till PLL is used as system clock source */ while ((RCC->CFGR & (uint32_t)RCC_CFGR_SWS) != (uint32_t)0x08); } #endif void SystemInit (void) { RCC->CR |= (uint32_t)0x00000001; // enable HSI, 8 MHz /* Reset SW, HPRE, PPRE1, PPRE2, ADCPRE and MCO bits */ #ifndef STM32F10X_CL RCC->CFGR &= (uint32_t)0xF8FF0000; #else RCC->CFGR &= (uint32_t)0xF0FF0000; #endif /* STM32F10X_CL */ /* Reset HSEON, CSSON and PLLON bits */ RCC->CR &= (uint32_t)0xFEF6FFFF; /* Reset HSEBYP bit */ RCC->CR &= (uint32_t)0xFFFBFFFF; /* Reset PLLSRC, PLLXTPRE, PLLMUL and USBPRE/OTGFSPRE bits */ RCC->CFGR &= (uint32_t)0xFF80FFFF; #ifdef STM32F10X_CL /* Reset PLL2ON and PLL3ON bits */ RCC->CR &= (uint32_t)0xEBFFFFFF; /* Disable all interrupts and clear pending bits */ RCC->CIR = 0x00FF0000; /* Reset CFGR2 register */ RCC->CFGR2 = 0x00000000; #elif defined (STM32F10X_LD_VL) || defined (STM32F10X_MD_VL) || (defined STM32F10X_HD_VL) /* Disable all interrupts and clear pending bits */ RCC->CIR = 0x009F0000; /* Reset CFGR2 register */ RCC->CFGR2 = 0x00000000; #else /* Disable all interrupts and clear pending bits */ RCC->CIR = 0x009F0000; #endif /* STM32F10X_CL */ #if defined (STM32F10X_HD) || (defined STM32F10X_XL) || (defined STM32F10X_HD_VL) #ifdef DATA_IN_ExtSRAM SystemInit_ExtMemCtl(); #endif /* DATA_IN_ExtSRAM */ #endif /* Configure the System clock frequency, HCLK, PCLK2 and PCLK1 prescalers */ /* Configure the Flash Latency cycles and enable prefetch buffer */ // SetSysClock(); #ifdef SYSCLK_FREQ_HSE SetSysClockToHSE(); #elif defined SYSCLK_FREQ_24MHz SetSysClockTo24(); #elif defined SYSCLK_FREQ_36MHz SetSysClockTo36(); #elif defined SYSCLK_FREQ_48MHz SetSysClockTo48(); #elif defined SYSCLK_FREQ_56MHz SetSysClockTo56(); #elif defined SYSCLK_FREQ_72MHz SetSysClockTo72(); #endif #ifdef VECT_TAB_SRAM SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM. */ #else // SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH. */ #endif }
Теперь вызов функции SystemInit() следует добавить в обработчик прерывания Reset:
.section .text.Reset_Handler .weak Reset_Handler .type Reset_Handler, %function Reset_Handler: ldr r0, =_estack mov sp, r0 /* set stack pointer */ cpsid i @ disable irq ldr r0, =SystemInit blx r0 b main
Осталось обновить Makefile:
MCU=cortex-m3 OBJCOPY=arm-none-eabi-objcopy CC=arm-none-eabi-gcc LD=arm-none-eabi-ld SIZE=arm-none-eabi-size INC = -ICMSIS/device INC += -ICMSIS/core INC += -ISPL/inc DEF = -DSTM32F10X_MD DEF +=-DSYSCLK_FREQ_72MHz CFLAGS=-mthumb -mcpu=$(MCU) -g -O0 $(DEF) $(INC) ASFLAGS=-mthumb -mcpu=$(MCU) -g LDFLAGS=-Tscript.ld OBJ=main.o init.o startup.o TARGET=blink .PHONY: all clean %.o: %.c $(CC) -c -o $@ $< $(CFLAGS) %.o: src/%.c $(CC) -c -o $@ $< $(CFLAGS) %.o: asm/%.s $(CC) -c -o $@ $< $(ASFLAGS) all: $(OBJ) $(LD) $(LDFLAGS) -g -o $(TARGET).elf $(OBJ) $(OBJCOPY) -O binary $(TARGET).elf $(TARGET).bin $(SIZE) $(TARGET).elf install: st-flash write $(TARGET).bin 0x08000000 clean: @rm -v $(TARGET).elf $(TARGET).bin $(OBJ)
После сборки проекта, вес прошивки увеличивается до ~700 байт.
Если все было сделано правильно, то после прошивки светодиод должен начать мигать в девять раз быстрее, что означает, что микроконтроллер работает на частоте 72 МНz.
Теперь, когда мы добрались до желанных до 72 MHz, хочется узнать, какова же реальная производительность такого микроконтроллера. Адепты STM32 при любом случае кричат, что 72 МHz это в несколько раза быстрее чем 16, но про влияние значения waitstate на производительность CPU, я упоминаний как-то не встречал.
Самым простым тестом производительности будет функция задержки на ассемблерных инструкциях. Маркером здесь будет служить число итераций которые микроконтроллер выполняет за 1 мс. В данном случае мы не учитываем тактовую частоту каждого микроконтроллера т.к. это не объективный показатель.
Итак, для реализации функции задержки добавим в ассемблерный файл asm/init.s следующий код:
.global delay delay: push {r1} l1: mov.w r1,#10285 @ 72000/7 lp: subs r1,#1 bne lp subs r0,#1 bne l1 pop {r1} bx lr
Тогда main.c примет такой вид:
#include "stm32f10x.h" #include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h" extern void delay(uint32_t ms); int main() { // enable GPIOC port RCC->APB2ENR |= RCC_APB2Periph_GPIOC; // --- GPIO setup ---- GPIOC->CRH &= ~(uint32_t)(0xf<<20); GPIOC->CRH |= (uint32_t)(0x2<<20); for(;;){ GPIOC->ODR ^= GPIO_Pin_13; delay(1000); } }
Как видно, цикл задержки проходит 10285 итераций за 1 мс, т.е. две инструкции выполняются вкупе за 7 тактов.
Так же функция на STM8 выполняется за 5333 итераций
.globl _delay _delay: ldw x, (03,sp) l0: ldw y, #5328 ; ->(16000/3 - 5) l1: decw y jrne l1 decw x jrne l0 ret
Т.о. на первый взгляд реальная производительность STM32 "всего" в два раза выше восьмибитного STM8. Это конечно не объективная оценка. Объективную оценку мы получим когда запустим RTOS и посмотрим загруженность системы.
Задержка на инструкциях все-таки специфическая вещь, счетчик цикла итераций на самом деле это не бенчмарк никакой. Для более точного определения производительности микроконтроллера нам понадобится функция задержки на системном таймере SysTick
Про регистры таймера SysTick как и про него самого можно почитать здесь: ARM. Учебный Курс. SysTick — Системный таймер | Электроника для всех
Реализация функция задержки на таймере SysTick у меня полностью поместилось в файле main.c:
#include "stm32f10x.h" #include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h" extern void delay(uint32_t ms); static __IO uint32_t s_timer; void SysTick_Handler(void) { if (s_timer) s_timer--; } void delay_ms(__IO uint32_t val) { // ------- SysTick CONFIG -------------- if (SysTick_Config(72000)) // set 1ms { while(1); // error } s_timer=val; while(s_timer) { asm("wfi"); }; SysTick->LOAD &= ~(SysTick_CTRL_ENABLE_Msk); // disable SysTick } int main() { // enable GPIOC port RCC->APB2ENR |= RCC_APB2Periph_GPIOC; // --- GPIO setup ---- GPIOC->CRH &= ~(uint32_t)(0xf<<20); GPIOC->CRH |= (uint32_t)(0x2<<20); // Let's go.. __enable_irq(); for(;;){ GPIOC->ODR ^= GPIO_Pin_13; delay_ms(1000); } }
Реализация используемой функции SysTick_Config() находится в CMSIS, так что никаких дополнительных файлов и библиотек подключать не надо. После сборки прошивка весит уже 990 байт. Таблицу прерываний в asm/init.s трогать не нужно. После компиляции удостоверьтесь, что адрес обработчика прерывания SysTick указывает именно на функцию SysTick_Handler().
Далее для вывода отладочной информации нам понадобится UART интерфейс. Работа с UART модулем в STM32 мало чем отличается от своего аналога в STM8, разве что только тем, здесь их три. При этом только USART1 тактируется от скоростной APB2 шины, остальные два тактируются от APB1.
Настройка UART через регистры подробнейшем образом разобрана в статье: ARM Учебный курс. USART | Электроника для всех. От STM8 процедура настройки отличается необходимостью включать альтернативный режим работы GPIO и немного другой формулой расчета регистра установки битрейта USART1->BRR.
Настройку USART1 интерфейса STM32 в режиме передатчика я поместил в файл main.c:
#include "stm32f10x.h" #include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h" #include "uart.h" extern void delay(uint32_t ms); static __IO uint32_t s_timer; void SysTick_Handler(void) { if (s_timer) s_timer--; } void delay_ms(__IO uint32_t val) { // ------- SysTick CONFIG -------------- if (SysTick_Config(72000)) // set 1ms { while(1); // error } s_timer=val; while(s_timer) { asm("wfi"); }; SysTick->LOAD &= ~(SysTick_CTRL_ENABLE_Msk); // disable SysTick } int main() { // enable GPIOC & GPIOA port RCC->APB2ENR |= RCC_APB2Periph_GPIOC; // enable PORT_C RCC->APB2ENR |= RCC_APB2Periph_GPIOA; // enable PORT_A RCC->APB2ENR |= RCC_APB2Periph_USART1; // enable UART1 // --- GPIO setup ---- GPIOC->CRH &= ~(uint32_t)(0xf<<20); GPIOC->CRH |= (uint32_t)(0x2<<20); // enable PushPull mode GPIOA->CRH &= ~(uint32_t)(0xf<<4); // enable Alterentive mode GPIOA->CRH |= (uint32_t)(0xa<<4); // for PA9 = USART1_TX // --- UART setup ---- //USART1->BRR = 0x1d4c; // 9600 Baud, when APB2=72MHz USART1->BRR = 0x271; // 115200 Baud, when APB2=72MHz USART1->CR1 |= (USART_CR1_UE_Set | USART_Mode_Tx); // enable USART1, enable TX mode // Let's go.. __enable_irq(); uint32_t i=0; for(;;){ GPIOC->ODR ^= GPIO_Pin_13; delay_ms(1000); usart1_print_string("count: "); usart1_print_number(i++); usart1_send_char('\n'); } }
Нам понадобится заголовочный файл из SPL stm32f10x_usart.h в котором содержатся некоторые битовые маски регистров. К сожалению не все битовые маски содержались в заголовочном файле. Часть находилась в stm32f10x_usart.c. Их пришлось перенести в свой заголовочный файл uart.h:
#ifndef __UART_H__ #define __UART_H__ #include "stm32f10x.h" #include "stm32f10x_usart.h" #define USART_CR1_UE_Set ((uint16_t)0x2000) /*!< USART Enable Mask */ #define USART_CR1_UE_Reset ((uint16_t)0xDFFF) /*!< USART Disable Mask */ #define USART_CR1_WAKE_Mask ((uint16_t)0xF7FF) /*!< USART WakeUp Method Mask */ #define USART_CR1_RWU_Set ((uint16_t)0x0002) /*!< USART mute mode Enable Mask */ #define USART_CR1_RWU_Reset ((uint16_t)0xFFFD) /*!< USART mute mode Enable Mask */ #define USART_CR1_SBK_Set ((uint16_t)0x0001) /*!< USART Break Character send Mask */ #define USART_CR1_CLEAR_Mask ((uint16_t)0xE9F3) /*!< USART CR1 Mask */ #define USART_CR2_Address_Mask ((uint16_t)0xFFF0) /*!< USART address Mask */ #define USART_CR2_LINEN_Set ((uint16_t)0x4000) /*!< USART LIN Enable Mask */ #define USART_CR2_LINEN_Reset ((uint16_t)0xBFFF) /*!< USART LIN Disable Mask */ #define USART_CR2_LBDL_Mask ((uint16_t)0xFFDF) /*!< USART LIN Break detection Mask */ #define USART_CR2_STOP_CLEAR_Mask ((uint16_t)0xCFFF) /*!< USART CR2 STOP Bits Mask */ #define USART_CR2_CLOCK_CLEAR_Mask ((uint16_t)0xF0FF) /*!< USART CR2 Clock Mask */ #define USART_CR3_SCEN_Set ((uint16_t)0x0020) /*!< USART SC Enable Mask */ #define USART_CR3_SCEN_Reset ((uint16_t)0xFFDF) /*!< USART SC Disable Mask */ #define USART_CR3_NACK_Set ((uint16_t)0x0010) /*!< USART SC NACK Enable Mask */ #define USART_CR3_NACK_Reset ((uint16_t)0xFFEF) /*!< USART SC NACK Disable Mask */ #define USART_CR3_HDSEL_Set ((uint16_t)0x0008) /*!< USART Half-Duplex Enable Mask */ #define USART_CR3_HDSEL_Reset ((uint16_t)0xFFF7) /*!< USART Half-Duplex Disable Mask */ #define USART_CR3_IRLP_Mask ((uint16_t)0xFFFB) /*!< USART IrDA LowPower mode Mask */ #define USART_CR3_CLEAR_Mask ((uint16_t)0xFCFF) /*!< USART CR3 Mask */ #define USART_CR3_IREN_Set ((uint16_t)0x0002) /*!< USART IrDA Enable Mask */ #define USART_CR3_IREN_Reset ((uint16_t)0xFFFD) /*!< USART IrDA Disable Mask */ #define USART_GTPR_LSB_Mask ((uint16_t)0x00FF) /*!< Guard Time Register LSB Mask */ #define USART_GTPR_MSB_Mask ((uint16_t)0xFF00) /*!< Guard Time Register MSB Mask */ #define USART_IT_Mask ((uint16_t)0x001F) /*!< USART Interrupt Mask */ /* USART OverSampling-8 Mask */ #define CR1_OVER8_Set ((u16)0x8000) /* USART OVER8 mode Enable Mask */ #define CR1_OVER8_Reset ((u16)0x7FFF) /* USART OVER8 mode Disable Mask */ /* USART One Bit Sampling Mask */ #define CR3_ONEBITE_Set ((u16)0x0800) /* USART ONEBITE mode Enable Mask */ #define CR3_ONEBITE_Reset ((u16)0xF7FF) /* USART ONEBITE mode Disable Mask */ void usart1_send_char(uint32_t ch); void usart1_print_string(char *str); void usart1_print_number(uint32_t num); #endif
Модуль uart.c практически не отличается от своего аналога для STM8:
#include "uart.h" #define len 8 void usart1_print_number(uint32_t num){ uint8_t n[len]; char *s=n+(len-1); *s=0; // EOL do { *(--s)=(uint32_t)(num%10 + 0x30); num=num/10; } while (num>0); usart1_print_string(s); } void usart1_send_char(uint32_t ch) { USART1->DR=ch; while(!(USART1->SR & USART_FLAG_TXE)); } void usart1_print_string(char *str) { while (*str) { usart1_send_char((uint32_t)*str++); } }
В связи с добавлением новых файлов: "uart.c", "uart.h" и директории "inc" в проект, Makefile также немного поменялся:
MCU=cortex-m3 OBJCOPY=arm-none-eabi-objcopy CC=arm-none-eabi-gcc LD=arm-none-eabi-ld SIZE=arm-none-eabi-size INC = -ICMSIS/device INC += -ICMSIS/core INC += -ISPL/inc INC += -Iinc DEF = -DSTM32F10X_MD DEF +=-DSYSCLK_FREQ_72MHz CFLAGS=-mthumb -mcpu=$(MCU) -g -O0 $(DEF) $(INC) ASFLAGS=-mthumb -mcpu=$(MCU) -g LDFLAGS=-Tscript.ld OBJ=main.o init.o startup.o uart.o TARGET=blink .PHONY: all clean %.o: %.c $(CC) -c -o $@ $< $(CFLAGS) %.o: src/%.c $(CC) -c -o $@ $< $(CFLAGS) %.o: asm/%.s $(CC) -c -o $@ $< $(ASFLAGS) all: $(OBJ) $(LD) $(LDFLAGS) -g -o $(TARGET).elf $(OBJ) $(OBJCOPY) -O binary $(TARGET).elf $(TARGET).bin $(SIZE) $(TARGET).elf install: st-flash write $(TARGET).bin 0x08000000 clean: @rm -v $(TARGET).elf $(TARGET).bin $(OBJ)
Общая структура проекта теперь выглядит так:
$ tree . . ├── CMSIS │ ├── core │ │ ├── core_cm3.c │ │ └── core_cm3.h │ └── device │ ├── stm32f10x.h │ └── system_stm32f10x.h ├── Makefile ├── SPL │ └── inc │ ├── stm32f10x_gpio.h │ ├── stm32f10x_rcc.h │ └── stm32f10x_usart.h ├── asm │ └── init.s ├── blink.bin ├── blink.elf ├── inc │ └── uart.h ├── init.o ├── main.c ├── main.o ├── script.ld ├── src │ ├── startup.c │ └── uart.c ├── startup.o └── uart.o 8 directories, 20 files
После компиляции прошивка теперь "весит" 1298 байт:
$ make all arm-none-eabi-gcc -c -o main.o main.c -mthumb -mcpu=cortex-m3 -g -O0 -DSTM32F10X_MD -DSYSCLK_FREQ_72MHz -ICMSIS/device -ICMSIS/core -ISPL/inc -Iinc arm-none-eabi-gcc -c -o init.o asm/init.s -mthumb -mcpu=cortex-m3 -g arm-none-eabi-gcc -c -o startup.o src/startup.c -mthumb -mcpu=cortex-m3 -g -O0 -DSTM32F10X_MD -DSYSCLK_FREQ_72MHz -ICMSIS/device -ICMSIS/core -ISPL/inc -Iinc arm-none-eabi-gcc -c -o uart.o src/uart.c -mthumb -mcpu=cortex-m3 -g -O0 -DSTM32F10X_MD -DSYSCLK_FREQ_72MHz -ICMSIS/device -ICMSIS/core -ISPL/inc -Iinc arm-none-eabi-ld -Tscript.ld -g -o blink.elf main.o init.o startup.o uart.o arm-none-eabi-objcopy -O binary blink.elf blink.bin arm-none-eabi-size blink.elf text data bss dec hex filename 1298 0 4 1302 516 blink.elf
Подключение USB-UART адаптера к Bluepill следующее:RX(адаптера) к PA9(bluebill), GND(адаптера) к GND(bluepill):
Когда я тестировал свои 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 с этим тагретом работать отказывалась.
FT232H Board
JTAG - интерфейс для тестирования печатных плат. Процесс тестирования называется - граничным сканированием (boundary scan). Данная технология позволяет определять непропай/замыкание дорожек или контактов без физического доступа к этим самым контактам. Это классная штука когда вам нужно протестировать BGA чип или чип в безвыводном корпусе, или вы запускаете микро/мелко/гига -серийное производство и вам нужно выявлять брак прямо на конвейере. На youtube есть ролики с процессом тестирования, посмотрите, не ленитесь. Нас же пока будет интересовать "побочная" сторона JTAG - возможность прошивки и отладки микроконтроллера.
Для этого я буду использовать плату с чипом FT232H. Этот чип - является преобразователем интерфейсов USB в UART, JTAG, SPI, I2C, bit-bang и т.е. Т.е. это такой универсальный "швейцарский нож", который будет вам UART преобразователем, JTAG - флешером, отладчиком, программатором SPI флешек и т.к. В какой-то мере его можно сравнить с микроконтроллером, но вся его управляющая программа находится на стороне компьютера. Нас будет интересовать возможность чипа работать в качестве JTAG - адаптера. В отличии от ST-LINK c протоколом SWD, JTAG'ом можно "подцепиться" к очень широкому спектру устройств. Собственно об плате я узнал из статьи: Бюджетный отладчик к ESP-32 и его настройка где речь шла об отладке ESP32. Мы же будем тренироваться на кошках на STM32.
Почитать об FT232H можно на хабре: FT232H, MPSSE и SPI-программатор за 15 евр, или здесь: FT232H и почти универсальный USB<->JTAG-адаптер за 15 евро
Большинство аппаратных отладчиков сделаны на чипе FT2232H, он имеет два канала, т.е. чип может служить и JTAG-адаптером и UART-конвертером, но для наших целей сгодится и одноканальный FT232H.
Я лично плату покупал на али за 500р с копейками, но хочу обратить внимание, что Adafruit выпускает аналогичную плату: Adafruit FT232H Breakout - General Purpose USB to GPIO+SPI+I2C . У них есть любопытный туториал по работе с платой на Python. Платы полностью совместимы друг с другом, так что не стесняйтесь, и выполните хотя бы несколько упражнений для знакомства с железкой.
В Linux, для работы с чипом нам понадобится библиотека libftdi1. В комплекте OpenOCD имеется файл "/usr/share/openocd/contrib/60-openocd.rules" c правилами для udev, в котором имеется правило и для FT232H:
# Original FT232H VID:PID ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6014", MODE="660", GROUP="plugdev", TAG+="uaccess"
Для доступа к железке из под непривилегированного пользователя, этот файл следует скопировать в /lib/udev/rules.d после чего нужно будет перезагрузить правила командой:
# udevadm control --reload-rules && udevadm trigger
Плата имеет следующую распиновку:
В руководстве на чип ищем JTAG пины: TMS, TCK, TDO, TDI:
Согласно руководству на чип stm32f103c8, JTAG пины TMS и TCL разделяют те же пины, что и SWDIO и SWCLK, куда мы подключали ST-LINK. JTDI это PA15, а JTDO это PB3.
Т.о. подключение FT232H к STM32F103C8 будет следующим: 3.3V к VCC, GND к GND, AD0 к SWCLK, AD3 к SWDIO, AD1 к PA15, AD2 к PB3.
Подключаем плату с FT232H к компьютеру, и видим такой лог:
[363652.166692] usb 1-2.2: new high-speed USB device number 81 using ehci-pci [363652.252481] usb 1-2.2: New USB device found, idVendor=0403, idProduct=6014 [363652.252492] usb 1-2.2: New USB device strings: Mfr=1, Product=2, SerialNumber=0 [363652.252500] usb 1-2.2: Product: Single RS232-HS [363652.252506] usb 1-2.2: Manufacturer: FTDI [363652.253036] ftdi_sio 1-2.2:1.0: FTDI USB Serial Device converter detected [363652.253110] usb 1-2.2: Detected FT232H [363652.253474] usb 1-2.2: FTDI USB Serial Device converter now attached to ttyUSB0
Создаем файл с описанием интерфейса - interface.cfg, следующего содержания:
interface ftdi ftdi_vid_pid 0x0403 0x6014 ftdi_layout_init 0x0c08 0x0f1b
Подключаемся:
$ openocd -f ./interface.cfg -f target/stm32f1x.cfg -c "init" -c "reset halt" Open On-Chip Debugger 0.10.0 Licensed under GNU GPL v2 For bug reports, read http://openocd.org/doc/doxygen/bugs.html Info : auto-selecting first available session transport "jtag". To override use 'transport select <transport>'. adapter speed: 1000 kHz adapter_nsrst_delay: 100 jtag_ntrst_delay: 100 none separate cortex_m reset_config sysresetreq Info : clock speed 1000 kHz Info : JTAG tap: stm32f1x.cpu tap/device found: 0x3ba00477 (mfg: 0x23b (ARM Ltd.), part: 0xba00, ver: 0x3) Info : JTAG tap: stm32f1x.bs tap/device found: 0x16410041 (mfg: 0x020 (STMicroelectronics), part: 0x6410, ver: 0x1) Info : stm32f1x.cpu: hardware has 6 breakpoints, 4 watchpoints Info : JTAG tap: stm32f1x.cpu tap/device found: 0x3ba00477 (mfg: 0x23b (ARM Ltd.), part: 0xba00, ver: 0x3) Info : JTAG tap: stm32f1x.bs tap/device found: 0x16410041 (mfg: 0x020 (STMicroelectronics), part: 0x6410, ver: 0x1) target halted due to debug-request, current mode: Thread xPSR: 0x01000000 pc: 0x08000b4c msp: 0x20005000
Поздравляю, вы в матрице ;) Далее все действия аналогичны как при отладке через ST-LINK.
Напоминаю, что посмотреть исходники, сборочные файлы, скачать скомпилированные прошивки, можно с портала GITLAB https://gitlab.com/flank1er/stm32_bare_metal