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

  • Tutorial
Сейчас уже никого не удивить микроконтроллерами с энергонезависимой (чаще всего Flash) памятью объемом 512 килобайт и более. Их стоимость постепенно снижается, а доступность напротив, растет. Наличие такого объема энергонезависимой памяти дает возможность писать «тяжелые» по объему занимаемой памяти приложения, облегчая при этом последующее сопровождение кода за счет использования готовых решений из различных стандартных библиотек. Однако это ведет к росту объема файла прошивки целевого устройства, который требуется каждый раз целиком заново загружать в энергонезависимую память микроконтроллера при малейшем изменении в коде.

Цель статьи — рассказать о методе построения проекта на C и/или C++, при котором, в случае изменения участка кода, отладка которого производится чаще всего, большая часть проекта не нуждалась в повторной перезаписи. А так же показать, почему данный метод не всегда является эффективным решением.

Требования к читателю


По ходу повествования я буду предполагать, что читатель:

  • владеет на среднем уровне языками C и C++;
  • имеет опыт работы с микроконтроллерами на базе ядер Cortex-M3/Cortex-M4 (например серией stm32f4);
  • знает, как происходит процесс сборки конечного файла пришивки (elf/bin) из исходников проекта;
  • представляет себе, для чего нужны linker script файлы;
  • имеет представление о секциях text, bss, data и прочих;
  • работал с каким-либо из дистрибутивов linux;
  • минимально владеет bash;
  • имеет опыт работы с gcc под архитектуру процессоров Cortex-M3/Cortex-M4 (toolchain arm-none-eabi);
  • имеет начальные навыки работы с cmake.

Суть метода


В «классическом» проекте под микроконтроллеры все неизменяемые данные (секции text, rodata, начальные значения data и прочие) обычно располагаются «подряд», начиная с адреса начала энергонезависимой памяти (в случае микроконтроллера на основе ядра Cortex-M3/Cortex-M4 — c адреса 0x08000000). В упрощенном виде карта использования энергонезависимой памяти программой микроконтроллера на базе ядра Cortex-M3/Cortex-M4, написанной с использованием C++, выглядит примерно так:



mem.ld файл для такого проекта, чаще всего, выглядит примерно следующим образом:

MEMORY
{
    FLASH (rx) :        ORIGIN = 0x08000000, LENGTH = 768K
    RAM (xrw) :         ORIGIN = 0x20000000, LENGTH = 112K
}

Здесь вся энергонезависимая память является одним единственным разделом с именем «FLASH», а вся оперативная память — раздел с именем «RAM». В таком виде, при изменении какого-то из участков кода, все остальные начинают «смещаться». Дабы этого избежать, можно «разбить» файл прошивки на какие-то логические блоки. Например, следующим образом:

  • таблица векторов прерываний;
  • собственные библиотеки;
  • сторонние библиотеки (в которые не планируется вносить изменения);
  • часто изменяемый код.

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

По сути говоря, в проект добавляются статические библиотеки.

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

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

Поле для эксперимента


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

Характеристика проекта


Проект содержит:

  • код основного проекта на C++14 с использованием виртуальных таблиц, new/delete (работающие через кучу FreeRTOS), shared_ptr (и прочие умные указатели) и другие прелести стандартных библиотек C++14;
  • FreeRTOS с использованием примерно 6 задач на поддержание инфраструктуры аппаратной периферии и порядка 10 на бизнес-логику (библиотека работы с графикой MakiseGUI, обработка нажатий, работа с fat (FatFS) и прочее);
  • 16 репозиториев с собственными библиотеками взаимодействия с аппаратной периферией на плате, заглушки системных вызовов и прочее;

При параметрах сборки -O0 -g3 код в полноценной реализации с поддержкой unicode, кириллиц и прочего занимает порядка 700 КБ. Однако на текущем этапе, когда аппаратная периферия работает стабильно, а в отладке нуждается лишь бизнес логика, то количество изменяемого кода примерно 20 КБ. Именно по этой причине, на первый взгляд, кажется, что текущий подход является идеальным решением проблемы (вариант с симуляцией на компьютере по некоторым причинам не рассматривается).

Перечень действий


Для реализации описанного метода потребуется:

  • собрать все субмодули как статические библиотеки (описание этого пункта не входит в перечень разбираемых пунктов данной статьи);
  • переписать mem.ld;
  • переписать section.ld;
  • добавить в основной проект утилиту для извлечения разделов из конечного bin файла;
  • добавить в проект вызов скрипта обновления энергонезависимой памяти микроконтроллера при обновлении файла прошивки.

Переписываем mem.ld


Первым шагом является доработка «стандартного» mem.ld под текущую концепцию. При доработке следует учитывать, что очистка энергонезависимой памяти происходит по секторам. Подробнее о том, как структурированы сектора в конкретном микроконтроллере следует читать в документации (в случае микроконтроллеров stm32 — в reference manual). Каждый раздел может занимать не менее одного сектора (больше можно), иначе один раздел перепишет другой.

Так же следует учесть, что если библиотека использует глобальные переменные, то под эту библиотеку нужно зарезервировать место в оперативной памяти на этапе компоновки. Иначе можно столкнуться с неприятными багами, которые будет крайне трудно отловить. Например код библиотеки FatFS будет находиться в разделе ROM_EXTERNAL_LIBRARIES, но он требует 4 байта в RAM на этапе компоновки. А значит нужно позаботиться о том, чтобы в оперативной памяти был раздел под поля, которые будет использовать код из ROM_EXTERNAL_LIBRARIES. В данном примере это RAM_EXTERNAL_LIBRARIES.

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

В контексте текущего проекта mem.ld будет выглядить так.
/* Карта памяти микроконтроллера stm32f405rgt6
   под проект ChiptunePlayer-2.22-MainBoard-v2-Firmware. */
MEMORY
{
    /*-----------------------------FLASH-------------------------------*/
    /* Сектора 0-1 отводятся загрузчику. */
    ROM_BOOTLOADER (RX)             : ORIGIN = 0x08000000, LENGTH = 32K

    /* Сектора 2 используется для хранения настроек пользователя. */
    ROM_SYSCFG_PAGE_1 (R)           : ORIGIN = 0x08008000, LENGTH = 16K

    /* Сектор 3 используется для хранения копии настроек пользователя. */
    ROM_SYSCFG_PAGE_2 (R)           : ORIGIN = 0x0800C000, LENGTH = 16K

    /* Сектор 4 зарезервирован. */
    ROM_RESERVE (R)                 : ORIGIN = 0x08010000, LENGTH = 16K

    /* Сектора 5, 6, 7 предназначены для хранения
       сторонник библиотек (FATFS, FREERTOS...). */
    ROM_EXTERNAL_LIBRARIES (RX)     : ORIGIN = 0x08020000, LENGTH = 384K

    /* Сектора 8, 9 предназначены для хранения собственных библиотек
       (драйвера дисплеев, графические объекты...). */
    ROM_USER_LIBRARIES (RX)         : ORIGIN = 0x08080000, LENGTH = 384K

    /* Сектора 5, 6 предназначены для хранения
           основного кода приложения. */
    ROM_MAIN_PROGRAMM (RX)          : ORIGIN = 0x080E0000, LENGTH = 128K

    /*-----------------------------RAM---------------------------------*/
    /* В режиме выполнения загрузчика карта RAM
       перекрывает карту целевого устройства. */
    RAM_PAGE_1 (RW)                 : ORIGIN = 0x20000000, LENGTH = 112K
    RAM_PAGE_2 (RW)                 : ORIGIN = 0x2001C000, LENGTH = 16K

    /* С этой областью памяти будут работать сторонние
       библиотеки на манер FATFS или FreeRTOS. */
    RAM_EXTERNAL_LIBRARIES (RW)     : ORIGIN = 0x20000000, LENGTH = 10K

    /* С этой областью памяти будут работать библиотеки пользователя. */
    RAM_USER_LIBRARIES (RW)         : ORIGIN = 0x20002800, LENGTH = 90K

    /* С этой областью RAM будет работать код пользователя. */
    RAM_MAIN_PROGRAMM (RW)          : ORIGIN = 0x20019000, LENGTH = 27K

    /* Немного памяти RAM выделяется под начальный стек.
       До запуска планировщика FreeRTOS. */
    RAM_MAIN_PROGRAMM_STACK (RW)    : ORIGIN = 0x2001FC00, LENGTH = 1K
}

Переписываем section.ld


После того, как имеющаяся карта памяти была поделена на разделы, следует описать, что в какой раздел будет помещено. Для каждой библиотеки (при наличии соответствующей секции в библиотеке) следует указать где располагаются секции .text, .rodata, .data, .bss и прочие. Перечень имеющихся в библиотеке секций можно посмотреть с помощью objdump. Например для библиотеки libstdc++_nano.a требуется указать где расположить секции text, ARM.attributes, rodata, data, bss, COMMON.

В контексте текущего проекта section.ld будет выглядить так.
/* Под начальный стек до запуска планировщика у нас есть своя секция в RAM. */
__estack = ORIGIN(RAM_MAIN_PROGRAMM_STACK) + LENGTH(RAM_MAIN_PROGRAMM_STACK);

/* Размер начального стека. */
__stack_size = LENGTH(RAM_MAIN_PROGRAMM_STACK);

/* Программа начинается с метода Reset_Handler. */
ENTRY(Reset_Handler)

/* Описание секций. */
SECTIONS
{
    /*---------------------ROM область загрузчика------------------------*/
    .section_bootloader : ALIGN(4)
    {
        /* На данный момент загрузчик отсутствует.
           Поэтому на месте кода загрузчика будет находиться
           только таблица векторов прерываний основного проекта.
           Таблица векторов прерываний должна быть передана
           компоновщику как отдельный .o файл, а не как часть
           библиотеки.*/
        . = ALIGN(4);    
        KEEP(*(.user_code_isr_vector .user_code_isr_vector*))
        . = ALIGN(4);
    } >ROM_BOOTLOADER

    /*----------------ROM область под внешние библиотеки-----------------*/
    /* Код. */
    .section_external_libraries_text : ALIGN(4)
    {
        /* Библиотеки компилятора. */
        . = ALIGN(4);    
        *libstdc++_nano.a:*(.text .text*);
        . = ALIGN(4);    
        *libgcc.a:*(.text .text*);
        . = ALIGN(4);    
        *libg_nano.a:*(.text .text*);

        /* Библиотеки пользователя */
        . = ALIGN(4);    
        *libSTM32F4_LOW_LEVEL_BY_ST.a:*(.text .text*);
        . = ALIGN(4);    
        *libFATFS.a:*(.text .text*);
        . = ALIGN(4);    
        *libFREERTOS.a:*(.text .text*);
        . = ALIGN(4);    
        *libMAKISE_GUI.a:*(.text .text*);
        . = ALIGN(4);
    } >ROM_EXTERNAL_LIBRARIES

    /* Секции, созданные компилятором */
    .section_external_libraries_required_by_the_compiler : ALIGN(4)
    {
        /* Библиотеки компилятора. */
        . = ALIGN(4);    
        *libgcc.a:*(.ARM.attributes .ARM.attributes*);
        . = ALIGN(4);    
        *libstdc++_nano.a:*(.ARM.attributes .ARM.attributes*);
        . = ALIGN(4);    
        *libg_nano.a:*(.ARM.attributes .ARM.attributes*);

        /* Библиотеки пользователя */
        . = ALIGN(4);    
        *libSTM32F4_LOW_LEVEL_BY_ST.a:*(.ARM.attributes .ARM.attributes*);
        . = ALIGN(4);    
        *libFATFS.a:*(.ARM.attributes .ARM.attributes*);
        . = ALIGN(4);    
        *libFREERTOS.a:*(.ARM.attributes .ARM.attributes*);
        . = ALIGN(4);    
        *libMAKISE_GUI.a:*(.ARM.attributes .ARM.attributes*);
        . = ALIGN(4);
    }  >ROM_EXTERNAL_LIBRARIES

    /* Сущности с неизменяемыми значениями. */
    .section_external_libraries_rodata : ALIGN(4)
    {
        /* Библиотеки компилятора. */
        . = ALIGN(4);    
        *libgcc.a:*(.rodata .rodata*);
        . = ALIGN(4);    
        *libstdc++_nano.a:*(.rodata .rodata*);
        . = ALIGN(4);    
        *libg_nano.a:*(.rodata .rodata*);

        /* Библиотеки пользователя */
        . = ALIGN(4);    
        *libSTM32F4_LOW_LEVEL_BY_ST.a:*(.rodata .rodata*);
        . = ALIGN(4);    
        *libFATFS.a:*(.rodata .rodata*);
        . = ALIGN(4);    
        *libFREERTOS.a:*(.rodata .rodata*);
        . = ALIGN(4);    
        *libMAKISE_GUI.a:*(.rodata .rodata*);
        . = ALIGN(4);
    } >ROM_EXTERNAL_LIBRARIES

    /*-----------------------Библиотеки пользователя---------------------*/
    /* Код. */
    .section_user_libraries_text : ALIGN(4)
    {
        . = ALIGN(4);    
        *libUSER_FREERTOS_LEVEL.a:*(.text .text*);
        . = ALIGN(4);    
        *libUSER_BSP_LEVEL.a:*(.text .text*);
        . = ALIGN(4);    
        *libMC_INTERRUPT.a:*(.text .text*);
        . = ALIGN(4);    
        *libMC_HARDWARE.a:*(.text .text*);
        . = ALIGN(4);    
        *libPCB_HARDWARE.a:*(.text .text*);
        . = ALIGN(4);    
        *libUSER_STARTUP.a:*(.text .text*);
        . = ALIGN(4);    
        *libBUTTONS.a:*(.text .text*);
        . = ALIGN(4);    
        *libCHIPTUNE.a:*(.text .text*);
        . = ALIGN(4);    
        *libDIGITAL_POTENTIOMETER.a:*(.text .text*);
        . = ALIGN(4);    
        *libLCD_DRIVER.a:*(.text .text*);
        . = ALIGN(4);    
        *libMAKISE_GUI_ELEMENTS_BY_VADIMATORIK_ELEMENTS_BY_VADIMATORIK.a:*(.text .text*);
        . = ALIGN(4);    
        *libMC_HARDWARE_INTERFACES_IMPLEMENTATION_FOR_STM32.a:*(.text .text*);
        . = ALIGN(4);    
        *libMICROSD_LOW_LEVEL_DRIVER.a:*(.text .text*);
        . = ALIGN(4);    
        *libSHIFT_REGISTER.a:*(.text .text*);
        . = ALIGN(4);    
        *libWAVE_GENERATORS.a:*(.text .text*);
        . = ALIGN(4);    
        *libRUN_TIME_LOGGER.a:*(.text .text*);
        . = ALIGN(4);
    } >ROM_USER_LIBRARIES

    /* Секции, созданные компилятором */
    .section_user_libraries_required_by_the_compiler : ALIGN(4)
    {
        . = ALIGN(4);    
        *libUSER_FREERTOS_LEVEL.a:*(.ARM.attributes .ARM.attributes*);
        . = ALIGN(4);    
        *libUSER_BSP_LEVEL.a:*(.ARM.attributes .ARM.attributes*);
        . = ALIGN(4);    
        *libMC_INTERRUPT.a:*(.ARM.attributes .ARM.attributes*);
        . = ALIGN(4);    
        *libMC_HARDWARE.a:*(.ARM.attributes .ARM.attributes*);
        . = ALIGN(4);    
        *libPCB_HARDWARE.a:*(.ARM.attributes .ARM.attributes*);
        . = ALIGN(4);    
        *libUSER_STARTUP.a:*(.ARM.attributes .ARM.attributes*);
        . = ALIGN(4);    
        *libUSER_CODE.a:*(.ARM.attributes .ARM.attributes*);
        . = ALIGN(4);    
        *libBUTTONS.a:*(.ARM.attributes .ARM.attributes*);
        . = ALIGN(4);    
        *libCHIPTUNE.a:*(.ARM.attributes .ARM.attributes*);
        . = ALIGN(4);    
        *libDIGITAL_POTENTIOMETER.a:*(.ARM.attributes .ARM.attributes*);
        . = ALIGN(4);    
        *libLCD_DRIVER.a:*(.ARM.attributes .ARM.attributes*);
        . = ALIGN(4);    
        *libMAKISE_GUI_ELEMENTS_BY_VADIMATORIK_ELEMENTS_BY_VADIMATORIK.a:*(.ARM.attributes .ARM.attributes*);
        . = ALIGN(4);    
        *libMC_HARDWARE_INTERFACES_IMPLEMENTATION_FOR_STM32.a:*(.ARM.attributes .ARM.attributes*);
        . = ALIGN(4);    
        *libMICROSD_LOW_LEVEL_DRIVER.a:*(.ARM.attributes .ARM.attributes*);
        . = ALIGN(4);    
        *libSHIFT_REGISTER.a:*(.ARM.attributes .ARM.attributes*);
        . = ALIGN(4);    
        *libWAVE_GENERATORS.a:*(.ARM.attributes .ARM.attributes*);
        . = ALIGN(4);    
        *libRUN_TIME_LOGGER.a:*(.ARM.attributes .ARM.attributes*);
        . = ALIGN(4);
    }  >ROM_EXTERNAL_LIBRARIES

    /* Сущности с неизменяемыми значениями. */
    .section_user_libraries_rodata : ALIGN(4)
    {
        . = ALIGN(4);    
        *libUSER_FREERTOS_LEVEL.a:*(.rodata .rodata*);
        . = ALIGN(4);    
        *libUSER_BSP_LEVEL.a:*(.rodata .rodata*);
        . = ALIGN(4);    
        *libMC_INTERRUPT.a:*(.rodata .rodata*);
        . = ALIGN(4);    
        *libMC_HARDWARE.a:*(.rodata .rodata*);
        . = ALIGN(4);    
        *libPCB_HARDWARE.a:*(.rodata .rodata*);
        . = ALIGN(4);    
        *libUSER_STARTUP.a:*(.rodata .rodata*);
        . = ALIGN(4);    
        *libBUTTONS.a:*(.rodata .rodata*);
        . = ALIGN(4);    
        *libCHIPTUNE.a:*(.rodata .rodata*);
        . = ALIGN(4);    
        *libDIGITAL_POTENTIOMETER.a:*(.rodata .rodata*);
        . = ALIGN(4);    
        *libLCD_DRIVER.a:*(.rodata .rodata*);
        . = ALIGN(4);    
        *libMAKISE_GUI_ELEMENTS_BY_VADIMATORIK_ELEMENTS_BY_VADIMATORIK.a:*(.rodata .rodata*);
        . = ALIGN(4);    
        *libMC_HARDWARE_INTERFACES_IMPLEMENTATION_FOR_STM32.a:*(.rodata .rodata*);
        . = ALIGN(4);    
        *libMICROSD_LOW_LEVEL_DRIVER.a:*(.rodata .rodata*);
        . = ALIGN(4);    
        *libSHIFT_REGISTER.a:*(.rodata .rodata*);
        . = ALIGN(4);    
        *libWAVE_GENERATORS.a:*(.rodata .rodata*);
        . = ALIGN(4);    
        *libRUN_TIME_LOGGER.a:*(.rodata .rodata*);
        . = ALIGN(4);
    } >ROM_USER_LIBRARIES

    /*-------------------------Основная программа------------------------*/
    /* Код. */
    .section_user_code_text : ALIGN(4)
    {
        . = ALIGN(4);    
        *(.text .text.*)
        . = ALIGN(4);
    } >ROM_MAIN_PROGRAMM

    /* Секции, созданные компилятором */
    .sections_user_code_required_by_the_compiler : ALIGN(4)
    {
        . = ALIGN(4);    
        *(.glue_7 .glue_7*)              /* Собственные методы-прослойки для ARMv7 */
        . = ALIGN(4);    
        *(.glue_7t .glue_7t*)
        . = ALIGN(4);    
        *(.vfp11_veneer .vfp11_veneer*)  /* Прочие секции поддержки. */
        . = ALIGN(4);    
        *(.v4_bx .v4_bx*)
        . = ALIGN(4);    
        *(.iplt .iplt*)
        . = ALIGN(4);    
        *(.rel.dyn .rel.dyn*)
        . = ALIGN(4);    
        KEEP(*(.eh_frame .eh_frame*))    /* Для использования исключений в CPP. */
        . = ALIGN(4);    
        *(.eh_framehdr .eh_framehdr*)
        . = ALIGN(4);    
        *(.ARM.attributes .ARM.attributes.*)       /* Песь оставшийся дополнительный код,
                                                                       созданный компилятором. */
        . = ALIGN(4);    *(vtable)					            /* C++ virtual tables */

        PROVIDE_HIDDEN (__preinit_array_start = .);             /* Список методов, вызываемых перед конструкторами. */
        . = ALIGN(4); 
        KEEP(*(.preinit_array_sysinit .preinit_array_sysinit*))
        . = ALIGN(4); 
        KEEP(*(.preinit_array_platform .preinit_array_platform.*))
        . = ALIGN(4); 
        KEEP(*(.preinit_array .preinit_array.*))
        PROVIDE_HIDDEN (__preinit_array_end = .);

        PROVIDE_HIDDEN (__init_array_start = .);                /* Вызов конструкторов глобальных объектов. */
        . = ALIGN(4);
        KEEP(*(SORT(.init_array.*)))
        . = ALIGN(4); 
        KEEP(*(.init_array))
        . = ALIGN(4);
        PROVIDE_HIDDEN (__init_array_end = .);

        PROVIDE_HIDDEN (__fini_array_start = .);                /* Вызов деструкторов глобальных объектов. */
        . = ALIGN(4); 
        KEEP(*(SORT(.fini_array.*)))
        . = ALIGN(4); 
        KEEP(*(.fini_array))
        . = ALIGN(4);
        PROVIDE_HIDDEN (__fini_array_end = .);

        . = ALIGN(4);    
        KEEP(*(.cfmconfig))
        . = ALIGN(4);    
        *(.after_vectors .after_vectors.*)
        . = ALIGN(4);
    } >ROM_MAIN_PROGRAMM

    /* Сущности с неизменяемыми значениями. */
    .section_user_code_rodata : ALIGN(4)
    {
        . = ALIGN(4);    *(.rodata .rodata.*)
        . = ALIGN(4);
    } >ROM_MAIN_PROGRAMM

    /* Для stack trace. */
    .ARM.exidx :
    {
        . = ALIGN(4);    
        *(.ARM.extab* .gnu.linkonce.armextab.*)
        . = ALIGN(4);    
        *(.ARM.exidx* .gnu.linkonce.armexidx.*)
        . = ALIGN(4);
    } > ROM_MAIN_PROGRAMM

    /*-------------------------------RAM-----------------------------*/
    /* Начальные значения изменяемых сущностей. */
    .section_external_libraries_data : ALIGN(4)
    {
        . = ALIGN(4);    __external_lib_data_start =  . ;

        /* Библиотеки компилятора. */
        . = ALIGN(4);    
        *libgcc.a:*(.data .data*);
        . = ALIGN(4);    
        *libstdc++_nano.a:*(.data .data*);
        . = ALIGN(4);    
        *libg_nano.a:*(.data .data*);

        /* Библиотеки пользователя */
        . = ALIGN(4);    
        *libSTM32F4_LOW_LEVEL_BY_ST.a:*(.data .data*);
        . = ALIGN(4);    
        *libFATFS.a:*(.data .data*);
        . = ALIGN(4);    
        *libFREERTOS.a:*(.data .data*);
        . = ALIGN(4);    
        *libMAKISE_GUI.a:*(.data .data*);

        . = ALIGN(4);    
        __external_lib_data_end =  . ;
    } >RAM_EXTERNAL_LIBRARIES AT> ROM_EXTERNAL_LIBRARIES

    /* Область с нулевыми начальными значениями в RAM */
    .section_external_libraries_bss : ALIGN(4)
    {
        . = ALIGN(4);    
        __external_lib_bss_start = .;

        /* Библиотеки компилятора. */
        . = ALIGN(4);    
        *libgcc.a:*(.bss .bss*);
        . = ALIGN(4);    
        *libstdc++_nano.a:*(.bss .bss*);
        . = ALIGN(4);    
        *libg_nano.a:*(*COMMON);

        . = ALIGN(4);    
        *libgcc.a:*(*COMMON);
        . = ALIGN(4);    
        *libstdc++_nano.a:*(*COMMON);
        . = ALIGN(4);    
        *libg_nano.a:*(*COMMON);

        /* Библиотеки пользователя */
        . = ALIGN(4);    
        *libSTM32F4_LOW_LEVEL_BY_ST.a:*(.bss .bss*);
        . = ALIGN(4);    
        *libFATFS.a:*(.bss .bss*);
        . = ALIGN(4);    
        *libFREERTOS.a:*(.bss .bss*);
        . = ALIGN(4);    
        *libMAKISE_GUI.a:*(.bss .bss*);

        . = ALIGN(4);    
        *libSTM32F4_LOW_LEVEL_BY_ST.a:*(*COMMON);
        . = ALIGN(4);    
        *libFATFS.a:*(*COMMON);
        . = ALIGN(4);    
        *libFREERTOS.a:*(*COMMON);
        . = ALIGN(4);    
        *libMAKISE_GUI.a:*(*COMMON);

        . = ALIGN(4);    __external_lib_bss_end = .;
    } >RAM_EXTERNAL_LIBRARIES

    /* Начальные значения изменяемых сущностей. */
    .section_user_libraries_data : ALIGN(4)
    {
        . = ALIGN(4);    __user_lib_data_start =  . ;

        . = ALIGN(4);    
        *libUSER_FREERTOS_LEVEL.a:*(.data .data*);
        . = ALIGN(4);    
        *libUSER_BSP_LEVEL.a:*(.data .data*);
        . = ALIGN(4);    
        *libMC_INTERRUPT.a:*(.data .data*);
        . = ALIGN(4);    
        *libMC_HARDWARE.a:*(.data .data*);
        . = ALIGN(4);    
        *libPCB_HARDWARE.a:*(.data .data*);
        . = ALIGN(4);    
        *libUSER_STARTUP.a:*(.data .data*);
        . = ALIGN(4);    
        *libBUTTONS.a:*(.data .data*);
        . = ALIGN(4);    
        *libCHIPTUNE.a:*(.data .data*);
        . = ALIGN(4);    
        *libDIGITAL_POTENTIOMETER.a:*(.data .data*);
        . = ALIGN(4);    
        *libLCD_DRIVER.a:*(.data .data*);
        . = ALIGN(4);    
        *libMAKISE_GUI_ELEMENTS_BY_VADIMATORIK_ELEMENTS_BY_VADIMATORIK.a:*(.data .data*);
        . = ALIGN(4);    
        *libMC_HARDWARE_INTERFACES_IMPLEMENTATION_FOR_STM32.a:*(.data .data*);
        . = ALIGN(4);    
        *libMICROSD_LOW_LEVEL_DRIVER.a:*(.data .data*);
        . = ALIGN(4);    
        *libSHIFT_REGISTER.a:*(.data .data*);
        . = ALIGN(4);    
        *libWAVE_GENERATORS.a:*(.data .data*);
        . = ALIGN(4);    
        *libRUN_TIME_LOGGER.a:*(.data .data*);

        . = ALIGN(4);    __user_lib_data_end =  . ;
    } >RAM_USER_LIBRARIES AT> ROM_USER_LIBRARIES

    .section_user_libraries_bss : ALIGN(4)
    {
        . = ALIGN(4);    __user_lib_bss_start = .;

        . = ALIGN(4);    
        *libUSER_FREERTOS_LEVEL.a:*(.bss .bss*);
        . = ALIGN(4);    
        *libUSER_BSP_LEVEL.a:*(.bss .bss*);
        . = ALIGN(4);    
        *libMC_INTERRUPT.a:*(.bss .bss*);
        . = ALIGN(4);    
        *libMC_HARDWARE.a:*(.bss .bss*);
        . = ALIGN(4);    
        *libPCB_HARDWARE.a:*(.bss .bss*);
        . = ALIGN(4);    
        *libUSER_CODE.a:*(.bss .bss*);
        . = ALIGN(4);    
        *libBUTTONS.a:*(.bss .bss*);
        . = ALIGN(4);    
        *libCHIPTUNE.a:*(.bss .bss*);
        . = ALIGN(4);    
        *libDIGITAL_POTENTIOMETER.a:*(.bss .bss*);
        . = ALIGN(4);    
        *libLCD_DRIVER.a:*(.bss .bss*);
        . = ALIGN(4);    
        *libMAKISE_GUI_ELEMENTS_BY_VADIMATORIK_ELEMENTS_BY_VADIMATORIK.a:*(.bss .bss*);
        . = ALIGN(4);    
        *libMC_HARDWARE_INTERFACES_IMPLEMENTATION_FOR_STM32.a:*(.bss .bss*);
        . = ALIGN(4);    
        *libMICROSD_LOW_LEVEL_DRIVER.a:*(.bss .bss*);
        . = ALIGN(4);    
        *libSHIFT_REGISTER.a:*(.bss .bss*);
        . = ALIGN(4);    
        *libWAVE_GENERATORS.a:*(.bss .bss*);
        . = ALIGN(4);    
        *libUSER_FREERTOS_LEVEL.a:*(.bss .bss*);
        . = ALIGN(4);    
        *libRUN_TIME_LOGGER.a:*(.bss .bss*);

        . = ALIGN(4);    
        *libUSER_BSP_LEVEL.a:*(*COMMON);
        . = ALIGN(4);    
        *libMC_INTERRUPT.a:*(*COMMON);
        . = ALIGN(4);    
        *libMC_HARDWARE.a:*(*COMMON);
        . = ALIGN(4);    
        *libPCB_HARDWARE.a:*(*COMMON);
        . = ALIGN(4);    
        *libUSER_CODE.a:*(*COMMON);
        . = ALIGN(4);    
        *libBUTTONS.a:*(*COMMON);
        . = ALIGN(4);    
        *libCHIPTUNE.a:*(*COMMON);
        . = ALIGN(4);    
        *libDIGITAL_POTENTIOMETER.a:*(*COMMON);
        . = ALIGN(4);    
        *libLCD_DRIVER.a:*(*COMMON);
        . = ALIGN(4);    
        *libMAKISE_GUI_ELEMENTS_BY_VADIMATORIK_ELEMENTS_BY_VADIMATORIK.a:*(*COMMON);
        . = ALIGN(4);    
        *libMC_HARDWARE_INTERFACES_IMPLEMENTATION_FOR_STM32.a:*(*COMMON);
        . = ALIGN(4);    
        *libMICROSD_LOW_LEVEL_DRIVER.a:*(*COMMON);
        . = ALIGN(4);    
        *libSHIFT_REGISTER.a:*(*COMMON);
        . = ALIGN(4);    
        *libWAVE_GENERATORS.a:*(*COMMON);
        . = ALIGN(4);    
        *libRUN_TIME_LOGGER.a:*(.COMMON*);

        . = ALIGN(4);    __user_lib_bss_end = .;
    } >RAM_USER_LIBRARIES

    /* Начальные значения изменяемых сущностей. */
    .section_user_code_data : ALIGN(4)
    {
        . = ALIGN(4);    __user_code_data_start =  . ;

        . = ALIGN(4);    *(.data .data.*)

        . = ALIGN(4);    __user_code_data_end =  . ;
    } >RAM_MAIN_PROGRAMM AT> ROM_MAIN_PROGRAMM

    .section_user_code_bss : ALIGN(4)
    {
        . = ALIGN(4);    __bss_start__ = .;    __user_code_bss_start = .;
        *(.bss .bss.*)
        *(COMMON)
        . = ALIGN(4);    __bss_end__ = .;    __user_code_bss_end = .;
    } >RAM_MAIN_PROGRAMM

    __external_lib_data_in_rom_start = LOADADDR(.section_external_libraries_data);
    __user_lib_data_in_rom_start = LOADADDR(.section_user_libraries_data);
    __user_code_data_in_rom_start = LOADADDR(.section_user_code_data);

    /*-------------------------Отладочная информация-----------------*/
    /* Stabs debugging sections.  */
    .stab          0 : { *(.stab) }
    .stabstr       0 : { *(.stabstr) }
    .stab.excl     0 : { *(.stab.excl) }
    .stab.exclstr  0 : { *(.stab.exclstr) }
    .stab.index    0 : { *(.stab.index) }
    .stab.indexstr 0 : { *(.stab.indexstr) }
    .comment       0 : { *(.comment) }

    /*
     * DWARF debug sections.
     * Symbols in the DWARF debugging sections are relative to the beginning
     * of the section so we begin them at 0.
     */
    /* DWARF 1 */
    .debug          0 : { *(.debug) }
    .line           0 : { *(.line) }
    /* GNU DWARF 1 extensions */
    .debug_srcinfo  0 : { *(.debug_srcinfo) }
    .debug_sfnames  0 : { *(.debug_sfnames) }
    /* DWARF 1.1 and DWARF 2 */
    .debug_aranges  0 : { *(.debug_aranges) }
    .debug_pubnames 0 : { *(.debug_pubnames) }
    /* DWARF 2 */
    .debug_info     0 : { *(.debug_info .gnu.linkonce.wi.*) }
    .debug_abbrev   0 : { *(.debug_abbrev) }
    .debug_line     0 : { *(.debug_line) }
    .debug_frame    0 : { *(.debug_frame) }
    .debug_str      0 : { *(.debug_str) }
    .debug_loc      0 : { *(.debug_loc) }
    .debug_macinfo  0 : { *(.debug_macinfo) }
    /* SGI/MIPS DWARF 2 extensions */
    .debug_weaknames 0 : { *(.debug_weaknames) }
    .debug_funcnames 0 : { *(.debug_funcnames) }
    .debug_typenames 0 : { *(.debug_typenames) }
    .debug_varnames  0 : { *(.debug_varnames) }

    .debug_macro  0 : { *(.debug_macro) }
    .debug_ranges  0 : { *(.debug_ranges) }
}

Добавляем в основной проект утилиту для извлечения разделов из конечного bin файла


К сожалению, не удалось найти ни в objcopy ни в objdump флагов для извлечения кода между определнными адресами из elf файла. Существует флаг --only-section, однако он не учитывает того факта, что после всех перечисленных в section.ld сущностей раздела в энергонезависимой памяти еще помещается отладочная информация. Без нее конечная прошивка, собранная из кусочков, работать не будет (по понятным причинам). Поэтому пришлось написать простенькую утилиту, которая принимает общий bin файл и по указанному промежутку адресов извлекает в отдельный файл требуемый участок. Однако тут возникает следующий нюанс. По умолчанию objcopy заполняет пространство между секциями 0-ми. Однако пустое пространство в flash памяти — это 0xFF. Для решения этой проблемы требуется компоновать выходной bin файл с флагом --gap-fill=0xff.

Добавить в проект вызов скрипта обновления энергонезависимой памяти микроконтроллера при обновлении файла прошивки


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

Код скрипта сравнения
#!/bin/bash

#
# $1 - старый файл. Он может отсутствовать, если происходит первая компиляция.
# $2 - только что извлеченный из elf файл.
# $3 - строка для прошивки требуемого сектора STM32.
#

echo "Old file name: $1" 
echo "New file name: $2" 

# Предполагаем, что ничего перепрошивать не придется.
flag_rewrite=0

# Если это первая компиляция, то старого файла не существует и можно просто
# переименовать новый, чтобы он назывался как старый (потому что прошивка
# происходит по имени старого файла).
# Если это новая компиляция, то нужно узнать, различаются ли чем-то файлы или нет
# если да, то надо заменить старый новым. Если нет, то удалить новый.
if [ -e $1 ]
then # Если это уже не первая компиляция и файл существовал.
echo "Both files exist."
# Получаем md5 сумму входных файлов, анализируя файл в бинарном формате.
buf=$(md5sum $1 --binary)
md5_old=${buf:0:32} # Из вывода нам нужна только md5 без имени флага. 
                    # Это первые 32 символа.
buf=$(md5sum $2 --binary)
md5_new=${buf:0:32}

echo "Started file comparison." 
if [ $md5_old == $md5_new ]
then # Если файл не был обновлен, удаляем новый.
echo "The file has not been updated."
echo "The new file will be deleted."
rm $2
echo "Removed."
else # Если они разные, тогда заменяем старый новым.
echo "The file has been modified."
echo "Old will be replaced by new."
mv $2 $1
echo "Replaced."
flag_rewrite=1 # Придется перепрошивать этот сектор.
fi
else # Если это первая компиляция.
echo "Old file does not exist."
echo "New will be renamed to old."
mv $2 $1 # Переименовываем файл под старый.
flag_rewrite=1 # Придется перепрошивать этот сектор.
echo "Renamed."
fi

# Если файл был обновлен или только появился, то им надо прошить контроллер.
if [ $flag_rewrite -eq 1 ]
then
echo "Started flashing."
echo "CMD params: $3"
$3
fi


В самом проекте можно вызвать cmake функцию, которая сделает все необходимое:
Cmake функция обновления
function(write_sector SECTOR ADDR_BASE ADDR_START ADDR_END)
    add_custom_command(TARGET ${PROJECT_NAME}.elf POST_BUILD
            COMMAND ${ARM_OBJCOPY} --output-target=binary --gap-fill=0xff ${PROJECT_BINARY_DIR}/${PROJECT_NAME}.elf ${PROJECT_BINARY_DIR}/${PROJECT_NAME}_all.bin
            COMMENT "Creating a binary file of the <<${SECTOR}>> sector"
            COMMAND ${BIN_EXTRACTOR} -p ${PROJECT_BINARY_DIR}/${PROJECT_NAME}_all.bin -o ${PROJECT_BINARY_DIR}/${PROJECT_NAME}_section_${SECTOR}_new.bin -b ${ADDR_BASE} -s ${ADDR_START} -e ${ADDR_END}
            COMMAND cd ${CMAKE_SOURCE_DIR} && ./cmp.sh ${PROJECT_BINARY_DIR}/${PROJECT_NAME}_section_${SECTOR}.bin ${PROJECT_BINARY_DIR}/${PROJECT_NAME}_section_${SECTOR}_new.bin "${STM32PROG} -c port=${STM32PROG_PORT} freq=${STM32PROG_FREQ} -w ${PROJECT_BINARY_DIR}/${PROJECT_NAME}_section_${SECTOR}.bin ${ADDR_START}")
endfunction(write_sector)

Для записи функция использует stm32programmer.
Пример использования функции из кода проекта
if (STM32PROG_USE STREQUAL "ON")
    write_sector("bootloader" ${SECTION_BOOTLOADER_ADDRESS} ${SECTION_BOOTLOADER_ADDRESS} ${SECTION_SYSCFG_PAGE_1_ADDRESS})
    write_sector("external_libraries" ${SECTION_BOOTLOADER_ADDRESS} ${SECTION_EXTERNAL_LIB_ADDRESS} ${SECTION_USER_LIBRARIES_ADDRESS})
    write_sector("user_libraries" ${SECTION_BOOTLOADER_ADDRESS} ${SECTION_USER_LIBRARIES_ADDRESS} ${SECTION_USER_CODE_ADDRESS})
    write_sector("main_programm" ${SECTION_BOOTLOADER_ADDRESS} ${SECTION_USER_CODE_ADDRESS} ${ADDR_END_FLASH})
endif ()


Выводы


Плюсы данного подхода:

  1. в 95% случаев обновляется действительно то, что нужно;

Минусы данного подхода:
  1. нет никакого выигрыша в скорости, поскольку перед каждой прошивкой требуется загружать в микроконтроллер загрузчик для прошивки энергонезависимой памяти (это делается автоматически stm32programmer-ом). Как раз напротив, когда проект пересобирается полностью, то зачастую приходиться шить заново все разделы;
  2. размер section.ld отбивает всякое желание добавить или изменить в нем что-то. Если и потребуется применить данную методологию в реальном проекте, то придется писать удобный GUI для редактирования этого файла;
  3. в случае, если устройство управляет собственным питанием, то можно не заметить, что один из разделов не был зашит верно (при просадке напряжения, например) и долго отлаживать разделы из разных сборок :).

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

Средняя зарплата в IT

120 000 ₽/мес.
Средняя зарплата по всем IT-специализациям на основании 7 453 анкет, за 1-ое пол. 2021 года Узнать свою зарплату
Реклама
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее

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

    +1

    С таким подходом можно было уже взять NuttX и ложить "изменяемую часть" в файловую систему.

      +2
      Сейчас отрабатываю вариант с Lua. Пробрасываю интерфейсы аппаратки к машине и вообще из консоли пишу логику. Данным методом решил поделиться, чтобы ни у кого не возникало желания попробовать самому. Или по крайней мере знали, к чему может привести. По сути — поделился опытом.
      +1
      Может имеет смысл написать свой bootloader, который будет делать IAP только изменившихся секций?
      Как раз напротив, когда проект пересобирается полностью, то зачастую приходиться шить заново все разделы;

      т.е. после пересборки если код для раздела не менялся, то бинарный код для раздела каждый раз получается разным?
        0
        Не всегда, но случается. Быстрый анализ по разделу показал, что при -g3 иногда добавляется отладочная информация о функциях, расположенных в соседних разделах. На -g2 такого уже не наблюдал. А про загрузчик — я уже описал выше свое решение. Ресурсы процессора позволяют.
        +1
        Подскажите, а есть какое-нить хорошее руководство по линкеру gcc?
        0
        Слишком много Си и C++. Читать сложно.
          0
          Сарказм? Не до конца понимаю, на что вы пытаетесь указать. Если вы про тег, то проект на С и С++. Правда цель статьи не разбор использования языка, а того, что «после него выходит».
            0
            Да. Сарказм. Просто ожидал увидеть хоть немного Си или C++ кода.
          0
          Нормальный отладчик грузит 512 кб кода во внутреннюю флэш за пару секунд. Решительно не вижу никакой необходимости тратить свои силы и время на оптимизацию этого процесса. Тем более, что отладка зачастую затрагивает сразу несколько изменений и требует на порядки больше времени. И да, у меня был аналогичный проект, но там чисто бизнес-код занимал почти всю внутреннюю память, строки, знакогенераторы, звуки, картинки и некоторые крупные структуры лежали во внешней памяти и были размером в районе 3 Мб.
            0
            Нормальный отладчик грузит 512 кб кода во внутреннюю флэш за пару секунд.

            Можно уточнить, какой? Конкретно stm32f405rgt6 с помощью официального st-link v2 на «4000» шьет порядка 10 секунд (на отладочной можно в режиме «9000» шить, но почему-то на моем конкретном чипе не прокатывает). J-link с AliExpres шьет примерно так же. Причем я сейчас говорю про скорость по JTAG порту, а не по SWD.
            Решительно не вижу никакой необходимости тратить свои силы и время на оптимизацию этого процесса.

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

            Тут когда как. Порой хочется исправить, например, неверный переход по меню и сразу посмотреть, как оно работает. Но это уже отладка бизнес-логики. Ее лучше производить либо в проекте собранном под компьютер, либо через скриптовую машину, если таковая есть в проекте.
            И да, у меня был аналогичный проект, но там чисто бизнес-код занимал почти всю внутреннюю память, строки, знакогенераторы, звуки, картинки и некоторые крупные структуры лежали во внешней памяти и были размером в районе 3 Мб.

            Предпочитаю, если есть возможность, ставить что-то типа micro-sd или, если недопустимо по требованиям вибрационной устойчивости или прочего, то на flash на плате накатывать fat и использовать файловую систему для всяких картинок и прочего. Это дает не сильные накладные расходы, зато удобно сопровождать.
              0
              У меня uLink2 был, работал с Keil, интерфейс — SWD, чисто визуально шилось 3-4 секунды.
              По поводу внешней карточки с FAT не соглашусь, оно, конечно, работать будет, но потеря скорости очень существенная, да и апдейтить такие устройства гораздо сложнее. Так шьется просто вся прошивка, а так нужно будет уже смотреть, что куда раскладывать, если какие-то картинки появились только в новой версии.
                0
                У меня uLink2 был

                Не работал. Возьму на заметку. Попробую достать. А какой контроллер шили?
                но потеря скорости очень существенная

                SDIO интерфейс использовали? Или по SPI? Я стараюсь первый использовать. 4 бита. В проекта что разбирал в статье — это избыточно. Но вот например когда идет запись видео, хотя бы 480p, то уже критично.
                да и апдейтить такие устройства гораздо сложнее

                Спорный вопрос. Если вы храните пакет на git, то вам нет труда отследить соответствие.

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

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