Маскирующиеся баги в эмбедде

Затыки неизбежны при разработке любого ПО. В эмбедде свои щедрые пять копеек могут подкинуть еще и аппаратные проблемы, но то отдельная песня. А вот чисто программные засады, когда застреваешь на, вроде бы, пустом месте… Для меня их три вида.

Проще всего, когда не до конца понят мануал, стандарт или, скажем, порядок конфигурирования библиотеки под железо. Здесь ясно: не все ходы исчерпаны, терпение и труд, еще пяток-другой экспериментов и оно оживет. Осциллограф и научный тык в помощь.


Выбор делителя частоты для настройки шины CAN

Хуже, когда проблема — в опечатке или ошибке в логике, которую не видишь в упор, пока по этому месту двадцать раз не пройдешься и глазами, и в пошаговой отладке. Потом осеняет, звонкий удар по лбу, крик «Ну екарный ты ж бабай!», правка. Работает.

И мрачный третий вид: глюк, окопавшийся в чужой библиотеке и вылезающий на стыке с железом. Шекспировские страсти рождает под ровный свет монитора. «Да ведь не может, не мо-жет система так себя вести, потому что не может никогда! Ну правда же! А?!» Не-а. Получите, распишитесь.

В итоге реальность оказывается ширше ширее шире ожидаемого. Пара примеров:

История №1. MicroSD-флэшка и работа по DMA


Анамнез


Нужно сбрасывать данные в файл на SD-карту. Конечно, самостоятельно писать файловую систему и драйвер SDIO нет ни времени, ни желания, поэтому беру готовую библиотеку. Настраиваю под железо, и все отлично работает. Поначалу. А потом выясняется, что данные записаны диковато: объемы точные, но в самих файлах отдельные пары-тройки байт то дублируются, то пропадают, без какой-либо закономерности. Нехорошо!

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

На раскопку собаки ушла где-то пара дней.

Диагноз


Проблема оказалась во взаимодействии библиотеки с аппаратурой DMA.

У SD-карточек есть особенность: они пишутся только блоками по 512 байт. Для этого библиотека буферизует данные в 512-байтный массив, и по его заполнении сбрасывает оттуда через DMA на флэш. Но!

Если я передаю на запись фрагмент, больший по размеру, чем <512хN+пустое место в буфере библиотеки> байт, то библиотека (очевидно, чтобы не гонять лишний раз память туда-сюда), делает так: дозаполняет свой буфер, пишет его на флэш, и следующие 512хN байт кидает в DMA прямиком из моего буфера! Ну и, если что-то осталось недописанное — снова копирует в свой, до следующего раза.

И все бы ничего, но контроллер DMA требует, чтобы данные были размещены в памяти с выравниванием по 4-байтной границе. Библиотечный-то буфер всегда так выровнен, язык это гарантирует. А вот с какого адреса, после копирования части данных, начинаются те оставшиеся 512xN с небольшим байт у меня — бог весть. И библиотека это никак не проверяет: адрес, как есть, передается контроллеру DMA.

«Корявое что-то прислали… Пес с ним.» Контроллер молча обнуляет младшие 2 бита переданного адреса. И запускает передачу.


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

Лечение


Пришлось добавить еще один буфер непосредственно перед вызовом аппаратной функции записи. Если передаваемый на запись адрес не кратен 4, данные сначала копируются в него. Заодно возросла средняя скорость за счет обоснованного выбора размера буфера. Конечно, на это ушла память, но что такое 4 килобайта на благое дело, когда у тебя в распоряжении — необозримые 192!

История №2. Рантайм и куча


Пролог


После очередного изменения программа начала падать, и падать как-то очень жестко, выкидывая процессор в обработчик Hard Fault-а. Причем выкидывало его туда сразу после старта, еще до того, как выполнение добиралось до main(), то есть ни строчки моего кода выполниться не успевало.

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

Глава 1


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

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

Продолжение анализа показало, что глюк — следствие попытки инициализации локальных статических переменных.

Лирическое отступление
Кстати, рассматривая дизассемблированный код, я попутно узнал ответ на вопрос, который иногда задавал себе, но был слишком ленив, чтобы сразу погуглить: как разруливается ситуация, когда такую переменную могут попытаться одновременно инициализировать 2 потока и более. Оказалось, что в этом случае компилятор обставляет инициализацию семафорами, гарантирующими, что только один поток за раз пройдет процедуру целиком, а остальные дождутся, пока закончит первый. Такое поведение стандартизировано, начиная с С++11. Вы знали? Я — нет.

Глава 2


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

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



Изменения в новом коде прошивки сломали malloc?! Но как это вообще возможно? Я ведь malloc точно не переопределял, она мне самому и не нужна нигде!

Полез в опции компилятора, искать какие-то ключевые слова, справки, но везде было сказано четко: malloc() гарантирует выдачу памяти, выровненной по фундаментальной границе. Либо null pointer в случае, если памяти недостаточно.

Глава 3


Долго я бессмысленно втыкал в код, ставил брекпойнты, страдал и ничего не понимал, пока в какой-то момент не торкнуло и я не поглядел на возвращаемые malloc-ом адреса внимательнее. До этого весь анализ состоял в том, чтобы посмотреть, кратна ли последняя цифра адреса 0x4. А теперь стал сравнивать целиком между собой адреса, выдаваемые последовательными вызовами malloc-а.

И, о, чудо!

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

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

Эпилог


Увеличил в настроечном файле размер кучи, и все починилось.

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

Век живи — век учись.
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

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

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

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

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

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

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


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

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

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


            int __aeabi_atexit(void)
            {
                return 1;
            }

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

              +2

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

                0
                Это просто прелестно. Спасибо!
                  +1

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


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


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

              +1

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


              Спойлер

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


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

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

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


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

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


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

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

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

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

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

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

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


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

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

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

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


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

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


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

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

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

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

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

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

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

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


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

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


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

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


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

                            0

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

                              0
                              Дык еще раз — почему в этом случае что-либо вдруг становится более безопасным itself? Память-то вы все равно выделять будете динамически. Вы же против динамического выделения как такового высказывались.
                                0
                                не будет она выделять динамически, она уже выделена и выделяете её вы сами, а не малок, она зарезервирована на этапе компиляции. А 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;
                                

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

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

                            0

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

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

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


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

                              0

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

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

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

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

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