Перейти к содержанию

Файлы прошивки: 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. Дальше в файле идут секции и две таблицы: таблица секций для компоновщика и отладчика и таблица сегментов (заголовок программы) для загрузчика.

Секции

СекцияТип и флаги в спецификацииЧто содержитГде оказывается в микроконтроллере
.textPROGBITS, ALLOC + EXECINSTRмашинный кодфлеш-память
.rodataPROGBITS, ALLOCконстанты, строки, таблицыфлеш-память; у классических AVR без PROGMEM — в ОЗУ вместе с .data
.dataPROGBITS, ALLOC + WRITEначальные значения инициализированных переменныххранится во флеш-памяти, работает в ОЗУ
.bssNOBITS, ALLOC + WRITEпеременные без начального значения или с нулевым; в файле места не занимает, при старте обнуляетсятолько ОЗУ
.symtab, .strtabSYMTAB, STRTABимена функций и переменных, их адреса и размерыникуда, остаётся в файле
.debug…, .linePROGBITS, без флаговотладочная информация: типы, соответствие адресов строкам исходниканикуда, остаётся в файле

Флаг 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
ПолеСимволыЗначение
Начало:начало записи
RECLEN100x10 = 16 байт данных
Смещение7E00первый байт данных ложится по адресу 0x7E00
Тип00запись данных
Данные11 24 84 B7 14 BE 81 FF F0 D0 85 E0 80 93 81 0016 байт кода, первая команда eor r1, r1 (байты 11 24)
CHKSUMF7контрольная сумма

Считаем сумму. Заголовок: 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 или перетащите его сюда. Файл не покидает браузер.

HEX

стёртая флеш-память — FF

Результат

Размер прошивки
24 байта
данные с 0x0000 по 0x0017, 1 диапазон
Формат
Intel HEX, 16-битные адреса3 записи, с данными 2
Проверка записей
ошибок нет
CRC-32 образа
0xEF3FD8CA24 байта от 0x0000, промежутки — FF: как у файла .bin
Сумма байтов
0x69 · 0x0E698 и 16 бит, по тому же образу

Записи: 3

  • :начало записи
  • ■длина
  • ■адрес
  • ■тип
  • ■данные
  • ■контрольная сумма
СтрокаЗаписьТипАдрес или значениеСумма: в файле / верная
1:10000000259A1D9A29E236E940E84A95F1F73A959200 данные0x0000, 16 байт92 / 92
2:08001000D9F72A95C1F7F5CFDD00 данные0x0010, 8 байтDD / DD
3:00000001FF01 конец файлаконец файлаFF / FF

Карта памяти

ЧтоНачалоКонецРазмер
данные0x00000x001724 байта
Всего данных24 байта

Hex-дамп образа

HEX
000025 9A 1D 9A 29 E2 36 E9 40 E8 4A 95 F1 F7 3A 95|%...).6.@.J...:.|
0010D9 F7 2A 95 C1 F7 F5 CF |..*..... |

Слева адрес, затем 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:

СмещениеРазмерСодержимое
04первое магическое число 0x0A324655: символы «UF2» и перевод строки
44второе магическое число 0x9E5D5157
84флаги
124адрес во флеш-памяти, куда писать данные
164число байтов данных в блоке, часто 256
204номер блока, с нуля
244всего блоков в файле
284размер файла, идентификатор семейства или 0
32476данные, дополненные нулями
5084последнее магическое число 0x0AB16F30

Флаги: 0x00000001 — блок не для основной флеш-памяти (например, исходный текст внутри файла), 0x00001000 — контейнер файлов, 0x00002000 — указан идентификатор семейства, 0x00004000 — в конце данных записана MD5 области флеш-памяти, 0x00008000 — есть дополнительные теги. Проверки самих данных в формате нет. Магические числа нужны, чтобы отличить блок UF2 от прочих записей на диск и заметить, что блок записан не целиком, а MD5 позволяет программе прошивки пропустить блоки, которые уже совпадают с содержимым флеш-памяти. Если идентификатор семейства не совпадает с идентификатором чипа, загрузчик пропускает блок целиком. Поэтому несколько прошивок для разных микроконтроллеров можно склеить в один файл: каждый чип запишет только свою.

СемействоИдентификатор
RP20400xe48bff56
RP2350, Arm Secure0xe48bff59
RP2350, RISC-V0xe48bff5a
RP2350, Arm Non-secure0xe48bff5b
RP2xxx, абсолютная загрузка без таблицы разделов0xe48bff57
STM32F40x57755a57
ESP32-S30xc47e5767

Цена удобства — размер: при обычной порции 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, hextext + data + bss в десятичной и шестнадцатеричной записи——

Оценка для этой строки и микроконтроллера с 64 КБ флеш-памяти и 20 КБ ОЗУ:

флеш≈text+data=14 512+120=14 632 байт  (22,3 % из 65 536)\text{флеш} \approx \text{text} + \text{data} = 14\,512 + 120 = 14\,632 \text{ байт} \;(22{,}3\,\%\ \text{из}\ 65\,536)
ОЗУ≈data+bss=120+2344=2464 байт  (12,0 % из 20 480)\text{ОЗУ} \approx \text{data} + \text{bss} = 120 + 2344 = 2464 \text{ байт} \;(12{,}0\,\%\ \text{из}\ 20\,480)

Если скрипт компоновщика не резервирует стек и кучу отдельной секцией, под них остаётся 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.3i — Intel HEX, s — S-record, r — двоичный образ, e — ELF (только как входной файл, для записи и проверки, и если avrdude собрана с libelf); a — определить формат самой, по умолчанию для входных файлов—
esptool 5.5write-flash: всегда пары «адрес — файл». Файл, который начинается с двоеточия, читается как Intel HEX и делится на части по промежуткам; адрес перед ним не используется, берутся адреса из файла. Остальные файлы пишутся как есть, поэтому ELF сначала превращают в образ командой elf2imageобязателен, перед каждым файлом
STM32CubeProgrammer.bin, .elf, .axf, .out, .hex, .srecобязателен: -w файл адрес
picotoolUF2, 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.

Источники

  1. 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
  2. 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 символа
  3. 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)
  4. 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
  5. GNU ld Documentation: Basic Linker Script Concepts, Output Section LMA, MEMORY Command, Builtin Functions, PHDRS — VMA — адрес при выполнении, LMA — адрес загрузки; секция данных хранится в ПЗУ и копируется в ОЗУ при старте; AT> и >region; LOADADDR(); ошибка компоновщика, если секции не помещаются в область памяти
  6. GDB Documentation: Commands to Specify Files; Commands for Managing Targets (load); Address Locations. Исходный текст gdb/symfile.c (gdb-16.3) — file читает из программы символы; load записывает загружаемые секции по их LMA, загружает таблицу символов и ставит счётчик команд на точку входа; точки останова по адресу *адрес работают и без отладочной информации
  7. GCC Manual: AVR Options; Optimize Options — ATmega328P — архитектура avr5; у avrxmega3 память программ видна в адресном пространстве ОЗУ; -fno-zero-initialized-in-bss: по умолчанию GCC кладёт переменные, инициализированные нулём, в BSS
  8. Intel. Hexadecimal Object File Format Specification, Revision A, 6 января 1988 — архивная копия официального файла Intel: поля записи, типы 00–05, RECLEN не больше FF, контрольная сумма — дополнение до двух суммы байтов от RECLEN до конца данных
  9. Motorola. M68000 Family Programmer's Reference Manual, 1992 (NXP M68000PRM), приложение C «S-Record Output Format» — поля S-записи, типы S0, S1–S3, S5, S7–S9, контрольная сумма — дополнение до единицы младшего байта суммы длины, адреса и данных
  10. 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
  11. Raspberry Pi. RP2040 Datasheet (build 2025-02-20) — разд. 2.2 — карта адресов: XIP с 0x10000000, SRAM с 0x20000000; разд. 2.6.3 — внешняя флеш-память читается через окно XIP 16 МБ с адреса 0x10000000
  12. 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 резервирует минимальные кучу и стек
  13. 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
  14. 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
  15. AVRDUDE 8.3. Страница руководства avrdude(1) (github.com/avrdudes/avrdude) — форматы файлов i, I, s, r, e (ELF только как входной файл и при сборке с libelf), a — автоопределение для входных файлов
  16. 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 по умолчанию
  17. STMicroelectronics. UM2237 Rev 29. STM32CubeProgrammer software description, ноябрь 2025 — разд. 2.2.2 и 2.3.1: форматы .bin, .elf, .axf, .out, .hex, .srec, для двоичного файла адрес задаёт пользователь; разд. 3.2.3: -w <файл> [адрес], адрес обязателен для двоичных файлов
  18. Raspberry Pi. picotool README (github.com/raspberrypi/picotool, коммит 2041936) — load, info, verify принимают файлы uf2, elf и bin; для BIN адрес задаётся ключом -o, по умолчанию 0x10000000; uf2 convert превращает ELF или BIN в UF2 и пытается определить идентификатор семейства, иначе его задают ключом --family

Нашли ошибку?