Pull to refresh

Comments 28

Странновато, что 8к оказалось впритык под такое, но, глядя на характеристики данного чипа, упоминающие 16к RAM, напрашивается другой классический путь - первичный загрузчик из ROM принимает более объёмистый вторичный загрузчик в RAM, прыгает в него, а тот уже творит что хочет. Дополнительные плюсы: можно тем же способом загружать и гонять любые сервисные вещи - тестер платы, «читальщик» flash для диагностики сбоев итд. Для уменьшения объёма вторичного загрузчика можно даже переиспользовать фкнкции из ROM (код вашего протокола, к примеру).

Спасибо за идею. Я бы до такого никогда не додумался.

первичный загрузчик из ROM принимает более объёмистый вторичный загрузчик в RAM, прыгает в него, а тот уже творит что хочет.

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

странновато, что 8к оказалось впритык под такое

Вся абсурдность ситуации в том, что мы в 2025 году пытаемся собрать прошивку в 8kByte EEPROM. В Европе такое в последний раз было только в 1982 году. Получается отставание на 43 года!.

Ну вы это бросьте, как будто в Европе не используют микроконтроллеры с минимальным объемом flash и не пишут для них загрузчики. Скажу, что видел загрузчик для STM32L0 по функционалу аналогичный вашему, разве что находится во внутренней flash, и прыгает в конце исполнения по другому адресу во внутренней флэш. Так вот, этот загрузчик умещается в 4 кБ

Сейчас как раз таки в тренде супер дешевые контроллеры с малым количеством памяти. Видимо есть под них ниши. Помимо огромного количества китайцев в эту стезю пошли STM с STM32C0, TI с MSPM0C1104, Silabs с EFM8BB5.

Правда в отличие от Амура все это стоит 0.05...0.20 $ :)

Их ниша- это умные снаряды для мортир .

В Xmega был загрузчик 8К. Я в такой утрамбовывал работу с Wiznet 5300. Так что и 10-15 лет назад таким тоже занимались.

А разве запись вторичного загрузчика в ram не перетрет глобальные переменные первичного загрузчика?

Если "в лоб" - да, перетрёт, нужно спланировать размещение и того и другого (в linker script). Учтите, что работа загрузчиков разделена во времени и в общем случае вторичный спокойно может при старте затереть секции данных первичного своими неинициализированными данными (.bss)/стеком и вообще не вспоминать, что до него исполнялось что-то ещё.

В случае же с переиспользованием (вызовом) функций первичного из вторичного какие-то данные первичного (относящиеся к вызываемым функциям) нужно будет оставить нетронутыми, и тут несколько вариантов:

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

  • вынести переиспользуемые данные в отдельные секции (в linker script можно указать размещение таких-то секций из таких-то файлов в такие-то области, либо самим переменным указать атрибут section("name") (см. gcc section attribute)) и оставить нетронутыми только их

  • в функциях первичного, требующих использования глобальных данных, не ссылаться на те жёстко, а передавать в них указатель на "контекст", который во время работы вторичного загрузчика сможет находиться уже в другом месте (но его нужно будет инициализировать).

А что если:

1--в первичном загрузчике выделить статический массив 10k-15k Byte в RAM памяти.
2--Записывать в этот массив вторичный загрузчик
3--и дать команду прыгнуть исполнять код из массива
?

Тогда и в linker файле не надо будет изменения делать.

Здесь проблема в том, что вторичный загрузчик собирается под некий начальный адрес. Т.е. нужно будет либо выяснять, скажем, из .map файла первичного загрузчика адрес этого массива и выставлять в linker script вторичного, либо собирать вторичный как position-independent executable, что увеличит размер (ну и всё равно потребует некоторых настроек).

"Подкрутить" linker script на самом деле просто - находите в нём объявление доступных диапазонов памяти memory { ... }, внутри находите объявление RAM с началом/длиной, в первичном загрузчике, к примеру, уменьшаете длину до 1к (вообще нужно посмотреть его .map файл, понять, сколько +- ему нужно и округлить в большую сторону), во вторичном прибавляете те же самые 1к к начальному адресу и вычитаете из длины - всё, первичный ограничен в первом кб RAM, у вторичного тот же самый первый кб выведен из доступных, они больше не пересекаются.

Первичный загрузчик:

MEMORY
{
RAM (rwx) : ORIGIN = 0x02000000, LENGTH = 0x400 /* первые 1К */
}

Вторичный:

MEMORY
{
RAM (rwx) : ORIGIN = 0x02000400, LENGTH = 0x3C00 /* оставшиеся 15К */
}

Исходный код EEPROM загрузчика (по NDA)

Ну хоть не ссылка на телеграмм канал

Terminal v1.9b. Да вы, батенька, из нас, из староверов! Похвально!

А какая ещё существует утилита, чтобы отправлять бинарные hex массивы и при этом позволять выставлять паузы между байтами?

Moxa COM-port toolkit. Единственно - не смотрел насчёт пауз. Но, если имеются в виду паузы для переключения направления 485, - то это драйвер порта, а не прикладное ПО.

У Terminal v1.9b аналогов нет.

coolterm - вот эта мне больше, чем Terminal v1.9b нравится

В программе Terminal надо установить паузу между символами. Отладка производилась на значениях в диапазоне 20ms-50ms между байтами. Иначе прошивка может захлебнуться от плотного пучка входных байтов.

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

У STM32 с libopencm3 в 8кБ флеша можно втиснуть USB MSC загрузчик. Причём штатно, без магии и прямой работы с регистрами.

Что -то верится с трудом.

Вполне реально, на самом деле, ценой меньшей переносимости.

Насколько вижу по https://github.com/aabzel/Artifacts/blob/main/start_mik32_v1_eeprom_bootloader_m/start_mik32_v1_eeprom_bootloader_m.elf, у вас высокие требования к переносимости кода, всё, что можно, абстрагировано в драйверы с интерфейсами в виде структур. Это в общем случае здорово (если приходится менять чипы итд), но ощутимо ограничивает компилятор в оптимизациях. Простой пример: ваш загрузчик общается через единственный и жёстко заданный UART, но адрес данного UART заносится в структуру UartHandle_t во время исполнения (в uart_mcal_init), оптимизатор не может докопаться до факта, что UART - один и тот же такой-то, в результате совсем низкоуровневая функция отправки массива в UART вынуждена постоянно жонглировать полями структуры (она вообще скомпилировалась так, что готова даже к изменению адреса UART посреди отправки массива). Остальной код выглядит примерно так же - львиная доля объёма (и времени выполнения!) приходится на манипуляции с полями структур драйверов, а не сам функционал загрузчика. Это, в целом, вопрос стиля кода, данный уровень абстракции вполне достижим и с учётом интересов оптимизатора (помнится, на Хабре был цикл статей по программированию микроконтроллеров на С++ с грамотным использованием шаблонов, позволявшим компилятору "разматывать" зависимости до удивительных глубин).

Но если вам нужно как-то уместить туда CRC8 и забыть, достаточно выкинуть оптимизированные на скорость в сильный ущерб объёму библиотечные memcpy и memset, заменив их своими побайтовыми версиями, и простой CRC8 (без таблицы, побитовый цикл с XOR с полиномом) уж точно влезет в освободившееся место.

Вдогонку: вообще странно, что оптимизатор не смог установить факт незименности UART. Доводилось смотреть бинарники на основе STM32 HAL, где вовсю практикуется подобный подход (структуры с контекстами периферии), и там оптимизатор распутывал вызовы наподобие HAL_GPIO_WritePin(&Pin123Handle, 0) вообще до замены вызова на inline запись нужного значения в нужный регистр.

Сам UartInstance, часом, не как volatile объявлен? (вот этой информации в elf не сохраняется, не вижу). Если нет, можно попробовать перенести инициализацию базового адреса UART из uart_mcal_init (node->UARTx=&UART0) непосредственно в статический инициализатор UartHandle_t UartInstance = { ..., .UARTx=&UART0, ... } (вижу, что он у вас присутствует, некоторые поля UartInstance инициализированы статически). Для пущего эффекта можно даже полю UARTx навесить const.

Чем Ваше решение принципиально лучше вот этого?
https://gitflic.ru/project/elron-tech/elbear_fw_bootloader

Оно же используется с небольшими доработками и для платы СТАРТ.
Не сказать, чтобы там всё волшебно, но оно работает, делает вроде бы всё то же самое и занимает около 3-4 кБ в контроллере.

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

Не получится. Это аппаратное разделение памяти программ и данных.

В каких-то девайсах код из ОЗУ не может выполняться. Тут может,

но в теории должен выполняется медленее чем из ROM.

Иначе такая арихтектура вообще не имеет смысла.

Эх вспомнились времена, когда я писал свой загрузчик для Амура года полтора назад. Когда информации было мало и редкий народ на форуме разработчиков пытался писать костыли для этого процессора. Одним из них была именно проблема с прошивкой записанной в флеш память. Программа там работала с подвисаниями (из-за чего например уарт пропускал символы,...), т.к. флеш по умолчанию никак не инициализировалась. Придумали простой загрузчик в ЕЕпром который инициировал флеш в зависимости от типа памяти и прыгал на адрес загрузки из флеша. А потом уже и с загрузчиком по уарт сделали. Думал эта тема уже давно пройдена, а нет оказывается. Фирма elbear вроде как одна из первых официальный загрузчик по уарту написала. Там у них и терминал толи на базе питона или сишника с открытым кодом был.

Проверка CRC8 отключена так, как код вычисления CRC8 не поместился в EEPROM память

Таблицу можно вычислить динамически. В особо тяжелом случае вместо crc можно сумму байт считать.

В целом в бинарнике слишком много нолей. Вангую - глобальные переменные инициализируются так:

typedef struct {
  u32 id;
  u8 buffer[32];
} MegaController_t;

MegaController_t MegaControllers[2] = {
  {
    .id = 0xdeadbeef
  },
  {
    .id = 0xc0febabe
  }
};

В итоге в rodata попадает бесполезная информация (начальное значение buffer из массы нолей). Вместо этого можно руками инициализировать только то, что нужно:

MegaController_t MegaControllers[2];

void main() {
  MegaControllers[0].id = 0xdeadbeef;
  MegaControllers[1].id = 0xc0febabe;
}
Sign up to leave a comment.

Articles