Дополнение статьи «Размещение кучи 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)
buratino
16.09.2021 08:46+2Аналогичная фингя происходит при использовании эзернет контроллера KS8721B (при использовании совместно с LPC1768). Штатная процедура ресета и инициализации не помогает, только выключение питания. После пролития бочки крови поставили отдельное включение питания.
rsashka Автор
16.09.2021 08:51Спасибо за информацию! Вот уж действительно, век живи - век учись.
Самое интересно, что мы с устройством из статьи поступали почти точно так же. Прикрутили внешнее реле для физического отключения питания. Выглядело это ужасно, но щелкало и работало.
bullitufa
20.09.2021 13:02Не уверен, но думаю что вы боритесь со "Straping Options", LATCH или не выдерживаете времена. Некоторые выводы используются для конфига (ну да извращенцы микрочиповцы). Reset Timing params, там есть ссылка на straping options. Strap кстати бывает аппаратный (ногами) и программный (через регистры).
Посмотрите, может оно.buratino
21.09.2021 00:32микрочиповцы конечно же извращенцы. Но в данном случае я брал
стандартный примеркусок стандартной библиотеки в исходниках филлипсовцев для инициализации эзернетов и там весь ресет заключаетсяв дрыгаье одной ногой с последующей задержкойинициализации эзернетовских регистров LPC, задержки и чтения регистра же LPC с готовностью. Так вот в случае аналогичном описанному готовность не появлялась никогда сколько не жди, при этом внешне KS работал, лампочки моргалиbullitufa
21.09.2021 07:50Что значит работал?) Лампочки моргали link и speed? Дык они без mac микроконтроллера будут работать! За RMII же всего лишь mac. Вот например вывод: RXD3/ PHYAD1 - выбирает phy addr при сбросе. А что у вас было в это время на ножках? А вот когда вы после иниц. мк сбрасывали питание на ногах мк в сторону eth уже другие значения. Ну как вариант. А сброс (pin reset) не помогал?
buratino
21.09.2021 09:04Поскольку эта херь приключалась не сразу, а у заказчика, то на ногах не смотрели. И и толку смотреть особо нет, потому как все общение с теми ногами идет через "неонку" в виде регистров LPC_EMAC.
Причем заказчик был не первый, но у него вот это вот стало проявляться относительно чаще. После этого заметили эффект и в лабораторных условиях. Ноги все разведены как демоплате, с которой проблем не было. И вообще такое ощущение, что проблемы появились через какое-то время, с закупкой другой партии KS... или LPC
Firelander
16.09.2021 12:55-1После SystemInit вызывается __main. Это функция в которой компилятор и делает все свои черные дела по инициализации. То что в вашем случае память не инициализируется возможно связано с опциями линкера для этого региона памяти.
Alex-111
16.09.2021 15:41Так если речь идет о куче динамической памяти, то и не должно быть гарантий, что она инициализирована нулем.
malloc()
тот же не инициализированный кусок памяти дает.rsashka Автор
16.09.2021 16:35malloc - да, не обнуляет выделяемую память (для этих целей есть calloc), но я то думал, что инициализацию память uint8_t ucHeap[ configTOTAL_HEAP_SIZE ] attribute((section(".ccmram"))) = {0};, но оказалось, что я думал неправильно.
Alex-111
16.09.2021 16:47+1Согласен, что тут есть нарушение стандарта, даже без
={0}
глобальный массив должен был быть инициализирован. Но в вашем конкретном примере речь идет о буфере с которым предполагается работа только через менеджер памяти, который не надеется на то, что этот буфер был инициализирован (если у вас конечно не какой-то кастомный менеджер).
Я так полагаю, где-то вы выделяете кусок памяти и используете его без инициализации.memset()
нужно поставить туда, где идет выделение, а не в начало программы.rsashka Автор
16.09.2021 17:01Нет, я использую обычную переменную класса, а память под экземпляр класса выделяю с помощью new из кучи.
Alex-111
16.09.2021 17:30Тогда переменную класса нужно инициализировать в конструкторе, который будет вызван new.
Вы поймите, что получать не инициализированную память "из вне" — это частая практика C/C++. Пользователь сам ответственен за инициализацию.
Я, кончено, тут отдалился от того, на что вы хотели обратить внимание изначально. Но с этой проблемой вы столкнулись из-за того, что неправильно работаете с хипом. И в следующий раз можете поймать такое же странное поведение, но уже не по вине редкоиспользуемого атрибута, а просто потому что нет гарантий, что в памяти не мусор.rsashka Автор
16.09.2021 18:08Да и не возражаю, что это мой косяк из-за использования не инициализированной переменной (хотя я и считал, что обнуляю выделяемую память). Проблема была это найти :-)
Вот если бы можно было писать программы без ошибок, эххх....
Alex-111
16.09.2021 18:23Смутило, что вы не остановились на том, чтобы просто инициализировать переменную, а решили раскопать причину. (Что, кончено, получилось познавательно!) А из стати мне показалось будто вы занулили память при старте и продолжили пользоваться не инициализированной (явно) переменной, как будто это нормально...
bullitufa
19.09.2021 23:55Наверняка g++ ругался на класс или конструктор в котором не инициализирован член класса. CubeIDE (Eclipse) подсвечивает! Есть (вроде!) ключ компиляции отслеживающий это дело в конструкторе. Если не забуду напишу, сейчас не под рукой.
ibrin
16.09.2021 17:07Какую цель преследует заполнение выделяемой памяти нулями? И почему именно нулями, а не, допустим, 0xFF?
rsashka Автор
16.09.2021 17:11По большому счету, какое значение без разницы, лишь бы память была инициализирована, но нулями привычнее.
ibrin
26.09.2021 12:12А если инициализировать результатом? Не записывать в выделенную область памяти что-то сначала, а потом сохранять там результаты вычислений, а сразу писать результаты?
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 */ ...
В итоге я получил все три массива на своих местах в ОЗУ при старте/рестарте МК.
rsashka Автор
16.09.2021 21:15Но вот загрузка при старте МК должна быть реализована программистом, типовые startup.s секцию ccmram не переносят.
Вот в этом и была засада.
predator86
17.09.2021 16:33Попробовал на IAR:
uint8_t ucHeap[ 100 ] @ ".ccram";
— всё нормально обнулилось.
VelocidadAbsurda
Отличный пример дичи, которую устроили вендоры Cortex-M в своих библиотеках, притащив туда из прошлого традицию нечитаемого ассемблерного стартового кода.
Что имею в виду: ядра Cortex-M были специально разработаны под программирование "без единой строчки на асме" (за счёт таблицы векторов, содержащей указатели на вершину стека и точки входа — такое, в отличие от jump table более старых ARM7-11, можно объявить как простой массив на С). Предполагалось, что при старте будем сразу попадать в пользовательский код, который в явном виде будет содержать инициализацию памяти, но вендоры вместо этого в своих библиотеках похоронили старт в startup_xxx.s и неявно вызываемой оттуда функции SystemInit (регулярно встречаю посты об "открытиях" вида "практически сразу после старта ушёл в сон, думая, что работаю на встроенном осцилляторе и сберегу энергию, а оно почему-то молотит на 168МГц от PLL, который я не включал!").
bullitufa
Какая разница что подразумевал вендор со своими библиотеками? Он Вас заставлял пользоваться startup_xxx.s? Нет. Мало ли какие Вы файлы подсунули в компиляцию.
А по поводу ccmram: эта область в плане значений по умолчанию после сброса, ничем не отличается от остальной! Ну да, Вы (@rsashka) просто пропустили этот момент.
Для отлова таких моментов стоит пользоваться асертами, memset-ами.
rsashka Автор
Она отличается тем, что загрузчик прошивки (startup_xxx.s) её не инициализирует, в отличие от основной памяти.
bullitufa
Правильно сказать, что файл startup_xxx.s от st-шников не содержит код инициализации bss и data данных размещенных в области ccmram.
Ладно хоть код для backup_sram нет))