Файлы прошивки: ELF, Intel HEX, S-record, BIN и UF2
Компоновщик выдаёт ELF, из него программа objcopy делает Intel HEX, S-record или BIN, а UF2 копируют на плату как на флешку. Что лежит внутри каждого формата, где в нём адреса, как по выводу size и сообщению Arduino IDE оценить занятую флеш-память и ОЗУ и какой файл нужен avrdude, esptool, STM32CubeProgrammer и picotool.
Обновлено Редакция GAW
Файл прошивки — это содержимое памяти микроконтроллера, упакованное для передачи программатору или загрузчику: какие байты и по каким адресам записать. Компоновщик выдаёт файл ELF, где кроме кода есть таблица секций с адресами, символы и отладочная информация. Программа objcopy вынимает из него только то, что нужно записать в память, и сохраняет в одном из форматов для прошивки: Intel HEX, Motorola S-record или двоичный образ BIN. Особняком стоит UF2, формат для загрузчиков, которые подключаются к компьютеру как флешка.
Коротко
- ELF — выход компоновщика: секции с адресами, символы, отладочная информация. Файл начинается с байтов
7F 45 4C 46, то есть 0x7F и буквы «ELF». - Intel HEX и S-record — текст: в каждой строке адрес, данные и контрольная сумма, каждый байт записан двумя шестнадцатеричными цифрами.
- BIN — только байты, без адресов и проверок: адрес начала сообщают программатору, промежутки между областями заполнены.
- UF2 — блоки по 512 байт; в каждом адрес, до 476 байт данных (обычно 256) и необязательный идентификатор семейства микроконтроллера.
- По выводу size флеш-память можно оценить как text + data, статически занятое ОЗУ — как data + bss: секция .data занимает место и там, и там. Это приближение, size считает по флагам секций.
Чем отличаются форматы
| Формат | Вид | Адреса | Проверка целостности | Символы и отладка | Откуда берётся |
|---|---|---|---|---|---|
| ELF (.elf, .axf, .out) | двоичный | VMA у секций (sh_addr), LMA у сегментов (p_paddr) | нет | есть | компоновщик |
| Intel HEX (.hex) | текст | 16 бит в записи, база задаётся записями 02 и 04 | сумма в каждой строке | нет | objcopy -O ihex |
| S-record (.srec, .s19) | текст | 16, 24 или 32 бита в каждой записи | сумма в каждой строке | нет | objcopy -O srec |
| BIN (.bin) | двоичный | нет | нет | нет | objcopy -O binary |
| образ ESP (.bin) | двоичный, со своим заголовком | адреса загрузки сегментов в заголовке; смещение во флеш-памяти задают при записи | контрольная сумма и SHA-256 | нет | esptool elf2image |
| UF2 (.uf2) | двоичный, блоки по 512 байт | в каждом блоке | данных не проверяет; магические числа отсекают чужие и неполные блоки, MD5 области по желанию | нет | picotool uf2 convert, esptool merge-bin --format uf2 |
Практическое правило простое. ELF оставляют для отладчика и для программ, которые его читают. Intel HEX подходит для обмена: адреса и контрольные суммы делают его самодостаточным, его читают avrdude, STM32CubeProgrammer и esptool, но не picotool и не загрузчики UF2. BIN нужен там, где программа ждёт точную копию памяти, и тогда адрес начала знает тот, кто прошивает. UF2 годится только для загрузчиков, которые его понимают: ПЗУ RP2040 и RP2350, загрузчики на основе TinyUF2 и другие из списка в спецификации.
ELF: что выдаёт компоновщик
ELF (Executable and Linking Format) описан спецификацией TIS 1.2 1995 года и главой 4 System V ABI. В начале файла заголовок: сигнатура 7F 45 4C 46, разрядность и порядок байтов, архитектура в поле e_machine (40 — ARM, 83 — AVR) и точка входа e_entry. Сам микроконтроллер после сброса начинает не с неё, а с адреса из вектора сброса (как это устроено, разобрано на страницах о флеш-памяти и загрузчике STM32 и о программировании AVR). Точка входа нужна инструментам: GDB после команды load ставит на неё счётчик команд, а objcopy переносит её в стартовую запись HEX. Дальше в файле идут секции и две таблицы: таблица секций для компоновщика и отладчика и таблица сегментов (заголовок программы) для загрузчика.
Секции
| Секция | Тип и флаги в спецификации | Что содержит | Где оказывается в микроконтроллере |
|---|---|---|---|
| .text | PROGBITS, ALLOC + EXECINSTR | машинный код | флеш-память |
| .rodata | PROGBITS, ALLOC | константы, строки, таблицы | флеш-память; у классических AVR без PROGMEM — в ОЗУ вместе с .data |
| .data | PROGBITS, ALLOC + WRITE | начальные значения инициализированных переменных | хранится во флеш-памяти, работает в ОЗУ |
| .bss | NOBITS, ALLOC + WRITE | переменные без начального значения или с нулевым; в файле места не занимает, при старте обнуляется | только ОЗУ |
| .symtab, .strtab | SYMTAB, STRTAB | имена функций и переменных, их адреса и размеры | никуда, остаётся в файле |
| .debug…, .line | PROGBITS, без флагов | отладочная информация: типы, соответствие адресов строкам исходника | никуда, остаётся в файле |
Флаг ALLOC означает, что секция занимает память при выполнении программы; секции без него, то есть символы и отладочная информация, в образ для прошивки обычно не попадают. Компиляторы создают и много других секций: таблицу векторов прерываний, .init_array, .noinit, .eeprom у AVR. Правило для них то же: важны флаги и адреса, а не имя.
Почему у AVR константы оказываются в ОЗУ: команды LD классических AVR не читают флеш-память, поэтому стандартный скрипт компоновщика из binutils для архитектуры avr5 (к ней относится ATmega328P) включает .rodata в выходную секцию .data. Константы копируются в ОЗУ при старте, как переменные, и остаются только во флеш-памяти, если объявить их с PROGMEM. У AVR, которые видят флеш-память в адресном пространстве данных (архитектура avrxmega3), и у ARM .rodata остаётся во флеш-памяти.
Таблица сегментов описывает то же самое для загрузки. У сегмента типа PT_LOAD есть смещение в файле, адреса p_vaddr и p_paddr, размер в файле p_filesz и размер в памяти p_memsz. Если размер в памяти больше, недостающие байты по спецификации заполняются нулями: так описывается .bss, которой в файле нет. У самой секции в ELF только один адрес, sh_addr, и это адрес выполнения. Адрес загрузки хранится в сегменте: GNU ld записывает в p_paddr адрес загрузки первой секции сегмента, а библиотека BFD из binutils, на которой работают objcopy и objdump, при чтении ELF вычисляет адрес загрузки каждой секции обратно по p_paddr.
Как это выглядит в реальном файле, видно по листингу загрузчика optiboot для ATmega328P из ядра Arduino AVR. Это вывод avr-objdump -h (выдержка), где столбец LMA вычислен по сегментам:
Idx Name Size VMA LMA File off Algn
0 .text 000001f4 00007e00 00007e00 00000054 2**1
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .version 00000002 00007ffe 00007ffe 00000248 2**0
CONTENTS, READONLY
4 .debug_info 000002a8 00000000 00000000 000002d1 2**0
CONTENTS, READONLY, DEBUGGING
6 .debug_line 00000488 00000000 00000000 000006f1 2**0
CONTENTS, READONLY, DEBUGGING
Код загрузчика занимает 0x1F4 = 500 байт с адреса 0x7E00, то есть последние 512 байт 32-килобайтной флеш-памяти, плюс 2 байта номера версии по адресу 0x7FFE. У секции .version нет флагов ALLOC и LOAD, поэтому Makefile загрузчика перечисляет нужные секции явно и добавляет ей эти флаги: avr-objcopy -j .text -j .data -j .version --set-section-flags .version=alloc,load -O ihex. В HEX-файл попадают только эти 502 байта. Девять отладочных секций вместе занимают 3662 байта, в семь раз больше кода, и в микроконтроллер не попадает ни одна из них.
VMA и LMA: адрес выполнения и адрес загрузки
У каждой загружаемой секции два адреса. VMA (virtual memory address) показывает, где секция находится, когда программа работает. LMA (load memory address) показывает, куда её записывают при прошивке. Для кода и констант они обычно совпадают. Для .data нет: начальные значения переменных должны пережить выключение питания, поэтому лежат во флеш-памяти (LMA), а работают переменные в ОЗУ (VMA). Копирует их код запуска до вызова main(), он же обнуляет .bss.
В скрипте компоновщика GNU ld это записывается так (условный пример для Cortex-M):
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 64K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}
SECTIONS
{
.text : { KEEP(*(.isr_vector)) *(.text*) *(.rodata*) } >FLASH
.data :
{
. = ALIGN(4);
_sdata = .;
*(.data*)
. = ALIGN(4);
_edata = .;
} >RAM AT>FLASH
_sidata = LOADADDR(.data);
.bss :
{
. = ALIGN(4);
_sbss = .;
*(.bss*) *(COMMON)
. = ALIGN(4);
_ebss = .;
} >RAM
}
>RAM задаёт VMA, AT>FLASH задаёт LMA, а LOADADDR(.data) возвращает адрес загрузки секции. Выравнивание . = ALIGN(4) на границах нужно потому, что код запуска ниже копирует и обнуляет память словами по 4 байта. Если секции не помещаются в область памяти, компоновщик остановится с ошибкой. Код запуска использует эти символы:
extern uint32_t _sidata, _sdata, _edata, _sbss, _ebss;
void copy_data_and_clear_bss(void)
{
uint32_t *src = &_sidata;
uint32_t *dst = &_sdata;
while (dst < &_edata) /* initial values: flash (LMA) -> RAM (VMA) */
*dst++ = *src++;
for (dst = &_sbss; dst < &_ebss; )
*dst++ = 0; /* .bss starts as zeros */
}
Файлы HEX, S-record и BIN строятся по LMA: в них .data лежит во флеш-памяти, сразу за кодом. Поэтому начальные значения переменных тоже часть прошивки.
Почему отладчику нужен ELF
В HEX и BIN нет ничего, кроме байтов для памяти: objcopy, создавая двоичный файл, отбрасывает все символы и сведения о перемещениях. Отладчику же нужно знать, где какая функция и переменная и какой строке исходника соответствует адрес, а это таблица символов и секции .debug… в ELF. GDB читает символы из файла программы, а команда load записывает в микроконтроллер загружаемые секции по их адресам загрузки из ELF и заодно загружает таблицу символов. Поэтому для отладки нужен именно .elf, даже если прошивали .hex. Без него можно остановиться только по адресу, например break *0x08000124.
Intel HEX: записи и контрольная сумма
Формат Intel HEX описан в спецификации Intel 1988 года. Каждая строка — запись: двоеточие, длина данных RECLEN (не больше FF), 16-битное смещение адреса, тип записи, данные и контрольная сумма. Всё, кроме двоеточия, — байты, записанные парами шестнадцатеричных цифр.
| Тип | Назначение |
|---|---|
| 00 | данные |
| 01 | конец файла, всегда :00000001FF |
| 02 | база сегмента: значение × 16, для адресов до 1 МБ |
| 03 | стартовый адрес CS:IP |
| 04 | старшие 16 бит линейного адреса, до 4 ГБ |
| 05 | стартовый линейный адрес, 32 бита |
Контрольная сумма — дополнение до двух младшего байта суммы всех байтов записи от длины до последнего байта данных. Проверка обратная: сумма всех байтов вместе с контрольной должна давать 00 в младшем байте.
Пример: разбираем запись вручную
Первая строка того же загрузчика optiboot:
:107E0000112484B714BE81FFF0D085E080938100F7
| Поле | Символы | Значение |
|---|---|---|
| Начало | : | начало записи |
| RECLEN | 10 | 0x10 = 16 байт данных |
| Смещение | 7E00 | первый байт данных ложится по адресу 0x7E00 |
| Тип | 00 | запись данных |
| Данные | 11 24 84 B7 14 BE 81 FF F0 D0 85 E0 80 93 81 00 | 16 байт кода, первая команда eor r1, r1 (байты 11 24) |
| CHKSUM | F7 | контрольная сумма |
Считаем сумму. Заголовок: 10 + 7E + 00 + 00 = 8E. Данные: 11 + 24 + 84 + B7 + 14 + BE + 81 + FF + F0 + D0 + 85 + E0 + 80 + 93 + 81 + 00 = 87B. Всего 8E + 87B = 909, младший байт 09. Дополнение до двух: 100 − 09 = F7, ровно то, что записано в конце строки. Проверка: 909 + F7 = A00, младший байт 00.
В конце того же файла стоит запись :0400000300007E007B со стартовым адресом CS:IP = 0000:7E00: в неё objcopy перенёс точку входа из ELF. Её сумма: 04 + 00 + 00 + 03 + 00 + 00 + 7E + 00 = 85, и 100 − 85 = 7B.
Проверить такие расчёты для целого файла можно в инструменте ниже: вставьте строки HEX или откройте файл .hex, .s19 или .bin, и он разберёт каждую запись, сверит контрольные суммы, построит карту памяти и hex-дамп. Файл ELF или UF2 инструмент узнает по сигнатуре и покажет дамп самого файла. Отдельно инструмент открывается на странице просмотра Intel HEX и S-record, там же подробно разобраны записи 02 и 04.
Исходные данные
Вставьте текст, откройте файл .hex, .s19 или .bin или перетащите его сюда. Файл не покидает браузер.
стёртая флеш-память — FF
Результат
- Размер прошивки
- 24 байта
- данные с 0x0000 по 0x0017, 1 диапазон
- Формат
- Intel HEX, 16-битные адреса3 записи, с данными 2
- Проверка записей
- ошибок нет
- CRC-32 образа
- 0xEF3FD8CA24 байта от 0x0000, промежутки — FF: как у файла .bin
- Сумма байтов
- 0x69 · 0x0E698 и 16 бит, по тому же образу
Записи: 3
- :начало записи
- ■длина
- ■адрес
- ■тип
- ■данные
- ■контрольная сумма
| Строка | Запись | Тип | Адрес или значение | Сумма: в файле / верная |
|---|---|---|---|---|
| 1 | :10000000259A1D9A29E236E940E84A95F1F73A9592 | 00 данные | 0x0000, 16 байт | 92 / 92 |
| 2 | :08001000D9F72A95C1F7F5CFDD | 00 данные | 0x0010, 8 байт | DD / DD |
| 3 | :00000001FF | 01 конец файла | конец файла | FF / FF |
Карта памяти
| Что | Начало | Конец | Размер |
|---|---|---|---|
| данные | 0x0000 | 0x0017 | 24 байта |
| Всего данных | 24 байта | ||
Hex-дамп образа
Слева адрес, затем 16 байтов и их символы ASCII; точка — непечатный байт. Бледные байты — заполнение промежутков, целые строки промежутка свёрнуты.
Преобразовать и скачать
:10000000259A1D9A29E236E940E84A95F1F73A9592
:08001000D9F72A95C1F7F5CFDD
:00000001FFЗаписи как у GNU objcopy: строки CRLF, запись не пересекает границу 64 КБ, база ниже 1 МБ задаётся записью 02, выше — записью 04, стартовый адрес сохраняется.
Motorola S-record
S-record — формат Motorola, описанный в приложении C справочника M68000. Запись начинается с буквы S и цифры типа: S0 — заголовок с произвольным текстом, S1, S2 и S3 — данные с 2-, 3- или 4-байтовым адресом, S5 — число переданных записей данных, S9, S8 и S7 — конец блока S1, S2 или S3 и, по желанию, стартовый адрес. От Intel HEX он отличается двумя вещами: полный адрес записан прямо в каждой записи данных, а контрольная сумма считается как дополнение до единицы младшего байта суммы, а не как дополнение до двух. Поле длины считает адрес, данные и контрольную сумму.
Те же 16 байт загрузчика в виде S1-записи:
S1137E00112484B714BE81FFF0D085E080938100F3
Длина 0x13 = 19: 2 байта адреса, 16 байт данных и 1 байт суммы. Сумма длины, адреса и данных: 13 + 7E + 00 + 87B = 90C, младший байт 0C, его инверсия F3.
objcopy создаёт S-записи ключом -O srec; --srec-forceS3 заставляет писать только S3, а --srec-len ограничивает длину записи.
BIN: только байты
Двоичный образ — точная копия памяти. objcopy с ключом -O binary делает дамп содержимого ELF, начиная с адреса загрузки самой младшей секции; символы и сведения о перемещениях отбрасываются. Если между секциями есть промежутки, они попадают в файл заполненными: байт заполнения задаёт --gap-fill, а --pad-to дополняет образ до нужного адреса.
Раз адресов в файле нет, адрес начала сообщают при прошивке:
- STM32CubeProgrammer:
-w firmware.bin 0x08000000, для двоичного файла адрес обязателен. С 0x08000000 начинается основная флеш-память STM32F1 и STM32F4, подробнее на странице о флеш-памяти и загрузчике STM32. - esptool:
write-flash 0x10000 app.bin, пары «адрес — файл» нужны всегда; полный образ, собранный командой merge-bin, пишут с 0x0. - picotool:
load firmware.bin -o 0x10000000. Адрес по умолчанию и есть 0x10000000: встроенной флеш-памяти у RP2040 нет, а внешнюю он видит через окно XIP размером 16 МБ, которое начинается с этого адреса.
У Espressif есть и свой вариант .bin: образ приложения, который делает esptool elf2image. В нём есть заголовок, сегменты с адресами загрузки, контрольная сумма и SHA-256, но смещение во флеш-памяти, как и для любого .bin, указывают при записи.
UF2: блоки для загрузки копированием
UF2 придуман Microsoft для MakeCode и рассчитан на загрузчик, который изображает USB-флешку. Файл состоит из независимых блоков по 512 байт, а USB-накопитель получает данные кусками, кратными 512 байтам, поэтому каждый блок приходит целиком, в каком бы порядке их ни прислала операционная система. Если страница флеш-памяти не больше 256 байт, загрузчик может записать блок сразу, иначе ему нужен буфер. Все поля, кроме данных, — 32-битные числа little-endian:
| Смещение | Размер | Содержимое |
|---|---|---|
| 0 | 4 | первое магическое число 0x0A324655: символы «UF2» и перевод строки |
| 4 | 4 | второе магическое число 0x9E5D5157 |
| 8 | 4 | флаги |
| 12 | 4 | адрес во флеш-памяти, куда писать данные |
| 16 | 4 | число байтов данных в блоке, часто 256 |
| 20 | 4 | номер блока, с нуля |
| 24 | 4 | всего блоков в файле |
| 28 | 4 | размер файла, идентификатор семейства или 0 |
| 32 | 476 | данные, дополненные нулями |
| 508 | 4 | последнее магическое число 0x0AB16F30 |
Флаги: 0x00000001 — блок не для основной флеш-памяти (например, исходный текст внутри файла), 0x00001000 — контейнер файлов, 0x00002000 — указан идентификатор семейства, 0x00004000 — в конце данных записана MD5 области флеш-памяти, 0x00008000 — есть дополнительные теги. Проверки самих данных в формате нет. Магические числа нужны, чтобы отличить блок UF2 от прочих записей на диск и заметить, что блок записан не целиком, а MD5 позволяет программе прошивки пропустить блоки, которые уже совпадают с содержимым флеш-памяти. Если идентификатор семейства не совпадает с идентификатором чипа, загрузчик пропускает блок целиком. Поэтому несколько прошивок для разных микроконтроллеров можно склеить в один файл: каждый чип запишет только свою.
| Семейство | Идентификатор |
|---|---|
| RP2040 | 0xe48bff56 |
| RP2350, Arm Secure | 0xe48bff59 |
| RP2350, RISC-V | 0xe48bff5a |
| RP2350, Arm Non-secure | 0xe48bff5b |
| RP2xxx, абсолютная загрузка без таблицы разделов | 0xe48bff57 |
| STM32F4 | 0x57755a57 |
| ESP32-S3 | 0xc47e5767 |
Цена удобства — размер: при обычной порции 256 байт данных в 512-байтовом блоке файл UF2 вдвое больше прошивки. esptool merge-bin --format uf2 по умолчанию берёт наибольшую возможную порцию и добавляет в блоки MD5 (ключ --md5-disable её отключает). Как прошивать Raspberry Pi Pico копированием файла, рассказано в термине UF2 и на странице RP2040 и RP2350.
Как получить HEX, S-record и BIN: objcopy
Все форматы для прошивки получают из одного ELF программой objcopy из GNU Binutils. У каждого набора инструментов (тулчейна) своя версия с префиксом:
arm-none-eabi-objcopy -O ihex firmware.elf firmware.hex
arm-none-eabi-objcopy -O srec firmware.elf firmware.srec
arm-none-eabi-objcopy -O binary firmware.elf firmware.bin
Ключ -j оставляет только указанные секции, -R удаляет их. Ядро Arduino AVR делает из скетча два файла: HEX без EEPROM (avr-objcopy -O ihex -R .eeprom) и отдельный .eep только с секцией .eeprom. Секции .eep при этом меняют адрес загрузки на 0 ключом --change-section-lma .eeprom=0, так что адреса в нём отсчитываются от начала EEPROM. Рецепты загрузки ядра записывают только флеш-память (-Uflash:w:…hex:i), поэтому .eep остаётся в папке сборки, и записывают его отдельно, например avrdude … -U eeprom:w:sketch.eep:i.
У Espressif своя утилита: esptool elf2image превращает ELF в образ приложения с заголовком для загрузчика ESP, а esptool merge-bin собирает загрузчик, таблицу разделов и приложение в один файл: BIN с заполнением промежутков байтом 0xFF, Intel HEX без заполнения или UF2. Для RP2040 и RP2350 picotool uf2 convert превращает ELF или BIN в UF2.
Размер прошивки: вывод size
Программа size (avr-size, arm-none-eabi-size) показывает, сколько места займёт программа. По умолчанию она пишет одну строку в формате Berkeley:
text data bss dec hex filename
14512 120 2344 16976 4250 firmware.elf
Секции по столбцам size раскладывает по флагам, а не по памяти: секции без флага ALLOC пропускает, код и данные только для чтения относит к text, остальные секции с содержимым к data, секции без содержимого к bss.
| Столбец | Что в нём | Флеш | ОЗУ |
|---|---|---|---|
| text | код и данные только для чтения | да | нет |
| data | секции с содержимым, доступные для записи: прежде всего начальные значения переменных | обычно да | обычно да |
| bss | секции без содержимого: обнуляемые переменные | нет | да |
| dec, hex | text + data + bss в десятичной и шестнадцатеричной записи | — | — |
Оценка для этой строки и микроконтроллера с 64 КБ флеш-памяти и 20 КБ ОЗУ:
Если скрипт компоновщика не резервирует стек и кучу отдельной секцией, под них остаётся 20 480 − 2464 = 18 016 байт. Число dec = 16 976 ни с чем не сравнивают: в нём data посчитана один раз, а bss во флеш-памяти не хранится. Двоичный образ займёт около 14 632 байт (плюс возможные байты выравнивания между секциями), HEX-файл около 40 КБ: каждая строка из 16 байт данных занимает 45 символов, то есть примерно в 2,8 раза больше. UF2 с порцией 256 байт займёт 58 блоков, 29 696 байт.
Почему формулы приблизительные:
- у AVR секция
.eepromс данными для EEPROM размещается компоновщиком и содержит данные, поэтому попадает в data, хотя её нет ни во флеш-памяти, ни в ОЗУ; .init_arrayи.fini_arrayпо спецификации доступны для записи и считаются в data, даже если скрипт компоновщика кладёт их во флеш-память, как шаблон ST для STM32F1;- секция-резерв без содержимого, например
._user_heap_stackв шаблонах ST, уже входит в bss, и тогда data + bss включает минимальные стек и кучу; - формат GNU (
size -G) относит данные только для чтения к data, и формулы выше к нему не подходят.
Точную картину по секциям даёт size -A. И в любом случае size знает только то, что разместил компоновщик: сколько стека понадобится во время работы, он не покажет.
Что пишет Arduino IDE
Arduino IDE 2 компилирует через arduino-cli, а размер считает по рецепту из platform.txt ядра: вызывает avr-size -A, получает таблицу размеров по секциям и складывает размеры строк, подходящих под регулярные выражения. Для флеш-памяти это секции .text, .data и .bootloader, для ОЗУ — .data, .bss и .noinit, так что .eeprom в расчёт не попадает. Пределы берутся из boards.txt: у Arduino Uno upload.maximum_size = 32 256 байт (512 байт из 32 768 занимает optiboot) и upload.maximum_data_size = 2048 байт. Проценты считаются целочисленным делением, то есть с отбрасыванием дробной части.
Пусть в таблице avr-size -A секция .text занимает 2768 байт, .data — 22, .bss — 184. Тогда флеш-память: 2768 + 22 = 2790 байт, 2790 × 100 / 32 256 = 8 %. ОЗУ: 22 + 184 = 206 байт, 206 × 100 / 2048 = 10 %, свободно 2048 − 206 = 1842 байта. IDE напишет:
Скетч использует 2790 байт (8%) памяти устройства. Всего доступно 32256 байт.
Глобальные переменные используют 206 байт (10%) динамической памяти, оставляя 1842 байт для локальных переменных. Максимум: 2048 байт.
«Локальные переменные» здесь означают стек. Строковые константы без PROGMEM на AVR входят в .data и поэтому увеличивают обе строки сообщения. Если глобальные переменные займут больше 75 % ОЗУ (порог по умолчанию), IDE предупредит: «Недостаточно памяти, программа может работать нестабильно». Если код не помещается в upload.maximum_size, сборка завершится ошибкой и сообщением «Скетч слишком большой».
Что принимают программы для прошивки
| Программа | Форматы | Адрес для BIN |
|---|---|---|
| avrdude 8.3 | i — Intel HEX, s — S-record, r — двоичный образ, e — ELF (только как входной файл, для записи и проверки, и если avrdude собрана с libelf); a — определить формат самой, по умолчанию для входных файлов | — |
| esptool 5.5 | write-flash: всегда пары «адрес — файл». Файл, который начинается с двоеточия, читается как Intel HEX и делится на части по промежуткам; адрес перед ним не используется, берутся адреса из файла. Остальные файлы пишутся как есть, поэтому ELF сначала превращают в образ командой elf2image | обязателен, перед каждым файлом |
| STM32CubeProgrammer | .bin, .elf, .axf, .out, .hex, .srec | обязателен: -w файл адрес |
| picotool | UF2, ELF и BIN; тип можно задать ключом -t uf2, -t elf или -t bin | -o, по умолчанию 0x10000000 |
| загрузчик RP2040 и RP2350 в ПЗУ | UF2, скопированный на USB-диск загрузчика | — |
Как прошивают конкретные платы, разобрано отдельно: avrdude для AVR и Arduino, esptool и прошивка ESP32, флеш-память и загрузчик STM32.
Типичные ошибки
- BIN записан не по тому адресу. Образ STM32 пишут с 0x08000000, приложение ESP-IDF на ESP32 — с 0x10000, а полный образ из merge-bin — с 0x0. Приложение, записанное с 0x0, затрёт загрузчик на 0x1000 и таблицу разделов на 0x8000. Образ, собранный для одного адреса и записанный по другому, не запустится: абсолютные адреса внутри него указывают не туда. HEX и ELF от этой ошибки защищены, адреса записаны в них самих.
- ELF там, где ждут HEX или BIN. esptool
write-flashне распознаёт ELF и запишет его байты как есть, а avrdude без libelf ELF не прочитает. Если программа приняла файл за двоичный, например по расширению .bin, в память уйдёт заголовок ELF или ASCII-символы HEX, и микроконтроллер не запустится. Обратная ситуация — отладка по HEX: точки останова по строкам исходника и имена переменных недоступны, остановиться можно только по адресу. - BIN размером в сотни мегабайт. У какой-то секции с содержимым адрес загрузки оказался в ОЗУ, например у .data без
AT>FLASH. Тогда от 0x08000000 до 0x20000000 лежит 384 МБ промежутка, и objcopy их заполнит. Секции без содержимого, такие как .bss, в двоичный файл не попадают и его не раздувают. Проверьте столбец LMA вobjdump -h, задайте секцииAT>FLASHили исключите её ключом-R. Сообщение Arduino IDE «Скетч слишком большой» означает другое: сумма .text и .data превысилаupload.maximum_size. - .data посчитана только в ОЗУ. Начальные значения переменных хранятся во флеш-памяти и копируются при старте, поэтому инициализированный глобальный массив на 1 КБ съедает 1 КБ флеш-памяти и 1 КБ ОЗУ. На ARM неизменяемые таблицы объявляют
const, и они остаются только во флеш-памяти. На классических AVR одногоconstмало: безPROGMEMконстанты попадают в .data и копируются в ОЗУ. Подробности в статье о языке C для микроконтроллеров. - Размер файла вместо размера прошивки. HEX примерно в 2,8 раза больше прошивки, UF2 с порцией 256 байт вдвое больше, ELF больше во много раз из-за отладочной информации. Размер прошивки — это text + data из вывода size или сумма данных записей HEX.
- UF2 не для того чипа. Блоки с чужим идентификатором семейства загрузчик молча пропускает: файл скопирован, а прошивка не обновилась. Для RP2350 важно ещё, собрана программа для ядер Arm или RISC-V.
Частые вопросы
Какой файл прошивки выбрать: HEX, BIN или ELF?
Тот, который понимает ваша программа прошивки. ELF нужен отладчику и годится для STM32CubeProgrammer, picotool и avrdude, собранной с libelf. В Intel HEX есть адреса и контрольные суммы, его читают avrdude, STM32CubeProgrammer и esptool, но не picotool и не загрузчики UF2. BIN берут, когда программа ждёт голый образ, например esptool write-flash, и тогда адрес начала нужно указать самому.
Почему отладчику нужен ELF, а не HEX?
В HEX и BIN лежат только байты для памяти. Отладчику нужны ещё имена функций и переменных с их адресами и соответствие адресов строкам исходного текста: это таблица символов и отладочные секции ELF. GDB читает символы из файла программы, а командой load, если отладочный сервер это поддерживает, сам записывает в микроконтроллер загружаемые секции по адресам из ELF. Без ELF остаются только точки останова по адресам.
Что значит «Скетч использует N байт» в Arduino IDE?
Это сумма размеров секций .text и .data (и .bootloader, если она есть) из вывода avr-size -A: код и начальные значения переменных, которые хранятся во флеш-памяти. На классических AVR в .data попадают и константы без PROGMEM. «Глобальные переменные используют» — сумма .data, .bss и .noinit, то есть статически занятое ОЗУ. Остаток ОЗУ IDE называет памятью для локальных переменных: из неё растёт стек.
Почему BIN-файл получился размером в сотни мегабайт?
objcopy -O binary пишет память сплошным куском от адреса загрузки самой младшей секции до конца самой старшей. Если у секции с данными, например у .data без AT>FLASH, адрес загрузки оказался в ОЗУ (у STM32 это 0x20000000 при флеш-памяти с 0x08000000), промежуток в 384 МБ будет заполнен. Секции без содержимого, такие как .bss, файл не раздувают. Проверьте столбец LMA в выводе objdump -h и задайте секции адрес загрузки во флеш-памяти или исключите её ключом -R.
Как сделать UF2 из ELF или BIN?
Для RP2040 и RP2350 — командой picotool uf2 convert: она принимает ELF или BIN и пытается сама определить идентификатор семейства, а если не получилось, его задают ключом --family. Для BIN адрес загрузки по умолчанию 0x10000000. Для ESP32 esptool merge-bin --format uf2 собирает UF2 из двоичных файлов с адресами, его копируют на диск платы-моста ESP USB Bridge.
Источники
- TIS Committee. Tool Interface Standard (TIS) Executable and Linking Format (ELF) Specification, Version 1.2, May 1995 — сигнатура 0x7F «ELF»; у секции одно поле адреса sh_addr; специальные секции .text, .rodata, .data, .bss, .symtab, .strtab, .debug, .line с типами и флагами (рис. 1-13); .bss типа SHT_NOBITS не занимает места в файле и обнуляется при запуске; заголовок программы: p_vaddr, p_paddr (физический адрес), p_filesz, p_memsz, сегмент PT_LOAD дополняется нулями, если p_memsz больше p_filesz
- System V Application Binary Interface, gABI, глава 4 «Object Files»: ELF Header, Sections, Symbol Table — e_machine: EM_ARM = 40, EM_AVR = 83; e_entry — точка входа; .init_array и .fini_array — SHF_ALLOC + SHF_WRITE; st_value и st_size символа
- GNU Binutils Documentation: objcopy, size — -O ihex, -O srec, -O binary; двоичный файл — дамп памяти с адреса загрузки самой младшей секции, символы и сведения о перемещениях отбрасываются; --gap-fill, --pad-to, -j, -R, --srec-len, --srec-forceS3; size: форматы Berkeley (по умолчанию, данные только для чтения — в text), GNU (они же — в data) и SysV (-A)
- GNU Binutils 2.45, исходные тексты: binutils/size.c, bfd/elf.c, ld/scripttempl/avr.sc, ld/emulparams/avr5.sh и avrxmega3.sh — size.c: секции без ALLOC пропускаются, код и (в формате Berkeley) только чтение — text, остальное с содержимым — data, без содержимого — bss; elf.c: p_paddr сегмента = LMA его первой секции, LMA секции при чтении вычисляется по p_paddr; avr.sc: у классических AVR без RODATA_PM_OFFSET и FLMAP (avr5) .rodata входит в .data и копируется в ОЗУ, .eeprom — в области eeprom
- GNU ld Documentation: Basic Linker Script Concepts, Output Section LMA, MEMORY Command, Builtin Functions, PHDRS — VMA — адрес при выполнении, LMA — адрес загрузки; секция данных хранится в ПЗУ и копируется в ОЗУ при старте; AT> и >region; LOADADDR(); ошибка компоновщика, если секции не помещаются в область памяти
- GDB Documentation: Commands to Specify Files; Commands for Managing Targets (load); Address Locations. Исходный текст gdb/symfile.c (gdb-16.3) — file читает из программы символы; load записывает загружаемые секции по их LMA, загружает таблицу символов и ставит счётчик команд на точку входа; точки останова по адресу *адрес работают и без отладочной информации
- GCC Manual: AVR Options; Optimize Options — ATmega328P — архитектура avr5; у avrxmega3 память программ видна в адресном пространстве ОЗУ; -fno-zero-initialized-in-bss: по умолчанию GCC кладёт переменные, инициализированные нулём, в BSS
- Intel. Hexadecimal Object File Format Specification, Revision A, 6 января 1988 — архивная копия официального файла Intel: поля записи, типы 00–05, RECLEN не больше FF, контрольная сумма — дополнение до двух суммы байтов от RECLEN до конца данных
- Motorola. M68000 Family Programmer's Reference Manual, 1992 (NXP M68000PRM), приложение C «S-Record Output Format» — поля S-записи, типы S0, S1–S3, S5, S7–S9, контрольная сумма — дополнение до единицы младшего байта суммы длины, адреса и данных
- Microsoft. USB Flashing Format (UF2): README.md и utils/uf2families.json (github.com/microsoft/uf2, коммит 90e9741) — блоки по 512 байт: заголовок 32 байта, до 476 байт данных, магические числа 0x0A324655, 0x9E5D5157 и 0x0AB16F30, поля little-endian; сразу записать блок можно при странице флеш-памяти до 256 байт; флаги, необязательная MD5 области; блоки с чужим familyID загрузчик пропускает; идентификаторы RP2040 0xe48bff56, RP2350 0xe48bff59–0xe48bff5b
- Raspberry Pi. RP2040 Datasheet (build 2025-02-20) — разд. 2.2 — карта адресов: XIP с 0x10000000, SRAM с 0x20000000; разд. 2.6.3 — внешняя флеш-память читается через окно XIP 16 МБ с адреса 0x10000000
- STMicroelectronics. STM32CubeF1, коммит c7656f1: шаблон скрипта компоновщика Projects/STM32F103RB-Nucleo/Templates/SW4STM32 (STM32F103VBIx_FLASH.ld) — FLASH с 0x08000000, RAM с 0x20000000; .init_array и .fini_array во FLASH; .data — >RAM AT> FLASH с ALIGN(4); секция ._user_heap_stack резервирует минимальные кучу и стек
- Arduino. ArduinoCore-avr, коммит 11b9130: platform.txt, boards.txt, загрузчик optiboot (Makefile, HEX и листинг) — avr-objcopy -O ihex -R .eeprom для .hex и -j .eeprom с --change-section-lma .eeprom=0 для .eep; рецепты загрузки пишут только flash; avr-size -A и регулярные выражения размера (.text, .data, .bootloader и .data, .bss, .noinit); Uno: 32 256 и 2048 байт; секции загрузчика optiboot
- Arduino. arduino-cli v1.5.1: internal/arduino/builder/sizer.go и перевод internal/locales/data/ru.po; README arduino-ide — текст сообщений о размере скетча и глобальных переменных, проценты целочисленным делением, предупреждение выше 75 % ОЗУ, ошибка при превышении; Arduino IDE 2 компилирует через arduino-cli
- AVRDUDE 8.3. Страница руководства avrdude(1) (github.com/avrdudes/avrdude) — форматы файлов i, I, s, r, e (ELF только как входной файл и при сборке с libelf), a — автоопределение для входных файлов
- Espressif. esptool 5.5.0: Basic Commands (docs/en/esptool/basic-commands.rst); esptool/cli_util.py, esptool/bin_image.py — write-flash всегда принимает пары адрес — файл; файл с двоеточием в начале читается как Intel HEX и делится по промежуткам, адрес перед ним не используется, остальные файлы пишутся как есть; elf2image: образ с заголовком, сегментами и SHA-256; merge-bin: raw с заполнением 0xFF, hex без заполнения, uf2 с наибольшей порцией и MD5 по умолчанию
- STMicroelectronics. UM2237 Rev 29. STM32CubeProgrammer software description, ноябрь 2025 — разд. 2.2.2 и 2.3.1: форматы .bin, .elf, .axf, .out, .hex, .srec, для двоичного файла адрес задаёт пользователь; разд. 3.2.3: -w <файл> [адрес], адрес обязателен для двоичных файлов
- Raspberry Pi. picotool README (github.com/raspberrypi/picotool, коммит 2041936) — load, info, verify принимают файлы uf2, elf и bin; для BIN адрес задаётся ключом -o, по умолчанию 0x10000000; uf2 convert превращает ELF или BIN в UF2 и пытается определить идентификатор семейства, иначе его задают ключом --family
Нашли ошибку?
Смотрите также
- Программирование МКC для МК, битовые операции, фиксированная точка, регистры
- Просмотр Intel HEXЗаписи HEX и S19, контрольные суммы, карта памяти, дамп, HEX ↔ BIN
- AVRDUDEПрошивка AVR из командной строки: ключи, Arduino, USBasp, фьюзы, ошибки
- esptoolПрошивка ESP32 и ESP8266: команды, адреса загрузчика, режим загрузки, ошибки
- Прошивка ESP32Загрузчик ПЗУ, GPIO0 и автосброс, write-flash и адреса, ошибки esptool и питание
- Flash и загрузчик STM32Страницы и секторы, BOOT0, системный загрузчик, AN3155, IAP, option bytes, RDP
- RP2040 и RP2350RP2040 и RP2350: ядра, память, GPIO, PIO, АЦП, 5 В на выводах, эрраты
- C для микроконтроллеровТипы, stdint, volatile, атомарность, регистры, константы во Flash