Arduino <-> STM32 HAL, или туда и обратно

…., а потом еще раз туда, и еще раз обратно… В общем «тудов» и «обратнов» у меня было достаточно много.

3150e8f501664c37826ded17e56a8043.png

Свой проект GPS Logger«a я начал на платформе Ардуино. Постепенно я вырос до контроллера STM32F103, но код остался на базе клона ардуины — stm32duino. Что именно я строю, зачем, какие библиотеки использую и прочие вопросы по самому устройству я сегодня оставлю за кадром — все это я описывал в упомянутых статьях (есть еще третья часть про билдсистему). Сегодняшняя тема — переезд на HAL (он же STM32Cube).

Время от времени в комментариях к моим статьям, а также в личных беседах с коллегами по цеху возникает вопрос «а почему ардуино? Есть же HAL!». Сначала я отмахивался, мол, у меня уже код на ардуиновских библиотеках, не хочу переписывать. И еще мне HAL не понравился своим некрасивым и громоздким стилем. Но врожденное любопытство подстегнуло все таки посмотреть еще разок на HAL с разных сторон.

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

Анализ архитектуры

Итак, архитектура, которая была вначале работы:
750b639278ab4ab58ae0148253a6c930.png

Системный слой реализуется библиотекой libmaple производства Leaf Labs. В ней происходит вся работа с регистрами, инициализация платы и другие низкоуровневые штуки. Библиотека STM32duino реализует интерфейс ардуино и базируется на libmaple. Библиотеки прикладного уровня построены в основном на STM32duino, но иногда спускаются на уровень libmaple для каких-то кастомных низкоуровневых вызовов (например FreeRTOS работает с SysTick таймером).

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

Но есть нюанс. Компания Leaf Labs скисла году в 2012 и потому библиотека libmaple поддерживается только силами комьюнити (stm32duino комьюнити!). С одной стороны там вроде как вылизали кучу багов, а саму библиотеку неплохо оптимизировали, но с другой стороны поддержки новых микроконтроллеров (как и допиливания новых фич к старым) ждать можно долго.

То ли дело HAL — выпускается самой ST, есть поддержка всего чего только движется, доступен удобный графический конфигуратор STM32CubeMX, есть большое (и профессиональное) сообщество. В общем, один шоколад! Вот только выкидывать все наработки и начинать все с нуля на HAL мне как то совершенно не хотелось. Я принялся искать порт arduino поверх HAL.

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

А вот порт STM32GENERIC выглядел поинтереснее. Кода там было существенно больше, влит свежий HAL и CMSIS, частые коммиты и пулл реквесты в мейнлайн — это все вселяло надежду. Но эти надежды тут же разбились, когда я попробовал скомпилировать свой проект с STM32GENERIC. При сборке сыпались тонны ворнингов, а в некоторых местах и вовсе не компилировалось. Может просто не вовремя скачал?

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

Итак, как меняется архитектура с STM32GENERIC?
545829f5acac479fb725944f8d3f1f54.png

А никак! Все практически тоже самое. Только вместо libmaple — HAL, вместо stm32duino — STM32GENERIC. Да, можно писать на фреймворке ардуино, сдабривая это все кодом на HAL, но общая архитектура оставалась такой же. В ней мне не нравились следующие моменты:

  • Инициализация платы (кусочек на схеме под названием board init) и main () находится в STM32GENERIC. В большинстве случаев этого более чем достаточно. Но я планирую использовать различные энергосберегающие режимы, а для этого нужно уметь корректно управлять тактированием и инициализацией контроллера. Выход из некоторых режимов подразумевает ресет ядра МК, а значит мне нужно быть поближе к этому ресету.
  • Я бы хотел, чтобы мое устройство одновременно реализовывало USB CDC (виртуальный СОМ порт) и Mass Storage Class Device. В реализации STM32GENERIC слой работы с USB зарыт достаточно глубоко и реализовывает либо CDC, либо MSC (переключается дефайном), тогда как я хотел бы иметь больше контроля над этим куском, чтобы реализовать оба интерфейса одновременно.
  • Наконец, ардуино оно как швейцарский нож — уступает отдельным специализированным инструментам, зато универсально. Как следствие имеем кучу кода, который там присутствует просто на всякий случай. Так, например, код по управлению GPIO содержит ссылки на АЦП, таймеры и ШИМ, просто потому, что выводы ардуино могут выполнять эти функции. У меня же вся периферия заранее распределена и может быть инициализирована более эффективно.


Я бы предпочел что нибудь типа такого.
35b8b5f6cea14904930423b19e45cd68.png

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

На счет библиотек. Я использую NeoGPS (парсер NMEA потока) и Adafruit GFX (графическая библиотека + драйвер дисплея на контроллере SSD1306). Эти библиотеки хорошо написаны и отлажены, они хорошо делают свою работу. Я не вижу смысла от них отказываться и переписывать клиентский код под что нибудь другое (которое еще нужно и протестировать). Также я нахожусь в поиске библиотеки для работы с SD картой. Пробовал библиотеку SD из комплекта Ардуины, но там жуть. Сейчас я активно смотрю в сторону библиотеки SdFat.

Туда: Инициализация платы


Сказано — сделано. Разумеется все и сразу спортировать на HAL не представляется возможным. Зато возможно портирование по кусочку. Первым делом я закомментировал в своем проекте весь код и отключил все библиотеки, оставив только main (). Функции инициализации платы из STM32GENERIC я тоже закомментировал и начал понемногу копировать нужные штуки к себе в main (). В качестве полезной нагрузки я добавил моргалку светодиодом. Довольно быстро вся эта конструкция скомпилировалась и слинковалась. Только не заработала.

Что именно мешало работе было не очевидно. Китайский ST-Link у меня не завелся. Нужно было искать причину каким нибудь другим способом. Я решил зайти с другой стороны — в CubeMX создать моргалку с нуля. При том, что сам код был практически идентичным, реализация имени CubeMX работала, а моя нет. В течении двух вечеров я сводил одну реализацию к другой, копировал код туда-сюда. В итоге я таки смог завести моргалку в своем проекте. Не могу сказать, что я нашел какую-то фундаментальную проблему. Скорее это был набор мелких косяков, без которых ничего не работало

  • Стартовый адрес флеша неверно пробрасывался из настроек проекта в код. Это важно, т.к. первые 8 кб флеша занимает бутлоадер, поэтому контроллеру прерываний нужно сказать, что таблица векторов чуток переехала.
  • Хендлеры прерываний у меня жили в .cpp файлах, но я им забыл сказать extern «C». Без этого функции манглились по другому и не перекрывали соответствующие weak функции из HAL.
  • Какой-то код был лишним, где-то наоборот не хватало какой то мелочи

Итак, инициализация платы готова. БОльшая часть кода сгенерирована CubeMX

Инициализация платы
// Set up board clocks
void SystemClock_Config(void)
{
        // Set up external oscillator to 72 MHz
        RCC_OscInitTypeDef RCC_OscInitStruct;
        RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
        RCC_OscInitStruct.HSEState = RCC_HSE_ON;
        RCC_OscInitStruct.LSEState = RCC_LSE_OFF;
        RCC_OscInitStruct.HSIState = RCC_HSI_ON;
        RCC_OscInitStruct.HSICalibrationValue = 16;
        RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
        RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
        RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
        RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
        HAL_RCC_OscConfig(&RCC_OscInitStruct);

        // Set up periperal clocking
        RCC_ClkInitTypeDef RCC_ClkInitStruct;
        RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                                                                  |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
        RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
        RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
        RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
        RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
        HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2);

        // Set up USB clock
        RCC_PeriphCLKInitTypeDef PeriphClkInit;
        PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USB;
        PeriphClkInit.UsbClockSelection = RCC_USBCLKSOURCE_PLL_DIV1_5;
        HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit);

        // Set up SysTTick to 1 ms
        // TODO: Do we really need this? SysTick is initialized multiple times in HAL
        HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);
        HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);

        // SysTick_IRQn interrupt configuration
        HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);
}

// Handle SysTick timer
extern "C" void SysTick_Handler(void)
{
 HAL_IncTick();
 HAL_SYSTICK_IRQHandler();
}

void InitBoard()
{
        // Initialize board and HAL
        HAL_Init();
        SystemClock_Config();

        HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
}

Туда: лампочки и кнопочки


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

По началу получалось весьма симпатично (насколько симпатичен может быть такой громоздкий код). Функционал управления диодом переехал в свой собственный класс.

Драйвер светодиода
#define LED_PORT GPIOC 
const uint16_t LED_PIN = GPIO_PIN_13;

// 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
{
public:
        LEDDriver()
        {
                // enable clock to GPIOC
                __HAL_RCC_GPIOC_CLK_ENABLE();

                // Turn off the LED by default
                HAL_GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_SET);

                // Initialize PC13 pin
                GPIO_InitTypeDef ledPinInit;
                ledPinInit.Pin = LED_PIN;
                ledPinInit.Mode = GPIO_MODE_OUTPUT_PP;
                ledPinInit.Speed = GPIO_SPEED_FREQ_LOW;
                HAL_GPIO_Init(LED_PORT, &ledPinInit);
        }

        void turnOn()
        {
                HAL_GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_RESET);
        }

        void turnOff()
        {
                HAL_GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_SET);
        }

        void toggle()
        {
                HAL_GPIO_TogglePin(LED_PORT, LED_PIN);
        }
};



Сама моргалка тривиальна и красива

Моргалка
int main(void)
{
        InitBoard();

        LEDDriver led;
        while(1)
        {
                HAL_Delay(500);
                led.toggle();
        }
}



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

Драйвер кнопочек
// Pins assignment
#define BUTTONS_PORT GPIOC
const uint16_t SEL_BUTTON_PIN = GPIO_PIN_14;
const uint16_t OK_BUTTON_PIN = GPIO_PIN_15;

// Initialize buttons related stuff
void initButtons()
{
        // enable clock to GPIOC
        __HAL_RCC_GPIOC_CLK_ENABLE();

        // Initialize button pins
        GPIO_InitTypeDef pinInit;
        pinInit.Mode = GPIO_MODE_INPUT;
        pinInit.Pull = GPIO_PULLDOWN;
        pinInit.Speed = GPIO_SPEED_FREQ_LOW;
        pinInit.Pin = SEL_BUTTON_PIN | OK_BUTTON_PIN;
        HAL_GPIO_Init(BUTTONS_PORT, &pinInit);
…
}

// Reading button state (perform debounce first)
inline bool getButtonState(uint16_t pin)
{
        if(HAL_GPIO_ReadPin(BUTTONS_PORT, pin))
        {
                // dobouncing
                vTaskDelay(DEBOUNCE_DURATION);
                if(HAL_GPIO_ReadPin(BUTTONS_PORT, pin))
                        return true;
        }
        
        return false;
}



Практика показывает, что потребление памяти нужно контролировать на каждом этапе. В текущем варианте прошивка занимала примерно 3.5к, из них порядка 2.5к это HAL (из них почти 2 кб занимает инициализация тактирования). Остальное — код инициализации платы и вектора прерываний. Многовато как для «простых оберток над регистрами», но терпимо. При желании можно включить link time optimization и тогда размер прошивки уменьшается до 1.8к. Штука интересная, но дизассемблированный код становится почти не читабельный. Оставил как есть на время разработки, этот флаг можно включить в самом конце, когда все будет готово.

Туда: FreeRTOS


Но на одних только лампочках и кнопках далеко не уедешь. Следующей частью которую я решил раскомментировать стал FreeRTOS. Мне хотелось по максимуму оторвать свой код от STM32GENERIC, потому я решил попробовать вкрутить FreeRTOS с нуля, вместо того, чтобы использовать копию из STM32GENERIC — мало ли что они там наменяли.

Скачав исходники с сайта FreeRTOS я принялся прикручивать их по инструкции. Для этого требовалось развернуть исходники, подложить пару файлов, специфичных для соответствующей платформы (port.c и portmacro.h). Также нужно не забыть установить свои настройки в файле конфигурации (FreeRTOSConfig.h), а также объявить парочку обработчиков нештатных ситуаций у себя в коде (vApplicationStackOverflowHook () и vApplicationMallocFailedHook ()) — без них не слинкуется.

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

/* Definitions that map the FreeRTOS port interrupt handlers to their CMSIS
standard names. */
#define vPortSVCHandler SVC_Handler
#define xPortPendSVHandler PendSV_Handler
#define xPortSysTickHandler HAL_SYSTICK_Callback


FreeRTOS запустилась без проблем, но меня смущал один ворнинг линковки (вообще-то 3 одинаковых)

ld.exe: warning: changing start of section .text by 4 bytes

Гуглеж ничего полезного не выдавал. Единственный более-менее релевантный совет предлагал поменять выравнивание в скрипте линковки определенных секций с 4 байт на 8, но совет не помог. Я прошерстил весь платформенно-зависимый код FreeRTOS по слову align и нашел его в двух кусках ассемблерного кода (как раз обработчики прерываний).

Обработчик прерывания
void vPortSVCHandler( void )
{
        __asm volatile (
        "       ldr     r3, pxCurrentTCBConst2          \n" /* Restore the context. */
        "       ldr r1, [r3]                                    \n" /* Use pxCurrentTCBConst to get the pxCurrentTCB address. */
        "       ldr r0, [r1]                                    \n" /* The first item in pxCurrentTCB is the task top of stack. */
        "       ldmia r0!, {r4-r11}                             \n" /* Pop the registers that are not automatically saved on exception entry and the critical nesting count. */
        "       msr psp, r0                                             \n" /* Restore the task stack pointer. */
        "       isb                                                             \n"
        "       mov r0, #0                                              \n"
        "       msr     basepri, r0                                     \n"
        "       orr r14, #0xd                                   \n"
        "       bx r14                                                  \n"
        "                                                                       \n"
        "       .align 4                                                \n"
        "pxCurrentTCBConst2: .word pxCurrentTCB                         \n"
        );
}


К сожалению никто их моих знакомых не знаком с ассемблером ARM и не мог пояснить суть этой строки. Но сравнив эти куски с аналогичным из stm32duino я увидел, что выравнивание там установлено в 2 байта, а не 4. Да, там версия FreeRTOS чуть более старая, но эти куски ассемблерного кода идентичны. Отличается только строкой .align. И там все работало. Поменяв выравнивание на 2 ворнинги ушли и ничего не сломалось. Кстати, буду благодарен, если кто нибудь пояснит мне суть этого выравнивания.

UPD: разработчики STM32GENERIC предложили другой вариант решения

Туда: USB


Что ж, пока все идет хорошо, но готовы только лампочки и кнопочки. Теперь пора браться за более тяжелую периферию — 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[0] == '1')&&(Buf[1] == 'E')&&(Buf[2] == 'A')&&(Buf[3] == '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):
40005400: 0 0 0 1 - I2C_CR1: Peripheral enable
40005404: 0 0 1 24 - I2C_CR2: Error interrupt enabled, 36Mhz
40005408: 0 0 0 0 - I2C_OAR1: zero own address
4000540C: 0 0 0 0 - I2C_OAR2: Own address register
40005410: 0 0 0 AF - I2C_DR: data register
40005414: 0 0 0 0 - I2C_SR1: status register
40005418: 0 0 0 0 - I2C_SR2: status register
4000541C: 0 0 80 1E - I2C_CCR: 400kHz mode
40005420: 0 0 0 B - I2C_TRISE

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[2];

        xBuffer[0] = 0x00;
        xBuffer[1] = 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[17];
        buf[0] = 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[gpsBufferSize];
        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 ко

© Geektimes