Библиотека hal для stm32 описание. Странный код отправки данных. Инициализация i2c из stm32duino

Итак, на ноги мы уже встали, в смысле на выводы микроконтроллера на плате STM32VL Discovery у нас подключено все что надо, говорить мы научились, на языке программирования Си, пора бы и в первый класс проект создать.

Написание программы

Закончив с созданием и настройкой проекта, можно приступить к написанию реальной программы. Как повелось у всех программистов, первой программой написанной для работы на компьютере, является программа, выводящая на экран надпись «HelloWorld», так и у всех микроконтроллерщиков первая программа для микроконтроллера производит мигание светодиода. Мы не будем исключением из этой традиции и напишем программу, которая будет управлять светодиодом LD3 на плате STM32VL Discovery.

После создания пустого проекта в IAR, он создает минимальный код программы:

Теперь наша программа будет всегда «крутиться» в цикле while .

Для того, чтобы мы могли управлять светодиодом, нам необходимо разрешить тактирование порта к которому он подключен и настроить соответствующий вывод порта микроконтроллера на выход. Как мы уже рассматривали ранее в первой части, за разрешение тактирования порта С отвечает битIOPCEN регистра RCC_APB2ENR . Согласно документу «RM0041 Reference manual .pdf » для разрешения тактирования шины порта С необходимо в регистре RCC_APB2ENR установить бит IOPCEN в единицу. Чтобы при установке данного бита, мы не сбросили другие, установленные в данном регистре, нам необходимо к текущему состоянию регистра применить операцию логического сложения (логического «ИЛИ») и после этого записать полученное значение в содержимое регистра. В соответствии со структурой библиотеки ST, обращение к значению регистра для его чтения и записи производится через указатель на структуру RCC -> APB 2 ENR . Таким образом, вспоминая материал со второй части, можно записать следующий код, выполняющий установку бита IOPCEN в регистре RCC_APB2ENR :

Как можно убедиться, из файла «stm32f10x.h», значение бита IOPCEN определено как 0x00000010, что соответствует четвертому биту (IOPCEN ) регистра APB2ENR и совпадает со значением указанным в даташите.

Теперь аналогичным образом настроим вывод 9 порта С . Для этого нам необходимо настроить данный вывод порта на выход в режиме push-pull. За настройку режима порта на вход/выход отвечает регистр GPIOC_CRH , мы его уже рассматривали в , его описание также находится в разделе «7.2.2 Port configuration register high» даташита. Для настройки вывода в режим выхода с максимальным быстродействием 2МГц, необходимо в регистре GPIOC_CRH установить MODE9 в единицу и сбросить бит MODE9 в нуль. За настройку режима работы вывода в качестве основной функции с выходом push-pull отвечают биты CNF 9 иCNF 9 , для настройки требуемого нам режима работы, оба эти бита должны быть сброшены в ноль.

Теперь вывод порта, к которому подключен светодиод, настроен на выход, для управления светодиодом нам необходимо изменить состояние вывода порта, установив на выходе логическую единицу. Для изменения состояния вывода порта существует два способа, первый это запись непосредственно в регистр состояния порта измененного содержимого регистра порта, так же как мы производили настройку порта. Данный способ не рекомендуется использовать в виду возможности возникновения ситуации, при которой в регистр порта может записаться не верное значение. Данная ситуация может возникнуть если во время изменения состояния регистра, с момента времени когда уже было произведено чтение состояния регистра и до момента когда произведется запись измененного состояния в регистр, какое либо периферийное устройство или прерывание произведет изменение состояния данного порта. По завершению операции по изменению состояния регистра произойдет запись значения в регистр без учета произошедших изменений. Хотя вероятность возникновения данной ситуации является очень низкой, все же стоит пользоваться другим способом, при котором описанная ситуация исключена. Для этого в микроконтроллере существует два регистра GPIOx_BSRR и GPIOx_BRR . При записи логической единицы в требуемый бит регистра GPIOx_BRR произойдет сброс соответствующего вывода порта в логический ноль. Регистр GPIOx_BSRR может производить как установку, так и сброс состояния выводов порта, для установки вывода порта в логическую единицу необходимо произвести установку битов BSn , соответствующих номеру необходимого бита, данные биты располагаются в младших регистрах байта. Для сброса состояния вывода порта в логический нуль, необходимо произвести запись битов BRn соответствующих выводов, эти биты располагаются в старших разрядах регистра порта.

Светодиод LD3 подключен к выводу 9 порта С . Для включения данного светодиода, нам необходимо подать на соответствующем выводе порта логическую единицу, чтобы «зажечь» светодиод.

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

//Не забываем подключить заголовочный файл с описанием регистров микроконтроллера

#include "stm32f10x.h"

void Delay (void );

void Delay (void )
{
unsigned long i;
for (i=0; i<2000000; i++);
}

//Наша главная функция

void main(void )
{


RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;

//очистим разряды MODE9 (сбросить биты MODE9_1 и MODE9_0 в нуль)
GPIOC->CRH &= ~GPIO_CRH_MODE9;

//Выставим бит MODE9_1, для настройки вывода на выход с быстродействием 2MHz
GPIOC->CRH |= GPIO_CRH_MODE9_1;

//очистим разряды CNF (настроить как выход общего назначения, симметричный (push-pull))
GPIOC->CRH &= ~GPIO_CRH_CNF9;

while (1)
{

//Установка вывода 9 порта С в логическую единицу («зажгли» светодиод)
GPIOC->BSRR = GPIO_BSRR_BS9;


Delay();


GPIOC->BSRR = GPIO_BSRR_BR9;


Delay();

}
}

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

Наша первая работоспособная программа написана, при её написании, для работы и настройки периферии, мы пользовались данными из официального даташита «RM0041 Reference manual .pdf », данный источник информации о регистрах микроконтроллера является самым точным, но для того чтобы им пользоваться приходится перечитывать много информации, что усложняет написание программ. Для облегчения процесса настройки периферии микроконтроллера, существуют различные генераторы кода, официальной утилитой от компании ST представлена программа Microxplorer , но она пока еще малофункциональна и по этой причине сторонними разработчиками была создана альтернативная программа «STM32 Генератор программного кода » . Данная программа позволяет легко получить код настройки периферии, используя удобный, наглядный графический интерфейс (см. рис. 2).


Рис. 2 Скриншот программы STM32 генератор кода

Как видно из рисунка 2, сгенерированный программой код настройки вывода светодиода совпадает с кодом написанным нами ранее.

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

Видео режима отладки программы мигания светодиодом

Видео работы программы мигания светодиодом на плате STM32VL Discovery

Библиотечные функции работы с периферией

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

Теперь напишем нашу программу с использованием библиотеки ST. В программе требуется произвести настройку портов ввода/вывода, для использования библиотечных функций настройки портов, необходимо произвести подключение заголовочного файла «stm32f10x_gpio.h » (см. табл. 1). Подключение данного файла можно произвести расскоментированием соответствующей строки в подключенном заголовочном конфигурационном файле «stm32f10x_conf.h ». В конце файла «stm32f10x_gpio.h » имеется список объявлений функций для работы с портами. Подробное описание всех имеющихся функций можно прочитать в файле «stm32f10x_stdperiph_lib_um.chm », краткое описание наиболее часто применяемых приведено в таблице 2.

Таблица 2.Описание основных функций настройки портов

Функция

Описание функции, передаваемых и возвращаемых параметров

GPIO_DeInit (
GPIO_TypeDef* GPIOx)

Производит установку значений регистров настройки порта GPIOx на значения по умолчанию

GPIO_Init (
GPIO_TypeDef* GPIOx,

Производит установку регистров настройки порта GPIOx в соответствии с указанными параметрами в структуре GPIO_InitStruct

GPIO_StructInit (
GPIO_InitTypeDef* GPIO_InitStruct)

Заполняет все поля структуры GPIO_InitStruct, значениями по умолчания

uint8_t GPIO_ReadInputDataBit(
GPIO_TypeDef* GPIOx,
uint16_t GPIO_Pin);

Чтение входного значения вывода GPIO_Pin порта GPIOx

uint16_t GPIO_ReadInputData (
GPIO_TypeDef* GPIOx)

Чтение входных значений всех выводов порта GPIOx

GPIO_SetBits(
GPIO_TypeDef* GPIOx,
uint16_t GPIO_Pin)

Установка выходного значения вывода GPIO_Pin порта GPIOx в логическую единицу

GPIO_ResetBits(
GPIO_TypeDef* GPIOx,
uint16_t GPIO_Pin)

Сброс выходного значения вывода GPIO_Pin порта GPIOx в логический ноль

GPIO_WriteBit(
GPIO_TypeDef* GPIOx,
uint16_t GPIO_Pin,
BitAction BitVal)

Запись значения BitVal в вывод GPIO_Pin порта GPIOx

GPIO_Write(
GPIO_TypeDef* GPIOx,
uint16_t PortVal)

Запись значения PortVal в порт GPIOx

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

Список переменных, включенных в структуры для функций работы с портами, описаны в том же файле несколько выше описания функций. Так, например, структура «GPIO_InitTypeDef » имеет следующую структуру:

typedef struct
{

uint16_t GPIO_Pin; /*!< Specifies the GPIO pins to be configured.
This parameter can be any value of @ref GPIO_pins_define */

GPIOSpeed_TypeDef GPIO_Speed; /*!< Specifies the speed for the selected pins.
This parameter can be a value of @ref GPIOSpeed_TypeDef */

GPIOMode_TypeDef GPIO_Mode; /*!< Specifies the operating mode for the selected pins.
This parameter can be a value of @ref GPIOMode_TypeDef */

}GPIO_InitTypeDef;

Первое поле данной структуры содержит переменную «GPIO _ Pin » типа unsigned short , в данную переменную необходимо записывать флаги номеров соответствующих выводов, для которых предполагается произвести необходимую настройку. Можно произвести настройку сразу несколько выводов, задав в качестве параметра несколько констант через оператор побитовое ИЛИ (см. ). Побитовое ИЛИ «соберёт» все единички из перечисленных констант, а сами константы являются маской, как раз предназначенной для такого использования. Макроопределения констант указаны в этом же файле ниже.

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

Описание возможных значений:

  • GPIO_Mode_AIN - аналоговый вход (англ. Analog INput);
  • GPIO_Mode_IN_FLOATING - вход без подтяжки, болтающийся (англ. Input float) в воздухе
  • GPIO_Mode_IPD - вход с подтяжкой к земле (англ. Input Pull-down)
  • GPIO_Mode_IPU - вход с подтяжкой к питанию (англ. Input Pull-up)
  • GPIO_Mode_Out_OD - выход с открытым стоком (англ. Output Open Drain)
  • GPIO_Mode_Out_PP - выход двумя состояниями (англ. Output Push-Pull - туда-сюда)
  • GPIO_Mode_AF_OD - выход с открытым стоком для альтернативных функций (англ. Alternate Function). Используется в случаях, когда выводом должна управлять периферия, прикрепленная к данному выводу порта (например, вывод Tx USART1 и т.п.)
  • GPIO_Mode_AF_PP - то же самое, но с двумя состояниями

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

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

//Объявляем структуру

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

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

/* Запишем в поле GPIO_Pin структуры GPIO_Init_struct номер вывода порта, который мы будем настраивать далее */

GPIO_Init_struct.GPIO_Pin=GPIO_Pin_9;

/* Подобным образом заполним поле GPIO_Speed */

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

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

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

//Не забываем подключить заголовочный файл с описание регистров микроконтроллера

#include "stm32f10x.h"
#include "stm32f10x_conf.h"

//объявляем функцию программной задержки

void Delay (void );

//сама функция программной задержки

void Delay (void )
{
unsigned long i;
for (i=0; i<2000000; i++);
}

//Наша главная функция

void main(void )
{

//Разрешаем тактирование шины порта С
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);

//Объявляем структуру для настройки порта
GPIO_InitTypeDef GPIO_Init_struct;

//Заполняем структуру начальными значениями
GPIO_StructInit(&GPIO_Init_struct);

/* Запишем в поле GPIO_Pin структуры GPIO_Init_struct номер вывода порта, который мы будем настраивать далее */
GPIO_Init_struct.GPIO_Pin = GPIO_Pin_9;

// Подобным образом заполним поля GPIO_Speed и GPIO_Mode
GPIO_Init_struct.GPIO_Speed= GPIO_Speed_2MHz;
GPIO_Init_struct.GPIO_Mode = GPIO_Mode_Out_PP;

//Передаем заполненную структуру, для выполнения действий по настройке регистров
GPIO_Init(GPIOC, &GPIO_Init_struct);

//Наш основной бесконечный цикл
while (1)
{
//Установка вывода 9 порта С в логическую единицу ("зажгли" светодиод)
GPIO_SetBits(GPIOC, GPIO_Pin_9);

//Добавляем программную задержку, чтобы светодиод светился некоторое время
Delay();

//Сброс состояния вывода 9 порта С в логический ноль
GPIO_ResetBits(GPIOC, GPIO_Pin_9);

//Добавляем снова программную задержку
Delay();
}
}

ссылке .

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

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

Обработчик прерывания

Микроконтроллеры имеют одну замечательную способность – останавливать выполнение основной программы по какому-то определенному событию, и переходить к выполнению специальной подпрограммы – обработчику прерывания . В качестве источников прерывания могут выступать как внешние события – прерывания по приему/передаче данных через какой либо интерфейс передачи данных, или изменение состояния вывода, так и внутренние – переполнение таймера и т.п.. Список возможных источников прерывания для микроконтроллеров серии STM32 приведен в даташите «RM0041 Reference manual » в разделе «8 Interrupts and events ».

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

Для начала работы с прерываниями следует настроить и проинициализировать контроллер прерываний NVIC. В архитектуре Cortex M3 каждому прерыванию можно выставить свою группу приоритета для случаев, когда возникает несколько прерываний одновременно. Затем следует произвести настройку источника прерывания.

В поле NVIC_IRQChannel указывается, какое именно прерывание мы хотим настроить. Константа USART1_IRQn обозначает канал, отвечающий за прерывания, связанные с USART1. Она определена в файле «stm32f10x.h », там же определены другие подобные константы.

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

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

Теперь в настройках модуля необходимо установить параметры, по которым данный модуль будет генерировать прерывание. Для начала следует произвести включение прерывания, это делается вызовом функции name _ITConfig() , которая находится заголовочном файле периферийного устройства.

//Разрешаем прерывания по окончанию передачи байта по USART1
USART_ITConfig(USART1, USART_IT_TXE, ENABLE);

//Разрешаем прерывания по окончанию приема байта по USART1
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);

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

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

Для выполнения различных, небольших, повторяющихся с точным периодом действий, в микроконтроллерах с ядром Cortex-M3 имеется специально предназначенный для этого системный таймер. В функции данного таймера входит только вызов прерывания через строго заданные интервалы времени. Как правило, в вызываемом этим таймером прерывании, размещают код для измерения продолжительности различных процессов. Объявление функции настройки таймера размещено в файле «core _ cm 3. h ». В качестве передаваемого функции аргумента указывается число тактов системной шины между интервалами вызова обработчика прерывания системного таймера.

SysTick_Config(clk);

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

Пример файла «main.с» программы мигания светодиода с использованием прерывания:

//Подключаем заголовочный файл с описанием регистров микроконтроллера

#include "stm32f10x.h"
#include "stm32f10x_conf.h"
#include "main.h"

unsigned int LED_timer;

//Функция, вызываемая из функции-обработчика прерываний системного таймера

void SysTick_Timer_main(void )
{
//Если переменная LED_timer еще не дошла до 0,
if (LED_timer)
{
//Проверяем ее значение, если оно больше 1500 включим светодиод
if (LED_timer>1500) GPIOC->BSRR= GPIO_BSRR_BS9;

//иначе если меньше или равно 1500 то выключим
else GPIOC->BSRR= GPIO_BSRR_BR9;

//Произведем декремент переменной LED_timer
LED_timer--;
}

//Ели же значение переменной дошло до нуля, зададим новое значение 2000
else LED_timer=2000;
}

//Наша главная функция

void main(void )
{

//Разрешаем тактирование шины порта С
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);

//Объявляем структуру для настройки порта
GPIO_InitTypeDef GPIO_Init_struct;

//Заполняем структуру начальными значениями
GPIO_StructInit(&GPIO_Init_struct);

/* Запишем в поле GPIO_Pin структуры GPIO_Init_struct номер вывода порта, который мы будем настраивать далее */
GPIO_Init_struct.GPIO_Pin = GPIO_Pin_9;

// Подобным образом заполним поля GPIO_Speed и GPIO_Mode
GPIO_Init_struct.GPIO_Speed= GPIO_Speed_2MHz;
GPIO_Init_struct.GPIO_Mode = GPIO_Mode_Out_PP;

//Передаем заполненную структуру, для выполнения действий по настройке регистров
GPIO_Init(GPIOC, &GPIO_Init_struct);

//выбираем приоритетную группу для прерываний
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);

//Настраиваем работу системного таймера с интервалом 1мс
SysTick_Config(24000000/1000);

//Наш основной бесконечный цикл
while (1)
{
//В этот раз тут пусто, все управление светодиодом происходит в прерываниях
}
}

Часть исходного кода в файле «stm32f10x_it.c»:


#include "main.h"

/**
* @brief This function handles SysTick Handler.
* @param None
* @retval None
*/

void SysTick_Handler(void )
{
SysTick_Timer_main();
}

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

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

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

Я указывал, что к системе подключается стандартная библиотека. На самом деле, подключается CMSIS - система обобщенного структурного представления МК, а также SPL - стандартная библиотека периферии. Рассмотрим каждую из них:

CMSIS
Представляет собой набор заголовочных файлов и небольшого набора кода для унификации и структурировании работы с ядром и периферией МК. По сути, без этих файлов невозможно нормально работать с МК. Получить библиотеку можно на странице к МК.
Эта библиотека если верить описанию создавалась для унификации интерфейсов пр работе с любым МК семейства Cortex. Однако, на деле выходит, что это справедливо только для одного производителя, т.е. перейдя на МК другой фирмы вы вынуждены изучать его периферию почти с нуля.
Хотя те файлы которые касаются процессорного ядра МК у всех производителей идентичны (хотя бы потому, что модель процессорного ядра у них одна - предоставленная в виде ip-блоков компанией ARM).
Поэтому работа с такими частями ядра как регистры, инструкции, прерывания и сопроцессорные блоки стандартна для всех.
Что касается периферии то у STM32 и STM8 (внезапно) она почти похожа, также частично это справедливо и для других МК выпущенных компанией ST. В практической части, я покажу насколько просто использовать CMSIS. Однако трудности в его использовании связаны с нежеланием людей читать документацию и разбираться в устройстве МК.

SPL
Standard Peripheral Library - стандартная библиотека периферии. Как следует из названия, назначение этой библиотеки - создание абстракции для периферии МК. Библиотека состоит из заголовочных файлов где объявлены человеко-понятные константы для конфигурирования и работы с периферией МК, а также файлы исходного кода собираемые собственно в саму библиотеку для операций с периферией.
SPL является абстракцией над CMSIS представляя пользователю общий интерфейс для всех МК не только одного производителя, но и вообще всех МК с процессорным ядром Cortex-Mxx.
Считается, что она более удобна новичкам, т.к. позволяет не думать как работает периферия, однако качество кода, универсальность подхода и скованность интерфейсов накладывают на разработчика определенные ограничения.
Также функционал библиотеки не всегда позволяет точно реализовать настройку некоторых компонентов таких как USART (универсальный синхронный-асинхронный последовательный порт) в определённых условиях. В практической части, я также опишу работу с этой частью библиотеки.

Что ж, пока все идет хорошо, но готовы только лампочки и кнопочки. Теперь пора браться за более тяжелую периферию - USB, UART, I2C и SPI. Я решил начать с USB - отладчик ST-Link (даже настоящий от Discovery) упорно не хотел дебажить мою плату, так что отладка на принтах через USB это единственный доступный мне способ отладки. Можно, конечно, через UART, но это куча дополнительных проводов.

Я опять пошел длинным путем - сгенерировал соответствующие заготовки в STM32CubeMX, добавил в свой проект USB Middleware из пакета STM32F1Cube. Нужно только включить тактирование USB, определить обработчики соответствующих прерываний USB и полирнуть по мелочи. По большей части все важные настройки USB модуля я скопировал из STM32GENERIC, разве что чуток подпилил распределение памяти (они использовали malloc, а я статическое распределение).

Вот парочка интересных кусков, которые я утащил к себе. Например, чтобы хост (компьютер) понял, что к нему что-то подключили, устройство “передергивает” линию USB D+ (которая подключена к пину A12). Увидев такое хост начинает опрашивать устройство на предмет кто оно такое, какие интерфейсы умеет, на какой скорости оно хочет общаться, и т.д. Я не очень понимаю, почему это нужно делать до инициализации USB, но в stm32duino делается примерно так же.

Передергивание USB

USBD_HandleTypeDef hUsbDeviceFS; void Reenumerate() { // Initialize PA12 pin GPIO_InitTypeDef pinInit; pinInit.Pin = GPIO_PIN_12; pinInit.Mode = GPIO_MODE_OUTPUT_PP; pinInit.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, &pinInit); // Let host know to enumerate USB devices on the bus HAL_GPIO_WritePin(GPIOA, GPIO_PIN_12, GPIO_PIN_RESET); for(unsigned int i=0; i<512; i++) {}; // Restore pin mode pinInit.Mode = GPIO_MODE_INPUT; pinInit.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOA, &pinInit); for(unsigned int i=0; i<512; i++) {}; } void initUSB() { Reenumerate(); USBD_Init(&hUsbDeviceFS, &FS_Desc, DEVICE_FS); USBD_RegisterClass(&hUsbDeviceFS, &USBD_CDC); USBD_CDC_RegisterInterface(&hUsbDeviceFS, &USBD_Interface_fops_FS); USBD_Start(&hUsbDeviceFS); }


Еще один интересный момент - поддержка бутлоадера stm32duino. Для того, чтобы заливать прошивку нужно сначала перезагрузить контроллер в бутлоадер. Самый простой способ это нажать кнопку ресет. Но чтобы сделать это более удобно можно перенять опыт ардуино. Когда деревья были молодыми контроллеры AVR еще не имели на борту поддержки USB, на плате находился переходник USB-UART. Сигнал DTR UART’а подключен к ресету микроконтроллера. Когда хост посылает сигнал DTR, то микроконтроллер перегружается в бутлоадер. Работает железобетонно!

В случае использования USB мы только эмулируем COM порт. Соответственно перезагрузку в бутлоадер нужно делать самостоятельно. Загрузчик stm32duino кроме сигнала DTR на всякий случай еще ожидает специальную магическую константу (1EAF - отсылка к Leaf Labs)

static int8_t CDC_Control_FS (uint8_t cmd, uint8_t* pbuf, uint16_t length) { ... case CDC_SET_CONTROL_LINE_STATE: dtr_pin++; //DTR pin is enabled break; ... static int8_t CDC_Receive_FS (uint8_t* Buf, uint32_t *Len) { /* Four byte is the magic pack "1EAF" that puts the MCU into bootloader. */ if(*Len >= 4) { /** * Check if the incoming contains the string "1EAF". * If yes, check if the DTR has been set, to put the MCU into the bootloader mode. */ if(dtr_pin > 3) { if((Buf == "1")&&(Buf == "E")&&(Buf == "A")&&(Buf == "F")) { HAL_NVIC_SystemReset(); } dtr_pin = 0; } } ... }

Обратно: MiniArduino

В общем USB заработал. Но этот слой работает только с байтами, а не строками. Поэтому дебаг принты выглядят вот так некрасиво.

CDC_Transmit_FS((uint8_t*)"Ping\n", 5); // 5 is a strlen(“Ping”) + zero byte
Т.е. поддержки форматированного вывода нет вообще - ни тебе число напечатать, ни собрать строку из кусочков. Вырисовываются следующие варианты:

  • Прикрутить классический printf. Вариант вроде бы неплохой, но тянет на +12кб прошивки (я уже как-то нечаянно вызвал у себя sprintf)
  • Откопать у себя в загашниках свою собственную реализацию printf. Я когда то под AVR писал, вроде эта реализация поменьше была.
  • Прикрутить класс Print из ардуино в реализации STM32GENERIC
Я выбрал последний вариант потому как код библиотеки Adafruit GFX так же опирается на Print, так что мне его все равно нужно вкручивать. К тому же код STM32GENERIC уже был у меня под рукой.

Я создал у себя в проекте директорию MiniArduino с целью положить туда минимально необходимое количество кода, чтобы реализовать нужные мне куски интерфейса arduino. Я начал копировать по одному файлику и смотреть какие еще нужны зависимости. Так у меня появилась копия класса Print и несколько файлов обвязки.

Но этого мало. По прежнему нужно было как то связать класс Print с функциями USB (например, CDC_Transmit_FS()). Для этого пришлось втянуть класс SerialUSB. Он потянул за собой класс Stream и кусок инициализации GPIO. Следующим шагом было подключение UART’а (у меня к нему GPS подключен). Так что я втянул к себе еще и класс SerialUART, который потянул за собой еще пласт инициализации периферии из STM32GENERIC.

В общем я оказался в следующей ситуации. Я скопировал в свою MiniArduino почти все файлы из STM32GENERIC. У меня также была своя копия библиотек USB и FreeRTOS (должна была бы быть еще копии HAL и CMSIS, но мне было лень). При этом я уже полтора месяца топтался на месте - подключал и отключал разные куски, но при этом не написал ни строчки нового кода.

Стало понятно, что моя оригинальная задумка взять под контроль всю системную часть не очень-то получается. Все равно часть кода инициализации живет в STM32GENERIC и, похоже, ему там комфортнее. Конечно, можно было рубануть все зависимости и написать свои классы-обертки под свои задачи, но это бы затормозило меня еще на месяц - этот код же еще отлаживать нужно. Конечно, для собственного ЧСВ это было бы круто, но нужно же двигаться вперед!

В общем, я выкинул все дубликаты библиотек и почти весь свой системный слой и вернулся к STM32GENERIC. Проект этот развивается достаточно динамично - несколько коммитов в день стабильно. К тому же за эти полтора месяца же я много изучил, прочитал большую часть STM32 Reference Manual, посмотрел как сделаны библиотеки HAL и обертки STM32GENERIC, продвинулся в понимании USB дескрипторов и периферии микроконтроллера. В общем я теперь был намного более уверен в STM32GENERIC чем ранее.

Обратно: I2C

Впрочем, мои приключения на этом не закончились. Еще оставался UART и I2C (у меня там дисплей живет). С UART все было достаточно просто. Я только убрал динамическое распределение памяти, а чтобы неиспользованные UART’ы эту самую память не жрали я их просто напросто закомментировал.

А вот реализация I2C в STM32GENERIC подложила каку. При чем весьма интересную, но которая отняла у меня как минимум 2 вечера. Ну или подарила 2 вечера жесткого дебага на принтах - это с какой стороны посмотреть.

В общем, реализация дисплея не завелась. В уже традиционном стиле - вот просто не работает и все. Что не работает - не понятно. Библиотека самого дисплея (Adafruit SSD1306) вроде как проверена на предыдущей реализации, но интерференцию багов исключать все же не стОит. Подозрение падает на HAL и реализацию I2C от STM32GENERIC.

Для начала я закомментировал весь код дисплея и I2C и написал инициализацию I2C без всяких библиотек, на чистом HAL

Инициализация I2C

GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.Pin = GPIO_PIN_6|GPIO_PIN_7; GPIO_InitStruct.Mode = GPIO_MODE_AF_OD; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Speed = GPIO_SPEED_HIGH; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); __I2C1_CLK_ENABLE(); hi2c1.Instance = I2C1; hi2c1.Init.ClockSpeed = 400000; hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2; hi2c1.Init.OwnAddress1 = 0; hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLED; hi2c1.Init.OwnAddress2 = 0; hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLED; hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLED; HAL_I2C_Init(&hi2c1);


Я задампил состояние регистров сразу после инициализации. Такой же дамп я сделал в рабочем варианте на stm32duino. Вот что я получил (с комментариями самому себе)

Good (Stm32duino):

40005404: 0 0 1 24 - I2C_CR2: Error interrupt enabled, 36Mhz
40005408: 0 0 0 0 - I2C_OAR1: zero own address

40005410: 0 0 0 AF - I2C_DR: data register

40005418: 0 0 0 0 - I2C_SR2: status register

Bad (STM32GENERIC):
40005400: 0 0 0 1 - I2C_CR1: Peripheral enable
40005404: 0 0 0 24 - I2C_CR2: 36Mhz
40005408: 0 0 40 0 - I2C_OAR1: !!! Not described bit in address register set
4000540C: 0 0 0 0 - I2C_OAR2: Own address register
40005410: 0 0 0 0 - I2C_DR: data register
40005414: 0 0 0 0 - I2C_SR1: status register
40005418: 0 0 0 2 - I2C_SR2: busy bit set
4000541C: 0 0 80 1E - I2C_CCR: 400kHz mode
40005420: 0 0 0 B - I2C_TRISE

Первое большое различие это установленный 14й бит в регистре I2C_OAR1. Этот бит вообще не описан в даташите и попадает в секцию reserved. Правда с оговоркой, что туда таки нужно писать единицу. Т.е. это бага в libmaple. Но раз там все работает, значит проблема не в этом. Копаем дальше.

Другое различие - выставленный бит busy. Поначалу я не придал ему значения, но забегая вперед скажу - это именно он сигнализировал о проблеме!.. Но обо всем по порядку.

Я на коленке сварганил код инициализации без всяких библиотек.

Инициализация дисплея

void sendCommand(I2C_HandleTypeDef * handle, uint8_t cmd) { SerialUSB.print("Sending command "); SerialUSB.println(cmd, 16); uint8_t xBuffer; xBuffer = 0x00; xBuffer = cmd; HAL_I2C_Master_Transmit(handle, I2C1_DEVICE_ADDRESS<<1, xBuffer, 2, 10); } ... sendCommand(handle, SSD1306_DISPLAYOFF); sendCommand(handle, SSD1306_SETDISPLAYCLOCKDIV); // 0xD5 sendCommand(handle, 0x80); // the suggested ratio 0x80 sendCommand(handle, SSD1306_SETMULTIPLEX); // 0xA8 sendCommand(handle, 0x3F); sendCommand(handle, SSD1306_SETDISPLAYOFFSET); // 0xD3 sendCommand(handle, 0x0); // no offset sendCommand(handle, SSD1306_SETSTARTLINE | 0x0); // line #0 sendCommand(handle, SSD1306_CHARGEPUMP); // 0x8D sendCommand(handle, 0x14); sendCommand(handle, SSD1306_MEMORYMODE); // 0x20 sendCommand(handle, 0x00); // 0x0 act like ks0108 sendCommand(handle, SSD1306_SEGREMAP | 0x1); sendCommand(handle, SSD1306_COMSCANDEC); sendCommand(handle, SSD1306_SETCOMPINS); // 0xDA sendCommand(handle, 0x12); sendCommand(handle, SSD1306_SETCONTRAST); // 0x81 sendCommand(handle, 0xCF); sendCommand(handle, SSD1306_SETPRECHARGE); // 0xd9 sendCommand(handle, 0xF1); sendCommand(handle, SSD1306_SETVCOMDETECT); // 0xDB sendCommand(handle, 0x40); sendCommand(handle, SSD1306_DISPLAYALLON_RESUME); // 0xA4 sendCommand(handle, SSD1306_DISPLAYON); // 0xA6 sendCommand(handle, SSD1306_NORMALDISPLAY); // 0xA6 sendCommand(handle, SSD1306_INVERTDISPLAY); sendCommand(handle, SSD1306_COLUMNADDR); sendCommand(handle, 0); // Column start address (0 = reset) sendCommand(handle, SSD1306_LCDWIDTH-1); // Column end address (127 = reset) sendCommand(handle, SSD1306_PAGEADDR); sendCommand(handle, 0); // Page start address (0 = reset) sendCommand(handle, 7); // Page end address uint8_t buf; buf = 0x40; for(uint8_t x=1; x<17; x++) buf[x] = 0xf0; // 4 black, 4 white lines for (uint16_t i=0; i<(SSD1306_LCDWIDTH*SSD1306_LCDHEIGHT/8); i++) { HAL_I2C_Master_Transmit(handle, I2C1_DEVICE_ADDRESS<<1, buf, 17, 10); }


После некоторых усилий этот код у меня заработал (в данном случае рисовал полоски). Значит проблема в I2C слое STM32GENERIC. Я начал понемногу удалять своей код, заменяя его соответствующими частями из библиотеки. Но как только я переключил код инициализации пинов с моей реализации на библиотечную вся передача по I2C стала валиться по таймаутам.

Тут я вспомнил про бит busy и попробовал понять когда он возникает. Оказалось что флаг busy возникает как только код инициализации включает тактирование I2c. Т.е. Модуль включается и сразу не работает. Интересненько.

Валимся на инициализации

uint8_t * pv = (uint8_t*)0x40005418; //I2C_SR2 register. Looking for BUSY flag SerialUSB.print("40005418 = "); SerialUSB.println(*pv, 16); // Prints 0 __HAL_RCC_I2C1_CLK_ENABLE(); SerialUSB.print("40005418 = "); SerialUSB.println(*pv, 16); // Prints 2


Выше этого кода только инициализация пинов. Ну что делать - обкладываем дебаг принтами через строку и там

Инициализация пинов STM32GENERIC

void stm32AfInit(const stm32_af_pin_list_type list, int size, const void *instance, GPIO_TypeDef *port, uint32_t pin, uint32_t mode, uint32_t pull) { … GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.Pin = pin; GPIO_InitStruct.Mode = mode; GPIO_InitStruct.Pull = pull; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; HAL_GPIO_Init(port, &GPIO_InitStruct); … }


Но вот незадача - GPIO_InitStruct заполняется правильно. Только моя работает, а эта нет. Реально, мистика!!! Все как по учебнику, но ничего не работает. Я изучал код библиотеки построчно в поисках хоть чего нибудь подозрительного. В конце концов я наткнулся на этот код (он вызывает функцию выше)

Еще кусочек инициализации

void stm32AfI2CInit(const I2C_TypeDef *instance, …) { stm32AfInit(chip_af_i2c_sda, …); stm32AfInit(chip_af_i2c_scl, …); }


Видите в нем багу? А она есть! Я даже убрал лишние параметры, чтобы проблема была виднее. В общем, разница в том, что мой код инициализирует оба пина сразу в одной структуре, а код STM32GENERIC по очереди. Видимо код инициализации пина как то влияет на на уровень на этом пине. До инициализации на этом пине ничего не выдается и резистором уровень подтягивается до единицы. В момент инициализации почему-то контроллер выставляет на соответствующей ноге ноль.

Этот факт сам по себе безобидный. Но проблема в том, что опускание линии SDA при поднятой линии SCL является start condition’ом для шины i2c. Из-за этого приемник контроллера сходит с ума, выставляет флаг BUSY и начинает ждать данных. Я решил не потрошить библиотеку, чтобы добавить возможность инициализации нескольких пинов сразу. Вместо этого я просто переставил эти 2 строки местами - инициализация дисплея прошла успешно. Фикс был принят в STM32GENERIC .

Кстати, в libmaple инициализация шины сделана интересно. Перед тем как начать инициализацию периферии i2c на шине сначала делают ресет. Для этого библиотека переводит пины в обычный GPIO режим и дрыгает этими ногами несколько раз, имитируя start и stop последовательности. Это помогает привести в чувство залипшие на шине устройства. К сожалению аналогичной штуки нет в HAL. Иногда мой дисплей таки залипает и тогда спасает только отключение питания.

Инициализация i2c из stm32duino

/** * @brief Reset an I2C bus. * * Reset is accomplished by clocking out pulses until any hung slaves * release SDA and SCL, then generating a START condition, then a STOP * condition. * * @param dev I2C device */ void i2c_bus_reset(const i2c_dev *dev) { /* Release both lines */ i2c_master_release_bus(dev); /* * Make sure the bus is free by clocking it until any slaves release the * bus. */ while (!gpio_read_bit(sda_port(dev), dev->sda_pin)) { /* Wait for any clock stretching to finish */ while (!gpio_read_bit(scl_port(dev), dev->scl_pin)) ; delay_us(10); /* Pull low */ gpio_write_bit(scl_port(dev), dev->scl_pin, 0); delay_us(10); /* Release high again */ gpio_write_bit(scl_port(dev), dev->scl_pin, 1); delay_us(10); } /* Generate start then stop condition */ gpio_write_bit(sda_port(dev), dev->sda_pin, 0); delay_us(10); gpio_write_bit(scl_port(dev), dev->scl_pin, 0); delay_us(10); gpio_write_bit(scl_port(dev), dev->scl_pin, 1); delay_us(10); gpio_write_bit(sda_port(dev), dev->sda_pin, 1); }

Опять туда: UART

Я был рад, наконец, вернуться к программированию и продолжить писать фичи. Следующим крупным куском было подключение SD карты через SPI. Это само по себе захватывающее, интересное и полное боли занятие. О нем я обязательно расскажу отдельно в следующей статье. Одной из проблем была большая загрузка (>50%) процессора. Это ставило под вопрос энергоэффективность устройства. Да и использовать устройство было некомфортно, т.к. UI тупил ужасно.

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

For (uint16_t i = 0; i < 512; i++) { spiSend(src[i]);
Нет, ну это же несерьезно! Есть же DMA! Да, библиотека SD (та, которая идет в комплекте с Ардуино) корявая и нужно менять, но ведь проблема то глобальнее. Та же самая картина наблюдается в библиотеке работы с экраном, и даже слушание UART’а у меня сделано через опрос. В общем, я начал думать, что переписывание всех компонентов на HAL это не такая уж и глупая идея.

Начал, конечно, с чего попроще - драйвера UART, который слушает поток данных от GPS. Интерфейс ардуино не позволяет прицепиться к прерыванию UART и выхватывать приходящие символы на лету. В итоге единственный способ получать данные - это постоянный опрос. Я, конечно, добавил vTaskDelay(10) в обработчик GPS, чтобы хоть немного снизить загрузку, но на самом деле это костыль.

Первая мысль, конечно, была прикрутить DMA. Это даже сработало бы, если бы не протокол NMEA. Проблема в том, что в этом протоколе информация просто идет потоком, а отдельные пакеты (строки) разделяются символом переноса строки. При этом каждая строка может быть различной длины. Из-за этого заранее неизвестно сколько данных нужно принять. DMA так не работает - там количество байт нужно задавать заранее при инициализации пересылки. Короче говоря, DMA отпадает, ищем другое решение.

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

Вырисовывается следующий дизайн

Класс драйвера UART

// Size of UART input buffer const uint8_t gpsBufferSize = 128; // This class handles UART interface that receive chars from GPS and stores them to a buffer class GPS_UART { // UART hardware handle UART_HandleTypeDef uartHandle; // Receive ring buffer uint8_t rxBuffer; volatile uint8_t lastReadIndex = 0; volatile uint8_t lastReceivedIndex = 0; // GPS thread handle TaskHandle_t xGPSThread = NULL;


Хотя инициализация слизана из STM32GENERIC она полностью соответствует той, которую предлагает CubeMX

Инициализация UART

void init() { // Reset pointers (just in case someone calls init() multiple times) lastReadIndex = 0; lastReceivedIndex = 0; // Initialize GPS Thread handle xGPSThread = xTaskGetCurrentTaskHandle(); // Enable clocking of corresponding periperhal __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_USART1_CLK_ENABLE(); // Init pins in alternate function mode GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.Pin = GPIO_PIN_9; //TX pin GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); GPIO_InitStruct.Pin = GPIO_PIN_10; //RX pin GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // Init uartHandle.Instance = USART1; uartHandle.Init.BaudRate = 9600; uartHandle.Init.WordLength = UART_WORDLENGTH_8B; uartHandle.Init.StopBits = UART_STOPBITS_1; uartHandle.Init.Parity = UART_PARITY_NONE; uartHandle.Init.Mode = UART_MODE_TX_RX; uartHandle.Init.HwFlowCtl = UART_HWCONTROL_NONE; uartHandle.Init.OverSampling = UART_OVERSAMPLING_16; HAL_UART_Init(&uartHandle); // We will be using UART interrupt to get data HAL_NVIC_SetPriority(USART1_IRQn, 6, 0); HAL_NVIC_EnableIRQ(USART1_IRQn); // We will be waiting for a single char right received right to the buffer HAL_UART_Receive_IT(&uartHandle, rxBuffer, 1); }


Вообще-то пин TX можно было бы и не инициализировать, а uartHandle.Init.Mode установить в UART_MODE_RX – мы же только принимать собираемся. Впрочем, пускай будет - вдруг мне понадобится как-то настраивать GPS модуль и писать в него команды.

Дизайн этого класса мог бы выглядеть и получше, если бы не ограничения архитектуры HAL. Так, мы не можем просто выставить режим, мол, принимай все подряд, напрямую прицепиться на прерывание и выхватывать принятые байты прямо из приемного регистра. Нужно заранее рассказать HAL’у сколько и куда мы будем принимать байт – соответствующие обработчики сами запишут принятые байты в предоставленный буфер. Вот для этого в последней строке функции инициализации есть вызов HAL_UART_Receive_IT(). Поскольку длина строки заранее неизвестна, приходится принимать по одному байту.

Также нужно объявить аж 2 коллбека. Один - это обработчик прерывания, но его работа всего лишь вызвать обработчик из HAL. Вторая функция – это «отзвон» HAL’а, что байт уже принят и он уже в буфере.

Коллбеки UART

// Forward UART interrupt processing to HAL extern "C" void USART1_IRQHandler(void) { HAL_UART_IRQHandler(gpsUart.getUartHandle()); } // HAL calls this callback when it receives a char from UART. Forward it to the class extern "C" void HAL_UART_RxCpltCallback(UART_HandleTypeDef *uartHandle) { gpsUart.charReceivedCB(); }


Метод charReceivedCB() готовит HAL к приему следующего байта. А еще именно он определяет, что строка уже закончилась и можно об этом сигнализировать основной программе. В качестве средства синхронизации можно было бы использовать семафор в режиме сигнала, но для таких простых целей рекомендуют использовать прямые нотификации.

Обработка принятого байта

// Char received, prepare for next one inline void charReceivedCB() { char lastReceivedChar = rxBuffer; lastReceivedIndex++; HAL_UART_Receive_IT(&uartHandle, rxBuffer + (lastReceivedIndex % gpsBufferSize), 1); // If a EOL symbol received, notify GPS thread that line is avaialble to read if(lastReceivedChar == "\n") vTaskNotifyGiveFromISR(xGPSThread, NULL); }


Ответной (ожидающей) функцией является waitForString(). Ее задача просто висеть на объекте синхронизации и ждать (или выходить с таймаутом)

Ждун конца строки

// Wait until whole line is received bool waitForString() { return ulTaskNotifyTake(pdTRUE, 10); }


Работает это так. Поток, который отвечает за GPS в обычном состоянии спит в функции waitForString(). Приходящие от GPS байтики обработчиком прерывания складываются в буфер. Если пришел символ \n (конец строки), то прерывание будит основной поток, который начинает переливать байты из буфера в парсер. Ну а когда парсер закончит обрабатывать пакет сообщений он обновит данные в GPS модели.

Поток GPS

void vGPSTask(void *pvParameters) { // GPS initialization must be done within GPS thread as thread handle is stored // and used later for synchronization purposes gpsUart.init(); for (;;) { // Wait until whole string is received if(!gpsUart.waitForString()) continue; // Read received string and parse GPS stream char by char while(gpsUart.available()) { int c = gpsUart.readChar(); //SerialUSB.write(c); gpsParser.handle(c); } if(gpsParser.available()) { GPSDataModel::instance().processNewGPSFix(gpsParser.read()); GPSDataModel::instance().processNewSatellitesData(gpsParser.satellites, gpsParser.sat_count); } vTaskDelay(10); } }


Я столкнулся с одним очень нетривиальным моментом, на котором залип на несколько дней. Вроде как код синхронизации взят из примеров, но он поначалу не работал – вешал всю систему. Я думал, что проблема в прямых нотификациях (функциях xTaskNotifyXXX), переделал на обычные семафоры, но приложение по прежнему вешалось.

Оказалось, нужно быть очень аккуратным с приоритетом прерываний. По умолчанию я всем прерываниям выставил нулевой (самый высший) приоритет. Но у FreeRTOS есть требование, чтобы приоритеты находились в заданном диапазоне. Прерываниям со слишком большим приоритетом нельзя вызывать функции FreeRTOS. Только прерывания с приоритетом configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY и ниже могут вызывать системные функции (неплохое объяснение и ). Эта настройка по умолчанию задана в 5. Я поменял приоритет прерывания UART на 6 и все завелось.

Опять туда: I2C через DMA

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

Тем не менее большинство устройств определяют протокол более высокого уровня примерно одинаково. устройство предоставляет пользователю набор регистров, каждый со своим адресом. При этом в протоколе общения первый байт (или несколько) в каждой транзакции определяет адрес ячейки (регистра) в которую дальше будем читать или писать. При этом возможен также многобайтный обмен в стиле «ща будем писать/читать много байт начиная с этого адреса». Последний вариант неплохо подходит для DMA.

К сожалению дисплей на базе контроллера SSD1306 предоставляет совсем другой протокол – командный. Первым байтом каждой транзакции идет признак «команда или данные». В случае команды вторым байтом идет код команды. В случае если команде нужны аргументы, то они передаются как отдельные команды следом за первой. Для инициализации дисплея нужно отправить порядка 30 команд, но их нельзя сложить в один массив и отправить одним блоком. Нужно их отправлять по одной.

А вот с отправкой массива пикселей (фрейм буфер) вполне можно воспользоваться услугами DMA. Это мы и попробуем.

Вот только библиотека Adafruit_SSD1306 написана весьма коряво и втиснуться туда малой кровью не получается. По всей видимости библиотеку сначала написали для общения с дисплеем по SPI. Потом кто-то дописал поддержку I2C, причем поддержка SPI осталась включенной. Потом кто-то начал дописывать всякие низкоуровневые оптимизации и прятать их за ifdef"ами. В итоге получилась лапша из кода поддержки разных интерфейсов. Так что прежде чем идти дальше нужно было это причесать.

Сначала я пробовал привести это в порядок обрамляя код для разных интерфейсов ифдефами. Но если я захочу писать код коммуникации с дисплеем, использовать DMA и синхронизацию через FreeRTOS, то у меня мало что получится. Точнее получится, но этот код нужно будет писать прямо в коде библиотеки. Поэтому я решил еще разок перетрусить библиотеку , сделать интерфейс и каждый драйвер вынести в отдельный класс. Код стал чище, и можно было бы безболезненно добавлять поддержку новых драйверов не меняя саму библиотеку.

Интерфейс драйвера дисплея

// Interface for hardware driver // The Adafruit_SSD1306 does not work directly with the hardware // All the communication requests are forwarded to the driver class ISSD1306Driver { public: virtual void begin() = 0; virtual void sendCommand(uint8_t cmd) = 0; virtual void sendData(uint8_t * data, size_t size) = 0; };


Итак, поехали. Инициализацию I2C я уже показывал. Ничего там не поменялось. А вот с отправкой команды немного упростилось. Помните я рассказывал про разницу между регистровым и командным протоколом для устройств I2C? И хотя дисплей реализует командный протокол, его неплохо можно имитировать с помощью регистрового. Просто нужно представить, что у дисплея всего 2 регистра – 0x00 для команд и 0x40 для данных. И HAL даже предоставляет функцию для такого вида передачи

Отправка команды в дисплей

void DisplayDriver::sendCommand(uint8_t cmd) { HAL_I2C_Mem_Write(&handle, i2c_addr, 0x00, 1, &cmd, 1, 10); }


С отправкой данных поначалу было не очень понятно. Исходный код отправлял данные небольшими пакетами по 16 байт

Странный код отправки данных

for (uint16_t i=0; i


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

Покореженный дисплей



Причина оказалась тривиальной – переполнение буфера. Класс Wire из ардуины (во всяком случае STM32GENERIC) предоставляет собственный буфер всего на 32 байта. Но зачем нам вообще дополнительный буфер, если у класса Adafruit_SSD1306 уже есть один? Тем более с HAL отправка получается в одну строку

Правильная передача данных

void DisplayDriver::sendData(uint8_t * data, size_t size) { HAL_I2C_Mem_Write(&handle, i2c_addr, 0x40, 1, data, size, 10); }


Итак, полдела сделано – написали драйвер для дисплея на чистом HAL. Но в таком варианте он все еще требователен к ресурсам – 12% проца для дисплея 128x32 и 23% для дисплея 128x64. Использование DMA тут аж просится.

Для начала инициализируем DMA. Мы хотим реализовать пересылку данных в I2C №1, а эта функция живет на шестом канале DMA. Инициализируем побайтовое копирование из памяти в периферию

Настройка DMA для I2C

// DMA controller clock enable __HAL_RCC_DMA1_CLK_ENABLE(); // Initialize DMA hdma_tx.Instance = DMA1_Channel6; hdma_tx.Init.Direction = DMA_MEMORY_TO_PERIPH; hdma_tx.Init.PeriphInc = DMA_PINC_DISABLE; hdma_tx.Init.MemInc = DMA_MINC_ENABLE; hdma_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; hdma_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; hdma_tx.Init.Mode = DMA_NORMAL; hdma_tx.Init.Priority = DMA_PRIORITY_LOW; HAL_DMA_Init(&hdma_tx); // Associate the initialized DMA handle to the the I2C handle __HAL_LINKDMA(&handle, hdmatx, hdma_tx); /* DMA interrupt init */ /* DMA1_Channel6_IRQn interrupt configuration */ HAL_NVIC_SetPriority(DMA1_Channel6_IRQn, 7, 0); HAL_NVIC_EnableIRQ(DMA1_Channel6_IRQn);


Прерывания - обязательная часть конструкции. Иначе функция HAL_I2C_Mem_Write_DMA() начнет I2C транзакцию, но никто ее не завершит. Опять имеем дело с громоздким дизайном HAL и необходимостью аж двух колбеков. Все точно так же как и с UART. Одна функция это обработчик прерывания - просто перенаправляем вызов в HAL. Вторая функция - сигнал о том, что данные уже отправились.

Обработчики прерываний DMA

extern "C" void DMA1_Channel6_IRQHandler(void) { HAL_DMA_IRQHandler(displayDriver.getDMAHandle()); } extern "C" void HAL_I2C_MemTxCpltCallback(I2C_HandleTypeDef *hi2c) { displayDriver.transferCompletedCB(); }


Разумеется, мы не будем постоянно опрашивать I2C а не закончилась ли уже пересылка? Вместо этого нужно уснуть на объекте синхронизации и ждать пока пересылка закончится

Передача данных через DMA с синхронизацией

void DisplayDriver::sendData(uint8_t * data, size_t size) { // Start data transfer HAL_I2C_Mem_Write_DMA(&handle, i2c_addr, 0x40, 1, data, size); // Wait until transfer is completed ulTaskNotifyTake(pdTRUE, 100); } void DisplayDriver::transferCompletedCB() { // Resume display thread vTaskNotifyGiveFromISR(xDisplayThread, NULL); }


Пересылка данных по прежнему занимает 24 мс - это практически чистое время пересылки 1 кб (размер дисплейного буфера) на скорости 400кГц. Только при этом бОльшую часть времени процессор просто спит (или занимается другими делами). Общая загрузка процессора упала с 23% всего лишь до 1.5-2%. Я думаю за этот показатель стОило бороться!

Опять туда: SPI через DMA

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

Интерфейс драйвера SPI для работы с SD картой

// This is custom implementation of SPI Driver class. SdFat library is // using this class to access SD card over SPI // // Main intention of this implementation is to drive data transfer // over DMA and synchronize with FreeRTOS capabilities. class SdFatSPIDriver: public SdSpiBaseDriver { // SPI module SPI_HandleTypeDef spiHandle; // GPS thread handle TaskHandle_t xSDThread = NULL; public: SdFatSPIDriver(); virtual void activate(); virtual void begin(uint8_t chipSelectPin); virtual void deactivate(); virtual uint8_t receive(); virtual uint8_t receive(uint8_t* buf, size_t n); virtual void send(uint8_t data); virtual void send(const uint8_t* buf, size_t n); virtual void select(); virtual void setSpiSettings(SPISettings spiSettings); virtual void unselect(); };


Как и прежде начинаем с простого - с дубовой реализации без всяких DMA. Инициализация частично сгенерирована CubeMX’ом, а отчасти слизана с SPI реализации STM32GENERIC

Инициализация SPI

SdFatSPIDriver::SdFatSPIDriver() { } //void SdFatSPIDriver::activate(); void SdFatSPIDriver::begin(uint8_t chipSelectPin) { // Ignore passed CS pin - This driver works with predefined one (void)chipSelectPin; // Initialize GPS Thread handle xSDThread = xTaskGetCurrentTaskHandle(); // Enable clocking of corresponding periperhal __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_SPI1_CLK_ENABLE(); // Init pins GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.Pin = GPIO_PIN_5|GPIO_PIN_7; //MOSI & SCK GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); GPIO_InitStruct.Pin = GPIO_PIN_6; //MISO GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); GPIO_InitStruct.Pin = GPIO_PIN_4; //CS GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // Set CS pin High by default HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // Init SPI spiHandle.Instance = SPI1; spiHandle.Init.Mode = SPI_MODE_MASTER; spiHandle.Init.Direction = SPI_DIRECTION_2LINES; spiHandle.Init.DataSize = SPI_DATASIZE_8BIT; spiHandle.Init.CLKPolarity = SPI_POLARITY_LOW; spiHandle.Init.CLKPhase = SPI_PHASE_1EDGE; spiHandle.Init.NSS = SPI_NSS_SOFT; spiHandle.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256; spiHandle.Init.FirstBit = SPI_FIRSTBIT_MSB; spiHandle.Init.TIMode = SPI_TIMODE_DISABLE; spiHandle.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; spiHandle.Init.CRCPolynomial = 10; HAL_SPI_Init(&spiHandle); __HAL_SPI_ENABLE(&spiHandle); }


Дизайн интерфейса заточен под ардуино с нумерованием пинов одним числом. В моем же случае задавать пин CS через параметры не было смысла - у меня этот сигнал жестко завязан на пин A4, но нужно было соблюдать интерфейс.

По дизайну библиотеки SdFat скорость SPI порта настраивается перед каждой транзакцией. Т.е. теоретически можно начинать общение с картой на малой скорости, а потом ее повышать. Но я на это забил и настроил скорость один раз в методе begin(). Так что методы activate/deactivate у меня получились пустые. Как и setSpiSettings()

Тривиальная обработчики транзакций

void SdFatSPIDriver::activate() { // No special activation needed } void SdFatSPIDriver::deactivate() { // No special deactivation needed } void SdFatSPIDriver::setSpiSettings(const SPISettings & spiSettings) { // Ignore settings - we are using same settings for all transfer }


Методы управления сигналом CS вполне тривиальны

Управление сигналом CS

void SdFatSPIDriver::select() { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); } void SdFatSPIDriver::unselect() { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); }


Подбираемся к самому интересному - чтению и записи. Первая самая дубовая реализация без DMA

Передача данных без DMA

uint8_t SdFatSPIDriver::receive() { uint8_t buf; uint8_t dummy = 0xff; HAL_SPI_TransmitReceive(&spiHandle, &dummy, &buf, 1, 10); return buf; } uint8_t SdFatSPIDriver::receive(uint8_t* buf, size_t n) { // TODO: Receive via DMA here memset(buf, 0xff, n); HAL_SPI_Receive(&spiHandle, buf, n, 10); return 0; } void SdFatSPIDriver::send(uint8_t data) { HAL_SPI_Transmit(&spiHandle, &data, 1, 10); } void SdFatSPIDriver::send(const uint8_t* buf, size_t n) { // TODO: Transmit over DMA here HAL_SPI_Transmit(&spiHandle, (uint8_t*)buf, n, 10); }


В интерфейсе SPI прием и передача данных происходит одновременно. Чтобы принять что нибудь нужно что нибудь при этом отправлять. Обычно HAL это делает за нас - мы просто вызываем функцию HAL_SPI_Receive() а она организует и отправку и прием. Но на самом деле эта функция отправляет мусор, который был в приемном буфере.
Чтобы продать что нибудь ненужное нужно сначала купить что нибудь ненужное (С) Простоквашино

Но есть нюанс. SD карточки весьма капризны. Они не любят, когда им подсовывают что попало во время того, как карта отправляет данные. Поэтому пришлось использовать функцию HAL_SPI_TransmitReceive() и насильно отправлять 0xff’ы во время приема данных.

Займемся измерениями. Пускай один поток будет в цикле записывать на карту 1кб данных.

Тестовый код по отправке потока данных в SD карту

uint8_t sd_buf; uint16_t i=0; uint32_t prev = HAL_GetTick(); while(true) { bulkFile.write(sd_buf, 512); bulkFile.write(sd_buf, 512); i++; uint32_t cur = HAL_GetTick(); if(cur-prev >= 1000) { prev = cur; usbDebugWrite("Saved %d kb\n", i); i = 0; } }


При таком подходе за секунду успевает записать порядка 15-16кб. Негусто. Но оказалось, что я поставил прескейлер аж на 256. Т.е. тактирование SPI выставлено намного меньше возможной пропускной способности. Экспериментальным путем я выяснил, что частоту выше чем 9МГц (прескейлер установлен в значение 8) ставить бессмысленно - скорость записи выше 100-110 кб/с достичь не получается (на другой флешке, кстати почему-то только 50-60кб/с получалось записывать, а на третьей вообще только 40кб/с). Видимо все упирается в таймауты самой флешки.

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

Инициализация DMA

// DMA controller clock enable __HAL_RCC_DMA1_CLK_ENABLE(); // Rx DMA channel dmaHandleRx.Instance = DMA1_Channel2; dmaHandleRx.Init.Direction = DMA_PERIPH_TO_MEMORY; dmaHandleRx.Init.PeriphInc = DMA_PINC_DISABLE; dmaHandleRx.Init.MemInc = DMA_MINC_ENABLE; dmaHandleRx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; dmaHandleRx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; dmaHandleRx.Init.Mode = DMA_NORMAL; dmaHandleRx.Init.Priority = DMA_PRIORITY_LOW; HAL_DMA_Init(&dmaHandleRx); __HAL_LINKDMA(&spiHandle, hdmarx, dmaHandleRx); // Tx DMA channel dmaHandleTx.Instance = DMA1_Channel3; dmaHandleTx.Init.Direction = DMA_MEMORY_TO_PERIPH; dmaHandleTx.Init.PeriphInc = DMA_PINC_DISABLE; dmaHandleTx.Init.MemInc = DMA_MINC_ENABLE; dmaHandleTx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; dmaHandleTx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; dmaHandleTx.Init.Mode = DMA_NORMAL; dmaHandleTx.Init.Priority = DMA_PRIORITY_LOW; HAL_DMA_Init(&dmaHandleTx); __HAL_LINKDMA(&spiHandle, hdmatx, dmaHandleTx);


Не забываем включить прерывания. У меня они будут идти с 8 приоритетом - чуть ниже чем у UART и I2C

Настройка прерываний DMA

// Setup DMA interrupts HAL_NVIC_SetPriority(DMA1_Channel2_IRQn, 8, 0); HAL_NVIC_EnableIRQ(DMA1_Channel2_IRQn); HAL_NVIC_SetPriority(DMA1_Channel3_IRQn, 8, 0); HAL_NVIC_EnableIRQ(DMA1_Channel3_IRQn);


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

Пересылка данных через DMA

const size_t DMA_TRESHOLD = 16; uint8_t SdFatSPIDriver::receive(uint8_t* buf, size_t n) { memset(buf, 0xff, n); // Not using DMA for short transfers if(n <= DMA_TRESHOLD) { return HAL_SPI_TransmitReceive(&spiHandle, buf, buf, n, 10); } // Start data transfer HAL_SPI_TrsnsmitReceive_DMA(&spiHandle, buf, buf, n); // Wait until transfer is completed ulTaskNotifyTake(pdTRUE, 100); return 0; // Ok status } void SdFatSPIDriver::send(const uint8_t* buf, size_t n) { // Not using DMA for short transfers if(n <= DMA_TRESHOLD) { HAL_SPI_Transmit(&spiHandle, buf, n, 10); return; } // Start data transfer HAL_SPI_Transmit_DMA(&spiHandle, (uint8_t*)buf, n); // Wait until transfer is completed ulTaskNotifyTake(pdTRUE, 100); } void SdFatSPIDriver::dmaTransferCompletedCB() { // Resume SD thread vTaskNotifyGiveFromISR(xSDThread, NULL); }


Конечно же без прерываний никак. Тут все также как и в случае I2C

Прерывания DMA

extern SdFatSPIDriver spiDriver; extern "C" void DMA1_Channel2_IRQHandler(void) { HAL_DMA_IRQHandler(spiDriver.getHandle().hdmarx); } extern "C" void DMA1_Channel3_IRQHandler(void) { HAL_DMA_IRQHandler(spiDriver.getHandle().hdmatx); } extern "C" void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { spiDriver.dmaTransferCompletedCB(); } extern "C" void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) { spiDriver.dmaTransferCompletedCB(); }


Запускаем, проверяем. Дабы не мучать флешку я решил отлаживать на чтении большого файла, а не на записи. Тут я обнаружил очень интересный момент: скорость чтения в не-DMA версии была порядка 250-260 кб/с, тогда как с DMA всего 5!!! Более того, потребление процессора без использования DMA было 3%, а с DMA - 75-80%!!! Т.е. результат прямо противоположный ожидаемому.

Оффтоп про 3%

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


Обложив логгированием код драйвера чуть ли не через строку я обнаружил проблему: я использовал не ту коллбек функцию. Изначально у меня в коде использовался HAL_SPI_Receive_DMA() и вместе с ним в паре использовался коллбек HAL_SPI_RxCpltCallback. Эта конструкция не работала из-за нюанса с одновременной отсылкой 0xff. Когда я поменял HAL_SPI_Receive_DMA() на HAL_SPI_TransmitReceive_DMA() нужно было заодно менять и коллбек на HAL_SPI_TxRxCpltCallback(). Т.е. по факту чтение проходило, но из-за отсутствия коллбеков скорость регулировалась таймаутом в 100мс.

Починив коллбек все встало на свои места. Загрузка процессора упала до 2.5% (теперь уже честных), а скорость даже подскочила аж до 500кб/с. Правда прескейлер пришлось поставить на 4 - с прескейлером на 2 сыпались ассерты в библиотеке SdFat. Похоже это предел скорости моей карточки.

К сожалению к скорости записи это отношение не имеет. Скорость записи по прежнему была около 50-60кб/с, а загрузка процессора колебалась в диапазоне 60-70%. Но проковырявшись целый вечер, и сделав замеры в разных местах я выяснил, что собственно функция send() моего драйвера (которая записывает один сектор 512 байт) отрабатывает всего за 1-2мс включая ожидание и синхронизацию. Иногда, правда, выстреливает таймаут какой нибудь и запись длится 5-7мс. Но проблема на самом деле не в драйвере а в логике работы с файловой системой FAT.

Поднимаясь на уровень файлов, разделов и кластеров задача записи 512 в файл не такая уж и тривиальная. Нужно прочитать таблицу FAT, найти в ней место для записываемого сектора, записать сам сектор, обновить записи в таблице FAT, записать и эти сектора на диск, обновить записи в таблице файлов и директорий, и еще кучу всего другого. В общем один вызов FatFile::write() мог занимать до 15-20мс, причем здоровенный кусок этого времени занимает собственно работа процессора по обработке записей в файловой системе.

Как я уже отметил, загрузка процессора при записи составляет 60-70%. Но это число также зависит и от типа файловой системы (Fat16 или Fat32), размера, а соответственно и количества этих кластеров на разделе, скорости самой флешки, забитости и фрагментированности носителя, использовании длинных имен файлов и многого другого. Так что прошу относится к этим замерам как к неким относительным цифрам.

Опять туда: USB с двойной буферизацией

С этим компонентом вышло интересно. В оригинальной реализации USB Serial от STM32GENERIC было некоторое количество недочетов и я его взялся переписывать под себя. Но пока я изучал как работает USB CDC, читал исходники и штудировал документацию, ребята из STM32GENERIC значительно улучшили свою реализацию. Но обо всем по порядку.

Итак, оригинальная реализация меня не устраивала по следующим причинам:

  • Отправка сообщений происходит синхронно. Т.е. банальное побайтное переливание данных из GPS UART в USB ждет отправки каждого отдельного байта. Из-за этого загрузка процессора может доходить до 30-50%, что разумеется очень много (скорость UART’а всего то 9600)
  • Отсутствует всякая синхронизация. При печати сообщений из нескольких потоков на выходе получается лапша из сообщений, которые частично затирают друг друга
  • Переизбыток буферов приема и отправки. Пара буферов объявлены в USB Middleware, но по факту не используются. Еще пара буферов объявлена в классе SerialUSB, но поскольку я использую только вывод, то приемный буфер только зря занимает память.
  • Наконец, меня просто раздражает интерфейс класса Print. Если я, например, хочу вывести строку “текущая скорость XXX км/ч”, то мне нужно сделать аж 3 вызова - для первой части строки, для числа и для остатка строки. Лично мне ближе по духу классический printf. Плюсовые потоки тоже ничего, но нужно смотреть какой именно код генерируется компилятором.
Пока начнем с простого - синхронная отправка сообщений, без синхронизации и форматирования. По факту код я честно слямзил из STM32GENERIC.

Реализация `в лоб`

extern USBD_HandleTypeDef hUsbDeviceFS; void usbDebugWrite(uint8_t c) { usbDebugWrite(&c, 1); } void usbDebugWrite(const char * str) { usbDebugWrite((const uint8_t *)str, strlen(str)); } void usbDebugWrite(const uint8_t *buffer, size_t size) { // Ignore sending the message if USB is not connected if(hUsbDeviceFS.dev_state != USBD_STATE_CONFIGURED) return; // Transmit the message but no longer than timeout uint32_t timeout = HAL_GetTick() + 5; while(HAL_GetTick() < timeout) { if(CDC_Transmit_FS((uint8_t*)buffer, size) == USBD_OK) { return; } } }


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

Разумеется это только заготовка, т.к. эта реализация не решает обозначенных проблем. Что же нужно, чтобы этот код стал асинхронными и неблокирующим? Ну как минимум буфер. Только вот когда этот буфер передавать?

Я думаю тут стОит сделать небольшой экскурс в принципы работы USB. Дело в том, что передачу в USB протоколе может инициировать только хост. Если устройству нужно передать данные в сторону хоста, данные подготавливаются в специальном PMA (Packet Memory Area) буфере и устройство ожидает пока хост заберет эти данные. Подготовкой PMA буфера занимается функция CDC_Transmit_FS(). Буфер этот живет внутри USB периферии, а не в пользовательском коде.

Честно хотел тут нарисовать красивую картинку, но так и не придумал как это лучше отобразить

Но было бы классно реализовать следующую схему. Клиентский код по мере необходимости записывает данные в некий накопительный (пользовательский) буфер. Время от времени приходит хост и забирает все что накопилось в буфере к этому моменту. Это очень похоже на то, что я описал в предыдущем абзаце, но есть один ключевой нюанс: данные находятся в пользовательском буфере, а не в PMA. Т.е. я бы хотел вообще обойтись без вызова CDC_Transmit_FS(), который переливает данные из пользовательского буфера в PMA, а вместо этого ловить коллбек “тут хост пришел, данные спрашивает”.

К сожалению в текущем дизайне USB CDC Middleware такой подход невозможен. Точнее может быть и возможен, но нужно вклиниваться в реализацию драйвера CDC. Я еще недостаточно искушен в протоколах USB, что-бы так делать. К тому же я не уверен, что временнЫх лимитов USB хватит на такую операцию.

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

USB Serial с двойной буферизацией

#define USB_SERIAL_BUFFER_SIZE 256 uint8_t usbTxBuffer; volatile uint16_t usbTxHead = 0; volatile uint16_t usbTxTail = 0; volatile uint16_t usbTransmitting = 0; uint16_t transmitContiguousBuffer() { uint16_t count = 0; // Transmit the contiguous data up to the end of the buffer if (usbTxHead > usbTxTail) { count = usbTxHead - usbTxTail; } else { count = sizeof(usbTxBuffer) - usbTxTail; } CDC_Transmit_FS(&usbTxBuffer, count); return count; } void usbDebugWriteInternal(const char *buffer, size_t size, bool reverse = false) { // Ignore sending the message if USB is not connected if(hUsbDeviceFS.dev_state != USBD_STATE_CONFIGURED) return; // Transmit the message but no longer than timeout uint32_t timeout = HAL_GetTick() + 5; // Protect this function from multiple entrance MutexLocker locker(usbMutex); // Copy data to the buffer for(size_t i=0; i < size; i++) { if(reverse) --buffer; usbTxBuffer = *buffer; usbTxHead = (usbTxHead + 1) % sizeof(usbTxBuffer); if(!reverse) buffer++; // Wait until there is a room in the buffer, or drop on timeout while(usbTxHead == usbTxTail && HAL_GetTick() < timeout); if (usbTxHead == usbTxTail) break; } // If there is no transmittion happening if (usbTransmitting == 0) { usbTransmitting = transmitContiguousBuffer(); } } extern "C" void USBSerialTransferCompletedCB() { usbTxTail = (usbTxTail + usbTransmitting) % sizeof(usbTxBuffer); if (usbTxHead != usbTxTail) { usbTransmitting = transmitContiguousBuffer(); } else { usbTransmitting = 0; } }


Идея этого кода в следующем. Хоть и не удалось словить нотификацию “хост пришел, данных хочет”, оказалось можно организовать коллбек “я данные хосту отправил, можешь следующие наливать”. Получается такой себе двойной буфер - пока устройство ожидает отправки данных из внутреннего PMA буфера, пользовательский код может дописывать байтики в накопительный буфер. Когда отправка данных завершилась накопительный буфер переливается в PMA. Осталось только организовать этот самый коллбек. Для этого нужно чуток подпилить функцию USBD_CDC_DataIn()

Подпиленный USB Middleware

static uint8_t USBD_CDC_DataIn (USBD_HandleTypeDef *pdev, uint8_t epnum) { USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*) pdev->pClassData; if(pdev->pClassData != NULL) { hcdc->TxState = 0; USBSerialTransferCompletedCB(); return USBD_OK; } else { return USBD_FAIL; } }


Кстати говоря функция usbDebugWrite защищена мутексом и должна правильно работать из нескольких потоков. Функцию USBSerialTransferCompletedCB() защищать не стал - она вызывается из прерывания и оперирует volatile переменными. Откровенно говоря, где-то бага тут таки гуляет, очень изредка глотаются символы. Но мне для дебага это не критично. В “продакшен” коде это вызываться не будет.

Опять туда: printf

Пока эта штука умеет оперировать только константными строками. Пора докрутить аналог printf(). Настоящую функцию printf() я использовать не хочу - она тянет за собой лишнего кода килобайт на 12 и “кучу” (heap), которой у меня нет. Я таки нашел свой debug logger, который я когда-то писал для AVR. Моя реализация умеет печатать строки а также числа в десятичном и шестнадцатеричном формате. После некоторого допиливания и тестирования получилось как то так:

Упрощенная реализация printf

// sprintf implementation takes more than 10kb and adding heap to the project. I think this is // too much for the functionality I need // // Below is a homebrew printf-like dumping function which accepts: // - %d for digits // - %x for numbers as HEX // - %s for strings // - %% for percent symbol // // Implementation supports also value width as well as zero padding // Print the number to the buffer (in reverse order) // Returns number of printed symbols size_t PrintNum(unsigned int value, uint8_t radix, char * buf, uint8_t width, char padSymbol) { //TODO check negative here size_t len = 0; // Print the number do { char digit = value % radix; *(buf++) = digit < 10 ? "0" + digit: "A" - 10 + digit; value /= radix; len++; } while (value > 0); // Add zero padding while(len < width) { *(buf++) = padSymbol; len++; } return len; } void usbDebugWrite(const char * fmt, ...) { va_list v; va_start(v, fmt); const char * chunkStart = fmt; size_t chunkSize = 0; char ch; do { // Get the next byte ch = *(fmt++); // Just copy the regular characters if(ch != "%") { chunkSize++; continue; } // We hit a special symbol. Dump string that we processed so far if(chunkSize) usbDebugWriteInternal(chunkStart, chunkSize); // Process special symbols // Check if zero padding requested char padSymbol = " "; ch = *(fmt++); if(ch == "0") { padSymbol = "0"; ch = *(fmt++); } // Check if width specified uint8_t width = 0; if(ch > "0" && ch <= "9") { width = ch - "0"; ch = *(fmt++); } // check the format switch(ch) { case "d": case "u": { char buf; size_t len = PrintNum(va_arg(v, int), 10, buf, width, padSymbol); usbDebugWriteInternal(buf + len, len, true); break; } case "x": case "X": { char buf; size_t len = PrintNum(va_arg(v, int), 16, buf, width, padSymbol); usbDebugWriteInternal(buf + len, len, true); break; } case "s": { char * str = va_arg(v, char*); usbDebugWriteInternal(str, strlen(str)); break; } case "%": { usbDebugWriteInternal(fmt-1, 1); break; } default: // Otherwise store it like a regular symbol as a part of next chunk fmt--; break; } chunkStart = fmt; chunkSize=0; } while(ch != 0); if(chunkSize) usbDebugWriteInternal(chunkStart, chunkSize - 1); // Not including terminating NULL va_end(v); }


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

Производительность данного кода около 150-200 кб/с вместе с передачей через USB и зависит от количества (длины) сообщений, сложности строки формата, а также от размера буфера. Такой скорости вполне достаточно для отправки пару тысяч небольших сообщений в секунду. Самое главное, что вызовы не блокирующие.

Еще тудее: Low Level HAL

В принципе, на этом можно было бы и закончить, но я обратил внимание, что дядьки из STM32GENERIC буквально на днях влили новый HAL . Интересно в нем то, что появилось много файликов в названием stm32f1xx_ll_XXXX.h. В них обнаружилась альтернативная и более низкоуровневая реализация HAL. Т.е. обычный HAL предоставляет достаточно высокоуровневый интерфейс в стиле “возьми вот этот массив и передай мне его вот по этому интерфейсу. О завершении доложи прерыванием”. Напротив, файлики с буквами LL в названии предоставляют более низкоуровневый интерфейс вроде “установи вот эти флаги такого-то регистра”.

Мистика нашего городка

Увидев новые файлы в репозитории STM32GENERIC я захотел скачать полный комплект с сайта ST. Но гуглеж приводил меня только к HAL (STM32 Cube F1) версии 1.4, которая не содержит этих новых файлов. Графический конфигуратор STM32CubeMX также предлагал эту версию. Я поинтересовался у разработчиков STM32GENERIC где они взяли новую версию. К моему удивлению я получил ссылку на ту же самую страницу, только теперь там предлагалось скачать версию 1.6. Гугл тоже вдруг стал “находить” новую версию, а также обновленный CubeMX. Мистика да и только!


Зачем это надо? В большинстве случаев высокоуровневый интерфейс действительно неплохо решает задачу. HAL (Hardware Abstraction Layer) полностью оправдывает свое название - абстрагирует код от регистров процессора и железа. Но в некоторых случаях HAL ограничивает полет фантазии программиста, тогда как используя более низкоуровневые абстракции можно было бы реализовать задачу более эффективно. В моем случае это GPIO и UART.

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

Судя по всему эти низкоуровневые штуки также можно поделить на 2 части:

  • чуть более высокоуровневые функции в стиле обычного HAL - вот тебе структура инициализации, проинициализируй мне, пожалуйста, периферию.
  • Чуть более низкоуровневые сеттеры и геттеры отдельных флагов или регистров. По большей части функции этой группы inline и header-only
По умолчанию первые отключены дефайном USE_FULL_LL_DRIVER. Ну отключены и черт с ними. Будем пользоваться вторыми. После небольшого шаманства я получил вот такой драйвер светодиода

Моргулька на LL HAL

// Class to encapsulate working with onboard LED(s) // // Note: this class initializes corresponding pins in the constructor. // May not be working properly if objects of this class are created as global variables class LEDDriver { const uint32_t pin = LL_GPIO_PIN_13; public: LEDDriver() { //enable clock to the GPIOC peripheral __HAL_RCC_GPIOC_IS_CLK_ENABLED(); // Init PC 13 as output LL_GPIO_SetPinMode(GPIOC, pin, LL_GPIO_MODE_OUTPUT); LL_GPIO_SetPinOutputType(GPIOC, pin, LL_GPIO_OUTPUT_PUSHPULL); LL_GPIO_SetPinSpeed(GPIOC, pin, LL_GPIO_SPEED_FREQ_LOW); } void turnOn() { LL_GPIO_ResetOutputPin(GPIOC, pin); } void turnOff() { LL_GPIO_SetOutputPin(GPIOC, pin); } void toggle() { LL_GPIO_TogglePin(GPIOC, pin); } }; void vLEDThread(void *pvParameters) { LEDDriver led; // Just blink once in 2 seconds for (;;) { vTaskDelay(2000); led.turnOn(); vTaskDelay(100); led.turnOff(); } }


Все очень просто! Приятно то, что тут действительно работа с регистрами и флагами напрямую идет. Нет оверхеда на модуль HAL GPIO, который сам по себе компилируется аж в 450 байт, и управления пинами от STM32GENERIC, который тянет еще на 670 байт. Тут вообще весь класс со всеми вызовами заинлайнился в функцию vLEDThread размером всего то 48 байт!

Управление тактированием через LL HAL я ниасилил. Но это не критично, т.к. вызов __HAL_RCC_GPIOC_IS_CLK_ENABLED() из обычного HAL на самом деле макрос, который всего лишь устанавливает парочку флагов в определенных регистрах.

С кнопочками все также просто

Кнопочки через LL HAL

// Pins assignment const uint32_t SEL_BUTTON_PIN = LL_GPIO_PIN_14; const uint32_t OK_BUTTON_PIN = LL_GPIO_PIN_15; // Initialize buttons related stuff void initButtons() { //enable clock to the GPIOC peripheral __HAL_RCC_GPIOC_IS_CLK_ENABLED(); // Set up button pins LL_GPIO_SetPinMode(GPIOC, SEL_BUTTON_PIN, LL_GPIO_MODE_INPUT); LL_GPIO_SetPinPull(GPIOC, SEL_BUTTON_PIN, LL_GPIO_PULL_DOWN); LL_GPIO_SetPinMode(GPIOC, OK_BUTTON_PIN, LL_GPIO_MODE_INPUT); LL_GPIO_SetPinPull(GPIOC, OK_BUTTON_PIN, LL_GPIO_PULL_DOWN); } // Reading button state (perform debounce first) inline bool getButtonState(uint32_t pin) { if(LL_GPIO_IsInputPinSet(GPIOC, pin)) { // dobouncing vTaskDelay(DEBOUNCE_DURATION); if(LL_GPIO_IsInputPinSet(GPIOC, pin)) return true; } return false; }


C UART все будет поинтереснее. Напомню проблему. При использовании HAL прием нужно было “перезаряжать” после каждого принятого байта. Режим “принимай все подряд” в HAL не предусмотрен. А с LL HAL у нас все должно получится.

Настройка пинов заставила не только призадуматься, но и заглянуть в Reference Manual

Настройка пинов UART

// Init pins in alternate function mode LL_GPIO_SetPinMode(GPIOA, LL_GPIO_PIN_9, LL_GPIO_MODE_ALTERNATE); //TX pin LL_GPIO_SetPinSpeed(GPIOA, LL_GPIO_PIN_9, LL_GPIO_SPEED_FREQ_HIGH); LL_GPIO_SetPinOutputType(GPIOA, LL_GPIO_PIN_9, LL_GPIO_OUTPUT_PUSHPULL); LL_GPIO_SetPinMode(GPIOA, LL_GPIO_PIN_10, LL_GPIO_MODE_INPUT); //RX pin


Переделываем инициализацию UART’а на новые интерфейсы

Инициализация UART

// Prepare for initialization LL_USART_Disable(USART1); // Init LL_USART_SetBaudRate(USART1, HAL_RCC_GetPCLK2Freq(), 9600); LL_USART_SetDataWidth(USART1, LL_USART_DATAWIDTH_8B); LL_USART_SetStopBitsLength(USART1, LL_USART_STOPBITS_1); LL_USART_SetParity(USART1, LL_USART_PARITY_NONE); LL_USART_SetTransferDirection(USART1, LL_USART_DIRECTION_TX_RX); LL_USART_SetHWFlowCtrl(USART1, LL_USART_HWCONTROL_NONE); // We will be using UART interrupt to get data HAL_NVIC_SetPriority(USART1_IRQn, 6, 0); HAL_NVIC_EnableIRQ(USART1_IRQn); // Enable UART interrupt on byte reception LL_USART_EnableIT_RXNE(USART1); // Finally enable the peripheral LL_USART_Enable(USART1);


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

Прерывание UART

// Store received byte inline void charReceivedCB(uint8_t c) { rxBuffer = c; lastReceivedIndex++; // If a EOL symbol received, notify GPS thread that line is available to read if(c == "\n") vTaskNotifyGiveFromISR(xGPSThread, NULL); } extern "C" void USART1_IRQHandler(void) { uint8_t byte = LL_USART_ReceiveData8(USART1); gpsUart.charReceivedCB(byte); }


Размер кода драйвера уменьшился с 1242 до 436 байт, а потребление ОЗУ с 200 до 136 (из них 128 это буфер). По моему неплохо. Жаль только, что это не самая прожорливая часть. Можно было бы еще что нибудь немного подпилить, но на данный момент за потреблением ресурсов я особо не гонюсь - у меня их еще есть. Да и высокоуровневый интерфейс HAL в случае остальной периферии работает весьма неплохо.

Оглядываясь назад

Хотя на старте этой фазы проекта я был настроен скептически на счет HAL, но мне все же удалось переписать всю работу с периферией: GPIO, UART, I2C, SPI и USB. Я глубоко продвинулся в понимании работы этих модулей и попытался передать знание в этой статье. Но это совсем не перевод Reference Manual. Напротив, я работал в контексте настоящего проекта и показал как можно писать драйвера периферии на чистом HAL.

В статье получилась более-менее линейная история. Но на самом деле у меня расплодилось некоторое количество бранчей в которых я одновременно пилил в прямо противоположных направлениях. Утром я мог упереться в проблемы с производительностью какой нибудь ардуино библиотеки и твердо решить все переписывать на HAL, а к вечеру обнаружить, что кто-то уже запилил поддержку DMA в STM32GENERIC и у меня возникало желание бежать назад. Или, например, пару дней бодаться с ардуино интерфейсами пытаясь понять как же удобнее передавать данные по I2C, тогда как на HAL это делается в 2 строки.

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

Что касается арудино и ее клонов. Мне по прежнему нравится этот фреймворк. С ним можно побырику напрототипировать что нибудь не особо утруждая себя чтением мануалов и даташитов. С ардуино, в принципе, можно делать даже конечные устройства, если нет особых требований по быстродействию, потреблению или памяти. В моем случае эти параметры весьма важны, поэтому мне пришлось переехать на HAL.

Начинал я работу на stm32duino. Этот клон действительно заслуживает внимания, если хочется иметь «ардуино» на STM32 и чтобы все работало из коробки. К тому же там внимательно следят за потреблением ОЗУ и флеша. Напротив STM32GENERIC сам по себе толще и базируется на монстрообразном HAL. Зато этот фреймворк активно развивается и гляди еще допилят. В целом могу рекомендовать оба фреймворка с небольшим предпочтением STM32GENERIC ибо HAL и более динамичное развитие в данный момент. К тому же для HAL в интернете полно примеров и всегда можно что нибудь подтюнить под себя.

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

Ладно интерфейс - внутренности тоже заставляют задуматься. Огромные функции с функционалом на все случаи жизни влекут за собой бесполезную трату ресурсов. Причем если с лишним кодом во флеше можно побороться с помощью link time optimization, то огромное потребление ОЗУ лечится разве что переписыванием на LL HAL.

Но расстраивает даже не это, а местами просто наплевательское отношение к ресурсам. Так я обратил внимание огромный перерасход памяти в коде USB Middleware (формально это не HAL, но поставляется в составе STM32Cube). Структуры usb занимают 2.5кб в памяти. При чем структура USBD_HandleTypeDef (544 байта) во многом повторяет PCD_HandleTypeDef из нижнего слоя (1056 байт) - в ней так же определяются эндпоинты. Приемопередающие буферы так же объявлены как минимум в двух местах - USBD_CDC_HandleTypeDef и UserRxBufferFS/UserTxBufferFS.

Дескрипторы вообще объявлены в ОЗУ. Зачем? Они же константные! Почти 400 байт в ОЗУ. Благо часть дескрипторов таки константные (чуть меньше 300 байт). Дескрипторы это неизменяемая информация. А тут есть специальный код, который их патчит, причем, опять же, константой. Да еще и такой, которая там уже вписана. Функции типа SetBuffer почему то принимают не константный буфер, что также мешает положить дискрипторы и некоторые другие штуки во флеш. В чем причина? Оно же фиксится за 10 минут!!!

Или вот, структура инициализации является частью хендла объекта (например i2c). Зачем это хранить после того, как периферия проинициализирована? Зачем мне указатели на неиспользуемые структуры - например зачем данные связанные с DMA, если я его не использую?

А еще дубликаты кода.

case USB_DESC_TYPE_CONFIGURATION: if(pdev->dev_speed == USBD_SPEED_HIGH) { pbuf = (uint8_t *)pdev->pClass->GetHSConfigDescriptor(&len); pbuf = USB_DESC_TYPE_CONFIGURATION; } else { pbuf = (uint8_t *)pdev->pClass->GetFSConfigDescriptor(&len); pbuf = USB_DESC_TYPE_CONFIGURATION; } break;


Особая конвертация в “типа юникод”, которую можно было бы и в компайл тайме делать. Да еще и специальный буфер под это выделен

Издевательство над констатными данными

ALIGN_BEGIN uint8_t USBD_StrDesc __ALIGN_END; void USBD_GetString(const char *desc, uint8_t *unicode, uint16_t *len) { uint8_t idx = 0; if (desc != NULL) { *len = USBD_GetLen(desc) * 2 + 2; unicode = *len; unicode = USB_DESC_TYPE_STRING; while (*desc != "\0") { unicode = *desc++; unicode = 0x00; } } }


Не смертельно, но заставляет задуматься, а так ли хорош HAL как о нем пишут апологеты? Ну не этого ожидаешь от библиотеки от производителя и рассчитанной на профессионалов. Это же микроконтроллеры! Тут люди каждый байт экономят и каждая микросекунда дорога. А тут, понимаешь, буфер на полкило и конвертация константных строк на лету. Стоит отметить, что бОльшая часть замечаний к USB Middleware относится.

UPD: в HAL 1.6 еще и I2C DMA Transfer Completed callback отламали. Т.е. там вообще исчез код, который в случае отсылки данных через DMA подтверждение генерит, хотя в документации оно описано. На прием есть, а на передачу нет. Пришлось переехать обратно на HAL 1.4 для модуля I2C, благо тут один модуль - один файл.

Напоследок приведу потребление флеша и ОЗУ различных компонентов. В разделе Drivers я привел значения как для драйверов на базе HAL, так и для драйверов на LL HAL. Во втором случае соответствующие секции из раздела HAL не используются.

Потребление по памяти

Category Subcategory .text .rodata .data .bss
System interrupt vector 272
dummy ISR handlers 178
libc 760
float math 4872
sin/cos 6672 536
main & etc 86
My Code My Code 7404 833 4 578
printf 442
Fonts 3317
NeoGPS 4376 93 300
FreeRTOS 4670 4 209
Adafruit GFX 1768
Adafruit SSD1306 1722 1024
SdFat 5386 1144
USB Middleware Core 1740 333 2179
CDC 772
Drivers UART 268 200
USB 264 846
I2C 316 164
SPI 760 208
Buttons LL 208
LED LL 48
UART LL 436 136
Arduino gpio 370 296 16
misc 28 24
Print 822
HAL USB LL 4650
SysTick 180
NVIC 200
DMA 666
GPIO 452
I2C 1560
SPI 2318
RCC 1564 4
UART 974
heap (not really used) 1068
FreeRTOS Heap 10240

На этом все. Буду рад получить конструктивные комментарии, а также рекомендации если что нибудь тут можно улучшить.

Теги:

  • HAL
  • STM32
  • STM32cube
  • arduino
Добавить метки

До этого момента мы использовали стандартную библиотеку ядра - CMSIS. Для настройки какого-либо порта на нужный режим работы нам приходилось обращаться к , чтобы найти отвечающий за определенную функцию регистр, а также искать по большому документу другую связанную с этим процессом информацию. Дело примет еще большие мучительные и рутинные обороты, когда мы приступим к работе с таймером или АЦП. Количество регистров там значительно больше, чем у портов ввода-вывода. Ручная настройка отнимает немало времени и повышает шанс допустить ошибку. Поэтому многие предпочитают работать со стандартной библиотекой периферии - StdPeriph. Что же она дает? Всё просто - повышается уровень абстракции, вам не нужно лезть в документацию и думать о регистрах в большинстве своеём. В этой библиотеке все режимы работы и параметры периферии МК описаны в виде структур. Теперь для настройки периферийного устройства необходимо лишь вызвать функцию инициализации устройства с заполненной структурой.

Ниже приведена картинка со схематичным изображением уровней абстракции.

Мы работали с CMSIS (которая находится «ближе» всего к ядру), чтобы показать, как устроен микроконтроллер. Следующим шагом является стандартная библиотека, пользоваться которой мы научимся сейчас. Дальше идут драйвера устройств. Под ними понимаются *.c \ *.h -файлы, которые обеспечивают удобный программный интерфейс для управления каким-либо устройством. Так, например, в этом курсе мы предоставим вам драйверы для микросхемы max7219 и WiFi-модуля esp8266.

Стандартный проект будет включать в себя следующие файлы:


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

Файлы библиотеки можно найти на странице, посвященной целевому МК (для нас это stm32f10x4), в разделе Design Resources (в среде CooCox IDE эти файлы скачиваются из репозитория среды разработки). Каждой периферии соответствуют два файла - заголовочный (*.h) и исходного кода (*.c). Детальное описание можно найти в файле поддержки, который лежит в архиве с библиотекой на сайте.

  • stm32f10x_conf.h - файл конфигурации библиотеки. Пользователь может подключить или отключить модули.
  • stm32f10x_ppp.h - заголовочный файл периферии. Вместо ppp может быть gpio или adc.
  • stm32f10x_ppp.c - драйвер периферийного устройства, написанный на языке Си.
  • stm32f10x_it.h - заголовочный файл, включающий в себя все возможные обработчики прерываний (их прототипы).
  • stm32f10x_it.c - шаблонный файл исходного кода, содержащий сервисные рутинные прерывания (англ. interrupt service routine , ISR) для исключительных ситуаций в Cortex M3. Пользователь может добавить свои ISR для используемой периферии.

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

  • PPP - акроним для периферии, например, ADC.
  • Системные, заголовочные файлы и файлы исходного кода - начинаются с stm32f10x_ .
  • Константы, используемые в одном файле, определены в этом файле. Константы, используемые в более чем одном файле, определены в заголовочных файлах. Все константы в библиотеке периферии чаще всего написаны в ВЕРХНЕМ регистре.
  • Регистры рассматриваются как константы и именуются также БОЛЬШИМИ буквами.
  • Имена функций, относящихся к периферии, содержат акроним, например, USART_SendData() .
  • Для настройки каждого периферийного устройства используется структура PPP_InitTypeDef , которая передается в функцию PPP_Init() .
  • Для деинициализации (установки значения по умолчанию) можно использовать функцию PPP_DeInit() .
  • Функция, позволяющая включить или отключить периферию, именуется PPP_Cmd() .
  • Функция включения/отключения прерывания именуется PPP_ITConfig .

С полным списком вы опять же можете ознакомиться в файле поддержки библиотеки. А теперь давайте перепишем мигание светодиода с использованием стандартной библиотеки периферии!

Перед началом работы заглянем в файл stm32f10x.h и найдем строчку:

#define USE_STDPERIPH_DRIVER

Если вы будете настраивать проект с нуля, используя файлы библиотеки из скачанного архива, то вам будет необходимо раскомментировать данную строчку. Она позволит использовать стандартную библиотеку. Данное определение (макрос) скомандует препроцессору подключить файл stm32f10x_conf.h:

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

В этом файле подключаются модули. Если вам нужны только конкретные - отключите остальные, это сэкономит время при компиляции. Нам, как вы уже могли догадаться, нужны модули RTC и GPIO (однако в будущем потребуются также _bkp.h , _flash , _pwr.h , _rtc.h , _spi.h , _tim.h , _usart.h):

#include "stm32f10x_flash.h" // for init_pll() #include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h"

Как и в прошлый раз, для начала нужно включить тактирование порта B. Делается это функцией, объявленной в stm32f10x_rcc.h:

Void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState);

Перечисление FunctionalState определено в stm32f10x.h:

Typedef enum {DISABLE = 0, ENABLE = !DISABLE} FunctionalState;

Объявим структуру для настройки нашей ножки (найти её можно в файле stm32f10x_gpio.h):

GPIO_InitTypeDef LED;

Теперь нам предстоит её заполнить. Давайте посмотрим на содержание этой структуры:

Typedef struct { uint16_t GPIO_Pin; GPIOSpeed_TypeDef GPIO_Speed; GPIOMode_TypeDef GPIO_Mode; } GPIO_InitTypeDef;

Все необходимые перечисления и константы можно найти в этом же файле. Тогда переписанная функция init_leds() примет следующий вид:

Void led_init() { // Включаем тактирование RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // Объявляем структуру и заполняем её GPIO_InitTypeDef LED; LED.GPIO_Pin = GPIO_Pin_0; LED.GPIO_Speed = GPIO_Speed_2MHz; LED.GPIO_Mode = GPIO_Mode_Out_PP; // Инициализируем порт GPIO_Init(GPIOB, &LED); }

Перепишем функцию main() :

Int main(void) { led_init(); while (1) { GPIO_SetBits(GPIOB, GPIO_Pin_0); delay(10000000); GPIO_ResetBits(GPIOB, GPIO_Pin_0); delay(10000000); } }

Главное - прочувствовать порядок инициализации: включаем тактирование периферии, объявляем структуру, заполняем структуру, вызываем метод инициализации. Другие периферические устройства обычно настраиваются по подобной схеме.

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

Ниже представлен рисунок платы STM32F3 Discovery , где: 1 — MEMS датчик. 3-осевой цифровой гироскоп L3GD20. 2 — МЕМС система-в-корпусе, содержащая 3-осевой цифровой линейный акселерометр и 3-осевой цифровой геомагнитный сенсор LSM303DLHC. 4 – LD1 (PWR) – питание 3.3V. 5 – LD2 – красный/зеленый светодиод. По умолчанию красный. Зеленый означает связь между ST-LINK/v2 (or V2-B) и ПК. У меня ST-LINK/v2-B, а также индикации пользовательского порта USB. 6. -LD3/10 (red), LD4/9 (blue), LD5/8 (orange) and LD6/7 (green). В прошлой записи мы с Вами мигали светодиодом LD4. 7. – Две кнопки: пользовательская USER и сброса RESET. 8. — USB USER with Mini-B connector.

9 — USB отладчик/программатор ST-LINK/V2. 10. — Микроконтроллер STM32F303VCT6. 11. — Внешний высокочастотный генератор 8 Мгц. 12. – Здесь должен быть низкочастотный генератор, к сожелению не запаян. 13. – SWD – интерфейс. 14. – Джемперы для выбора программирования внешних контроллеров или внутреннего, в первом случае должны быть удалены. 15 – Джемпер JP3 – перемычка, предназначена для подключения амперметра, что б измерить потребление контроллера. Понятно если она удалена, то наш камушек не запустится. 16. – STM32F103C8T6 на нем находится отладочная плата. 17. — LD3985M33R Регулятор с низким падением напряжения и уровнем шума, 150мА, 3.3В.

Теперь познакомимся поближе с архитектурой микроконтроллера STM32F303VCT6. Его техническая характеристика: корпус LQFP-100, ядро ARM Cortex-M4, максимальная частота ядра 72МГц, объём памяти программ 256 Кбайт, тип памяти программ FLASH, объём оперативной памяти SRAM 40 кбайт, RAM 8 кбайт, количество входов/выходов 87, интерфейсы (CAN, I²C, IrDA, LIN, SPI, UART/USART, USB), периферия (DMA, I2S, POR, PWM, WDT), АЦП/ЦАП 4*12 bit/2*12bit, напряжение питания 2 …3.6 В, рабочая температура –40 …...+85 С. На рисунке ниже распиновка, где видим 87 портов ввода/вывода, 45 из них Normal I/Os (TC, TTa), 42 5-volt tolerant I/Os (FT, FTf) – совместимые с 5 В. (на плате справа 5В выводы, слева 3,3В). Каждая цифровая линия ввода-вывода может выполнять функцию линии ввода-вывода общего
назначения или альтернативную функцию. По ходу продвижения проектов мы с Вами будем последовательно знакомится с периферией.

Рассмотрим блок диаграмму ниже. Сердцем является 32-битное ядро ARM Cortex-M4, работающее до 72 МГц. Имеет встроенный блок с плавающей запятой FPU и блок защиты памяти MPU, встроенные макро-ячейки трассировки — Embedded Trace Macrocell (ETM), которые могут быть использованы для наблюдения за процессом выполнения основной программы внутри микроконтроллера. Они способны непрерывно выводить данные этих наблюдений через контакты ETM до тех пор, пока устройство работает. NVIC (Nested vectored interrupt controller) – модуль контроля прерываний. TPIU (Trace Port Interface Unit). Содержит память FLASH –256 Кбайт, SRAM 40 кбайт, RAM 8 кбайт. Между ядром и памятью расположена Bus matrix (Шинная матрица), которая позволяет соединить устройства напрямую. Также здесь видим два типа шинной матрицы AHB и APB, где первая более производительная и используется для связи высокоскоростных внутренних компонентов, а последняя для периферии (устройств ввода/вывода). Контроллер имеет 4 –ри 12-разрядных ADC (АЦП) (5Мбит/с) и датчик температуры, 7 компараторов (GP Comparator1 …7), 4-ри программируеммых операционных усилителя (OpAmp1…4) (PGA (Programmable Gain Array)), 2 12 разрядных канала DAC (ЦАП), RTC (часы реального времени), два сторожевых таймера — независимый и оконный (WinWatchdog and Ind. WDG32K), 17 таймеров общего назначения и многофункциональные.

В общих чертах мы рассмотрели архитектуру контроллера. Теперь рассмотори доступные программные библиотеки. Сделав обзор можна выделить следующие: CMSIS, SPL и HAL. Рассмотрим каждую применив в простом примере мигая светодиодом.

1). CMSIS (Cortex Microcontroller Software Interface Standard) - стандартная библиотека для Cortex®-M. Обеспечивает поддержку устройств и упрощает программные интерфейсы. CMSIS предоставляет последовательные и простые интерфейсы для ядра, его периферии и операционных систем реального времени. Ее использования является профессиональным способом написания программ, т.к. предполагает прямую запись в регистры и соответственно необходимо постоянное чтение и изучение даташитов. Независим от производителя аппаратного уровня.
CMSIS включает в себя следующие компоненты:
— CMSIS-CORE: Consistent system startup and peripheral access (Постоянный запуск системы и периферийный доступ);
— CMSIS-RTOS : Deterministic Real-Time Software Execution (Детерминированное исполнение программного обеспечения реального времени);
— CMSIS-DSP: Fast implementation of digital signal processing (Быстрая реализация цифровой обработки сигналов);
— CMSIS-Driver: Generic peripheral interfaces for middleware and application code (Общие периферийные интерфейсы для промежуточного программного обеспечения и кода приложения);
— CMSIS-Pack : Easy access to reusable software components (Легкий доступ к многократно используемым программным компонентам);
— CMSIS-SVD: Consistent view to device and peripherals (Согласованное представление устройства и периферийных устройств);
— CMSIS-DAP: Connectivity to low-cost evaluation hardware (Возможность подключения к недорогому оборудованию для оценки). ПО для отладки.

Для примера напишем программу – мигаем светодиодом. Для этого нам понадабится документация описывающая регистры. В моем случае RM0316 Reference manual STM32F303xB/C/D/E, STM32F303x6/8, STM32F328x8, STM32F358xC, STM32F398xE advanced ARM ® -based MCUs , а также описние конткретной ножки за что отвечает DS9118 : ARM®-based Cortex®-M4 32b MCU+FPU, up to 256KB Flash+ 48KB SRAM, 4 ADCs, 2 DAC ch., 7 comp, 4 PGA, timers, 2.0-3.6 V. Для начала в программе затактируем порт, т.к. по умолчанию все отключено чем достигается уменьшенное эрергопотребление. Открываем Reference manual и смотрим раздел Reset and clock control далее RCC register map и смотрим что за включение IOPEEN отвечает регистр

Перейдем в описание тактирования перефирии данного регистра AHB peripheral clock enable register (RCC_AHBENR) , где видим что данный порт находится под 21-м битом. Включаем его RCC->AHBENR|=(1<<21) . Далее сконфигурируем регистры GPIO. Нас интересует три: GPIOE_MODER и GPIOx_ODR . C помощью них повторим программу с предыдущей статьи, затактируем PE8. Первый отвечает за конфигурацию входа выхода, выбираем 01: General purpose output mode. GPIOE->MODER|=0×10000 . Второй за включение низкого/высокого уровня на ножке. Ниже программа:

#include "stm32f3xx.h" //Заголовочный файл микроконтроллера
unsigned int i;
void delay () {
for (i=0;i<500000;i++);
}
int main (void) {
RCC->AHBENR|=(1<<21);
GPIOE->MODER|=0×10000;
while (1){
delay ();
GPIOE->ODR|=0×100;
delay ();
GPIOE->ODR&=~(0×100);
} }

2). SPL (Standard Peripherals Library) - данная библиотека предназначена для объединения всех процессоров фирмы ST Electronics. Разработана для урощения преносимости кода и в первую очередь расчитана на начинающег разаработчика. ST работала над заменой SPL под названием «low layer», которая совместима с HAL. Драйверы Low Layer (LL) разработаны для обеспечения почти легкого экспертно-ориентированного уровня, который ближе к оборудованию, чем HAL. В дополнение к HAL также доступны LL API . Пример тойже программы на SPL.

#include
#include
#include
#define LED GPIO_Pin_8
int main() {
long i;
GPIO_InitTypeDef gpio;
// Blue LED is connected to port E, pin 8 (AHB bus)
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOE, ENABLE);
// Configure port E (LED)
GPIO_StructInit(&gpio); //объявляем и инициализируем переменную структуры данных
gpio.GPIO_Mode = GPIO_Mode_OUT;
gpio.GPIO_Pin = LED;
GPIO_Init(GPIOE, &gpio);
// Blinking LEDS
while (1) {
// On
GPIO_SetBits(GPIOE, LED);
for (i = 0; i < 500000; i++);
// All off
GPIO_ResetBits(GPIOE, LED);
for (i = 0; i < 500000; i++);
} }

Каждая функция описывается в технической документации UM1581 User manual Description of STM32F30xx/31xx Standard Peripheral Library . Здесь подключаем три заголовочных файла которые содержат необходимоые данные, структуры, функции управления сбросом и синхронизацией, а также для конфигурации портов ввода/вывода.

3). HAL - (Hardware Acess Level , Hardware Abstraction Layer) - Еще одна общая библиотека для разработки. С которой вышла и программа CubeMX для конфигурации, которую мы с Вами использовали в прошлой статье. Там же мы написали программу мигания светодиодом используя данную библиотеку. Как видим на рисунке, ниже, куб генерирует драйвера HAL and CMSIS. Что ж опишем основные используемые файлы:
— system_stm32f3x.c и system_stm32f3x.h - предоставляют минимальные наборы функций для конфигурации системы тактирования;
— core_cm4.h – предоставляет доступ к регистрам ядра и его периферии;
— stm32f3x.h - заголовочный файл микроконтроллера;
— startup_system32f3x.s — стартовый код, содержит таблица векторов прерываний и др.

#include «main.h»
#include «stm32f3xx_hal.h»
void SystemClock_Config (void); /*Объявляем функции конфигурации тактирования*/
static void MX_GPIO_Init (void); /*Инициализация ввода/вывода*/
int main (void) {
/*Reset of all peripherals, Initializes the Flash interface and the Systick.*/
HAL_Init ();
/* Configure the system clock */
SystemClock_Config ();
/* Initialize all configured peripherals */
MX_GPIO_Init ();
while (1) {
HAL_GPIO_TogglePin (GPIOE, GPIO_PIN_8);//Переключаем состояние ножки
HAL_Delay (100); }
}
void SystemClock_Config (void) {
RCC_OscInitTypeDef RCC_OscInitStruct;
RCC_ClkInitTypeDef RCC_ClkInitStruct;

RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.HSICalibrationValue = 16;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;
if (HAL_RCC_OscConfig (&RCC_OscInitStruct) != HAL_OK) {

}
/**Initializes the CPU, AHB and APB busses clocks */
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSI;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
if (HAL_RCC_ClockConfig (&RCC_ClkInitStruct, FLASH_LATENCY_0) != HAL_OK) {
_Error_Handler (__FILE__, __LINE__);
}
/**Configure the Systick interrupt time*/
HAL_SYSTICK_Config (HAL_RCC_GetHCLKFreq ()/1000);
/**Configure the Systick */
HAL_SYSTICK_CLKSourceConfig (SYSTICK_CLKSOURCE_HCLK);
/* SysTick_IRQn interrupt configuration */
HAL_NVIC_SetPriority (SysTick_IRQn, 0, 0);
}
/** Configure pins as Analog Input Output EVENT_OUT EXTI */
static void MX_GPIO_Init (void) {
GPIO_InitTypeDef GPIO_InitStruct;
/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOE_CLK_ENABLE ();
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin (GPIOE, GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_10|GPIO_PIN_11
|GPIO_PIN_12|GPIO_PIN_13|GPIO_PIN_14|GPIO_PIN_15, GPIO_PIN_RESET);
/*Configure GPIO pins: PE8 PE9 PE10 PE11 PE12 PE13 PE14 PE15 */
GPIO_InitStruct.Pin = GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_10|GPIO_PIN_11
|GPIO_PIN_12|GPIO_PIN_13|GPIO_PIN_14|GPIO_PIN_15;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init (GPIOE, &GPIO_InitStruct);
}
void _Error_Handler (char * file, int line) {
while (1) {
} }
#ifdef USE_FULL_ASSERT

Void assert_failed (uint8_t* file, uint32_t line) {
}
#endif
Здесь также как и в предыдущем примере можем просмотреть описание каждой функции в документации, например UM1786 User Manual Description of STM32F3 HAL and low-layer drivers.

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