Pull to refresh

Comments 49

Был интересный случай: использовали BLE-чип CC2650MODA для одного проекта, активно использовали UART для связи с STM32F10x. В описании BLE-чипа ясно написано, что все входы-выходы свободно настраиваемые. Отлично, написали софт, всё работает, как надо. Коллеги сделали HW-layout и всё бы хорошо. Но потом пришёл допзаказ на бутлоадер для BLE-чипа. Написал начало, проверил у себя на макетной плате — всё работает. Прошил на прототип — контроллер не стартует. Ломали голову недолго, но сильно. Причина: если в нормальном режиме входы и выходы BLE-чипа можно свободно настраивать, то в режиме загрузчика они прописаны жёстко. Точно так же жёстко эти выходы были подключены к UART-входам STM32 (которые менять нельзя). В итоге имели Tx-Rx и Rx-Tx в нормальном режиме и Tx-Tx и Rx-Rx в режиме загрузчика. Заказчик отказался вносить изменения в прототип, так как уже большой тираж вышел в производство, поэтому отказались от бутлоадера совсем.

P.S.: на навесной плате проблем не было, потому что я на автомате подключал всё правильно.
Сплошь и рядом информации в мануале столько, что даже и не пытаешься узнать всё: все равно не запомнишь. Оно и хорошо, лучше больше данных по чипу, чем меньше, но там в середине может быть зарыта вот такая «мелочь». И пока не побьешься головой о какой-то странный случай — не узнаешь, что в твоих условиях это, внезапно, важно.
В 2650 отдельно бесит, что ножки загрузчика разные в разных вариантах упаковки чипа — DIO 0/1, DIO 1/2 или DIO 2/3 для UART в трёх вариантах корпуса.

Интересно, сколько народа на этом прокололось, в процессе поменяв корпус на более жирный, а ножки оставив прежние.
А количество ножек одинаковое в разных упаковках? Если разное, то можно и не надеяться особо на совпадения, а вот если одно и то же — тогда да, абыдна!
Разное.

Но фокус в том, что не совпадает не положение ножек на корпусе, а их номер в порту, то есть сущность чисто виртуальная.
А, в этом смысле. Ну это обалдеть можно тогда.
Примерно через то же место реализован их BLE-стек. И многие разработчики находят решение только на форуме ТП Texas Instruments. Где сами разрабы сначала отвечают «ща мы посмотрим» и через неделю-месяц отвечают, что они написали модуль/код/функцию, которая реализует хотелку обратившегося. С одной стороны хорошо работают и идут навстречу, с другой стороны почему сразу не сделать нормально?
Кстати, не возьмусь утверждать за все модели STM32, но на многих (старших версиях) видел возможность свапа TX-RX на аппаратном уровне в расширенной инициализации библиотеки HAL. Мало ли, может кому-то поможет избежать неприятностей.

На Meeting C++ 2015 демонстрировали ряд приемов, как избавляться в частности от выделения на куче с помощью шаблонов и compile-time оптимизации в статические объекты. Очень много метапрограммирования.


Еще сейчас в рамках работы над embedded Rust очень активно разрабатывают решение в принципе этих же проблем: оно очень хорошо ложится на Rust-овскую философию "все проверяем на compile-time". Пока уверенно работает только на некоторых stm32 и немножко на msp430, пока не продакшн, но для некоторых проектов уже можно. В любом случае сильно проще C++ получается.

А ведь до того момента я об ее объеме даже и задумывался. На черта ли сдалась мне эта куча, полагал я. Все равно у меня все переменные и объекты либо статические, либо лежат на стеке. Так, просто по инерции оставил под нее 0х300 байт, раз уж какой-то объем под кучу выделяется во всех шаблонных проектах. Ан нет вот, для рантайма С++ все равно нужна динамически выделяемая память, причем в достаточно заметных, по меркам контроллеров, количествах.

С этим можно справится, если переопределить функцию, которая вызывается после main'a. Если у вас ARM (и Keil), то эта функция называется __aeabi_atexit. Если сделать ее пустой, то куча будет не нужна.


int __aeabi_atexit(void)
{
    return 1;
}

Я вообще предпочитаю делать #pragma import(__use_no_heap_region), чтобы сразу получать ошибку компиляции, если кто-то где-то пытается трогать кучу.

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

Вообще, статические объекты в области видимости функции, зная как они создаются и разрушаются, а разрушаться, по стандарту, они должны в строго противоположном конструированию порядке (поэтому и замут с atexit, так бы просто: аллоцировали памяти под объект чуть больше и флаг "создан/нет" туда, а затем в список деструкторов, как для глобальных статических объектов /которые в единице трансляции, ну или просто глобальные/), стоит избегать, как вносящие дополнительную путаницу и неопределённость, ровно, как и динамическая аллокация памяти.


А вообще, у меня на проекте с Cypress FX3 рантайм стал самописным, только строго то, что нужно /зато потом на язык и код смотришь сильно по другому, очень многое становится на свои места/. Хотя динамическая аллокация используется в коде.


Ну и если пошло про malloc, то, по крайней мере в случае с Newlib, ещё стоит реализовать свои __malloc_lock()/__malloc_unlock(). Даже без RTOS, как минимум два контекста есть: ISR и пользовательский. На большом проекте может что-то просочиться и попытаться сделать malloc() из ISR.

Поправка, на самом деле все немного не так, но это не очень существенно:


Спойлер

до main'a при каждом вызове конструктора статического или глобального объекта, через длинную цепочку вызывается __aeabi_atexit, который должен "зарегистрировать" этот объект для удаления после вызова main'a (когда вызывается _sys_exit).


Я в очередной раз повелся на название "atexit", но не суть, работать это работает.

Гм, не совсем понял — а почему в связи с этим объекты создаются не на куче? Ну не происходит регистрация, что ж с того функции выделения памяти?
Так объекты статические, память под них уже выделена. На godbolt проще просмотреть: хорошо видно, что, собственно, конструирование объектов ничего на куче не выделяет, а зато регистрирует деструктор через вызов __aeabi_atexit, чтобы он вызвался при выходе из программы… и вот уже __aeabi_atexit — выделяет память.

Соответственно если вместо __aeabi_atexit у вас «пустышка» — то память никто не аллоцирует и куча не нужна.
Так объекты статические, память под них уже выделена


Так вот я потому и не пойму, при чем тут вообще куча, если речь выше про глобальные переменные.

и вот уже __aeabi_atexit — выделяет память


Ну не под объект же?

UPD: Кажется ниже пояснили…

Да, речь тут только про вспомогательную память на куче, которая требуется для деструкторов статичских объектов. В куче создается только список каких-то записей про объекты, а не они сами.

А, теперь кажется понял. Да, для корректного удаления объектов нужен список того, что удаляем. И он создаваться, что логично, должен на куче. И если __aeabi_atexit фактически пустая, то и список создаваться не будет. Но вот что непонятно — неужели этот список настолько большой, что автор статьи в него уперся?
У меня довольно большой проект, не зря под него был выбран камень со 256 килобайтами ОЗУ. Плюс, возможно, объем кучи, который я оставил на момент возникновения ошибки, был совсем маленьким: это сейчас под нее отведен какой-то вполне значительный кусок, а какой был тогда — мне лень искать :)

Тут чуть более полное описание способов решения проблемы.

Собственно да, использование динамического выделения памяти не хорошо влияет на надежность программы. Можно взамен использовать плейсмент нью с выравниванием.
Кстати для выравнивания можно использовать std::aligned_storage Вот тут пример использования я накидал:
Синглтон создающий объекты в RAM и RОМ
Этот код-то использует С++14, а в Кейле поддерживается только С++11, и то лишь частично. Среда подрезает крылья :)

Вообще, если хочется поиграться, то начиная с Кейла 5.23 примерно можно выбрать другой компилятор — armclang, форк clang'a, — который вполне умеет в С++14. И стандартная библиотека к нему идет нормальная, а не от С++03.


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

Это который там называется в опциях проекта «Compiler V6.xx.x»? Да, я тоже думал попробовать на него перейти, но он же при попытке компиляции текущего проекта выкидывает нечеловеческое количество ошибок. Бесспорно, большинство из них можно было бы быстро починить, но некоторые наверняка займут время, да и страшно пускать в релиз код с таким кардинальным изменением. Так что это разве что в каком-то следующем проекте можно попробовать сразу заложиться на новую версию, а текущий пусть уж живет на 5-й. Но за напоминание спасибо, я периодически забываю об этой возможности.

Ага, он самый.
Большинство страшных ошибок из-за несовместимости опций со старым компилятором, тут лучше создавать пустой проект заново.

Собственно да, использование динамического выделения памяти не хорошо влияет на надежность программы


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

Можно взамен использовать плейсмент нью с выравниванием


Вот тут не понял. Как new () принципиально облегчает описанную ситуацию и делает создание объектов более безопасным?
Не просто new(), а плейсмент нью — когда память уже выделена, а осталось только на ней конструктор вызвать.
Я понял, о чем речь, просто вместо placement new написал new () — как в реале оно и пишется (в скобках указывается ссылка на выделенную память, ну или указатель).
Можно взамен использовать плейсмент нью с выравниванием
Вот тут не понял. Как new () принципиально облегчает описанную ситуацию и делает создание объектов более безопасным?
Примерно так.

Вы в других местах описываете extern Foo foo;, а в нужном месте отводите под него память и насильно заставляете компилятор вызвать конструктор.

Хотя строго говоря это нарушение ODR и есть вероятность получить приключения при использовании LTO, так что если уж хотеть быть «святее папы римского», то вот так.

Хотя если переносимость не нужна, то можно сущности без необходимости и не умножать.
Я в курсе, зачем нужен placement new (буду теперь всегда тут так писать, ок). Мой вопрос был именно в том, почему вдруг это становится vice versa к «динамическому выделению».

А вот ваш второй пример (спасибо за него) я не совсем понял. Зачем нужна строка 12? Мне показалось, что Foo& foo впоследствии не использовано.
Зачем нужна строка 12? Мне показалось, что Foo& foo впоследствии не использовано.
Рука-лицо. Да, здесь не использовано, конечно — но зато вся остальная программа может обращаться к foo, считая, что это просто глобальный объект.

Компилятор, видя, что foo ссылается на foo_placeholder просто заменит все ссылки на foo_placeholder во всех местах — а вам не нужно будет городить никакого синтаксического сахара и вообще помнить о том, что объекты у вас — это не просто глобалы, а что они размещены через placement new…

Как-то примерно так (сравните код функций Bar и Baz). На x86-64 лучше видно, что ссылка извелась, но и на ARM — это тоже происходит…

P.S. Вообще про этот трюк мне рассказали знакомые из Гугла — правда он там для других целей используется: в многопоточной программе бывает очень больно, когда main успешно завершился и основной поток все глобалы поудалял, а какие-то другие потоки к ним всё ещё обращаются. Схема «placeholder, placement new, да плюс ссылка» позволяет перевести в программе все глобалы на эту схему изменив только заголовок — что очень полезно, когда у вас программа состоит из сотен тысяч файлов… понятно, что для удобства там к этой схеме ещё пару макросов добавили — но тут, я думаю, вы и сами справитесь… меня просто позабавило, что трюки от «сурового HighLoad'а», где бинарники имеют вес в сотни мегабайт, а объёмы памяти меряются в десятках гигабайт — применимы, оказывается, и в микроконтроллерах…
Рука-лицо. Да, здесь не использовано, конечно — но зато вся остальная программа может обращаться к foo, считая, что это просто глобальный объект.


А, ну это да. Просто это к самой инициализации не относится, вот я и спросил.
И вообще — не будьте строги к идиотам. :)

Компилятор, видя, что foo ссылается на foo_placeholder просто заменит все ссылки на foo_placeholder во всех местах


Да, так и есть, спасибо за очередной пример.

вам не нужно будет городить никакого синтаксического сахара и вообще помнить о том, что объекты у вас — это не просто глобалы, а что они размещены через placement new


Зато имеет место дополнительный код при объявлении и создании объекта. Впрочем это все попросту неизбежно, если нужно создать объект в конкретном хранилище.

Это же placement new, вы сами размещаете обьпкты в выделенной заранее вами памяти. И в данном случае куча не используется, а выделять память вы можете точно мод размер обьекта.

Дык еще раз — почему в этом случае что-либо вдруг становится более безопасным itself? Память-то вы все равно выделять будете динамически. Вы же против динамического выделения как такового высказывались.
не будет она выделять динамически, она уже выделена и выделяете её вы сами, а не малок, она зарезервирована на этапе компиляции. А new только разместит там ваш объект.
static typename std::aligned_storage<sizeof(T),alignof(T)>::type placeholder; //выделили память статически (не на куче), заранее, размером с объект типа T с выравниванием.  В принципе можно вообще массивом выделить, типа static std::array<t_unit8, sizeof(T)> placeholder, но это будет неправильно для 32 и 16 битных микро из-за выравнивания.
... 
//а затем где-то в коде разместили там объект
 new (&placeholder) T;

Понятно, произошло недопонимание. Видимо вы сразу (с первого сообщения) имели в виду, что выделяете статическое хранилище и дальше через placement new на нем размещаете объекты, а я почему-то подумал, что вы предлагает замаллочить память, а потом передать ее в placement new (что как бы непонятно чем лучше в контексте борьбы с динамическим выделением). Я там выше специально malloc упомянул для уточнения, но никто не возразил, а то вопрос был бы снят сразу…
Да ошибки в либах это жесть. Буквально на днях решил заапдейтить свой старый проект на MSP430 (еще с тех времен как LaunchPad почти на шару раздавали) написанный в среде Energia — это форк ардуины под MSP430.
Естественно, обновился до последней версии этой самой Energia. И началось:
1. Контроллер стартует с задержкой 30 сек — оказывается они припилили автоинициализацию часового кварца (зочем!!!, если на плате с завода он не распаян).
2. Тактовая частота в 4 раза меньше — нашел ответ на форуме, что это поломка каким-то коммитом, пропатчил либу.
3. Не работает UART — опять косяк в либе.
4. Не работает I2C — за пару дней обсуждений таки подсказали патч.
Но, вы итоге, я все равно вернулся на старую версию, ибо после всех патчей, режимы сна начали криво работать и я забил.
Я конечно благодарен форумчанам выручили, но блин как это достает особенно если за 4 года архитектуру этих МК почти забыл и понятия не имеешь даже куда соваться.
Так много людей ратуют за опенсурс, но мало кто понимаем сколько труда надо вложить, что бы от релиза к релизу не только добавлять фишки, но и просто ничего не сломать.
Теоретически такие вещи должны бы, хоть частично, предотвращаться тестами перед коммитом. Но как реализовать тесты в части работы с железом? Это же придется, в конце концов, целый программный имитатор камня создать, необъятная работа. Я бы с великим интересом прочитал материал на эту тему, если у кого-нибудь есть опыт решения этой задачи.

Ну в статьях про мобильную разработку мелькали фотки, что у них там отдельно стенд с десятками смартов, для тестирования. Касательно же моего случая то поддерживаемых прогой плат не более 1.5 десятка. Плюс в случае МК можно написать не очень большую прогу которая бы тестировала хоть бы частично функционал (например не все пины АЦП а часть), и сделать соответствующий шилд с известными значениями напряжения, i2c датчиком и тд. Но это все время и деньги, а там похоже все на энтузиастах держится.
Поэтому по возможности я стараюсь не использовать ардуино.

Так чей рантайм оказался с кривым маллоком?

Keil. Не скажу сейчас, какой точно версии, но мажорная версия 5.

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


http://www.keil.com/support/man/docs/armlib/armlib_chr1358938928135.htm

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

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

А по поводу выделения памяти, не надо использовать malloc(), вот просто не надо и всё, про это написано в MISRA C/C++, и не стоит этим пренебрегать, даже если вас не ставят в рамки этих правил, они не просто так писались.
В идеале-то да, но откуда рантайму заранее узнать, к скольким локальным статикам я получу доступ в данном запуске программы? Вот ему и приходится выделять себе память под список через malloc, по мере того, как мой код запрашивает тот или иной статик. И я на этот процесс никак напрямую повлиять не могу. Зато вон, выше по комментариям даны отличные примеры обхода этой засады. Аж хочется попробовать у себя.
Sign up to leave a comment.

Articles