Assert param error

При отладке программы очень удобно для вывода сообщений об той или иной ситуации, иметь универсальную функцию, которая позволяла бы выводить сообщение об ошибке, номер строки и файл в котором эта о…

При отладке программы очень удобно для вывода сообщений об той или иной ситуации, иметь универсальную функцию, которая позволяла бы выводить сообщение об ошибке, номер строки и файл в котором эта ошибка возникла. Для этого, в библиотеке периферии от ST, используется assert_param. 

Перед тем как использовать данную функцию необходимо указать, что мы ее используем:

#define USE_FULL_ASSERT

И создать функцию обработчик:

void assert_failed(uint8_t* file, uint32_t line)
{
  debug_printf("Wrong parameters value: file %s on line %drn", file, (int)line);
}

Пример использования:

assert_param(TESTING(test));

#define TESTING(PAR) (((PAR) == 10) || 
                      ((PAR) == 11) || 
                      ((PAR) == 12))

uint8_t tmp = 13;

assert_param(TESTING(tmp));
assert_param(tmp == 13);

На самом деле assert_param представляет собой:

assert_param(expr) ((expr) ? (void)0 : assert_failed((uint8_t *)__FILE__, __LINE__))

Или в более простой записи:

if(expr) 
  продолжаем программу
else
  assert_failed

expr — это выражение, которое передается в assert_param(expr)assert_param(x > 10).


В этой публикации я попытаюсь акцентировать внимание на основных моментах для быстрого начала работы с микроконтроллерами STM32F10x на основе библиотеки стандартной периферии от компании-производителя STMicroelectronics.

В качестве среды разработки в статье будет использоваться Eclipse CDT. Поскольку основное внимание будет сосредоточено вокруг программного кода, то вы можете спокойно проделать все манипуляции в Code::Blocks.

Общая структура проекта для ARM микроконтроллеров описана в моей статье «Программирование AVR и ARM микроконтроллеров в Eclipse. Часть 2».

Здесь я коротко напомню, что для сборки проекта для ARM — микроконтроллеров (STM32F10x в частности) понадобится скрипт компоновщика и C-Startup файл.

Скрипт компоновщика представляет собой файл с инструкциями по размещению кода программы и данных в памяти микроконтроллера. Он может скомандовать загрузить код вашей программы в Flash -память программ или SRAM -память данных.

Для микроконтроллеров с различным объемом памяти программ и данных необходимы разные скрипты компоновки. Их можно достать у производителя микроконтроллеров — компании STMicroelectronics.
Распакуйте из архива ARM_Toolchain/Lib/stm32f10x_stdperiph_lib.zip библиотеку STM32F10x standard peripheral library.
В ней имеются примеры проектов для различных сред разработки ( IAR EWB, Keil uVision, Atollic True Studio и т.д). Наиболее близким для нас является Atollic True Studio, поскольку представляет собой модификацию Eclipse.
Зайдите в каталог Project/StdPeriph_Template/TrueSTUDIO, там есть несколько подкаталогов, названия которых соответствуют названиям отладочных плат STM3210x-EVAL.

Узнайте, в какой из этих плат используется микроконтроллер той же линейки, что и ваш. Скопируйте файл stm32_flash.ld из соответствующего каталога в свой проект.

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

Стартовый код (C-Startup) для микроконтроллеров STM32 может быть написан на С или Assembler.
Хотя библиотеку STM32F10x Standard Peripheral Library (далее по тексту используется сокращение STM32F10x SPL) часто критикуют за наличие ошибок, все же для начала программирования под STM32 использование этой библиотеки предоставляет самый простой способ быстро приступить к работе.
Но всегда хочется, чтобы была какая-то альтернатива. На самом деле их множество, например, программировать на языке ассемблера 🙂 .

Это самый тяжелый и бессмысленный путь. Второй способ — использовать библиотеку CMSIS, которая предоставляет синтаксис обращения к структурам языка С для доступа к различной периферии микроконтроллера. Самым простым и логичным способом (на мой взгляд) является использование библиотек.

Если вы категорически настроены против STM32F10x SPL, то специально для вас имеется еще одна альтернатива — библиотека libopencm3. В ней основное количество примеров сосредоточено вокруг основной серии микроконтроллеров STM32F10x , но появление примеров для других серий ( STM32F2xx/4xx) является только вопросом времени. Вы всегда можете присоединиться к проекту libopencm3 и ускорить этот процесс.

Стандарт CMSIS также является не обязательным для применения в ваших программах.
Можно обойтись и без него, потратив некоторые усилия и время для реализации HAL ( Hardware Abstraction Layer ) уровня на языке программирования С.

Такой способ может оказаться в некоторых случаях единственным доступным способом. Например, ваша организация использует заказные микросхемы на основе разработанных компанией ARM вычислительных ядер и специфической для отрасли периферией.

Или вам необходимо реализовать программное обеспечение на языке С для микроконтроллеров с ядром ARM9, для которого производители ориентируются на использование готовых операционных систем (Linux, QNX, Windows CE), поэтому библиотек для программирования на языке С в чистом виде или в сочетании с более легковесной RTOS производители могут не предоставлять.

К счастью производители микроконтроллеров на основе ядра Cortex-M3 предоставляют в распоряжение разработчиков большое количество библиотек кода. Это касается и микроконтроллеров STM32.
Продолжим рассмотрение библиотеки STM32F10x SPL. Рассматривать ее будем на примере stm32f10xQuickstart.
Вы можете открыть этот пример или же создать свой проект «с нуля», чтобы лучше осознать весь процесс происходящего.

Для второго случая я перечислю список необходимых шагов :

  • Создать в Eclipse новый пустой проект
  • Скопировать в проект скрипт компоновки и стартовый файл
  • Создать новый или скопировать шаблонный Makefile
  • При использовании в качестве шаблона Makefile из моего примера необходимо создать внутри проекта каталоги src, inc, bin, obj , внутри каталогов bin и obj создать подкаталоги Debug и Release.
  • Скопировать необходимые исходные и заголовочные файлы из библиотек CMSIS и STM32F10x SPL.
  • Внести необходимые изменения в секции настроек пользователя шаблонного Makefile, если он используется.
  • Создать в окне Eclipse “make target ” новые цели “Debug”, “cleanDebug”, “Release”, “cleanRelease”, “Program”.
  • Запустить на выполнение цель «Debug» и проследить за ее выполнением в окне «Console».

Для лучшего понимания материала я разбил статью на несколько независимых параграфов, в каждом из которых описывается только какой-то один аспект работы с библиотекой STM32F10x SPL.

Конфигурирование STM32F10x SPL с помощью макроопределений


Для конфигурирования библиотеки используются предопределенные значения макросов, которые мы сейчас и рассмотрим.
Их можно задать внутри заголовочных файлов с помощью директивы препроцессора #define или же передать значения макроопределений через ключ -D компилятора GCC.
В своем примере я использую второй способ.
В Makefile переменная DEFINE содержит макросы, необходимые для компиляции библиотеки STM32F10x SPL.
Макроопределение STM32F10X_MD задает принадлежность используемого микроконтроллера к линейке Medium-density.
Сюда входят микроконтроллеры с объемом Flash-памяти от 64 до 128кБ .
В следующей таблице перечислены названия макросов для разных серий микроконтроллеров :

Наименование серии Макрос Описание
Low density Value line STM32F10X_LD_VL микроконтроллеры серии STM32F100xx с объемом Flash-памяти 16 — 32кБ
Low density STM32F10X_LD микроконтроллеры серии STM32F101xx, STM32F102xx, STM32F103xx
с объемом Flash-памяти 16 — 32кБ
Medium density Value line STM32F10X_MD_VL микроконтроллеры серии STM32F100xx с объемом Flash — памяти
64 — 128кБ
Medium-density STM32F10X_MD микроконтроллеры серии STM32F101xx, STM32F102xx, STM32F103xx с объемом Flash- памяти 64 — 128кБ
High density Value line STM32F10X_HD_VL микроконтроллеры серии STM32F100xx с объемом
Flash — памяти 256 — 512кБ
High density STM32F10X_HD микроконтроллеры серии STM32F101xx, STM32F103xx с объемом
Flash- памяти 256 — 512кБ
XL-density STM32F10X_XL микроконтроллеры серии STM32F101xx, STM32F103xx с объемом
Flash- памяти 512 — 1024кБ
Connectivity line STM32F10X_CL микроконтроллеры серии STM32F105xx и STM32F107xx

Для задания тактовой частоты микроконтроллера необходимо раскомментировать в файле system_stm32f10x.c макрос с необходимым значение тактовой частоты.

#if defined (STM32F10X_LD_VL) || (defined STM32F10X_MD_VL) || (defined STM32F10X_HD_VL)

/* #define SYSCLK_FREQ_HSE    HSE_VALUE */

#define SYSCLK_FREQ_24MHz  24000000

#else

/* #define SYSCLK_FREQ_HSE    HSE_VALUE */

/* #define SYSCLK_FREQ_24MHz  24000000 */

/* #define SYSCLK_FREQ_36MHz  36000000 */

/* #define SYSCLK_FREQ_48MHz  48000000 */

/* #define SYSCLK_FREQ_56MHz  56000000 */

#define SYSCLK_FREQ_72MHz  72000000

#endif

Предполагается использование кварцевого резонатора с частотой 8МГц для всех основных
серий микроконтроллеров, кроме Connectivity line, для которой необходимо установить кварцевый резонатор на 25МГц.
Если вы используете кварцевые резонаторы с другими значениями частоты, то необходимо изменить значение макроса HSE_VALUE в заголовочном файле stm32f10x.h и адаптировать соответствующим образом все зависимые функции.
Назначение макроса USE_STDPERIPH_DRIVER нетрудно догадаться — использовать библиотеку STM32F10x standard peripheral library.
USE_FULL_ASSERT – использовать макрос ASSERT для отладки программы.

Использование макроса assert_param в библиотеке


Все функции библиотеки STM32F10x SPL используют для проверки своих аргументов макрос assert_param.
Этот макрос выполняет проверку выражения с участием проверяемого аргумента функции на равенство нулю. Если значение выражения равно нулю, то вызывается функция-обработчик ошибки аргумента assert_failed, в противном случае (выражение не равно нулю) проверка аргумента проходит успешно.
В своей программе вам необходимо реализовать функцию assert_failed.
В ней выводится сообщение об ошибке , название файла и номер строки кода, вызвавшей ошибку.
Макрос debug_printf может осуществлять вывод через USART c помощью стандартной библиотеки new_lib или, например, библиотеки от мистера Чена.

#define debug_printf xprintf /* printf */

#ifdef  USE_FULL_ASSERT

void assert_failed(uint8_t* file, uint32_t line)

{

debug_printf(«Wrong parameters value: file %s on line %drn», file, (int)line);

while (1)

{

}

}/* assert_failed */

#endif/*USE_FULL_ASSERT*/

Реализованная в вашем коде функция assert_failed используется только когда объявлен макрос USE_FULL_ASSERT. В противном случае весь отладочный код исключается из исходника. Этот функционал реализован в заголовочном файле настроек библиотеки драйверов stm32f10x_conf.h.

#ifdef  USE_FULL_ASSERT

  #define assert_param(expr) ((expr) ? (void)0 : assert_failed((uint8_t *)__FILE__, __LINE__))

  void assert_failed(uint8_t* file, uint32_t line);

#else

  #define assert_param(expr) ((void)0)

#endif /* USE_FULL_ASSERT */

Тут объяснять особо нечего. Лучше рассмотрим пример использования assert_param.

void set_param(uint8_t * param, uint8_t value)

{

assert_param( param != NULL);

*param = value;

}/*set_param*/

Функция устанавливает значение параметра через указатель, передаваемый в качестве аргумента. Если макрос USE_FULL_ASSERT не объявлен, то можно считать , что строчки
assert_param( param != NULL) в коде просто нет, иначе в этом определении происходит проверка параметра.
Если указатель не определен, то значение param != NULL будет ложно и будет запущена функция assert_failed , которая выведет через USART название файла и номер строки с ошибкой, после чего зациклится, тем самым предотвратив присвоение значения по неопределенному адресу в памяти.
Вы совсем не обязаны использовать макрос assert_param в своем коде, но в коде библиотеки
STM32F10x SPL он используется везде.
Функцию set_param можно реализовать с проверкой ошибок аргумента без применения assert_param.

#define ERROR (-1)

#define OK (0)

int set_param(uint8_t * param, uint8_t value)

{

int r = ERROR;

if ( param == NULL)

  return r;

*param = value;

r = OK;

return r;

}/*set_param*/

C-Startup файл в библиотеке STM32F10x SPL


В стартовом коде выполняется первичная инициализация микроконтроллера, настраивается стек, выполняется обнуление секции BSS, происходит вызов основной функции main().
Прямого отношения к библиотеке STM32F10x SPL стартовый код не имеет. Однако в этом загрузочном коде перед вызовом основной функции main() программы вызывается функция инициализации микроконтроллера SystemInit(), что входит в состав CMSIS.
Его можно без труда отыскать в библиотеке CMSIS.
Зайдите в каталог Libraries/CMSIS/CM3/DeviceSupport/ST/STM32F10x/startup/TrueSTUDIO и скопируйте нужный файл. Осталось лишь выяснить к какой линейке принадлежит используемый в вашем проекте микроконтроллер.
Для этого просмотрите следующую таблицу :

Наименование серии Название файла Описание
Low density Value line startup_stm32f10x_ld_vl.s микроконтроллеры серии STM32F100xx с объемом
Flash- памяти 16 — 32кБ
Low density startup_stm32f10x_ld.s микроконтроллеры серии STM32F101xx, STM32F102xx, STM32F103xx
с объемом Flash- памяти 16 — 32кБ
Medium density Value line startup_stm32f10x_md_vl.s микроконтроллеры серии STM32F100xx
с объемом Flash- памяти 64 — 128кБ
Medium-density startup_stm32f10x_md.s микроконтроллеры серии STM32F101xx, STM32F102xx, STM32F103xx
с объемом Flash- памяти 64 — 128кБ
High density Value line startup_stm32f10x_hd_vl.s микроконтроллеры серии STM32F100xx
с объемом Flash- памяти 256 — 512кБ
High density startup_stm32f10x_hd.s микроконтроллеры серии STM32F101xx, STM32F103xx
с объемом Flash- памяти 256 — 512кБ
XL-density startup_stm32f10x_xl.s микроконтроллеры серии STM32F101xx, STM32F103xx
с объемом Flash- памяти 512 — 1024кБ
Connectivity line startup_stm32f10x_cl.s микроконтроллеры серии STM32F105xx и STM32F107xx

В стартап-файле прописаны названия обработчиков векторов прерываний и исключений, однако реализован только обработчик вектора сброса, внутри которого и производится вся начальная инициализация перед вызовом функции main().
Реализация всех остальных обработчиков исключений возложена на программиста приложения. Если в вашей программе не используется ни один обработчик, то и нет нужды их прописывать. В случае возникновения исключения будет использован обработчик по-умолчанию — зацикливание кода программы.

Состав библиотеки CMSIS


Как было уже написано ранее в этой публикации библиотека CMSIS предоставляет доступ к периферийным модулям микроконтроллера с помощью элементов структур языка C.
Реализация этой библиотеки делится на две части. Первая часть обеспечивает доступ к периферии ядра Cortex-M3 , а вторая — к периферии конкретной модели микроконтроллера.
Поскольку стандарт CMSIS единый для всех микроконтроллеров с ядром Cortex-M3, то реализация первой части будет одинакова у всех производителей, а вот вторая часть у каждого производителя будет своей.
В состав CMSIS входят несколько заголовочных и исходных файлов . К первой части относятся файлы :

  • core_cm3.h
  • core_cm3.c

Вторая часть CMSIS включает в себя С-Startup файл, а также файлы :

  • stm32f10x.h
  • system_stm32f10x.h
  • system_stm32f10x.c

Заголовочный файл stm32f10x.h содержит макроопределения для доступа к периферийным модулям микроконтроллеров stm32f10x.
В файлах system_stm32f10x.h и system_stm32f10x.c реализована начальная инициализация микроконтроллера.

Состав и конфигурация библиотеки STM32F10x SPL


Библиотека состоит из одноименных с периферийными модулями исходных и заголовочных файлов с префиксом stm32f10x_ .
Например, реализация взаимодействия с модулем USART содержится в файлах stm32f10x_usart.h и stm32f10x_usart.c .
Существуют соглашения об именах элементов библиотеки и определенные правила кодирования , которые описаны в документации .
Библиотека содержит реализацию драйверов периферийных модулей микроконтроллера.
В названиях элементов библиотеки используются такие акронимы для модулей периферии:

Акроним Периферийный модуль
ADC аналогово-цифровой преобразователь
BKP регистры резервного копирования
CAN интерфейс CAN
CEC контроллер потребления
CRC модуль расчета контрольной суммы
DAC цифро-аналоговый преобразователь
DBGMCU отладка микроконтроллера
DMA контроллер прямого доступа к памяти
EXTI контроллер внешних прерываний
FSMC контроллер внешней памяти
FLASH Flash-память программ
GPIO порты ввода/вывода общего назначения
I2C интерфейс I2C
I2S интерфейс I2S (Sound)
IWDG независимый сторожевой таймер
NVIC контроллер вложенных прерываний
PWR контроллер питания
RCC контроллер сброса и тактирования
RTC контроллер реального времени (часы)
SDIO интерфейс SDIO
SPI интерфейс SPI
SysTick системный таймер
TIM базовый таймер или таймер с расширенными возможностями
USART универсальный последовательный синхронно-асинхронный
приемо-передатчик
WWDG оконный сторожевой таймер

На основе этих акронимов формируются названия программных модулей библиотеки. Вам не обязательно использовать все модули библиотеки.
Для того, чтобы использовать в проекте только необходимые модули , библиотеку нужно сконфигурировать.
Для этих целей в каждом проекте, который использует библиотеку STM32F10x SPL, должен быть заголовочный файл stm32f10x_conf.h.

#include «stm32f10x_gpio.h»

//#include «stm32f10x_i2c.h»

//#include «stm32f10x_iwdg.h»

//#include «stm32f10x_pwr.h»

#include «stm32f10x_rcc.h»

Для включения нужного модуля необходимо раскомментировать директиву #include с соответствующим заголовочным файлам.
Заголовочный файл stm32f10x_conf.h включен в stm32f10x.h, поэтому для использования функций библиотеки STM32F10x SPL достаточно подключить в своем исходнике только один заголовочный файл stm32f10x.h

// в файле  stm32f10x.h

#ifdef USE_STDPERIPH_DRIVER

  #include «stm32f10x_conf.h»

#endif

Повторюсь, что в проекте также должны быть определены макросы USE_STDPERIPH_DRIVER, USE_FULL_ASSERT и макрос , задающий серию используемого микроконтроллера (например STM32F10X_MD для линейки Medium density ).
Если вы используете стандартное значение частоты кварцевого резонатора и контроллер будет работать на максимальной тактовой частоте 72МГц , то ничего больше изменять не придется.
В Makefile необходимо добавить список компилируемых файлов библиотеки.
Например :

SRC += stm32f10x_rcc.c

SRC += stm32f10x_gpio.c

Использование библиотеки STM32F10x SPL. Механизмы работы


Для начала программирования с использованием библиотеки периферии проще всего смотреть в примеры, поставляемые в комплекте с библиотекой. Но все же для понимания кода этих примеров необходимо иметь базовые знания по синтаксису и использованию библиотеки.
Все перечисленные ранее периферийные модули микроконтроллера изначально являются дезактивированными, тактовый сигнал на них не подается и электроэнергии они не потребляют.
Для использования периферийного модуля в первую очередь необходимо подать на него тактовый сигнал. Подачу тактового сигнала обеспечивает модуль тактирования и сброса RCC.
Для этих целей в библиотеке существуют такие функции :

RCC_AHBPeriphClockCmd(RCC_AHBPeriph_PPPx, ENABLE);

RCC_APB2PeriphClockCmd(RCC_APB2Periph_PPPx, ENABLE);

RCC_APB1PeriphClockCmd(RCC_APB1Periph_PPPx, ENABLE);

Тут PPP обозначает название актонима модуля (например ADC или USART) , а x – номер периферийного модуля.
Прежде всего необходимо узнать к какой шине подключен используемый вами модуль.
Всего у микроконтроллеров с архитектурой ядра Cortex-M3 имеется в наличии три шины :
шина инструкций, шина данных и системная шина. Шина инструкций соединяет ядро с Flash — памятью программ. Шины данных и системная объединены в матрицу шин AHB ( ARM Hi-Speed Bus), которая работает на частоте ядра. Однако частота шины AHB может быть понижена путем установки делителей. Через шину AHB соединяются высокоскоростные устройства, например ядро и модуль ПДП .
Устройства ввода/вывода подсоединяются к шине AHB через промежуточные шины APB1 и APB2 (ARM Peripheral Bus).
Максимальная частота работы шины APB2 составляет 72МГц , частота шины APB1
ограничена значением 36МГц.
К какой из шин подключен задействованный вами периферийный модуль можно узнать из документации или посмотреть в заголовочном файле stm32f10x_rcc.h.
Откройте этот файл и выполните последовательно поиск значений RCC_AHBPeriph, RCC_APB1Periph и RCC_APB2Periph.

#define RCC_AHBPeriph_DMA1               ((uint32_t)0x00000001)

#define RCC_AHBPeriph_DMA2               ((uint32_t)0x00000002)

#define RCC_AHBPeriph_SRAM               ((uint32_t)0x00000004)

#define RCC_AHBPeriph_FLITF              ((uint32_t)0x00000010)

#define RCC_AHBPeriph_CRC                ((uint32_t)0x00000040)

По названиям макросов определяем какие модули к каким шинам подключены. Также для определения принадлежности к одной из трех шин можно воспользоваться здравым смыслом. Например, модуль USART является устройством ввода/вывода, значит подключен к одной из шин APB. USART является достаточно низко-скоростным интерфейсом, поэтому наверняка он подключен к шине APB1.

#define RCC_APB1Periph_USART2            ((uint32_t)0x00020000)

#define RCC_APB1Periph_USART3            ((uint32_t)0x00040000)

#define RCC_APB1Periph_UART4             ((uint32_t)0x00080000)

#define RCC_APB1Periph_UART5             ((uint32_t)0x00100000)

Однако USART1 подключен к более высокоскоростной шине APB2 :

#define RCC_APB2Periph_USART1            ((uint32_t)0x00004000)

После подачи тактового сигнала на периферийный модуль можно настроить его параметры путем вызова функции инициализации :

PPP_Init(PPP, &PPP_InitStructure);  

Поскольку для инициализации периферийного модуля в функцию инициализации необходимо передать множество параметров, то в качестве аргумента используется указатель на структуру. Сама же структура с параметрами инициализации должна быть создана в памяти до вызова функции инициализации, элементам структуры должны быть присвоены необходимые значения :

PPP_InitTypeDef PPP_InitStructure = {  val1, val2, , valN};/* инициализация структуры

при объявлении*/

Можно сначала создать структуру, а потом присвоить ее элементам необходимые значения:

PPP_InitTypeDef PPP_InitStructure;

PPP_InitStructure.member1 = val1;

PPP_InitStructure.member2 = val2;

PPP_InitStructure.memberN = valN;  

Рассмотрим пример из проекта stm32f10xQuickstart :

GPIO_InitTypeDef GPIO_InitStructure;

#ifdef USE_STM32H_103

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;

GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;

GPIO_Init(GPIOC, &GPIO_InitStructure);

Элементам структуры GPIO_InitStructure присваивается значение номера вывода , режим и скорость работы порта.
Вызовом функции GPIO_Init производится инициализация линии 12 порта GPIOC.
Первый аргумент функции GPIO_Init представляет собой указатель на область памяти периферийного устройства GPIOC, преобразованный в указатель на структуру GPIO_TypeDef.

// stm32f10x.h

#define GPIOC               ((GPIO_TypeDef *) GPIOC_BASE)

#define GPIOC_BASE          (APB2PERIPH_BASE + 0x1000)

#define APB2PERIPH_BASE       (PERIPH_BASE + 0x10000)

#define PERIPH_BASE           ((uint32_t)0x40000000)

typedef struct

{

  __IO uint32_t CRL;

  __IO uint32_t CRH;

  __IO uint32_t IDR;

  __IO uint32_t ODR;

  __IO uint32_t BSRR;

  __IO uint32_t BRR;

  __IO uint32_t LCKR;

} GPIO_TypeDef;

Структура GPIO_InitStructure имеет тип GPIO_InitTypeDef, описана в заголовочном файле
stm32f10x_gpio.h :

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

//stm32f10x_gpio.h

typedef struct

{

  uint16_t GPIO_Pin;            

  GPIOSpeed_TypeDef GPIO_Speed;  

  GPIOMode_TypeDef GPIO_Mode;    

}GPIO_InitTypeDef;

typedef enum

{

  GPIO_Speed_10MHz = 1,

  GPIO_Speed_2MHz,

  GPIO_Speed_50MHz

}GPIOSpeed_TypeDef;

typedef enum

{ GPIO_Mode_AIN = 0x0,

  GPIO_Mode_IN_FLOATING = 0x04,

  GPIO_Mode_IPD = 0x28,

  GPIO_Mode_IPU = 0x48,

  GPIO_Mode_Out_OD = 0x14,

  GPIO_Mode_Out_PP = 0x10,

  GPIO_Mode_AF_OD = 0x1C,

  GPIO_Mode_AF_PP = 0x18

}GPIOMode_TypeDef;

Как видите, в качестве типов данных инициализируемой структуры могут использоваться как пользовательские типы , вроде GPIOSpeed_TypeDef, так и типы данных с определенными значениями для удобства инициализации регистров периферии, вроде GPIOMode_TypeDef.
Для конфигурирования каждого вывода GPIO выделено 4 бита.
На следующей картинке отображен формат для нулевого бита GPIO :

Mode – режим работы вывода (вход/выход). Точнее этих значений несколько больше, настроенные как выходные, порты имеют ограничение максимальной частоты выводимого сигнала.

Режим Mode Описание
00 вход
01 выход с частотой до 10МГц
10 выход с частотой до 2МГц
11 выход с частотой до 50МГц

CNF – биты конфигурации вывода. Зависят от режима работы :

CNF Mode Описание конфигурации вывода
00 вход (00) Вход с подтяжкой к питанию
00 выход(01 — 11) Аналоговый режим
01 вход(00) Плавающий вход
01 выход(01 -11) Выход с открытым стоком
10 вход(00) Вход с подтяжкой к питанию/земле
10 выход(01 — 11) Альтернативная функция вывода с подтяжкой к питанию
11 вход(00) Зарезервировано
11 выход(01 — 11) Альтернативная функция вывода с открытым стоком

Согласитесь, что при такой структуре регистра конфигурации выводов устанавливать самостоятельно все биты для конфигурации будет крайне неудобно. Гораздо проще это будет сделать при помощи библиотечной функции GPIO_Init.
После того , как вы инициализируете модуль периферии его необходимо активировать с помощью функции PPP_Cmd :

Для модулей GPIO этой функции не существует, после инициализации вы можете сразу пользоваться выводами GPIO. Необходимо помнить, что библиотека предоставляет только интерфейс к аппаратуре микроконтроллера. Если у аппаратного модуля нет флага активации/деактивации, то и вызов функции PPP_Cmd(PPP, ENABLE) невозможен.
Для управления состоянием вывода GPIOx в режиме выхода и считывания значения в режиме входа или выхода в библиотеке предусмотрены следующие функции :

void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);

void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);

uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);

uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx);

uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);

uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx);

Аналогичным образом производится настройка и работа с остальными периферийными модулями. Однако существуют некоторые отличия в связи со спецификой конкретного аппаратного модуля, поэтому я настоятельно рекомендую сначала просмотреть примеры использования выбранного модуля для библиотеки STM32F10x SPL.

Обработка прерываний и исключений


В составе ядра Cortex-M3 присутствует контроллер вложенных векторизованных прерываний . Контроллер поддерживает до 240 источников, которые могут вызывать прерывания процессорного ядра. Сколько векторов из 240 возможных реализовано в конкретной модели микроконтроллера зависит от производителя. У микроконтроллеров stm32f10x этих векторов может быть до 43. Эти линии прерываний называются маскируемыми. Кроме того существует 15 векторов прерываний ядра Cortex-M3 и одно внешнее не маскируемое прерывание EXTI.
Контроллер поддерживает вложенные прерывания, когда внутри одного обработчика может возникнуть другое прерывание. В связи с этим каждый источник прерывания имеет свой приоритет. Поддерживается 16 уровней приоритетов прерываний.
Наивысшие значения приоритетов имеют векторы прерываний ядра Cortex-M3.
Три самых высоких уровня прерываний закреплены за векторами жестко и не могут быть изменены :

Номер Обработчик Приоритет Описание
1 Reset_Handler -3(наивысший) Вектор сброса
2 NMI_Handler -2 Немаскируемое прерывание
3 HardFault_Handler -1 Аварийные состояния

Всем остальным векторам прерываний могут быть назначены уровни приоритета от 0 до 15.
Высшему уровню приоритета соответствует меньшее его значение. Уровень приоритета можно назначить не только отдельному вектору, но и целой группе векторов. Такая возможность упрощает работу с большим количеством векторов прерываний.
Для установки группы приоритетов используется функция из библиотеки STM32F10x SPL :

void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup);

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

Название группы приоритетов Количество
приоритетов
Количество
субприоритетов
NVIC_PriorityGroup_0 0 16
NVIC_PriorityGroup_1 2 8
NVIC_PriorityGroup_2 4 4
NVIC_PriorityGroup_3 8 2
NVIC_PriorityGroup_4 16 0

Рассмотрим использование контроллера прерываний в примере stm32f10xQuickstart :

NVIC_InitTypeDef NVIC_InitStructure;

NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;

NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;

NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;

NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;

NVIC_Init(&NVIC_InitStructure);

Функция назначения группы приоритетов в коде не используется, поэтому по-умолчанию будет использована группа NVIC_PriorityGroup_0 . Приоритеты всех маскируемых векторов прерываний будут одинаковыми. Это означает , что при одновременном возникновении нескольких прерываний последовательность их обработки будет определятся субприоритетами в единственной группе. Вложенные прерывания для векторов, которым можно назначить приоритет, будут недоступны, поскольку у всех у них будет одинаковый приоритет , равный 0. В этом случае обработчик маскируемого прерывания может прерваться только одним из трех исключений с неизменным значением приоритета (-3 … -1).
Чтобы разрешить/запретить прерывания в CMSIS для компилятора GNU C используются такие функции:

__enable_irq();

__disable_irq();

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

__enable_fault_irq();

__disable_fault_irq();

В библиотеке STM32F10x SPL для реализации обработчиков прерываний программы принято использовать файлы stm32f10x_it.h и stm32f10x_it.с.
Названия функций — обработчиков прерываний определены в стартовом файле (в моем примере это startup_stm32f10x_md.s) . В стартовом файле для каждого прерывания назначен обработчик по-умолчанию, который можно заменить на свой в stm32f10x_it.с:

.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

К примеру, если в stm32f10x_it.с определить обработчик HardFault_Handler, то будет использоваться именно этот обработчик при возникновении исключения HardFault, а не объявленный в стартовом файле.

//  stm32f10x_it.с

void HardFault_Handler()

{

debug_printf(HardFault_Handler”);

for(;;)

;

}

Анализ содержимого Makefile.


Переменная SOURCES содержит список всех компилируемых файлов с различными расширениями ( c, cpp, S, s). Поскольку все эти файлы компилируются с различными опциями , то есть необходимость их разделить на С, С++ и ассемблерные исходники.
Для этого предназначены конструкции ifneq/endif . В результате переменная OBJECTS будет содержать список всех объектных файлов, которые потом нужно скомпоновать в исполняемый образ программы.
Дальше описаны правила компиляции различных типов файлов. Компиляция ассемблерных файлов представлена дважды, поскольку исходники на ассемблере могут иметь расширение *.S или *.s .
Директива

include $(wildcard $(OUT_DIR)/*.d)

включает в мейкфайл зависимости (все файлы с расширением .d). Образуются эти файлы при компиляции исходных текстов программ .c и .cpp при помощи опции -MMD.
Эта опция gcc записывает зависимости объектного файла от исходных и заголовочных файлов в одноименный с компилируемым файл с расширением .d.
Особенность опции -MMD в том, что в зависимости включаются только заголовочные файлы , подключаемые с помощью директивы

То есть находящиеся в каталоге inc нашего проекта.
Для заголовочных файлов, включаемых с помощью следующей директивы, зависимости не генерируются :

Следовательно при изменении этих файлов автоматическая сборка проекта не будет запущена.
Список объектных файлов OBJECTS описан выше и запускает зависимости для выполнения компиляции автоматически (make умный, он может самостоятельно определить что нужно сделать для удовлетворения нужных зависимостей ).
Созданные hex и bin файлы не используются при программировании из Makefile запуском цели «Program», но вы всегда можете переписать опции программатора , использовать встроенный загрузчик или другой адаптер, имеющийся у вас в наличии . Тогда они могут пригодиться.

Viewed 49817 times by 13180 viewers

Все функции библиотеки  HAL, SPL используют для проверки своих аргументов макрос assert_param.
Этот макрос выполняет проверку выражения с участием проверяемого аргумента функции на равенство нулю. Если значение выражения равно нулю, то вызывается функция-обработчик ошибки аргумента assert_failed, в противном случае (выражение не равно нулю) проверка аргумента проходит успешно.
В своей программе вам необходимо реализовать функцию assert_failed.
В ней выводится сообщение об ошибке , название файла и номер строки кода, вызвавшей ошибку.

#ifdef  USE_FULL_ASSERT
void assert_failed(uint8_t* file, uint32_t line)
{
 while (1)
 {
 }
}/* assert_failed */
#endif/*USE_FULL_ASSERT*/

Реализованная в вашем коде функция assert_failed используется только когда объявлен макрос USE_FULL_ASSERT. В противном случае весь отладочный код исключается из исходника.  Пример использования assert_param. 

void set_param(uint8_t * param, uint8_t value)
{
 assert_param( param != NULL);
 *param = value;
}/*set_param*/

Функция устанавливает значение параметра через указатель, передаваемый в качестве аргумента. Если макрос USE_FULL_ASSERT не объявлен, то можно считать , что строчки
assert_param( param != NULL) в коде просто нет, иначе в этом определении происходит проверка параметра.
Если указатель не определен, то значение param != NULL будет ложно и будет запущена функция assert_failed , которая выведет через USART название файла и номер строки с ошибкой, после чего зациклится, тем самым предотвратив присвоение значения по неопределенному адресу в памяти.
Вы совсем не обязаны использовать макрос assert_param в своем коде, но в коде библиотеки SPL он используется везде.
Функцию set_param можно реализовать с проверкой ошибок аргумента без применения assert_param.

#define ERROR (-1)
#define OK (0)
 
int set_param(uint8_t * param, uint8_t value)
{
 int r = ERROR;
 if ( param == NULL)
   return r;
 *param = value;
 r = OK;
 return r;
}/*set_param*/

HAL (Hardware Abstraction Layer) — это библиотека для создания приложений на stm32, разработанная компанией

ST

в 2014 году. HAL пришёл на смену SPL.

Заранее скажу, что эта статья никакой не мануал, это просто попытка описать внутреннее устройство HAL, ну или можно сказать, что это метод изучения сабжа.

Итак, HAL позволяет абстрагироваться от работы с регистрами и прочей сложной магии. Грубо говоря, HAL это обёртка над низкоуровневыми операциями. Конечно же это не отменяет необходимости понимания устройства микроконтроллеров, но значительно снижает уровень вхождения.

Например, чтоб запустить таймер, достаточно перед бесконечным циклом прописать вот такую функцию…

HAL_TIM_Base_Start_IT(&htim1);

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

Сама функция выглядит так:

Вначале происходит проверка параметров на ошибки (assert_param), и после этого активируется прерывание и запускается таймер.

Строчки начинающиеся с

__двойного подчеркивания

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

Однако я немного забежал вперёд. Прежде чем изучать HAL, нужно познакомиться с программой CubeMX (в просторечии «Куб») так как HAL является неотъемлемой частью «Куба», и именно в нём генерится весь начальный код будущего приложения включая описанные выше функции. Подробно про CubeMX читайте здесь…

Познакомились — тогда продолжим…

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

Итак мы сгенерировали проект, в котором есть таймер вызывающий прерывание при переполнении, и GPIO. Открываем этот проект в среде разработки (у меня TrueStudio) и в левой панели клацаем файл main.c…

Куб создал все необходимые функции инициализации…

void SystemClock_Config(void)

— инициализация тактирования.

static void MX_TIM1_Init(void)

— инициализация таймера.

static void MX_GPIO_Init(void)

— инициализация GPIO.

… и избавил нас от возни с настройками, и от возможных ошибок.

Все функции типичны

— параметры записываются в структуры, и адреса этих структур передаются в соответствующие HAL-функции. Каждая функция возвращает статус. Если возвращается ошибка, то вызывается функция

Error_Handler()

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

void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  // ШЕФ ВСЁ ПРОПАЛО
  /* USER CODE END Error_Handler_Debug */
}

Ниже есть ещё одна функция проверок на ошибки —

void assert_failed(uint8_t *file, uint32_t line)

. Если макрос

assert_param

(упомянутый в начале статьи) возвращает ошибку, то в эту функцию прилетает имя файла в котором произошла ошибка, и номер строки.

Функция работает при условии, что задефайнен

USE_FULL_ASSERT

. Сам по себе этот дефаин находится в файле

stm32f1xx_hal_conf.h

, но он закомментирован…

посмотреть

/* ########################## Assert Selection ############################## */
/**
  * @brief Uncomment the line below to expanse the "assert_param" macro in the 
  *        HAL drivers code
  */
/* #define USE_FULL_ASSERT    1U */

В конце файла обрисован механизм передачи assert_param() в void assert_failed()…

/* Exported macro ------------------------------------------------------------*/
#ifdef  USE_FULL_ASSERT
/**
  * @brief  The assert_param macro is used for function's parameters check.
  * @param  expr: If expr is false, it calls assert_failed function
  *         which reports the name of the source file and the source
  *         line number of the call that failed. 
  *         If expr is true, it returns no value.
  * @retval None
  */
  #define assert_param(expr) ((expr) ? (void)0U : assert_failed((uint8_t *)__FILE__, __LINE__))
/* Exported functions ------------------------------------------------------- */
  void assert_failed(uint8_t* file, uint32_t line);
#else
  #define assert_param(expr) ((void)0U)
#endif /* USE_FULL_ASSERT */

Если хотите чтоб он раскомментировался, то надо в Кубе сделать так…


Enable Full Assert. Эти ассерты занимают определённое количество памяти, поэтому их лучше использовать только для отладки, а в релизе отключать.

В общем с проверками на ошибки у HAL’а всё очень удобно и информативно.

Теперь давайте рассмотрим процесс инициализации на примере таймера.

В функции

static void MX_TIM1_Init(void)

, в объявленную глобально структуру

htim1

заносятся различные параметры таймера, после чего эта структура передаётся в функцию

HAL_TIM_Base_Init(&htim1)

.

Теперь клацните функцию

if (HAL_TIM_Base_Init(&htim1) != HAL_OK)

левой кнопкой, а потом правой — вылезет контекстное меню, в котором нужно выбрать

Open Declaration

. Откроется файл

stm32f1xx_hal_tim.c

Здесь происходит следующее:

Проверяется не пустой ли указатель структуры (htim == NULL) и заполнены ли все элементы структуры (assert_param).

Проверяется статус таймера (htim->State == HAL_TIM_STATE_RESET). В данном случае статус HAL_TIM_STATE_RESET говорит о том, что устройство еще не инициализировано или отключено.

посмотреть


Заголовочный файл stm32f1xx_hal_tim.h.

Если статус удовлетворяет, то снимается блокировка (htim->Lock = HAL_UNLOCKED) и вызывается функция

HAL_TIM_Base_MspInit(htim)

посмотреть

Здесь проверяется какой именно таймер настраивается (htim_base->Instance==TIM1) и вызываются функции которые включают тактирование таймера, активирует прерывание и настраивают приоритет.

Далее устанавливается статус «занято» (htim->State= HAL_TIM_STATE_BUSY) — если по каким-то причинам, параллельно будет вызвана ещё одна функция инициализации таймера, то она не сможет ничего испортить.

После этого вызывается функция

TIM_Base_SetConfig(htim->Instance, &htim->Init)

(у этой функции нет приставки HAL, поэтому можно назвать её низкоуровневой) работающая напрямую с регистрами…

посмотреть


Файл stm32f1xx_hal_tim.c

Ну и наконец устанавливается статус «готов к труду и обороне» (htim->State= HAL_TIM_STATE_READY) и возвращается —

return HAL_OK;

Функции связанные с таймером находятся либо в том же файле (stm32f1xx_hal_tim.c), либо в

stm32f1xx_hal_tim_ex.c

.

Все функции имеют характерные названия определяющие их назначение…

Окончание

_IT

означает, что устройство будет вызывать прерывание. Это относится к любым функциям используемым в HAL.

Например запуск таймера без прерываний выглядит так:

HAL_TIM_Base_Start(&htim1);

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

При работе с любой другой периферией, все необходимые функции вы найдёте в соответствующих файлах…

Названия файлов говорят сами за себя.

Функция запуска таймера…

/* USER CODE BEGIN 2 */
  HAL_TIM_Base_Start_IT(&htim1);
  /* USER CODE END 2 */

… сама по себе не особо интересна.

посмотреть

Функция устанавливает бит разрешающий прерывания по переполнению — __HAL_TIM_ENABLE_IT(htim, TIM_IT_UPDATE) и бит активации таймера — __HAL_TIM_ENABLE(htim).

А вот механизм вызова прерывания поможет понять устройство библиотеки HAL. Разберём его…

Когда мы в Кубе активируем прерывание от какой-либо периферии, то в файле

stm32f1xx_it.c

автоматически создаётся обработчик с соответствующим именем…


Сюда программа переходит как только сработает прерывание от любого из событий таймера №1.

Этот обработчик (условно назовём его низкоуровневым) вызывает HAL-обработчик

HAL_TIM_IRQHandler(&htim1)

находящийся в файле

stm32f1xx_hal_tim.c

. HAL-обработчик состоит из нескольких блоков, каждый из которых отвечает за определённое событие — захват/сравнение, переполнение, триггерный сигнал и т.д…

события

Программа войдя в функцию

HAL_TIM_IRQHandler

проверяет какой из флагов был установлен и найдя нужный блок выполняет его содержимое.

Нас интересует блок TIM Update event…

про макросы

Библиотека HAL под завязку напичкана различными макросами. Как уже говорилось в начале статьи, они начинаются с __двойного подчёркивания и имеют характерные имена определяющие их назначение. Эти макросы очёнь клёвая штука, они позволяют оперировать различными битами в различных регистрах без необходимости копаться в даташитах.

_GET_

— читать биты,

_SET_

— устанавливать биты,

_CLEAR_

— очищать биты, и т.д. Посмотреть макросы можно в хедерах соответствующей периферии, например, всё что связано с таймерами находится в файле

stm32f1xx_hal_tim.h

.

Внутри макроса

__HAL_TIM_CLEAR_IT(htim, TIM_IT_UPDATE)

содержится вот такая конструкция…

#define __HAL_TIM_CLEAR_IT(__HANDLE__, __INTERRUPT__) ((__HANDLE__)->Instance->SR = ~(__INTERRUPT__))

Этот макрос сбрасывает бит (указанный вторым аргументом) в регистре состояния (Status Register).

В первый аргумент подставляется указатель на структуру таймера, а вторым аргументом идёт дефаин флага который взводится при возникновении прерывания…

#define TIM_IT_UPDATE  (TIM_DIER_UIE)

Написав программу на HAL вы можете проследить где-какие макросы/функции вызываются, и работать с регистрами напрямую. То есть HAL можно с лёгкостью использовать как пособие для изучения низкоуровневых операций.

Если установлен флаг переполнения (TIM_FLAG_UPDATE) и источником является прерывание по переполнению (TIM_IT_UPDATE), тогда флаг сбрасывается и вызывается колбек

HAL_TIM_PeriodElapsedCallback(htim)

.

Колбек это характерная фишка HAL’а.

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

заметка

В принципе нам ничто не мешает мигать лампочкой прямо в обработчике, да ещё и оперировать регистрами напрямую (немного хардкора)


В этом примере делается то же самое, что делает HAL — сбрасывается флаг прерывания и вместо вызова колбека сразу же выполняется действие (мигание светиком).

Таким образом количество кода сократилось бы в разы, но не было бы никаких проверок, и в любом случае нужно было бы самостоятельно писать проверку — какое именно событие случилось (если бы их было несколько). Тем не менее, в дальнейшем, когда хорошенько прокачаетесь, будете потихонечку переходить на работу с регистрами.

Если вы внимательно посмотрите, то увидите что у каждого события есть свой колбек. Например у захвата/сравнения их несколько…

посмотреть

HAL_TIM_IC_CaptureCallback, HAL_TIM_OC_DelayElapsedCallback и HAL_TIM_PWM_PulseFinishedCallback.

Все эти колбеки прописаны в том же файле, с атрибутом

__weak

.

weak

Атрибут

__weak

говорит компилятору, что эта функция может быть переопределена. То есть если такую же функцию, но без weak, прописать ещё где-то, то функция с weak будет игнорироваться.

Находим нужный нам колбек…

… и переопределяем его в файл

main.c

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	if(htim->Instance == TIM1) //check if the interrupt comes from TIM1
	{
		HAL_GPIO_TogglePin(led13_GPIO_Port, led13_Pin); //Toggle the state of pin
	}
}

Проверяем что прерывание пришло от таймера №1 и мигаем светиком.

Проверять от какого таймера пришло прерывание нужно в том случае, если используется несколько таймеров. Тут дело вот в чём: если мы настроим ещё один таймер, например №2, и он тоже будет вызывать прерывания, тогда в файле

stm32f1xx_it.c

появится второй обработчик…

Не смотря на то, что обработчиков два, функция

HAL_TIM_IRQHandler()

одна и та же. Соответственно и колбек будет вызываться один и тот же. Поэтому для двух таймеров нужно делать так…

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	if(htim->Instance == TIM1)
	{
		HAL_GPIO_TogglePin(led13_GPIO_Port, led13_Pin); 
	}

	if(htim->Instance == TIM2)
	{
		HAL_GPIO_TogglePin(led13_GPIO_Port, led13_Pin);
	}
}

Это касается не только таймеров, но и прочей периферии — USART, SPI, I2C и т.д.

Программирование всего остального выглядит примерно так же как и таймера. Открываем соответствующий файл, например

stm32f1xx_hal_uart.c

, если работаем с USART’ом, находим там нужные функции, а в файле

stm32f1xx_hal_uart.h

макросы. Читаем комментарии (все функции и макросы прокомментированы) и пишем код…

Рассмотрим работу USART’а с DMA, там механизм несколько сложнее чем с таймером. В Кубе настройте USART с использованием DMA на приём…

Инициализация USART’а точно такая же как и у таймера…


Параметры загружаются в структуру и передаются в функцию.

Команда запуска опять же схожа с таймером (передаётся структура + доп. аргументы)

HAL_UART_Receive_DMA(&huart1, (uint8_t*)rx_buff, 10);

Первый аргумент это адрес структуры, второй — это адрес массива в который будут складываться полученные данные, третий — это количество байт которые нужно принять.

Здесь у нас много чего интересненького.

В первую очередь происходит проверка — занят USART или нет (HAL_UART_STATE_READY).

Если до этого функция уже запускалась и данные ещё не получены, то эта проверка не пройдёт и функция вернёт статус «занято» (return HAL_BUSY). Если же необходимо перезапустить функцию, то предварительно надо вызвать — HAL_UART_AbortReceive(&huart1). Как видите названия функций говорят сами за себя.

Далее проверяется не пустой ли указатель на приёмный буфер, и чтоб размер данных был не нулевой. Устанавливается блокировка (__HAL_LOCK) и начинается заполнение структуры

huart

. Первые четыре пункта вопросов не вызывают, а дальше в соответствующие элементы структуры записываются указатели на функции

(в языке СИ имя функции без скобок является указателем на эту функцию)

содержащие колбеки…

посмотреть

huart->hdmarx->XferCpltCallback = UART_DMAReceiveCplt;

Здесь помимо проверки и нового вида макроса (CLEAR_BIT) мы наконец-то видим колбек —

HAL_UART_RxCpltCallback(huart)

, который и нужно прописывать в

main.c

. Этот колбек вызывается когда буфер будет заполнен полностью.

Прерывание может вызываться при заполнении половины буфера. За это отвечает

huart->hdmarx->XferHalfCpltCallback = UART_DMARxHalfCplt

.

посмотреть

Для ошибки тоже есть функция с колбеком —

huart->hdmarx->XferErrorCallback = UART_DMAError

.

посмотреть

Следом идёт запуск DMA —

HAL_DMA_Start_IT()

посмотреть

В функцию передаётся: указатель на структуру, источник данных (в нашем случае это регистр данных (DR) USART’а), получатель данных (адрес буфера), и ожидаемое кол-во байт.

Потом всё это хозяйство передаётся в функцию конфигурирования —

DMA_SetConfig(hdma, SrcAddress, DstAddress, DataLength)

, после чего происходит это…

Если элемент структуры

hdma->XferHalfCpltCallback

не пустой, то разрешаются прерывания по заполнению буфера полностью (DMA_IT_TC), по заполнению буфера наполовину (DMA_IT_HT), и при ошибке (DMA_IT_TE). Если нам не нужно отслеживать заполнение половины буфера, то надо в

huart->hdmarx->XferHalfCpltCallback

записать NULL.

Далее сбрасывается флаг ошибки переполнения (__HAL_UART_CLEAR_OREFLAG), снимается блокировка (__HAL_UNLOCK), с помощью макроса

SET_BIT

устанавливаются различные биты и возвращается статус —

return HAL_OK

.

На этом функция

HAL_UART_Receive_DMA(&huart1, (uint8_t*)rx_buff, BUFSIZE)

закончена.

Низкоуровневый обработчик прерываний от DMA выглядит так же как и в случае с таймером…

… вызывает HAL-обработчик

HAL_DMA_IRQHandler(&hdma_usart1_rx);

И опять же как и у таймера, функция состоит из нескольких блоков. Первый блок срабатывает при заполнении половинки буфера, второй — целиком, а третий при ошибке. Для примера рассмотрим блок полного буфера…

Проверяются флаги полного буфера (DMA_FLAG_TC1) и разрешённого прерывания (DMA_IT_TC).

Если отключён циклический режим DMA —

hdma->Instance->CCR & DMA_CCR_CIRC) == 0U

, тогда отключаются прерывания —

__HAL_DMA_DISABLE_IT(hdma, DMA_IT_TE | DMA_IT_TC)

. При работе DMA в циклическом режиме, отключать прерывания конечно же не нужно.

Заметьте, разработчики HAL снабдили всё функции, макросы и флаги связанные с прерываниями буквами

IT.

Далее устанавливается статус готовности к очередному приёму —

hdma->State = HAL_DMA_STATE_READY

, сбрасывается флаг окончания приёма через DMA —

__HAL_DMA_CLEAR_FLAG(hdma, __HAL_DMA_GET_TC_FLAG_INDEX(hdma))

, и снимается блокировка…

блокировка

Блокировка организована очень просто…

Если сделать

__HAL_LOCK(huart)

, то при обращении к структуре

huart

будет возвращаться статус «занято» —

return HAL_BUSY;

Файл stm32f1xx_hal_def.h.

Последнее условие связано с тем, что было сделано в функции запуска. Если мы там сделали так —

huart->hdmarx->XferCpltCallback = UART_DMAReceiveCplt

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

UART_DMAReceiveCplt()

. Соответственно условие сработает и будет вызвана функция

UART_DMAReceiveCplt()

, которая в свою очередь вызовет колбек.

Такая вот хитроумная конструкция

Если приём ведётся без DMA…

HAL_UART_Receive_IT(&huart1, (uint8_t*)rx_buff, 10);

Тогда после включения глобального прерывания USART’а появится его обработчик…

Перейдём к функции

HAL_UART_IRQHandler(&huart1)

. Полностью её рассматривать не будем, разбёрём только часть отвечающую за приём. Отправка схожа с приёмом.

Тут появился ещё один макрос —

READ_REG

, с помощью которого читаются регистры и проверяется нет ли ошибок — (errorflags == RESET).

Далее проверятся что произошло:

USART_SR_RXNE

— в USART пришёл байт,

USART_CR1_RXNEIE

— было сгенерировано прерывание. Если всё так, то вызывается функция

UART_Receive_IT(huart)

. Эта функция вызывается каждый раз при приёме очередного байта.

В зависимости от длины принимаемого слова (8 или 9 бит) выбирается первая или вторая конструкция, и данные из регистра DR (Data Register) записываются в приёмный буфер —

pRxBuffPtr

.
Если длина слова 9 бит, то для его сохранения используется два байта — huart->pRxBuffPtr += 2U;

Следом проверяется счётчик принятых байт —

RxXferCount

(он считает «вниз» от максимального значения буфера), и если он равен нулю (то есть приняты все запрошенные данные), то вызывается колбек —

HAL_UART_RxCpltCallback(huart);

Вы наверно обратили внимание, что при принятии одного байта происходит очень много операций

(как раз за такую избыточность некоторые пользователи и ругают HAL, хотя если подумать, то там только проверки и ничего лишнего)

, поэтому при большом количестве данных и/или интенсивном обмене лучше использовать DMA, там это всё происходит на аппаратном уровне.

В завершение хочется рассказать про копирование через DMA. Для этого режима у DMA есть механизм создания колбеков.

Настроим Куб для копирование массива из одной области памяти в другую при помощи DMA…


Длина слова указана Word (32 бита), то есть копироваться будет по четыре байта за один такт.

программа

#include "main.h"

#define BUFFSIZE 20

DMA_HandleTypeDef hdma_memtomem_dma1_channel1;

uint8_t src_buff[BUFFSIZE] = {0,};
uint8_t dst_buff[BUFFSIZE] = {0,};

void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_DMA_Init(void);
static void MX_USART1_UART_Init(void);

void DMA_m2m_Callback(DMA_HandleTypeDef *hdma_memtomem_dma1_channel1) // колбек по окончанию копирования через DMA
{
	// копирование завершено
}

int main(void)
{
  HAL_Init();

  SystemClock_Config();

  MX_GPIO_Init();
  MX_DMA_Init();
  MX_USART1_UART_Init();

  // регистрация колбека по окончанию копирования через DMA
  if(HAL_DMA_RegisterCallback(&hdma_memtomem_dma1_channel1, HAL_DMA_XFER_CPLT_CB_ID, DMA_m2m_Callback) != HAL_OK)
  {
      Error_Handler();
  }

  while (1)
  {
	  // запускаем копирование через DMA
	  HAL_DMA_Start_IT(&hdma_memtomem_dma1_channel1, (uint32_t)src_buff, (uint32_t)dst_buff, BUFFSIZE / 4);
	  HAL_Delay(1000);
  }
}

Функция регистрации колбека…

В функцию передаются три аргумента:

1. Указатель на структуру.
2. Ключ, по которому определяется какое событие должно вызвать колбек — скопирован весь буфер, скопирована половина буфера и т.д.


В нашем случае указан полный буфер — HAL_DMA_XFER_CPLT_CB_ID.

3. Название колбека. Придумайте сами.

Таким образом мы зарегистрировали колбек — DMA_m2m_Callback(), который будет вызываться после полного копирования.

Функция запуска копирования…

HAL_DMA_Start_IT(&hdma_memtomem_dma1_channel1, (uint32_t)src_buff, (uint32_t)dst_buff, BUFFSIZE / 4);

Аргументы: указатель на структуру, массив из которого копируется, массив в который копируется, количество байт (ячейки массива 8-ми битные, а DMA будет копировать по 32 бита за раз).

Содержимое этой функции поизучайте самостоятельно, вы уже всё знаете

По окончанию копирования произойдёт прерывание и будет вызван обработчик…

В функции

HAL_DMA_IRQHandler()

прописан такой же механизм как и в случае с USART’ом — несколько блоков отвечающих за каждое событие (полный буфер, половинка и т.д.) и вот это…

Элемент структуры

hdma->XferCpltCallback

был заполнен во время регистрации колбека.


На этом наверно всё.

Всем спасибо

User Manual — UM1850

Телеграм-чат istarik

Телеграм-чат STM32

Та же проблема на STM32F429

Какая такая проблема-то? У меня прекрасно все собирается в тех проектах, где использую SPL для настройки периферии, а самое главное, без редактирования файлов библиотек

Давайте по порядку. Я свой проект открыл на STM32F429 и из него писать буду. Среда Keil, раз уж за нее говорим.

1. Настраиваем пути проекта в опциях

image.png

2. Любой заголовочный файл из библиотеки SPL подключает «stm32f4xx.h». В этом Вы можете убедиться лично.

3. Лезем в «stm32f4xx.h» и вверху видим

#if !defined (STM32F40_41xxx) && !defined (STM32F427_437xx) && !defined (STM32F429_439xx) && !defined (STM32F401xx)
  /* #define STM32F40_41xxx */   /*!< STM32F405RG, STM32F405VG, STM32F405ZG, STM32F415RG, STM32F415VG, STM32F415ZG,  
                                     STM32F407VG, STM32F407VE, STM32F407ZG, STM32F407ZE, STM32F407IG, STM32F407IE, 
                                     STM32F417VG, STM32F417VE, STM32F417ZG, STM32F417ZE, STM32F417IG and STM32F417IE Devices */

/*#define STM32F427_437xx   !< STM32F427VG, STM32F427VI, STM32F427ZG, STM32F427ZI, STM32F427IG, STM32F427II,  
                                       STM32F437VG, STM32F437VI, STM32F437ZG, STM32F437ZI, STM32F437IG, STM32F437II Devices */
                                    
   #define STM32F429_439xx   /* !< STM32F429VG, STM32F429VI, STM32F429ZG, STM32F429ZI, STM32F429BG, STM32F429BI,  
                                       STM32F429NG, STM32F439NI, STM32F429IG, STM32F429II, STM32F439VG, STM32F439VI, 
                                       STM32F439ZG, STM32F439ZI, STM32F439BG, STM32F439BI, STM32F439NG, STM32F439NI,
                                       STM32F439IG and STM32F439II Devices */
                                    
  /* #define STM32F401xx */     /*!< STM32F401CB, STM32F401CC,  STM32F401RB, STM32F401RC, STM32F401VB, STM32F401VC  
                                     STM32F401CD, STM32F401RD, STM32F401VD, STM32F401CExx, STM32F401RE, STM32F401VE Devices */  
  
#endif

Тут просто разблокируем макроопределение на нужный микроконтроллер.

Дальше в том же файле «stm32f4xx.h» есть строки

#if !defined  (USE_STDPERIPH_DRIVER)
/**
* @brief Comment the line below if you will not use the peripherals drivers.
   In this case, these drivers will not be included and the application code will 
   be based on direct access to peripherals registers 
   */
  #define USE_STDPERIPH_DRIVER
#endif /* USE_STDPERIPH_DRIVER */

От этих строк зависит, будет ли подключаться файл «stm32f4xx_conf.h» (внизу файла «stm32f4xx.h»)

#ifdef USE_STDPERIPH_DRIVER
  #include "stm32f4xx_conf.h"
#endif /* USE_STDPERIPH_DRIVER */

4. В «stm32f4xx_conf.h» у меня

#ifdef USE_FULL_ASSERT

#define assert_param(expr) ((expr) ? (void)0 : assert_failed((uint8_t *)__FILE__, __LINE__))

void assert_failed(uint8_t* file, uint32_t line);

#else

#define assert_param(expr) ((void)0)

#endif

Я особо не трогал его даже.

5. Собираем проект и радуемся, что он прекрасно собирается.

Да. Можно проще. Можно обойтись без кучи связок заголовочных файлов, для этого нужно подумать головой и выкинуть лишнее, перенеся нужное в свои файлы. Либо вовсе избавиться от SPL, что иногда разумнее.

В библиотеке прошивки STM32 и в предоставленных подпрограммах вы можете увидеть использование assert_param () повсюду. Если вы откроете файл stm32f10x_conf.h в любой подпрограмме, вы увидите, что assert_param на самом деле является определением макроса;

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

Например:

  assert_param(IS_USART_ALL_PERIPH(USARTx));  

Этот код используется для проверки допустимости параметра USARTx. IS_USART_ALL_PERIPH (USARTx) — это определение макроса, как показано ниже:

#define IS_USART_ALL_PERIPH(PERIPH) (((PERIPH) == USART1) || 
                                     ((PERIPH) == USART2) || 
                                     ((PERIPH) == USART3) || 
                                     ((PERIPH) == USART4) || 
                                     ((PERIPH) == USART5) || 
                                     ((PERIPH) == USART6) || 
                                     ((PERIPH) == USART7) || 
                                     ((PERIPH) == USART8))

Функция, определяемая макросом, заключается в том, что параметр USARTx является одним из USART1 ~ USART8, что означает, что параметр USARTx действителен и возвращает true, в противном случае он возвращает false.

assert_param () также является макросом, определенным в stm32f0xx_conf.h следующим образом:

/* #define USE_FULL_ASSERT    1 */

/* Exported macro ------------------------------------------------------------*/
#ifdef  USE_FULL_ASSERT

/**
  * @brief  The assert_param macro is used for function's parameters check.
  * @param  expr: If expr is false, it calls assert_failed function which reports 
  *         the name of the source file and the source line number of the call 
  *         that failed. If expr is true, it returns no value.
  * @retval None
  */
  #define assert_param(expr) ((expr) ? (void)0 : assert_failed((uint8_t *)__FILE__, __LINE__))
/* Exported functions ------------------------------------------------------- */
  void assert_failed(uint8_t* file, uint32_t line);
#else
  #define assert_param(expr) ((void)0)
#endif /* USE_FULL_ASSERT */

Если макрос USE_FULL_ASSERT определен, выполняется следующий код:

#define assert_param (expr) ((expr)? (void) 0: assert_failed ((uint8_t *) __ FILE__, __LINE__)) // указывает, что параметр expr имеет значение false, тогда будет выполнена следующая функция assert_failed (), __FILE__, __LINE__ являются стандартными. Определение макроса в библиотечной функции указывает имя файла и номер строки.
 void assert_failed(uint8_t* file, uint32_t line); // Объявить функцию

Если USE_FULL_ASSERT не имеет определения макроса, выполнить ((void) 0), то есть ничего не делать.

Конкретная реализация функции assert_failed () находится в main.c (шаблон для этой функции есть в main.c в любой подпрограмме), а ее содержимое выглядит следующим образом:

 1 #ifdef  USE_FULL_ASSERT
 2 
 3 /**
 4   * @brief  Reports the name of the source file and the source line number
 5   *         where the assert_param error has occurred.
 6   * @param  file: pointer to the source file name
 7   * @param  line: assert_param error line source number
 8   * @retval None
 9   */
10 void assert_failed(uint8_t* file, uint32_t line)
11 { 
12   /* User can add his own implementation to report the file name and line number,
13      ex: printf("Wrong parameters value: file %s on line %drn", file, line) */
14 
15   /* Infinite loop */
16   while (1)
17   {
18   }
19 }
20 #endif

Функция assert_failed () дает основу. Конкретная внутренняя реализация должна быть написана разработчиком. Обычно имя файла и номер строки распечатываются через последовательный порт.

Чтобы функция assert_failed () работала, вам необходимо определить макрос USE_FULL_ASSERT и включить заголовочный файл stm32f0xx_conf.h; в файле stm32f0xx.h есть следующие коды:

#ifdef USE_STDPERIPH_DRIVER
  #include "stm32f0xx_conf.h"
#endif

Пока макрос определяет USE_STDPERIPH_DRIVER, может быть включен заголовочный файл stm32f0xx_conf.h.

Определение макроса может быть записано в файл или напрямую установлено в программном обеспечении, как показано на рисунке ниже:

Примечание. Функция assert_failed () обычно используется при отладке кода, которая может помочь разработчикам проверить ошибку неверных входных параметров, но поскольку функция assert_failed () влияет на эффективность выполнения кода, ее необходимо экранировать при выпуске программы. , и определение макроса USE_FULL_ASSERT Вот и все.

Adding to the top answer, if you need to invoke your function as part of the test (i.e. your function should only throw an error if certain parameters are passed), you can wrap your function call in an anonymous function, or, in ES6+, you can pass your function in an arrow function expression.

// Function invoked with parameter.
// TEST FAILS. DO NOT USE.
assert.throws(iThrowError(badParam), Error, "Error thrown"); // WRONG!

// Function invoked with parameter; wrapped in anonymous function for test.
// TEST PASSES.
assert.throws(function () { iThrowError(badParam) }, Error, "Error thrown");

// Function invoked with parameter, passed as predicate of ES6 arrow function.
// TEST PASSES.
assert.throws(() => iThrowError(badParam), Error, "Error thrown");

And, just for the sake of thoroughness, here’s a more literal version:

// Explicit throw statement as parameter. (This isn't even valid JavaScript.)
// TEST SUITE WILL FAIL TO LOAD. DO NOT USE, EVER.
assert.throws(throw new Error("Error thrown"), Error, "Error thrown"); // VERY WRONG!

// Explicit throw statement wrapped in anonymous function.
// TEST PASSES.
assert.throws(function () { throw new Error("Error thrown") }, Error, "Error thrown");

// ES6 function. (You still need the brackets around the throw statement.)
// TEST PASSES.
assert.throws(() => { throw new Error("Error thrown") }, Error, "Error thrown");

Понравилась статья? Поделить с друзьями:
  • Assert efi error status invalid parameter
  • Assembly referencing error people playground
  • Assembly csharp dll как изменить
  • Assembler syntax error
  • Assembler error a2206