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

Orange Pi R1 Plus LTS
Orange Pi R1 Plus LTS

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

Схема RK3328 SOC
Схема 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 ведет себя как ячейка памяти, но с динамически меняющимися значениями.

Архитектура UART RK3328
Архитектура UART RK3328

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

 Memory-map для RK3328
Memory-map для RK3328
Регистры UART RK3328
Регистры UART RK3328

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

Memory-map для ESP32
Memory-map для 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.S
aarch64-linux-gnu-ld -Ttext 0x00200000 -o hello.elf hello.o
aarch64-linux-gnu-objcopy -O binary hello.elf hello.bin

*Примечание: адрес 0x00200000 используется, чтобы указать программе начальный адрес исполнения. Для правильной работы программу нужно будет загрузить именно по этому адресу.

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

В следующей части я расскажу, как программа попадает в память, о процессе загрузки и загрузчиках. Увидимся.

Полезные материалы:

[1]: Orange Pi R1 Plus LTS

[2]: Rockchip RK3328 Technical Reference Manual

[3]: ARMv8 Instruction Set