Embedded systems and linux

Embedded Linux в двух словах. Первое

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

Платформой послужит плата BeagleBone Black с процессором производства Техасских Инструментов AM3358 и ядром Arm Cortex-A8, и, чтобы не плодить мигающие светодиодами мануалы, основной задачей устройства будет отправка смайлов в топовый чат, широко известного в узких кругах, сайта, в соответствии с командами от смайл-пульта. Впрочем, без мигания светодиодами тоже не обошлось.

Итак, на столе лежит чистая, т.е. без каких-либо предустановленных дистрибутивов, плата BeagleBone Black, блок питания, переходник USB-UART. Для общения с платой, переходник нужно подключить к 6-ти выводному разъему, где первый вывод обозначен точкой — это GND, выводы 4/5 — RX/TX соответственно. После установки скорости в какой-либо терминальной программе, например putty, на 115200, можно взаимодействовать с платой, о подключении подробнее и с картинками здесь.

Топовые чаты, пульты и светодиоды будут позже, а сейчас на плату подается питание и плата отвечает CCCCCCCCCCC

В переводе с бутлоадерского это означает, что первичному загрузчику, зашитому в ROM процессора, нечего загружать. Ситуацию проясняет Reference Manual, где на странице 5025 в разделе 26.1.5 описана процедура начальной загрузки. Процедура такая: первичный загрузчик проводит некоторую инициализацию: тактирование процессора, необходимой периферии, того же UART, и, в зависимости от логических уровней на выводах SYSBOOT, строит приоритетный список источников где можно взять следующий загрузчик, т.е. посмотреть сначала на MMC карте, SPI-EEPROM или сразу ждать данных по Ethernet.

Я использую способ загрузки с помощью SD карты, вот что говорит об этом раздел RM 26.1.8.5.5 на странице 5057: первичный загрузчик сначала проверяет несколько адресов 0x0/ 0x20000/ 0x40000/ 0x60000 на наличие так называемой TOC структуры, по которой он может определить загрузочный код, если так код не найти, то первичный загрузчик, предполагая на SD карте наличие файловой системы FAT, будет искать там файл с названием MLO, как это расшифровывается в RM не сказано, но многие склоняются что Master LOader. Возникает резонный вопрос, где же взять этот MLO?

Das U-Boot

Das U-Boot или просто U-Boot — Universal Boot Loader, один из самых, если не самый, распространенный загрузчик для встроенных систем, именно с его помощью можно создать требуемый вторичный загрузчик (MLO), который будет загружать третичный загрузчик (сам U-Boot), который будет загружать ядро Linux.

Перед скачиванием U-Boot, стоит сходить в репозиторий и найти тег последней версии, далее

U-Boot содержит больше тысячи конфигураций, в том числе нужную:

Это конфигурация платы AM335x evaluation module, этот модуль лежит в основе других плат, в том числе BeagleBone Black, что можно видеть, к примеру, по Device Tree, но о нем позже. Настраивается и собирается U-Boot с помощью Kconfig, то же, что используется и при сборке ядра Linux.

Установка нужного конфига:

Можно, к примеру, убрать, установленную по умолчанию, 2-х секундную задержку при загрузке платы с U-Boot

Boot options —> Autoboot options —> (0) delay in seconds before automatically booting

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

ARM Toolchain

Один из видов кросскомпиляции это сборка на одной архитектуре, как правило x86-64, именуемой HOST, исходного кода для другой, именуемой TARGET. Например, для TARGET архитектуры ARMv7-A, ядра ARM CortexA-8 процессора AM3358, платы BeagleBone Black. К слову, чтобы не запутаться в ARM’ах, даже есть свой справочник, так их много и разных.

Сама сборка осуществляется набором инструментов — компилятор, компоновщик, runtime библиотеки, заголовочные файлы ядра; так называемый Toolchain. Toolchain можно собрать самостоятельно либо с помощью crosstool-NG, а можно взять готовый от компании Linaro, или самой ARM. Здесь я буду использовать Toolchain от ARM “GNU Toolchain for the A-profile Architecture Version 10.2-2020.11, x86_64 Linux hosted cross compilers, AArch32 target with hard float (arm-linux-none-gnueabihf)», если не вдаваться в излишние подробности, то это все означает, что набор инструментов будет работать на десктопной машине с Linux и собирать программы для 32-х битной ARM платформы с аппаратной поддержкой операций с плавающей запятой.

Теперь для успешной сборки U-Boot, нужно указать в переменных ARCH и CROSS_COMPILE требуемые архитектуру и путь к кросскомпилятору соответственно, например так

Либо использовать export ARCH/CROSS_COMPILE , чтобы каждый раз не набирать все это. Я, для наглядности, буду каждый раз набирать все это.

После сборки U-Boot, в папке появятся необходимые файлы, а именно

MLO — вторичный загрузчик (напомню, первичный зашит в самом процессоре)

u-boot.img — третичный загрузчик, собственно U-Boot

Для успешной загрузки с SD карты, нужно ее некоторым образом разметить. Карта должна содержать минимум два раздела, первый, отмеченный как BOOT, с файловой системой FAT, второй раздел с ext4. Разметить карту можно, к примеру, программой fdisk.

Теперь нужно просто скопировать результаты сборки U-Boot в FAT раздел, вставить карту в BeagleBone Black и в терминале наблюдать уже более осознанный ответ платы

В ответе платы есть такие строки

Failed to load ‘boot.scr’

Failed to load ‘uEnv.txt’

U-Boot, во время загрузки, смотрит наличие дополнительных команд, сначала в файле boot.scr, при его наличии, затем, если boot.scr не нашлось, в uEnv.txt. Эти файлы, помимо очередности при поиске, отличаются тем, что в файле uEnv.txt, дополнительные команды представлены в текстовом виде, т.е. он проще для восприятия и редактирования. U-Boot не создает файлы с дополнительными командами, делать это нужно самостоятельно.

Здесь происходят некоторые манипуляции в результате которых U-Boot загружает из SD карты в RAM по адресу [loadaddr] — образ ядра [zImage], и по адресу [fdtaddr] — дерево устройств [Flattened Device Tree]. Формируются аргументы, передаваемые ядру Linux, это параметры консоли, к которой подключен переходник USB-UART [console=ttyS0,115200n8], место размещения корневой файловой системы [bootpartition=mmcblk0p2], параметры разрешения на чтение/запись корневой файловой системы [rw], ее тип [ext4] и ожидание появления корневой файловой системы [rootwait]. Чтобы раскрутить всю цепочку действий U-Boot, можно, после того как U-Boot прекратит попытки найти что бы загрузить и выдаст приглашение на работу в виде =>, ввести команду printenv , она покажет значения всех переменных, которыми располагает U-Boot.

В завершении своей работы U-Boot, командой bootz , вместе с вышеуказанными аргументами и адресом дерева устройств, передает управление ядру Linux.

Ядро Linux

Прежде чем приступать к любым действиям с ядром, стоит заглянуть сюда и убедится в наличии необходимых пакетов. Следующим шагом нужно определиться с тем, какую версию ядра использовать. Здесь я использую версию 5.4.92 и вот по каким соображениям. Одной из основных причин того, что не стоит брать просто последнюю версию ядра, доступную на данный момент, наряду с наличием драйверов, является невозможность быстро протестировать это ядро на всем разнообразии платформ поддерживаемых Linux, а значит можно потратить кучу сил и времени на исправление неполадок, если что-то пойдет не так, и не факт что это вообще получится сделать. BeagleBone Black имеет официальный репозиторий, где можно найти версию ядра, протестированную на данной платформе, и long term версия 5.4.92 была последней на тот момент.

Читайте также:  Creative audigy audio processor wdm windows 10 64 bit

Нужный конфиг, расположенный в /arch/arm/configs, называется omap2plus_defconfig, OMAP — это название линейки процессоров, продолжением которых является AM3358, впринципе, подойдет и более общий multi_v7_defconfig.

Сам конфиг пока остается без изменений, поэтому можно просто его установить и запустить компиляцию ядра(zImage), модулей(modules) и дерева устройств(dtbs)

Проходит некоторое время.

Результат сборки, в виде zImage, находится в /arch/arm/boot, там же в папке /dts находится скомпилированное дерево устройств am335x-boneblack.dtb, оба отправляются на SD карту к файлам загрузчика. На этом FAT раздел SD карты можно считать скомплектованным. Итого, там присутствуют:

MLO — вторичный загрузчик

u-boot.img — третичный загрузчик

uEnv.txt — дополнительные команды загрузчика

zImage — образ ядра Linux

am335x-boneblack.dtb — скомпилированное дерево устройств платы

Еще при сборке ядра заказывались модули ядра, но они уже относятся к корневой файловой системе.

Корневая файловая система. BusyBox

Ядро получает корневую файловую систему путем монтирования блочного устройства, заданного в, переданном при запуске ядра, аргументе root=, и далее, первым делом, исполняет оттуда программу под названием init.

Если запустить BeagleBone Black, имея только вышеуказанные файлы для FAT раздела, то ядро будет паниковать по причине отсутствия init и, в целом, по причине пустой rootfs, т.е. корневой файловой системы.

Можно шаг за шагом создать все минимально необходимые компоненты корневой файловой системы, такие как оболочка, различные демоны запускаемые init, сам init, конфигурационные файлы, узлы устройств, псевдофайловые системы /proc и /sys и просто системные приложения. Для желающих совершать подобные подвиги, существует проект Linux From Scratch, здесь же я воспользуюсь швейцарским ножом встроенных систем с Linux, утилитой BusyBox.

Скачивание последней, на тот момент, версии:

Настройка конфигурации по умолчанию:

Чтобы не думать сейчас о разделяемых библиотеках, стоит установить статическую сборку BusyBox:

Settings —> Build static binary (no shared libs)

Установка в папку по умолчанию _install:

Теперь в папке _install можно видеть будущую корневую файловую систему, в которую нужно добавить некоторые вещи.

Папки, помимо созданных BusyBox:

Стартовый скрипт. Дело в том, что, запускаемая в первую очередь, программа init, делает много полезного, например, выводит в консоль приглашение, но до выдачи приглашения, init проверяет наличие стартового скрипта /etc/init.d/rcS, и, при наличии, запускает его.

Этот скрипт монтирует псевдофайловые системы proc и sysfs, и ничего не мешает ему запускать, к примеру, пользовательскую программу, отвечающую за функционал устройства, но лучше будет делать это в отдельных скриптах, скомпонованных по функциональному назначению.

Стоит сказать, что работа init, на самом деле, начинается с чтения конфигурационного файла /etc/inittab, но BusyBox’овская init включает таблицу inittab по умолчанию, если таковой не окажется в корневой файловой системе.

Теперь пора вспомнить про модули ядра. Их также нужно разместить в корневой файловой системе в /lib/modules/5.4.92/, но сейчас они разбросаны по всей папке в которой собиралось ядро. Чтобы собрать модули в кучу, нужно в папке с ядром выполнить

Где в INSTALL_MOD_PATH указать путь к папке с корневой файловой системой, кросскомпилятор указывать не нужно, т.к. здесь модули ядра просто копируются по месту назначения. В результате папка /lib корневой файловой системы пополнится разделом /lib/mudules/5.4.92/ содержащим модули ядра, полученные при компиляции ядра.

Осталось скопировать все содержимое папки _install во второй раздел SD карты, тот который с ext4, и поменять владельца всего содержимого на root.

После запуска BeagleBone Black с корневой файловой системой, через 1.910315 секунды после старта ядра, система предложит активировать консоль и начать работу.

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

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

Источник

Mastering Embedded Linux, Part 1: Concepts

One of my favorite hobbies is hacking low-cost embedded systems that run Linux. These systems are absolutely everywhere: the combination of powerful yet inexpensive processors, mass production of consumer goods, and the flexibility of Linux means that hobbyists can buy or build an embedded device capable of running Linux, often for less than $10.

I’ll say that again: not only is embedded Linux within reach for makers and hobbyists, it’s also cheap to throw into your next design.

I’d like to show you just how diverse this ecosystem is, and then I’d like to empower you to start tinkering with these systems. This article series will walk you through choosing hardware and compiling your first system image from source code. Then, we’ll dive deeper into how to pick components for building a custom Linux board, how to deliver updates in a reliable, production-ready way, and how to customize the Linux kernel.

In short, this series of articles will get you up and running with the embedded Linux ecosystem. Ideally you should have some Linux command line knowledge, a good understanding of the overall components of an embedded system (flash memory, processor, peripherals), and a good chunk of free time for learning and experimenting.

Let’s start with some high-level concepts. Having a big-picture view will help prevent being overwhelmed when we start going into more detail later.

Embedded Linux concepts

If you’re already familiar with microcontrollers, you’re by-and-large used to having everything in a single package. You buy an STM32F1 or whatever, and you get flash program memory, RAM, a processor core or two, and some peripherals. If you buy a part specifically designed for it, you might get one or two really nice peripherals tailored to an application — built-in Bluetooth, USB 3.0, or a MIPI camera interface. The device probably has a development studio in which you write your code and send it over to run “bare metal” without a real operating system to get in its way. Finally, all this tends to come in a fairly hobbyist-friendly package: SOIC or QFP packages are usually available for at least one part in a microcontroller line.

The architecture is a little different with Linux-capable processors. Typically, these processors cram in nearly every peripheral you can think of, and many you haven’t heard of. In exchange, they frequently lack built-in RAM, and always lack storage. On top of that, starting one of these devices up is a more involved setup: the processor typically has a built-in boot ROM that is responsible only for loading your bootloader.

Читайте также:  Linux tar скрытые файлы

From there, the bootloader will load Linux, which will configure the rest of the peripherals and run a preset series of programs. Networking, application logic, and user interface are all available, just like a bigger Linux system. As far as the application is concerned, the environment looks identical to a desktop Linux system — all the APIs are the same, the filesystem looks normal, and it can talk to the outside world using normal networking protocols.

All this is stored in a tiny system image that can be as small as 4MB, depending on the needs of the device. But this is a key point, so I’ll say it again: every software component running on Linux is nearly identical to the version you’d run on a desktop! Linux provides all the abstraction your applications need, so your job is mostly reduced to getting the bootloader and Linux kernel ported to your hardware.

Components

Almost every one of these components is replaceable. The choices made for each of these constitute a large portion of the design decisions you make while choosing an embedded system.

Microprocessor

Of course, your processor will determine a lot of your system’s capabilities. Most of the small, cheap embedded Linux systems you’ll be interacting with will either be using an ARM or MIPS core, and most of the industry is moving further and further toward ARM. (Keep your eye on RISC-V; it’s an open-source instruction set architecture that isn’t quite ready for prime time but has already seen a lot of industry interest.)

On top of the architecture, each silicon vendor adds many different peripherals. Much like their microcontroller brethren, these are small bits of silicon that do one thing, like USB or SPI. They are often configured via memory-mapped registers, and often, manufacturers will add more capabilites to a new product (a “part”) by copy-pasting peripheral IP from an older part, or by adding more copies of the peripheral to the new part.

Memory

These processors typically don’t ship with memory. This is provided separately. The usual kinds of memory you might be familiar with apply here: SDRAM is often used on the low end, while DDR, DDR2, and DDR3 are used for more powerful parts. Although I haven’t yet seen a whole lot of devices using DDR4, I’m sure we’ll get there. The processor provides a module to manage the memory; this module is either initialized by the manufacturer’s boot code or by the first stage of the bootloader.

Note how I said processors typically don’t ship with RAM embedded in their package? Sometimes, they do. If you’re buying an embedded system on a module, or a ready-to-go development board, you don’t care about this, because someone else has already done the hard work of putting the memory on the PCB.

But if you are a hobbyist building your own from individual parts, you care very deeply about these parts (whether or not you know it yet). That’s because laying memory out on a PCB is difficult for a few different reasons. So when I’m talking about hardware in my next article, I’ll be sure call out some parts I’ve found that ship their own memory.

Storage

Storage, distinct from memory, is the involatile place your code and data is stored. On embedded systems, it will almost certainly be some form of flash memory. Even in cases where the system boots from the network, typically this is still assisted by a bootloader stored in flash.

There are multiple kinds of flash memory. You are likely already familiar with SD or microSD cards. These provide raw storage, plus a flash controller whose job is to abstract the raw flash into a well-behaved storage medium (more on this in a second). SD cards are not known for their reliability, as anybody who has used a Raspberry Pi can tell you. However, later in this series, I’ll explain how best to work around questionably reliable storage media.

eMMC is an embeddable version of an SD card. Conceptually it is very similar; it has raw flash and a controller bundled into a single part. Unfortunately it is usually sold in nasty BGA packages like is pictured, which aren’t a problem for large companies but are pretty much unusable for hobbyists because you can’t easily solder it.

You can also buy raw flash without a controller attached to it. This is typically cheaper than eMMC and is sold in two varieties, NOR and NAND. NOR flash is slow to write, very cheap, and not very dense. Boot ROMs almost always know how to read from NOR flash accessed over SPI. If you’re not writing very often, maybe only once to burn firmware to the device, it’s a great option. NAND flash is faster, denser, and a little more expensive. You can buy NAND that’s accessed via SPI, but there are also faster parts that can be accessed over a dedicated NAND bus.

Raw flash isn’t really all that nice. You cannot write to it willy-nilly; flash can only be written once before you must erase it in large blocks. Furthermore, flash can be written a limited number of times before it wears out (between 1000 and 100000, depending on the technology).

A software guy’s first instinct is to throw software at a hardware problem, and that’s what we’ll do here, later on. A Linux subsystem called UBI can help work around these limitations and make raw flash nicer to use.

Software

If you’re developing for one of these systems, you provide the bootloader, kernel, and filesystem. Thankfully, once Linux has booted, it often supports many of the peripherals on a system out of the box using a wonderfully consistent set of driver interfaces — in stark contrast to how often you must write drivers yourself on a microcontroller.

In general the software typically follows a very understandable pattern that you can apply anywhere.

You don’t have to micromanage all these pieces at once. There are embedded Linux distributions that provide a complete toolkit to help you build a firmware image with all of this included. Later on, we’ll start using a distribution and compile our own complete system.

Bootloader

The bootloader is the first program that the engineer or hacker has control over. It is only as complex as is needed to load the kernel and get it running. In practice, the bootloader can still be pretty complex.

Your embedded Linux system will almost certainly be using Das U-Boot, the so-called “universal bootloader.” (It really does run on nearly everything!) The bootloader has stripped-down drivers for the onboard storage, perhaps a couple of other peripherals, and just enough code to read the kernel into memory and start executing it.

Читайте также:  Javascript in windows forms

A lot of times, people don’t mess with the bootloader that comes with their board, and they just follow the conventions it’s expecting. That’s fine, but often you want your system to do something the stock bootloader can’t. With the right tools, you have no reason to be afraid of modifying the bootloader — it is a program like any other.

Boot ROM

I should mention the Boot ROM here. The Boot ROM is a small chunk of code embedded in the processor, provided by the manufacturer. It is very, very simple, and typically it immediately runs the real bootloader from a couple different storage media — often this “boot order” is specified in the processor’s datasheet.

Boot ROMs have one other cool trick — they often speak USB! This allows you to connect a brand-new, unprogrammed system to a computer over USB and run code, or flash a storage medium. If it’s present, this feature makes a board nearly impossible to brick. Different manufacturers call this mode different things. NXP/Freescale calls it Download Mode. Allwinner calls it FEL mode. You will generally need a special program on your workstation to use this mode, and capabilities vary between different processors.

Kernel

You already know what Linux is, I hope! Linux must be ported to each architecture, each part, and each board. All these drivers ship with the kernel source. There’s a lot of them, but because of the tendency of manufacturers to reuse IP, it’s managable. Typically, a port consists of the following:

  • Architecture code provides low-level routines for very basic things like register manipulation, synchronization primitives, etc. Porting Linux to a completely new architecture is an immense amount of work, and you will likely not have to do this anytime soon.
  • Drivers make up the bulk of the kernel source code. This is because Linux ships drivers for every device supported by Linux, in one source code tree. Most of them are not needed by your embedded system — for example, a MIPS router has absolutely no use for a driver for Intel QuickSync. So most of these drivers are not even compiled for very small systems.
  • The device tree is a very important part of the port that explains to the kernel how hardware is actually connected to the system. Device trees are the “config files” for Linux drivers. This means that Linux drivers are easy to reuse — if you add a device tree entry for a piece of hardware on your system and compile the relevant driver, the driver will find the entry and set itself up. We’ll talk a lot about device trees later.

Userland

Like I mentioned already, the “rest” of the software on an embedded system is pretty much identical to its desktop counterparts. Together, this “everything else” is frequently called “user land,” because it’s the land where user software can roam freely, without worries of the nasty hardware gremlins that lurk underneath the peaceful abstraction of the Linux API.

We’ll go over userland in more detail later. The major components you need to know right now are the filesystem, the init system, and the shell.

The filesystem is important because some are better for embedded systems than others — remember the SD card reliability we talked about? Filesystems can help with this. If the system is using eMMC or an SD card, it looks like a normal “block device,” and you can use the standard ext2/3/4 or stuff like the flash-friendly filesystem (f2fs), which helps with reliability in various ways. If you’re on raw flash, you need to use more esoteric filesystems that are designed for raw flash, such as JFFS2 or UBIFS. My personal favorite is to use squashfs on an UBI partition; we’ll talk more about this when we start customizing our firmware image!

Next, the init system is responsible for managing userland. You might have heard of systemd? It’s used on bigger embedded systems. However, it’s too big for the really small systems, which usually use a SysV init scheme of simple shell scripts. Most embedded Linux distributions provide this, and again, when we’re customizing firmware, I’ll show you how to add your programs to run automatically.

And finally, the shell is what you’ll interact with. Traditionally, this is done over a UART serial connection, but occasionally you might have the luxury of a screen. If you get your system to a shell prompt, you have definitely gotten your system up and running!

Next steps

Okay. That’s a lot of info all at once. Thanks for staying with me!

With that out of the way, here’s my rough plan for the series:

  • Hardware: We’ll choose a development board. Spoilers: I’ll use a Raspberry Pi Zero for demo for the first few software tutorials. But here, I’ll also point out various other cheap Linux-capable hardware. If you’re interested in building your own, I am also recommending hacker-friendly parts that you can add to your own PCB.
  • Buildroot: For a quick win, we’ll download Buildroot and compile a complete operating system from scratch. We’ll flash it and boot it on real hardware. I’ll be sure to explain what’s happening at each step.
  • Customizing the firmware image: here we’ll depart from what the Buildroot developers’ defaults and begin to make changes to the Raspberry Pi’s firmware. Each individual step is pretty straightforward, and all combined, your finished system can look completely different from the stock one. It’s all up to the engineer.
  • Going smaller: We leave the sunny skies of Raspberry Pi land and start working on an image for a very small board with just 4MB of storage. It’s quite impressive how much functionality you can cram in—take that, node_modules!
  • Hacking on U-Boot and Linux: The available code for your board’s processor is out of date. What do you do?
  • Building a board from scratch: We’ll go back to the parts I recommend in the Hardware stage and start building our very own dev board. (This part is subject to change as I figure out the best approach!)

In the next article we’ll look around at some hardware. If you’d like to follow along, you can subscribe.

In the meantime, if you’d like to Linux-enable your project, product, or program, leave a comment or drop me a line—I’d love to hear from you and I’m always happy to hear about cool stuff.

Источник

Оцените статью