Во второй части мы создали простой проект, в котором написали немного кода. Я нарочно не вдавался в описание этого кода, так как к процессу настройки рабочей среды это отношения не имеет. В этой статье я восполню недосказанное а также расскажу чем отличается CMSIS и SPL. Обе библиотеки являются открытыми, по этому мы имеем замечательную возможность посмотреть "из чего они сделаны".

Перед тем как рассмотреть пример, в котором мигают светодиоды, взглянем внутрь библиотеки CMSIS, которая использована при его написании.

Указатели на структуры в CMSIS

В заголовочном файле stm32f10x.h объявлены указатели на структуры и сами структуры, соответствующие определенным периферийным модулям. Например рассмотрим объявление указателя на структуру модуля Reset and clock control (RCC):

#define RCC     ((RCC_TypeDef *) RCC_BASE)

В свою очередь RCC_TypeDef это сама структура, которая отображена на адресное пространство по адресу RCC_BASE:

typedef struct
{
  __IO uint32_t CR;
  __IO uint32_t CFGR;
  __IO uint32_t CIR;
  __IO uint32_t APB2RSTR;
  __IO uint32_t APB1RSTR;
  __IO uint32_t AHBENR;
  __IO uint32_t APB2ENR;
  __IO uint32_t APB1ENR;
  __IO uint32_t BDCR;
  __IO uint32_t CSR;
#ifdef STM32F10X_CL  
  __IO uint32_t AHBRSTR;
  __IO uint32_t CFGR2;
#endif /* STM32F10X_CL */ 
#if defined (STM32F10X_LD_VL) || defined (STM32F10X_MD_VL) || defined (STM32F10X_HD_VL)   
  uint32_t RESERVED0;
  __IO uint32_t CFGR2;
#endif /* STM32F10X_LD_VL || STM32F10X_MD_VL || STM32F10X_HD_VL */ 
} RCC_TypeDef;

В структуре объявлены 32-х битные поля, соответствующие регистрам периферийного модуля. Регистры, которые есть только в конкретных линейках, заключены в ifdef-ы, таким образом они попадут в структуру только в том случае, если будет задефайнено название линейки в устройствах которой они присутствуют.

Сам адрес RCC_BASE это тоже define, разворачиваемый с помощью других defino-в:

#define RCC_BASE          (AHBPERIPH_BASE + 0x1000)
...
#define AHBPERIPH_BASE    (PERIPH_BASE + 0x20000)
...
#define PERIPH_BASE        ((uint32_t)0x40000000) /*!< Peripheral base address in the alias region */

Что бы проверить согласованность библиотеки с документацией откройте справочное руководство (Reference manual) "RM0041.  STM32F100xxadvanced ARM-based 32-bit MCUs". В разделе 2.3 Memory map, в таблице мы видим, что периферийный модуль Reset and clock control  занимает кусок адресного пространства с 0x40021000 до 0x400213FF. Собственно если вычислить дефайны:  0x40000000 + 0x20000 + 0x1000, то как раз таки получается  0x40021000. 

Теперь о самих регистрах: к примеру возьмем регистр APB2ENR, отвечающий за включение тактирования разных периферийных модулей на шине APB2 (раздел 6.3.7 того же документа). Как видно адрес регистра "Address: 0x18". Под адресом регистра, тут понимается смещение в байтах относительно начала базового адреса периферийного модуля, то есть относительно 0x40021000. Фактический же адрес будет равен  0x40021000 + 0x18 = 0x40021018. Теперь взглянем на структуру RCC_TypeDef: как мы видим перед полем APB2ENR есть еще 6 штук полей размером в 32бита (тип uint32_t). Каждое поле занимает 4 байта, а всего их 6. Следовательно смещение регистра APB2ENR от начала структуры равно 6 * 4 = 24 = 0х18, что как раз таки и наблюдается в RM.

В конечном счете теперь если мы выполним запись

RCC->APB2ENR = 0x12345678

То по адресу 0x40021018 запишется 4-х байтное значение 0x12345678.

Вывод: каждый периферийный модуль из Reference Manual соответствует указателю на структуру в библиотеке CMSIS. Поля структуры отвечают за регистры этого периферийного модуля.

Определения битов регистров в CMSIS

Продолжим чтение Reference Manual. В разделе 6.3.7 есть табличка, показывающая названия битов:

APB2ENR(1).jpg

Чуть ниже расписано за что какой бит отвечает. Например что бы затактировать I/O port B нужно установить бит №3 называемый IOPBEN. Что бы взвести третий бит в регистре достаточно выполнить or регистра с двоичной константой 0b1000 (что равно шестнадцатеричной константе 0x8). То есть:

RCC->APB2ENR |= 0x8;
RCC->APB2ENR |= 0b1000; //Или так
RCC->APB2ENR |= (1 << 3); //А так вообще шикарно

Однако представьте сколько нужно было бы потратить времени, что бы искать эти все номера битов в Reference Manual, и сколько бы ошибок можно было бы при этом допустить. По этому в библиотеке CMSIS предусмотрены define для битов:

#define  RCC_APB2ENR_IOPBEN   ((uint32_t)0x00000008)       /*!< I/O port B clock enable */

Еще и комментарий есть, соответствующий описанию из RM. Так что если вам не понятена какая-нибудь константа, и вы работаете в Eclipse, вы можете с нажатой клавишей Ctrl кликнуть по константе, таким образом вы перейдете к ее определению.

GPIO и таймер на CMSIS

Рассмотрим код, используемый в тестовом проекте. 

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

Для Discovery:

#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 PC.8 and PC.9 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
  }
}

Как я уже говорил, этот код не использует ни капли SPL. Все обращения к регистрам осуществляются исключительно с помощью CMSIS. В первой строке кода подключается главный модуль CMSIS. Во второй строке объявляется обычная байтовая переменная. В третей строке объявлена функция main. Именно она начинает выполнятся после сброса контроллера (который всегда происходит при подаче питания), а также после выполнения некоторого инициализирующего кода. В четвертой строке включается тактирование модуля ввода вывода I/O Port B(Или Port C для Discovery). На ногах порта этого модуля висят светодиоды (PB4 и PB5 в моей демоплатке и PC8 PC9 в Discovery). В пятой строке включается тактирование таймера TIM2. Этот таймер "подвешан" на шину APB1, по этому тут изменяется регистр RCC->APB1ENR, а не RCC->APB2ENR.

В 7ой и 8ой строках выключается функция JTAG. Дело в том, что нога PB4 после сброса контроллера выполняет функцию JTAG NJTRST. Это видно на 28ой странице в даташите "STM32F100x4 STM32F100x6STM32F100x8 STM32F100xB".

PB4ds(1).jpg

Такая особенность весьма логична, поскольку интерфейс JTAG часто используется для прошивки контроллеров и он должен быть включен "по умолчанию". Но мы используем удобный SWD а пин от JTAG можно использовать как обычный пин ввода/вывода, но для этого нужно выполнить Remap альтернативной функции. Что бы это сделать в строке 7 включается тактирование модуля "Alternate function I/O", а в 8ой собственно выключается отладочный порт JTAG-DP (Debug Port) а порт SW-DP при этом остается включенным. Для Discavery этот код особо не нужен - светодиоды там и так висят на других пинах, но все же для освобождения PB4 я этот код оставил и там.

В 10ой и 11ой строках выполняется очистка (сброс 0) конфигурационных бит в регистре Configuration Register Low. Очищаются пары бит CNFy и MODEy, где y - номер пина в порте. В 13 строке нужные биты взводятся в 1 что бы выбрать нужный режим работы ног порта. В результате настройка пинов 4 и 5 будет выполнена таким образом (скриншот из RM0041 на стр. 110):

gpiobcrl(2).jpg

Как видно выбран режим ног "на выход", который может менять свои состояния с частотой до 10 МГц и работает в режиме Push-pull (Двухтактный выход). Это тип вывода реализованного на двух  транзисторах - открытие одного приводит к коммутации VDD на выход, открытие другого  к коммутации VSS на выход. 

Что касается самого кода, то вот тут мы столкнулись с одним неудобством чистой библиотеки CMSIS: она предоставляет нам определения битов, а не режимов с помощью которых эти биты устанавливаются. То есть, например, что бы выбрать режим Output mode, max speed 10 MHz на 4ом пине, мне пришлось заглянуть в даташит и понять что мне нужно установить бит Mode4[1] в 0, а бит Mode4[0] в 1, и только после этого я смог написать участок кода вроде:

GPIOB->CRL &= ~GPIO_CRL_MODE4; //clear mode4[1] and mode4[0] bit
GPIOB->CRL |= GPIO_CRL_MODE4_0;  //set mode4[0] bit

Однако продолжим рассмотрение кода. В строке 14 у нас начинается настройка таймера, а именно запись в регистр предделителя (Prescaler).  Таймер тактируется от шины, работающей на частоте ядра, по этому при расчете предделителя нужно исходить из тактовой частоты, на которой работает контроллер. По умолчанию он стартует на частоте 24 МГц. Это максимально возможная частота в линейке STM32F100 Value Line, получаемая путем умножения частоты внешнего 8 МГц кварцевого резонатора (или генератора). Если библиотека не сможет инициализировать внешний источник импульсов, то она сама выберет внутреннюю цепочку на 8 МГц. В любой момент времени тактовую частоту в Гц можно получить из переменной SystemCoreClock. В нашем случае мы конечно знаем, что она будет равна 24000000, но лучше всегда использовать эту переменную, так как частоту потом можно поменять. В предделитель мы записываем значение 24000-1, и в результате таймер будет тикать с частотой 1000 тиков за секунду. В 15-ой строке настраивается значение Auto-reload Register, то есть через сколько тиков нужно перезагрузить таймер и вызвать прерывание таймера. Таким образом, тикая с частотой 1000 тиков/с, таймер выполнит 1000 тиков ровно за одну секунду и следовательно период вызова прерываний таймера будет составлять 1 с. В 16 строке разрешается прерывание от таймера, а в 17ой запускается счет. 18ая строка содержит вызов функции глобального разрешения прерываний, в качестве параметра передается номер глобального прерывания (в нашем случае define этого номера, предоставленный CMSIS) . В 19ой строке контроллер уходит в бесконечный цикл.

В 21ой строке объявлен обработчик прерывания TIM2_IRQ. Функция обработчика должна называться строго TIM2_IRQHandler и ни как иначе. В 23ей строке очищается флаг обновления прерывания (update interrupt flag) таймера. Флаг взводится аппаратно при поступлении прерывания и должен быть очищен программно, что мы и сделали. В 24ой строке выполняется проверка на парность значения переменной i и ее постинкримент. В случае непарности PB4 устанавливает на выходе лог.1 (включая светодиод) а PB5 лог.0. В случае парности все наоборот. Для Discovery все аналогично, только вместо PB4 и PB5 тут PC8 и PC9, ну надеюсь вы это уже давно поняли=). 

GPIO и таймер на SPL

Пора переписать код мигания светодиодом на SPL и посмотреть на отличия.

#include "stm32f10x.h"
uint8_t i=0;
int main(void) {
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // Enable PORTB Periph clock
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);  // Enable TIM2 Periph clock
    // Disable JTAG for release LED PIN
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
    GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);
    // Configure PB4 and PB5 as Push Pull output at max 10Mhz
    GPIO_InitTypeDef gpio_port;
    gpio_port.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5;
    gpio_port.GPIO_Mode = GPIO_Mode_Out_PP;  //Push Pull
    gpio_port.GPIO_Speed = GPIO_Speed_10MHz;
    GPIO_Init(GPIOB, &gpio_port);
    GPIO_ResetBits(GPIOB, GPIO_Pin_4 | GPIO_Pin_5); // Turn off leds
    // Timer base configuration
    TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
    TIM_TimeBaseStructure.TIM_Period = 1000;
    TIM_TimeBaseStructure.TIM_Prescaler = (uint16_t) (SystemCoreClock / 1000) - 1;  //1000 Hz
    TIM_TimeBaseStructure.TIM_ClockDivision = 0;
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
    TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
    TIM_Cmd(TIM2, ENABLE);
    //Enable IRQ
    NVIC_InitTypeDef NVIC_InitStructure;
    NVIC_InitStructure.NVIC_IRQChannel =  TIM2_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
    while(1); // Infinity loop
}
void TIM2_IRQHandler(void)
{
    TIM_ClearITPendingBit(TIM2, TIM_SR_UIF);
    if (1 == (i++ & 0x1)) {
        GPIO_WriteBit(GPIOB, GPIO_Pin_4, Bit_SET);
        GPIO_WriteBit(GPIOB, GPIO_Pin_5, Bit_RESET);
    } else {
        GPIO_WriteBit(GPIOB, GPIO_Pin_5, Bit_SET);
        GPIO_WriteBit(GPIOB, GPIO_Pin_4, Bit_RESET);
    }
}

И сразу же для Discovery:

#include "stm32f10x.h"
uint8_t i=0;
int main(void) {
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); // Enable PORTC Periph clock
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);  // Enable TIM2 Periph clock
  // Disable JTAG for release LED PIN
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
  GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);
  // Configure PC8 and PC9 as Push Pull output at max 10Mhz
  GPIO_InitTypeDef gpio_port;
  gpio_port.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;
  gpio_port.GPIO_Mode = GPIO_Mode_Out_PP;  //Push Pull
  gpio_port.GPIO_Speed = GPIO_Speed_10MHz;
  GPIO_Init(GPIOC, &gpio_port);
  GPIO_ResetBits(GPIOC, GPIO_Pin_8 | GPIO_Pin_9); // Turn off leds
  // Timer base configuration
  TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
  TIM_TimeBaseStructure.TIM_Period = 1000;
  TIM_TimeBaseStructure.TIM_Prescaler = (uint16_t) (SystemCoreClock / 1000) - 1;  //1000 Hz
  TIM_TimeBaseStructure.TIM_ClockDivision = 0;
  TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
  TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
  TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
  TIM_Cmd(TIM2, ENABLE);
  //Enable IRQ
  NVIC_InitTypeDef NVIC_InitStructure;
  NVIC_InitStructure.NVIC_IRQChannel =  TIM2_IRQn;
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  NVIC_Init(&NVIC_InitStructure);
  while(1); // Infinity loop
}
void TIM2_IRQHandler(void)
{
  TIM_ClearITPendingBit(TIM2, TIM_SR_UIF);
  if (1 == (i++ & 0x1)) {
    GPIO_WriteBit(GPIOC, GPIO_Pin_8, Bit_SET);
    GPIO_WriteBit(GPIOC, GPIO_Pin_9, Bit_RESET);
  } else {
    GPIO_WriteBit(GPIOC, GPIO_Pin_9, Bit_SET);
    GPIO_WriteBit(GPIOC, GPIO_Pin_8, Bit_RESET);
  }
}

Первое отличие замечаем в 4ой строке: вместо обычной записи в поле APB2ENR мы вызываем какую-то функцию RCC_APB2PeriphClockCmd. Зачем? Зажмем Ctrl и нажмем на заголовок функции, чтобы перейти к её определению и посмотреть что там внутри. Откроется заголовочный файл stm32f10x_rcc.c (из папки spl/src), в котором мы увидим:

/**
  * @brief  Enables or disables the High Speed APB (APB2) peripheral clock.
  * @param  RCC_APB2Periph: specifies the APB2 peripheral to gates its clock.
  *   This parameter can be any combination of the following values:
  *     @arg RCC_APB2Periph_AFIO, RCC_APB2Periph_GPIOA, RCC_APB2Periph_GPIOB,
  *          RCC_APB2Periph_GPIOC, RCC_APB2Periph_GPIOD, RCC_APB2Periph_GPIOE,
  *          RCC_APB2Periph_GPIOF, RCC_APB2Periph_GPIOG, RCC_APB2Periph_ADC1,
  *          RCC_APB2Periph_ADC2, RCC_APB2Periph_TIM1, RCC_APB2Periph_SPI1,
  *          RCC_APB2Periph_TIM8, RCC_APB2Periph_USART1, RCC_APB2Periph_ADC3,
  *          RCC_APB2Periph_TIM15, RCC_APB2Periph_TIM16, RCC_APB2Periph_TIM17,
  *          RCC_APB2Periph_TIM9, RCC_APB2Periph_TIM10, RCC_APB2Periph_TIM11     
  * @param  NewState: new state of the specified peripheral clock.
  *   This parameter can be: ENABLE or DISABLE.
  * @retval None
  */
void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState)
{
  /* Check the parameters */
  assert_param(IS_RCC_APB2_PERIPH(RCC_APB2Periph));
  assert_param(IS_FUNCTIONAL_STATE(NewState));
  if (NewState != DISABLE)
  {
    RCC->APB2ENR |= RCC_APB2Periph;
  }
  else
  {
    RCC->APB2ENR &= ~RCC_APB2Periph;
  }
}

В самом начале мы видим красивый Doxygen комментарий, который понятно объясняет для чего нужна функция. В начале вызывается макрос assert_param, который может проверять параметр на валидность, и в случае неправильности выпадает в специальную функцию. Подробнее рассмотрим ниже. После проверки обычный if разруливает человекопонятные определения Enable и Disable. На этом и основан весь принцип библиотеки - добиться как можно большей удобности, лаконичности и понятности. В 10ой строке объявляется структура типа GPIO_InitTypeDef. Она служит для настройки одного или сразу нескольких пинов одного порта. Как видно тут уже даже не надо читать даташит: в 12ой строке мы сразу выбираем режим Push Pull (PP) не задумываясь о битах регистров и даже не зная названия регистра. Структура для таймера тоже позволяет оперировать достаточно понятными названиями а не сокращенными двух-трех-буквенными именами регистров. В строках 26-31 включается прерывание IRQ. Строк кода тут больше, но и возможности увеличились: теперь можно настраивать приоритет прерывания (имеет смысл если их много). После подробного рассмотрения примера на CMSIS остальной код должен быть интуитивно понятен. В любом случае вы можете продолжать исследовать библиотеки, читать комментарии к нужным определениям или функциям, все описано достаточно подробно и понятно.

Контроль правильности входных данных

По умолчанию проверка правильности входных данных выключена и все макросы assert_param ни чего не делают. Что бы использовать проверку необходимо сделать два действия:

1. Выполнить определение символа USE_FULL_ASSERT. (Для этого можно зайти в Project->Properties->C/C++ Build -> Settings -> ARM Sourcery Linux GCC C Compiler и добавить к Defined Symbols символ USE_FULL_ASSERT).

2. Объявить у себя в коде функцию со следующим заголовком:

void assert_failed(uint8_t* file, uint32_t line)
{
  while (1){}
}

В ней и зависнет программа, если вы введете что нибудь не правельно. В переменных file и line вы можете узнать файл и строку, в которой неудачно выполнился assert_param.

Например представим что мы случайно попытались включить тактирование TIM3 на шине APB1:

RCC_APB2PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);

На самом же деле TIM3 подключен к шине APB1 и код не включит тактирование таймера. Такая ошибка возможна всегда так как человеку свойственно ошибаться. Вы не знаете где вы допустили ошибку, но прерывания упорно не вызывается. Придется сделать два действия о которых я написал выше и запустить Debug. Теперь в функции assert_failed на строке с while нужно поставить точку прерывания, и нажать Resume (F8). В результате макро assert_param найдет баг, и программа вывалится в бесконечный while. В переменной file вы увидите строку с именем файла из которого произошел вход в assert_failed:

Таким образом мы видим что это файл stm32f10x_rcc.c , таким образом вы понимаете, что ошибка где-то в настройке RCC, но и это еще не все! В переменной line содержится номер строки, из который произошел выброс. У меня это номер 1098. То есть мы можем зайти в файл stm32f10x_rcc.c, и выполнить Ctrl + L, затем указать номер строки 1098, ОК, и мы увидим что мы ошиблись при вводе параметра RCC_APB2Periph.

Этот метод позволяет выявить очень много ошибок и на практике даже очень распространен. Но есть одно огромное НО! Он способен выявить не все ошибки. Например если вы допустите ошибку

RCC_APB2PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

То assert не сработает. Это объясняется тем что часть периферии имеет одинаковые идентификаторы для обеих шин и RCC_APB1Periph_TIM2 по числовой константе точно также подходит к APB2, только вот включит он не таймер а RCC_APB2Periph_AFIO.

Необъявленые обработчики прерываний

В startup-файле библиотеки CMSIS есть еще одна полезная штука - она умеет сама отлавливать отсутствующие обработчики прерываний.

Если вы включите какой-нибудь IRQ, например для часов реального времени:

  NVIC_InitTypeDef NVIC_InitStructure;
  /* Enable the RTC Interrupt */
  NVIC_InitStructure.NVIC_IRQChannel = RTC_IRQn;
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  NVIC_Init(&NVIC_InitStructure);

а функцию-обработчик void RTC_IRQHandler(void) не объявите, то при запуске, вы тут же окажетесь в бесконечном цикле (строка  с Infinite_Loop):

Default_Handler:
Infinite_Loop:
  b  Infinite_Loop
  .size  Default_Handler, .-Default_Handler

Обнаружить этом можно нажав кнопку Pause в режиме выполнения. Как видим тут все очень просто: при отсутствии нужной функции-обработчика используется дефолтный обработчик, в котором нам просто подсунули бесконечный зависающий цикл, намекающий на наш косяк.

Выводы

Обе библиотеки реализуют уровень HAL (Hardware Abstraction Layer) разрабатываемых на языке C приложений: они дают однородный доступ к периферии микроконтроллеров разных линеек. При этом уровень абстракции SPL выше, и следственно она удобней, и в то же время в некоторых случаях медленней. Библиотека SPL это обертка над библиотекой CMSIS: внутри она настравивает точно те же регистры, но делает еще кучу всяких проверок и универсальных настроек.

Для использования библиотеки SPL должна быть объявлена директива USE_STDPERIPH_DRIVER (которую мы настроили еще во второй части). При этом может использоваться чисто библиотека CMSIS, без структур и функций SPL (как и было в первом примере). Но все же мешать код для разных библиотек не желательно, даже с точки зрения здравой логики.