U-BOOT на ARM-устройстве

О U-boot

Загрузчик Das U-Boot, наравне с известными lilo и grub, разработан для того, чтобы, запустившись после включения компьютера (будь то настольный PC или мобильный телефон), подготовить его к запуску основной операционной системы. Как и другие загрузчики, он облегчит выбор между вариантами загрузки, а также поможет при восстановлении после сбоя.

В отличии от своих "коллег", U-boot изначально использовался в системах на базе PowerPC, sparc, arm. Поддержка x86 появилась в нём сравнительно недавно.

Я опишу работу ARM-варианта загрузчика U-boot, а также приведу последовательность действий, которые следует выполнить чтобы добавить в код поддержку новой аппаратуры. В качестве базовой версии, я использовал U-boot от 2010-11-13 (коммит 227b72515546f). Исходный код доступен по этой ссылке.

Набор утилит для сборки загрузчика

Для сборки U-boot, нам потребуется компилятор gcc, способный генерировать код для архитектуры ARM. Пользователи Gentoo Linux могут воспользоваться системой crossdev. Другие пользователи возможно примут это предложение codesourcery. Так или иначе, после завершения установки, в системе должен появиться набор утилит arm-none-linux-gnueabi-{gcc,ld,as,nm}, способных компилировать, линковать и отлаживать бинарный код ARM.

Начинаем

Запуск U-boot на одной из поддерживаемых им плат обычно не требует редактирования исходного кода. Зная, что файл настроек include/configs/BOARDNAME.h уже составлен, необходимо выбрать нужную конфигурацию, скомпилировать и записать получившийся образ на загрузочный носитель. Весь процесс подробно описан в соответствующем разделе инструкции загрузчика.

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

Процесс загрузки

Для лучшего понимания предстоящей работы важно иметь представление о действиях, совершаемых загрузчиком с момента своего старта и до передачи управления операционной системе. Следует помнить, что устройства на базе ARM имеют адресное пространство, используемое как для доступа к памяти, так и к регистрам устройств. В частности, некоторые системы поддерживают отображение на него данных, хранящихся на внешних накопителях, таких как SPI-Flash.

U-Boot спроектирован с расчётом на то, что он будет "самым первым" загрузчиком в системе. (к примеру, см. этот вопрос из FAQ) Обычно такие загрузчики располагаются в некой области памяти "только для чтения", код из которой начинает выполняться сразу после включения устройства, в частности "релокация" или перемещение собственного кода в другой участок адресного пространства.

Рассмотрим в качестве примера карту памяти некого ARM устройства:

000000000  Начало области памяти, на которую отображаются
           данные SPI Flash-накопителя

000400000  Конец области памяти, на которую отображаются
           данные Flash-накопителя. Неиспользуемая память.

400000000  Начало области памяти, отображаемой в RAM

480000000  Конец области памяти, отображаемой в RAM.
           Неиспользуемая память.

800000000  Начало области памяти, используемой для
           доступа к регистрам устройств (в том числе, для
           доступа к регистрам контроллера RAM)

FFFFFFFFF  Конец области памяти, используемой для
           доступа к регистрам устройств

Допустим, что после включения такого устройства, процессор начинает выполнять инструкции с 0-го адреса.

Записав (возможно, с помощью JTAG) образ u-boot (файл u-boot.bin), мы обеспечим выполнение, системы управление получит код, расположенный по метке _start (точка входа в программу).

Точка входа в программу

Обычно, точка входа в программу расположена в файле arch/arm/CPU/start.S. То, что именно этот файл содержит точку входа, определяется скриптом линковщика u-boot.lds.

Старотвый код, следующий за точкой входа (меткой _start) должен выполнить следующие действия:

  • Перевести процессор в "начальное состояние" - выключить MMU и кеширование.
  • Очистить память, используемую для хранения не инициализированных данных (секция .bss, определяется *lds скриптом)
  • Настроить первичное положение стэка (определяется настройкой CONFIG_SYS_INIT_SP_ADDR)

Вызов C-функции первичной инициализации board_init_f завершает эту последовательность.

Первичная инициализация

Главная задача процедуры первичной инициализации - подготовка системы к релокации кода в RAM. Обычно для этого требуется выполнить настройку контроллера SDRAM и расчитать адреса основных областей памяти (области кода, области глобальных данных, положение стека и "кучи" - памяти используемой malloc/free). Также необходимо провести настройка вспомогательного оборудования - контроллера UART, таймеров.

Первичная инициализация выполняется функцией board_init_f, (файл arch/arm/lib/board.c). Из неё совершаются вызовы функций, определяемых отдельно для каждой платы или системы на кристалле. Их определения традиционно размещают в board/BOARDNAME/* и arch/arm/cpu/CPU/SOC соответственно. Именно в этот момент производится вывод приветствия в последовательный порт. После завершения настроек, вызывается функция

	relocate_code (addr_sp, id, addr)

передающая управление коду в start.S. В качестве параметров она использует: адрес начала стека (addr_sp), адрес области глобальных данных (id) и стартовый адрес области памяти, куда требуется скопировать код (addr).

Релокация кода

Процедура релокации написана на ассемблере и размещена в start.S. Она выполняет следующие действия:

  1. Собственно, копирование кода в место назначения.
  2. Исправление абсолютных указателей в соотвтетствии с данными секций __got* (формуруются компилятором gcc). Примером может служить struct stdio_dev, хранящая указатели на функции-методы putc/getc и др.
  3. Заполнение нулями области неинициализированных данных.
  4. Настройка нового положения стека
  5. Расчёт местоположения и передача управления копии функции вторичной инициализации board_init_r.

Необходимо заметить, что алгоитм работал бы неверно, если бы в файле arch/arm/config.mk не был бы включен режим Position independent executable (флаг -pie линковщика).

Вторичная инициализация и вход в главный цикл

Вторичная инициализация представлена функцией

void board_init_r (gd_t *id, ulong dest_addr)

переход на копию которую производит процедура релокации после завершения своей работы. Она представляет собой последовательность вызовов процедур инициаллизации malloc, консоли, Flash-накопителей, сетевых устройств и пр., завершающуюся входом в цикл опроса устройств ввода и выполнения команд пользователя.

Команды

Основные команды (bootm, setenv, help, version) включены по-умолчанию. С их помощью пользователь может настраивать переменные окружения, получить помощь, передать управление заранее размещенному в памяти образу ядра Linux. Разместить образ можно, например, скопировав его с флеш-накопителя (группа команд nand) или с tftp-сервера (группа команд tftpboot). Разумеется, перед этим необходимо правильно настроить соответствующее оборудование. К примеру, для корректной работы сетевых команд (ping, tftp), в конфигурационном файле могут быть определены макросы:

#define CONFIG_NET_MULTI          1
#define CONFIG_CMD_NET
#define CONFIG_NETMASK            255.255.255.0
#define CONFIG_GATEWAYIP          10.0.0.1
#define CONFIG_SERVERIP           10.0.0.1
#define CONFIG_IPADDR             10.0.0.2
#define CONFIG_ETHADDR            "00:02:F7:00:27:0C"
#define CONFIG_BOOTFILE           "/tftpboot/"

а также макрос, соответствующий u-boot драйверу имеющегося
сетевого контроллера. К примеру

#define CONFIG_GRETH

включит в сборку драйвер контроллера фирмы Aeroflex Gasiler.

Реализации остальных команд пользователя собраны в папке common. Как правило, за включение той или иной команды в сборку отвечает макрос вида CONFIG_CMD_<имя_команды>. Их перечень можно посмотреть в common/Makefle.

Добавляем в U-Boot поддержку нового оборудования

Определим задачу. Допустим, требуется добавить поддержку новой платы, работающей на основе процессора ARM 1176 и имеющей имя "UEMD". Пусть процессор имеет доступ к двум банкам памяти:

  1. К внутреннему банку памяти (IM0) размером 256 Кб, расположенном по адресам 0x00100000 .. 0x00140000 в адресном пространстве.
  2. К банку DDR2 памяти (EM0) размером 128 Мб, доступному по адресам 0x40000000 .. 0x48000000. Это - внешняя память, включающаяся только после инициализации контроллера DDR, регистры которого расположены начиная с адреса 0x80000000.

Пусть также известно, что после включении питания, процессор, благодаря коду, зашитому в специальную внутреннюю ROM на стадии изготовления, копирует 256 Кб данных с внешнего флеш накопителя в IM0, после чего передаёт управление по адресу 0x00100000.

Прежде чем начать

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

while(!compiled_successfully) {
	edit_config();
	compile();
}

Действительно, в коде загрузчика используется большое количество макросов вида CONFIG_* , несогласованное использование которых приводит к таким ошибкам компиляции и линковки, как повторное объявление или отсутствие объявления функции. Процесс портирования U-boot в большой степени заключается в поиске этих несогласованности и способов их устранения.

  • Внести изменения в главный Makefile
  • Составить конфигурационный файл include/configs/BOARDNAME.h
  • Расположить файлы, имеющие отношение к описание работы процессора или системы на кристалле в arch/arm/CPU/.
  • Отредактировать скрипт загрузчика u-boot.lds, в случае, если стандартный по каким-либо причинам не подходит.

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

Регистрация новой архитектуры в системе сборки

В главный Makefile проекта необходимо добавить правило, соответствующее новой плате.

Makefile:

uemd_config	: unconfig
	$(MKCONFIG) $(@:_config=) arm arm1176 uemd - uemd

Конфигурационный файл

Добавим файл include/configs/uemd.h. В нем необходимо объявить системные константы, адреса регистров устройств, набор поддерживаемых команд командной строки, параметры работы UART- и NAND-, Ethernet- и других подсистем u-boot, которые могут пригодиться на этапе загрузки.

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

В случае, если виртуальная память (MMU) не используется, параметр CONFIG_SYS_UBOOT_BASE приравнивают уже определенному CONFIG_SYS_TEXT_BASE.

#define CONFIG_SYS_UBOOT_BASE     CONFIG_SYS_TEXT_BASE

Группа настроек, отвечающих за конфигурацию памяти. Следует помнить, в нашем случае u-boot стартует из внутренней памяти CONFIG_SYS_TEXT_BASE, инициализирует DDR и только потом копируется (релоцируется) в неё.

include/configs/uemd.h:

#define CONFIG_ENV_SIZE           0x100

Размер памяти, выделяемый для хранения переменный окружения U-boot.

#define CONFIG_SYS_MALLOC_LEN     0x8000

Размер области динамической памяти

#define CONFIG_SYS_GBL_DATA_SIZE  128

Размер области памяти, отводимой для хранения глобальных данных Максимальный прелполагаемый размер стека

#define CONFIG_STACKSIZE          (128*1024)

Начальный адрес области памяти, соответствующий DDR.

#define CONFIG_SYS_SDRAM_BASE     0x40000000

Размер области памяти DDR

#define CONFIG_SYS_SDRAM_SIZE     0x48000000

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

#define CONFIG_SYS_NS16550
#define CONFIG_SYS_NS16550_SERIAL

Размер регистров контроллера. Для little-endian регистров размером 4 байта необходимо указать значение -4.

#define CONFIG_SYS_NS16550_REG_SIZE -4
#define CONFIG_CONS_INDEX	      1

Тактовая частота, на которой работает контроллер UART, в Гц.

#define CONFIG_SYS_NS16550_CLK    54000000

Начальный адрес области адресного пространства,
соответствующего регистрам контроллера UART.

#define CONFIG_SYS_NS16550_COM1   0x2002b000
#define CONFIG_BAUDRATE           38400

Набор опций CONFIG_ENV_IS_* определяет вид ПЗУ, где будут сохраняться переменные окружения U-Boot между его запусками. В данном случае, хранение переменных не требуется.

#define CONFIG_ENV_IS_NOWHERE

Адрес памяти, откуда предполагается стартовать ядро Linux. Адрем соответствует смедению 0x8000 от начала банка RAM.

#define CONFIG_SYS_LOAD_ADDR      0x40008000

Адрес параметров "командной строки" ядра Linux.

#define CONFIG_SYS_PARAM_ADDR     0x40000100

Остальные настройки могут быть подсмотрены в многочисленных конфигурационных файлах других систем.

В частности, для включения в сборку дополнительных команд, необходимо необходимо определять макросы CONFIG_CMD_*. Их перечень проще всего посмотреть в файле common/Makefile, а также в файлах common/cmd_*.c

Файлы board/uemd/*

Файл board/uemd/config.mk

Файл используется для объявления переменных Makefile, специфичных для текущей платы. Здесь необходимо объявить переменную CONFIG_SYS_TEXT_BASE, значение которой подставится в С-макрос с таким же имененм. LDSCRIPT переопределяет расположение скрипта линкера. О нём см. ниже.

CONFIG_SYS_TEXT_BASE = 0x00100000

LDSCRIPT := $(SRCTREE)/board/uemd/u-boot.lds

Создадим файл board/uemd/uemd.c, содержащий объявления функций, описанных в разделе "Первичная инициализация".

Файл board/uemd/Makefile

Основное отличие от других Makefile заключено в строчках

COBJS := uemd.o
SOBJS := lowlevel_init.o

где должны быть перечислены все объектные файлы, получающиеся из *c и *S файлов данной директории.

Файл board/uemd/uemd.c

Содержит процедуры инициаллизации оборудования.

int board_init(void)

- общая инициализация. Самое важное, что нужно здесь сделать - присвоить значение переменной gd->ram_size, влияющей на расчёт параметров релокации.

int board_eth_init(bd_t *bis)

- инициализация контроллера ethernet.

void __udelay (unsigned long usec)

- задержка на usec микросекунд. Типичная реализация должна опрашивать в цикле таймер, доступный в системе.

ulong get_timer(ulong base)

- возвращает текущее системное время в миллисекундах, прошедшее с момента base, также заданного в миллисекундах.

void reset_cpu(ulong addr)

- выполняет программный сброс устройства. Типичная реализация будет использовать устройство watchdog, доступное в системе.

int dram_init(void)

- инициализация контроллера DDR. Самостоятельная реализация этой функции может стать нелегкой задачей, поскольку некоторые контроллеры обладают большим количеством доступных программисту настроек. Кроме того, режим работы контроллера может зависеть от параметров конкретной платы (например, длины, ёмкости и сопротивления проводников, соединяющих микросхемы памяти с процессором). Перед началом работы, желательно подобрать несколько примеров данной функции, подходящих под Ваш процессор и контроллер DDR.

Файл board/uemd/lowlevel_init.S

Файл может содержать процедуру lowlevel_init, написанную на языке ассеблера. Эта процедура вызывается в самом начале работы загрузчика и по-умолчанию ничего не делает.

.globl lowlevel_init
lowlevel_init:
	mov pc, lr

Файл board/uemd/u-boot.lds

Поскольку рассматриваемая система на кристалле собрана на базе процессора ARM1176, разумно взять за основу скрипт arch/arm/cpu/arm1176/u-boot.lds

Его следует скопировать в board/uemd/u-boot.lds, указав новый путь в переменной LDSCRIPT из board/uemd/config.mk.

Отладка

Традиционный способ отладки предполагает использование отладчика GDB, подключаемого к устройству с помощью интерфейса JTAG. Неудобство состоит в том, что после выполнения релокации, адреса функций оказываются изменёнными, что затрудняет расстановку брейкпоинтов. Однако проблема решаема - организации удобной отладки посвящены отдельные разделы руководства U-boot.

Дополнительные отладочные сообщения также весьма информативны. Чтобы их включить, достаточно объявить макрос DEBUG в главном config.mk, добавив -DDEBUG к флагам компиляции. Иногда, полезым может оказаться отключение оптимизации по размеру, для чего следует заменить флаг -Os на -O0. Кроме этого, многие драйверы и библиотеки, входящие в состав U-boot, имеют собственные механизмы отладки, подсказки по использованию которых можно найти в коде.

Выключение релокации

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

  1. Заменить вызов функции relocate_code() на board_init_r()
  2. Выключить режим Position independent executable - убрать опцию -pie из arch/arm/config.mk
  3. Написать код, заполняющий нулями область неинициаллизированных данных (секция .bss). Сам по себе этот код уже присутствует в start.S, необходимо только перенести и упростить его так, чтобы он был выполнен до первичной инициаллизации.
  4. Помнить, что выключая таким образом релокацию в U-boot для одной системы, мы делаем невозможной его работу с большинством остальных.

Примером выполнения всего трюка может служить коммит 986369e94c. Если всё сработало как нужно, то появится возможность, к примеру, провести трассировку функции board_init_r без каких-либо дополнительных настроек gdb.

Вот спасибо, в закладки :)

Вот спасибо, в закладки :)

Working on Gentoo Linux for Asus P535 and Qtopia :-)

Пару вопросов 1. Как

Пару вопросов
1. Как настроить Eclipse CDT для crossdev?
2. Поддерживает ли GRUB ARM11, ARM Cortex-M3?
3. Возможно ли поставить Gentoo на телефон с ARM11 и у которого есть только USB разъём?

RuZzz написал(а): Пару

RuZzz написал(а):
Пару вопросов
1. Как настроить Eclipse CDT для crossdev?

Лично, я не пользовался Eclipse CDT, но думаю как и везде просто выбрать соответствующий компилятор/линкер/отладчик

RuZzz написал(а):
2. Поддерживает ли GRUB ARM11, ARM Cortex-M3?

Нет

RuZzz написал(а):
3. Возможно ли поставить Gentoo на телефон с ARM11 и у которого есть только USB разъём?

Возможность есть, но в первую очередь нужно ядро поддерживающее все железо телефона(ну или частично)

Working on Gentoo Linux for Asus P535 and Qtopia :-)

Поставить Gentoo на телефон в

Поставить Gentoo на телефон в принципе должно быть можно. Но есть риск получить устройство, которое будет не уметь звонить (так как не окажется драйвера GSM-модема) и с непригодным для использования с маленьких экранов ПО. К сожалению, установка обычной "десктопной" версии генты (как и многих других дистрибутивов) даже на нетбуке требует серьезной работы по настройке. Знаю, так как сам в свое время намучался с тачскрином своего IdeaPad'а. В качестве примера linux-ОС, пригодной для использования на карманных устройствах можно назвать Meego. К сожалению, ей явно не достает той самой "гибкости" настроек. Жду с нетерпением, когда Glade программам , входящим в её состав, кто-нибудь напишет ебилд :)

Настройки просмотра комментариев

Выберите нужный метод показа комментариев и нажмите "Сохранить установки".