Привет, Хабр! В этом цикле статей я попытаюсь наглядно и сжато объяснить устройство встраиваемых систем на базе Rockchip. Пройдусь по всем шагам загрузки, начиная с первой инструкции и заканчивая разворачиванием всей системы. Для демонстрации я выбрал плату Orange Pi R1 Plus LTS на базе Rockchip RK3328 SoC, ARM Cortex-A53 64-Bit Processor.

Внутрь RK3328 SOC встроены различные контроллеры периферии, памяти, графики и другие.

Общение процессора (CPU Cortex-A53) с периферией основано на обращении к памяти. Для этого процессор использует Memory-Mapped I/O (MMIO). Модель памяти процессора условно разделена на сектора, часть из которых зарезервировано под периферию.
Для примера возьмем интерфейс UART. Записывая и считывая данные по определенному адресу, можно общаться с физическим устройством — контроллером UART. Общение происходит по набору шин, к которому подключены все доступные процессору устройства.

На самом деле архитектура шин RK3328 имеет более сложное устройство и отличается от схемы выше. В RK3328 используются не одна, а несколько оптимизированных шин для различных видов устройств, а также внутренние протоколы передачи данных. RK3328 использует набор шин AMBA (Advanced Microcontroller Bus Architecture). Внутри этого набора существуют:
AXI (Advanced eXtensibility Interface) — основная скоростная магистраль.
AHB (Advanced High-performance Bus) — шина средней производительности.
APB (Advanced Peripheral Bus) — медленная периферийная шина.
Для подробного ознакомления можно посмотреть Technical Reference Manual для RK3328. Вот так выглядит схема контроллера UART: он подключен к медленной ABP-шине, которая, в свою очередь, соединена с быстрой AXI. На схеме можно увидеть ресивер и трансивер, отвечающие за физическое кодирование и декодирование информации, блоки буфера и тактирования. Всё это выводится в блок регистров для взаимодействия с CPU через интерфейс ABP-шины. Регистры — это и есть те адреса памяти, по которым приходят запросы от CPU. Благодаря им контроллер UART ведет себя как ячейка памяти, но с динамически меняющимися значениями.

Ознакомиться с адресами и регистрами памяти также можно прочитав Technical Reference Manual для RK3328. В разделе Address Mapping можно увидеть, что UART1 имеет адрес FF13_0000, а также регистры, адреса которых имеют смещение относительно базового адреса.


Для сравнения, ниже приведен пример распределения адресов памяти для другого процессора — Xtensa на базе ESP32.

На карте видно, что через адреса памяти процессор получает доступ к внешней памяти (external flash/sram). Для преобразования виртуального адреса в физический используется Memory Management Unit (MMU). Получая запрос на определенный адрес, он конвертирует его во внутренний физический адрес хранилища, читает данные и отдает их обратно, как если бы это был настоящий адрес памяти.
Для каждой архитектуры процессора есть свой ассемблер. В данном случае процессор Cortex-A53 имеет архитектуру ARM V8-A. Архитектура ARM изначально создавалась для использования в мобильных устройствах и встраиваемых системах, поэтому набор команд и регистров облегчен по сравнению с x86. В качестве демонстрации попробуем написать программу для этой архитектуры.
.section .text
.global _start
.equ UART_BASE, 0xff130000 // Базовый адрес UART2
.equ UART_THR, 0x00 // Отступ регистра передачи
.equ UART_LSR, 0x14 // Отступ регистра состояния линии
.equ LSR_THRE, (1 << 5) // Маска регистра говности к приему (5 бит)
_start:
adr x0, msg // Загрузка сообщения в относительный адрес памяти
print_loop:
ldrb w1, [x0], #1 // Загрузить 1 байт сообщения из x0 в w1, затем увеличить x0 на 1
cbz w1, done // Проверить w1 на конец строки (0)
wait_uart:
ldr x2, =UART_BASE // Поместить в x2 базовый адрес UART2
ldr w3, [x2, #UART_LSR] // Записать в w3 значение регистра UART_LSR
tst w3, #LSR_THRE // Если значение UART_LSR true, UART готов к приему
beq wait_uart // Если значение UART_LSR false повторить
str w1, [x2, #UART_THR] // Записать значение w1 в регистр передачи
b print_loop // Запустить функцию заново
done:
ret // Возврат
.section .data
msg:
.asciz "Hello from RK3328!\r\n" //Строка, заканчивющаяся 0В коде выше рассмотрено общение с контроллером UART. Контроллер осуществляет физическое кодирование и декодирование информации, содержащейся в его регистрах, постоянно их обновляя и выставляя флаги готовности.
Скомпилируем программу, используя компилятор aarch64-linux-gnu-as
aarch64-linux-gnu-as -o hello.o hello.Saarch64-linux-gnu-ld -Ttext 0x00200000 -o hello.elf hello.oaarch64-linux-gnu-objcopy -O binary hello.elf hello.bin
*Примечание: адрес 0x00200000 используется, чтобы указать программе начальный адрес исполнения. Для правильной работы программу нужно будет загрузить именно по этому адресу.
Теперь у нас есть бинарный файл, то есть набор инструкций, понятных процессору, для взаимодействия с контроллером UART. Для того чтобы процессор мог прочитать этот набор инструкций, его нужно загрузить в память. Программы, как правило, работают в оперативной памяти. За инициализацию оперативной памяти и запись в нее программы отвечает загрузчик.
В следующей части я расскажу, как программа попадает в память, о процессе загрузки и загрузчиках. Увидимся.
Полезные материалы:
