Издательский дом ООО "Гейм Лэнд"ЖУРНАЛ ХАКЕР 124, АПРЕЛЬ 2009 г.

Встраиваем пингвина. Учимся ставить Linux на микроконтроллеры

Diver (diver@edu.ioffe.ru)




Открытость и гибкость GNU/Linux позволяет заточить ее буквально подо что угодно. Эта ОС одинаково хорошо работает на маршрутизаторах, мобильниках и профессиональных системах сбора данных – в общем, на так называемых встраиваемых устройствах. Как этого добиваются?

Итак, у тебя есть Идея, и звучит она так: «Хочу Linux на Микроконтроллере». Как и почти любую замечательную Идею, ее придумали еще до тебя и уже досконально проработали опытные линуксоиды и программисты. Для начала определись с задачей. Что твое устройство будет делать? Как будет реагировать на внешние раздражители? Задай себе главный вопрос: ЗАЧЕМ тебе здесь Linux?

Если определился, выбирай девайс, на который твоя ОСь будет водружаться. На нем должен быть поддерживаемый Linux'ом микроконтроллер, а также достаточно памяти и быстродействия для твоих задач. В идеале, для первых экспериментов подойдет какой-нибудь простенький роутер (типа D-Link на MIPS-архитектуре, с уже предустановленным Linux'ом). Перекомпиль ядро под свою задачу и залей обратно на роутер, – это и будет первый опыт.

Дальше можешь спаять или купить какую-нибудь отладочную плату. Только следи, чтобы на твоем одноплатнике было не менее 16 Мб оперативки, и контроллер имел Блок Управления Памятью MMU (смотри врезку), иначе придется довольствоваться сильно урезанным ядром ucLinux. Как запустишь ядро и примонтируешь файловую систему, пиши или ищи драйвера для интерфейсов и внешней периферии.

Обобщим. Для более-менее успешной реализации Идеи, тебе надо:

  • Знать основы микроэлектроники.
  • Знать язык С (желательно Асм).
  • Уметь ориентироваться в Datasheets и прочей документации к контроллеру и его внешней периферии.
  • Иметь опыт программирования МК (базовые знания процесса загрузки Linux также будут нелишними).

Звучит страшно? Но основы я расскажу здесь, а остальное прочитаешь по ссылкам, если заинтересует.

Низкий уровень

У любого микроконтроллера на кристалле, помимо собственно ядра, живет периферия. По сути, это отдельные устройства, объединенные в один корпус. Ядро управляет всей внутренней периферией путем записи или чтения из регистров, замапленных в специальную область памяти. Они называются SFR (Special Function Register). Запомни эту аббревиатуру, в документации к процессорам она часто используется. А само процессорное ядро, кроме арифметики да чтения/записи в память, по сути, ничего и не умеет и все перекладывает на плечи периферии.

В нашей задаче установки Linux контроллер обязательно должен иметь два периферийных устройства:

  1. PDC - контроллер прямого доступа к памяти. Вместо того чтобы «вручную» принимать данные с портов ввода-вывода и копировать их в оперативную память, процессорное ядро может отдать эту операцию на откуп контроллеру DMA (прямого доступа к памяти), и, пока тот перемещает данные, заниматься действительно полезным делом. По окончании операций с памятью, PDC дергает прерывание, и ядро переключается на обработку данных.
  2. Memory Management Unit (или, по-русски, Блок Управления Памятью). Работает в тандеме с PDC и контроллером оперативки. Главная его роль – это трансляция виртуальной памяти в физическую, а также контроль доступа. На твоем десктопе архитектуры х86/х86-64 он присутствует всегда, а вот в микроконтроллерах его может и не быть. Linux, как многозадачная система, использует этот блок для установки прав доступа на страницы памяти. Как только какой-нибудь код попытается взаимодействовать с запрещенной областью памяти, сработает аппаратное прерывание, обрабатываемое ядром Linux.

Также пригодятся контроллеры карточек NAND-Flash и CompactFlash, но они, наверняка, присутствуют везде, где есть MMU. Остальную периферию тоже не следует обделять вниманием – например, периферийный контроллер микросхемы физического уровня Ethernet и поддержка USB-Host дадут тебе нехилые преимущества в возможностях.

Как известно, процесс загрузки ОС на базе Linux, вне зависимости от архитектуры, происходит в несколько этапов. Вкратце напомню: при старте компьютера первым запускается загрузчик, подготавливающий все необходимое для запуска ядра – конфигурирует контроллер памяти, последовательный порт, стек и шину, распаковывает временный образ корневого раздела в память. После низкоуровневой настройки железа загрузчик должен найти, скопировать в память и запустить непосредственно ядро с нужными параметрами командной строки и окружением. В «больших» компьютерах типа IBM-PC первую часть загрузки выполняет BIOS, а вторую берет на себя загрузчик типа GRUB или LILO.

Следом стартует ядро Linux – распаковывает себя, определяет окружающее железо, инициализирует прерывания, запускает процесс Инит и подцепляет корневую файловую систему. Последняя может иметь вид сжатого РАМ-диска и быть распакованной в память еще загрузчиком (или же сразу монтироваться на Флеш-диске, если имеет драйвера для доступа к нему внутри ядра).

Далее, процессом Инит с готовой корневой ФС, стартуются уже другие процессы, и выполняются стартовые скрипты. Вскоре у тебя на устройстве – рабочая система.
Естественно, загрузившись, ядру надо будет выполнять свои прямые обязанности, делать то, ради чего система и ставилась (например, собирать какие-нибудь данные об окружающем мире и показывать их на экране по требованию, поступившему с клавиатуры). Для общения с периферией нужны драйвера, которым взяться в ядре неоткуда. Значит, следующая задача - найти или написать их.

Что нужно со стороны Старшего Брата

Для начала – компиляторы для сборки софта под устройство. Понятно, что нужны GNU'тые GCC и toolchain, ибо они входят в официальный инструментарий компиляции Линукса, и именно под ними он скомпилируется без проблем. «Обыкновенные» GNU C компиляторы под х86-ю архитектуру не подойдут, так как архитектура контроллера в твоем embedded-устройстве, полагаю, любая другая, но только не х86-я. Поэтому придется качать кросс-компиляторы. Для ARM7/9/11 это, например, GNUARM (www.gnuarm.com), для AVR – GNU AVR (есть в репозитории Дебиана, пакет gcc-avr), ну а для совсем маленьких устройств типа архитектуры С51 - SDCC (хотя туда ядро Linux уже будет проблематично засунуть).

Для создания образов файловых систем нам подойдут стандартные утилиты типа mkfs.(что_угодно), gzip и cpio. Здесь проблем возникнуть не должно. Теперь по поводу железа. Во-первых, не забывай про мощный отладочный интерфейс JTAG, который есть на борту у каждого уважающего себя контроллера. С ним заливка и отладка софта становятся вообще сказкой. JTAG-отладчик для LPT-порта можно собрать самостоятельно (смотри, к примеру, схему – www.diygadget.com/store/building-simple-jtag-cable/info_12.html) или купить готовый. Софта под интерфейс JTSG завались, и там уже выбирай под нужную тебе задачу.

Ядро Linux при загрузке очень любит плеваться сообщениями на отладочный интерфейс (DBGU) контроллера. Естественно, нам первое время надо будет общаться с Linux'ом и искать ошибки напрямую. Для этого загрузчиком заранее конфигурируется DBGU-интерфейс, который представляет собой обыкновенный RS-232 порт, к которому присоединяются с помощью кабеля для COM-порта (надеюсь, COM-разъем у тебя на отладочной плате распаян).

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

Не все контроллеры умеют без загрузчика шиться по USB, многим для этого требуются спецпротоколы, скажем, C2 или ActiveSerial. Про твое устройство, как и про способ его загрузки, я ничего не знаю, поэтому - марш на сайт производителя изучать способы прошивки своего отдельно взятого МК. Возможно, тебе придется докупить или допаять какой-нибудь простенький шнурок.

Что, куда и как заливать

Загрузчик, запакованное ядро и ФС с софтом надо где-то хранить. Чаще всего, загрузчик заливают в набортную флэш-память микроконтроллера, а все остальное лежит в распаянной рядом NAND-памяти.

Собранный софт (в нашем случае – это ядро Linux и образ ФС) нужно залить в энергонезависимую память твоей платы. Способ целиком и полностью зависит от архитектуры и периферии устройства, так что все действия смотри в документации к твоему контроллеру. Например, контроллер Atmel AT91SAM9260, на котором я отлаживаю свой Linux, никакой внутренней флэш-памяти не имеет, и прошивку надо заливать в запаянные рядом микросхемы Data или NAND-Flash памяти. Это было бы муторно, но к счастью, мой контроллер, не находя нигде подходящей программы, самостоятельно переключается в так называемый режим SAM-BA Boot, и, будучи воткнутым по USB в компьютер, определяется как usbserial-устройство. После чего через специальный софт, написанный Atmel'овцами, я могу напрямую заливать нужные образы по адресам в память устройства сквозь микроконтроллер.

Ну а если ты не поленился найти или запаять JTAG-интерфейс, то с легкостью сможешь не только заливать в контроллер софт, но и трассировать его.

Загрузчики

Это самая сложная и ответственная часть загрузки Linux. Чемпионом по зоопарку поддерживаемых архитектур и размеру сообщества является универсальный загрузчик U-Boot (www.denx.de/wiki/U-Boot). На страницах журнала о нем не раз упоминалось. Для первых опытов советую именно его, так как тут есть, в том числе, драйвера на огромный список внешней периферии. Если утрировать, заточка U-Boot под конкретное устройство часто сводится к чтению маркировок окружающих контроллер микросхем. Для совсем ленивых этот загрузчик уже допилен под процессоры Atmel AT91SAM и AVR32, смотри сайты linux4sam.org и avrfreaks.net.
На втором месте - загрузчик RedBoot, с не меньшими возможностями, но с меньшим комьюнити. Каюсь, я про него доселе сам ничего не знал, но если те же ЕмДебиановцы советуют, то он точно чего-то стоит :).

Помимо универсальных загрузчиков, существуют еще заточенные под отдельные контроллеры, пишущиеся производителями и включающиеся ими в так называемые Software Packages. Просто поищи на сайте производителя, нечто подобное для облегчения труда программиста там всегда выкладывают. Например, для Atmel AT91SAM9 уже существует быстрый и компактный загрузчик AT91 Bootstrap. Он понимает файловые системы JFFS2, FAT, подцепляет флешки и умеет загружать Linux. Ну а большего нам и не надо.

Дистрибутивы

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

Полагаю, абсолютное чемпионство по количеству разных архитектур и пакетов принадлежит проекту Дебиан и его подразделу EmDebian (www.emdebian.org). Последний отличается от оригинального Debian тем, что у него меньший размер - там просто повыкидывали ненужные файлы типа документации. Хелпов на сайте проекта завались, есть подробные HowTo, много вспомогательных утилит и прекомпилированные пакеты. Выполняй по пунктам процесс установки - и будет тебе счастье. У проекта нет своего собственного загрузчика, поэтому мантейнеры советуют использовать сторонние, типа U-boot или RedBoot (www.emdebian.org/tools/bootloader.html).

Вообще, если к твоему устройству можно подключить винчестер или мегафлешку, то можешь не заморачиваться и ставить «большой» дистрибутив Debian. Всяко, там пакетов больше, да и система «взрослая». А если ты приверженец Генту? Пожалуйста, к твоим услугам Embedded Gentoo (www.gentoo.org/proj/en/base/embedded)! Хелп хороший, примеров много, а в загрузчики нам сватают все тот же U-Boot.

Более продвинутые линуксоиды, которым знакомы слова Linux from Scratch, могут попробовать метадистрибутив OpenEmbedded (www.openembedded.org). По сути, это набор скриптов, build-утилита BitBake и набор метаданных, призванные облегчить сборку как ядра Линукс, так и любого софта под сторонние дистрибутивы. Никаких репозиториев, как в бинарных дистрибах, у него нет, – зато есть инструменты для легкого создания пакетов под IPK, RPM, DEB или tar.gz форматы.

Кроме «универсальных» дистрибутивов, которые можно поставить на все, что угодно, в природе существуют специализированные, заточенные под определенный тип устройств. Например, проект OpenWrt (openwrt.org), предназначенный для установки исключительно на точки доступа. Народ помаленьку портирует туда софт, который ты запросто сможешь скачать прямо на устройство с помощью программы opkg, родственника дебиановского dpkg/apt. Прописываешь в местный аналог sources.list репозиторий под твою архитектуру и наслаждаешься круглосуточным файл-сервером и торрент-клиентом на стенке.

Под мобильники (они относятся к встраиваемым устройствам!) сейчас тоже создаются варианты Линукса. Это – MontaVista (www.mvista.com) и нашумевший недавно OpenMoko c платформой Neo FreeRunner (openmoko.org). Направление новое, перспективное, поэтому дистрибутивами в этом секторе занимаются не столько энтузиасты, сколько вполне себе корпорации. Google с Nokia также поспешили выпустить свои продукты, и мы получили бегающие под Linux'ом Android'ы и N810.
Не следует забывать и про самые маленькие и «глупые» устройства с примитивными МК без MMU, например, ARM7. Для них был проработан дистрибутив ucLinux (uclinux.org/ports) с отвязанными от ядра функциями управления памятью. Несмотря на отсутствие какой бы то ни было защиты и безопасности (одна программа может запросто повредить память другой), ucLinux - настоящая и полноценная система с поддержкой многозадачности.

Заключение

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

Виртуальная память

MMU создает абстрактный слой между реальной, «железной» памятью и так называемой виртуальной адресацией, к которому, в итоге, и обращается процессорное ядро. Пример: загрузчик микроконтроллера AT91SAM9 может жить в двух разных местах – в микросхемах Dataflash и NANDFlash. При включении питания встроенный SAM-BA Boot запускается из внутренней ROM контроллера. В этот момент внутренняя read-only память «находится» по адресу 0x00, поэтому ядро честно начинает выполнять код, записанный там. Заметь, ядро универсально и ничего не знает про окружающую его периферию.

Оно тупо и наивно начинает выполнять программу, находящуюся по нулевому адресу. SAM-BA - примитивный загрузчик, он может только опросить по очереди Dataflash и NANDFlash и скопировать первые 4 Кб найденного кода во внутреннюю оперативную память SRAM микроконтроллера. После копирования SAM-BA исполняет особую процессорную операцию, и адрес 0x00 уже указывает на внутреннюю статическую память. Далее 4-килобайтный загрузчик может скопировать основной код из оставшегося объема Flash-карты в оперативную память SDRAM, после чего нулевой адрес виртуальной памяти будет уже указывать в начало оперативки, и начнется нормальная загрузка. Вот такое вот прыгание нулевого адреса, про которое ничего не обязано знать ядро ARM.

В процессе работы тот же контроллер MMU проецирует все микросхемы памяти, висящие на шине External Bus Interface, в блоки 256 Мб каждый, и мы уже почти можем не задумываться о реальном местонахождении наших данных. Все «прозрачно» и в одном пространстве. В верхнюю область памяти (4 Гб) обычно мапятся адреса регистров для управления периферией. Это стандартная фишка всех микроконтроллеров.

Типы Flash памяти

В «большом» компьютере твое ядро Linux хранится на жестком диске и копируется в оперативную память RAM (ее динамический тип – SDRAM) при загрузке, откуда и работает. Ядро, подцепив драйвера жесткого диска, работает уже с постоянной ФС.

Во встраиваемых устройствах жесткий диск используется довольно редко (если это не NAS и не DVD-плеер), вместо него - например, Flash-память.
Наиболее известный ее тип - NAND (Not AND), знакомый тебе по USB-флешкам и mp3-плеерам. Главный плюс NAND - огромная емкость, десятки гигабайт. Зато минусов тоже немало. Из наиболее известных - чтение/запись только по блокам. Ей нужен драйвер, и софт напрямую оттуда исполняться не может, только после копирования в оперативку. Поэтому эту память часто используют как хранилище редко изменяемых данных, типа файловых систем.

Второй тип памяти - NOR (Not OR), намного более дорогой и быстрый на чтение. Основная ее фишка - возможность «случайного доступа» (Random Access), прямо как в оперативной памяти SDRAM. Поэтому если программу из NAND надо сначала скопировать в оперативку, чтобы нормально ее исполнить, то с NOR она может исполняться напрямую, через общую адресацию. Туда ты можешь засунуть, например, загрузчик, он будет доступен сразу.

Есть еще тип постоянной памяти, малоизвестный, но часто использующийся в embedded-устройствах. Это – Serial (последовательный) Flash. Его плюс – простой последовательный внешний интерфейс SPI. Много ног ему не надо, поэтому микросхемы Serial Flash чаще всего небольшие, восьминогие. Объем такой памяти также невелик, порядка 8 Мб, но если делается дешевое устройство, то монстров для параллельных NAND и NOR обычно не городят.

Оперативная память

Бывает два вида. Известная тебе SDRAM – и SRAM. SDRAM (Synchronous Dynamic RAM) - это та самая обыкновенная «оперативка», которую ты держишь в руках в виде, например, DDR-модуля. Низкая цена и большие объемы - вот ее плюсы. Но из-за конструктивных особенностей данного вида памяти ячейкам каждые несколько миллисекунд нужна «подзарядка», иначе они все забудут и обнулятся. К тому же, нельзя прочитать следующий ряд ячеек, пока не будет полностью «закрыт» предыдущий. Поэтому там, где нужна скорость, используют SRAM (Static RAM), где у каждой ячейки есть положительная обратная связь, поддерживающая ее значение. А еще SRAM не требует сложного управления, и любой бит будет доступен строго в одно и то же, заранее известное время. Так что, эту память устанавливают туда, где нужна скорость, и не смущают высокие энергопотребление и стоимость.

Файловые системы

Для embedded-устройств и не только были изобретены специальные файловые системы, рассчитанные на малый размер носителей и на их природу.

  • YAFFS. Полноценная ФС, изобретенная специально для NAND-flash, учитывает ее особенности и рассчитана на то, чтобы минимизировать количество циклов перезаписи одной и той же ячейки. Умеет собирать «мусор» в виде выживших страниц из испорченных блоков и переносить их на новое место.
  • JFFS2. Плюсы - поддержка сжатия и жестких ссылок. Но в момент монтирования драйвер сканирует все дерево файлов в память, что создает некоторые проблемы со скоростью.
  • CramFS. Сжатая файловая система, из которой можно только читать, причем без предварительной распаковки, на уровне драйвера. Очень простая и эффективная, идеальна для ФС на встраиваемых устройствах. Если твое устройство не сохраняет никакие данные, то хранить в CramFS можно корневой раздел, а в /var монтировать временную ФС в оперативной памяти.
  • SquashFS. То же, что и CramFS, но использует алгоритм gzip вместо zlib, к тому же, сжимает все данные (в отличие от CramFS, где сжимаются только файлы, а метаданные остаются в чистом виде).

WWW

Наслаждайся информацией:

Содержание
ttfb: 6.2201023101807 ms