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 массивы и при этом позволять выставлять паузы между байтами?
У 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 с полиномом) уж точно влезет в освободившееся место.
https://kevincuzner.com/2018/06/28/building-a-usb-bootloader-for-an-stm32/ - 8кБ
https://github.com/sfyip/STM32F103_MSD_BOOTLOADER - 13кБ
у rp2040 bootrom 16кБ
а если по вот этому основательно напильником пройтись
https://github.com/adafruit/tinyuf2
упихать думаю возможно
Вдогонку: вообще странно, что оптимизатор не смог установить факт незименности 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;
}
EEPROM Загрузчик для MIK32 (K1948BK018)