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

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



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

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

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

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


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

    Системный слой реализуется библиотекой 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?

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

    • Инициализация платы (кусочек на схеме под названием board init) и main() находится в STM32GENERIC. В большинстве случаев этого более чем достаточно. Но я планирую использовать различные энергосберегающие режимы, а для этого нужно уметь корректно управлять тактированием и инициализацией контроллера. Выход из некоторых режимов подразумевает ресет ядра МК, а значит мне нужно быть поближе к этому ресету.

    • Я бы хотел, чтобы мое устройство одновременно реализовывало USB CDC (виртуальный СОМ порт) и Mass Storage Class Device. В реализации STM32GENERIC слой работы с USB зарыт достаточно глубоко и реализовывает либо CDC, либо MSC (переключается дефайном), тогда как я хотел бы иметь больше контроля над этим куском, чтобы реализовать оба интерфейса одновременно.

    • Наконец, ардуино оно как швейцарский нож — уступает отдельным специализированным инструментам, зато универсально. Как следствие имеем кучу кода, который там присутствует просто на всякий случай. Так, например, код по управлению GPIO содержит ссылки на АЦП, таймеры и ШИМ, просто потому, что выводы ардуино могут выполнять эти функции. У меня же вся периферия заранее распределена и может быть инициализирована более эффективно.

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


    В этой схеме ардуине отводится только роль С++ обертки над 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 коллбека. Один — это обработчик прерывания, но его работа всего лишь вызвать обработчик из 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 % gpsBufferSize];
     
           	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<size; i++)
    {
           	// send a bunch of data in one xmission
           	Wire.beginTransmission(_i2caddr);
           	WIRE_WRITE(0x40);
           	for (uint8_t x=0; x<16; x++)
           	{
                   	WIRE_WRITE(data[i]);
                   	i++;
           	}
           	i--;
           	Wire.endTransmission();
    }


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

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


    Причина оказалась тривиальной – переполнение буфера. Класс 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[512];
    
    	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[USB_SERIAL_BUFFER_SIZE];
    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[usbTxTail], 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[usbTxHead] = *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[12];
    				size_t len = PrintNum(va_arg(v, int), 10, buf, width, padSymbol);
    				usbDebugWriteInternal(buf + len, len, true);
    				break;
    			}
    			case 'x':
    			case 'X':
    			{
    				char buf[9];
    				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[lastReceivedIndex % gpsBufferSize] = 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[1] = USB_DESC_TYPE_CONFIGURATION;
       }
       else
       {
         pbuf   = (uint8_t *)pdev->pClass->GetFSConfigDescriptor(&len);
         pbuf[1] = USB_DESC_TYPE_CONFIGURATION;
       }
       break;


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

    Издевательство над констатными данными
    __ALIGN_BEGIN uint8_t USBD_StrDesc[USBD_MAX_STR_DESC_SIZ] __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[idx++] = *len;
       unicode[idx++] =  USB_DESC_TYPE_STRING;
      
       while (*desc != '\0')
       {
         unicode[idx++] = *desc++;
         unicode[idx++] =  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



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

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 71

      0
      Хорошо бы статью перевести на английский и разместить… не знаю где, но там где читает много народа. Может и комментарии от создателей этих библиотек будут.
        0
        Часто встречал в комментариях здесь и на других технических ресурсах реплики о том, что разработчики CubeMX не особо то и спешат исправлять косяки, на которые им указывают. Некоторые ошибки в библиотеках гуляют уже не первый год, разработчики обещали рассмотреть и исправить, но все так и осталось на своем месте.
          0
          Такое ощущение, что статья уже была на английском языке. «Системный слой» носители русского языка не говорят, а автопереводчик может.
            +1
            Статья писалась на русском, но вы можете предложить свой вариант названия для этой части
              0
              Обычно в данном контексте употребляется «уровень»: «уровень абстракции», «системный уровень»,…
                0
                Как по мне что уровень что слой звучит как то странно.
                Сойдемся на том, что это профдеформация — я уже не могу думать про код на русском. Вот и получаются такие «англизмы» :)
                  0
                  Значит, у меня было в чём-то правильное ощущение. :)

                  По поводу термина можете мне на слово поверить, можете погуглить, но это именно уровень
                    0
                    ок, принято.
                    Кстати HAL он Layer (слой) :)
                      0
                      Ну так я об этом и говорю. По словарю «слой», а в контексте «уровень».
          0
          С моим произношением «HAL» получается «hell». Шефа-австралийца забавляет :)
            +2
            Все-таки осциллограф — незаменимая штука — экономит уйму времени.
            Особенно в таких случаях, когда не работает и вообще не понятно почему.
              –1
              А ещё логический анализатор. А самое главное — программатор-отладчик. Если бы автор его осилил настроить, было бы сильно проще.
                0
                Честно, я не знаю что с ним не так. Точнее скорее не так с моей платой.

                У меня под рукой есть китайский STLink и плата discovery с отладчиком на борту.
                Оба отладчика могут отлаживать чип на discovery. Но оба отладчика не могут подключиться к моей борде.
                Возможно моя двухбаксовая борда разведена как-то неправильно. Возможно причина в том, что я использую 64к контроллер как 128к — судя по всему внутри они одинаковые и отличаются только маркировкой.

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

                  0

                  Было такое. Проблема была в питающем напряжении, если запитать целевую плату от дев-платы, то всё работало. Купил ST-Link оригинал, он работает всегда вне зависимости от напряжения целевой платы.

                    0
                    Спасибо, попробуюю
                    Но не понял кого от кого запитывать? Я питал свою плату от STLink, который в свою очередь кормился от USB.
                    У меня там потребление мизерное, вообще то
                0
                Я программист, а не электронщик. Я понимаю принцип работы осциллографа, но как им эффективно пользоваться я не знаю :( Вот бы кто показал…

                Кстати, каким образом осцыллограф помог бы в случае неверно настроеных прерываний или криво сконфигурированой периферии? ИМХО бОльшая часть проблем в этой статье были сугубо програмные…
                  0
                  В цифровом осциллографе можно глазами посмотреть какие пакеты передаются, что там с тактированием, нет ли плохого контакта (тоже нередко является причиной появления проблем на пустом месте).

                  Многие также умеют декодировать UART, I2C, SPI и даже показывают ошибки в пакетах.
                    0
                    Значит не зря заказал анализатор уровней. Хотя все равно у меня ошибки внутренние были, снаружи их не словишь
                      0
                      А внутренние ошибки помогает поймать отладочное дрыгание свободными ногами контроллера. Типа вошел в прерывание — выставил 1, а перед выходом выставил 0. На осцилографе/логическом анализаторе видно время, проведенное в прерывании. Можно посмотреть не случилось ли каких других событий в этот момент. Ну и всякое такое.

                      Я, кстати, тоже обычный программист и микроконтроллеры программировал не много и «для себя». Так что это не «советы профессионала», а, скорее, личный опыт. Если что не так — с удовольствием прочитаю как надо :-)
                        0
                        Ок, спасибо. Примерно так я себе и представлял.
                        Когда то дебажил USB стек (v-usb) на 8 светодиодах :) Так что подход для меня не новый.
                0
                Пробовал я этот HAL около года назад. Порты и переферию сконфигурить еще можно, а в остальном — муть страшная. Проще вкурить даташит и написать самому.
                  +1
                  Порты и периферию тоже можно самому сконфигурить. И ошибок туда насажать своих, уникальных и не очень. И времени потерять на их отлове. Всё же, не всегда лучше самому по граблям ходить.

                  Даташит (рефренс мануал в случае STM32) курить полезно в любом случае. Без этого все библиотеки будут казаться кривыми.
                    0
                    Первый раз ошибок наделаешь в любом случае. Но дальше, в новых проектах, вся эта инициализация, прерывания и прочее будут появляться просто копипастом из старых с минимальными изменениями.
                      0
                      Часто новый проект на новом чипе и переходить с чипа на чип с HAL гораздо быстрее.
                        0
                        +1
                        HAL на то и Hardware Abstraction Layer, что абстрагирует от конкретного контроллера.

                        К тому же использование функций с названием типа Transmit_DMA лучше описывает происходящее чем набор флагов и констант. Пускай даже сдобреных обильными коментариями
                    0
                    Ох. Только что заглянул в stm32f4xx_hal_dma.h
                    Этот код должен выполняться в ISR:
                    #define __HAL_DMA_GET_TC_FLAG_INDEX(__HANDLE__) \
                    (((uint32_t)((__HANDLE__)->Instance) == ((uint32_t)DMA1_Stream0))? DMA_FLAG_TCIF0_4 :\
                     ((uint32_t)((__HANDLE__)->Instance) == ((uint32_t)DMA2_Stream0))? DMA_FLAG_TCIF0_4 :\
                     ((uint32_t)((__HANDLE__)->Instance) == ((uint32_t)DMA1_Stream4))? DMA_FLAG_TCIF0_4 :\
                     ((uint32_t)((__HANDLE__)->Instance) == ((uint32_t)DMA2_Stream4))? DMA_FLAG_TCIF0_4 :\
                     ((uint32_t)((__HANDLE__)->Instance) == ((uint32_t)DMA1_Stream1))? DMA_FLAG_TCIF1_5 :\
                     ((uint32_t)((__HANDLE__)->Instance) == ((uint32_t)DMA2_Stream1))? DMA_FLAG_TCIF1_5 :\
                     ((uint32_t)((__HANDLE__)->Instance) == ((uint32_t)DMA1_Stream5))? DMA_FLAG_TCIF1_5 :\
                     ((uint32_t)((__HANDLE__)->Instance) == ((uint32_t)DMA2_Stream5))? DMA_FLAG_TCIF1_5 :\
                     ((uint32_t)((__HANDLE__)->Instance) == ((uint32_t)DMA1_Stream2))? DMA_FLAG_TCIF2_6 :\
                     ((uint32_t)((__HANDLE__)->Instance) == ((uint32_t)DMA2_Stream2))? DMA_FLAG_TCIF2_6 :\
                     ((uint32_t)((__HANDLE__)->Instance) == ((uint32_t)DMA1_Stream6))? DMA_FLAG_TCIF2_6 :\
                     ((uint32_t)((__HANDLE__)->Instance) == ((uint32_t)DMA2_Stream6))? DMA_FLAG_TCIF2_6 :\
                       DMA_FLAG_TCIF3_7)
                    

                    –5
                    У меня создалось впечатление, что вместо настоящей разработки вы занимаетесь какой-то ерундой. Первая ерунда — плюсы на МК — им там совсем не место — проекты уровня ардуино будут жить, но больше — не — у вас скушается вся рама за раз. Вторая — функциональность — ХАЛ в миллиарды раз функцинальней этих библиотек и никакое сокращение команды на пару символов не стоит того, но самый главный проигрыш у вас в производительности. Это в коде выглядит всё просто и понятно обёртка функции — что может быть проще, на деле же из-за с++ эта обёртка превращается в лишние 10-30 команд.

                    Не вижу с ХАЛ никаких проблем — замечательная библиотека. Есть проблемы, но всё решаемо. Написано с её помощью множество проектов. Для вхождения более чем достаточными были примеры входящие в пакет CubeMX
                      0
                      плюсы на МК — им там совсем не место — проекты уровня ардуино будут жить, но больше — не — у вас скушается вся рама за раз.

                      Этот стереотип понятен, но неверен. На С++ можно писать точно так же, как и на С. Только при этом типизация будет чуть строже. И никакая рама сама по себе не скушается, если понимать, что происходит.

                      Не вижу с ХАЛ никаких проблем — замечательная библиотека. Есть проблемы, но всё решаемо.

                      Я один из тех, кто несколько раз пытался перейти на ХАЛ и несколько раз забивал. Для меня основные проблемы — баги (да еще форум вместо багтрекера, фу) и очень неудобная работа с UART (очень дорого прикручивается кольцевой буфер).

                      Но вот про LL HAL я узнал только из этой статьи (за что автору отдельное спасибо), надо будет пощупать.
                        +1
                        А я не пытался. Понадобилось — перешёл за 1 день.
                        0
                        Ардуино — очень хорошая штука! Когда надо лампочку с дистанционным управлением сделать. С год назад это было очень модно.
                          +1
                          Я тоже скептически относился к теме Ардуино, ровно до того момента когда понял простую мысль — это готовый работающий прямо из коробки toolchain, который к тому же поддерживает все больше и больше платформ!
                          Да, с совершенно убогим ide, да сам язык — тот еще бейсик. И нужен действительно для поморгать и хелло написать — зато дальше в любом порядке кому-что-нравится: пишем на C и/или на C++, разбираемся как происходит компиляция, рассматривая реализацию Arduino'вских библиотек учимся работать без них, прикручиваем любимый ide…
                          То есть вместо нехилого такого барьера вхождения — винтовая лестница с положительным фидбеком на каждой ступеньке.
                            +3
                            Я с ним столкнулся на двадцатом году профессионального embedded программирования (мне на поддержку передали проект, написанный студентами под присмотром менеджера, не имеющего опыта разработки).

                            Да, для выпускников МАрхИ и домохозяек, как лесенка для высокого порога он незаменим. НО: как только человек вскарабкался на этот порог, он оказывается в ловушке простоты (которая хуже воровства), и появляются такие статьи, как эта.
                              0
                              Arduino вполне годится для домашних поделок, типа таких:
                              https://geektimes.ru/post/289389/
                                +1
                                Может проблема не в ардуино, а в студентах и менеджере? :)
                                  +1
                                  В них тоже, конечно! А я один — д'Артаньян ;)
                            0
                            Посмотрите сколько памяти сжирает HAL_RCC_OscConfig, потом еще раз напишите про проблемы c++ :)
                              +2
                              Первая ерунда — плюсы на МК — им там совсем не место — проекты уровня ардуино будут жить, но больше — не — у вас скушается вся рама за раз.

                              Вы не любите кошек? вы просто не умеете их готовить! (С) анекдот

                              Дизассемблировал код, оверхеда не увидел.
                              Память тоже не течет. Она вообще не может тут течь никак ибо я выкорчевал new/delete + malloc/free.

                              Зато всякие полиморфизмы, инкапсуляции, RAII, строгая типизация и подобные штуки помогают писать код чище и структурирование. Я честно пробовал на каком то этапе писать на С (https://geektimes.ru/post/286348/ — там вконце), но код стал ужасен, размазан плохо структурирован. Встал вопрос «зачем»? Чего мы хотим добиться? Сохранить пару байт во флеше? Его у нас достаточно. Производительность? не вижу в скомпилированом коде существенной разницы.

                              Вторая — функциональность — ХАЛ в миллиарды раз функцинальней этих библиотек и никакое сокращение команды на пару символов не стоит того

                              Брррр, какие библиотеки вы сравниваете с HAL?

                              У меня SdFat отвечает за работу с SD картой, NeoGPS отвечает за парсинг NMEA потока, Adafruit_GFX за графику и дисплей. Как к этому относится HAL, который отвечает за абстракцию над железом?

                              но самый главный проигрыш у вас в производительности.

                              Хотелось бы узнать в каком месте.

                              Одна из целей на данном этапе была разгрузить процессор. Вместо того, чтобы молотить байты на процессоре, данные передаются через DMA. Цель достигнута.

                              А вопрос вообще не относится ни к библиотекам, ни к языку программирования — корявые библиотеки можно найти на любом языке.
                              0
                              В сторону mbed смотрели?
                                0
                                не довелось.
                                На что там стОит обратить внимание?
                                +2
                                Я так понимаю, в итоге получился некий монстр Франкенштейна с торчащими во все стороны разнородными библиотеками. А всего то надо было разобраться в протоколах и написать всё самому, после чего следующие проекты создавались бы копипастом нужных модулей и функций из предыдущих с минимальными правками. Вообще, у STM32 есть только одна по-настоящему больная мозоль — USB, с которым без библиотек совладать очень сложно. Ну, и I2C в F1 кривоват. Остальное всё достаточно просто.

                                Кстати, я делал похожий проект с GPS, экраном и картой памяти на STM32F030F4P6 с 16 Кб флеша, и всё прекрасно влезало и работало.
                                  0
                                  Если есть желание поковыряться с USB без библиотек STM:
                                  http://mcu.goodboard.ru/viewtopic.php?id=40

                                  А вообще библиотеки конечно кривые, у кого то с DMA не работает на нормальной скорости, у меня были глюки при сборке композитного устройства (HID + CDC)…
                                    0
                                    Спасибо за наводку, гляну
                                    +1
                                    Я так понимаю, в итоге получился некий монстр Франкенштейна с торчащими во все стороны разнородными библиотеками.

                                    Если честно, не понял замечания.

                                    У меня SdFat отвечает за работу с SD картой, NeoGPS отвечает за парсинг NMEA потока, Adafruit_GFX за графику и дисплей, HAL за абстракцию над железом. Конечно же эти библиотеки разнородные — они же разный функционал реализуют!

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

                                    Библиотеки на то люди и пишут, чтобы другие люди их переиспользовали, а не писали собственные велосипеды.
                                      0
                                      А можно даже и не писать самому, а использовать что-то типа HAL ChibiOS или libopencm3. Правда всё равно придётся делать обёртки плюсовые = оверхед (только чаще всего он несущественен, с учётом мощностей современных контроллеров).

                                      Почему-то нет до сих пор ни одной хорошей библиотеки для STM32, написанной на чистых плюсах и максимально оптимизированной (шаблоны, constexpr). Благо плюсы располагают к тому, чтобы написать хорошую библиотеку, без всякого оверхеда.
                                        0
                                        На libopencm3 я смотрел. К сожалению она мне попалась примерно в середине работы, так что поворачивать было уже поздно. Может как нибудь в следующий раз.
                                      0

                                      Я довольно профан и новичок в STM32, использую не HAL, а SPL (Standard Perepherial Library), мне она кажется приятнее, оверхеда меньше и низкоуровневый доступ есть. Что более опытные разработчики думают на этот счет?

                                        –1
                                        Низкоуровневый доступ везде одинаковый.
                                        #define IO8 *(uint8_t*)
                                        #define IO16 *(uint16_t*)
                                        #define IO32 *(uint32_t*)

                                        IO8(0xDEADBEEF) |= 0x55
                                        IO16(0xDEADBEEF) |= 0xB0B
                                        IO32(0xDEADBEEF) |= 0xBADBAD
                                          +1
                                          За такой низкоуровневый доступ надо руки отрывать, вообще то.
                                            –2
                                            Кстати, такой код успешно сбивает крылатые ракеты.
                                            Так как с Вашей стороны обоснования не воспоследовало, можете себе оборвать, за одно и рукава на штанах отпорете. ;)
                                          +1
                                          SPL выгнали на пенсию и заменили HAL, а HAL писали индусы, так что счастья нет… Только напильник спасет.
                                            0
                                            SPL тоже писали индусы
                                              0
                                              Да они сейчас все пишут…
                                            0
                                            Наверное, то, что на новые камни есть только HAL. А вообще это вопрос религии, наверное, я вот в новых версиях Keil эти все выверты с библиотеками не признаю, потому что потом синхронизировать проекты между разработчиками с разными версиями становится крайне неудобно, в итоге все вырезал лишнее, оставил набор нужных нам файлов с SPL и CMSIS — работает на любой версии без лишних телодвижений.
                                            0

                                            Вопрос к автору: как Вы определили, сколько занимает каждый компонент (последняя таблица)?

                                              0
                                              Я использую такой подход:
                                              1) дизассемблирую elf файл с помощью objdump и c++filt. Для удоства у меня для этого есть специальный таргет в мейкфайле
                                              https://github.com/grafalex82/GPSLogger/blob/master/cmake/gcc_stm32.cmake#L90
                                              В принципе этого уже достаточно если нужно посмотреть конкретную функцию, объект, переменную или массив

                                              2) Чтобы было удобнее следить за группой функций у меня есть скрипт, который берет вывод предыдущего пункта, генерирует csv файл со всеми символами. Скрипт также сортирует их по начальному адресу и высчитывает размер
                                              https://github.com/grafalex82/GPSLogger/blob/master/Scripts/dump_firmware.py

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

                                              На заполнение этой таблицы у меня ушло примерно час, но время от времени мне даже нравится таким заниматься.
                                                0
                                                https://github.com/govind-mukundan/MapViewer

                                                Я там что-то подпиливал, чтобы заработала…
                                                0
                                                Offtop:
                                                В 2017 уже стало нормальным 3мегабайтные png картинки до подката вставлять?
                                                Заголовок спойлера
                                                image
                                                  0
                                                  Ой, да кто в 2017 году считает эти мегабайты? :)

                                                  ЗЫ: рука дрогнула. поправил
                                                  0
                                                  Спасибо за статью!
                                                  Делаю локальную сеть из устройств умного дома на базе uart. В качестве роутеров решил ставить STMки, как дешевые МК с тремя hardware uart.
                                                  Взял всеми распиаренную stm32duinom… И словил кучу проблем. при попытке работы со всеми тремя uart одновременно — получал кашу на входе и кашу на выходе…
                                                  Пришел к выводу, что скорее всего проблема в коде либ для МК.
                                                  Пошел разбираться как писать для stm32 напрямую. Нагуглил информацию про устаревшую SPL и новый HAL. Уже начал переписывать всё на HAL. И тут в процессе поиска информации нашел вашу статью. Узнал из неё о STM32GENERIC, и о чудо! Изначально написанный ардуиновский проект заработал ровно и стабильно, без каких либо заморочек.
                                                  Эх, на пару суток бы раньше наткнуться на вашу статью — глядишь не потратил бы два своих выходных на попытки починить чужие ошибки. Всё таки привычка гуглить на английском не всегда позитивна…
                                                    +1
                                                    Рад что пригодилось!

                                                    Как там STM32GENERIC поживает? А то я застрял на аппаратной части своего проекта, за обновлениями в софте давно не слежу…

                                                    Оффтоп: сам строю умный дом. А зачем Вам вообще провода? На ESP смотрели? Или ZigBee штуки от Xiaomi?.. Я вот построил парочку датчиков на ESP8266 с прошивкой на базе ардуины — уже месяц работает без сбоев. Как раз шлюз между UART/RS485 и TCP сделал по мотивам этого.
                                                      0
                                                      Я глубоко в дебри GENERIC е лазил. По сути только вчера вечером проверил свой проект на нём и всё.
                                                      Видел, что ваш пулл реквест так и не приняли. Даже возникла мысль, что проект заброшен. Но нет, активность есть. Видимо просто еще не уверены, что выравнивание надо переделать. Поэтому и висит.

                                                      По поводу проводов:
                                                      У меня изначально всё на espшках и было. ОДнако меня начал напрягать менеджмент такого количества WiFi устройств. Плюс меня напрягает, что простенькое embedded устройство без обновлений светит наружу WiFiем. Учитывая что умный дом управляет достаточно чувствительными вещами — я не хочу такую дыру оставолять. Плюс иногда WiFi устройства не реагируют на команды. И вот сиди думай чего оно. Толи зависло, толи вафля глючит, толи канал забит…
                                                      Плюс заняты розетки блоками питания.
                                                      Учитывая что питание один фиг вести к устройствам — в беспроводном соединении нет смысла. PowerLine мне не понравился в силу дороговизны и они очень горячие. А у меня часть устройств умного дома под полом на втором этаже. Греться там негоже. Да и не хочется везде иметь 220 вольт. Мало ли, ребенок дотянется…
                                                      Вот в итоге я и протянул по дому шину на 24 вольта, и рядом с ней экранированный кабель для Serial соединения.
                                                      Сейчас делаю роутер, который выполняет две функции: маршрутизирует и усиливает uart соединение, а также дает питание собственно подключенному устройству умного дома.
                                                        0
                                                        Ок, понял. Получается я со своими ЕСПшками только в начале пути :)
                                                        Кстати, у Вас чистый TTL uart или RS485?
                                                          0
                                                          Чистый TTL uart. Но я только проработал теоретически архимтектуру и сейчас реализацией занимаюсь. На практике у меня всё еще висят ESPшки в основном.

                                                          По поводу RS485:
                                                          Этот вариантя рассмотрел. Конечно, сразу смутило что на RS485 придется разруливать коллизиции и вообще можно забыть о передаче чего либо более менее объемного. Но это показалось вполне приемлемым.
                                                          Однако, уже в процессе планирования сети, я осознал, что RS485 не терпит сложных топологий сети, типа звезды. Только последовательное подключение без разветвлений. Что для меня оказалось крайне печальным. Конечно, звезды можно эмулировать таская провод туда-сюда, но это тоже не хорошо — увеличивается длинна кабеля, а значит выше вероятность ухудшения сигнала ниже допустимого уровня.
                                                          Есть репитеры, но цена на них начинается с 2000 рублей, что прилично учитывая мои потребности. И вот когда я смотрел на репитеры для RS485 до меня дошло, что я могу просто сделать маршрутизация для традиционного uart и решить все проблемы.
                                                          Стоимость роутера на STM32 выходит в 300 рублей с корпусом и деталями(но без пайки). Это не так уж и дорого. А на выходе: полноценная сеть, с любой топологией, длина сети может быть любой, потому что её качество определяется самым длиннным соединением между двумя роутерами, а не общей продолжительностью линии, два устройства подключенные рядом могут активно обмениваться данными совершенно не влияя на сеть за пределами своего маршрута.
                                                          Одни плюсы, вобщем.
                                                          Естественно пробовал гуглить готовые решения, ничего не вылезло. Так что сейчас пилю спецификацию протокола, софт и железо под эту задачу.
                                                            0
                                                            Советую взглянуть в сторону драйверов RS-485/RS-422 ADM485AR — 5 Мбит на 30..50 метров, на али — по 110рублей/десяток. С ними легко построить любую топологию, как с роутерами на STM32, так и без них, просто сделав из них репитеры и хабы.
                                                              0
                                                              RS-485 я уже описывал.
                                                              А RS-422 — не позволяет же иметь несколько передатчиков в сети. Или как вы предлагаете реализовать на них нормальную сеть?
                                                              Плюс не забываем, что хаб не возможен на низком уровне передачи сигнала, т.к. не имея знания о протоколе передачи — не сможет определить где конец передачи, а значит не сможет корректно отделить пакеты друг от друга, что приведет к каше на выходе где смешиваются два сигнала. Поэтому хаб без вариантов надо делать на МК, который можно запрограммировать на обработку конкретного протокола.
                                                                0
                                                                Хаб на низком уровне возможен, если пакеты отделены друг от друга паузами не менее 1,5 интервалов передачи байта, а байты в пакете идут без пауз, или почти без пауз, как, например, в Modbus RTU. В этом случае конец передачи можно определить по времени присутствия высокого уровня — передача байта всегда начинается со старт бита низкого уровня. В простейшем случае это можно сделать просто диодом и RC-цепочкой, правда в этом случае нужна либо пауза побольше, либо точные номиналы R и С. Другие протоколы передачи данных также как правило предусматривают наличие паузы между пакетами — это нужно для восстановления байтовой синхронизации после сбоя из-за помехи, или начала приёма в случайный момент времени. А RS-422 я предлагаю использовать вместе с вашими роутерами — у TTL сигналов помехоустойчивость очень плохая, и приличную скорость передачи данных на типичных для устройств умного дома длинах сегментов не получить.
                                                                  0
                                                                  Паузча — тоже часть протокола же. Хотя согласен, её вероятно можно обработать на хабах без МК.
                                                                  Приличная скорость — это сколько? Вроде бы не нужна в умном доме высокая скорость. Усложнять уж больно не хочется. Сейчас роутер — это два блока БП+МК и всё. А на устройстве так и вообще ничего нету: подключение сразу к пинам.
                                                                  Причем есть возможностьиспользовать готовые блоки: stm32 — bluepill, например. Что дает высокую ремонтопригодность и постороннему проще разобраться.
                                                                  Введение дополнительных элементов на схеме — сразу поднимет сложность ремонта для не посвященного.
                                                                    0
                                                                    Говоря о приличной скорости я имел в виду скорость выше 100кбит/с. Конечно, для передачи команд такая скорость избыточна. Но для того, чтобы дистанционно обновить прошивку сразу всем устройствам одного типа, высокая скорость весьма полезна, иначе эта операция может длиться часами.
                                                                      0
                                                                      У… Да. Такая скорость недостижима.
                                                                      Но меня смущает высокая скорость обмена, т.к. её не смогут обеспечить мелкие и тупые устройства. Да и в целом загружать устройства обработкой высокоскоростной сети — не хочется.
                                                                      По поводу прошивки — я к сожалению далек от того, чтобы устройства обновлять по сети, хотя и очень хочется.
                                                                      Но даже здесь проблемы именно со скорость не будет, потому что можно все однотипные устройства обновлять за один раз через широковещательные сообщения.
                                                                        0
                                                                        STM32F103 вполне может работать со скоростью UART 1 мбит и больше — если использовать DMA — обмен не будет сильно загружать процессор. А на 460 кбит мне удавалось заставить работать даже ATmega32 в сети RS-485 из > 40 устройств с Modbus-подобным протоколом — но тут нужно жестко контролировать время обработки прерываний, и вручную реализовывать управление их вложенностью.
                                                                        Обновлять прошивку через широковещательные сообщения можно, но обычно прошивку некуда записать целиком кроме памяти программ, поэтому в этом случае после широковещательной рассылки придётся организовать проверку доставки прошивки индивидуально для каждого устройства целиком или поблочно, и в случае сбоев повторять, пока прошивка не будет обновлена во всех устройствах. Это несколько нетривиально, и готовых реализаций этого алгоритма я не встречал.
                                                    0
                                                    comment deleted

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

                                                    Самое читаемое