В прошлой части мы установили необходимое для разработки программное обеспечение. В этой статье я расскажу как его правильно настроить и связать. Все коммерческие среды такие как IAR EWARM или Keil uVision обычно сами выполняют эту интеграцию, однако в нашем случае все придется настраивать вручную, потратив на это немало времени. Преимуществом является то, что у вас есть шанс понять как это все работает изнутри, и в дальнейшем гибко настраивать все под себя. Перед началом настройки рассмотрим структуру среды в которой мы будем работать:

eclipse_p_openocd.jpg

Eclipse будет использован для удобного редактирования файлов реализации функций (.c), заголовочных файлов (.h), а также файлов ассемблера (.S). Под "удобным" я понимаю использование автодополненения кода, подсветки синтаксиса, рефакторинга, навигации по функциям и их прототипам. Файлы автоматически скармливаются нужным компиляторам, которые генерируют объектный код (в файлах .o).  Пока что этот код не содержит абсолютных адресов переменных и функций и по этому для выполнения не пригоден. Полученные объектные файлы собираются воедино сборщиком (linker-ом). Чтобы знать, какие участки адресного пространства использовать, сборщик использует специальный файл (.ld), который называется линкер-скриптом. Он обычно содержит определение адресов секций и их размеров (секция кода, отображаемая на флеш, секция переменных, отображаемая на ОЗУ и т.д.). 

В конце концов linker генерирует .elf файл (Executable and Linkable Format), который содержит в себе кроме инструкций и данных отладочную информацию (Debugging information), используемую отладчиком. Для обычной прошивки программой vsprog этот формат не подходит, поскольку для этого нужен более примитивный файл образа памяти (например Intel HEX - .hex). Для его генерации тоже есть инструмент из набора Sourcery CodeBench (arm-none-eabi-objcopy), и он отлично интегрируются в eclipse с помощью установленного ARM-плагина.

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

  1. сам eclipse, дающий возможность программисту "визуально" использовать отладку, ходить по строкам, наводить курсором мышки на переменные для просмотра их значений, и прочие удобности
  2. arm-none-eabi-gdb - GDB клиент - отладчик, который скрыто управляется eclips-ом(через stdin) в качестве реакции на действия, указанные в п.1. В свою очередь GDB подключается к Debug-серверу OpenOCD, и все поступающие на вход команды транслируются отладчиком GDB в команды,  понятные для OpenOCD. Канал GDB <-> OpenOCD реализуется по протоколу TCP.
  3. OpenOCD - это debug-сервер, который может общаться непосредственно с программатором. Он запускается перед клиентом и ожидает подключения по TCP.

Данная схема может показаться вам весьма бесполезной: зачем использовать клиент и сервер в отдельности и выполнять лишний раз трансляцию команд, если все это можно было бы делать одним отладчиком? Дело в том, что такая архитектура теоретически позволяет удобно делать взаимозамену клиента и сервера. Например, если вам будет нужно вместо versaloon использовать другой программатор, который не будет поддерживать OpenOCD, а будет поддерживать другой специальный Debug-сервер (например texane/stlink для программатора stlink - который находится в отладочной плате STM32VLDiscovery), то вы просто вместо запуска OpenOCD будете запускать нужный сервер и все должно работать, без каких-либо дополнительных телодвижений. В то же время возможна обратная ситуация: допустим вы захотели использовать вместо связки Eclipse + CodeBench, среду IAR EWARM вместе с versaloon. У IAR есть свой встроенный Debug-клиент, который успешно свяжется с OpenOCD и будет им рулить, а также получать в ответ нужные данные. Однако все это иногда остается только в теории, поскольку стандарты общения клиента и сервера регламентированы не жестко, и местами могут отличатся, однако указанные мною конфигурации с st-link+eclipse и IAR+versaloon мне удавались.

Обычно клиент и сервер запускаются на одной машине и подключение к серверу происходит по адресу localhost:3333 (Для openocd), или localhost:4242 (для texane/stlink st-util). Но никто не мешает открыть порт 3333 или 4242 (и пробросить этот порт на роутере во внешнюю сеть) и ваши коллеги из другого города смогут подключится и отладить вашу железку. Данный трюк часто используется ембеддерами, работающими на удаленных объектах, доступ к которым ограничен.

Приступаем

Запускаем eclipse и выбираем File->New->C Project, выбираем тип проекта ARM Linux GCC (Sorcery G++ Lite) и имя "stm32_ld_vl" (Если у вас STV32VLDiscovery то логичнее будет назвать "stm32_md_vl"):

stm32pro1.jpg

Нажимаем Finish, сворачиваем или закрываем окно Welcome. Итак, проект создан, и в вашем workspace должна появиться папка stm32_ld_vl. Теперь ее нужно наполнить необходимыми библиотеками.

Как вы поняли из названия проекта, я буду создавать проект для вида линейки low-density value line (LD_VL). Чтобы создать проект для других микроконтроллеров вы должны заменить все файлы и define-ы в названии которых присутствует _LD_VL (или _ld_vl) на нужные вам, в  соответствии с таблицей:

Вид линейки  Обозначение  Микроконтроллеры (х может менятся)
Low-density value line _LD_VL STM32F100x4 STM32F100x6
Low-density _LD  STM32F101x4 STM32F101x6
STM32F102x4 STM32F102x6
STM32F103x4 STM32F103x6
Medium-density value line _MD_VL STM32F100x8 STM32F100xB
Medium-density
_MD
STM32F101x8 STM32F101xB
STM32F102x8 STM32F102xB
STM32F103x8 STM32F103xB
High density Value line _HD_VL STM32F100xC STM32F100xD STM32F100xE
High density _HD STM32F101xC STM32F101xD STM32F101xE
STM32F103xC STM32F103xD STM32F103xE
XL-density  _XL STM32F101xF STM32F101xG
STM32F103xF STM32F103xG
Connectivity line _CL STM32F105xx и STM32F107xx

Чтобы понять логику таблицы, вы должны быть знакомы с маркировкой STM32. То есть, если у вас VLDiscovery то дальше вам придется заменять все что связано с _LD_VL на _MD_VL, поскольку в дискавери распаян чип STM32F100RB, относящийся к Medium-density value line.

Добавление в проект библиотек CMSIS и STM32F10x Standard Peripherals Library

CMSIS (Cortex Microcontroller Software Interface Standard) - стандартизированная библиотека работы с микроконтроллерами Cortex, выполняющая реализацию уровня HAL (Hardware Abstraction Layer), тоесть позволяет абстрагироваться от деталей работы с регистрами, поиска адресов регистров по даташитам и т.д. Библиотека представляет собой набор из исходников на языке С и Asm. Ядерная (Core) часть библиотеки одинакова для всех Cortex-ов (Будь это ST, NXP, ATMEL, TI или еще кто другой), и разрабатывается компанией ARM. Другая же часть библиотеки отвечает за периферию, которая естественно различна у разных производителей. Поэтому в конечном итоге полная библиотека все равно распространяется производителем, хотя ядерную часть все же можно скачать отдельно на сайте ARM. Библиотека содержит определения адресов, код инициализации тактового генератора (удобно настраиваемый define-ами), и все прочее, что избавляет программиста от ручного введения в свои проекты определения адресов всяческих регистров периферии и определения битов значений этих регистров.

Но ребята из ST пошли дальше. Помимо поддержки CMSIS они предоставляют еще одну библиотеку для STM32F10x под названием Standard Peripherals Library (SPL), которая может использоваться в дополнение к CMSIS. Библиотека обеспечивает более быстрый и удобный доступ к периферии, а также контролирует (в некоторых случаях) правильность работы с периферией. Поэтому данную библиотек часто называют набором драйверов к периферийным модулям. Она сопровождается пакетом примерчиков, разделенных по категориям для разной перифериии. Библиотека также есть не только для STM32F10x, но и под другие серии.

Скачать всю SPL+CMSIS версии 3.5 можно тут: STM32F10x_StdPeriph_Lib_V3.5.0 или на сайте ST. Разархивируйте архив. Создайте папки CMSIS и SPL в папке проекта и начнем копировать файлы к себе в проект:

Что копировать

Куда копировать (учитывая,
что папка проекта stm32_ld_vl

 Описание файла
Libraries/CMSIS/CM3/
CoreSupport/core_cm3.c
stm32_ld_vl/CMSIS/core_cm3.c Описание ядра Cortex M3
Libraries/CMSIS/CM3/
CoreSupport/core_cm3.h
stm32_ld_vl/CMSIS/core_cm3.h  Заголовки описания ядра
Libraries/CMSIS/CM3/DeviceSupport/
ST/STM32F10x/system_stm32f10x.c
stm32_ld_vl/CMSIS/system_stm32f10x.c Функции инициализации и
управления тактовой частотой
Libraries/CMSIS/CM3/DeviceSupport/
ST/STM32F10x/system_stm32f10x.h
stm32_ld_vl/CMSIS/system_stm32f10x.h Заголовки к этим функциям 
Libraries/CMSIS/CM3/DeviceSupport/
ST/STM32F10x/stm32f10x.h
stm32_ld_vl/CMSIS/stm32f10x.h Основное описание периферии
Libraries/CMSIS/CM3/DeviceSupport/
ST/STM32F10x/startup/gcc_ride7/
startup_stm32f10x_ld_vl.s
stm32_ld_vl/CMSIS/startup_stm32f10x_ld_vl.S
(!!! Внимание расширение файла CAPITAL S)
Файл с таблицей векторов
прерываний и init-ами на asm
Project/STM32F10x_StdPeriph_Template/
stm32f10x_conf.h
stm32_ld_vl/CMSIS/stm32f10x_conf.h Шаблон для настройки
периферийных модулей
Libraries/STM32F10x_StdPeriph_Driver/
inc/*
stm32_ld_vl/SPL/inc/* Заголовочные файлы SPL
Libraries/STM32F10x_StdPeriph_Driver/
src/*
stm32_ld_vl/SPL/src/* Реализация SPL

После копирования зайдите в Eclipse и сделайте Refresh в контекстном меню проекта. В результате в Project Explorer вы должны получить такую же структуру как на картинке справа. 

Возможно вы заметили, что в папке Libraries/CMSIS/CM3/DeviceSupport/ST/STM32F10x/startup/ есть папки для разных IDE (в разных IDE используются разные компиляторы). Я выбрал IDE Ride7, так как в ней используется компилятор GNU Tools for ARM Embedded, совместимый с нашим Sourcery CodeBench.

Вся библиотека конфигурируется с помощью препроцессора (с помощью define-ов), это позволят решить все необходимые ветвления еще на стадии компиляции (вернее даже перед ней) и избежать нагрузки в работе самого контроллера (которая наблюдалась бы, если бы конфигурирование выполнялось в RunTime). Например все оборудование различное для разных линеек и поэтому чтобы библиотека "узнала", какую линейку вы хотите использовать, вас просят раскомментировать в файле stm32f10x.h один из define-ов (соответствующих вашей линейке):

/* #define STM32F10X_LD */     /*!< STM32F10X_LD: STM32 Low density devices */
/* #define STM32F10X_LD_VL */    /*!< STM32F10X_LD_VL: STM32 Low density Value Line devices */
/* #define STM32F10X_MD */     /*!< STM32F10X_MD: STM32 Medium density devices */

И так далее ...

Но делать этого я не советую. Файлы библиотек мы трогать пока не будем, а define мы сделаем позже с помощью настроек компилятора в Eclipse. И тогда Eсlipse будет вызвать компилятор с ключем -D STM32F10X_LD_VL, что для препроцессора абсолютно эквивалентно ситуации если бы вы расскомментировали "#define STM32F10X_LD_VL". Таким образом код мы менять не будем, в следствии, при желании, когда-нибудь вы сможете вынести библиотеку в отдельную директорию и не копировать в папку каждого нового проекта.

Linker-скрипт

В контекстном меню проекта выбираем New->File->Other->General->File, Next. Выбираем корневую папку проекта (stm32_ld_vl). Вводим имя файла "stm32f100c4.ld" (или "stm32f100rb.ld" для дискавери). Теперь копируем и вставляем в eclipse:

ENTRY(Reset_Handler)
MEMORY
{
  FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 16K
  RAM   (xrw) : ORIGIN = 0x20000000, LENGTH = 4K
}
_estack = ORIGIN(RAM) + LENGTH(RAM);
MIN_HEAP_SIZE = 0;
MIN_STACK_SIZE = 256;
SECTIONS
{
  /* Interrupt vector table */
  .isr_vector :
  {
    . = ALIGN(4);
    KEEP(*(.isr_vector))
    . = ALIGN(4);
  } >FLASH
  /* The program code and other data goes into FLASH */
  .text :
  {
    . = ALIGN(4);
    /* Code */
    *(.text)
    *(.text*)
    /* Constants */
    *(.rodata)
    *(.rodata*)
    /* ARM->Thumb and Thumb->ARM glue code */
    *(.glue_7)
    *(.glue_7t)
    KEEP (*(.init))
    KEEP (*(.fini))
    . = ALIGN(4);
    _etext = .;
  } >FLASH
  .ARM.extab :
  {
    *(.ARM.extab* .gnu.linkonce.armextab.*)
  } >FLASH
  .ARM :
  {
    __exidx_start = .;
    *(.ARM.exidx*)
    __exidx_end = .;
  } >FLASH
  .ARM.attributes :
  {
    *(.ARM.attributes)
  } > FLASH
  .preinit_array :
  {
    PROVIDE_HIDDEN (__preinit_array_start = .);
    KEEP (*(.preinit_array*))
    PROVIDE_HIDDEN (__preinit_array_end = .);
  } >FLASH
  .init_array :
  {
    PROVIDE_HIDDEN (__init_array_start = .);
    KEEP (*(SORT(.init_array.*)))
    KEEP (*(.init_array*))
    PROVIDE_HIDDEN (__init_array_end = .);
  } >FLASH
  .fini_array :
  {
    PROVIDE_HIDDEN (__fini_array_start = .);
    KEEP (*(.fini_array*))
    KEEP (*(SORT(.fini_array.*)))
    PROVIDE_HIDDEN (__fini_array_end = .);
  } >FLASH
  _sidata = .;
  /* Initialized data */
  .data : AT ( _sidata )
  {
    . = ALIGN(4);
    _sdata = .;   /* create a global symbol at data start */
    *(.data)
    *(.data*)
    . = ALIGN(4);
    _edata = .;   /* define a global symbol at data end */
  } >RAM
  /* Uninitialized data */
  . = ALIGN(4);
  .bss :
  {
    /* This is used by the startup in order to initialize the .bss secion */
    _sbss = .;    /* define a global symbol at bss start */
    __bss_start__ = _sbss;
    *(.bss)
    *(.bss*)
    *(COMMON)
    . = ALIGN(4);
    _ebss = .;    /* define a global symbol at bss end */
    __bss_end__ = _ebss;
  } >RAM
  PROVIDE(end = _ebss);
  PROVIDE(_end = _ebss);
  PROVIDE(__HEAP_START = _ebss);
  /* User_heap_stack section, used to check that there is enough RAM left */
  ._user_heap_stack :
  {
    . = ALIGN(4);
    . = . + MIN_HEAP_SIZE;
    . = . + MIN_STACK_SIZE;
    . = ALIGN(4);
  } >RAM
  /DISCARD/ :
  {
    libc.a(*)
    libm.a(*)
    libgcc.a(*)
  }
}

Данный линкер-скрипт будет предназначен именно для контроллера STM32F100C4 (у которого 16 Кб флеша и 4 Кб ОЗУ), если у вас другой, то придется поменять параметры LENGTH у областей FLASH и RAM в начале файла (для STM32F100RB, который в Discovery: Flash 128K и ОЗУ 8К). 

Сохраняем файл.

Настройка сборки (C/C++ Build) 

Заходим в Project->Properties->C/C++ Build-> Settings->Tool Settings, и начинаем настраивать инструменты сборки:

1) Target Precessor

Выбираем под какое именно ядро Cortex компилятор будет работать. 

  • Processor: cortex-m3

stm32pro2(4).jpg

2) ARM Sourcery Linux GCC C Compiler -> Preprocessor

Добавляем два define-a путем передачи их через ключ -D компилятору. 

  • STM32F10X_LD_VL - определяет линейку  (о этом define-е я писал выше)
  • USE_STDPERIPH_DRIVER - указание библиотеке CMSIS, что она должна использовать драйвер SPL

armnonedefinit(1).jpg

3) ARM Sourcery Linux GCC C Compiler -> Directories

Добавляем пути к includ-ам библиотек.

  • "${workspace_loc:/${ProjName}/CMSIS}"
  • "${workspace_loc:/${ProjName}/SPL/inc}"

armnonepathtoinc(1).jpg

Теперь, например, если мы напишем:

#include "stm32f10x.h

То компилятор должен сначала поискать файл stm32f10x.h в директории проекта (он это делает всегда), он его там не найдет и приступит к поиску в папке CMSIS, путь к которой мы указали, ну и найдет его. 

4) ARM Sourcery Linux GCC C Compiler -> Optimization

Включим оптимизацию функций и данных

  • -ffunction-sections
  • -fdata-sections

armnoneclipseexpressoptimiz.jpg

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

5) ARM Sourcery Linux GCC C Compiler -> General

Добавляем путь к нашему linker-скрипту: "${workspace_loc:/${ProjName}/stm32f100c4.ld}" (или как он у вас называется).

И ставим опции:

  • Do not use standard start files - не использовать стандартные файлы запуска.
  • Remove unused sections - удалить неиспользованные секции

armnonelinker.jpg

Все, настройка закончена. OK.

С момента создания проекта мы много всего сделали, и кое-чего Eclipse мог не заметить, по этому нам нужно сказать ему чтобы он пересмотрел структуру файлов проекта. Для этого из контекстного меню проекта нужно сделать Index -> rebuild.

Hello светодиоды на STM32

Пора создать главный файл проекта: File -> New -> C/C++ -> Source File. Next. Имя файла Source file: main.c.

Копируем и вставляем в файл следующее:

#include "stm32f10x.h"
uint8_t i=0;
int main(void) {
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;  // Enable PORTB Periph clock
    RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;  // Enable TIM2 Periph clock
    // Disable JTAG for release LED PIN
    RCC->APB2ENR |= RCC_APB2ENR_AFIOEN;
    AFIO->MAPR |= AFIO_MAPR_SWJ_CFG_JTAGDISABLE;
    // Clear PB4 and PB5 control register bits
    GPIOB->CRL &= ~(GPIO_CRL_MODE4 | GPIO_CRL_CNF4 |
                    GPIO_CRL_MODE5 | GPIO_CRL_CNF5);
    // Configure PB.4 and PB.5 as Push Pull output at max 10Mhz
    GPIOB->CRL |= GPIO_CRL_MODE4_0 | GPIO_CRL_MODE5_0;
    TIM2->PSC = SystemCoreClock / 1000 - 1; // 1000 tick/sec
    TIM2->ARR = 1000;  // 1 Interrupt/1 sec
    TIM2->DIER |= TIM_DIER_UIE; // Enable tim2 interrupt
    TIM2->CR1 |= TIM_CR1_CEN;   // Start count
    NVIC_EnableIRQ(TIM2_IRQn);  // Enable IRQ
    while(1); // Infinity loop
}
void TIM2_IRQHandler(void)
{
    TIM2->SR &= ~TIM_SR_UIF; //Clean UIF Flag
    if (1 == (i++ & 0x1)) {
        GPIOB->BSRR = GPIO_BSRR_BS4;   // Set PB4 bit
        GPIOB->BSRR = GPIO_BSRR_BR5;  // Reset PB5 bit
    } else {
        GPIOB->BSRR = GPIO_BSRR_BS5;   // Set PB5 bit
        GPIOB->BSRR = GPIO_BSRR_BR4;    // Reset PB4 bit
    }
}

Хоть мы подключали библиотек SPL, тут она использована не была. Все обращения к полям вроде RCC->APB2ENR полностью описаны в CMSIS.

Можно выполнять Project -> Build All. Если все получилось, то в папке Debug проекта должен появится файл stm32_ld_vl.hex. Он был автоматически сгенерирован из elf встроенными инструментами. Прошиваем файл и видим как мигают светодиоды с частотой раз в секунду:

vsprog -sstm32f1 -ms -oe -owf -I /home/user/workspace/stm32_ld_vl/Debug/stm32_ld_vl.hex -V "tvcc.set 3300"

Естественно вместо /home/user/workspace/ вы должны вписать свой путь к workspace. 

Для STM32VLDiscovery

Код немного отличается от того, который я дал выше для своей отладочной платки. Отличие заключается в пинах, на которых "висят" светодиоды. Если у меня в плате это были PB4 и PB5, то в Discovery это PC8 и PC9.

#include "stm32f10x.h"
uint8_t i=0;
int main(void) 
{
  RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;  // Enable PORTC Periph clock
  RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;  // Enable TIM2 Periph clock
  // Clear PC8 and PC9 control register bits
  GPIOC->CRH &= ~(GPIO_CRH_MODE8 | GPIO_CRH_CNF8 |
          GPIO_CRH_MODE9 | GPIO_CRH_CNF9);
  // Configure PC8 and PC9 as Push Pull output at max 10Mhz
  GPIOC->CRH |= GPIO_CRH_MODE8_0 | GPIO_CRH_MODE9_0;
  TIM2->PSC = SystemCoreClock / 1000 - 1; // 1000 tick/sec
  TIM2->ARR = 1000;  // 1 Interrupt/sec (1000/100)
  TIM2->DIER |= TIM_DIER_UIE; // Enable tim2 interrupt
  TIM2->CR1 |= TIM_CR1_CEN;   // Start count
  NVIC_EnableIRQ(TIM2_IRQn);  // Enable IRQ
  while(1); // Infinity loop
}
void TIM2_IRQHandler(void)
{
  TIM2->SR &= ~TIM_SR_UIF; //Clean UIF Flag
  if (1 == (i++ & 0x1)) {
    GPIOC->BSRR = GPIO_BSRR_BS8;   // Set PC8 bit
    GPIOC->BSRR = GPIO_BSRR_BR9;   // Reset PC9 bit
  } else {
    GPIOC->BSRR = GPIO_BSRR_BS9;   // Set PC9 bit
    GPIOC->BSRR = GPIO_BSRR_BR8;   // Reset PC8 bit
  }
}

Под Windows, прошить полученный hex(/workspace/stm32_md_vl/Debug/stm32_md_vl.hex) можно утилитой от ST.

Ну а под linux утилитой st-flash. НО!!! Утилита не хавает hex формата Intel HEX (который генерируется по дефолту), поэтому крайне важно в настройках создания Flash-образа, выбрать формат binary:

armformathex.jpg

Расширение файла при этом не поменяется (останется hex как и было), но формат файла изменится. И только после этого можно выполнять:

st-flash write v1 /home/user/workspace/stm32_md_vl/Debug/stm32_md_vl.hex 0x08000000

Кстати, на счет расширения и формата: обычно бинарные файлы помечают расширением .bin, в то время как файлы формата Intel HEX именуют расширением .hex. Отличие в этих двух форматах скорей техническое, чем функциональное: бинарный формат содержит просто байты инструкций и данных, которые будут просто записываться в контроллер программатором "как есть". IntelHEX же имеет не бинарный формат, а текстовый: точно те же байты разбиты по 4 бита и представлены посимвольно в формате ASCII, причем использованы только символы 0-9, A-F (bin и hex - системы счисления с кратными основаниями, то есть 4 бита в bin можно представить одной цифрой в hex). Так что формат ihex более чем в 2 раза превышает размер обычного бинарного файла (каждые 4 бита заменяются байтом + переносы строк для удобного чтения), но его можно читать в обычном текстовом редакторе.  Поэтому, если вы собираетесь отправить этот файл кому-то, или использовать его в других программах-программаторах, то желательно переименовать его в stm32_md_vl.bin, дабы не вводить в заблуждение тех, кто будет смотреть на его имя.

Итак мы настроили сборку прошивки для stm32. В следующий раз я расскажу как настроить отладку в eclipse.