Дополнение статьи «Размещение кучи FreeRTOS в разделе CCMRAM для STM32», и в продолжение серии статей про различные полезности для STM32 (1, 2 и 3), хочу обратить внимание на одну особенность работы с CCM RAM памятью, которая может быть причиной совершенно не очевидных ошибок в работе устройств, одна из которых выпила у автора достаточно много крови, так что с чистой совестью её действительно можно назвать «кровавой».

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

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


Так как даже аппаратный сброс не всегда помогает, я сперва грешил на проблемы с железом. Ведь если проблема остается и после перезагрузки по кнопке RESET и по NVIC_SystemReset(), то самое логичное, это искать ошибку в аппаратной части. Если бы не одно, но очень веское НО, подобное поведение было у всех устройств, а не на единичном экземпляре оборудования.

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

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

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

Вы наверно уже догадались, что причина была в том, что куча для STM32F4xx была перенесена в регион CCMRAM, а параметры работы оборудования хранились именно там.
uint8_t ucHeap[ configTOTAL_HEAP_SIZE ] __attribute__((section(".ccmram"))) = {0};

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

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

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



Комментарии (28)


  1. VelocidadAbsurda
    15.09.2021 22:38
    +9

    Отличный пример дичи, которую устроили вендоры Cortex-M в своих библиотеках, притащив туда из прошлого традицию нечитаемого ассемблерного стартового кода.
    Что имею в виду: ядра Cortex-M были специально разработаны под программирование "без единой строчки на асме" (за счёт таблицы векторов, содержащей указатели на вершину стека и точки входа — такое, в отличие от jump table более старых ARM7-11, можно объявить как простой массив на С). Предполагалось, что при старте будем сразу попадать в пользовательский код, который в явном виде будет содержать инициализацию памяти, но вендоры вместо этого в своих библиотеках похоронили старт в startup_xxx.s и неявно вызываемой оттуда функции SystemInit (регулярно встречаю посты об "открытиях" вида "практически сразу после старта ушёл в сон, думая, что работаю на встроенном осцилляторе и сберегу энергию, а оно почему-то молотит на 168МГц от PLL, который я не включал!").


    1. bullitufa
      16.09.2021 16:15
      +1

      Какая разница что подразумевал вендор со своими библиотеками? Он Вас заставлял пользоваться startup_xxx.s? Нет. Мало ли какие Вы файлы подсунули в компиляцию.

      А по поводу ccmram: эта область в плане значений по умолчанию после сброса, ничем не отличается от остальной! Ну да, Вы (@rsashka) просто пропустили этот момент.

      Для отлова таких моментов стоит пользоваться асертами, memset-ами.


      1. rsashka Автор
        16.09.2021 16:18

        Она отличается тем, что загрузчик прошивки (startup_xxx.s) её не инициализирует, в отличие от основной памяти.


        1. bullitufa
          19.09.2021 23:40

          Правильно сказать, что файл startup_xxx.s от st-шников не содержит код инициализации bss и data данных размещенных в области ccmram.

          Ладно хоть код для backup_sram нет))


  1. buratino
    16.09.2021 08:46
    +2

    Аналогичная фингя происходит при использовании эзернет контроллера KS8721B (при использовании совместно с LPC1768). Штатная процедура ресета и инициализации не помогает, только выключение питания. После пролития бочки крови поставили отдельное включение питания.


    1. rsashka Автор
      16.09.2021 08:51

      Спасибо за информацию! Вот уж действительно, век живи - век учись.

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


    1. bullitufa
      20.09.2021 13:02

      Не уверен, но думаю что вы боритесь со "Straping Options", LATCH или не выдерживаете времена. Некоторые выводы используются для конфига (ну да извращенцы микрочиповцы). Reset Timing params, там есть ссылка на straping options. Strap кстати бывает аппаратный (ногами) и программный (через регистры).
      Посмотрите, может оно.


      1. buratino
        21.09.2021 00:32

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


        1. bullitufa
          21.09.2021 07:50

          Что значит работал?) Лампочки моргали link и speed? Дык они без mac микроконтроллера будут работать! За RMII же всего лишь mac. Вот например вывод: RXD3/ PHYAD1 - выбирает phy addr при сбросе. А что у вас было в это время на ножках? А вот когда вы после иниц. мк сбрасывали питание на ногах мк в сторону eth уже другие значения. Ну как вариант. А сброс (pin reset) не помогал?


          1. buratino
            21.09.2021 09:04

            Поскольку эта херь приключалась не сразу, а у заказчика, то на ногах не смотрели. И и толку смотреть особо нет, потому как все общение с теми ногами идет через "неонку" в виде регистров LPC_EMAC.

            Причем заказчик был не первый, но у него вот это вот стало проявляться относительно чаще. После этого заметили эффект и в лабораторных условиях. Ноги все разведены как демоплате, с которой проблем не было. И вообще такое ощущение, что проблемы появились через какое-то время, с закупкой другой партии KS... или LPC


  1. Firelander
    16.09.2021 12:55
    -1

    После SystemInit вызывается __main. Это функция в которой компилятор и делает все свои черные дела по инициализации. То что в вашем случае память не инициализируется возможно связано с опциями линкера для этого региона памяти.


  1. Arcanum7
    16.09.2021 15:09
    +1

    Не камень в огород автору, но всё таки обнулять все переменные логики верхнего уровня руками (memset для массивов и структур и = 0 для переменных) - хороший тон.


    1. rsashka Автор
      16.09.2021 16:21

      В том-то и дело, что я обнуляю память ={0}, только это не работает для CCMRAM.


  1. Alex-111
    16.09.2021 15:41

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


    1. rsashka Автор
      16.09.2021 16:35

      malloc - да, не обнуляет выделяемую память (для этих целей есть calloc), но я то думал, что инициализацию память uint8_t ucHeap[ configTOTAL_HEAP_SIZE ] attribute((section(".ccmram"))) = {0};, но оказалось, что я думал неправильно.


      1. Alex-111
        16.09.2021 16:47
        +1

        Согласен, что тут есть нарушение стандарта, даже без ={0} глобальный массив должен был быть инициализирован. Но в вашем конкретном примере речь идет о буфере с которым предполагается работа только через менеджер памяти, который не надеется на то, что этот буфер был инициализирован (если у вас конечно не какой-то кастомный менеджер).
        Я так полагаю, где-то вы выделяете кусок памяти и используете его без инициализации. memset() нужно поставить туда, где идет выделение, а не в начало программы.


        1. rsashka Автор
          16.09.2021 17:01

          Нет, я использую обычную переменную класса, а память под экземпляр класса выделяю с помощью new из кучи.


          1. Alex-111
            16.09.2021 17:30

            Тогда переменную класса нужно инициализировать в конструкторе, который будет вызван new.
            Вы поймите, что получать не инициализированную память "из вне" — это частая практика C/C++. Пользователь сам ответственен за инициализацию.
            Я, кончено, тут отдалился от того, на что вы хотели обратить внимание изначально. Но с этой проблемой вы столкнулись из-за того, что неправильно работаете с хипом. И в следующий раз можете поймать такое же странное поведение, но уже не по вине редкоиспользуемого атрибута, а просто потому что нет гарантий, что в памяти не мусор.


            1. rsashka Автор
              16.09.2021 18:08

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

              Вот если бы можно было писать программы без ошибок, эххх....


              1. Alex-111
                16.09.2021 18:23

                Смутило, что вы не остановились на том, чтобы просто инициализировать переменную, а решили раскопать причину. (Что, кончено, получилось познавательно!) А из стати мне показалось будто вы занулили память при старте и продолжили пользоваться не инициализированной (явно) переменной, как будто это нормально...


          1. bullitufa
            19.09.2021 23:55

            Наверняка g++ ругался на класс или конструктор в котором не инициализирован член класса. CubeIDE (Eclipse) подсвечивает! Есть (вроде!) ключ компиляции отслеживающий это дело в конструкторе. Если не забуду напишу, сейчас не под рукой.


      1. ibrin
        16.09.2021 17:07

        Какую цель преследует заполнение выделяемой памяти нулями? И почему именно нулями, а не, допустим, 0xFF?


        1. rsashka Автор
          16.09.2021 17:11

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


          1. ibrin
            26.09.2021 12:12

            А если инициализировать результатом? Не записывать в выделенную область памяти что-то сначала, а потом сохранять там результаты вычислений, а сразу писать результаты?


      1. 100h
        16.09.2021 21:00

        Ваш случай меня заинтересовал, поскольку сам интенсивно использую CCMRAM в проектах на STM32F40x и STM32F303.

        В качестве тулчейна - gcc-arm-none-eabi.

        Попробовал воспроизвести ситуацию.

        Объявил три массива, два из них инициализированы ненулевым значением, а третий - как у вас, {0U}.

        #define ATTR_CCMRAM __attribute__((section (".ccmram")))
        
        uint8_t BUFFF1[256] ATTR_CCMRAM = {"BUFF... ...BUFF"};
        uint8_t BUFFF2[256] ATTR_CCMRAM = {0U};
        uint8_t BUFFF3[256] ATTR_CCMRAM = {"BUFF... ...BUFF"};                  

        После компиляции и линковки, все три массива попали в бинарник для прошивки. Это же подтверждает и .map файл:

        .ccmram	.ccmram	10005030	256	BUFFF3		Core/Src/freertos.c.obj	BUFFF3
        .ccmram	.ccmram	10005130	256	BUFFF2		Core/Src/freertos.c.obj	BUFFF2
        .ccmram	.ccmram	10005230	256	BUFFF1		Core/Src/freertos.c.obj	BUFFF1

        В линкер скрипте указано:

          _siccmram = LOADADDR(.ccmram);
        
          /* CCM-RAM section 
          * 
          * IMPORTANT NOTE! 
          * If initialized variables will be placed in this section,
          * the startup code needs to be modified to copy the init-values.  
          */
          .ccmram :
          {
            . = ALIGN(4);
            _sccmram = .;       /* create a global symbol at ccmram start */
            *(.ccmram)
            *(.ccmram*)
            
            . = ALIGN(4);
            _eccmram = .;       /* create a global symbol at ccmram end */
          } >CCMRAM AT> FLASH

        Т.е. секция гарантированно будет в прошивке. Но вот загрузка при старте МК должна быть реализована программистом, типовые startup.s секцию ccmram не переносят. Нужно добавить вручную:

        /* startup_stm32f405xx.s */
        ....
        CopyDataInit1:
          ldr     r3, =_siccmram
          ldr     r3, [r3, r1]
          str     r3, [r0, r1]
          adds    r1, r1, #4
        
        LoopCopyDataInit1:
          ldr     r0, =_sccmram
          ldr     r3, =_eccmram
          adds    r2, r0, r1
          cmp     r2, r3
          bcc     CopyDataInit1
        /* ccm ram load end */
        ...

        В итоге я получил все три массива на своих местах в ОЗУ при старте/рестарте МК.


        1. rsashka Автор
          16.09.2021 21:15

          Но вот загрузка при старте МК должна быть реализована программистом, типовые startup.s секцию ccmram не переносят.

          Вот в этом и была засада.


          1. predator86
            17.09.2021 16:33

            Попробовал на IAR: uint8_t ucHeap[ 100 ] @ ".ccram"; — всё нормально обнулилось.


            1. rsashka Автор
              17.09.2021 16:36

              У меня STM32CubeIDE с gcc-arm-none-eabi