Как стать автором
Обновить

Почти детективная история одной «кровавой» ошибки

Блог компании Timeweb Программирование *Промышленное программирование *Программирование микроконтроллеров *Производство и разработка электроники *
Tutorial
Всего голосов 31: ↑29 и ↓2 +27
Просмотры 7.1K
Комментарии 28

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Ваш случай меня заинтересовал, поскольку сам интенсивно использую 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 */
...

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

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

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

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

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

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