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

"Blue Pill" - плата с микроконтроллером STM32F103C8T6
Внимание! На данный момент(май-июнь 2025г.) статья находится в стадии редактирования. Первоначальную версию статьи можно открыть по следующей ссылке.
Когда я впервые начал знакомиться с микроконтроллерами семейства STM32, то обратил внимание на то, что прошивки даже с самыми простыми алгоритмами (например Blink), по меркам 8-битных микроконтроллеров, имеют огромный размер: от одного килобайта и больше. Поэтому целью этой статьи стала попытка написания прошивок для STM32 в стиле 8-битных микроконтроллеров, когда вы полностью контролируете процесс компиляции, используя лишь: компилятор, флешер и текстовый редактор. Соответственно в статье рассматриваются типовые на мой взгляд вопросы при переходе с 8-битников на 32-разрядную архитектуру: как помигать светодиодом, как настроить тактирование, как завести SPI и поднять I2C.
Если вам чего-то из этого не хватает, то вы легко сможете подтянуть "матчасть" по статьям на хабре: STM32F4: GNU AS: Программирование на ассемблере в семи частях, по методичке "Народная электроника" выпуск 2. А.В. Немоляев. GCC Cortex-M3. PDF, или по книге "Джозеф Ю. Ядро Cortex - МЗ компании ARM. Полное руководство", Козаченко В.Ф.(ред.): "Практический курс микропроцессорной техники на базе процессорных ядер ARM-Cortex-M3/M4/M4F" 2019 М.: МЭИ
Оборудование. В статье я буду использовать популярную плату "Blue Pill" на микроконтроллере STM32F103C8T6, программатор ST-LINK v2 (китайская реплика), USB-UART преобразователь FT232RL, 4-x разрядный семи-сегментный индикатор, на SPI интерфейсе и RTC DS3231 на I2C интерфейсе.
Список используемой документации:
- Перевод на русский язык руководства по программированию STM32F10x и справочного руководства по STM32F105-107 альтернативная ссылка
-
Cortex-M3 TRM Cortex-M3 Technical Reference Manual
-
Cortex-M3: Руководство программиста (PM0056), для чипов серий: STM32F10xxx/20xxx/21xxx/L1xxxx.
-
Справочное руководство (Reference Manual: RM0008), для чипов следующих серий: STM32F101xx, STM32F102xx, STM32F103xx, STM32F105xx and STM32F107xx advanced Arm.
-
Datasheet на чипы: STM32F103x8/STM32F103xB.
-
STM32F10xxx I2C optimized examples, Application note AN2824
Содержание:
I. Создание базового проекта для STM32F103xx
-
Вместо введения
-
Минималистический Blink размером в 148 байт
-
Небольшой ликбез по ассемблеру Cortex-M3
-
Таблица векторов
-
Настройка системы тактирования - RCC (Reset and Clock Control)
II. Работа с периферией микроконтроллера STM32F103xx
-
Функция задержки на ассемблерных инструкциях
-
Функция задержки на прерывании таймера SysTick
-
Настройка и передача данных по UART
-
Прием строки через UART интерфейс
-
Числа с плавающей запятой, подключение стандартной библиотеки Си к проекту
-
USART в режиме SPI-master
-
USART + DMA в режиме SPI-master
-
Битовая память в Cortex-M3
-
Semihosting
-
Настройка аппаратного интерфейса SPI для драйвера 4-х разрядного семисегментного индикатора
-
Регистры I2C интерфейса, делаем сканер I2C шины
-
Однобайтный режим чтения по шине I2C
-
Двухбайтный режим чтения по шине I2C
-
Запись массива через шину I2C
-
Чтение массива через шину I2C
Продолжение цикла STM32F103C8 без HAL и SPL:
-
Работа с SPI дисплеями Nokia_5110 и ST7735
-
Работа с монохромными дисплеями STE2007 и SSD1306
-
Вывод текста на дисплей ST7735
При написании примеров я использовал кросс-компилятор arm-none-eabi-gcc версии 10.3 скачанный с сайта ARM. Данная версия сейчас (май 2025г.) объявлена устаревшей (deprecated), однако у меня она работала стабильно, потому приведу альтернативные ссылки для скачивания: версия 10.3 тарбол для linux x86_64, zip-архив для win32. Актуальная версия компилятора - 14.2 тарбол для linux x86_64, и zip-архив для win64.
Скачать исходники с демо-проектами к обновленным главам статьи можно по ссылке. Лицензия: "делай с этим что хочешь".
1) Вместо введения
В настоящее время (2025 год), доступно большое число микроконтролеров с архитектурой Cortex-M3. Это STM32F1xx, их реплики, клоны, и различные "мутанты" с непонятными характеристиками. Данные изделия предлагаются производителями со своими библиотеками и средами разработки. В текущей ситуации нужно уметь создать проект с нуля, имея на руках только даташит. Нужно уметь интегрировать этот проект в современную среду разработки, дабы не прыгать из одной программы в другую. Так или иначе, STM32F103xx сейчас является образцом, который копируют другие производители, и который следует знать.
Началось все с того, что меня несколько обескуражил размер прошивки минимального проекта в TrueStudio (после покупки компанией ST была переименованна в STM32CubeIDE) - 1572 байт:
В SW4STM32 получалась какая-то такая же цифра, при этом у меня в настройках проекта была выставлена опция: --gc-section, которая даёт команду компоновщику удалять неиспользуемый код:
Даже в Arduino-STM32 Blink "весил" в пределах одного килобайта.
Сначала я подумал, что все дело в используемой SPL и если переписать код на регистрах, то ситуация исправится. Давайте посмотрим так ли это.
Первая вызываемая функция имеет такой вид:
void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState)
{
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;
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));
currentmode = ((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x0F);
if ((((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x10)) != 0x00)
{
assert_param(IS_GPIO_SPEED(GPIO_InitStruct->GPIO_Speed));
currentmode |= (uint32_t)GPIO_InitStruct->GPIO_Speed;
}
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;
currentpin = (GPIO_InitStruct->GPIO_Pin) & pos;
if (currentpin == pos)
{
pos = pinpos << 2;
pinmask = ((uint32_t)0x0F) << pos;
tmpreg &= ~pinmask;
tmpreg |= (currentmode << pos);
if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD)
{
GPIOx->BRR = (((uint32_t)0x01) << pinpos);
}
else
{
if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU)
{
GPIOx->BSRR = (((uint32_t)0x01) << pinpos);
}
}
}
}
GPIOx->CRL = tmpreg;
}
if (GPIO_InitStruct->GPIO_Pin > 0x00FF)
{
tmpreg = GPIOx->CRH;
for (pinpos = 0x00; pinpos < 0x08; pinpos++)
{
pos = (((uint32_t)0x01) << (pinpos + 0x08));
currentpin = ((GPIO_InitStruct->GPIO_Pin) & pos);
if (currentpin == pos)
{
pos = pinpos << 2;
pinmask = ((uint32_t)0x0F) << pos;
tmpreg &= ~pinmask;
tmpreg |= (currentmode << pos);
if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD)
{
GPIOx->BRR = (((uint32_t)0x01) << (pinpos + 0x08));
}
if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU)
{
GPIOx->BSRR = (((uint32_t)0x01) << (pinpos + 0x08));
}
}
}
GPIOx->CRH = tmpreg;
}
}
По большому счету, эта функция устанавливает значение регистров: GPIOx->CRL или/и GPIOx->CRH. Вспоминаем, что это за регистры:
Регистр GPIOx->CRL конфигурирует режим пинов от 0 до 7. Регистр GPIOx->CRH конфигурирует режим пинов от 8 до 15. Т.к. светодиод на Blue Pill подключён к PC13, то нам нужен регистр GPIOС->CRH. Чтобы его сконфигурировать. В поле MOD13[1:0] можно задать максимальную частоту переключения нужного пина в Output режиме. Полагаю 2МГц будет вполне достаточно, значит записываем в него значение 2. По умолчанию, в режиме выхода поле CNF13[1:0] конфигурирует пин в Push-Pull режим, что нас вполне устраивает, следовательно оставляем там по нулям.
Т.о. вызов функции GPIO_Init(GPIOC, &GPIO_InitStructure) можно заменить следующей парой строк:
GPIOC->CRH &= ~(uint32_t)(0xf<<20);
GPIOC->CRH |= (uint32_t)(0x2<<20);
Переключение пина осуществляется через регистр GPIOx->ODR:
Т.е. вызов функции: "GPIO_SetBits(GPIOC,GPIO_Pin_13)" можно заменить на: "GPIOC->ODR |= GPIO_Pin_13".
В Cortex-M3 нет тех удобных битовых инструкций что были в STM8 (имеется ввиду bset,bres,bcpl). Поэтому для побитового изменения состояния регистра GPIOx->ODR, были специально введены дополнительные регистры: GPIOx->BSRR и GPIOx->BRR. Посмотрим на их описание:
Эти регистры управляют состоянием GPIOx->ODR путем записи единицы в их соответствующий разряд. Т.е. вместо GPIOC->ODR &= ~(GPIO_Pin_13); можно использовать GPIOC->BRR = GPIO_Pin_13. Такой вариант разложится компилятором в ОДНУ ассемблерную инструкцию, вместо трёх, если делать непосредственно через GPIOC->ODR регистр. Обратите внимание, что через BSRR/BRR регистры одной инструкцией можно менять состояние сразу нескольких пинов порта. При желании можно организовать параллельную шину через bit-bang.
В итоге, функция main() примет вид:
int main()
{
RCC->APB2ENR |= RCC_APB2Periph_GPIOC;
GPIOC->CRH &= ~(uint32_t)(0xf<<20);
GPIOC->CRH |= (uint32_t)(0x2<<20);
for(;;){
GPIOC->BSRR=GPIO_Pin_13;
dummy_loop(600000);
GPIOC->BRR=GPIO_Pin_13;
dummy_loop(600000);
}
}
Примечание от 2025г. Вместо использования непонятных цифр при конфигурации регистра ввода-вывода "GPIOC->CRH", можно было использовать именованные константы определенные в заголовочных файлах SPL:
2
GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13);
GPIOC->CRH |= GPIO_CRH_MODE13_1; 2
После компиляции размер прошивки сократился до 1084 байт. Уже лучше, но все-равно многовато. После дизассеблирования, становится ясно, к прошивке кроме нашего кода добавляется ещё стандартная библиотека Си: libc или newlib (облегчённый вариант стандартной библиотеки Cи), а также некоторые функции конфигурации микроконтроллера (startup files) включая таблицу прерываний. Все вместе это и занимает один килобайт. Ничего не имею против таблицы, но от всего остального (библиотеки libc и startup файлов) хотелось бы избавиться. В TrueStudio у нас нет полного контроля за Makefile'ом, поэтому предлагаю закрыть эту IDE и перейти в консоль.
2) Минималистичный Blink размером в 148 байт
Для начала давайте разберемся с форматом бинарной прошивки, т.е. с чего процессор начинает работу и как он находит программу. Потому-что, как вы наверно догадываетесь, прошивка в ARM начинается не с программного кода, и даже не с таблицы векторов.
Те, кто внимательно читали документацию, знают, что прошивка для Cortex-M3 обязательно должна начинаться со значения указателя стека, после чего должны идти адреса обработчиков прерываний: Reset, NMI, Hard Fault:
На адрес 0x00000000 происходит маппинг: или флеш-памяти или ОЗУ, в зависимости от конфигурации boot-пинов. Флеш-память начинается с адреса 0x08000000.
Т.о. минимальный вариант программы Blink для stm32f103 можно записать следующим образом:
#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
#define WAIT 600000
asm(".word 0x20005000\n\t"
".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 fault_irq() {
while (1);
}
void dummy_loop(volatile uint32_t count) {
while (--count);
}
int main() {
RCC->APB2ENR |= RCC_APB2Periph_GPIOC;
2
GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13);
GPIOC->CRH |= GPIO_CRH_MODE13_1; 2
while (1) {
GPIOC->BSRR = GPIO_Pin_13;
dummy_loop(WAIT);
GPIOC->BRR = GPIO_Pin_13;
dummy_loop(WAIT);
}
}
Здесь, мы в начале программы пишем заголовок, в который включены: значение стека и адреса обязательных векторов прерываний. Среди векторов прерываний выделется Reset, который указывает на начало программы.
Так же как в AVR, где для включения имен регистров ввода-вывода и констант пишется:
#include <avr/io.h>
В STM32 используется аналогичные включения:
#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
В файле "stm32f10x.h" содержатся физические адреса регистров, а в "stm32f10x_gpio.h" и "stm32f10x_rcc.h" описаны структуры, маски и константы, чтобы к регистрам ввода-вывода можно было обращаться в стиле "RCC->APB2ENR = какое-то значение". Данные заголовочные файлы были взяты из SPL для stm32f103xx, они не содержат код, только адреса и структуры.
Таблица векторов в Cortex-M3 это просто массив c адресами обработчиков прерываний. Таких инструкций как: INT и IRET, в Cortex-M3 не существует. Все адреса прерываний должны быть нечётными! Т.е. единица прибавляемая к адресу метки нужна для получения нечётного числа, чтобы указать, что инструкция в обработчике прерывания из набора Thumb/Thumb2, а не из 32-битных инструкций ARM. Если вектор будет указывать на чётный адрес, то переход по нему приведёт к срабатыванию прерывания Usage Fault.
Пока на данном этапе, структура проекта выглядит так:
$ tree .
.
├──
│ ├──
│ │ ├── core_cm3.c
│ │ └── core_cm3.h
│ └──
│ ├── stm32f10x.h
│ └── system_stm32f10x.h
├──
│ └──
│ ├── stm32f10x_gpio.h
│ └── stm32f10x_rcc.h
└── main.c
5 directories, 7 files
Для компиляции прошивки под ARM-микроконтроллер потребуется скрипт компоновщика. В AVR он тоже требуется, но обычно входит в пакет компилятора, и никто о нем не думает. Что бы не кидаться сразу многостраничным скриптом компоновщика, про который нам в принципе нужно знать совем немного, я взял скрипт из шаблонного проекта SW4STM32 и сократил его настолько, что бы данный Blink просто напросто собрался:
/* Highest address of the user mode stack */
_estack = 0x20005000; /* end of RAM */
/* Memories definition */
MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K
ROM (rx) : ORIGIN = 0x8000000, LENGTH = 64K
}
/* Sections */
SECTIONS
{
/* The program code and other data into ROM memory */
.text :
{
. = ALIGN(4);
*(.text) /* .text sections (code) */
} >ROM
/* Constant data into ROM memory*/
.rodata :
{
. = ALIGN(4);
*(.rodata) /* .rodata sections (constants, strings, etc.) */
} >ROM
/* Initialized data sections into RAM memory */
.data :
{
. = ALIGN(4);
*(.data) /* .data sections */
} >RAM AT> ROM
/* Uninitialized data section into RAM memory */
. = ALIGN(4);
.bss :
{
*(.bss)
*(COMMON)
. = ALIGN(4);
_ebss = .; /* define a global symbol at bss end */
__bss_end__ = _ebss;
} >RAM
}
Назовем его "stm32f10xx8.ld". Теперь у нас есть все для компиляции, поэтому давайте сделаем это:
$ arm-none-eabi-gcc -mthumb -mcpu=cortex-m3 -O0 -c -g -DSTM32F10X_MD -I ./CMSIS/device -I ./CMSIS/core -I ./SPL/inc -o main.o ./main.c
Компоновка:
$ arm-none-eabi-ld -Tstm32f10xx8.ld main.o -o firmware.elf
Тоже самое можно сделать одной командой:
$ arm-none-eabi-gcc -T stm32f10xx8.ld -nostartfiles -mcpu=cortex-m3 -g -mthumb -DSTM32F10X_MD -I ./CMSIS/device -I ./CMSIS/core -I ./SPL/inc -o firmware.elf main.c
Смотрим на размер получившийся прошивки:
$ arm-none-eabi-size ./firmware.elf
text data bss dec hex filename
148 0 0 148 94 ./firmware.elf
148 байт, как и было обещано. А почему-бы не добавить оптимизации? Добавим ключ "-Os":
$ arm-none-eabi-gcc -T stm32f10xx8.ld -nostartfiles -mcpu=cortex-m3 -g -Os -mthumb -DSTM32F10X_MD -I ./CMSIS/device -I ./CMSIS/core -I ./SPL/inc -o firmware.elf main.c
$ arm-none-eabi-size ./firmware.elf
text data bss dec hex filename
108 0 0 108 6c ./firmware.elf
108 байт - это уже близко к прошивке 8-битного микроконтроллера. Для ATtiny13A прошивка с Blink выходит на 68 байт. Но для 32-разрядного микроконтроллера программа всегда будет больше памяти занимать, т.к. физические адреса тут в два раза длиннее.
Прошиваем:
$ arm-none-eabi-objcopy -O binary firmware.elf ./firmware.bin
$ st-flash write ./firmware.bin 0x08000000
После прошивки на плате должен начать мигать зеленый светодиод. На логическом анализаторе это выглядит следующим образом:
Сейчас микроконтроллер работает на дефолтных настройках, т.е. он тактируется от HSI с частотой 8 МГц.
Для удобства сборки можно добавить к проекту Makefile:
# Toolchain setup (adjust paths if needed)
CC = arm-none-eabi-gcc
OBJCOPY = arm-none-eabi-objcopy
SIZE = arm-none-eabi-size
RM = rm -f
# Target and output names
TARGET = firmware
BIN = $(TARGET).bin
HEX = $(TARGET).hex
ELF = $(TARGET).elf
# Include paths
INC = -ICMSIS/device
INC += -ICMSIS/core
INC += -ISPL/inc
DEF = -DSTM32F10X_MD
# MCU and flags
MCU = cortex-m3
CFLAGS = -mcpu=$(MCU) -mthumb -Wall -Os -ffunction-sections -fdata-sections
LDFLAGS = -T stm32f10xx8.ld -nostartfiles
# Source files
SRCS = main.c
# Build rules
all: $(BIN) $(HEX) size
$(ELF): $(SRCS)
$(CC) $(CFLAGS) $(DEF) $(INC) $(LDFLAGS) $^ -o $@
$(BIN): $(ELF)
$(OBJCOPY) -O binary $< $@
$(HEX): $(ELF)
$(OBJCOPY) -O ihex $< $@
size: $(ELF)
$(SIZE) --format=berkeley $<
# Flash to STM32 via ST-Link
flash: $(BIN)
st-flash write $< 0x08000000
# Clean
clean:
$(RM) $(ELF) $(BIN) $(HEX)
Здесь я думаю, что все понятно.
Что еще можно сделать? Можно добавить к проекту CMakeLists.txt для сборки посредством CMake, его я спрятал под спойлер.
показать CMakeLists.txt
cmake_minimum_required(VERSION 3.12)
project(firmware LANGUAGES C)
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR arm)
set(CMAKE_C_COMPILER arm-none-eabi-gcc)
set(CMAKE_OBJCOPY arm-none-eabi-objcopy)
set(CMAKE_SIZE arm-none-eabi-size)
set(TARGET firmware)
set(ELF ${TARGET}.elf)
set(BIN ${TARGET}.bin)
set(HEX ${TARGET}.hex)
include_directories(
CMSIS/device
CMSIS/core
SPL/inc
)
add_definitions(-DSTM32F10X_MD)
set(MCU cortex-m3)
set(CMAKE_C_FLAGS "-mcpu=${MCU} -mthumb -Wall -Os -ffunction-sections -fdata-sections")
set(CMAKE_EXE_LINKER_FLAGS "-T ${CMAKE_SOURCE_DIR}/stm32f10xx8.ld -nostartfiles")
set(SRCS main.c)
add_executable(${ELF} ${SRCS})
add_custom_command(
TARGET ${ELF} POST_BUILD
COMMAND ${CMAKE_OBJCOPY} -O binary ${ELF} ${BIN}
COMMAND ${CMAKE_OBJCOPY} -O ihex ${ELF} ${HEX}
COMMENT "Generating ${BIN} and ${HEX}"
)
add_custom_target(
size
COMMAND ${CMAKE_SIZE} --format=berkeley ${ELF}
DEPENDS ${ELF}
)
add_custom_target(
flash
COMMAND st-flash write ${BIN} 0x08000000
DEPENDS ${BIN}
)
Использование:
$ mkdir build && cd $_
$ cmake -DCMAKE_BUILD_TYPE=Release ..
$ make && make size
Qbs-скрипт я здесь выкладывать не буду, он будет доступен на git. Скрипт для Qbs версии:
$ qbs --version
1.24.1
Подробно о связке STM32+Qbs+QtCreator я писал в: "Qbs-скрипт для STM32F103C8-проекта на CMSIS (пошаговая инструкция)"
Qbs-скрипт позволяет использовать IDE QtCreator для написании кода, и при этом контролировать процесс сборки не хуже Makefile'ов (Cmake все-равно какие-то свои флаги добавляет и прошивка получается толще):
QtCreator это нативная программа, а не Java как STM32CubeIDE, шустро ворочается, не требует регистрации, имеет статический линтер на Clang и возможность отладки:
На мой взгляд это самая недооцененная IDE для разработки.
3) Небольшой ликбез по ассемблеру Cortex-M3
С помощью команды:
$ arm-none-eabi-objdump -S ./firmware.elf
можно дизассемблировать прошивку, что бы убедиться, что заголовок с константами расположен именно по адресу 0x8000000:
Disassembly of section .text:
08000000 <.text>:
8000000: 20005000 .word 0x20005000
8000004: 08000031 .word 0x08000031
8000008: 0800001d .word 0x0800001d
800000c: 0800001d .word 0x0800001d
8000010: 0800001d .word 0x0800001d
8000014: 0800001d .word 0x0800001d
8000018: 0800001d .word 0x0800001d
Disassembly of section .text.fault_irq:
0800001c <fault_irq>:
".word fault_irq+1\n\t" // MemManage
".word fault_irq+1\n\t" // BusFault
".word fault_irq+1\n\t"); // UsageFault
void fault_irq() {
while (1);
800001c: e7fe b.n 800001c <fault_irq>
Disassembly of section .text.dummy_loop:
0800001e <dummy_loop>:
}
void dummy_loop(volatile uint32_t count) {
800001e: b082 sub sp, #8
8000020: 9001 str r0, [sp, #4]
while (--count);
8000022: 9b01 ldr r3, [sp, #4]
8000024: 3b01 subs r3, #1
8000026: 9301 str r3, [sp, #4]
8000028: 2b00 cmp r3, #0
800002a: d1fa bne.n 8000022 <dummy_loop+0x4>
}
800002c: b002 add sp, #8
800002e: 4770 bx lr
Disassembly of section .text.startup.main:
08000030 <main>:
GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13);
GPIOC->CRH |= GPIO_CRH_MODE13_1; // Output mode, max speed 2 MHz (0b10)
// Super loop to toggle PC13
while (1) {
GPIOC->BSRR = GPIO_Pin_13; // Set PC13
8000030: f44f 5100 mov.w r1, #8192 ; 0x2000
RCC->APB2ENR |= RCC_APB2Periph_GPIOC;
8000034: 4a0b ldr r2, [pc, #44] ; (8000064 <main+0x34>)
int main() {
8000036: b508 push {r3, lr}
RCC->APB2ENR |= RCC_APB2Periph_GPIOC;
8000038: 6993 ldr r3, [r2, #24]
dummy_loop(WAIT);
800003a: 480b ldr r0, [pc, #44] ; (8000068 <main+0x38>)
RCC->APB2ENR |= RCC_APB2Periph_GPIOC;
800003c: f043 0310 orr.w r3, r3, #16
8000040: 6193 str r3, [r2, #24]
GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13);
8000042: f5a2 3280 sub.w r2, r2, #65536 ; 0x10000
8000046: 6853 ldr r3, [r2, #4]
8000048: f423 0370 bic.w r3, r3, #15728640 ; 0xf00000
800004c: 6053 str r3, [r2, #4]
GPIOC->CRH |= GPIO_CRH_MODE13_1; // Output mode, max speed 2 MHz (0b10)
800004e: 6853 ldr r3, [r2, #4]
8000050: f443 1300 orr.w r3, r3, #2097152 ; 0x200000
8000054: 6053 str r3, [r2, #4]
GPIOC->BSRR = GPIO_Pin_13; // Set PC13
8000056: 6111 str r1, [r2, #16]
dummy_loop(WAIT);
8000058: f7ff ffe1 bl 800001e <dummy_loop>
GPIOC->BRR = GPIO_Pin_13; // Reset PC13
800005c: 6151 str r1, [r2, #20]
dummy_loop(WAIT);
800005e: f7ff ffde bl 800001e <dummy_loop>
while (1) {
8000062: e7f8 b.n 8000056 <main+0x26>
8000064: 40021000 .word 0x40021000
8000068: 000927c1 .word 0x000927c1
Здесь, возможно, потребуется небольшой ликбез.
1. Cortex-M3 был разработан двадцать лет назад, но до сих пор явлется популярным ядром для микроконтроллеров, несмотря на то, что впоследствии были выпущены более оптимизированные Cortex-M0/M0+ и более прогрессивные Cortex-M4, на смену которым в свою очередь выпущены Cortex-M23 и Cortex-M33. Все эти ядра занимают свою нишу в разработке электроники.
Coretx-M3 это совокупный набор инструкций Thumb и Thumb II. Наглядно на количество команд в том и другом наборе можно посмотреть на следующей картинке:
Thumb - это набор 16-битных инструкций. Исключением являются инструкция BL и барьерные инструкции (BL, DMB, DSB, ISB), которые имеют длину 4 байта. Thumb II - это набор 32-битных инструкций. Несмотря на то, что количество инструкций Thumb II в разы больше, чем у Thumb, многие (но не все) эти инструкции можно заменить связкой из пары инструкций Thumb I. Но зачастую, инструкции Thumb II будут работать быстрее, нежели такая связка.
Все инструкции в Cortex-M3 либо 16-битные, либо 32-битные. Одно-байтных, трех-байтных, пяти-байтных инструкций в данной архитектуре не существует.
2. В Cortex M3, чтобы выполнить какие-то действия над данными в ОЗУ, их нужно загрузить сначала в регистры, потом произвести над ними какие-то действия, и затем снова переслать их в ОЗУ. Эти операции перемещения занимают много времени, особенно когда данных много, например при обработке звука или изображения. Для того что бы сгладить этот недостаток, используется кеширование, SIMD инструкции и DMA.
3. В Cortex-M3 имеется 13 регистров общего назначения (РОН) R0-R12:
Все регистры делятся на нижние (R0-R7) и верхние (R8-R15). Инструкции из набора Thumb I могут адресовать только нижние регистры. Исключеним является инструкция MOV которая может адресовать в том числе и верхние регистры общего назначения. Инструкции из набора Thumb II могут адресовать все РОН, а так же регистры LR и SP. Регистр R15 имеет является счетчиком команд - PC, R14 - имеет обозначение LR - Link register, в него помещается адрес возврата при вызове подпрограммы. Регистр R13 - является указателем стека - SP, он "с двойным дном", т.е. в зависимости от того, выполняется ли обычная программа, либо прерывание (более строго будет говорить о выполнении кода с повышенными привилегиями, т.е. код который будет исполняться в первую очередь), указатель стека показывает разные значения. Кроме этих 16-регистров, имеется специальный статусный регистр xPSR. Он явлется составным из трех регистров: APSR + IPSR + EPSR.
4. В Cortex-M3 нет инструкций CALL/RET. Вместо них есть переход с сохранением адреса возврата в регистре R14/LR. Собственно: BL и BX. Если подпрограмма содержит в себе вызов другой подпрограммы, то содержимое R14/LR следует сохранить в стеке. Возврат тогда будет по инструкции: POP {PC}. например:
MySubroutine:
PUSH {LR} ; сохраняем в стеке значение LR
BL AnotherSub ; переход на другую подпрограмму, адрес возврата автоматически сохраняется в LR
POP {PC} ; возврат из подпрограммы
4. В Cortex-M3 нет переходов по абсолютному адресу, все переходы относительные. Это означает, что весь код являются перемещаемым.
5. Инструкций INC, DEC в Cortex-M3 так же нет, но вместо них можно использовать инструкции "SUB Reg,#1" и "ADD reg,#1". Они занимают два байта вместе с операндом. Если операнд больше одного байта, то тут потребуется 32-битная инструкция.
5. В Thumb II есть режим индексной адресации с автоинкрементом и автодекриментом для инструкций LDR и STR:
LDR R0, [R1, #4]! ; пост инкремент : R1 += 4 после выполнения инструкции
STR R0, [R1, #-4]! ; пост декремент : R1 -= 4 после выполнения инструкции
6. Указатель стека R13/SP выровнен по границе слова, т.е. его младшие два бита аппаратно сброшены в ноль. Указатель стека всегда указывает на последние сохраненные данные:
MOV SP, #0x20005000 ; инициализация SP
PUSH {R0} ; SP = 0x20004FFC, R0 сохранен по адресу [0x20004FFC]
PUSH {R1} ; SP = 0x20004FF8, R1 сохранен по адресу [0x20004FF8]
POP {R1} ; R1 = [0x20004FF8], SP = 0x20004FFC
7. Многие инструкции могут работать с группой регистров (reglist). Причем такие операции выполняются одной инструкцией. Например - сохранение контекста:
PUSH {R0-R7, LR} // сохраняем в стеке девять регистров
Или копирование блока памяти:
loop:
LDMIA R0!, {R1-R4}
STMIA R1!, {R1-R4}
SUBS R2, #16
BNE loop
8. С помощью суффиксов .b .h .w может указываться размер операнда: байт, полуслово или слово. Например:
LDRH R0, [R1] // Загрузка полуслова (16-бит)
STRB.W R2, [R3, #4] // Сохранение байта (8-бит) с приведением к 32-битному виду
Но существуют инструкции с жестко прописанными размерами данных:
LDR = слово (32-бит)
LDRH = полуслово (16-бит)
LDRB = байт (8-битt)
9. С помощью условного выполнения, некоторые инструкция может быть выполнена или нет в зависимости от значений того или иного флага регистра состояний. В Thumb наборе инструкций, это применяется полько с инструкциями перехода, т.е. так образуются инструкции условного перехода. В наборе инструкций Thumb II с большинством логических и арифметических операций, суффиксы условного выполнения применяться не могут.
10. Добавление суффикса "S" (например ADDS, SUBS), указывает, что команда должна изменить флаги APSR: N, Z, C, V (разные инструкции влиют на разные флаги) на основе результата выполнения.
ADDS R0, R1, R2 ; Изменяет APSR флаги (N, Z, C, V)
ADD R0, R1, R2 ; не изменяет флаги
Суффикс "S" работает с арифметическими и логическими операторами. Не работает с LDR, STR, B, BX, MSR, MRS. ИНструкции сравнения: CMP, CMN, TST, TEQ, всегда изменяют флаги состояния. Также всегда влиают на них некоторые 16-битные инструкции из набора Thumb-I, например: ADD/SUB. Пример использования суффикса S:
SUBS R0, R0, #1 ; R0=R0-1
BNE loop ; переход по метке если (R0 != 0)
11. В Thumb II наборе инструкций присутствует блок ветвления IT (If-Then), который призван реализовать небольшие ветвления, до 4-х инструкций, не прибегая к инструкциям ветвления и избегая сброса конвейера, т.к. каждый переход по метке влечет задержку в работе процессора на 2-3 машинных цикла. Он работает со статусными флагами: N (признак отрицательного числа), Z (признак нуля), C (перенос), V (переполнение) регистра APSR. Блок может выполняться с 16 суффиксами ветвления (EQ, NE, GT, LT, и др.).
Блок IT имеет следующий синтаксис:
IT{x{y{z}}} <условие>
где:
x - T(hen) или E(lse) для первой инструкции;
y - тоже самое для второй инструкции;
z - для третьей.
максимум - 4 инструкции.
Пример кода без IT блока
CMP R0, #10 ; выполняется за 1 машинный цикл
BNE skip ; выполняется за 1 машинный цикл если R0 != 10, и выполняется за 3 машинных цикла, если осуществляется переход по метке.
MOV R1, #100 ; 1 цикл
skip:
Итого, данный код выполняется за 3 или 4 машинных цикла.
Тот же самый код с использованием IT блока:
CMP R0, #10 ; 1 цикл
IT EQ ; 1 цикл
MOVEQ R1, #100 ; 1 цикл, если R1 = 10
Итого: 2-3 машинных цикла.
12. Когда происходит прерывание или исключение, процессор автоматически сохраняет в стеке восемь регистров, процесс называется stacking " Coretx-M3 Technical Reference Manual - глава 5.5.1"
Сохраняются регистры: R0, R1, R2, R3, R12, LR, PC, xPSR. Регистр LR во время выполнения обработчика прерывания содержит адрес выхода из прерывания.
большие адреса
| xPSR |
| PC |
| LR |
| R12 |
| R3 |
| R2 |
| R1 |
| R0 | <-- SP after stacking
меньшие адреса
Остальные регистры следует сохранять вручную, если это требуется:
ISR_Handler:
PUSH {R4-R7, LR} ; Manual save (R4-R7 and LR if nested calls exist)
... ; ISR code
POP {R4-R7, PC} ; Restore and return (PC replaces LR)
13. Ассемблер ARM поддерживает несколько режимов адресации.
a) Регистровая адресация, когда операндами являются исключительно регистры R0-R15. Пример:
ADD R0, R1, R2 ; R0 = R1 + R2 (все операнды являются регистрами)
б) Непосредственная адресация, когда операнд указан в самой инструкции. При таком типе адресации не требуется обращения к памяти. Например:
MOV R0, #0xAB ; R0 = 0xAB (регистру R0 присваивается непосредственно само число стоящее в операнде)
ADD R3, #10 ; R3 += 10
в) Прямая (абсолютная) адресация (англ. direct (absolute) addressing) - это режим адресации, в котором адрес данных указан напрямую, без использования каких-либо косвенных указаний или расчетов. Например:
LDR R0, =0x40021000 ; Загрузка значения находящегося в оперативной памяти по адресу 0x40021000 (например RCC register)
г) Косвенно-регистровая адресация (Register Indirect Addressing) - это режим адресации, при котором адрес операнда (данных) хранится в регистре процессора, а не непосредственно в поле адреса инструкции. Например:
LDR R0, [R1] ; регистру R0 присваивается значение хранящееся по адресу записаному в R1
STR R2, [R3] ; сохранение значения регистра R2 по адресу записаному в R3
д) Косвенно-регистровая адресация со смещением - это режим адресации, при котором к адресу операнда (данных) прибавляется какое-то смещение. Например:
LDR R0, [R1, #8] ; R0 = *(R1 + 8)
STR R2, [R3, R4] ; Сохранение R2 по адресу (R3 + R4)
е) Пре-индексная адресация, когда индексный регистр предварительно увеличивается. Например:
LDR R0, [R1, #4]! ; раскладывается две операции: R1 += 4, и R0 = *R1
ё) Пост-индексная адресация, когда после операции индексный регистр увеличивается. Например:
LDR R0, [R1], #4 ; раскладывается также в две операции: R0 = *R1 и R1 += 4
ж) Адресация относительно регистра PC. В этом случае PC выступает как индексный регистр. Например:
LDR R0, [PC, #0x100] ; Загрузка в R0 значения по адресу (PC + 0x100)
з) Адресация относительно регистра SP. В этом случае SP выступает как индексный регистр. Например:
LDR R0, [SP, #8] ;Загрузка в R0 значения по адресу SP+8
STR R1, [SP, #-4]! ; Эквивалентно операции PUSH
Использование SP в качестве индексного довольно ограничено, если вы хотите что-то достать из стека, или изменить хранящиеся там данные, то следует SP скопировать в РОН и уже его использовать в качестве индексного. Например:
MOV R4, SP ; Копируем SP в R4
LDR R0, [R4, R2] ; Используем R4 как индексный
и) Адресация с блочной загрузкой-выгрузкой, когда в какой-то блок регистров загружается или выгружаются значения из памяти. Например:
LDMIA R0!, {R1-R4} ; Загрузка в R1-R4 значений из памяти, при этом R0 используется как индексный регистр с автоинкрементом, после выполнения инструкции его значение увеличится на 16, т.е. R0 += 16
STMDB SP!, {R5-R7} ; Аналог PUSH {R5-R7}
Если вам что-то было не понятно, вы можете почитать учебник Козаченко В.Ф.(ред.): "Практический курс микропроцессорной техники на базе процессорных ядер ARM-Cortex-M3/M4/M4F" 2019 М.: МЭИ, там очень дотошно всё разобрано.
4) Таблица векторов
Первоначальная версия программы успешно собралась только потому, что таблица векторов была размещена в самом начале программы, а проект состоял лишь из одного файла с исходным кодом.
Технически, можно отказаться как от использования ассемблера так и от размещения таблицы в "шапке" исходника. Для этого перепишем программу следующим образом:
#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
#define WAIT 600000
void dummy_loop(volatile uint32_t count) {
while (--count);
}
int main() {
RCC->APB2ENR |= RCC_APB2Periph_GPIOC;
2
GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13);
GPIOC->CRH |= GPIO_CRH_MODE13_1; 2
while (1) {
GPIOC->BSRR = GPIO_Pin_13;
dummy_loop(WAIT);
GPIOC->BRR = GPIO_Pin_13;
dummy_loop(WAIT);
}
}
void fault_irq() {
while (1);
}
0x08000000
__attribute__((section(".isr_vector")))
const void* vectors[] = {
(void*)0x20005000,
(void*)(main + 1), 1
(void*)(fault_irq + 1),
(void*)(fault_irq + 1),
(void*)(fault_irq + 1),
(void*)(fault_irq + 1),
(void*)(fault_irq + 1)
};
Здесь заголовок прошивки был вынесен в отдельную секцию ".isr_vector". Теперь следует эту секцию указать в скрипте компоновщика. Для этого в блоке "SECTIONS", переж разделом ".text" разместить объявление новой секции:
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector))
. = ALIGN(4);
} >ROM
Под спойлером размещен полный вариант скрипта компоновщика.
показать
_estack = 0x20005000;
MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K
ROM (rx) : ORIGIN = 0x8000000, LENGTH = 64K
}
SECTIONS
{
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector))
. = ALIGN(4);
} >ROM
.text :
{
. = ALIGN(4);
*(.text)
} >ROM
.rodata :
{
. = ALIGN(4);
*(.rodata)
} >ROM
.data :
{
. = ALIGN(4);
*(.data)
} >RAM AT> ROM
. = ALIGN(4);
.bss :
{
*(.bss)
*(COMMON)
. = ALIGN(4);
_ebss = .;
__bss_end__ = _ebss;
} >RAM
}
Теперь, после компиляции, утилита "size" выдаст такое распределение кода:
$ arm-none-eabi-size --format=berkeley firmware.elf
text data bss dec hex filename
80 28 0 108 6c firmware.elf
Размер прошивки не изменился - 108 байт, но 28 байт заголовка перешли из секции "text" в секцию "data". Теперь, если дизассемблировать прошивку командой "arm-none-eabi-objdump -S", то таблицу даже не покажет, т.к. это не код. Но в том, что заголовок прошивки скомпоновался по адресу "0x8000000", можно убедиться с помощью команд:
$ arm-none-eabi-objdump -h firmware.elf |head
firmware.elf: file format elf32-littlearm
Sections:
Idx Name Size VMA LMA File off Algn
0 .isr_vector 0000001c 08000000 08000000 00010000 2**2
CONTENTS, ALLOC, LOAD, DATA
1 .text 00000000 0800001c 0800001c 0001001c 2**1
CONTENTS, ALLOC, LOAD, READONLY, CODE
2 .text.fault_irq 00000002 0800001c 0800001c 0001001c 2**1
или
$ arm-none-eabi-objdump -s -j .isr_vector firmware.elf
firmware.elf: file format elf32-littlearm
Contents of section .isr_vector:
8000000 00500020 31000008 1d000008 1d000008 .P. 1...........
8000010 1d000008 1d000008 1d000008 ............
или
$ xxd -g 4 firmware.bin | head
00000000: 00500020 31000008 1d000008 1d000008 .P. 1...........
00000010: 1d000008 1d000008 1d000008 fee782b0 ................
00000020: 0190019b 013b0193 002bfad1 02b07047 .....;...+....pG
00000030: 4ff40051 0b4a08b5 93690b48 43f01003 O..Q.J...i.HC...
00000040: 9361a2f5 80325368 23f47003 53605368 .a...2Sh#.p.S`Sh
00000050: 43f40013 53601161 fff7e1ff 5161fff7 C...S`.a....Qa..
00000060: defff8e7 00100240 c0270900 .......@.'..
Теперь можно вынести таблицу прерываний в отдельный модуль, и подключить его к проекту. Однако я предлагаю не изобретать велосипед, а привести проект, как принято говорить: "к стандартам принятым в индустрии".
В частности, я предлагаю подключить к проекту готовый "startup.s" - это ассемблерный файл, состоящий из полной таблицы прерываний STM32F103. С нашей стороны потребуется создание файлов с правилами сборки проекта Makefile, CMakeLists.txt и Qbs-скрипта.
В STM32F103 довольно обширная таблица прерываний, состоящая из 70-векторов обработчиков и еще шести зарезервированных (Reserved):
показать
Итого 76 * 4 = 304 байта. Я взял готовый "startup.s" из шаблонного проекта SW4STM32, который содержит полную таблицу векторов и стартовый код:
показать startup.s
.syntax unified
.cpu cortex-m3
.fpu softvfp
.thumb
.global g_pfnVectors
.global Default_Handler
.word _sidata
.word _sdata
.word _edata
.word _sbss
.word _ebss
.section .text.Reset_Handler
.weak Reset_Handler
.type Reset_Handler, %function
Reset_Handler:
ldr r0, =_estack
mov sp, r0
ldr r0, =_sdata
ldr r1, =_edata
ldr r2, =_sidata
movs r3,
b LoopCopyDataInit
CopyDataInit:
ldr r4, [r2, r3]
str r4, [r0, r3]
adds r3, r3,
LoopCopyDataInit:
adds r4, r0, r3
cmp r4, r1
bcc CopyDataInit
ldr r2, =_sbss
ldr r4, =_ebss
movs r3,
b LoopFillZerobss
FillZerobss:
str r3, [r2]
adds r2, r2,
LoopFillZerobss:
cmp r2, r4
bcc FillZerobss
bl main
LoopForever:
b LoopForever
.size Reset_Handler, .-Reset_Handler
.section .text.Default_Handler,"ax",%progbits
Default_Handler:
Infinite_Loop:
b Infinite_Loop
.size Default_Handler, .-Default_Handler
Note
.section .isr_vector,"a",%progbits
.type g_pfnVectors, %object
.size g_pfnVectors, .-g_pfnVectors
g_pfnVectors:
.word _estack
.word Reset_Handler
.word NMI_Handler
.word HardFault_Handler
.word MemManage_Handler
.word BusFault_Handler
.word UsageFault_Handler
.word 0
.word 0
.word 0
.word 0
.word SVC_Handler
.word DebugMon_Handler
.word 0
.word PendSV_Handler
.word SysTick_Handler
.word WWDG_IRQHandler
.word PVD_IRQHandler
.word TAMPER_IRQHandler
.word RTC_IRQHandler
.word FLASH_IRQHandler
.word RCC_IRQHandler
.word EXTI0_IRQHandler
.word EXTI1_IRQHandler
.word EXTI2_IRQHandler
.word EXTI3_IRQHandler
.word EXTI4_IRQHandler
.word DMA1_Channel1_IRQHandler
.word DMA1_Channel2_IRQHandler
.word DMA1_Channel3_IRQHandler
.word DMA1_Channel4_IRQHandler
.word DMA1_Channel5_IRQHandler
.word DMA1_Channel6_IRQHandler
.word DMA1_Channel7_IRQHandler
.word ADC1_2_IRQHandler
.word USB_HP_CAN_TX_IRQHandler
.word USB_LP_CAN_RX0_IRQHandler
.word CAN_RX1_IRQHandler
.word CAN_SCE_IRQHandler
.word EXTI9_5_IRQHandler
.word TIM1_BRK_IRQHandler
.word TIM1_UP_IRQHandler
.word TIM1_TRG_COM_IRQHandler
.word TIM1_CC_IRQHandler
.word TIM2_IRQHandler
.word TIM3_IRQHandler
.word TIM4_IRQHandler
.word I2C1_EV_IRQHandler
.word I2C1_ER_IRQHandler
.word I2C2_EV_IRQHandler
.word I2C2_ER_IRQHandler
.word SPI1_IRQHandler
.word SPI2_IRQHandler
.word USART1_IRQHandler
.word USART2_IRQHandler
.word USART3_IRQHandler
.word EXTI15_10_IRQHandler
.word RTCAlarm_IRQHandler
.word 0
.word TIM8_BRK_IRQHandler
.word TIM8_UP_IRQHandler
.word TIM8_TRG_COM_IRQHandler
.word TIM8_CC_IRQHandler
.word ADC3_IRQHandler
.word FSMC_IRQHandler
.word SDIO_IRQHandler
.word TIM5_IRQHandler
.word SPI3_IRQHandler
.word UART4_IRQHandler
.word UART5_IRQHandler
.word TIM6_IRQHandler
.word TIM7_IRQHandler
.word DMA2_Channel1_IRQHandler
.word DMA2_Channel2_IRQHandler
.word DMA2_Channel3_IRQHandler
.word DMA2_Channel4_5_IRQHandler
.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":
Reset_Handler:
ldr r0, =_estack
mov sp, r0
ldr r0, =_sdata
ldr r1, =_edata
ldr r2, =_sidata
movs r3,
b LoopCopyDataInit
CopyDataInit:
ldr r4, [r2, r3]
str r4, [r0, r3]
adds r3, r3,
LoopCopyDataInit:
adds r4, r0, r3
cmp r4, r1
bcc CopyDataInit
ldr r2, =_sbss
ldr r4, =_ebss
movs r3,
b LoopFillZerobss
FillZerobss:
str r3, [r2]
adds r2, r2,
LoopFillZerobss:
cmp r2, r4
bcc FillZerobss
Здесь два цикла. В первом цикле копируются данные из секции "data" в оперативную память, и тем самым, происходит инициализация глобальных переменных теми значениями, что были заданы в тексте программы. Второй цикл заполняет нулями bss секцию. В режиме отладки я запускал трассировку, и в случае данной программы, выполнение обоих циклов игнорировалось. После завершения стартового кода, происходит переход на функции SystemInit и __libc_init_array. Первая функция настраивает систему тактирования, которой пока в проекте нет, поэтому я ее закомментировал. Вторая функция находится в стандартной библиотеке, которой тоже нет, поэтому она тоже закомментирована. В финале, происходит переход на функцию "main".
Также я взял полный, неурезанный скрипт компоновщика:
показать stm32f10xx8.ld
ENTRY(Reset_Handler)
_estack = 0x20005000;
_Min_Heap_Size = 0;
_Min_Stack_Size = 0x100;
MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K
ROM (rx) : ORIGIN = 0x8000000, LENGTH = 64K
}
SECTIONS
{
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector))
. = ALIGN(4);
} >ROM
.text :
{
. = ALIGN(4);
*(.text)
*(.text*)
*(.glue_7)
*(.glue_7t)
*(.eh_frame)
KEEP (*(.init))
KEEP (*(.fini))
. = ALIGN(4);
_etext = .;
} >ROM
.rodata :
{
. = ALIGN(4);
*(.rodata)
*(.rodata*)
. = ALIGN(4);
} >ROM
.ARM.extab : {
. = ALIGN(4);
*(.ARM.extab* .gnu.linkonce.armextab.*)
. = ALIGN(4);
} >ROM
.ARM : {
. = ALIGN(4);
__exidx_start = .;
*(.ARM.exidx*)
__exidx_end = .;
. = ALIGN(4);
} >ROM
.preinit_array :
{
. = ALIGN(4);
PROVIDE_HIDDEN (__preinit_array_start = .);
KEEP (*(.preinit_array*))
PROVIDE_HIDDEN (__preinit_array_end = .);
. = ALIGN(4);
} >ROM
.init_array :
{
. = ALIGN(4);
PROVIDE_HIDDEN (__init_array_start = .);
KEEP (*(SORT(.init_array.*)))
KEEP (*(.init_array*))
PROVIDE_HIDDEN (__init_array_end = .);
. = ALIGN(4);
} >ROM
.fini_array :
{
. = ALIGN(4);
PROVIDE_HIDDEN (__fini_array_start = .);
KEEP (*(SORT(.fini_array.*)))
KEEP (*(.fini_array*))
PROVIDE_HIDDEN (__fini_array_end = .);
. = ALIGN(4);
} >ROM
_sidata = LOADADDR(.data);
.data :
{
. = ALIGN(4);
_sdata = .;
*(.data)
*(.data*)
. = ALIGN(4);
_edata = .;
} >RAM AT> ROM
. = ALIGN(4);
.bss :
{
_sbss = .;
__bss_start__ = _sbss;
*(.bss)
*(.bss*)
*(COMMON)
. = ALIGN(4);
_ebss = .;
__bss_end__ = _ebss;
} >RAM
._user_heap_stack :
{
. = ALIGN(8);
PROVIDE ( end = . );
PROVIDE ( _end = . );
. = . + _Min_Heap_Size;
. = . + _Min_Stack_Size;
. = ALIGN(8);
} >RAM
/DISCARD/ :
{
libc.a ( * )
libm.a ( * )
libgcc.a ( * )
}
.ARM.attributes 0 : { *(.ARM.attributes) }
}
Модуль "main.c" теперь будет выглядеть так:
#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
#define WAIT 600000
void dummy_loop(volatile uint32_t count) {
while (--count);
}
int main() {
RCC->APB2ENR |= RCC_APB2Periph_GPIOC;
2
GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13);
GPIOC->CRH |= GPIO_CRH_MODE13_1; 2
while (1) {
GPIOC->BSRR = GPIO_Pin_13;
dummy_loop(WAIT);
GPIOC->BRR = GPIO_Pin_13;
dummy_loop(WAIT);
}
}
Итак, имеются файлы: "main.c", "startup.s" и "stm32f10xx8.ld". Теперь это все вместе надо собрать в единую прошивку. Сделать это можно разными способами.
Одной командой это делается довольно просто:
$ arm-none-eabi-gcc -T stm32f10xx8.ld -nostartfiles -mcpu=cortex-m3 -g -Os -mthumb -DSTM32F10X_MD -I ./CMSIS/device -I ./CMSIS/core -I ./SPL/inc -o firmware.elf main.c ./asm/startup.s
s arm-none-eabi-objcopy -O binary firmware.elf firmware.bin
$ arm-none-eabi-size ./firmware.elf
text data bss dec hex filename
480 0 256 736 2e0 ./firmware.elf
Прошивка весит 480 байт, bss область в прошивку не входит, она располагается в оперативной памяти. Проверяем таблицу векторов:
$ arm-none-eabi-objdump -s -j .isr_vector firmware.elf
firmware.elf: file format elf32-littlearm
Contents of section .isr_vector:
8000000 00500020 95010008 dd010008 dd010008 .P. ............
8000010 dd010008 dd010008 dd010008 00000000 ................
8000020 00000000 00000000 00000000 dd010008 ................
8000030 dd010008 00000000 dd010008 dd010008 ................
8000040 dd010008 dd010008 dd010008 dd010008 ................
8000050 dd010008 dd010008 dd010008 dd010008 ................
8000060 dd010008 dd010008 dd010008 dd010008 ................
8000070 dd010008 dd010008 dd010008 dd010008 ................
8000080 dd010008 dd010008 dd010008 dd010008 ................
8000090 dd010008 dd010008 dd010008 dd010008 ................
80000a0 dd010008 dd010008 dd010008 dd010008 ................
80000b0 dd010008 dd010008 dd010008 dd010008 ................
80000c0 dd010008 dd010008 dd010008 dd010008 ................
80000d0 dd010008 dd010008 dd010008 dd010008 ................
80000e0 dd010008 dd010008 00000000 dd010008 ................
80000f0 dd010008 dd010008 dd010008 dd010008 ................
8000100 dd010008 dd010008 dd010008 dd010008 ................
8000110 dd010008 dd010008 dd010008 dd010008 ................
8000120 dd010008 dd010008 dd010008 dd010008 ................
0x130 - это 304 байта, на этот раз полная таблица векторов.
Для того, чтобы составлять скрипты сборки проекта, сперва следует уточнить структуру каталогов проекта. Я привык использовать следующую структуру проекта:
project/
├── main.c # в корне всегда файл "main.c"
├── Makefile
├── stm32f10xx8.ld
├── inc/
│ └── (заголовочные файлы)
├── src/
│ └── (остальные Си-файлы проекта)
└── asm/
└── (ассемблерные файлы проекта)
Самый простой Makefile для такого проекта у меня вышел следующим:
# Toolchain setup
CC = arm-none-eabi-gcc
AS = arm-none-eabi-gcc
OBJCOPY = arm-none-eabi-objcopy
SIZE = arm-none-eabi-size
RM = rm -f
# Target and output names
TARGET = firmware
BIN = $(TARGET).bin
HEX = $(TARGET).hex
ELF = $(TARGET).elf
# Directory structure
BUILD_DIR = build
SRC_DIR = src
ASM_DIR = asm
INC_DIR = inc
# Source files
SRCS_C = main.c
SRCS_C += $(wildcard $(SRC_DIR)/*.c)
SRCS_S = $(wildcard $(ASM_DIR)/*.s)
OBJS += $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.o,$(SRCS_C)) \
$(patsubst $(ASM_DIR)/%.s,$(BUILD_DIR)/%.o,$(SRCS_S))
# Include paths
INC = -I$(INC_DIR) -ICMSIS/device -ICMSIS/core -ISPL/inc
DEF = -DSTM32F10X_MD
# MCU and flags
MCU = cortex-m3
CFLAGS = -mcpu=$(MCU) -mthumb -Wall -g -Os -ffunction-sections -fdata-sections $(INC) $(DEF)
ASFLAGS = -mcpu=$(MCU) -mthumb -g
LDFLAGS = -T stm32f10xx8.ld -nostartfiles -Wl,--gc-sections -specs=nano.specs
# Build rules
all: $(BUILD_DIR)/$(BIN) $(BUILD_DIR)/$(HEX) size
$(BUILD_DIR)/$(ELF): $(OBJS)
@mkdir -p $(@D)
$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)
# Rule for C files
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c
@mkdir -p $(@D)
$(CC) -c $(CFLAGS) $< -o $@
# Rule for assembly files
$(BUILD_DIR)/%.o: $(ASM_DIR)/%.s
@mkdir -p $(@D)
$(AS) -c $(ASFLAGS) $< -o $@
$(BUILD_DIR)/$(BIN): $(BUILD_DIR)/$(ELF)
$(OBJCOPY) -O binary $< $@
$(BUILD_DIR)/$(HEX): $(BUILD_DIR)/$(ELF)
$(OBJCOPY) -O ihex $< $@
size: $(BUILD_DIR)/$(ELF)
$(SIZE) --format=berkeley $<
flash: $(BUILD_DIR)/$(BIN)
st-flash write $< 0x08000000
clean:
$(RM) -r $(BUILD_DIR)
.PHONY: all size flash clean
Сборка:
$ make
arm-none-eabi-gcc -c -mcpu=cortex-m3 -mthumb -g asm/startup.s -o build/startup.o
arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -Wall -g -Os -ffunction-sections -fdata-sections -Iinc -ICMSIS/device -ICMSIS/core -ISPL/inc -DSTM32F10X_MD main.c build/startup.o -o build/firmware.elf -T stm32f10xx8.ld -nostartfiles -Wl,--gc-sections -specs=nano.specs
arm-none-eabi-objcopy -O binary build/firmware.elf build/firmware.bin
arm-none-eabi-objcopy -O ihex build/firmware.elf build/firmware.hex
arm-none-eabi-size --format=berkeley build/firmware.elf
text data bss dec hex filename
460 0 256 716 2cc build/firmware.elf
Здесь довольно странная конструкция получилась, когда смешана компиляция и компоновка. Я бы хотел, что бы компиляция и компоновка были разделены.
В конечном итоге, я остановился на следующем варианте Makefile:
# Toolchain setup
CC = arm-none-eabi-gcc
AS = arm-none-eabi-gcc
OBJCOPY = arm-none-eabi-objcopy
SIZE = arm-none-eabi-size
RM = rm -f
# Target and output names
TARGET = firmware
BIN = $(TARGET).bin
HEX = $(TARGET).hex
ELF = $(TARGET).elf
# Directory structure
BUILD_DIR = build
SRC_DIR = src
ASM_DIR = asm
INC_DIR = inc
PROJECT_ROOT = .
# Source files
SRCS_C = $(wildcard $(SRC_DIR)/*.c) $(wildcard $(PROJECT_ROOT)/main.c)
SRCS_S = $(wildcard $(ASM_DIR)/*.s)
OBJS = $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.o,$(filter $(SRC_DIR)/%,$(SRCS_C))) \
$(patsubst $(PROJECT_ROOT)/%.c,$(BUILD_DIR)/%.o,$(filter $(PROJECT_ROOT)/%,$(SRCS_C))) \
$(patsubst $(ASM_DIR)/%.s,$(BUILD_DIR)/%.o,$(SRCS_S))
# Include paths
INC = -I$(INC_DIR) -I$(PROJECT_ROOT) -ICMSIS/device -ICMSIS/core -ISPL/inc
DEF = -DSTM32F10X_MD
# MCU and flags
MCU = cortex-m3
CFLAGS = -mcpu=$(MCU) -mthumb -Wall -g -Os -ffunction-sections -fdata-sections $(INC) $(DEF)
ASFLAGS = -mcpu=$(MCU) -mthumb -g
LDFLAGS = -T stm32f10xx8.ld -nostartfiles -Wl,--gc-sections -specs=nano.specs
# Build rules
all: $(BUILD_DIR)/$(BIN) $(BUILD_DIR)/$(HEX) size
$(BUILD_DIR)/$(ELF): $(OBJS)
@mkdir -p $(@D)
$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)
# Rule for C files in src directory
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c
@mkdir -p $(@D)
$(CC) -c $(CFLAGS) $< -o $@
# Rule for main.c in project root
$(BUILD_DIR)/%.o: $(PROJECT_ROOT)/%.c
@mkdir -p $(@D)
$(CC) -c $(CFLAGS) $< -o $@
# Rule for assembly files
$(BUILD_DIR)/%.o: $(ASM_DIR)/%.s
@mkdir -p $(@D)
$(AS) -c $(ASFLAGS) $< -o $@
$(BUILD_DIR)/$(BIN): $(BUILD_DIR)/$(ELF)
$(OBJCOPY) -O binary $< $@
$(BUILD_DIR)/$(HEX): $(BUILD_DIR)/$(ELF)
$(OBJCOPY) -O ihex $< $@
size: $(BUILD_DIR)/$(ELF)
$(SIZE) --format=berkeley $<
flash: $(BUILD_DIR)/$(BIN)
st-flash write $< 0x08000000
clean:
$(RM) -r $(BUILD_DIR)
.PHONY: all size flash clean
Makefile будет примерно в два раза меньше, если все исходники хранить в "src" каталоге, но как я говорил, я привык именно к такой структуре проекта:
project/
├── main.c # в корне всегда файл "main.c"
├── Makefile
├── stm32f10xx8.ld
├── inc/
│ └── (заголовочные файлы)
├── src/
│ └── (остальные Си-файлы проекта)
└── asm/
└── (ассемблерные файлы проекта)
Сборка проекта:
$ make clean && make
rm -f -r build
arm-none-eabi-gcc -c -mcpu=cortex-m3 -mthumb -g asm/startup.s -o build/startup.o
arm-none-eabi-gcc -c -mcpu=cortex-m3 -mthumb -Wall -g -Os -ffunction-sections -fdata-sections -Iinc -I. -ICMSIS/device -ICMSIS/core -ISPL/inc -DSTM32F10X_MD main.c -o build/main.o
arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -Wall -g -Os -ffunction-sections -fdata-sections -Iinc -I. -ICMSIS/device -ICMSIS/core -ISPL/inc -DSTM32F10X_MD build/main.o build/startup.o -o build/firmware.elf -T stm32f10xx8.ld -nostartfiles -Wl,--gc-sections -specs=nano.specs
arm-none-eabi-objcopy -O binary build/firmware.elf build/firmware.bin
arm-none-eabi-objcopy -O ihex build/firmware.elf build/firmware.hex
arm-none-eabi-size --format=berkeley build/firmware.elf
text data bss dec hex filename
460 0 256 716 2cc build/firmware.elf
В этот раз все нормально, все модули собираются по отдельности, после чего происходит компоновка.
CMakeLists.txt для сборки проекта посредством CMake:
cmake_minimum_required(VERSION 3.12)
project(firmware LANGUAGES C ASM)
set(CMAKE_VERBOSE_MAKEFILE OFF)
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR arm)
set(CMAKE_C_COMPILER arm-none-eabi-gcc)
set(CMAKE_ASM_COMPILER arm-none-eabi-gcc)
set(CMAKE_OBJCOPY arm-none-eabi-objcopy)
set(CMAKE_SIZE arm-none-eabi-size)
set(TARGET firmware)
set(ELF ${TARGET}.elf)
set(BIN ${TARGET}.bin)
set(HEX ${TARGET}.hex)
set(BUILD_DIR ${CMAKE_BINARY_DIR})
set(SRC_DIR src)
set(ASM_DIR asm)
set(INC_DIR inc)
file(GLOB SRCS_C
${SRC_DIR}/*.c
${CMAKE_SOURCE_DIR}/main.c
)
file(GLOB SRCS_S ${ASM_DIR}/*.s)
set(INC
${INC_DIR}
CMSIS/device
CMSIS/core
SPL/inc
)
add_definitions(-DSTM32F10X_MD)
set(MCU cortex-m3)
set(CWARN "-Wall")
set(CMAKE_C_FLAGS "-mcpu=${MCU} -mthumb ${CWARN} -g -Os -ffunction-sections -fdata-sections")
set(CMAKE_ASM_FLAGS "-mcpu=${MCU} -mthumb -g")
set(LINKER_SCRIPT "${CMAKE_SOURCE_DIR}/stm32f10xx8.ld")
set(CMAKE_EXE_LINKER_FLAGS "-nostartfiles -specs=nano.specs -T${LINKER_SCRIPT} -Wl,--gc-sections")
add_executable(${ELF} ${SRCS_C} ${SRCS_S})
target_include_directories(${ELF} PRIVATE ${INC})
add_custom_command(TARGET ${ELF} POST_BUILD
COMMAND ${CMAKE_OBJCOPY} -O binary ${ELF} ${BIN}
COMMAND ${CMAKE_OBJCOPY} -O ihex ${ELF} ${HEX}
COMMENT "Generating ${BIN} and ${HEX}"
)
add_custom_target(size
COMMAND ${CMAKE_SIZE} --format=berkeley ${ELF}
DEPENDS ${ELF}
)
add_custom_target(flash
COMMAND st-flash write ${BIN} 0x08000000
DEPENDS ${BIN}
)
install(FILES ${BIN} DESTINATION bin)
Сборка проекта посредством CMake:
$ mkdir cbuild && cd $_
$ cmake ..
$ make && make size
Прошивка:
$ make flash
Под спойлером выкладываю скрипт для сборки проекта посредством Qbs:
показать Qbs-скрипт
import qbs
Product {
type: ["application","flash"]
Depends { name: "cpp" }
name: "firmware"
property string Home: sourceDirectory
property string Inc: Home + "/inc"
property string Asm: Home + "/asm"
cpp.positionIndependentCode: false
cpp.executableSuffix: ".elf"
cpp.includePaths:[
"CMSIS/device",
"CMSIS/core",
"SPL/inc",
"inc",
]
cpp.driverFlags: [
"-nostartfiles",
"-specs=nano.specs",
]
cpp.linkerFlags:
[
"--gc-sections",
"-T" + sourceDirectory + "/stm32f10xx8.ld",
]
cpp.cFlags: [
"-mthumb",
"-mcpu=cortex-m3",
"-Wall",
]
cpp.defines: [
"STM32F10X_MD",
]
Properties
{
condition: qbs.buildVariant === "debug"
cpp.defines: outer.concat(["DEBUG=1"])
cpp.debugInformation: true
cpp.optimization: "debug"
cpp.cFlags: outer.concat(["-ggdb"])
}
Properties
{
condition: qbs.buildVariant === "release"
cpp.debugInformation: false
cpp.optimization: "small"
}
Group {
name: "Headers"
files: [
Inc + "/*.h",
]
fileTags: ["h_src"]
}
Group {
name: "Assembly"
fileTags: ["asm"]
files: [
Asm + "/*.s",
]
}
files: [
"main.c",
]
Rule
{
inputs: ["application"]
Artifact
{
filePath: product.name + ".bin"
fileTags: "flash"
}
prepare:
{
var size_name=input.filePath
var argsObjcopy = ["-O", "binary", input.filePath, output.filePath]
var cmd_bin=new Command("arm-none-eabi-objcopy",argsObjcopy)
var cmd_size=new Command("arm-none-eabi-size",size_name)
cmd_bin.description = "Generating binary: " + output.fileName
cmd_size.description = "Size of " + output.fileName
return [cmd_bin,cmd_size]
}
}
}
Сборка с помощь команды:
$ qbs build config:release profile:arm-none-eabi-gcc-10_3 --command-echo-mode command-line
Build graph does not yet exist for configuration 'release'. Starting from scratch.
Resolving project for configuration release
WARNING: Could not detect target platform ('linux' given)
Setting up build graph for configuration release
Building for configuration release
/usr/local/bin/arm-none-eabi-as -I/home/flanker/qtcreator_projects/00_blink_148/CMSIS/device -I/home/flanker/qtcreator_projects/00_blink_148/CMSIS/core -I/home/flanker/qtcreator_projects/00_blink_148/SPL/inc -I/home/flanker/qtcreator_projects/00_blink_148/inc -o /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/2445be8241aabd34/startup.s.o /home/flanker/qtcreator_projects/00_blink_148/asm/startup.s
/usr/local/bin/arm-none-eabi-gcc -Os -Wall -Wextra -nostartfiles -specs=nano.specs -pipe -fvisibility=default -mthumb -mcpu=cortex-m3 -Wall -DNDEBUG -DSTM32F10X_MD -I/home/flanker/qtcreator_projects/00_blink_148/CMSIS/device -I/home/flanker/qtcreator_projects/00_blink_148/CMSIS/core -I/home/flanker/qtcreator_projects/00_blink_148/SPL/inc -I/home/flanker/qtcreator_projects/00_blink_148/inc -o /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/3a52ce780950d4d9/main.c.o -c /home/flanker/qtcreator_projects/00_blink_148/main.c
/usr/local/bin/arm-none-eabi-gcc -Wl,--gc-sections,-T/home/flanker/qtcreator_projects/00_blink_148/stm32f10xx8.ld -nostartfiles -specs=nano.specs -o /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/firmware.elf /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/2445be8241aabd34/startup.s.o /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/3a52ce780950d4d9/main.c.o
/usr/local/bin/arm-none-eabi-objcopy -O binary /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/firmware.elf /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/firmware.bin
/usr/local/bin/arm-none-eabi-size /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/firmware.elf
Build done for configuration release.
/usr/local/bin/arm-none-eabi-size /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/firmware.elf
text data bss dec hex filename
460 0 256 716 2cc /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/firmware.elf
5) Настройка системы тактирования - RCC (Reset and Clock Control)
После добавления таблицы прерываний необходимо настроить систему тактирования. Для микроконтроллера STM32F103C8 она выглядит следующим образом:
Вообще-то, в шаблонном проекте TrueStudio у нас уже была настроена система тактирования RCC, хоть мы к этому и не приложили ни капли усилий. С помощью добрых глаз и дизассемблера, мне удалось выяснить, что функции настройки RCC содержатся в файле system_stm32f10x.c:
Настройка системы тактирования начинается с вызова функции SystemInit(). В каталоге проекта "src", откроем новый файл system_init.c. В этот файл мы и скопируем функцию SystemInit():
#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
void SystemInit (void)
{
RCC->CR |= (uint32_t)0x00000001; 8
#ifndef STM32F10X_CL
RCC->CFGR &= (uint32_t)0xF8FF0000;
#else
RCC->CFGR &= (uint32_t)0xF0FF0000;
#endif
RCC->CR &= (uint32_t)0xFEF6FFFF;
RCC->CR &= (uint32_t)0xFFFBFFFF;
RCC->CFGR &= (uint32_t)0xFF80FFFF;
#ifdef STM32F10X_CL
RCC->CR &= (uint32_t)0xEBFFFFFF;
RCC->CIR = 0x00FF0000;
RCC->CFGR2 = 0x00000000;
#elif defined (STM32F10X_LD_VL) || defined (STM32F10X_MD_VL) || (defined STM32F10X_HD_VL)
RCC->CIR = 0x009F0000;
RCC->CFGR2 = 0x00000000;
#else
RCC->CIR = 0x009F0000;
#endif
#if defined (STM32F10X_HD) || (defined STM32F10X_XL) || (defined STM32F10X_HD_VL)
#ifdef DATA_IN_ExtSRAM
SystemInit_ExtMemCtl();
#endif
#endif
#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;
#else
#endif
}
Данная функция сбрасывает систему тактирования, т.е. делает то, что и так должно происходить после RESET. В завершении работы, в зависимости от флагов компиляции, включается вызов функций для настройки тактирования.
Мы будем использовать другую функцию: void SetSysClockTo72(). Эта функция сначала переключается на внешний кварц, затем запускает PLL-генератор, после его стабилизации устанавливается PLL-множитель на 9, в результате чего частота SYSCLK устанавливается в значение 72 MHz. Далее устанавливаются делители: для AHB и APB2 шин по единице, для APB1 шины равной двум, т.е. на 36МГц. Кроме того для флеш-памяти устанавливается WaitState равный двум, т.е. флеш-память работает на меньшей частоте чем процессор. Но давайте по порядку.
1. В начале запускается внешний кварц:
RCC->CR |= ((uint32_t)RCC_CR_HSEON);
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 генератор, после чего некоторое время ожидается пока он стабилизируется.
Идем далее:
FLASH->ACR |= FLASH_ACR_PRFTBE;
2
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 - флаг буфера превыборки.
RCC->CFGR |= (uint32_t)RCC_CFGR_HPRE_DIV1;
RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE2_DIV1;
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.
972
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 и ожидается его готовность:
RCC->CR |= RCC_CR_PLLON;
while((RCC->CR & RCC_CR_PLLRDY) == 0)
{
}
В завершении, тактирование SYSCLK переключается на PLL:
RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_SW));
RCC->CFGR |= (uint32_t)RCC_CFGR_SW_PLL;
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
static void SetSysClockToHSE(void)
{
__IO uint32_t StartUpCounter = 0, HSEStatus = 0;
RCC->CR |= ((uint32_t)RCC_CR_HSEON);
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
FLASH->ACR |= FLASH_ACR_PRFTBE;
0
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
#endif
RCC->CFGR |= (uint32_t)RCC_CFGR_HPRE_DIV1;
RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE2_DIV1;
RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE1_DIV1;
RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_SW));
RCC->CFGR |= (uint32_t)RCC_CFGR_SW_HSE;
while ((RCC->CFGR & (uint32_t)RCC_CFGR_SWS) != (uint32_t)0x04)
{
}
}
else
{
}
}
#elif defined SYSCLK_FREQ_72MHz
static void SetSysClockTo72(void)
{
RCC->CR |= ((uint32_t)RCC_CR_HSEON);
while (!(RCC->CR & RCC_CR_HSERDY))
FLASH->ACR |= FLASH_ACR_PRFTBE;
2
FLASH->ACR &= (uint32_t)((uint32_t)~FLASH_ACR_LATENCY);
FLASH->ACR |= (uint32_t)FLASH_ACR_LATENCY_2;
RCC->CFGR |= (uint32_t)RCC_CFGR_HPRE_DIV1;
RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE2_DIV1;
RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE1_DIV2;
972
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);
RCC->CR |= RCC_CR_PLLON;
while(!(RCC->CR & RCC_CR_PLLRDY));
RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_SW));
RCC->CFGR |= (uint32_t)RCC_CFGR_SW_PLL;
while ((RCC->CFGR & (uint32_t)RCC_CFGR_SWS) != (uint32_t)0x08);
}
#endif
void SystemInit (void)
{
RCC->CR |= (uint32_t)0x00000001; 8
#ifndef STM32F10X_CL
RCC->CFGR &= (uint32_t)0xF8FF0000;
#else
RCC->CFGR &= (uint32_t)0xF0FF0000;
#endif
RCC->CR &= (uint32_t)0xFEF6FFFF;
RCC->CR &= (uint32_t)0xFFFBFFFF;
RCC->CFGR &= (uint32_t)0xFF80FFFF;
#ifdef STM32F10X_CL
RCC->CR &= (uint32_t)0xEBFFFFFF;
RCC->CIR = 0x00FF0000;
RCC->CFGR2 = 0x00000000;
#elif defined (STM32F10X_LD_VL) || defined (STM32F10X_MD_VL) || (defined STM32F10X_HD_VL)
RCC->CIR = 0x009F0000;
RCC->CFGR2 = 0x00000000;
#else
RCC->CIR = 0x009F0000;
#endif
#if defined (STM32F10X_HD) || (defined STM32F10X_XL) || (defined STM32F10X_HD_VL)
#ifdef DATA_IN_ExtSRAM
SystemInit_ExtMemCtl();
#endif
#endif
#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;
#else
#endif
}
Данный файл следует разместить в каталоге проекта "src". В обработчике прерывания Reset (файл startup.s) следует раскомментировать вызов функции SystemInit:
bl SystemInit
В Makefile нужно добавить define:
DEF += -DSYSCLK_FREQ_72MHz
Тоже самое в "CMakeLists.txt"
add_definitions(-DSTM32F10X_MD -DSYSCLK_FREQ_72MHz)
Самые большие изменения в Qbs скрипте, там кроме дефйна нужно еще добавить группу для src файлов. Патч:
$ diff -Naur 00_blink_148.qbs ~/mydev/mcu/f103_25/02_blink_project/00_blink_148.qbs
--- 00_blink_148.qbs 2025-05-29 09:37:37.000000000 +0400
+++ /home/flanker/mydev/mcu/f103_25/02_blink_project/00_blink_148.qbs 2025-05-27 16:08:04.000000000 +0400
@@ -7,7 +7,6 @@
property string Home: sourceDirectory
property string Inc: Home + "/inc"
- property string Src: Home + "/src"
property string Asm: Home + "/asm"
cpp.positionIndependentCode: false
@@ -39,7 +38,6 @@
cpp.defines: [
"STM32F10X_MD",
- "SYSCLK_FREQ_72MHz",
]
Properties
@@ -74,13 +72,6 @@
]
}
- Group {
- name: "Src"
- files: [
- Src + "/*.c",
- ]
- }
-
files: [
"main.c",
]
Сборка:
$ make
arm-none-eabi-gcc -c -mcpu=cortex-m3 -mthumb -Wall -g -Os -ffunction-sections -fdata-sections -Iinc -I. -ICMSIS/device -ICMSIS/core -ISPL/inc -DSTM32F10X_MD -DSYSCLK_FREQ_72MHz main.c -o build/main.o
arm-none-eabi-gcc -c -mcpu=cortex-m3 -mthumb -Wall -g -Os -ffunction-sections -fdata-sections -Iinc -I. -ICMSIS/device -ICMSIS/core -ISPL/inc -DSTM32F10X_MD -DSYSCLK_FREQ_72MHz src/system_init.c -o build/system_init.o
arm-none-eabi-gcc -c -mcpu=cortex-m3 -mthumb -g asm/startup.s -o build/startup.o
arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -Wall -g -Os -ffunction-sections -fdata-sections -Iinc -I. -ICMSIS/device -ICMSIS/core -ISPL/inc -DSTM32F10X_MD -DSYSCLK_FREQ_72MHz build/system_init.o build/main.o build/startup.o -o build/firmware.elf -T stm32f10xx8.ld -nostartfiles -Wl,--gc-sections -specs=nano.specs
arm-none-eabi-objcopy -O binary build/firmware.elf build/firmware.bin
arm-none-eabi-objcopy -O ihex build/firmware.elf build/firmware.hex
arm-none-eabi-size --format=berkeley build/firmware.elf
text data bss dec hex filename
648 0 256 904 388 build/firmware.elf
Сигнал на логическом анализаторе принял такую форму:
Итого, размер прошивки составил 648 байт, и это самая-самая минимальная программа (на мой взгляд) для микроконтроллера stm32f103xx.
Как известно, в Cortex-M3 нет поддержки чисел с плавающей запятой, она реализуется программно. Будет ли это работать в данном проекте? Чтобы ответить на этот вопрос, добавим операцию с вещественными числами в "main.c"
#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
#define WAIT 600000
void dummy_loop(volatile uint32_t count) {
while (--count);
}
int main() {
RCC->APB2ENR |= RCC_APB2Periph_GPIOC;
float a=0.1f;
2
GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13);
GPIOC->CRH |= GPIO_CRH_MODE13_1; 2
while (1) {
a += 0.25;
GPIOC->BSRR = GPIO_Pin_13;
dummy_loop(WAIT * a);
GPIOC->BRR = GPIO_Pin_13;
dummy_loop(WAIT);
}
}
После сборки проекта, размер прошивки увеличится до 1656 байт:
$ arm-none-eabi-size --format=berkeley build/firmware.elf
text data bss dec hex filename
1656 0 256 1912 778 build/firmware.elf
Прошиваем и смотрим на частоту мигания светодиода:
Здесь при нижнем уровне светодиод загорается, а при верхнем - гаснет. И как видно по записи с логического анализатора, светодиод с течением времени загорается все реже и реже, что полностью соответствует алгоритму программы.
6) Функция задержки на ассемблерных инструкциях
Теперь, когда мы добрались до желанных до 72 MHz, хочется узнать, какова же реальная производительность такого микроконтроллера. Адепты STM32 при любом случае кричат, что 72 МHz это в несколько раза быстрее чем 16, но про влияние значения waitstate на производительность CPU, я упоминаний как-то не встречал.
Самым простым тестом производительности будет функция задержки на ассемблерных инструкциях. Маркером здесь будет служить число итераций которые микроконтроллер выполняет за 1 мс. В данном случае мы не учитываем тактовую частоту каждого микроконтроллера т.к. это необъективный показатель.
Итак, для реализации функции задержки добавим в ассемблерный файл asm/assembly.s следующий код:
.global delay
delay:
push {r1}
l1: mov.w r1,#10285
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()
{
RCC->APB2ENR |= RCC_APB2Periph_GPIOC;
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
l1:
decw y
jrne l1
decw x
jrne l0
ret
Т.о. на первый взгляд реальная производительность STM32 "всего" в два раза выше восьмибитного STM8.
Далее добавлено в 2025г. Для вычисления производительности процессоров составляют сложные бенчмарки, и говорить о производительности на данном примере некорректно.
Вместо того, чтобы добавлять в проект ассемблерный модуль с одной единственной функцией, можно воспользоваться inline ассемблером, и тогда "main.c" примет следующий вид:
#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
#define MS_1000 1000
void delay_ms(uint32_t milliseconds) {
__asm volatile (
"push {r0-r1}\n"
"1:\n"
"mov.w r1, #10285\n" 72000710285.71410285
"2:\n"
"subs r1, #1\n"
"bne 2b\n" 2
"subs %[milliseconds], #1\n"
"bne 1b\n" 1
"pop {r0-r1}\n"
:
: [milliseconds] "r" (milliseconds)
: "r1", "cc"
);
}
int main() {
RCC->APB2ENR |= RCC_APB2Periph_GPIOC;
2
GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13);
GPIOC->CRH |= GPIO_CRH_MODE13_1; 2
while (1) {
GPIOC->BSRR = GPIO_Pin_13;
delay_ms(MS_1000);
GPIOC->BRR = GPIO_Pin_13;
delay_ms(MS_1000);
}
}
По логике вещей, компилятор должен сохранять все используемые регистры при вызове такой функции, и "Clobber List" это список регистров которые должны сохраняться компилятором в стеке, но если посмотреть ассемблерный код модуля "main.c", то будет видно, что там ничего подобного не происходит:
void delay_ms(uint32_t milliseconds) {
__asm volatile (
0: b403 push {r0, r1}
2: f642 012d movw r1, #10285 ; 0x282d
6: 3901 subs r1, #1
8: d1fd bne.n 6 <delay_ms+0x6>
a: 3801 subs r0, #1
c: d1f9 bne.n 2 <delay_ms+0x2>
e: bc03 pop {r0, r1}
"pop {r0-r1}\n" // Restore r0,r1
: /* No outputs */
: [milliseconds] "r" (milliseconds) // input
: "r1", "cc" // clobber list
);
}
10: 4770 bx lr
Disassembly of section .text.startup.main:
00000000 <main>:
int main() {
// Enable GPIOC clock
RCC->APB2ENR |= RCC_APB2Periph_GPIOC;
0: 4a0c ldr r2, [pc, #48] ; (34 <main+0x34>)
int main() {
2: b508 push {r3, lr}
RCC->APB2ENR |= RCC_APB2Periph_GPIOC;
4: 6993 ldr r3, [r2, #24]
6: f043 0310 orr.w r3, r3, #16
a: 6193 str r3, [r2, #24]
// Configure PC13 as push-pull output (max speed 2 MHz)
GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13);
c: 4b0a ldr r3, [pc, #40] ; (38 <main+0x38>)
e: 685a ldr r2, [r3, #4]
10: f422 0270 bic.w r2, r2, #15728640 ; 0xf00000
14: 605a str r2, [r3, #4]
GPIOC->CRH |= GPIO_CRH_MODE13_1; // Output mode, max speed 2 MHz (0b10)
16: 685a ldr r2, [r3, #4]
18: f442 1200 orr.w r2, r2, #2097152 ; 0x200000
1c: 605a str r2, [r3, #4]
// Super loop to toggle PC13
while (1) {
GPIOC->BSRR = GPIO_Pin_13; // Set PC13
1e: f44f 5200 mov.w r2, #8192 ; 0x2000
delay_ms(MS_1000);
22: f44f 707a mov.w r0, #1000 ; 0x3e8
GPIOC->BSRR = GPIO_Pin_13; // Set PC13
26: 611a str r2, [r3, #16]
delay_ms(MS_1000);
28: f7ff fffe bl 0 <main> // вызов функции delay_ms
GPIOC->BRR = GPIO_Pin_13; // Reset PC13
2c: 615a str r2, [r3, #20]
delay_ms(MS_1000);
2e: f7ff fffe bl 0 <main> // вызов функции delay_ms
while (1) {
32: e7f6 b.n 22 <main+0x22>
34: 40021000 .word 0x40021000
38: 40011000 .word 0x40011000
В 22-й строке имеется инструкция " mov.w r0, #1000" которая заносит в r0 параметр функции, однако при втором вызове функции, этой операции нет. И поэтому обязанность сохранения регистров значение которых меняется во время выполнения функции, ложится на плечи программиста. Напомню, что в данном случае программы была компилирована с ключем оптимизации "-Os".
Давайте посмотрим, насколько точно функция формирует задержки.
Как можно видеть на логическом анализаторе, два вызова функции соответствуют 2000 мс, что очень точный результат.
Однако есть нюанс. Если отключить в обработчике прерывания Reset вызов функции "SystemInit", т.е. перевести микроконтроллер снова на дефолтную частоту 8 МГц, то микроконтроллер не будет работать в 9 раз медленнее. Посмотрите на лог:
Функция "delay_ms(1000)" на частоте 8 МГц, отрабатывает за 3.85 секунды, а не за 9 сек. Потому что на 8 МГц, у микроконтроллера флеш-память работает на частоте процессора, а когда микроконтроллер "разгоняют" до 72 МГц, флеш-память продолжает работать на той же самой низкой частоте, и получается, что "подтормаживает". Т.е. работа микроконтроллера на частоте 72 МГц не означает, что ваша программа будет работать в девять раз быстрее, нежели на дефолтных 8 МГц, всего лишь в четыре раза.
В микроконтроллерах других серий этот эффект постарались исправить, в частности, STM32F030 на своих 48 МГц работает на уровне STM32F103 c частотой 72 МГц. Если STM32F103 за 1мс выполняет 10285 итераций пустого цикла, то STM32F030 на 48МГц совершает 10000 итераций за одну миллисекунду. А микроконтроллер компании Artery AT32F403 выполняет 40000 итераций за 1 миллисекунду на рабочей частоте 200МГц. Техника идет вперед.
7) Функция задержки на таймере SysTick
Наиболее универсальным способом осуществления задержек, является использование таймера SysTick. Это простой 32-битный счетчик с одним прерыванием, который является частью ядра Cortex-M. Наиболее доходчиво про его конфигурацию можно почитать в статье - " ARM. Учебный Курс. SysTick — Системный таймер | Электроника для всех", либо в фирменной документации - "Cortex-M3 Technical Reference Manual", глава 8.2.2.
Чтобы добавить работу с таймером SysTick в проект, я предлагаю добавить в корневой каталог проекта три файла. Два заголовочных "inc/main.h", "inc/utils.h" и один с исходным кодом: "src/utils.c". В файлы сборки проекта ничего добавлять не надо, всё подхватится автоматически.
В "main.h" пока скинем скинем именованные константы:
#ifndef __MAIN_H
#define __MAIN_H
#define MS_1000 1000
#define MS_100 100
#endif
В "utils.h" будет декларация функций модуля "utils.c":
#ifndef __UTILS_H
#define __UTILS_H
#include "stm32f10x.h"
void systick_init(void);
void delay_ms_asm(uint32_t milliseconds);
void delay_ms(uint32_t ms);
#endif
В "utils.c" перенесем функцию задержки из "main.c" и добавим код для работы с таймером SysTick:
#include "utils.h"
volatile uint32_t systick_counter = 0;
void SysTick_Handler(void) {
systick_counter++;
}
void systick_init(void) {
systick_counter=0;
if (SysTick_Config(SystemCoreClock / 1000)) {
while (1);
}
}
uint32_t get_ticks(void) {
return systick_counter;
}
void delay_ms(uint32_t ms) {
uint32_t start = get_ticks();
while ((get_ticks() - start) < ms) {
__WFI();
}
}
void delay_ms_asm(uint32_t milliseconds) {
__asm volatile (
"push {r0-r1}\n"
"1:\n"
"mov.w r1, #10285\n" 72000710285.71410285
"2:\n"
"subs r1, #1\n"
"bne 2b\n" 2
"subs %[milliseconds], #1\n"
"bne 1b\n" 1
"pop {r0-r1}\n"
:
: [milliseconds] "r" (milliseconds)
: "r1", "cc"
);
}
Здесь "SysTick_Handler" - обработчик прерывания таймера, содержит инкрементный счетчик. "get_ticks()" возвращает значение счетчика, "systick_init()" настраивает таймер на частоту 1 кГц и запускает его, а "delay_ms()" - реализация задержки.
Работа таймер SysTick требует включения работы прерываний. Поэтому модуль "main.c" примет следующий вид:
#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
#include "main.h"
#include "utils.h"
uint32_t SystemCoreClock = 72000000;
int main() {
RCC->APB2ENR |= RCC_APB2Periph_GPIOC;
2
GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13);
GPIOC->CRH |= GPIO_CRH_MODE13_1; 2
systick_init();
__enable_irq();
while (1) {
GPIOC->BSRR = GPIO_Pin_13;
delay_ms(MS_1000);
GPIOC->BRR = GPIO_Pin_13;
delay_ms(MS_1000);
}
}
В отладочном модуле Cortex-M имеется 32-битный таймер который работает на частоте процессора. С его помощью можно реализовать задержку измеряемую в микросекундах. Он описан в TRM "Cortex-M3 Technical Reference Manual", глава 11.5.
В файл "utils.c" добавим две функции:
bool isDWT_Init=false;
void DWT_Init(void) {
if ((CoreDebug->DEMCR & CoreDebug_DEMCR_TRCENA_Msk) == 0) {
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT_CYCCNT = 0;
DWT_CTRL |= 1U; 0
}
isDWT_Init=true;
}
void delay_us(uint32_t us) {
if (!isDWT_Init) {
DWT_Init();
}
uint32_t cycles = us * (SystemCoreClock/1000000);
if (cycles < 12) cycles = 12;
DWT_CYCCNT = 0;
while (DWT_CYCCNT < cycles);
}
Для проверки я выставил в суперцикле задержки на 1000 мкс и вот какой результат показал логический анализатор:
Т.е. работает довольно точно.
8) Настройка и передача данных по UART
Далее для вывода отладочной информации нам понадобится UART интерфейс. Работа с UART модулем в STM32 мало чем отличается от своего аналога в STM8, разве что только тем, здесь их три. При этом только USART1 тактируется от скоростной APB2 шины, остальные два тактируются от APB1.
Настройка UART через регистры подробнейшем образом разобрана в статье: ARM Учебный курс. USART | Электроника для всех. От STM8 процедура настройки отличается необходимостью включать альтернативный режим работы GPIO и немного другой формулой расчета регистра установки битрейта USART1->BRR.
Для реализации функций UART потребуется добавить модуль "uart" в проект (файлы автоматически подхватятся сборочными скриптами).
Настройку USART1 интерфейса STM32F103 я разместил в модуле "main.c":
#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
#include "stm32f10x_usart.h"
#include "main.h"
#include "utils.h"
#include "uart.h"
uint32_t SystemCoreClock = 72000000;
int main() {
RCC->APB2ENR |= RCC_APB2Periph_GPIOC;
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_USART1EN;
2
GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13);
GPIOC->CRH |= GPIO_CRH_MODE13_1;
GPIOA->CRH &= ~(GPIO_CRH_CNF9 | GPIO_CRH_MODE9);
GPIOA->CRH |= (GPIO_CRH_CNF9_1 | GPIO_CRH_MODE9);
GPIOA->CRH &= ~(GPIO_CRH_CNF10 | GPIO_CRH_MODE10);
GPIOA->CRH |= GPIO_CRH_CNF10_0;
0x1d4c960072
USART1->BRR = 0x271; 115200
USART1->CR1 = USART_CR1_TE | USART_CR1_RE | USART_CR1_UE;
systick_init();
__enable_irq();
while (1) {
GPIOC->ODR ^= GPIO_Pin_13;
USART1->DR = 'A';
while (!(USART1->SR & USART_SR_TXE));
delay_ms(1000);
}
}
В суперцикле небольшой отладочный код, для проверки работы интерфейса.
Нам понадобится заголовочный файл из SPL stm32f10x_usart.h в котором содержатся некоторые битовые маски регистров. К сожалению не все битовые маски содержались в заголовочном файле. Часть находилась в stm32f10x_usart.c. Их пришлось перенести в свой заголовочный файл uart.h:
показать
#ifndef __UART_H__
#define __UART_H__
#include "stm32f10x.h"
#include "stm32f10x_usart.h"
#define USART_CR1_UE_Set ((uint16_t)0x2000)
#define USART_CR1_UE_Reset ((uint16_t)0xDFFF)
#define USART_CR1_WAKE_Mask ((uint16_t)0xF7FF)
#define USART_CR1_RWU_Set ((uint16_t)0x0002)
#define USART_CR1_RWU_Reset ((uint16_t)0xFFFD)
#define USART_CR1_SBK_Set ((uint16_t)0x0001)
#define USART_CR1_CLEAR_Mask ((uint16_t)0xE9F3)
#define USART_CR2_Address_Mask ((uint16_t)0xFFF0)
#define USART_CR2_LINEN_Set ((uint16_t)0x4000)
#define USART_CR2_LINEN_Reset ((uint16_t)0xBFFF)
#define USART_CR2_LBDL_Mask ((uint16_t)0xFFDF)
#define USART_CR2_STOP_CLEAR_Mask ((uint16_t)0xCFFF)
#define USART_CR2_CLOCK_CLEAR_Mask ((uint16_t)0xF0FF)
#define USART_CR3_SCEN_Set ((uint16_t)0x0020)
#define USART_CR3_SCEN_Reset ((uint16_t)0xFFDF)
#define USART_CR3_NACK_Set ((uint16_t)0x0010)
#define USART_CR3_NACK_Reset ((uint16_t)0xFFEF)
#define USART_CR3_HDSEL_Set ((uint16_t)0x0008)
#define USART_CR3_HDSEL_Reset ((uint16_t)0xFFF7)
#define USART_CR3_IRLP_Mask ((uint16_t)0xFFFB)
#define USART_CR3_CLEAR_Mask ((uint16_t)0xFCFF)
#define USART_CR3_IREN_Set ((uint16_t)0x0002)
#define USART_CR3_IREN_Reset ((uint16_t)0xFFFD)
#define USART_GTPR_LSB_Mask ((uint16_t)0x00FF)
#define USART_GTPR_MSB_Mask ((uint16_t)0xFF00)
#define USART_IT_Mask ((uint16_t)0x001F)
8
#define CR1_OVER8_Set ((u16)0x8000)
#define CR1_OVER8_Reset ((u16)0x7FFF)
#define CR3_ONEBITE_Set ((u16)0x0800)
#define CR3_ONEBITE_Reset ((u16)0xF7FF)
#endif
Подключение USB-UART адаптера к Bluepill следующее:RX(адаптера) к PA9(bluebill), GND(адаптера) к GND(bluepill):
После компиляции и прошивки, на ножке PA9 появится UART сигнал с литерой "эй" (hex-код 0x41):
Для того, чтобы иметь возможность передавать через USART интерфейс строки и числа (предварительно прошедшию сериализацию), в модуле "uart.c" разместим следующий код:
#include "uart.h"
#include <stdarg.h>
void usart1_send_char(uint8_t ch) {
USART1->DR = ch;
while(!(USART1->SR & USART_SR_TXE));
}
void usart1_print_string(const char *str) {
while(*str) {
usart1_send_char(*str++);
}
}
32
void usart1_print_number(uint32_t num) {
char buffer[10]; 1032
char *p = buffer + sizeof(buffer) - 1;
*p = '\0';
do {
*(--p) = '0' + (num % 10);
num /= 10;
} while(num > 0);
usart1_print_string(p);
}
void usart1_printf(const char *format, ...) {
va_list args;
va_start(args, format);
while(*format) {
if(*format == '%') {
format++;
switch(*format) {
case 'd': {
int32_t num = va_arg(args, int32_t);
if(num < 0) {
usart1_send_char('-');
num = -num;
}
usart1_print_number((uint32_t)num);
break;
}
case 'u':
usart1_print_number(va_arg(args, uint32_t));
break;
case 's':
usart1_print_string(va_arg(args, char*));
break;
case 'c':
usart1_send_char(va_arg(args, int));
break;
case '%':
usart1_send_char('%');
break;
}
} else {
usart1_send_char(*format);
}
format++;
}
va_end(args);
}
Для проверки кода, суперцикл в "main.c" пусть будет таким:
int i=0;
while (1) {
GPIOC->ODR ^= GPIO_Pin_13;
usart1_print_string("Hello World\r\n");
usart1_print_number(314159);
usart1_printf("Temperature: %d°C, Status: %s\r\n", i++, "OK");
delay_ms(1000);
}
После компиляции, размер прошивки перевалит за первый килобайт:
arm-none-eabi-size --format=berkeley build/firmware.elf
text data bss dec hex filename
1116 4 260 1380 564 build/firmware.elf
К счастью, в микроконтроллере имеется еще 63 кБ флеш-памяти.
На логическом анализаторе, передача по UART будет выглядеть так:
В терминальной программе пойдет лог:
Можно немного доработать модуль "uart.c" добавив функцию печати шестнадцатеричного числа:
const char hex[] = "0123456789ABCDEF";
void usart1_print_hex(uint32_t num) {
char buffer[11]; "0x"8
char *p = buffer + sizeof(buffer) - 1;
*p = '\0';
do {
*(--p) = hex[num & 0xF];
num >>= 4; 4
} while (num != 0);
"0x"
*(--p) = 'x';
*(--p) = '0';
usart1_print_string(p);
}
Теперь, в функции "usart1_printf", в switch-блок добавляем два кейса:
case 'x':
usart1_print_hex(va_arg(args, uint32_t));
break;
case 'X': {
int32_t num = va_arg(args, int32_t);
if(num < 0) {
usart1_send_char('-');
num = -num;
}
usart1_print_hex((uint32_t)num);
break;
}
Для примера, с помощью данной функции можно вывести Unique ID микроконтроллера. Он прошит по адресу: 0x1FFF_F7E8
страница из Reference Manual RM0008
В модуле "main.c" добавим адрес:
volatile uint32_t *uid = (volatile uint32_t*)0x1FFFF7E8;
А в главный цикл печать Unique ID:
usart1_printf("STM32-%x-%x-%x",uid[0],uid[1],uid[2]);
Теперь в терминальной программе пойдет следующий лог:
314159
Temperature: 19°C, Status: OK
STM32-0x670FF52-0x53536752-0x87163913
Hello World
314159
Temperature: 20°C, Status: OK
STM32-0x670FF52-0x53536752-0x87163913
9) Прием строки через UART интерфейс
Прием данных по UART осуществляется с помощью 37-го прерывания "USART1 Global Interrupt", вектор 0xD4. В отличии от прерывания таймера SysTick, его надо включать через контроллер прерываний. Для этого, в модуле "main.c" следует раскомментировать строки:
USART1->CR1 |= USART_CR1_RXNEIE;
NVIC_EnableIRQ(USART1_IRQn);
При необходимости можно выставить свой приоритет прерывания командой:
NVIC_SetPriority(USART1_IRQn,1); // set priority for USART1 IRQ
Данная команда должна быть перед " NVIC_EnableIRQ(USART1_IRQn)". Приоритеты могут быть от 0 до 15. Чем меньше номер приоритета, тем он "сильнее/главнее".
В модуле "uart.c" разместим обработчик прерывания:
volatile bool UART_RDY = false;
volatile uint8_t UART_IDX = 0;
volatile char UART_BUF[UART_BUFFER_LEN];
void USART1_IRQHandler(void)
{
uint32_t status = USART1->SR;
uint8_t ch=USART1->DR;
if ((status & USART_SR_ORE) || UART_RDY) {
return;
}
if (status & USART_SR_RXNE ) {
if (ch == '\n' || ch == '\r') {
UART_BUF[UART_IDX] = '\0';
UART_RDY = true;
}
else if (UART_IDX < (UART_BUFFER_LEN-1)) {
UART_BUF[UART_IDX++] = ch;
}
else {
UART_BUF[UART_BUFFER_LEN-1] = '\0';
UART_RDY = true;
}
}
}
Данное прерывание принимает строку, которая должна заканчиваться символами '\n' или '\r'. Строка размещается в кольцевом буфере "UART_BUF". После получении строки выставляется флаг "UART_RDY".
Прерывание по RXNEIE могут вызывать два флага в статусном регистре SR - RXNE и ORE:
Флаг RXNE выставляется, когда приемник UART принимает байт с линии передачи. Флаг снимается, при чтении регистра USARTx_DR.
Флаг ORE выставляется, когда приемник UART принимает байт с линии передачи, но регистр USART_DR не пуст, т.е. флаг RXNE выставлен, а регистр USART_DR все еще не считан. Флаг ORE очищается последовательным чтением регистров USART_SR и USART_DR.
Итак, разбираем код обработчика прерывания USART1:
void USART1_IRQHandler(void)
{
uint32_t status = USART1->SR;
uint8_t ch=USART1->DR;
Считываем регистры SR и DR, тем самым сбрасывая флаги ORE и RXNE если они были установлены. Далее:
if ((status & USART_SR_ORE) || UART_RDY) {
return;
}
Если был выставлен флаг ORE, или строка уже принята, но еще не обработана, то выходим из прерывания. Последнее может произойти, когда в конце строки идут несколько символов окончания строки. Такие символы отбрасываются.
if (status & USART_SR_RXNE ) {
В этих скобках идет обработка принимаемых данных.
if (ch == '\n' || ch == '\r') {
UART_BUF[UART_IDX] = '\0';
UART_RDY = true;
}
Если принимается конец строки, то буфер терминируется записью нуля и выставляется флаг UART_DRY о готовности строки к обработке.
else if (UART_IDX < (UART_BUFFER_LEN-1)) {
UART_BUF[UART_IDX++] = ch;
}
Здесь происходит запись принятых данных в буфер.
else {
UART_BUF[UART_BUFFER_LEN-1] = '\0';
UART_RDY = true;
}
Если буфер подошёл к концу, т.е. слишком длинная строка, то терминируем буфер и выставляем флаг UART_RDY.
Должен заметить, что если при отладке поставить break point на прерывания и посмотреть регистр UART1_SR, то RXNE флаг там будет сброшен:
Возможно потому, что отладчик читает регистр DR, и это приводит к сбросу флага. Хотя флаг ORE почему-то не сбрасывается. Такая вот странность.
Также в модуле "uart.c" разместим функцию для обработки принятых данных.
bool uart_command_processing(bool flag) {
bool ret=flag;
char local_buf[UART_BUFFER_LEN];
__disable_irq();
for (uint32_t i = 0; i < UART_BUFFER_LEN; i++) {
local_buf[i] = UART_BUF[i];
UART_BUF[i]=0;
}
UART_IDX=0;
UART_RDY = false;
__enable_irq();
char* cmd = local_buf;
while(*cmd == ' ') cmd++;
if (embedded_strncmp(cmd, "help",UART_BUFFER_LEN) == 0 || \
embedded_strncmp(cmd, "?",UART_BUFFER_LEN) == 0) {
usart1_print_string("Help menu:\r\n");
usart1_print_string("t- Disable print tick\r\n");
usart1_print_string("t+ Enable print tick\r\n");
}
else if (embedded_strncmp(cmd, "t-",UART_BUFFER_LEN) == 0) {
usart1_print_string("Tick disabled\r\n");
ret = false;
}
else if (embedded_strncmp(cmd, "t+",UART_BUFFER_LEN) == 0) {
usart1_print_string("Tick enabled\r\n");
ret =true;
}
else {
usart1_print_string("Unknown command: ");
usart1_print_string(cmd);
usart1_print_string("\r\n");
}
return ret;
}
функция управляет только одним флагом, поэтому возвращает булевое значение. В самом начале функции, буфер UART_BUF копируется в локальный буфер, одновременно очищая его. После чего сбрасывается флаг UART_RDY. Дальше происходит парсинг принятой строки. Доступные команды: "t+", "t-", "help", "?".
Для сравнения строк используется функция:
int embedded_strncmp(volatile const char *s1, const char *s2, uint8_t max_len) {
for (uint8_t i = 0; i < max_len; i++) {
char c1 = s1[i];
char c2 = s2[i];
if (c1 == '\0' || c2 == '\0' || c1 != c2) {
return (int)(c1 - c2);
}
}
return 0;
}
Которая также размещается в модуле "uart.c".
Т.к. в суперцикле используется блокирующая функция delay_ms(), для обработки входящих данных потребуется заменить ее неблокирующей версией, которая при выходе из спящего режима будет проверять флаг UART_RDY, и если он окажется остановлен, она должная передать управление обработчику принятых данных "uart_command_processing()". Я назвал ее "smart-delay()":
extern bool UART_RDY;
int smart_delay(uint32_t ms) {
uint32_t start = get_ticks();
while (((get_ticks() - start) < ms) && \
(UART_RDY == false)) {
__WFI();
}
uint32_t ticks=get_ticks();
int ret = ms - (ticks -start);
return ret;
}
Если после выхода из спящего режима установлен флаг UART_DRY, то функция прерывает работу и возвращает количество миллисекунд, которые не успела отработать. Эта функция размещена в модуле "utils.c", вместе с остальными функциями задержки.
Суперцикл с приемом команд по UART получился таким:
bool tick=true;
uint32_t cnt=0;
usart1_print_string("ready...\r\n");
uint32_t interval=WAIT;
while (1) {
int remainder=smart_delay(interval);
if (UART_RDY) {
tick=uart_command_processing(tick);
}
if ( remainder > 0 ) {
interval = remainder;
continue;
} else {
interval =WAIT;
}
++cnt;
GPIOC->ODR ^= GPIO_Pin_13;
if (tick) {
usart1_printf("tick - %u\r\n",cnt);
}
}
}
Суперцикл раз в секунду мигает светодиодом и отправляет по UART строку "tick" с количеством итераций. "smart_delay()" если обнаруживает новые данные, прекращает работу и в дело вступает "uart_command_processing()". После завершения обработки принятых данных, управление вновь возвращается "smart_delay()", чтобы она "дотикала" временной интервал до конца.
Доступны команды "t+" и "t-", которые включают и выключают передачу "тика".
Давайте посмотрим, как быстро происходит обработка принимаемых данных:
Выглядит практически мгновенно, программа мне показала интервал в 20 мс между окончанием приема и началом передачи. Примерно туже цифру можно увидеть мониторе последовательного порта:
Осталось добавить про подключение. Для UART1 используются пины PA9 (TX) и PA10(RX). USB-UART конвертор подключается кроссированно: RX пин конвертора к TX пину микроконтроллера, TX пин конвертора к RX пину микроконтроллера.
10) Числа с плавающей запятой, подключение стандартной библиотеки Си к проекту
Имеется целый класс задач, которые требуют операций с вещественными числами. Это вынуждает использовать стандартную библиотеку Си, где имеется модуль "math", в котором реализованы различные математические функции. Иногда просто требуется какая-то функция из стандартной библиотеки, скажем получение псевдослучайного числа, а изобретать велосипед нет времени.
Итак, для подключения стандартной библиотеки Си к проекту необходимо:
-
Убедиться в том, что в скрипте компоновщика раскомментирована следующая секция:
/DISCARD/ :
{
libc.a ( * )
libm.a ( * )
libgcc.a ( * )
}
-
В обработчике прерывания "Reset" (файл startup.s) следует раскомментировать строку:
bl __libc_init_array
-
В Makefile, в опциях компоновщика, заменить опцию "-nostartfiles" на "-specs=nosys.specs":
LDFLAGS = -T stm32f10xx8.ld -Wl,--gc-sections -specs=nosys.specs -specs=nano.specs
Пересобираем проект:
$ make clean && make all
rm -f -r build
arm-none-eabi-gcc -c -mcpu=cortex-m3 -mthumb -Wall -g -Os -ffunction-sections -fdata-sections -Iinc -I. -ICMSIS/device -ICMSIS/core -ISPL/inc -DSTM32F10X_MD -DSYSCLK_FREQ_72MHz src/system_init.c -o build/system_init.o
arm-none-eabi-gcc -c -mcpu=cortex-m3 -mthumb -Wall -g -Os -ffunction-sections -fdata-sections -Iinc -I. -ICMSIS/device -ICMSIS/core -ISPL/inc -DSTM32F10X_MD -DSYSCLK_FREQ_72MHz src/uart.c -o build/uart.o
arm-none-eabi-gcc -c -mcpu=cortex-m3 -mthumb -Wall -g -Os -ffunction-sections -fdata-sections -Iinc -I. -ICMSIS/device -ICMSIS/core -ISPL/inc -DSTM32F10X_MD -DSYSCLK_FREQ_72MHz src/utils.c -o build/utils.o
arm-none-eabi-gcc -c -mcpu=cortex-m3 -mthumb -Wall -g -Os -ffunction-sections -fdata-sections -Iinc -I. -ICMSIS/device -ICMSIS/core -ISPL/inc -DSTM32F10X_MD -DSYSCLK_FREQ_72MHz main.c -o build/main.o
arm-none-eabi-gcc -c -mcpu=cortex-m3 -mthumb -g asm/startup.s -o build/startup.o
arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -Wall -g -Os -ffunction-sections -fdata-sections -Iinc -I. -ICMSIS/device -ICMSIS/core -ISPL/inc -DSTM32F10X_MD -DSYSCLK_FREQ_72MHz build/system_init.o build/uart.o build/utils.o build/main.o build/startup.o -o build/firmware.elf -T stm32f10xx8.ld -Wl,--gc-sections -specs=nosys.specs -specs=nano.specs
arm-none-eabi-objcopy -O binary build/firmware.elf build/firmware.bin
arm-none-eabi-objcopy -O ihex build/firmware.elf build/firmware.hex
arm-none-eabi-size --format=berkeley build/firmware.elf
text data bss dec hex filename
1908 12 300 2220 8ac build/firmware.elf
Прошивка увеличилась почти на сотню с небольшим байт. Обращаю внимание, что опция: "-specs=nano.specs" указывает на использование реализации стандартной библиотеки "newlib", которая идет вместе с компилятором, что был скачан с сайта arm. Если вы тулчейн ставили с репозитория вашего дистрибутива, то этой библиотеки там может и не быть, возможно ее надо будет установить отдельно.
Теперь прошиваем микроконтроллер командой:
$ make flash
И если зеленый светодиод продолжит мигать, то значит, что все было успешно.
В "CMakeLists.txt" также потребуется заменить в строке:
set(CMAKE_EXE_LINKER_FLAGS "-nostartfiles -specs=nano.specs -T${LINKER_SCRIPT} -Wl,--gc-sections")
опцию "-nostartfiles" на "-specs=nosys.specs". После чего собираем:
$ mkdir cbuild && cd $_
$ cmake ..
-- The C compiler identification is GNU 11.2.0
-- The ASM compiler identification is GNU
-- Found assembler: /usr/bin/cc
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/flanker/qtcreator_projects/00_blink_148/cbuild
$ make all && make size
Scanning dependencies of target firmware.elf
[ 16%] Building C object CMakeFiles/firmware.elf.dir/src/uart.c.o
[ 33%] Building C object CMakeFiles/firmware.elf.dir/main.c.o
[ 50%] Building C object CMakeFiles/firmware.elf.dir/src/system_init.c.o
[ 66%] Building C object CMakeFiles/firmware.elf.dir/src/utils.c.o
[ 83%] Building ASM object CMakeFiles/firmware.elf.dir/asm/startup.s.o
[100%] Linking C executable firmware.elf
[100%] Built target firmware.elf
Consolidate compiler generated dependencies of target firmware.elf
[100%] Built target firmware.elf
text data bss dec hex filename
1908 12 300 2220 8ac firmware.elf
[100%] Built target size
Размер прошивки как можно видеть совпал. Обязательно прошиваем и проверяем.
Самые обширные изменения потребовал скрипт Qbs. Там пришлось добавить секцию "cpp.DriverLinkerFlags" и перетасовать остальные секции. Посто приведу готовый скрипт под спойлером:
открыть спойлер
import qbs
Product {
type: ["application","flash"]
Depends { name: "cpp" }
name: "firmware"
property string Home: sourceDirectory
property string Inc: Home + "/inc"
property string Src: Home + "/src"
property string Asm: Home + "/asm"
cpp.positionIndependentCode: false
cpp.executableSuffix: ".elf"
cpp.includePaths:[
"CMSIS/device",
"CMSIS/core",
"SPL/inc",
"inc",
]
cpp.driverFlags: [
"-mcpu=cortex-m3",
"-mthumb",
"-Wall",
"-ffunction-sections",
"-fdata-sections",
]
cpp.driverLinkerFlags: [
"-Wl,--gc-sections",
"-specs=nosys.specs",
"-specs=nano.specs",
"-T" + sourceDirectory + "/stm32f10xx8.ld",
]
cpp.cFlags: [
"-mthumb",
"-mcpu=cortex-m3",
"-Wall",
]
cpp.defines: [
"STM32F10X_MD",
"SYSCLK_FREQ_72MHz",
]
Properties
{
condition: qbs.buildVariant === "debug"
cpp.defines: outer.concat(["DEBUG=1"])
cpp.debugInformation: true
cpp.optimization: "debug"
cpp.cFlags: outer.concat(["-ggdb"])
}
Properties
{
condition: qbs.buildVariant === "release"
cpp.debugInformation: false
cpp.optimization: "small"
}
Group {
name: "Headers"
files: [
Inc + "/*.h",
]
fileTags: ["h_src"]
}
Group {
name: "Assembly"
fileTags: ["asm"]
files: [
Asm + "/*.s",
]
}
Group {
name: "Src"
files: [
Src + "/*.c",
]
}
files: [
"main.c",
]
Rule
{
inputs: ["application"]
Artifact
{
filePath: product.name + ".bin"
fileTags: "flash"
}
prepare:
{
var size_name=input.filePath
var argsObjcopy = ["-O", "binary", input.filePath, output.filePath]
var cmd_bin=new Command("arm-none-eabi-objcopy",argsObjcopy)
var cmd_size=new Command("arm-none-eabi-size",size_name)
cmd_bin.description = "Generating binary: " + output.fileName
cmd_size.description = "Size of " + output.fileName
return [cmd_bin,cmd_size]
}
}
}
Сборка проекта с помощью Qbs:
$ qbs build config:release profile:arm-none-eabi-gcc-10_3 --command-echo-mode command-line
Build graph does not yet exist for configuration 'release'. Starting from scratch.
Resolving project for configuration release
WARNING: Could not detect target platform ('linux' given)
Setting up build graph for configuration release
Building for configuration release
/usr/local/bin/arm-none-eabi-as -I/home/flanker/qtcreator_projects/00_blink_148/CMSIS/device -I/home/flanker/qtcreator_projects/00_blink_148/CMSIS/core -I/home/flanker/qtcreator_projects/00_blink_148/SPL/inc -I/home/flanker/qtcreator_projects/00_blink_148/inc -o /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/2445be8241aabd34/startup.s.o /home/flanker/qtcreator_projects/00_blink_148/asm/startup.s
/usr/local/bin/arm-none-eabi-gcc -Os -Wall -Wextra -mcpu=cortex-m3 -mthumb -Wall -ffunction-sections -fdata-sections -pipe -fvisibility=default -mthumb -mcpu=cortex-m3 -Wall -DNDEBUG -DSTM32F10X_MD -DSYSCLK_FREQ_72MHz -I/home/flanker/qtcreator_projects/00_blink_148/CMSIS/device -I/home/flanker/qtcreator_projects/00_blink_148/CMSIS/core -I/home/flanker/qtcreator_projects/00_blink_148/SPL/inc -I/home/flanker/qtcreator_projects/00_blink_148/inc -o /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/f27fede2220bcd32/system_init.c.o -c /home/flanker/qtcreator_projects/00_blink_148/src/system_init.c
/usr/local/bin/arm-none-eabi-gcc -Os -Wall -Wextra -mcpu=cortex-m3 -mthumb -Wall -ffunction-sections -fdata-sections -pipe -fvisibility=default -mthumb -mcpu=cortex-m3 -Wall -DNDEBUG -DSTM32F10X_MD -DSYSCLK_FREQ_72MHz -I/home/flanker/qtcreator_projects/00_blink_148/CMSIS/device -I/home/flanker/qtcreator_projects/00_blink_148/CMSIS/core -I/home/flanker/qtcreator_projects/00_blink_148/SPL/inc -I/home/flanker/qtcreator_projects/00_blink_148/inc -o /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/f27fede2220bcd32/uart.c.o -c /home/flanker/qtcreator_projects/00_blink_148/src/uart.c
/usr/local/bin/arm-none-eabi-gcc -Os -Wall -Wextra -mcpu=cortex-m3 -mthumb -Wall -ffunction-sections -fdata-sections -pipe -fvisibility=default -mthumb -mcpu=cortex-m3 -Wall -DNDEBUG -DSTM32F10X_MD -DSYSCLK_FREQ_72MHz -I/home/flanker/qtcreator_projects/00_blink_148/CMSIS/device -I/home/flanker/qtcreator_projects/00_blink_148/CMSIS/core -I/home/flanker/qtcreator_projects/00_blink_148/SPL/inc -I/home/flanker/qtcreator_projects/00_blink_148/inc -o /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/3a52ce780950d4d9/main.c.o -c /home/flanker/qtcreator_projects/00_blink_148/main.c
/usr/local/bin/arm-none-eabi-gcc -Os -Wall -Wextra -mcpu=cortex-m3 -mthumb -Wall -ffunction-sections -fdata-sections -pipe -fvisibility=default -mthumb -mcpu=cortex-m3 -Wall -DNDEBUG -DSTM32F10X_MD -DSYSCLK_FREQ_72MHz -I/home/flanker/qtcreator_projects/00_blink_148/CMSIS/device -I/home/flanker/qtcreator_projects/00_blink_148/CMSIS/core -I/home/flanker/qtcreator_projects/00_blink_148/SPL/inc -I/home/flanker/qtcreator_projects/00_blink_148/inc -o /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/f27fede2220bcd32/utils.c.o -c /home/flanker/qtcreator_projects/00_blink_148/src/utils.c
/usr/local/bin/arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -Wall -ffunction-sections -fdata-sections -o /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/firmware.elf /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/2445be8241aabd34/startup.s.o /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/3a52ce780950d4d9/main.c.o /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/f27fede2220bcd32/system_init.c.o /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/f27fede2220bcd32/uart.c.o /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/f27fede2220bcd32/utils.c.o -Wl,--gc-sections -specs=nosys.specs -specs=nano.specs -T/home/flanker/qtcreator_projects/00_blink_148/stm32f10xx8.ld
/usr/local/bin/arm-none-eabi-objcopy -O binary /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/firmware.elf /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/firmware.bin
/usr/local/bin/arm-none-eabi-size /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/firmware.elf
/usr/local/bin/arm-none-eabi-size /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/firmware.elf
text data bss dec hex filename
1908 12 300 2220 8ac /home/flanker/qtcreator_projects/00_blink_148/release/firmware.9bcf18e4/firmware.elf
Build done for configuration release.
И тут тоже размер релизной прошивки тютелька в тютельку. Но они не одно и тоже:
$ md5sum ./build/firmware.bin
08997fd09274d0430ec641de8e7590a9 ./build/firmware.bin
$ md5sum ./release/firmware.9bcf18e4/firmware.bin
ae292df7944e2c15c764f577c4350b25 ./release/firmware.9bcf18e4/firmware.bin
Теперь, в модуль "uart.c" можно добавить функцию печати вещественного числа:
#ifdef USE_FLOAT
void usart1_print_float(float num) {
if(num < 0.f) {
usart1_send_char('-');
num = -num;
}
int32_t int_part = (int32_t)num;
usart1_print_number(int_part);
usart1_send_char('.');
float frac = num - int_part;
uint32_t frac_part = (uint32_t)(frac * 100 + 0.5);
if(frac_part < 10) usart1_send_char('0');
usart1_print_number(frac_part);
}
#endif
Строго говоря, функция печатает число с фиксированной точкой, с двумя знаками после запятой.
В switch-блок добавляем следующий кейс:
#ifdef USE_FLOAT
case 'f':
usart1_print_float(va_arg(args, double));
break;
#endif
В перечень используемых именованных констант в Makefile, CMakeLists.txt и Qbs-скрипт, следует добавить "USE_FLOAT", чтобы весь код использующий вещественные числа можно было легко выключить или включить.
После пересборки проекта, размер прошивки составит 4372 байт, т.е. в два раза больше чем было. К счастью, с флеш-памятью в 32-битных микроконтроллерах проблем нет, в отличии 8/16-битных.
В главном цикле модуля "main.c" будем вычислять синусы:
#ifdef USE_FLOAT
for(float i = 0.f; ; i+=0.25f) {
int remainder=smart_delay(interval);
if (UART_RDY) {
tick=uart_command_processing(tick);
}
if ( remainder > 0 ) {
interval = remainder;
continue;
} else {
interval =WAIT;
}
GPIOC->ODR ^= GPIO_Pin_13;
if (tick) {
usart1_printf("tick: %f",i);
usart1_printf(" hex: %x\r\n",(uint32_t)i);
float sin=sinf((i * M_PI)/180.f);
usart1_printf("sin: %f\r\n", sin);
}
}
#else
uint32_t cnt=0;
while (1) {
int remainder=smart_delay(interval);
if (UART_RDY) {
tick=uart_command_processing(tick);
}
if ( remainder > 0 ) {
interval = remainder;
continue;
} else {
interval =WAIT;
}
++cnt;
GPIOC->ODR ^= GPIO_Pin_13;
if (tick) {
usart1_printf("tick - %u\r\n",cnt);
}
}
#endif
В модуль "main.c" потребуется включить заголовочный файл "math.h":
#include "math.h"
После компиляции, размер прошивки составит 10172 байта:
arm-none-eabi-size --format=berkeley build/firmware.elf
text data bss dec hex filename
10172 12 300 10484 28f4 build/firmware.elf
После перепрошивки в терминальной программе пойдет примерно такой лог:
tick: 5.50 hex: 0x5
sin: 0.10
tick: 5.75 hex: 0x5
sin: 0.10
tick: 6.00 hex: 0x6
sin: 0.10
tick: 6.25 hex: 0x6
sin: 0.11
tick: 6.50 hex: 0x6
sin: 0.11
tick: 6.75 hex: 0x6
sin: 0.12
tick: 7.00 hex: 0x7
sin: 0.12
tick: 7.25 hex: 0x7
sin: 0.13
tick: 7.50 hex: 0x7
sin: 0.13
tick: 7.75 hex: 0x7
sin: 0.13
tick: 8.00 hex: 0x8
sin: 0.14
tick: 8.25 hex: 0x8
sin: 0.14
tick: 8.50 hex: 0x8
sin: 0.15
Для проверки, синус восьми градусов равен 0,139.
Т.о. от создания минималистического "Blink" в 148 байт, мы дошли до 10-и килобайтной прошивки.
11) USART в режиме SPI-master
В микроконтроллере stm32f103c8 имеется три USART интерфейса, и если один использовать для отладки, остальные можно использовать как "слабенькие" SPI. В даташите указано, что на USART_1 максимальная скорость будет 4.5 Мбит, на USART_2 и USART_3 - 2.25 Мбит. Это немного, но для монохромного дисплея этого вполне достаточно. Для примера возьмем дисплей от Nokia 1202 на контроллере ste2007, о котором я рассказывал пару лет назад: "Общий обзор дисплея Nokia 1202". Дисплей использует 9-битный SPI протокол, который не поддерживается SPI модулем stm32f103xx, его можно реализовать только через USART, либо bit-bang'ом.
Использовать будем интерфейс USART_2:
Для подключения дисплея понадобится сконфигурировать ножки PA2(TX) и PA4(CLK) в альтернативный режим, а PB4(RST) и PA5(CS) в Push-Pull режим. Соответственно, первым делом понадобится включить тактирование для GPIOA, GPIOB и USART2:
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN;
RCC->APB1ENR |= RCC_APB1ENR_USART2EN;
После чего конфигурируем ножки:
GPIOB->CRL &= ~(GPIO_CRL_CNF4 | GPIO_CRL_MODE4);
GPIOB->CRL |= (GPIO_CRL_MODE4_0 | GPIO_CRL_MODE4_1); 1150
GPIOA->CRL &= ~(GPIO_CRL_CNF2 | GPIO_CRL_MODE2);
GPIOA->CRL |= (GPIO_CRL_CNF2_1 | GPIO_CRL_MODE2);
GPIOA->CRL &= ~(GPIO_CRL_CNF4 | GPIO_CRL_MODE4);
GPIOA->CRL |= (GPIO_CRL_CNF4_1 | GPIO_CRL_MODE4);
GPIOA->CRL &= ~(GPIO_CRL_CNF5 | GPIO_CRL_MODE5);
GPIOA->CRL |= (GPIO_CRL_MODE5_0 | GPIO_CRL_MODE5_1);
И последнее, конфигурируем USART_2 интерфейс в режим 9-битного синхронного USART:
USART2->BRR = 0x10; 2.25
USART2->CR2 = USART_CR2_CLKEN |
USART_CR2_STOP_1 | 1
USART_CR2_LBCL; 9
USART2->CR1 = USART_CR1_M | 91
USART_CR1_TE |
USART_CR1_UE;
Этот режим также будет SPI. Т.е. данный режим передачи будет соответствовать двум протоколам: SPI и USART. На линии это выглядит так:
Передача одновременно является как USART, так и SPI.
Добавим в проект модуль "ste2007.c" и там разместим функцию передачи байта:
extern uint8_t reverse_bits_asm(uint32_t);
void USART2_SPI_Write9(uint8_t dc,uint8_t data){
uint16_t packet=(uint16_t)reverse_bits_asm((uint32_t)data);
packet= (packet<<1) | (dc & 0x01);
GPIOA->BSRR = GPIO_BSRR_BR5;
USART2->DR = packet; 9
while(!(USART2->SR & USART_FLAG_TC));
GPIOA->BSRR = GPIO_BSRR_BS5;
}
Т.к. при UASRT протоколе данные передаются младшим битом вперед, а при SPI протоколе обычно данные передаются старшим битом вперед, то перед отправкой, данные следует "переворачивать". Для этого существуют различные алгоритмы, я долгое время пользовался примером подсмотренном на stackoverflow. Но т.к. функция отправки данных будет выполняться в цикле, я посчитал, что имеет смысл использовать вариант с аппаратной акселерацией, т.е. на ассемблере.
Это тоже можно осуществить по разному. Можно например, в ассемблерном модуле "startup.s", сразу после функции "Infinite_Loop" разместить код функции "reverse_bits_asm()", объявление которой фигурировало выше:
.global reverse_bits_asm
reverse_bits_asm:
rbit r0, r0
lsr r0, r0, #24
bx lr
Но "startup.s" я бы трогать не хотел, и поэтому решил снова воспользоваться inline ассемблером, и добавил функцию в модуль "ste2007.c":
uint8_t reverse_bits_asm(uint32_t data) {
uint32_t result;
__asm volatile (
"rbit %[result], %[data]\n"
"lsr %[result], %[result], #24\n"
: [result] "=r" (result)
: [data] "r" (data)
:
);
return (uint8_t)result;
}
Далее понадобится функция инициализации дисплея:
void ste2007_init() {
GPIOB->BSRR = GPIO_BSRR_BR4;
delay_us(100);
GPIOB->BSRR = GPIO_BSRR_BS4;
delay_us(100);
USART2_SPI_Write9(LCD_C, 0xE2);
delay_us(100);
USART2_SPI_Write9(LCD_C, 0x2F);
delay_us(100);
USART2_SPI_Write9(LCD_C, 0xA0);
USART2_SPI_Write9(LCD_C, 0xC8);
USART2_SPI_Write9(LCD_C, 0x94); 0x90
USART2_SPI_Write9(LCD_C, 0xA4);
USART2_SPI_Write9(LCD_C, 0x40); 0
USART2_SPI_Write9(LCD_C, 0xA6);
USART2_SPI_Write9(LCD_C, 0xAF);
USART2_SPI_Write9(LCD_C, 0xB0); 0
USART2_SPI_Write9(LCD_C, 0x10); 0
USART2_SPI_Write9(LCD_C, 0x00); 0
}
В строке "USART2_SPI_Write9(LCD_C, 0x94);" я увеличил контрастность. Она варьируется от 0х80 до 0х9f, и дефолтным значением является 0х90. Но при выводе анимации контрастность снижается (дисплей немного моргает при перезаписи видеопамяти), поэтому я выставил значение 0х94.
Для отрисовки графики потребуется фрейм-буфер:
#define LCD_C 0x00
#define LCD_D 0x01
#define LCD_X 96
#define LCD_Y 68
#define LCD_LINE 9
#define LCD_LEN LCD_LINE * LCD_X
volatile uint8_t fb[LCD_LEN];
А для работы с фрейм-буфером понадобятся функции заливки и вывода буфера на дисплей:
void ste2007_fill(uint8_t value)
{
for(int i=0;i<LCD_LEN; i++)
fb[i]=value;
}
void ste2007_update_display() {
USART2_SPI_Write9(LCD_C, 0xB0);
USART2_SPI_Write9(LCD_C, 0x10); 03
USART2_SPI_Write9(LCD_C, 0x00); 04
USART2_SPI_Write9(LCD_C, 0x40); 0
for(uint32_t i=0;i<LCD_LEN; i++)
USART2_SPI_Write9(LCD_D,fb[i]);
}
Вывод буфера на дисплей занимает 5.58 мс. Т.е. при анимации 30fps, на вывод буфера будет уходить 167.5 мс или 16.75 % времени работы CPU. Технически, можно перекинуть дисплей на USART_1, но на мой взгляд, 16 и 8 - это числа одного порядка, и какого-то ощутимого профита получить не получится.
Для создания тестовой анимации, добавим в модуль "ste2007.c" функцию заполнения буфера вертикальными линиями:
void ste2007_vertical_lines(uint8_t line_count, uint8_t spacing, uint8_t value) {
if (line_count == 0) return; 0
0
if (spacing == 0) {
spacing = LCD_X / line_count;
}
ste2007_clear();
for (uint8_t col = 0; col < LCD_X; col += spacing) {
8
for (uint8_t page = 0; page < LCD_LINE; page++) {
uint16_t pos = page * LCD_X + col;
if (pos < LCD_LEN) {
fb[pos] = value;
}
}
}
}
Чтобы функция рисовала линии с равными интервалами, нужно указывать число линий, которое без остатка делится на 96. Например:
ste2007_vertical_lines(12,0,0xff); // 12 vertical solid lines
Для анимации потребуется функция горизонтального скроллинга. Дисплей Nokia 1202 - это бюджетный дисплей, там имеется аппаратная функция "вертикального" сколлинга (Row by row), что бы можно было мотать текст. Горизонтальный скроллинг приходится делать программно. :
void ste2007_horizont_scroll(int8_t direction) {
uint8_t temp;
for (int page = 0; page < LCD_LINE; page++) {
if (direction > 0) {
temp = fb[(page + 1) * LCD_X - 1];
for (int col = LCD_X - 1; col > 0; col--) {
fb[page * LCD_X + col] = fb[page * LCD_X + col - 1];
}
fb[page * LCD_X] = temp;
}
else {
temp = fb[page * LCD_X];
for (int col = 0; col < LCD_X - 1; col++) {
fb[page * LCD_X + col] = fb[page * LCD_X + col + 1];
}
fb[(page + 1) * LCD_X - 1] = temp;
}
}
Скролл может быть как вправо, так и влево.
Итак, программа будет такой. Заполняем буфер вертикальными линиями и включаем скроллинг. Что бы скроллинг был более-менее плавным, нужно функцию "ste2007_horizont_scroll()" запускать хотя бы 20 раз в секунду. Это потребует изменений в главном цикле, который отрабатывает 1 раз в секунду. Уменьшим задержку в главном цикле до 50 мс, а для светодиода введем специальный счетчик, который будет считать до 20, и переключать светодиод. Получилось как-то так:
bool tick_enabled=true;
const uint32_t DEFAULT_INTERVAL = WAIT_MS; 50
uint32_t interval = DEFAULT_INTERVAL;
uint32_t toggle_interval=1000/interval;
uint32_t toggle_counter=toggle_interval;
ste2007_init();
ste2007_vertical_lines(12,0,0xff); 12
ste2007_update_display();
for(uint32_t cycle_count = 0; ; cycle_count++) {
1.
int32_t remaining_delay = smart_delay(interval);
if (UART_RDY) {
tick_enabled = uart_command_processing(tick_enabled);
}
2.
interval = (remaining_delay > 0) ? (uint32_t)remaining_delay : DEFAULT_INTERVAL;
3.
if (remaining_delay <= 0 && --toggle_counter == 0) {
GPIOC->ODR ^= GPIO_Pin_13;
toggle_counter=toggle_interval;
if (tick_enabled) {
usart1_printf("Cycle: %u\r\n", cycle_count/toggle_interval);
}
}
ste2007_horizont_scroll(1);
ste2007_update_display();
}
В результате работы программы на дисплее будут бегущие вправо вертикальные линии.
Под спойлерами полный код "main.c" и "ste2007.c":
показать main.c
#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
#include "stm32f10x_usart.h"
#include "stdbool.h"
#include "main.h"
#include "utils.h"
#include "uart.h"
#include "math.h"
#include "ste2007.h"
extern bool UART_RDY;
uint32_t SystemCoreClock = 72000000;
int main() {
RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_USART1EN;
RCC->APB1ENR |= RCC_APB1ENR_USART2EN;
2
GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13);
GPIOC->CRH |= GPIO_CRH_MODE13_1; 2
GPIOA->CRH &= ~(GPIO_CRH_CNF9 | GPIO_CRH_MODE9);
GPIOA->CRH |= (GPIO_CRH_CNF9_1 | GPIO_CRH_MODE9); 50
GPIOA->CRH &= ~(GPIO_CRH_CNF10 | GPIO_CRH_MODE10);
GPIOA->CRH |= GPIO_CRH_CNF10_0;
GPIOB->CRL &= ~(GPIO_CRL_CNF4 | GPIO_CRL_MODE4);
GPIOB->CRL |= (GPIO_CRL_MODE4_0 | GPIO_CRL_MODE4_1); 1150
GPIOA->CRL &= ~(GPIO_CRL_CNF2 | GPIO_CRL_MODE2);
GPIOA->CRL |= (GPIO_CRL_CNF2_1 | GPIO_CRL_MODE2);
GPIOA->CRL &= ~(GPIO_CRL_CNF4 | GPIO_CRL_MODE4);
GPIOA->CRL |= (GPIO_CRL_CNF4_1 | GPIO_CRL_MODE4);
GPIOA->CRL &= ~(GPIO_CRL_CNF5 | GPIO_CRL_MODE5);
GPIOA->CRL |= (GPIO_CRL_MODE5_0 | GPIO_CRL_MODE5_1);
9
0x138115200
USART2->BRR = 0x10; 2.250x1016162.25
USART2->CR2 = USART_CR2_CLKEN |
USART_CR2_STOP_1 | 1
USART_CR2_LBCL; 9
USART2->CR1 = USART_CR1_M | 91
USART_CR1_TE |
USART_CR1_UE;
0x1d4c960072
USART1->BRR = 0x271; 115200
USART1->CR1 = USART_CR1_TE | USART_CR1_RE | USART_CR1_UE;
USART1->CR1 |= USART_CR1_RXNEIE;
NVIC_EnableIRQ(USART1_IRQn);
systick_init();
__enable_irq();
usart1_print_string("System ready...\r\n");
bool tick_enabled=true;
const uint32_t DEFAULT_INTERVAL = WAIT_MS; 50
uint32_t interval = DEFAULT_INTERVAL;
uint32_t toggle_interval=1000/interval;
uint32_t toggle_counter=toggle_interval;
ste2007_init();
ste2007_vertical_lines(12,0,0xff); 12
ste2007_update_display();
for(uint32_t cycle_count = 0; ; cycle_count++) {
1.
int32_t remaining_delay = smart_delay(interval);
if (UART_RDY) {
tick_enabled = uart_command_processing(tick_enabled);
}
2.
interval = (remaining_delay > 0) ? (uint32_t)remaining_delay : DEFAULT_INTERVAL;
3.
if (remaining_delay <= 0 && --toggle_counter == 0) {
GPIOC->ODR ^= GPIO_Pin_13;
toggle_counter=toggle_interval;
if (tick_enabled) {
usart1_printf("Cycle: %u\r\n", cycle_count/toggle_interval);
}
}
ste2007_horizont_scroll(1);
ste2007_update_display();
}
}
показать ste2007.c
#include "ste2007.h"
#include "utils.h"
#include "stm32f10x_usart.h"
#define LCD_C 0x00
#define LCD_D 0x01
#define LCD_X 96
#define LCD_Y 68
#define LCD_LINE 9
#define LCD_LEN LCD_LINE * LCD_X
volatile uint8_t fb[LCD_LEN];
void USART2_SPI_Write9(uint8_t dc,uint8_t data);
uint8_t reverse_bits_asm(uint32_t);
void ste2007_update_display();
void ste2007_init() {
GPIOB->BSRR = GPIO_BSRR_BR4;
delay_us(100);
GPIOB->BSRR = GPIO_BSRR_BS4;
delay_us(100);
USART2_SPI_Write9(LCD_C, 0xE2);
delay_us(100);
USART2_SPI_Write9(LCD_C, 0x2F);
delay_us(100);
USART2_SPI_Write9(LCD_C, 0xA0);
USART2_SPI_Write9(LCD_C, 0xC8);
USART2_SPI_Write9(LCD_C, 0x94); 0x90
USART2_SPI_Write9(LCD_C, 0xA4);
USART2_SPI_Write9(LCD_C, 0x40); 0
USART2_SPI_Write9(LCD_C, 0xA6);
USART2_SPI_Write9(LCD_C, 0xAF);
USART2_SPI_Write9(LCD_C, 0xB0); 0
USART2_SPI_Write9(LCD_C, 0x10); 0
USART2_SPI_Write9(LCD_C, 0x00); 0
}
void ste2007_fill(uint8_t value)
{
for(int i=0;i<LCD_LEN; i++)
fb[i]=value;
}
void ste2007_vertical_lines(uint8_t line_count, uint8_t spacing, uint8_t value) {
if (line_count == 0) return; 0
0
if (spacing == 0) {
spacing = LCD_X / line_count;
}
ste2007_clear();
for (uint8_t col = 0; col < LCD_X; col += spacing) {
8
for (uint8_t page = 0; page < LCD_LINE; page++) {
uint16_t pos = page * LCD_X + col;
if (pos < LCD_LEN) {
fb[pos] = value;
}
}
}
}
void ste2007_update_display() {
USART2_SPI_Write9(LCD_C, 0xB0);
USART2_SPI_Write9(LCD_C, 0x10); 03
USART2_SPI_Write9(LCD_C, 0x00); 04
USART2_SPI_Write9(LCD_C, 0x40); 0
for(uint32_t i=0;i<LCD_LEN; i++)
USART2_SPI_Write9(LCD_D,fb[i]);
}
void ste2007_horizont_scroll(int8_t direction) {
uint8_t temp;
for (int page = 0; page < LCD_LINE; page++) {
if (direction > 0) {
temp = fb[(page + 1) * LCD_X - 1];
for (int col = LCD_X - 1; col > 0; col--) {
fb[page * LCD_X + col] = fb[page * LCD_X + col - 1];
}
fb[page * LCD_X] = temp;
}
else {
temp = fb[page * LCD_X];
for (int col = 0; col < LCD_X - 1; col++) {
fb[page * LCD_X + col] = fb[page * LCD_X + col + 1];
}
fb[(page + 1) * LCD_X - 1] = temp;
}
}
}
void USART2_SPI_Write9(uint8_t dc,uint8_t data){
uint16_t packet=(uint16_t)reverse_bits_asm((uint32_t)data);
packet= (packet<<1) | (dc & 0x01);
GPIOA->BSRR = GPIO_BSRR_BR5;
USART2->DR = packet; 9
while(!(USART2->SR & USART_FLAG_TC));
GPIOA->BSRR = GPIO_BSRR_BS5;
}
uint8_t reverse_bits_asm(uint32_t data) {
uint32_t result;
__asm volatile (
"rbit %[result], %[data]\n"
"lsr %[result], %[result], #24\n"
: [result] "=r" (result)
: [data] "r" (data)
:
);
return (uint8_t)result;
}
12) USART + DMA в режиме SPI-master
Сейчас у нас имеется относительно быстрый процессор и относительно медленная линия передачи, которая вынуждает процессор постоянно находиться в простое. Большую часть времени во время передачи данных, процессор находится в функции "USART2_SPI_Write9()", в этой строке:
while(!(USART2->SR & USART_FLAG_TC));
Т.е. он ждет, пока медленная периферия передаст очередной пакет на линию. Чтобы освободить процессор для чего-то более полезного, можно воспользоваться DMA.
DMA - это механизм, который позволяет "в фоне", т.е. без участия CPU, копировать данные из одного места в другое, включая периферию. Для отправки фрейм-буфера через DMA, потребуется подготовить данные, предварительно выполнив операции по переворачиванию битов и дополнению 9-м D/C битом, после чего достаточно будет запустить передачу, и пойти выполнять какие-то другие задачи, в то время как "в фоне" будет происходить передача буфера через USART. Единственное, что перед следующей отправкой данных, следует проверять, завершилась ли предыдущая.
Т.к. 9-битные данные - это фактически 16-битные, для DMA потребуется второй 16-битный буфер:
volatile uint16_t fb_dma[LCD_LEN];
Перед передачей на линию, данные из первого буфера следует скопировать во второй, выполнив при этом все преобразования. Для проверки правильности преобразований, создадим новую функцию, которая будет выводить данные на дисплей со второго буфера:
void ste2007_update_dma_buffer() {
for (int i=0; i<LCD_LEN; i++) {
uint16_t packet=(uint16_t)reverse_bits_asm((uint32_t)fb[i]);
packet= (packet<<1) | (uint16_t)0x01;
fb_dma[i]=packet;
}
USART2_SPI_Write9(LCD_C, 0xB0);
USART2_SPI_Write9(LCD_C, 0x10); 03
USART2_SPI_Write9(LCD_C, 0x00); 04
USART2_SPI_Write9(LCD_C, 0x40); 0
GPIOA->BSRR = GPIO_BSRR_BR5;
while(!(USART2->SR & USART_SR_TC));
for (int i=0; i<LCD_LEN; i++) {
USART2->DR = fb_dma[i]; 9
while(!(USART2->SR & USART_FLAG_TC));
}
GPIOA->BSRR = GPIO_BSRR_BS5;
}
Эта функция заменяет "ste2007_update_display()". Она состоит из трех блоков: 1) копирования данных из первого буфера во второй, выполняя при этом преобразования, 2) затем устанавливается указатель видеопамяти дисплея на начало, 3) после чего передаются данные на дисплей со второго буфера.
Здесь немного был изменен порядок работы с CS-пином. Он теперь щелкает не на каждый передаваемый байт, а лишь зажимает линию в начале передачи, и отпускает ее в конце. Во время передачи по DMA мы не сможем "щелкать" CS-пином, т.к. передача будет происходить через USART, для которого CS-пин не предусмотрен. Поэтому не лишним было проверить, примет ли такие данные дисплей.
На логическом анализаторе начало передачи выглядит так:
Видно отрабатывание CS-сигнала четыре раза при передаче команд установки указателя видеопамяти, после чего сплошным пакетом идут данные.
Можно немного доработать алгоритм, если четыре команды установки указателя видеопамяти упаковать в буфер, который будет выводиться. Для этого потребуется увеличить размер буфера на четыре полуслова, и вместо отправки команд, вписать эти команды в начало буфера, не забыв применить преобразования. Тогда код будет следующим:
#define CMD_LEN 4
volatile uint16_t fb_dma[LCD_LEN+CMD_LEN];
void ste2007_update_dma_buffer() {
fb_dma[0]=(uint16_t)(reverse_bits_asm((uint32_t)0xB0)<<1);
fb_dma[1]=(uint16_t)(reverse_bits_asm((uint32_t)0x10)<<1); 03
fb_dma[2]=(uint16_t)(reverse_bits_asm((uint32_t)0x00)<<1); 04
fb_dma[3]=(uint16_t)(reverse_bits_asm((uint32_t)0x40)<<1); 0
for (int i=CMD_LEN; i<(LCD_LEN+CMD_LEN); i++) {
uint16_t packet=(uint16_t)reverse_bits_asm((uint32_t)fb[i-CMD_LEN]);
packet= (packet<<1) | (uint16_t)0x01;
fb_dma[i]=packet;
}
GPIOA->BSRR = GPIO_BSRR_BR5;
while(!(USART2->SR & USART_SR_TC));
for (int i=0; i<(LCD_LEN+CMD_LEN); i++) {
USART2->DR = fb_dma[i]; 9
while(!(USART2->SR & USART_FLAG_TC));
}
GPIOA->BSRR = GPIO_BSRR_BS5;
}
Что касается DMA, то в STM32F103C8 имеется два DMA контроллера, один на 7 каналов, второй на 5. Между этими каналами разбросана вся периферия микроконтроллера, и для USART2_TX нужен будет седьмой канал первого DMA контроллера:
Главный конфигурационный регистр DMA - это DMA_CCR:
Описание его флагов следующее:
Флаги регистра DMA_CCR (биты)
Бит(ы) Имя Описание
---------------------------------------------------------------------
0 EN Включить канал
- 0: Канал отключен
- 1: Канал включен
---------------------------------------------------------------------
1 TCIE Прерывание по завершения передачи
- 0: Прерывание отключено
- 1: Прерывание включено (срабатывает после завершения передачи)
---------------------------------------------------------------------
2 HTIE Прерывание по заверешении половины передачи
- 0: Прерывание отключено
- 1: Прерывание включено (срабатывает при половине передачи)
---------------------------------------------------------------------
3 TEIE Прерывание при ошибке передачи
- 0: Прерывание отключено
- 1: Прерывание включено (срабатывает при ошибке передачи)
---------------------------------------------------------------------
4 DIR Направление передачи данных
- 0: Чтение с периферийного устройства (Периферийное устройство → Память)
- 1: Чтение из памяти (Память → Периферийное устройство)
---------------------------------------------------------------------
5 CIRC Циклический (бесконечный) режим
- 0: Циклический режим отключен
- 1: Включен циклический режим (вызывает автоматическую перезагрузку счетчиков)
---------------------------------------------------------------------
6 PINC Инкремент (увеличение) периферийного адреса
- 0: Периферийный адрес фиксированный
- 1: Периферийный адрес увеличивается после передачи
---------------------------------------------------------------------
7 MINC Инкремент (увеличение) адреса памяти
- 0: Адрес памяти фиксированный
- 1: Адрес памяти увеличивается после передачи
---------------------------------------------------------------------
8:9 PSIZE Размер данных периферии
- 00: 8-бит
- 01: 16-бит
- 10: 32-бит
---------------------------------------------------------------------
10:11 MSIZE Размер данных памяти
- 00: 8-бит
- 01: 16-бит
- 10: 32-бит
---------------------------------------------------------------------
12:13 PL Уровень приоритета канала
- 00: Низкий
- 01: Средний
- 10: Высокий
- 11: Очень высокий
---------------------------------------------------------------------
14 MEM2MEM Режим «память-в-память»
- 0: Отключено (передача периферийных устройств ↔ памяти)
- 1: Включено (передача память ↔ память)
Чтобы сконфигурировать DMA на передачу буфера через DMA, следует в модуле "main.c" включить тактирование DMA1:
RCC->AHBENR |= RCC_AHBENR_DMA1EN;
И разрешить прерывание 7-го канала DMA1 в контролере прерываний:
NVIC_EnableIRQ(DMA1_Channel7_IRQn);
В модуле "ste2007.c" потребуется ввести еще одну глобальную переменную, которая будет устанавливаться в значение "true", в обработчике прерывания DMA после завершения передачи:
volatile bool dma_complete = true;
Добавляем обработчик прерывания:
void DMA1_Channel7_IRQHandler(void) {
if(DMA1->ISR & DMA_ISR_TCIF7) {
DMA1->IFCR |= DMA_IFCR_CTCIF7;
dma_complete = true;
GPIOA->BSRR = GPIO_BSRR_BS5;
USART2->CR3 &= ~USART_CR3_DMAT;
DMA1_Channel7->CCR &= ~DMA_CCR7_EN;
}
}
В функции "ste2007_update_dma_buffer()" будет конфигурирование DMA и запуск передачи:
int ste2007_update_dma_buffer() {
int timeout=0;
while (!dma_complete) {
if (++timeout > 10000)
return STATUS_ERROR;
}
fb_dma[0]=(uint16_t)(reverse_bits_asm((uint32_t)0xB0)<<1);
fb_dma[1]=(uint16_t)(reverse_bits_asm((uint32_t)0x10)<<1); 03
fb_dma[2]=(uint16_t)(reverse_bits_asm((uint32_t)0x00)<<1); 04
fb_dma[3]=(uint16_t)(reverse_bits_asm((uint32_t)0x40)<<1); 0
for (int i=CMD_LEN; i<(LCD_LEN+CMD_LEN); i++) {
uint16_t packet=(uint16_t)reverse_bits_asm((uint32_t)fb[i-CMD_LEN]);
packet= (packet<<1) | (uint16_t)0x01;
fb_dma[i]=packet;
}
7
DMA1_Channel7->CPAR = (uint32_t)&(USART2->DR);
DMA1_Channel7->CMAR = (uint32_t)fb_dma;
DMA1_Channel7->CNDTR = LCD_LEN+CMD_LEN;
DMA1_Channel7->CCR = DMA_CCR7_MINC |
DMA_CCR7_MSIZE_0 | 16
DMA_CCR7_PSIZE_0 | 16
DMA_CCR7_DIR |
DMA_CCR7_TCIE |
DMA_CCR7_PL;
DMA1->IFCR |= DMA_IFCR_CTCIF7;
dma_complete = false;
GPIOA->BSRR = GPIO_BSRR_BR5;
DMA1_Channel7->CCR |= DMA_CCR7_EN;
USART2->CR3 |= USART_CR3_DMAT;
Здесь через регистр DMA_CCR устанавливается: 16-битный размер данных буфера и регистра периферии (DMA_CCR7_MSIZE_0 и DMA_CCR7_PSIZE_0), однопроходной режим работы (CIRC=0), инкремент адреса буфера во время передачи (DMA_CCR7_MINC=1), вызов прерывания по завершении передачи (DMA_CCR7_TCIE=1).
После завершения конфигурации DMA и запуска передачи, функция завершает работу, т.е. она не ждет окончания передачи.
При использовании DMA время передачи буфера через USART2 сократилось с 5.5 мс до 4.5 мс за счет уменьшения интервалов между пакетами clock сигналов до 1.5 мкс (было 2.5 мкс).
Если ранее, на вывод анимации расходовалось 16.75 % времени работы CPU, то теперь эта операция осуществляется "в фоне", без участия CPU.
Полные версии "main.c" и "ste2007.c" под спойлерами:
показать main.c
#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
#include "stm32f10x_usart.h"
#include "stdbool.h"
#include "main.h"
#include "utils.h"
#include "uart.h"
#include "math.h"
#include "ste2007.h"
extern bool UART_RDY;
uint32_t SystemCoreClock = 72000000;
int main() {
RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_USART1EN;
RCC->APB1ENR |= RCC_APB1ENR_USART2EN;
RCC->AHBENR |= RCC_AHBENR_DMA1EN;
2
GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13);
GPIOC->CRH |= GPIO_CRH_MODE13_1; 2
GPIOA->CRH &= ~(GPIO_CRH_CNF9 | GPIO_CRH_MODE9);
GPIOA->CRH |= (GPIO_CRH_CNF9_1 | GPIO_CRH_MODE9); 50
GPIOA->CRH &= ~(GPIO_CRH_CNF10 | GPIO_CRH_MODE10);
GPIOA->CRH |= GPIO_CRH_CNF10_0;
GPIOB->CRL &= ~(GPIO_CRL_CNF4 | GPIO_CRL_MODE4);
GPIOB->CRL |= (GPIO_CRL_MODE4_0 | GPIO_CRL_MODE4_1); 1150
GPIOA->CRL &= ~(GPIO_CRL_CNF2 | GPIO_CRL_MODE2);
GPIOA->CRL |= (GPIO_CRL_CNF2_1 | GPIO_CRL_MODE2);
GPIOA->CRL &= ~(GPIO_CRL_CNF4 | GPIO_CRL_MODE4);
GPIOA->CRL |= (GPIO_CRL_CNF4_1 | GPIO_CRL_MODE4);
GPIOA->CRL &= ~(GPIO_CRL_CNF5 | GPIO_CRL_MODE5);
GPIOA->CRL |= (GPIO_CRL_MODE5_0 | GPIO_CRL_MODE5_1);
9
0x138115200
USART2->BRR = 0x10; 2.250x1016162.25
USART2->CR2 = USART_CR2_CLKEN |
USART_CR2_STOP_1 | 1
USART_CR2_LBCL; 9
USART2->CR1 = USART_CR1_M | 91
USART_CR1_TE |
USART_CR1_UE;
0x1d4c960072
USART1->BRR = 0x271; 115200
USART1->CR1 = USART_CR1_TE | USART_CR1_RE | USART_CR1_UE;
USART1->CR1 |= USART_CR1_RXNEIE;
NVIC_EnableIRQ(USART1_IRQn);
NVIC_EnableIRQ(DMA1_Channel7_IRQn);
systick_init();
__enable_irq();
usart1_print_string("System ready...\r\n");
bool tick_enabled=true;
const uint32_t DEFAULT_INTERVAL = WAIT_MS; 50
uint32_t interval = DEFAULT_INTERVAL;
uint32_t toggle_interval=1000/interval;
uint32_t toggle_counter=toggle_interval;
ste2007_init();
ste2007_vertical_lines(12,0,0xff); 12
ste2007_update_dma_buffer();
for(uint32_t cycle_count = 0; ; cycle_count++) {
1.
int32_t remaining_delay = smart_delay(interval);
if (UART_RDY) {
tick_enabled = uart_command_processing(tick_enabled);
}
2.
interval = (remaining_delay > 0) ? (uint32_t)remaining_delay : DEFAULT_INTERVAL;
3.
if (remaining_delay <= 0 && --toggle_counter == 0) {
GPIOC->ODR ^= GPIO_Pin_13;
toggle_counter=toggle_interval;
if (tick_enabled) {
usart1_printf("Cycle: %u\r\n", cycle_count/toggle_interval);
}
}
ste2007_horizont_scroll(1);
ste2007_update_dma_buffer();
}
}
показать ste2007.c
#include "ste2007.h"
#include "main.h"
#include "utils.h"
#include "stm32f10x_usart.h"
#include "stdbool.h"
#define LCD_C 0x00
#define LCD_D 0x01
volatile bool dma_complete = true;
volatile uint8_t fb[LCD_LEN];
volatile uint16_t fb_dma[LCD_LEN+CMD_LEN];
void USART2_SPI_Write9(uint8_t dc,uint8_t data);
uint8_t reverse_bits_asm(uint32_t);
void DMA1_Channel7_IRQHandler(void) {
if(DMA1->ISR & DMA_ISR_TCIF7) {
DMA1->IFCR |= DMA_IFCR_CTCIF7;
dma_complete = true;
GPIOA->BSRR = GPIO_BSRR_BS5;
USART2->CR3 &= ~USART_CR3_DMAT;
DMA1_Channel7->CCR &= ~DMA_CCR7_EN;
}
}
void ste2007_init() {
GPIOB->BSRR = GPIO_BSRR_BR4;
delay_us(100);
GPIOB->BSRR = GPIO_BSRR_BS4;
delay_us(100);
USART2_SPI_Write9(LCD_C, 0xE2);
delay_us(100);
USART2_SPI_Write9(LCD_C, 0x2F);
delay_us(100);
USART2_SPI_Write9(LCD_C, 0xA0);
USART2_SPI_Write9(LCD_C, 0xC8);
USART2_SPI_Write9(LCD_C, 0x94); 0x90
USART2_SPI_Write9(LCD_C, 0xA4);
USART2_SPI_Write9(LCD_C, 0x40); 0
USART2_SPI_Write9(LCD_C, 0xA6);
USART2_SPI_Write9(LCD_C, 0xAF);
USART2_SPI_Write9(LCD_C, 0xB0); 0
USART2_SPI_Write9(LCD_C, 0x10); 0
USART2_SPI_Write9(LCD_C, 0x00); 0
}
void ste2007_fill(uint8_t value)
{
for(int i=0;i<LCD_LEN; i++)
fb[i]=value;
}
void ste2007_vertical_lines(uint8_t line_count, uint8_t spacing, uint8_t value) {
if (line_count == 0) return; 0
0
if (spacing == 0) {
spacing = LCD_X / line_count;
}
ste2007_clear();
for (uint8_t col = 0; col < LCD_X; col += spacing) {
8
for (uint8_t page = 0; page < LCD_LINE; page++) {
uint16_t pos = page * LCD_X + col;
if (pos < LCD_LEN) {
fb[pos] = value;
}
}
}
}
void ste2007_update_display() {
USART2_SPI_Write9(LCD_C, 0xB0);
USART2_SPI_Write9(LCD_C, 0x10); 03
USART2_SPI_Write9(LCD_C, 0x00); 04
USART2_SPI_Write9(LCD_C, 0x40); 0
for(uint32_t i=0;i<LCD_LEN; i++)
USART2_SPI_Write9(LCD_D,fb[i]);
}
void ste2007_horizont_scroll(int8_t direction) {
uint8_t temp;
for (int page = 0; page < LCD_LINE; page++) {
if (direction > 0) {
temp = fb[(page + 1) * LCD_X - 1];
for (int col = LCD_X - 1; col > 0; col--) {
fb[page * LCD_X + col] = fb[page * LCD_X + col - 1];
}
fb[page * LCD_X] = temp;
}
else {
temp = fb[page * LCD_X];
for (int col = 0; col < LCD_X - 1; col++) {
fb[page * LCD_X + col] = fb[page * LCD_X + col + 1];
}
fb[(page + 1) * LCD_X - 1] = temp;
}
}
}
int ste2007_update_dma_buffer() {
int timeout=0;
while (!dma_complete) {
if (++timeout > 10000)
return STATUS_ERROR;
}
fb_dma[0]=(uint16_t)(reverse_bits_asm((uint32_t)0xB0)<<1);
fb_dma[1]=(uint16_t)(reverse_bits_asm((uint32_t)0x10)<<1); 03
fb_dma[2]=(uint16_t)(reverse_bits_asm((uint32_t)0x00)<<1); 04
fb_dma[3]=(uint16_t)(reverse_bits_asm((uint32_t)0x40)<<1); 0
for (int i=CMD_LEN; i<(LCD_LEN+CMD_LEN); i++) {
uint16_t packet=(uint16_t)reverse_bits_asm((uint32_t)fb[i-CMD_LEN]);
packet= (packet<<1) | (uint16_t)0x01;
fb_dma[i]=packet;
}
7
DMA1_Channel7->CPAR = (uint32_t)&(USART2->DR);
DMA1_Channel7->CMAR = (uint32_t)fb_dma;
DMA1_Channel7->CNDTR = LCD_LEN+CMD_LEN;
DMA1_Channel7->CCR = DMA_CCR7_MINC |
DMA_CCR7_MSIZE_0 | 16
DMA_CCR7_PSIZE_0 | 16
DMA_CCR7_DIR |
DMA_CCR7_TCIE |
DMA_CCR7_PL;
DMA1->IFCR |= DMA_IFCR_CTCIF7;
dma_complete = false;
GPIOA->BSRR = GPIO_BSRR_BR5;
DMA1_Channel7->CCR |= DMA_CCR7_EN;
USART2->CR3 |= USART_CR3_DMAT;
return STATUS_OK;
}
void USART2_SPI_Write9(uint8_t dc,uint8_t data){
uint16_t packet=(uint16_t)reverse_bits_asm((uint32_t)data);
packet= (packet<<1) | (dc & 0x01);
GPIOA->BSRR = GPIO_BSRR_BR5;
USART2->DR = packet; 9
while(!(USART2->SR & USART_FLAG_TC));
GPIOA->BSRR = GPIO_BSRR_BS5;
}
uint8_t reverse_bits_asm(uint32_t data) {
uint32_t result;
__asm volatile (
"rbit %[result], %[data]\n"
"lsr %[result], %[result], #24\n"
: [result] "=r" (result)
: [data] "r" (data)
:
);
return (uint8_t)result;
}
13) Битовая память в Cortex-M3
В Cortex-M3 имеется два региона с битовой памятью, один для ОЗУ, другой для периферийных регистров. Каждый байт такой памяти соответствует каком-либо биту ОЗУ или биту периферийных регистров. Битовая память для ОЗУ находится по адресам 0х22000000 - 0х23FFFFFC и охватывает один мегабайт ОЗУ. Битовая память для регистров периферийных устройств находится по адресам 0х42000000 - 0х43FFFFFC.
Если взять адрес ОЗУ =0х20000000, то нулевому биту будет соответствовать адрес 0х22000000, первому биту 0х22000001, второму биту 0х22000002, и т.д. Имеются нехитрые формулы для расчета смещения того или иного бита, они приведены в " Coretx-M3 Technical Reference Manual - глава 4.2"
В адреса битовой памяти можно записать или считать только два значения: ноль или единица.
Теперь давайте подумаем. Сейчас каждый пиксель дисплея упакован в какой-либо байт фрейм-буфера. И если бы была нужна функция рисования точки, то сперва нужно было бы рассчитать байт в котором упакован пиксель, потом считать этот байт, поменять нужный бит, и записать байт обратно в массив. Это так называемая процедура: Read - Modify - Write или RMW. Как-то так:
void ste2007_draw_pixel_rmw(uint8_t x, uint8_t y, bool color) {
uint8_t page = y / 8;
uint8_t bit = y % 8;
uint16_t idx = page * LCD_X + x;
if (color) fb[idx] |= (1 << bit);
else fb[idx] &= ~(1 << bit);
}
В случае же использования битовой памяти, нужно лишь рассчитать смещение для адреса нужного бита, и просто записать в него новое значение:
#define BITBAND_SRAM_REF 0x20000000
#define BITBAND_SRAM_BASE 0x22000000
#define BITBAND_ADDR(addr, bit) (BITBAND_SRAM_BASE + (((uint32_t)(addr) - BITBAND_SRAM_REF) * 32) + (bit) * 4)
10
void ste2007_draw_pixel(uint8_t x, uint8_t y, bool color) {
1.1202
if (x >= LCD_X || y >= LCD_Y) return;
2.
uint8_t page = y / 8; 8
uint16_t fb_index = page * LCD_X + x;
3.
volatile uint32_t *bitband_addr = (volatile uint32_t*)BITBAND_ADDR(&fb[fb_index], y % 8);
4.
*bitband_addr = color ? 1 : 0;
}
Это у нас функция рисования точки. Еще нам понадобится нециклическая функция горизонтального скроллинга, т.е. когда изображение "уплывает" за край, оно не появлется вновь с другой стороны.
void ste2007_horizontal_scroll_non_cyclic(int8_t direction) {
for (int page = 0; page < LCD_LINE; page++) {
if (direction > 0) {
for (int col = LCD_X - 1; col > 0; col--) {
fb[page * LCD_X + col] = fb[page * LCD_X + col - 1];
}
fb[page * LCD_X] = 0;
}
else {
for (int col = 0; col < LCD_X - 1; col++) {
fb[page * LCD_X + col] = fb[page * LCD_X + col + 1];
}
fb[(page + 1) * LCD_X - 1] = 0;
}
}
}
Эта функция упрощенная версия предыдущей циклической версии "ste2007_horizont_scroll()".
В главном цикле модуля "main.c" будем рисовать функцию синуса:
bool tick_enabled=true;
const uint32_t DEFAULT_INTERVAL = WAIT_MS; 50
uint32_t interval = DEFAULT_INTERVAL;
uint32_t toggle_interval=1000/interval;
uint32_t toggle_counter=toggle_interval;
ste2007_init();
ste2007_fill(0x0);
1200xff12
ste2007_update_dma_buffer();
for(float cycle_count = 0.f; ; cycle_count += 5.f) {
1.
int32_t remaining_delay = smart_delay(interval);
if (UART_RDY) {
tick_enabled = uart_command_processing(tick_enabled);
}
2.
interval = (remaining_delay > 0) ? (uint32_t)remaining_delay : DEFAULT_INTERVAL;
3.
if (remaining_delay <= 0 && --toggle_counter == 0) {
GPIOC->ODR ^= GPIO_Pin_13;
toggle_counter=toggle_interval;
if (tick_enabled) {
usart1_printf("Cycle: %u\r\n", (int)cycle_count/toggle_interval);
}
}
float sin=sinf((cycle_count * M_PI)/180.f);
sin = sin *20.f +32;
ste2007_horizontal_scroll_non_cyclic(1);
ste2007_draw_pixel(0,(uint32_t)sin,1);
ste2007_update_dma_buffer();
}
Здесь при каждой новой итерации суперцикла вычисляем функцию синуса, масштабируем значение относительно размера дисплея, и заносим значение в первый столбец фрейм-буфера. В результате получаем график бегущей синусоиды:
В принципе, сейчас все готово, чтобы в режиме реального времени отображать данные с АЦП, но делать это будем на другом дисплее.
14) Semihosting
Я предлагаю на время забыть про какие-либо дисплеи, и откатиться на пункт: "Числа с плавающей запятой, подключение стандартной библиотеки Си к проекту", когда к проекту был подключен лишь ST-Link и USB-UART преобразователь.
На мой взгляд цеплять USB-UART преобразователь к BluePill не очень удобно. С помощью semihosting можно выводить отладочные сообщения прямо через ST-Link.
Semihosting - это отладочная технология ARM, через нее можно использовать функции ввода-вывода, загружать файлы и пр. В документации ARM приведен список операций осуществляемых с помощью semihosting:
* angel_SWIreason_EnterSVC (0x17)
* angel_SWIreason_ReportException (0x18)
* SYS_CLOSE (0x02)
* SYS_CLOCK (0x10)
* SYS_ELAPSED (0x30)
* SYS_ERRNO (0x13)
* SYS_FLEN (0x0C)
* SYS_GET_CMDLINE (0x15)
* SYS_HEAPINFO (0x16)
* SYS_ISERROR (0x08)
* SYS_ISTTY (0x09)
* SYS_OPEN (0x01)
* SYS_READ (0x06)
* SYS_READC (0x07)
* SYS_REMOVE (0x0E)
* SYS_SEEK (0x0A)
* SYS_SYSTEM (0x12)
* SYS_TICKFREQ (0x31)
* SYS_TIME (0x11)
* SYS_TMPNAM (0x0D)
* SYS_WRITE (0x05)
* SYS_WRITEC (0x03)
* SYS_WRITE0 (0x04)
Еще из плюсов semihosting'а - USART микроконтроллера не будет занят под отладку, он будет свободен для чего-то более полезного.
Минусы использования semihosting: некоторая медлительность и несколько увеличенный размер прошивки. Чтобы нивелировать последнее обстоятельство, потребуется доработать сборочные файлы: Makefile и CMakeLists.txt добавив профили сборки Debug и Release, чтобы в релизную прошивку не тащить отладочный код.
Чтобы использовать semihosting, нужно вызвать функцию "initialise_monitor_handles()". В "main.c" пишем следующее:
#ifdef DEBUG
extern void initialise_monitor_handles(void);
void enable_semihosting() {
if (CoreDebug->DHCSR & CoreDebug_DHCSR_C_DEBUGEN_Msk) {
initialise_monitor_handles();
printf("Debugger connected, semihosting ready\n");
}
}
#endif
int main() {
#ifdef DEBUG
enable_semihosting();
#endif
Здесь проверка:
if (CoreDebug->DHCSR & CoreDebug_DHCSR_C_DEBUGEN_Msk)
нужна для того, чтобы прошивка была работоспособной и без подключенного отладчика. Далее, после переключения светодиода в главном цикле, добавляем отладочный вывод:
#ifdef DEBUG
if (CoreDebug->DHCSR & CoreDebug_DHCSR_C_DEBUGEN_Msk) {
printf("semihosting tick - %u\r\n",(unsigned int)cnt);
}
#endif
Здесь приведение типов "(unsigned int)cnt" нужно из-за того, что мой компилятор и линтер (cppcheck) не могли решить какого типа переменная "cnt". Компилятор утверждал, что "long unsigned int", а линтер был несогласен. Приведение типов устроило всех.
Полный текст "main.c" приведен ниже под спойлером.
показать main.c
#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
#include "stm32f10x_usart.h"
#include "stdbool.h"
#include "main.h"
#include "utils.h"
#include "uart.h"
#include <stdio.h>
extern volatile bool UART_RDY;
uint32_t SystemCoreClock = 72000000;
#ifdef DEBUG
extern void initialise_monitor_handles(void);
void enable_semihosting() {
if (CoreDebug->DHCSR & CoreDebug_DHCSR_C_DEBUGEN_Msk) {
initialise_monitor_handles();
printf("Debugger connected, semihosting ready\n");
}
}
#endif
int main() {
#ifdef DEBUG
enable_semihosting();
#endif
RCC->APB2ENR |= RCC_APB2Periph_GPIOC;
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_USART1EN;
2
GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13);
GPIOC->CRH |= GPIO_CRH_MODE13_1; 2
GPIOA->CRH &= ~(GPIO_CRH_CNF9 | GPIO_CRH_MODE9);
GPIOA->CRH |= (GPIO_CRH_CNF9_1 | GPIO_CRH_MODE9); 50
GPIOA->CRH &= ~(GPIO_CRH_CNF10 | GPIO_CRH_MODE10);
GPIOA->CRH |= GPIO_CRH_CNF10_0;
0x1d4c960072
USART1->BRR = 0x271; 115200
USART1->CR1 = USART_CR1_TE | USART_CR1_RE | USART_CR1_UE;
USART1->CR1 |= USART_CR1_RXNEIE;
NVIC_EnableIRQ(USART1_IRQn);
systick_init();
__enable_irq();
bool tick=true;
usart1_print_string("ready...\r\n");
uint32_t interval=WAIT;
for(uint32_t cnt=0; ; cnt++) {
int remainder=smart_delay(interval);
if (UART_RDY) {
tick=uart_command_processing(tick);
}
if ( remainder > 0 ) {
interval = remainder;
continue;
} else {
interval =WAIT;
}
GPIOC->ODR ^= GPIO_Pin_13;
#ifdef DEBUG
if (CoreDebug->DHCSR & CoreDebug_DHCSR_C_DEBUGEN_Msk) {
printf("semihosting tick - %u\r\n",(unsigned int)cnt);
}
#endif
if (tick) {
usart1_printf("tick - %u\r\n",cnt);
}
}
}
При линковке проекта, нужно опцию "--specs=nosys.specs" заменить на "--specs=rdimon.specs -lc -lrdimon".
Готовый Makefile вышел таким:
# Toolchain setup
CC = arm-none-eabi-gcc
AS = arm-none-eabi-gcc
OBJCOPY = arm-none-eabi-objcopy
SIZE = arm-none-eabi-size
RM = rm -f
# Target and output names
TARGET = firmware
BIN = $(TARGET).bin
HEX = $(TARGET).hex
ELF = $(TARGET).elf
# Directory structure
ifeq ($(DEBUG),1)
BUILD_DIR = debug_build
else
BUILD_DIR = build
endif
SRC_DIR = src
ASM_DIR = asm
INC_DIR = inc
PROJECT_ROOT = .
# Source files
SRCS_C = $(wildcard $(SRC_DIR)/*.c) $(wildcard $(PROJECT_ROOT)/main.c)
SRCS_S = $(wildcard $(ASM_DIR)/*.s)
OBJS = $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.o,$(filter $(SRC_DIR)/%,$(SRCS_C))) \
$(patsubst $(PROJECT_ROOT)/%.c,$(BUILD_DIR)/%.o,$(filter $(PROJECT_ROOT)/%,$(SRCS_C))) \
$(patsubst $(ASM_DIR)/%.s,$(BUILD_DIR)/%.o,$(SRCS_S))
# Include paths
INC = -I$(INC_DIR) -I$(PROJECT_ROOT) -ICMSIS/device -ICMSIS/core -ISPL/inc
# Defines
DEF = -DSTM32F10X_MD
DEF += -DSYSCLK_FREQ_72MHz
ifeq ($(DEBUG),1)
DEF += -DDEBUG
endif
# MCU and flags
MCU = cortex-m3
CFLAGS = -mcpu=$(MCU) -mthumb -Wall -ggdb -Og -ffunction-sections -fdata-sections $(INC) $(DEF)
ASFLAGS = -mcpu=$(MCU) -mthumb -ggdb
ifeq ($(DEBUG),1)
LDFLAGS = -T stm32f10xx8.ld -Wl,--gc-sections --specs=nano.specs --specs=rdimon.specs -lc -lrdimon
else
LDFLAGS = -T stm32f10xx8.ld -Wl,--gc-sections --specs=nosys.specs --specs=nano.specs
endif
# Build rules
all: $(BUILD_DIR)/$(BIN) $(BUILD_DIR)/$(HEX) size
$(BUILD_DIR)/$(ELF): $(OBJS)
@mkdir -p $(@D)
$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)
# Rule for C files in src directory
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c
@mkdir -p $(@D)
$(CC) -c $(CFLAGS) $< -o $@
# Rule for main.c in project root
$(BUILD_DIR)/%.o: $(PROJECT_ROOT)/%.c
@mkdir -p $(@D)
$(CC) -c $(CFLAGS) $< -o $@
# Rule for assembly files
$(BUILD_DIR)/%.o: $(ASM_DIR)/%.s
@mkdir -p $(@D)
$(AS) -c $(ASFLAGS) $< -o $@
$(BUILD_DIR)/$(BIN): $(BUILD_DIR)/$(ELF)
$(OBJCOPY) -O binary $< $@
$(BUILD_DIR)/$(HEX): $(BUILD_DIR)/$(ELF)
$(OBJCOPY) -O ihex $< $@
debug: $(BUILD_DIR)/$(ELF)
arm-none-eabi-gdb -ex "target extended-remote :3333" \
-ex "monitor arm semihosting enable" \
-ex "load" \
-ex "continue" \
$<
size: $(BUILD_DIR)/$(ELF)
$(SIZE) --format=berkeley $<
flash: $(BUILD_DIR)/$(BIN)
st-flash write $< 0x08000000
clean:
$(RM) -r $(BUILD_DIR)
.PHONY: all size flash clean
Работает это так. Обычная сборка и прошивка работает также как обычно:
$ make clean && make
$ make flash
Размер релизной прошивки:
$ make size
arm-none-eabi-size --format=berkeley build/firmware.elf
text data bss dec hex filename
2200 12 300 2512 9d0 build/firmware.elf
Сборка отладочной версии:
$ make DEBUG=1 clean && make DEBUG=1 all
rm -f -r debug_build
arm-none-eabi-gcc -c -mcpu=cortex-m3 -mthumb -Wall -ggdb -Og -ffunction-sections -fdata-sections -Iinc -I. -ICMSIS/device -ICMSIS/core -ISPL/inc -DSTM32F10X_MD -DSYSCLK_FREQ_72MHz -DDEBUG src/system_init.c -o debug_build/system_init.o
arm-none-eabi-gcc -c -mcpu=cortex-m3 -mthumb -Wall -ggdb -Og -ffunction-sections -fdata-sections -Iinc -I. -ICMSIS/device -ICMSIS/core -ISPL/inc -DSTM32F10X_MD -DSYSCLK_FREQ_72MHz -DDEBUG src/utils.c -o debug_build/utils.o
arm-none-eabi-gcc -c -mcpu=cortex-m3 -mthumb -Wall -ggdb -Og -ffunction-sections -fdata-sections -Iinc -I. -ICMSIS/device -ICMSIS/core -ISPL/inc -DSTM32F10X_MD -DSYSCLK_FREQ_72MHz -DDEBUG src/uart.c -o debug_build/uart.o
arm-none-eabi-gcc -c -mcpu=cortex-m3 -mthumb -Wall -ggdb -Og -ffunction-sections -fdata-sections -Iinc -I. -ICMSIS/device -ICMSIS/core -ISPL/inc -DSTM32F10X_MD -DSYSCLK_FREQ_72MHz -DDEBUG main.c -o debug_build/main.o
arm-none-eabi-gcc -c -mcpu=cortex-m3 -mthumb -ggdb asm/startup.s -o debug_build/startup.o
arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -Wall -ggdb -Og -ffunction-sections -fdata-sections -Iinc -I. -ICMSIS/device -ICMSIS/core -ISPL/inc -DSTM32F10X_MD -DSYSCLK_FREQ_72MHz -DDEBUG debug_build/system_init.o debug_build/uart.o debug_build/utils.o debug_build/main.o debug_build/startup.o -o debug_build/firmware.elf -T stm32f10xx8.ld -Wl,--gc-sections --specs=nano.specs --specs=rdimon.specs -lc -lrdimon
arm-none-eabi-objcopy -O binary debug_build/firmware.elf debug_build/firmware.bin
arm-none-eabi-objcopy -O ihex debug_build/firmware.elf debug_build/firmware.hex
arm-none-eabi-size --format=berkeley debug_build/firmware.elf
text data bss dec hex filename
8068 124 492 8684 21ec debug_build/firmware.elf
Разница 6 КБ, на мой взгляд не так много. Без использования newlib, размер отладочной прошивки достигал 35 КБ, это половина флеш-памяти микроконтроллера.
Далее подключаем микроконтроллер через ST-Link. Предположим, что используем мультиплексор screen. Тогда в одном окне запускаем openOCD командой:
$ openocd -f interface/stlink-v2.cfg -f target/stm32f1x.cfg
В другом окне запускаем отладчик:
$ arm-none-eabi-gdb ./debug_build/firmware.elf
Подключаемся к OpenOCD:
(gdb) target extended-remote :3333
Remote debugging using :3333
0x08001aa6 in _swistat ()
(gdb)
Останавливаем выполнение текущей программы:
(gdb) monitor reset halt
[stm32f1x.cpu] halted due to debug-request, current mode: Thread
xPSR: 0x01000000 pc: 0x08000780 msp: 0x20005000
(gdb)
Включаем semihosting:
(gdb) monitor arm semihosting enable
semihosting is enabled
(gdb)
Загружаем прошивку:
(gdb) load
Loading section .isr_vector, size 0x130 lma 0x8000000
Loading section .text, size 0x1cb4 lma 0x8000130
Loading section .rodata, size 0x1a0 lma 0x8001de4
Loading section .init_array, size 0x4 lma 0x8001f84
Loading section .fini_array, size 0x4 lma 0x8001f88
Loading section .data, size 0x74 lma 0x8001f8c
Start address 0x08000780, load size 8192
Transfer rate: 13 KB/sec, 1365 bytes/write.
(gdb)
Запускаем программу:
(gdb) continue
Continuing.
И в окне где запущен OpenOCD увидим вывод отладочных сообщений:
И если посмотреть на время отправки сообщений через UART, то видно, что semihosting подтормаживает где-то на 100 мс.
Ввод всех этих команд можно автоматизировать, достаточно будет набрать команду:
$ make DEBUG=1 debug
И все запустится автоматом. OpenOCD только надо не забыть запустить.
CMakeLists.txt тоже получился объемным, спрятал под спойлер.
показать CMakeLists.txt
cmake_minimum_required(VERSION 3.12)
project(firmware LANGUAGES C ASM)
set(CMAKE_VERBOSE_MAKEFILE OFF)
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR arm)
set(CMAKE_C_COMPILER arm-none-eabi-gcc)
set(CMAKE_ASM_COMPILER arm-none-eabi-gcc)
set(CMAKE_OBJCOPY arm-none-eabi-objcopy)
set(CMAKE_SIZE arm-none-eabi-size)
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose build type: Debug or Release" FORCE)
endif()
set(TARGET firmware)
set(ELF ${TARGET}.elf)
set(BIN ${TARGET}.bin)
set(HEX ${TARGET}.hex)
set(BUILD_DIR ${CMAKE_BINARY_DIR})
set(SRC_DIR src)
set(ASM_DIR asm)
set(INC_DIR inc)
file(GLOB SRCS_C
${SRC_DIR}/*.c
${CMAKE_SOURCE_DIR}/main.c
)
file(GLOB SRCS_S ${ASM_DIR}/*.s)
set(INC
${INC_DIR}
${CMAKE_SOURCE_DIR}
CMSIS/device
CMSIS/core
SPL/inc
)
add_definitions(-DSTM32F10X_MD -DSYSCLK_FREQ_72MHz)
set(MCU cortex-m3)
set(CWARN "-Wall")
set(CMAKE_C_FLAGS_DEBUG "-mcpu=${MCU} -mthumb ${CWARN} -g -Og -ffunction-sections -fdata-sections")
set(CMAKE_C_FLAGS_RELEASE "-mcpu=${MCU} -mthumb ${CWARN} -g -Os -ffunction-sections -fdata-sections")
set(CMAKE_ASM_FLAGS "-mcpu=${MCU} -mthumb -g")
set(LINKER_SCRIPT "${CMAKE_SOURCE_DIR}/stm32f10xx8.ld")
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
add_definitions(-DDEBUG)
set(CMAKE_EXE_LINKER_FLAGS "-specs=nano.specs -specs=rdimon.specs -lc -lrdimon -T${LINKER_SCRIPT} -Wl,--gc-sections")
else()
set(CMAKE_EXE_LINKER_FLAGS "-specs=nosys.specs -specs=nano.specs -T${LINKER_SCRIPT} -Wl,--gc-sections")
endif()
add_executable(${ELF} ${SRCS_C} ${SRCS_S})
target_include_directories(${ELF} PRIVATE ${INC})
add_custom_command(TARGET ${ELF} POST_BUILD
COMMAND ${CMAKE_OBJCOPY} -O binary ${ELF} ${BIN}
COMMAND ${CMAKE_OBJCOPY} -O ihex ${ELF} ${HEX}
COMMENT "Generating ${BIN} and ${HEX}"
)
add_custom_target(size
COMMAND ${CMAKE_SIZE} --format=berkeley ${ELF}
DEPENDS ${ELF}
)
add_custom_target(flash
COMMAND st-flash write ${BIN} 0x08000000
DEPENDS ${BIN}
)
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
add_custom_target(debug
COMMAND arm-none-eabi-gdb
-ex "target extended-remote :3333"
-ex "monitor arm semihosting enable"
-ex "load"
-ex "continue"
${ELF}
DEPENDS ${ELF}
COMMENT "Starting debug session with semihosting"
)
endif()
install(FILES ${BIN} DESTINATION bin)
Использование. Создаем каталог для сборки проекта и переходим в него:
$ mkdir cbuild && cd $_
Сборка релизной прошивки:
$ cmake -DCMAKE_BUILD_TYPE=Release ..
$ make all && make size
Прошивка:
$ make flash
Сборка отладочной прошивки:
$ cmake -DCMAKE_BUILD_TYPE=Debug ..
$ make all && make size
Запуск отладочной прошивки аналогичен использованию Makefile. В одном окне запускаем OpenOCD:
$ openocd -f interface/stlink-v2.cfg -f target/stm32f1x.cfg
В другом окне - отладчик с прошивкой:
$ make debug
Qbs-скрипт приводить не буду, он будет на git, там минимальные изменения, т.к. скрипт уже содержит раздельные профили компиляции, а команды OpenOCD следует добавлять в профиль устройства BareMetal.
В QtCreator отладочные сообщения попадают в окно "Application Output":
15) Пишем простой планировщик задач (RTOS)
На самом деле глупо меряться тем, насколько быстро выполняется операция деления, если все остальное время микроконтроллер пребывает в режиме ожидания. В таком случае и программная реализация вполне сойдет, спешить то некуда. По нормальному распределить и оценить нагрузку на CPU поможет планировщик задач или как его еще называют - RTOS.
Зачем оно надо? Планировщик поможет распределить задачи согласно приоритетам. Он сделает это автоматически, от вас не потребуется писать кода для переключения между задачами, а это экономия флеш-памяти. Кроме того планировщик может управлять задачами в процессе их выполнения, например завершение одной задачи может привести в возникновению другой, и т.д.
Если раньше мне надо было написать неблокирующее выполнение какой-либо задачи/функции, то я использовал алгоритм из примеров Arduino - "Blink without Delay":
void loop() {
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= interval) {
previousMillis = currentMillis;
if (ledState == LOW) {
ledState = HIGH;
} else {
ledState = LOW;
}
digitalWrite(ledPin, ledState);
}
Для одной или двух задач такой способ сносно работает, но когда у вас пять задач и больше, то ваш код превращается в туалетную бумагу (показывать не буду, кто плавал - знает). RTOS избавляет вас от необходимости писать такой код.
Как это работает? Представим, что в главном цикле "крутится" функция delay(1000). Delay() выполняется за одну секунду. Эта секунда - это "колбаса", ресурс т.е. "Колбаса" делится на тысячу маленьких кусочков (миллисекунды), которые нужно распределить между котиками (задачами). Если кому-то своего кусочка не хватает - это определённо плохо. Но еще хуже, если остаются несъеденные кусочки и их выбрасывают. Получается, что мы не используем ресурс микроконтроллера полностью. Распределение кусочков между котиками - это задача планировщика. Т.к. RTOS кооперативная, то мы не сможем помешать какому-то слишком толстому котику съесть кусочки других котиков. Планировщик в данном случае должен выдать остальным котикам оставшиеся свободными кусочки. Это всё, что нужно знать о планировщике RTOS.
Для STM32 уже существует достаточно мощная freeRTOS, но мне показалось, что часто бывает нужен простой планировщик. В качестве примера приведу свой планировщик, который я писал для STM8. Он мало что умеет, он "сырой" и тестировался всего на паре светодиодов. Но свою работу он делает: переключает задачи, показывает статистику, занимает мало места на флеше и не использует динамическую память.
В основе планировщика лежит следующая структура:
typedef struct TASK {
uint32_t loop;
uint32_t period;
uint32_t counter;
void (*handler)(void);
} TASK;
Здесь указатель на callback-функцию - это сама задача. Далее period - это интервал между выполнением задачи. Переменная counter - это убывающий счетчик, который показывает сколько еще осталось до выполнения задачи. loop - это флаг который указывает, будет ли следующее выполнение задачи последним или нет. Если следующее выполнение задачи последнее, то после её выполнения задача удаляется из таблицы планировщика.
Таблица планировщика это простой массив:
TASK task[TSK];
Для инициализации таблицы используется функция clear_task():
void clear_task(void) {
for(char i=0;i<TSK;i++) {
task[i].loop=ENABLE;
task[i].period=0;
task[i].counter=0;
task[i].handler=NULL;
}
load_cpu=0;
current_load=0;
add_task(task_stat,TOP,LOOP,1000);
}
Сам планировщик реализован в обработчике прерывания таймера SysTick:
void SysTick_Handler(void) {
for(char i=0; i<TSK; i++)
{
if (task[i].handler == NULL) break;
if (!task[i].counter)
{
task[i].counter=task[i].period;
task[i].handler();
if (!task[i].loop)
remove_task(i);
break;
} else
task[i].counter--;
}
current_load +=(72000-SysTick->VAL);
}
Как можно видеть, он совсем небольшой. Обход таблицы начинается с нулевого значения, и следовательно задачи с меньшим номером имеют больший приоритет. Второй оператор break выделенный красным имеет принципиальное значение в работе планировщика. Когда планировщик "натыкается" на задачу которую следует запустить, он передаёт управление этой задаче, после чего завершает работу игнорируя обработку статусов других задач. На двух светодиодах запущенных с равным интервалом это очень четко видно. Сначала они мигают синхронно, а затем, начинается разсинхронизация. Каждый раз, когда планировщик запускает первый светодиод, время обработки второго сдвигается на один шаг. Впоследствии сдвиг накапливается и имеет место разсинхронизация. Если оператор break убрать, то светодиоды будут мигать всегда синхронно. Конечно, вместо тупого break можно использовать более хитрый алгоритм, но... нужно ли?
Планировщик имеет еще пару служебных функций и одну задачу. Функция void remove_task(uint8_t num) удаляет задачу из таблицы если ее флаг LOOP оказался равным нулю:
void remove_task(uint8_t num) {
char i;
for(i=num; i<(TSK-2);i++) {
task[i]=task[i+1];
}
task[TSK-1].counter=0;
task[TSK-1].period=0;
task[TSK-1].loop=NOLOOP;
task[TSK-1].handler=NULL;
}
Функция add_task(), напротив, добавляет задачу в таблицу:
void add_task( void (*callback)(void),
Task_Priority_TypeDef rank,
Task_Loop_TypeDef loop,
uint32_t period_ms)
{
char i=TSK-1;
do {
if (task[i-1].handler != NULL) {
task[i]=task[i-1];
}
i--;
} while (i);
task[i].loop=loop;
task[i].period=period_ms;
task[i].handler=callback;
task[i].counter=period_ms;
}
В параметре функции можно задать приоритет задачи. Положить ее "на дно" или "под крышку" (BOTTOM/TOP).
Одна служебная задача void task_stat() подсчитывает статистику загруженности CPU. Работает это так. Допустим, что таймер SysTick настроен на интервал в 1 мс. Cчетчик таймера убывающий, и когда он доходит до нуля, то: a) во-первых вызывается прерывание, б) во-вторых в счетчик загружается значение инициализации, в нашем случае 72000. В то время пока работает обработчик прерывания, счетчик продолжает себе тихонько тикать. CPU для этого не используется. Когда обработчик прерывания заканчивает работу, он смотрит сколько натикал в этот раз таймер и суммирует это с общим значением current_load:
current_load +=(72000-SysTick->VAL);
И так тысячу раз. Служебная задача: add_task(task_stat,TOP,LOOP,1000) добавляемая при инициализации таблицы, вызывается один раз в секунду. Она обновляет статистическое значение переменной load_cpu и обнуляет счетчик current_load:
void task_stat() {
load_cpu=current_load;
current_load=0;
}
Полный код планировщика приведен под спойлерами:
показать полный код task.h
#ifndef __TASK_H__
#define __TASK_H__
#include <stddef.h>
#include "stm32f10x.h"
#define TSK 5
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.c
#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()
{
RCC->APB2ENR |= RCC_APB2Periph_GPIOC;
RCC->APB2ENR |= RCC_APB2Periph_GPIOA;
RCC->APB2ENR |= RCC_APB2Periph_USART1;
GPIOC->CRH &= ~(uint32_t)(0xf<<20);
GPIOC->CRH |= (uint32_t)(0x2<<20);
GPIOA->CRH &= ~(uint32_t)(0xf<<4);
GPIOA->CRH |= (uint32_t)(0xa<<4);
0x1d4c9600
USART1->BRR = 0x271; 115200
USART1->CR1 |= USART_CR1_UE_Set | USART_Mode_Tx;
clear_task();
add_task(toggle_led,TOP, LOOP, 1000);
if (SysTick_Config(72000))
{
while(1);
}
__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.
16 Драйвер 4-x разрядного семисегментного индикатора (программный SPI)
Для тестирования планировщика возьмем для примера драйвер 4-x разрядного семисегментного индикатора. Он использует динамическую индикацию, что требует довольно высокой скорости обновления: от 5 мс и меньше. Посмотрим как планировщик будет с этим справляться.
Для начала будем использовать программную реализацию SPI протокола. За образец возьмём код драйвера для STM8 из статьи STM8S + SDCC: Программирование на связке языков Си и ассемблера. Немного усложним задачу и будем использовать для вывода все 4 сегмента, вместо трех, как было в оригинале.
Итак, добавляем в проект заголовочный файл драйвера: led.h
#ifndef __LED_H__
#define __LED_H__
#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#define SCLK GPIO_Pin_5
#define RCLK GPIO_Pin_4
#define DIO GPIO_Pin_7
uint8_t reg;
__IO uint32_t led;
void show_led();
#endif
Файл с исходным код драйвера led.c
#include "led.h"
const char digit[10] = {
0b11000000, 0
0b11111001, 1
0b10100100, 2
0b10110000, 3
0b10011001, 4
0b10010010, 5
0b10000010, 6
0b11111000, 7
0b10000000, 8
0b10010000, 9
};
void spi_transmit(uint8_t data) {
for (char i=0; i<8; i++)
{
if (data & 0x80)
GPIOA->BSRR=DIO;
else
GPIOA->BRR=DIO;
data=(data<<1);
GPIOA->BSRR = SCLK;
GPIOA->BRR = SCLK;
}
}
void to_led(uint8_t value, uint8_t reg) {
GPIOA->BRR = RCLK;
spi_transmit(digit[value]);
spi_transmit(reg);
GPIOA->BSRR = RCLK;
}
void show_led() {
switch (reg) {
case 0:
to_led((uint8_t)(led%10),1);
break;
case 1:
if (led>=10)
to_led((uint8_t)((led%100)/10),2);
break;
case 2:
if (led>=100)
to_led((uint8_t)((led%1000)/100),4);
break;
case 3:
if (led>=1000)
to_led((uint8_t)(led/1000),8);
break;
}
reg = (reg == 3) ? 0 : reg+1;
}
main.c в таком случае будет выглядеть так:
#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
#include "uart.h"
#include "task.h"
#include "led.h"
extern void delay(uint32_t ms);
void toggle_led();
int main()
{
RCC->APB2ENR |= RCC_APB2Periph_GPIOC;
RCC->APB2ENR |= RCC_APB2Periph_GPIOA;
RCC->APB2ENR |= RCC_APB2Periph_USART1;
GPIOC->CRH &= ~(uint32_t)(0xf<<20);
GPIOC->CRH |= (uint32_t)(0x2<<20);
GPIOA->CRH &= ~(uint32_t)(0xf<<4);
GPIOA->CRH |= (uint32_t)(0xa<<4);
GPIOA->CRL &= ~(uint32_t)(0xf<<16);
GPIOA->CRL &= ~(uint32_t)(0xf<<20);
GPIOA->CRL &= ~(uint32_t)(0xf<<28);
GPIOA->CRL |= (uint32_t)(0x3<<16);
GPIOA->CRL |= (uint32_t)(0x3<<20);
GPIOA->CRL |= (uint32_t)(0x3<<28);
0x1d4c9600
USART1->BRR = 0x271; 115200
USART1->CR1 |= USART_CR1_UE_Set | USART_Mode_Tx;
reg=0; led=0;
clear_task();
add_task(show_led,TOP,LOOP, 5);
add_task(toggle_led,TOP,LOOP,1000);
if (SysTick_Config(72000))
{
while(1);
}
__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.
17) Настройка аппаратного интерфейса SPI для драйвера 4-х разрядного семисегментного индикатора
Признаюсь, что с SPI модулем мне пришлось порядком повозиться. И хотя конфигурация производится всего одним регистром - SPI_CR1, тут имеются свои подводные камни. Первая загвоздка заключается в том, что максимальная скорость SPI интерфейса в STM32F103C8T6 согласно спецификации составляет 18MHz, при том, что на SPI1, который тактируется от скоростной шины APB2 можно выставить значение в 36MHz (как я конечно же пытался сделать). Это работать не будет!
Вторая загвоздка заключалась в флагах TXE и BSY. В сети есть множество примеров работы в SPI в STM32 где анализируется только флаг TXE. На самом же деле, прежде чем опустить защелку нужно ожидать сброса флага BSY, т.к. при этом флаге производится передача из сдвигового регистра на шину, и если раньше времени опустить защелку, передача прервется. Подробнее об этом можно почитать на хабре:
STM32: SPI: LCD — Вы всё делаете не так [восклицательный знак].
В отличии от STM8, микроконтроллеры STM32 поддерживают 8-и и 16-и битные режимы. Т.к. в индикаторе стоит два сдвиговых регистра, то мы будем использовать 16-битный режим.
Подробно про SPI в STM32 можно почитать здесь: SPI (перевод из книги Mastering STM32)
Вкратце пробежимся по регистрам SPI_CR1 и SPI_SR.
По большому счету, в регистре SPI_CR1 мы видим все те-же флаги, что и в STM8. Но если там они были разбросаны по двум 8-битным регистрам: SPI_CR1 и SPI_CR2, то здесь они собраны в один регистр SPI_CR1.
Биты: BIDIMODE, BIDIOE, CRCEN, CRCNEXT, RXONLY нас сейчас не будут интересовать, они должны быть сброшены в ноль. Биты CPOL, CPHA устанавливают SPI режим. В нашем случае, для установки SPI режима "Mode 0" требуется чтобы CPOL и CPHA были сброшены в ноль. LSBFIRST устанавливает порядок передачи битов, в нашем случае данные передаются старшим битом вперед, следовательно LSBFIRST также должен быть сброшен в ноль.
Биты SSM и SSI разрешают программное управление защелкой, они должны быть установлены в "1". MSTR - включает режим мастера, должен быть установлен в "1". SPE - включает SPI модуль, должен быть установлен в "1". DFF - включает 16-битный режим, также должен быть установлен в "1". BR - устанавливает предделитель. Минимальный предделитель равен двум. Интерфейс SPI1 - тактируется от периферийной шины APB2, максимальная частота которой 72 МГц. Интерфейс SPI2 - тактируется от периферийной шины APB1, максимальная частота которой 36 МГц. Следовательно максимальная частота интерфейсов: SPI1 - 36 МГц, а SPI2 - 18 МГц. Но не забываем, что SPI не будет работать со скоростью 36 MHz, поэтому фактический минимальный делитель для SPI1, в случае STM32F103xx, равен четырём.
В регистре SPI_SR нас будут интересовать флаги BSY и TXE. Флаг TXE автоматически устанавливается при записи в регистр данных - SPI_DR, и сбрасывается когда значение из регистра данных уходит в сдвиговый регистр. Флаг BSY устанавливается в "1", когда сдвиговый регистр не пуст, т.е. идет передача на линию.
Для использования аппаратного SPI модуля потребуется добавить в проект заголовочный файл stm32f10x_spi.h из библиотеки SPL. Все заголовочные файлы из SPL добавляются проект без изменений, как есть. К сожалению, в stm32f10x_spi.h были не все битовые маски, поэтому именованные константы из stm32f10x_spi.с пришлось перенести в spi.h:
#ifndef __SPI_H__
#define __SPI_H__
#include "stm32f10x.h"
#include "stm32f10x_spi.h"
#define CR1_SPE_Set ((uint16_t)0x0040)
#define CR1_SPE_Reset ((uint16_t)0xFFBF)
#define I2SCFGR_I2SE_Set ((uint16_t)0x0400)
#define I2SCFGR_I2SE_Reset ((uint16_t)0xFBFF)
#define CR1_CRCNext_Set ((uint16_t)0x1000)
#define CR1_CRCEN_Set ((uint16_t)0x2000)
#define CR1_CRCEN_Reset ((uint16_t)0xDFFF)
#define CR2_SSOE_Set ((uint16_t)0x0004)
#define CR2_SSOE_Reset ((uint16_t)0xFFFB)
#define CR1_CLEAR_Mask ((uint16_t)0x3040)
#define I2SCFGR_CLEAR_Mask ((uint16_t)0xF040)
#define SPI_Mode_Select ((uint16_t)0xF7FF)
#define I2S_Mode_Select ((uint16_t)0x0800)
#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
Файл 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()
{
RCC->APB2ENR |= RCC_APB2Periph_GPIOC;
RCC->APB2ENR |= RCC_APB2Periph_GPIOA;
RCC->APB2ENR |= RCC_APB2Periph_USART1;
RCC->APB2ENR |= RCC_APB2Periph_SPI1;
GPIOC->CRH &= ~(uint32_t)(0xf<<20);
GPIOC->CRH |= (uint32_t)(0x2<<20);
GPIOA->CRH &= ~(uint32_t)(0xf<<4);
GPIOA->CRH |= (uint32_t)(0xa<<4);
GPIOA->CRL &= ~(uint32_t)(0xf<<16);
GPIOA->CRL &= ~(uint32_t)(0xf<<20);
GPIOA->CRL &= ~(uint32_t)(0xf<<28);
GPIOA->CRL |= (uint32_t)(0x3<<16);
GPIOA->CRL |= (uint32_t)(0xb<<20);
GPIOA->CRL |= (uint32_t)(0xb<<28);
0x1d4c9600
USART1->BRR = 0x271; 115200
USART1->CR1 |= USART_CR1_UE_Set | USART_Mode_Tx;
SPI1->CR1 = CR1_SPE_Set|SPI_Mode_Master|SPI_DataSize_16b|SPI_NSS_Soft|SPI_BaudRatePrescaler_4;;
reg=0; led=0;
clear_task();
add_task(show_led,TOP,LOOP, 5);
add_task(toggle_led,TOP,LOOP,1000);
if (SysTick_Config(72000))
{
while(1);
}
__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.
18) Регистры I2C интерфейса, делаем сканер I2C шины
I2C интерфейс STM32 так же в чем то походит на свой аналог в STM8: здесь имеются standard и fast режимы, возможность работать с 7-битными и 10-битными I2C адресами, наличествуют режимы чтения по одному и по двум байтам, и т.д. Однако в деталях и в порядке работы с интерфейсом имеются существенные отличия. Давайте разбираться.
Для начала попробуем сделать сканер I2C шины. Для этого потребуется написать код инициализации I2C модуля STM32, и код инициализации I2C устройства, т.е. получения от него отклика ACK.
Для этого нам понадобится ознакомиться с регистрами I2C модуля. Ниже представлен первый конфигурационный регистр:
Здесь нам будет мешаться биты SMB шины, которая является разновидностью I2C, и в STM32 реализована на базе I2C модуля. Биты: ALERT, PEC, NO STRETCH, ENGC, ENPEC, ENARP, SMB TYPE, SMBUS - нас не будут интересовать т.к. они отвечают за работу SMB шины. Оставшиеся биты знакомы по STM8. PE - включает/выключает I2C модуль. Установка этого бита в ноль, сбрасывает I2C модуль и переводит его в состояние IDLE. SWRST - "отпускает" I2C шину. START - посылает сигнал START на линию, и переводит модуль в режим мастера. Если I2C модуль уже находился в режиме мастера, то повторный START посылает сигнал RESTART. Бит START устанавливается программно, а сбрасывается аппаратно. STOP - посылает одноимённый сигнал на линию, и так же сбрасывается аппаратно. Передача сигнала STOP переводит I2C модуль в режим слейва. ACK и POS конфигурируют режим чтения, в зависимости от комбинации этих битов, а также бита STOP, задают тот или иной режим чтения данных. Подробнее об этом ниже.
В настоящий момент нас пока будет волновать только бит PE отвечающий за включение I2C модуля.
Второй конфигурационный регистр:
Здесь нас будет интересовать только последнее поле FREQ, в котором следует указать частоту периферийной шины в мегагерцах, которая тактирует I2C модуль. В нашем случае это APB1 с частотой 36МНz. Т.о. в разделе инициализации I2C пишем:
I2C1->CR2 &= I2C_CR2_FREQ_Reset; 0xffc0
I2C1->CR2 |= 36;
Регистр 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"
#define I2C_CR1_PE_Set ((uint16_t)0x0001)
#define I2C_CR1_PE_Reset ((uint16_t)0xFFFE)
#define I2C_CR1_START_Set ((uint16_t)0x0100)
#define I2C_CR1_START_Reset ((uint16_t)0xFEFF)
#define I2C_CR1_STOP_Set ((uint16_t)0x0200)
#define I2C_CR1_STOP_Reset ((uint16_t)0xFDFF)
#define I2C_CR1_ACK_Set ((uint16_t)0x0400)
#define I2C_CR1_ACK_Reset ((uint16_t)0xFBFF)
#define I2C_CR1_ENGC_Set ((uint16_t)0x0040)
#define I2C_CR1_ENGC_Reset ((uint16_t)0xFFBF)
#define I2C_CR1_SWRST_Set ((uint16_t)0x8000)
#define I2C_CR1_SWRST_Reset ((uint16_t)0x7FFF)
#define I2C_CR1_PEC_Set ((uint16_t)0x1000)
#define I2C_CR1_PEC_Reset ((uint16_t)0xEFFF)
#define I2C_CR1_ENPEC_Set ((uint16_t)0x0020)
#define I2C_CR1_ENPEC_Reset ((uint16_t)0xFFDF)
#define I2C_CR1_ENARP_Set ((uint16_t)0x0010)
#define I2C_CR1_ENARP_Reset ((uint16_t)0xFFEF)
#define I2C_CR1_NOSTRETCH_Set ((uint16_t)0x0080)
#define I2C_CR1_NOSTRETCH_Reset ((uint16_t)0xFF7F)
#define I2C_CR1_CLEAR_Mask ((uint16_t)0xFBF5)
#define I2C_CR2_DMAEN_Set ((uint16_t)0x0800)
#define I2C_CR2_DMAEN_Reset ((uint16_t)0xF7FF)
#define I2C_CR2_LAST_Set ((uint16_t)0x1000)
#define I2C_CR2_LAST_Reset ((uint16_t)0xEFFF)
#define I2C_CR2_FREQ_Reset ((uint16_t)0xFFC0)
#define I2C_OAR1_ADD0_Set ((uint16_t)0x0001)
#define I2C_OAR1_ADD0_Reset ((uint16_t)0xFFFE)
#define I2C_OAR2_ENDUAL_Set ((uint16_t)0x0001)
#define I2C_OAR2_ENDUAL_Reset ((uint16_t)0xFFFE)
#define I2C_OAR2_ADD2_Reset ((uint16_t)0xFF01)
#define I2C_CCR_FS_Set ((uint16_t)0x8000)
#define I2C_CCR_CCR_Set ((uint16_t)0x0FFF)
#define I2C_FLAG_Mask ((uint32_t)0x00FFFFFF)
#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
#define disable_i2c I2C1->CR1 &= I2C_CR1_PE_Reset;
#define stop_i2c I2C1->CR1 |= I2C_CR1_STOP_Set; 0x0200
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
while (!(I2C1->SR1 & I2C_FLAG_SB));
(void) I2C1->SR1;
I2C1->DR=adr;
while (!(I2C1->SR1 & I2C_FLAG_ADDR))
{
if(I2C1->SR1 & I2C_IT_AF)
return 1;
}
(void) I2C1->SR1;
(void) I2C1->SR2;
I2C1->DR=value;
if (last == LAST) {
while(!(I2C1->SR1 & I2C_FLAG_BTF));
I2C1->CR1 |= I2C_CR1_STOP_Set;
while (I2C1->CR1 & I2C_CR1_STOP_Set);
} else {
while(!(I2C1->SR1 & I2C_FLAG_TXE));
}
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()
{
RCC->APB2ENR |= RCC_APB2Periph_GPIOA;
RCC->APB2ENR |= RCC_APB2Periph_GPIOB;
RCC->APB2ENR |= RCC_APB2Periph_GPIOC;
RCC->APB2ENR |= RCC_APB2Periph_USART1;
RCC->APB2ENR |= RCC_APB2Periph_SPI1;
RCC->APB1ENR |= RCC_APB1Periph_I2C1;
GPIOC->CRH &= ~(uint32_t)(0xf<<20);
GPIOC->CRH |= (uint32_t)(0x2<<20);
GPIOA->CRH &= ~(uint32_t)(0xf<<4);
GPIOA->CRH |= (uint32_t)(0xa<<4);
GPIOA->CRL &= ~(uint32_t)(0xf<<16);
GPIOA->CRL &= ~(uint32_t)(0xf<<20);
GPIOA->CRL &= ~(uint32_t)(0xf<<28);
GPIOA->CRL |= (uint32_t)(0x3<<16);
GPIOA->CRL |= (uint32_t)(0xb<<20);
GPIOA->CRL |= (uint32_t)(0xb<<28);
GPIOB->CRL &= ~(uint32_t)(0xf<<24);
GPIOB->CRL &= ~(uint32_t)(0xf<<28);
GPIOB->CRL |= (uint32_t)(0xe<<24);
GPIOB->CRL |= (uint32_t)(0xe<<28);
0x1d4c9600
USART1->BRR = 0x271; 115200
USART1->CR1 |= USART_CR1_UE_Set | USART_Mode_Tx;
SPI1->CR1 = CR1_SPE_Set|SPI_Mode_Master|SPI_DataSize_16b|SPI_NSS_Soft|SPI_BaudRatePrescaler_4;;
disable_i2c;
I2C1->CR2 &= I2C_CR2_FREQ_Reset; 0xffc0
I2C1->CR2 |= 36;
I2C1->CCR = 180; 100
I2C1->TRISE = 37;
reg=0; led=0;
clear_task();
add_task(show_led,TOP,LOOP, 5);
add_task(toggle_led,TOP,LOOP,1000);
if (SysTick_Config(72000))
{
while(1);
}
__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++;
}
Насколько понимаю, в данном случае работа I2C модуля не совсем корректна, т.к. модуль приходится постоянно отключать и включать заново. В дальнейшем, при нормальной работе это не потребуется.
Я тестировал код на китайском модуле RTC DS3231 + EEPROM AT24С32. Подключение: SCL на PB6, SDA на PB7. Результат работы сканера:
19 Однобайтный режим чтения по шине I2C
Однобайтное чтение используется когда нужно прочитать какой-либо регистр на I2C устройстве. Для примера, с помощью функции чтения одного байта мы прочитаем текущее время в DS3231.
Порядок работы с I2C модулем в режиме чтения одного байта продемонстрирован на следующей блок-схеме:
Вооружившись этим знанием, добавляем в src/i2c.c функцию чтения:
uint8_t read_byte(uint8_t adr){
uint8_t ret;
I2C1->CR1 |= I2C_CR1_START_Set; 0x0100
while (!(I2C1->SR1 & I2C_FLAG_SB)); 0x0001
(void) I2C1->SR1;
I2C1->DR=adr;
while (!(I2C1->SR1 & I2C_FLAG_ADDR)); 0x0002
I2C1->CR1 &= I2C_CR1_ACK_Reset; 1.0xFBFF
__disable_irq();
(void) I2C1->SR1; 2.
(void) I2C1->SR2; 2.
I2C1->CR1 |= I2C_CR1_STOP_Set; 3.0x0200
__enable_irq();
while(!(I2C1->SR1 & I2C_IT_RXNE)); 0x0040
ret=I2C1->DR;
while (I2C1->CR1 & I2C_CR1_STOP_Set);
I2C1->CR1 |= I2C_CR1_ACK_Set;
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()
{
RCC->APB2ENR |= RCC_APB2Periph_GPIOA;
RCC->APB2ENR |= RCC_APB2Periph_GPIOB;
RCC->APB2ENR |= RCC_APB2Periph_GPIOC;
RCC->APB2ENR |= RCC_APB2Periph_USART1;
RCC->APB2ENR |= RCC_APB2Periph_SPI1;
RCC->APB1ENR |= RCC_APB1Periph_I2C1;
GPIOC->CRH &= ~(uint32_t)(0xf<<20);
GPIOC->CRH |= (uint32_t)(0x2<<20);
GPIOA->CRH &= ~(uint32_t)(0xf<<4);
GPIOA->CRH |= (uint32_t)(0xa<<4);
GPIOA->CRL &= ~(uint32_t)(0xf<<16);
GPIOA->CRL &= ~(uint32_t)(0xf<<20);
GPIOA->CRL &= ~(uint32_t)(0xf<<28);
GPIOA->CRL |= (uint32_t)(0x3<<16);
GPIOA->CRL |= (uint32_t)(0xb<<20);
GPIOA->CRL |= (uint32_t)(0xb<<28);
GPIOB->CRL &= ~(uint32_t)(0xf<<24);
GPIOB->CRL &= ~(uint32_t)(0xf<<28);
GPIOB->CRL |= (uint32_t)(0xe<<24);
GPIOB->CRL |= (uint32_t)(0xe<<28);
0x1d4c9600
USART1->BRR = 0x271; 115200
USART1->CR1 |= USART_CR1_UE_Set | USART_Mode_Tx;
SPI1->CR1 = CR1_SPE_Set|SPI_Mode_Master|SPI_DataSize_16b|SPI_NSS_Soft|SPI_BaudRatePrescaler_4;;
disable_i2c;
I2C1->CR2 &= I2C_CR2_FREQ_Reset; 0xffc0
I2C1->CR2 |= 36;
I2C1->CCR = 180; 100
I2C1->TRISE = 37;
enable_i2c;
reg=0; led=0;
clear_task();
add_task(show_led,TOP,LOOP, 5);
add_task(toggle_led,TOP,LOOP,1000);
if (SysTick_Config(72000))
{
while(1);
}
__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++;
}
На скриншоте представлен результат работы программы:
20 Двухбайтный режим чтения по шине I2C
Двухбайтный режим чтения используется для чтения 16-битных регистров, например в RDA5807. В STM32 можно организовать и трехбайтное чтение, но мне трудно представить, где это может понадобится. А вот однобайтный и двухбайтные режимы, на мой взгляд, используются довольно часто.
Порядок работы с I2C модулем в режиме двухбайтного чтения продемонстрирован на следующей блок-схеме:
Реализация этого алгоритма на Си у меня получилась такой:
uint16_t read_two_byte(uint8_t adr){
uint16_t ret=0;
uint8_t vl;
I2C1->CR1 |= I2C_CR1_START_Set; 0x0100
while (!(I2C1->SR1 & I2C_FLAG_SB)); 0x0001
(void) I2C1->SR1;
I2C1->DR=adr;
while (!(I2C1->SR1 & I2C_FLAG_ADDR)); 0x0002
I2C1->CR1 |=I2C_PECPosition_Next; 1
__disable_irq();
(void) I2C1->SR1;
(void) I2C1->SR2;
I2C1->CR1 &= I2C_CR1_ACK_Reset; 0xFBFF
__enable_irq();
while(!(I2C1->SR1 & I2C_FLAG_BTF));
__disable_irq();
I2C1->CR1 |= I2C_CR1_STOP_Set; ; 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); 0
I2C1->CR1 |= I2C_CR1_ACK_Set; 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;
}
21 Запись массива через шину I2C
Алгоритм записи одного или нескольких байт через I2C шину в STM32 я уже приводил ранее:
Опираясь на эту блок-схему нетрудно написать функцию записи одного числа:
void ds3231_write_register(uint8_t reg, uint8_t value) {
if (init_i2c(DS3231_I2C_ADDR, reg, NOLAST) == 0) {
I2C1->DR=value;
while(!(I2C1->SR1 & I2C_FLAG_BTF));
I2C1->CR1 |= I2C_CR1_STOP_Set;
while (I2C1->CR1 & I2C_CR1_STOP_Set);
} else
stop_i2c;
}
и функцию записи массива:
void i2c_write(uint8_t adr, uint8_t reg, uint8_t count, uint8_t* data) {
"count"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));
I2C1->CR1 |= I2C_CR1_STOP_Set;
while (I2C1->CR1 & I2C_CR1_STOP_Set);
} else
while(!(I2C1->SR1 & I2C_FLAG_TXE));
}
} 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;
}
22 Чтение массива через шину I2C
Функцию чтения одного байта можно развить до функции чтения массива. Для этого придётся ее утяжелить циклом последовательного чтения байтов с шины I2C.
У меня это получилось так:
void i2c_read(uint8_t adr, uint8_t count,uint8_t* data){
uint8_t ret;
I2C1->CR1 |= I2C_CR1_START_Set; 0x0100
while (!(I2C1->SR1 & I2C_FLAG_SB)); 0x0001
(void) I2C1->SR1;
I2C1->DR=adr;
while (!(I2C1->SR1 & I2C_FLAG_ADDR)); 0x0002
I2C1->CR1 |= I2C_CR1_ACK_Set; 0x0400
(void) I2C1->SR1;
(void) I2C1->SR2;
for(uint8_t i=1;i<=count;i++, data++) {
if (i<count) {
while(!(I2C1->SR1 & I2C_IT_RXNE)); 0x0040
*data=I2C1->DR;
} else {
I2C1->CR1 &= I2C_CR1_ACK_Reset; 0xFBFF
I2C1->CR1 |= I2C_CR1_STOP_Set; 0x0200
while(!(I2C1->SR1 & I2C_IT_RXNE)); 0x0040
*data=I2C1->DR;
while (I2C1->CR1 & I2C_CR1_STOP_Set); 0x0200
}
}
I2C1->CR1 |= I2C_CR1_ACK_Set; 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;
}
Результат работы программы:
23 Отладка в консоли с использованием OpenOCD
Отладку STM32 c помощью связки st-util + stlink_v2 в консоли я уже рассматривал ранее, однако большинство IDE в качестве gdb-сервера используют OpenOCD. Прежде чем "прикручивать" OpenOCD к IDE, нужно научиться запускать его в консоли.
Моя версия OpenOCD:
$ openocd --version
Open On-Chip Debugger 0.10.0
Licensed under GNU GPL v2
For bug reports, read
http://openocd.org/doc/doxygen/bugs.html
Посмотрим, к чему мы можем подключиться с помощью OpenOCD:
$ ls /usr/share/openocd/scripts/target/
1986ве1т.cfg at91sam9g45.cfg icepick.cfg lpc4350.cfg stm32f1x.cfg
adsp-sc58x.cfg at91sam9rl.cfg imx.cfg lpc4357.cfg stm32f1x_stlink.cfg
aduc702x.cfg at91samdXX.cfg imx21.cfg lpc4370.cfg stm32f2x.cfg
aducm360.cfg at91samg5x.cfg imx25.cfg lpc8xx.cfg stm32f2x_stlink.cfg
alphascale_asm9260t.cfg atheros_ar2313.cfg imx27.cfg mc13224v.cfg stm32f3x.cfg
altera_fpgasoc.cfg atheros_ar2315.cfg imx28.cfg mdr32f9q2i.cfg stm32f3x_stlink.cfg
am335x.cfg atheros_ar9331.cfg imx31.cfg nds32v2.cfg stm32f4x.cfg
am437x.cfg atmega128.cfg imx35.cfg nds32v3.cfg stm32f4x_stlink.cfg
amdm37x.cfg atsamv.cfg imx51.cfg nds32v3m.cfg stm32f7x.cfg
ar71xx.cfg avr32.cfg imx53.cfg nrf51.cfg stm32l0.cfg
armada370.cfg bcm281xx.cfg imx6.cfg nrf51_stlink.tcl stm32l1.cfg
at32ap7000.cfg bcm4706.cfg is5114.cfg nrf52.cfg stm32l1x_dual_bank.cfg
at91r40008.cfg bcm4718.cfg ixp42x.cfg nuc910.cfg stm32l4x.cfg
at91rm9200.cfg bcm47xx.cfg k1921vk01t.cfg numicro.cfg stm32lx_stlink.cfg
at91sam3XXX.cfg bcm5352e.cfg k40.cfg omap2420.cfg stm32w108_stlink.cfg
at91sam3ax_4x.cfg bcm6348.cfg k60.cfg omap3530.cfg stm32w108xx.cfg
at91sam3ax_8x.cfg c100.cfg ke02.cfg omap4430.cfg stm32xl.cfg
at91sam3ax_xx.cfg c100config.tcl ke04.cfg omap4460.cfg str710.cfg
at91sam3nXX.cfg c100helper.tcl ke06.cfg omap5912.cfg str730.cfg
at91sam3sXX.cfg c100regs.tcl kex.cfg omapl138.cfg str750.cfg
at91sam3u1c.cfg cc2538.cfg kl25.cfg or1k.cfg str912.cfg
at91sam3u1e.cfg cc26xx.cfg kl25z_hla.cfg pic32mx.cfg swj-dp.tcl
at91sam3u2c.cfg cc32xx.cfg kl46.cfg psoc4.cfg test_reset_syntax_error.cfg
at91sam3u2e.cfg cs351x.cfg klx.cfg psoc5lp.cfg test_syntax_error.cfg
at91sam3u4c.cfg davinci.cfg ks869x.cfg pxa255.cfg ti-ar7.cfg
at91sam3u4e.cfg dragonite.cfg kx.cfg pxa270.cfg ti-cjtag.cfg
at91sam3uxx.cfg dsp56321.cfg lpc11xx.cfg pxa3xx.cfg ti_calypso.cfg
at91sam4XXX.cfg dsp568013.cfg lpc12xx.cfg quark_d20xx.cfg ti_dm355.cfg
at91sam4c32x.cfg dsp568037.cfg lpc13xx.cfg quark_x10xx.cfg ti_dm365.cfg
at91sam4cXXX.cfg efm32.cfg lpc17xx.cfg readme.txt ti_dm6446.cfg
at91sam4lXX.cfg efm32_stlink.cfg lpc1850.cfg renesas_s7g2.cfg ti_msp432p4xx.cfg
at91sam4sXX.cfg em357.cfg lpc1xxx.cfg samsung_s3c2410.cfg ti_rm4x.cfg
at91sam4sd32x.cfg em358.cfg lpc2103.cfg samsung_s3c2440.cfg ti_tms570.cfg
at91sam7a2.cfg epc9301.cfg lpc2124.cfg samsung_s3c2450.cfg ti_tms570ls20xxx.cfg
at91sam7se512.cfg exynos5250.cfg lpc2129.cfg samsung_s3c4510.cfg ti_tms570ls3137.cfg
at91sam7sx.cfg faux.cfg lpc2148.cfg samsung_s3c6410.cfg tmpa900.cfg
at91sam7x256.cfg feroceon.cfg lpc2294.cfg sharp_lh79532.cfg tmpa910.cfg
at91sam7x512.cfg fm3.cfg lpc2378.cfg sim3x.cfg u8500.cfg
at91sam9.cfg fm4.cfg lpc2460.cfg smp8634.cfg vybrid_vf6xx.cfg
at91sam9260.cfg fm4_mb9bf.cfg lpc2478.cfg spear3xx.cfg xmc1xxx.cfg
at91sam9260ext_flash.cfg fm4_s6e2cc.cfg lpc2900.cfg stellaris.cfg xmc4xxx.cfg
at91sam9261.cfg gp326xxxa.cfg lpc2xxx.cfg stellaris_icdi.cfg xmos_xs1-xau8a-10_arm.cfg
at91sam9263.cfg hilscher_netx10.cfg lpc3131.cfg stm32_stlink.cfg zynq_7000.cfg
at91sam9g10.cfg hilscher_netx50.cfg lpc3250.cfg stm32f0x.cfg к1879xб1я.cfg
at91sam9g20.cfg hilscher_netx500.cfg lpc40xx.cfg stm32f0x_stlink.cfg
Можно предположить, что нам подойдут таргеты stm32f1x_stlink.cfg или/и stm32f1x.cfg.
Пробуем подключиться с помощью скрипта stm32f1x_stlink.cfg:
$ openocd -f interface/stlink-v2.cfg -f target/stm32f1x_stlink.cfg -c "init" -c "reset halt"
Open On-Chip Debugger 0.10.0
Licensed under GNU GPL v2
For bug reports, read
http://openocd.org/doc/doxygen/bugs.html
WARNING: target/stm32f1x_stlink.cfg is deprecated, please switch to target/stm32f1x.cfg
Info : auto-selecting first available session transport "hla_swd". To override use 'transport select <transport>'.
Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD
adapter speed: 1000 kHz
adapter_nsrst_delay: 100
none separate
Info : Unable to match requested speed 1000 kHz, using 950 kHz
Info : Unable to match requested speed 1000 kHz, using 950 kHz
Info : clock speed 950 kHz
Info : STLINK v2 JTAG v27 API v2 SWIM v6 VID 0x0483 PID 0x3748
Info : using stlink api v2
Info : Target voltage: 3.254076
Info : stm32f1x.cpu: hardware has 6 breakpoints, 4 watchpoints
target halted due to debug-request, current mode: Thread
xPSR: 0x01000000 pc: 0x08000b4c msp: 0x20005000
Если верить этому сообщению: "WARNING: target/stm32f1x_stlink.cfg is deprecated, please switch to target/stm32f1x.cfg", то мы должны использовать таргет stm32f1x.cfg. Пока проигнорируем это.
В другом окне запускаем отладчик командой:
$ arm-none-eabi-gdb ./blink.elf --tui
Порт OpenOCD по умолчанию - 3333. Подключаемся к OpenOCD:
(gdb) target remote localhost:3333
Remote debugging using localhost:3333
Reset_Handler () at asm/init.s:20
В окне OpenOCD видим отклик:
Info : accepting 'gdb' connection on tcp/3333
Info : device id = 0x20036410
Info : flash size = 64kbytes
Открываем окно с ассемблерным листингом: "layout asm", окно с регистрами: layout regs", и видим что находимся в обработчике прерывания Reset:
Что делать дальше - думаю понятно. Добавлю, что вместо таргета stm32f1x_stlink.cfg можно использовать stm32f1x.cfg, я разницы не заметил. Предыдущая версия OpenOCD 0.8 c с этим тагретом работать отказывалась.
Напоминаю, что посмотреть исходники, сборочные файлы, скачать скомпилированные прошивки, можно с портала GITLAB https://gitlab.com/flank1er/stm32_bare_metal