RISC-V и его ассемблер, часть 1

Чтобы скомпилировать компилятор, нужен компилятор

Те кто подписан на мой канал в Телеграме знают, что в начале года я решил приобщиться к миру встраиваемой электроники и приобрёл набор разработчика на базе микроконтроллера Мик32 Амур от Микрона. Тогда я сделал только первые шаги: настроил расширение Platform IO в Visual Studio Code и опробовал плату на самой примитивной программе мигания светодиодом. Но если уж начал погружаться в мир разработки электроники, то нужно спускаться на всю глубину. Ведь всё самое интересное там, куда мало кто заходит! В этом небольшом цикле статей я расскажу об изучении ассемблера Risc-V в контексте его применения на отечественных микроконтроллерах. Итак, что нам нужно, чтобы научить микроконтроллер делать что-то полезное? Ну, помимо кода программы, конечно. Прежде всего, необходимы компилятор с поддержкой нужной архитектуры и утилита прошивки через серийный порт. Также нам понадобится подходящая плата с микроконтроллером и программатор, чтобы её прошивать. Конечно, прошивать можно и через встроенный программатор, т.к. платы разработки чаще всего поддерживают такую функциональность. Но моя глобальная цель - научиться использовать как можно более полный инструментарий разработчика, в том числе и интерфейс отладки JTAG.

Установка всего необходимого

Что ж, прежде чем начать писать код, нужно подготовить всё необходимое. Утилиту OpenOCD можно установить прямо из репозитория, т.к. она универсальная и не зависит от целевой архитектуры:

sudo apt install openocd

А вот тулчейн не подойдёт - в репозитории он собран для линуксового таргета, а не для embedded. Тулчейн берём из репозитория xPack Binary Tools. Это тот самый тулчейн, который используется под капотом Platform IO, так что нет смысла выдумывать что-то другое. Скачиваем свежайший релиз под свою платформу и распаковываем в удобное место. Я буду использовать путь $HOME/Developer/Tools/riscv64. Теперь нужно загрузить всё необходимое для работы с МК Мик32 Амур. Создаём директорию mik32 и в неё клонируем набор для сборки приложений. В эту же директорию скачиваем утилиту заливки прошивки mik32-uploader из официального репозитория. В итоге должна получиться вот такая структура директорий:

~/Developer/Tools/riscv64 ❯ find -maxdepth 2 -type d
./xpack-riscv-none-elf-gcc-14.2.0-3
./xpack-riscv-none-elf-gcc-14.2.0-3/riscv-none-elf
./xpack-riscv-none-elf-gcc-14.2.0-3/lib64
./xpack-riscv-none-elf-gcc-14.2.0-3/bin
./xpack-riscv-none-elf-gcc-14.2.0-3/share
./xpack-riscv-none-elf-gcc-14.2.0-3/distro-info
./xpack-riscv-none-elf-gcc-14.2.0-3/lib
./xpack-riscv-none-elf-gcc-14.2.0-3/libexec
./xpack-riscv-none-elf-gcc-14.2.0-3/include
./mik32
./mik32/mik32v2-shared
./mik32/mik32-uploader

Для удобства дальнейшей работы я создал симлинк toolchain на директорию с gcc, чтобы не писать каждый раз так много букв. Кроме того, я завёл новую переменную окружения в .zshrc:

export RISCV_TOOLS_PATH="$HOME/Developer/Tools/riscv64"

В целом этого уже достаточно для сборки и прошивки программ для МК. Для более удобной работы скорее всего понадобятся дополнительные библиотеки HAL (Hardware Abstraction Layer), чтобы не работать напрямую с регистрами МК. Для МК Амур их можно взять в официальном репозитории, но мы же хотим писать на ассемблере, так что HAL нам ничем не поможет.

Пробуем собрать базовый пример

Но прежде чем перейти к интересному, нужно проверить, что наш сборочный тулчейн вообще работает. Для этого возьмём базовую программу мигания светодиодом из руководства по запуску платы Elbear. В своей рабочей директории проектов создаём новую директорию Blink с файлом blink.c внутри, а в него помещаем код примера:

#include <mik32_memory_map.h>
#include <pad_config.h>
#include <gpio.h>
#include <power_manager.h>

// Вывод, к которому подключен светодиод - PORT_2_7
#define PIN_LED 7

void InitClock() {
    // Включение тактирования GPIO
    PM->CLK_APB_P_SET |= PM_CLOCK_APB_P_UART_0_M | PM_CLOCK_APB_P_GPIO_0_M | PM_CLOCK_APB_P_GPIO_1_M | PM_CLOCK_APB_P_GPIO_2_M;

    // Включение тактирования блока для смены режима выводов
    PM->CLK_APB_M_SET |= PM_CLOCK_APB_M_PAD_CONFIG_M | PM_CLOCK_APB_M_WU_M | PM_CLOCK_APB_M_PM_M;
}

void ledBlink(const unsigned long delay_us) {
    // Инвертирование вывода
    GPIO_2->OUTPUT ^= 1 << PIN_LED;
    // Задержка
    for (volatile int i = 0; i < delay_us; ++i);
}

int main() {
    // Включение тактирования GPIO
    InitClock();

    // Установка вывода 7 порта 2 в режим GPIO
    PAD_CONFIG->PORT_2_CFG &= ~(0b11 << (2 * PIN_LED));
    // Установка направления вывода 7 порта 2 на выход
    GPIO_2->DIRECTION_OUT = 1 << PIN_LED;

    while (1) {
        // Моргание светодиода
        ledBlink(250000);
    }
}

Теперь попробуем этот код скомпилировать. Для этого нам потребуется указать дополнительные параметры компиляции:

  • указать архитектуру процессора и его настройки;
  • перечислить пути к директориям с заголовочными файлами;
  • указать дополнительные параметры линковки, чтобы учесть специфику адресного пространства МК;
  • добавить стартовый код инициализации МК из SDK – находится в mik32v2-shared/runtime/crt0.S. В итоге, чтобы не писать это всё вручную каждый раз, получился вот такой скрипт:
#!/bin/bash

if [[ -z "${RISCV_TOOLS_PATH}" ]]; then
  echo "RISC-V toolchain not found! Set the RISCV_TOOLS_PATH environment variable!"
  exit 1
fi

FILENAME="firmware.elf"

OPTS="-march=rv32imc_zicsr_zifencei -mabi=ilp32 -mcmodel=medlow -std=gnu11 -Os -s \
  -flto -fsigned-char -ffunction-sections -fdata-sections -fstrict-volatile-bitfields \
  -fno-strict-aliasing -fno-common -fno-builtin-printf -nostartfiles -ffreestanding"

DEFINES="-DMIK32V2 -DOSC_SYSTEM_VALUE=32000000L"

INCLUDES="-I${RISCV_TOOLS_PATH}/mik32/mik32v2-shared/include \
  -I${RISCV_TOOLS_PATH}/mik32/mik32v2-shared/periphery \
  -I${RISCV_TOOLS_PATH}/mik32/mik32v2-shared/libs \
  -I${RISCV_TOOLS_PATH}/mik32/mik32v2-shared/runtime"

LIBS="-L${RISCV_TOOLS_PATH}/mik32/mik32v2-shared/ldscripts"

WARNINGS="-Wall -Wextra"

CC="${RISCV_TOOLS_PATH}/toolchain/bin/riscv-none-elf-gcc ${OPTS} ${DEFINES} ${WARNINGS} ${INCLUDES} ${LIBS} \
  -Wl,-T,${RISCV_TOOLS_PATH}/mik32/mik32v2-shared/ldscripts/eeprom.ld,-Map,${FILENAME}.map,-gc-sections,--print-memory-usage \
  -lc ${RISCV_TOOLS_PATH}/mik32/mik32v2-shared/runtime/crt0.S \
  ${RISCV_TOOLS_PATH}/mik32/mik32v2-shared/libs/*.c"

echo ${CC} $@ -o ${FILENAME}
${CC} $@ -o ${FILENAME}

if [ $? -ne 0 ]; then
  echo "Abort."
  exit $?
fi

OBJ="${RISCV_TOOLS_PATH}/toolchain/bin/riscv-none-elf-objcopy"

echo "Extracting binary firmware..."
$OBJ -O ihex ${FILENAME} ${FILENAME%.*}.hex
$OBJ -O binary ${FILENAME} ${FILENAME%.*}.bin

echo "Done."

Этот скрипт я положил в директорию $RISCV_TOOLS_PATH/mik32 и назвал mik32-gcc, а также сделал симлинк в директорию ~/.local/bin, чтобы иметь возможность вызывать его из любого места. Теперь можно перейти в директорию нашего тестового проекта, в которой находится файл blink.c. Чтобы не захламлять директорию проекта, создаём в ней сборочную директорию, например build и переходим в неё. Для сборки прошивки вызываем нашу утилиту:

mik32-gcc ../blink.c

Если всё было настроено правильно, то в нашей сборочной директории появятся следующие файлы:

firmware.bin
firmware.elf
firmware.elf.map
firmware.hex

Теперь можно попробовать залить нашу прошивку на плату!

Медведь снова мигает огоньком

Для загрузки нашей прошивки на плату нужны всего два ингедиента (помимо самой прошивки): утилита OpenOCD и скрипт загрузки от Микрона. Команда загрузки прошивки тоже довольно длинная и содержит множество аргументов, так что удобнее будет использовать следующий скрипт:

#!/bin/bash

UPLOADER=${RISCV_TOOLS_PATH}/mik32/mik32-uploader

python3 ${UPLOADER}/mik32_upload.py \
  --run-openocd \
  --openocd-exec /usr/bin/openocd \
  --openocd-scripts ${UPLOADER}/openocd-scripts \
  --log-path openocd.log \
  --boot-mode spifi $1

Я точно так же создал симлинк ~/.local/bin/mik32-upload и теперь прошивку можно выполнить из директории build простой командой:

mik32-upload firmware.hex

Главное не забыть подключить плату через программатор к компьютеру. Тогда в случае успеха мы увидим следующий вывод:

mik32-uploader-v0.3.3
Using MIK32V2
Open On-Chip Debugger 0.12.0
Licensed under GNU GPL v2
For bug reports, read
        http://openocd.org/doc/doxygen/bugs.html
Info : set servers polling period to 200ms
Info : clock speed 500 kHz
Info : JTAG tap: riscv.cpu tap/device found: 0xdeb11001 (mfg: 0x000 (<invalid>), part: 0xeb11, ver: 0xd)
Info : JTAG tap: riscv.sys tap/device found: 0xfffffffe (mfg: 0x7ff (<invalid>), part: 0xffff, ver: 0xf)
Info : datacount=2 progbufsize=6
Info : Examined RISC-V core; found 1 harts
Info :  hart 0: XLEN=32, misa=0x40001104
Info : starting gdb server for riscv.cpu on 3333
Info : Listening on port 3333 for gdb connections
Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections
Info : accepting 'tcl' connection on tcp/6666
Clock init... OK!
MCU clock init...
Uploading driver... OK!
Uploading data...   OK!
Run driver...
EEPROM writing successfully completed!
[14:19:26] Wrote 896 bytes in 0.25 seconds (effective 3.5 kbyte/s)

В случае ошибок они будут выведены в консоль, также дополнительно можно посмотреть лог OpeOCD в файле openocd.log. Если же всё прошло хорошо, то мы увидим как плата Elbear замигала зеленым светодиодом! Теперь настройку сборочного тулчейна для микроконтроллера Амур можно считать законченой. Однако, всё самое интересное только начинается, ведь изначальный план был - научиться мигать светодиодом на ассемблере! Но об этом я расскажу в следующей части.