VPR (произносится как Viper) — первый процессор компании Nordic Semiconductor на архитектуре RISC-V, представленный в новых версиях nRF54H и nRF54L линеек SoC после их первоначальных анонсов в апреле и октябре 2023 года соответственно. Читателям этого блога знаком мой давний, одержимый интерес к RISC-V (см. мои серии статей Советы по RISC-V и Байты RISC-V.) Тем не менее внедрение процессора RISC-V компанией Nordic особенно интересно для меня, ведь их линейка микроконтроллеров крайне популярна в беспроводных доменах с низким энергопотреблением. А это обычный случай применения для клиентов Golioth.

Естественно, мне хотелось изучить VPR в деталях по мере поступления дополнительной информации. Базовое описание регистров и инициализации можно найти в документации nRF54L. VPR — процессор RV32E. Это означает, что он использует 32-битные регистры, но реализует только младшие 16 регистров, требуемые спецификацией встраиваемости (E — embedded), а не 32, определенные спецификацией полного 32-битного целого числа (RV32I). В процессоре также реализованы операции умножения и деления (M) и расширение сжатых инструкций (C), которое добавляет поддержку 16-битных вариантов инструкций, чтобы повысить плотность кода. Все инструкции выполняются в машинном режиме (M) — единственном реализованном уровнем привилегий.

Помимо прикладного процессора Arm Cortex-M33, микроконтроллеры nRF54L содержат один процессор VPR, называемый быстрым легковесным периферийным процессором (FLPR, произносится как flipper — «флиппер»). Тогда как nRF54H20 включает в себя и FLPR, и периферийный процессор (PPR, произносится как pepper — пеппер) вместе с двухъядерным приложением Arm Cortex-M33 и сетевыми процессорами. PPR предназначен для периферийного обслуживания с низким энергопотреблением и работает на частоте 16 МГц. FLPR работает на частоте 320 МГц и предназначен для программно-определяемых периферийных устройств.

Процессоры VPR должны быть настроены и запущены управляющим процессором приложений. Эти процессы заключаются в установке счетчика программ VPR в регистре INITPC и последующей записи в регистр CPURUN, чтобы запустить процессор. В nRF54H20 PPR VPR выполняет код из медленной глобальной оперативной памяти (RAM3x). Поэтому управляющий процессор перед обновлением счетчика программ и запуском процессора VPR должен скопировать код в соответствующую область.

Ранее я писал о процессе загрузки Zephyr и особенно об инициализации оборудования. Zephyr использует devicetree, чтобы описать оборудование и периферийные устройства поддерживаемых процессоров и плат. Во включаемом файле дерева устройств — nrf54h20.dtsi есть узел, представляющий процессор PPR VPR.

        cpuppr: cpu@d {
            compatible = "nordic,vpr";
            reg = <13>;
            device_type = "cpu";
            clocks = <&fll16m>;
            clock-frequency = <DT_FREQ_M(16)>;
            riscv,isa = "rv32emc";
            nordic,bus-width = <32>;

            cpuppr_vevif_rx: mailbox {
                compatible = "nordic,nrf-vevif-task-rx";
                status = "disabled";
                interrupt-parent = <&cpuppr_clic>;
                interrupts = <0 NRF_DEFAULT_IRQ_PRIORITY>,
                         <1 NRF_DEFAULT_IRQ_PRIORITY>,
                         <2 NRF_DEFAULT_IRQ_PRIORITY>,
                         <3 NRF_DEFAULT_IRQ_PRIORITY>,
                         <4 NRF_DEFAULT_IRQ_PRIORITY>,
                         <5 NRF_DEFAULT_IRQ_PRIORITY>,
                         <6 NRF_DEFAULT_IRQ_PRIORITY>,
                         <7 NRF_DEFAULT_IRQ_PRIORITY>,
                         <8 NRF_DEFAULT_IRQ_PRIORITY>,
                         <9 NRF_DEFAULT_IRQ_PRIORITY>,
                         <10 NRF_DEFAULT_IRQ_PRIORITY>,
                         <11 NRF_DEFAULT_IRQ_PRIORITY>,
                         <12 NRF_DEFAULT_IRQ_PRIORITY>,
                         <13 NRF_DEFAULT_IRQ_PRIORITY>,
                         <14 NRF_DEFAULT_IRQ_PRIORITY>,
                         <15 NRF_DEFAULT_IRQ_PRIORITY>;
                #mbox-cells = <1>;
                nordic,tasks = <16>;
                nordic,tasks-mask = <0xfffffff0>;
            };
        };

Определены ранее упомянутые атрибуты, такие как поддержка rv32emc RISC-V ISA, и тактовая частота 16 МГц (DT_FREQ_M(16)). Существует также узел в soc, определяющий PPR как периферийное устройство сопроцессора.

            cpuppr_vpr: vpr@908000 {
                compatible = "nordic,nrf-vpr-coprocessor";
                reg = <0x908000 0x1000>;
                status = "disabled";
                #address-cells = <1>;
                #size-cells = <1>;
                ranges = <0x0 0x908000 0x1000>;
                power-domains = <&gpd NRF_GPD_SLOW_ACTIVE>;

                cpuppr_vevif_tx: mailbox@0 {
                    compatible = "nordic,nrf-vevif-task-tx";
                    reg = <0x0 0x1000>;
                    status = "disabled";
                    #mbox-cells = <1>;
                    nordic,tasks = <16>;
                    nordic,tasks-mask = <0xfffffff0>;
                };
            };

Свойство compatible связывает PPR с соответствующим драйвером, который управляющий процессор, — в данном это случае процессор приложений nRF54H20 (cpuapp), — может использовать для его настройки и запуска. В составе прочих драйверов есть nordic_vpr_launcher, который определяет совместимость устройства.

#define DT_DRV_COMPAT nordic_nrf_vpr_coprocessor

Драйвер относительно прост: он отвечает только за инициализацию устройства. Функция инициализации выполняет операции, описанные в документации.

static int nordic_vpr_launcher_init(const struct device *dev)
{
    const struct nordic_vpr_launcher_config *config = dev->config;

#if DT_ANY_INST_HAS_PROP_STATUS_OKAY(source_memory)
    if (config->size > 0U) {
        LOG_DBG("Loading VPR (%p) from %p to %p (%zu bytes)", config->vpr,
            (void *)config->src_addr, (void *)config->exec_addr, config->size);
        memcpy((void *)config->exec_addr, (void *)config->src_addr, config->size);
    }
#endif

#if defined(CONFIG_SOC_NRF54L_CPUAPP_COMMON) && !defined(CONFIG_TRUSTED_EXECUTION_NONSECURE)
    nrf_spu_periph_perm_secattr_set(NRF_SPU00, nrf_address_slave_get((uint32_t)config->vpr),
                    true);
#endif
    LOG_DBG("Launching VPR (%p) from %p", config->vpr, (void *)config->exec_addr);
    nrf_vpr_initpc_set(config->vpr, config->exec_addr);
    nrf_vpr_cpurun_set(config->vpr, true);

    return 0;
}

Различные функции nrf_vpr_* предоставляются в hal_nordic как inline — встроеные. Конфигурация устройства, которая определяет используемые средством запуска адреса источника и назначения памяти, инстанцируются с использованием различных макросов DT_* дерева устройств. Эти макросы выполняются для каждого совместимого узла с помощью DT_INST_FOREACH_STATUS_OKAY.

/* получить адрес VPR либо из памяти, либо из раздела */
#define VPR_ADDR(node_id)                                                                          \
    (DT_REG_ADDR(node_id) +                                                                    \
     COND_CODE_0(DT_FIXED_PARTITION_EXISTS(node_id), (0), (DT_REG_ADDR(DT_GPARENT(node_id)))))

#define NORDIC_VPR_LAUNCHER_DEFINE(inst)                                                           \
    IF_ENABLED(DT_INST_NODE_HAS_PROP(inst, source_memory),                                     \
           (BUILD_ASSERT((DT_REG_SIZE(DT_INST_PHANDLE(inst, execution_memory)) <=          \
                  DT_REG_SIZE(DT_INST_PHANDLE(inst, source_memory))),              \
                 "Execution memory exceeds source memory size");))                 \
                                                                                                   \
    static const struct nordic_vpr_launcher_config config##inst = {                            \
        .vpr = (NRF_VPR_Type *)DT_INST_REG_ADDR(inst),                                     \
        .exec_addr = VPR_ADDR(DT_INST_PHANDLE(inst, execution_memory)),                    \
        IF_ENABLED(DT_INST_NODE_HAS_PROP(inst, source_memory),                             \
               (.src_addr = VPR_ADDR(DT_INST_PHANDLE(inst, source_memory)),            \
                .size = DT_REG_SIZE(DT_INST_PHANDLE(inst, execution_memory)),))};      \
                                                                                                   \
    DEVICE_DT_INST_DEFINE(inst, nordic_vpr_launcher_init, NULL, NULL, &config##inst,           \
                  POST_KERNEL, CONFIG_NORDIC_VPR_LAUNCHER_INIT_PRIORITY, NULL);

DT_INST_FOREACH_STATUS_OKAY(NORDIC_VPR_LAUNCHER_DEFINE)

Мы можем увидеть инициализацию в действии, собрав приложения как для процессора приложений, так и для PPR. [Инструмент сборки] Zephyr sysbuild позволяет создавать несколько целевых объектов. Пример hello_world демонстрирует это сборкой приложения, которое просто выводит Hello world from {target} для всех указанных целей.

west build -p -b nrf54h20dk/nrf54h20/cpuapp -T sample.sysbuild.hello_world.nrf54h20dk_cpuapp_cpuppr .

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

  sample.sysbuild.hello_world.nrf54h20dk_cpuapp_cpuppr:
    platform_allow:
      - nrf54h20dk/nrf54h20/cpuapp
    integration_platforms:
      - nrf54h20dk/nrf54h20/cpuapp
    extra_args:
      SB_CONF_FILE=sysbuild/nrf54h20dk_nrf54h20_cpuppr.conf
      hello_world_SNIPPET=nordic-ppr

SB_CONF_FILE определяет конфигурацию sysbuild. Для комплекта разработки nRF24H20 (DK) файл конфигурации служит, только чтобы указать целевую плату варианта приложения REMOTE, которым является PPR.

SB_CONFIG_REMOTE_BOARD="nrf54h20dk/nrf54h20/cpuppr"

hello_world_SNIPPET указывает фрагмент, который следует включить в сборку. Он включает в себя два файла наложения дерева устройств. Один, nordic-ppr.overlay, включает узел cpuppr_vpr для любых плат, включающих PPR.

&cpuppr_vpr {
    status = "okay";
};

Внимательные читатели заметят, что предстоящий nRF9280 от Nordic также содержит PPR.

Второй, nrf54h20_cpuapp.overlay, включает область памяти, которая совместно используется PPR и процессором приложения.

&cpuppr_ram3x_region {
    status = "okay";
};

После сборки образ процессора приложения будет находиться в build/hello_world, а образ PPR — в build/remote. Можно использовать соответствующие тулчейны Arm и RISC-V из Zephyr SDK, чтобы проверить файлы ELF и понять, как конфигурация преобразуется в инструкции в бинарном файле.

Чтобы дизассемблировать образ процессора приложения, используйте objdump из тулчейна arm-zephyr-eabi.

$ZEPHYR_SDK_PATH/arm-zephyr-eabi/bin/arm-zephyr-eabi-objdump -D build/hello_world/zephyr/zephyr.elf

Для образа PPR можно использовать вариант riscv64-zephyr-elf.

$ZEPHYR_SDK_PATH/riscv64-zephyr-elf/bin/riscv64-zephyr-elf-objdump -D build/remote/zephyr/zephyr.elf

В показанной ранее инициализации драйвера nordic_vpr_launcher макрос DEVICE_DT_INST_DEFINE используется для определения периферийного устройства PPR с уровнем инициализации POST_KERNEL. Это означает, что устройство должно быть инициализировано после загрузки ядра. Это обрабатывается вызовом z_sys_init_run_level с уровнем POST_KERNEL.

static void z_sys_init_run_level(enum init_level level)
{
    static const struct init_entry *levels[] = {
        __init_EARLY_start,
        __init_PRE_KERNEL_1_start,
        __init_PRE_KERNEL_2_start,
        __init_POST_KERNEL_start,
        __init_APPLICATION_start,
#ifdef CONFIG_SMP
        __init_SMP_start,
#endif /* CONFIG_SMP */
        /* Маркер конца */
        __init_end,
    };
    const struct init_entry *entry;

    for (entry = levels[level]; entry < levels[level+1]; entry++) {
        const struct device *dev = entry->dev;
        int result;

        sys_trace_sys_init_enter(entry, level);
        if (dev != NULL) {
            result = do_device_init(entry);
        } else {
            result = entry->init_fn.sys();
        }
        sys_trace_sys_init_exit(entry, level, result);
    }
}

Дизасемблированный код можно посмотретть в образе процессора приложения.

0e0a9ca4 <z_sys_init_run_level>:
 e0a9ca4:    b538        push    {r3, r4, r5, lr}
 e0a9ca6:    4b09        ldr r3, [pc, #36]   ; (e0a9ccc <z_sys_init_run_level+0x28>)
 e0a9ca8:    f853 4020   ldr.w   r4, [r3, r0, lsl #2]
 e0a9cac:    3001        adds    r0, #1
 e0a9cae:    f853 5020   ldr.w   r5, [r3, r0, lsl #2]
 e0a9cb2:    42a5        cmp r5, r4
 e0a9cb4:    d800        bhi.n   e0a9cb8 <z_sys_init_run_level+0x14>
 e0a9cb6:    bd38        pop {r3, r4, r5, pc}
 e0a9cb8:    6863        ldr r3, [r4, #4]
 e0a9cba:    b123        cbz r3, e0a9cc6 <z_sys_init_run_level+0x22>
 e0a9cbc:    4620        mov r0, r4
 e0a9cbe:    f003 f81f   bl  e0acd00 <do_device_init>
 e0a9cc2:    3408        adds    r4, #8
 e0a9cc4:    e7f5        b.n e0a9cb2 <z_sys_init_run_level+0xe>
 e0a9cc6:    6823        ldr r3, [r4, #0]
 e0a9cc8:    4798        blx r3
 e0a9cca:    e7fa        b.n e0a9cc2 <z_sys_init_run_level+0x1e>
 e0a9ccc:    0e0ae818    mcreq   8, 0, lr, cr10, cr8, {0}

Вторая инструкция, ldr r3, [pc, #36], загружает адрес со смещением в 36 байтов от текущего счетчика программы (pc), этот счетчик — последняя запись дизассемблированной функции: 0e0ae818.

Игнорируйте попытку декодирования инструкций с помощью objdump для этого адреса и всех адресов сохраненной памяти, указанных ниже.

Это адрес массива levels, который содержит элементы init_entry.

0e0ae818 <levels.0>:
 e0ae818:    0e0ad3cc    cdpeq   3, 0, cr13, cr10, cr12, {6}
 e0ae81c:    0e0ad3cc    cdpeq   3, 0, cr13, cr10, cr12, {6}
 e0ae820:    0e0ad40c    cdpeq   4, 0, cr13, cr10, cr12, {0}
 e0ae824:    0e0ad414    cfmvdlreq   mvd10, sp
 e0ae828:    0e0ad464    cdpeq   4, 0, cr13, cr10, cr4, {3}
 e0ae82c:    0e0ad474    mcreq   4, 0, sp, cr10, cr4, {3}
 e0ae830:    3566726e    strbcc  r7, [r6, #-622]!    ; 0xfffffd92
 e0ae834:    30326834    eorscc  r6, r2, r4, lsr r8
 e0ae838:    30406b64    subcc   r6, r0, r4, ror #22
 e0ae83c:    302e392e    eorcc   r3, lr, lr, lsr #18
 e0ae840:    66726e2f    ldrbtvs r6, [r2], -pc, lsr #28
 e0ae844:    32683435    rsbcc   r3, r8, #889192448  ; 0x35000000
 e0ae848:    70632f30    rsbvc   r2, r3, r0, lsr pc
 e0ae84c:    70706175    rsbsvc  r6, r0, r5, ror r1
 e0ae850:    6c654800    stclvs  8, cr4, [r5], #-0
 e0ae854:    77206f6c    strvc   r6, [r0, -ip, ror #30]!
 e0ae858:    646c726f    strbtvs r7, [ip], #-623 ; 0xfffffd91
 e0ae85c:    6f726620    svcvs   0x00726620
 e0ae860:    7325206d            ; <UNDEFINED> instruction: 0x7325206d
 e0ae864:    6146000a    cmpvs   r6, sl
 e0ae868:    64656c69    strbtvs r6, [r5], #-3177    ; 0xfffff397
 e0ae86c:    206f7420    rsbcs   r7, pc, r0, lsr #8
 e0ae870:    6f626572    svcvs   0x00626572
 e0ae874:    203a746f    eorscs  r7, sl, pc, ror #8
 e0ae878:    6e697073    mcrvs   0, 3, r7, cr9, cr3, {3}
 e0ae87c:    676e696e    strbvs  r6, [lr, -lr, ror #18]!
 e0ae880:    646e6520    strbtvs r6, [lr], #-1312    ; 0xfffffae0
 e0ae884:    7373656c    cmnvc   r3, #108, 10    ; 0x1b000000
 e0ae888:    2e2e796c    vnmulcs.f16 s14, s28, s25   ; <UNPREDICTABLE>
 e0ae88c:    69000a2e    stmdbvs r0, {r1, r2, r3, r5, r9, fp}
 e0ae890:    322d6370    eorcc   r6, sp, #112, 6 ; 0xc0000001
 e0ae894:    0032312d    eorseq  r3, r2, sp, lsr #2
 e0ae898:    2d637069    stclcs  0, cr7, [r3, #-420]!    ; 0xfffffe5c
 e0ae89c:    00332d32    eorseq  r2, r3, r2, lsr sp
 e0ae8a0:    736d6369    cmnvc   sp, #-1543503871    ; 0xa4000001
 e0ae8a4:    6f775f67    svcvs   0x00775f67
 e0ae8a8:    00716b72    rsbseq  r6, r1, r2, ror fp

Четвертая запись, 0e0ad414, представляет собой init_entry для периферийного устройства PPR. По этому адресу мы найдем адрес его init_function (0e0ac7cb) и указатель на структуру устройства (0e0ad474).

0e0ad414 <__init___device_dts_ord_60>:
 e0ad414:    0e0ac7cb    cdpeq   7, 0, cr12, cr10, cr11, {6}
 e0ad418:    0e0ad474    mcreq   4, 0, sp, cr10, cr4, {3}

Как и ожидалось, адрес init_function — это местоположение функции nordic_vpr_launcher_init. Или почти. В адресе, который хранится в init_entry (0e0ac7cb), имеется смещение в один байт от адреса функции запуска (0e0ac7ca). Через мгновение мы увидим, почему:

0e0ac7ca <nordic_vpr_launcher_init>:
 e0ac7ca:    b510        push    {r4, lr}
 e0ac7cc:    6844        ldr r4, [r0, #4]
 e0ac7ce:    68e2        ldr r2, [r4, #12]
 e0ac7d0:    b11a        cbz r2, e0ac7da <nordic_vpr_launcher_init+0x10>
 e0ac7d2:    e9d4 0101   ldrd    r0, r1, [r4, #4]
 e0ac7d6:    f000 fd56   bl  e0ad286 <memcpy>
 e0ac7da:    e9d4 3200   ldrd    r3, r2, [r4]
 e0ac7de:    f8c3 2808   str.w   r2, [r3, #2056] ; 0x808
 e0ac7e2:    2201        movs    r2, #1
 e0ac7e4:    6823        ldr r3, [r4, #0]
 e0ac7e6:    2000        movs    r0, #0
 e0ac7e8:    f8c3 2800   str.w   r2, [r3, #2048] ; 0x800
 e0ac7ec:    bd10        pop {r4, pc

z_sys_init_run_level вызывает do_device_init на периферийном устройстве PPR.

static int do_device_init(const struct init_entry *entry)
{
    const struct device *dev = entry->dev;
    int rc = 0;

    if (entry->init_fn.dev != NULL) {
        rc = entry->init_fn.dev(dev);
        /* Отметка, что устройство инициализировано. Если инициализация
         * не удалась, запись состояния этой ошибки.
         */
        if (rc != 0) {
            if (rc < 0) {
                rc = -rc;
            }
            if (rc > UINT8_MAX) {
                rc = UINT8_MAX;
            }
            dev->state->init_res = rc;
        }
    }

    dev->state->initialized = true;

    if (rc == 0) {
        /* Запуск автоматического включения устройства во время выполнения */
        (void)pm_device_runtime_auto_enable(dev);
    }

    return rc;
}

Он загружает адрес init_function и указатель устройства из переданного init_entry в r3 и r4 соответственно (ldrd r3, r4, [r0]). Затем, если функция инициализации устройства не равна NULL, вызывает функцию (blx r3) после перемещения указателя устройства в r0 для передачи ( mov r0, r4).

0e0acd00 <do_device_init>:
 e0acd00:    b510        push    {r4, lr}
 e0acd02:    e9d0 3400   ldrd    r3, r4, [r0]
 e0acd06:    b933        cbnz    r3, e0acd16 <do_device_init+0x16>
 e0acd08:    2000        movs    r0, #0
 e0acd0a:    68e2        ldr r2, [r4, #12]
 e0acd0c:    7853        ldrb    r3, [r2, #1]
 e0acd0e:    f043 0301   orr.w   r3, r3, #1
 e0acd12:    7053        strb    r3, [r2, #1]
 e0acd14:    bd10        pop {r4, pc}
 e0acd16:    4620        mov r0, r4
 e0acd18:    4798        blx r3
 e0acd1a:    2800        cmp r0, #0
 e0acd1c:    d0f4        beq.n   e0acd08 <do_device_init+0x8>
 e0acd1e:    2800        cmp r0, #0
 e0acd20:    bfb8        it  lt
 e0acd22:    4240        neglt   r0, r0
 e0acd24:    28ff        cmp r0, #255    ; 0xff
 e0acd26:    bfa8        it  ge
 e0acd28:    20ff        movge   r0, #255    ; 0xff
 e0acd2a:    68e3        ldr r3, [r4, #12]
 e0acd2c:    7018        strb    r0, [r3, #0]
 e0acd2e:    e7ec        b.n e0acd0a <do_device_init+0xa>

Использование инструкции blx («Ветвь со ссылкой и обменом») — причина смещения адреса на 1 байт. Cortex-M33 реализует архитектуру Armv8-M, которая использует набор инструкций T32 (ранее Thumb2.) Armv8 поддерживает «взаимодействие» (interworking), которое позволяет динамически переключаться между наборами инструкций A32 и T32, используя для применимых инструкций этого взаимодействия младший бит адреса назначения, указывающий применяемый вызывающей стороной ISA. Однако Armv8-M поддерживает только T32, поэтому младший бит всегда должен быть 1. Справочное руководство по архитектуре Armv8-M содержит следующее описание инструкции blx.

Bit[0] соответствует правилам взаимодействия архитектуры Arm для переключения между наборами инструкций A32 и T32. Однако Armv8-M поддерживает только набор инструкций T32, поэтому bit[0] должен быть равен 1. Если bit[0] равен 0, PE для инструкции по целевому адресу принимает исключение INVSTATE UsageFault.

В разделе device_area бинарного файла процессора приложения имеется структура device для периферийного устройства PPR. Его адрес соответствует второму члену init_entry, который был передан в do_device_init (0e0ad474.)

0e0ad474 <__device_dts_ord_60>:
 e0ad474:    0e0ae95e            ; <UNDEFINED> instruction: 0x0e0ae95e
 e0ad478:    0e0ae5e8    cfsh32eq    mvfx14, mvfx10, #-8
 e0ad47c:    00000000    andeq   r0, r0, r0
 e0ad480:    2f011404    svccs   0x00011404
 e0ad484:    00000000    andeq   r0, r0, r0

Второй элемент device — это указатель на config периферийного устройства PPR (0e0ae5e8).

0e0ae5e8 <config0>:
 e0ae5e8:    5f908000    svcpl   0x00908000
 e0ae5ec:    2fc00000    svccs   0x00c00000
 e0ae5f0:    0e0e4000    cdpeq   0, 0, cr4, cr14, cr0, {0}
 e0ae5f4:    0000f800    andeq   pc, r0, r0, lsl #16

nordic_vpr_launcher определяет config следующим образом:

struct nordic_vpr_launcher_config {
    NRF_VPR_Type *vpr;
    uintptr_t exec_addr;
#if DT_ANY_INST_HAS_PROP_STATUS_OKAY(source_memory)
    uintptr_t src_addr;
    size_t size;
#endif
};

В документации по структуре памяти nRF54H20 медленная глобальная оперативная память (RAM3x), из которой выполняется PRR, имеет диапазон адресов от 2fc00000 до 2fc14000. exec_addr соответствует верхней части диапазона 2fc00000. src_addr — это 0e0e4000, который находится в диапазоне адресов MRAM_10 (от 0e000000 до 0e100000< /g10>). MRAM\_10 — энергонезависимая память, используемая для хранения образов прошивки. Средство запуска VPR копирует прошивку PPR из MRAM\_10 в RAM3x, устанавливает счетчик программы PPR на начальный адрес RAM3x, а затем запускает PPR. В дампе образа прошивки PPR (сейчас мы смотрим на инструкции RISC-V) видно, что начальный адрес соответствует ожидаемому символу__start`.

2fc00000 <__start>:
2fc00000:    00001297            auipc   t0,0x1
2fc00004:    90028293            addi    t0,t0,-1792 # 2fc00900 <_isr_wrapper>
2fc00008:    00328293            addi    t0,t0,3
2fc0000c:    30529073            csrw    mtvec,t0
2fc00010:    00000297            auipc   t0,0x0
2fc00014:    0f028293            addi    t0,t0,240 # 2fc00100 <_irq_vector_table>
2fc00018:    30729073            csrw    0x307,t0
2fc0001c:    0a50006f            j   2fc008c0 <_vector_end>

Однако перед копированием образ прошивки PPR должен присутствовать в регионе MRAM_10. Это обрабатывается sysbuild на west flash, поскольку порядок flash определен в сгенерированном файле domains.yaml.

default: hello_world
build_dir: zephyr/samples/sysbuild/hello_world/build
domains:
  - name: hello_world
    build_dir: zephyr/samples/sysbuild/hello_world/build/hello_world
  - name: remote
    build_dir: zephyr/samples/sysbuild/hello_world/build/remote
flash_order:
  - remote
  - hello_world

После успешного программирования процессор приложения загрузится, инициализирует и запустит PPR, а затем выведет данные на настроенную консоль, в /dev/ttyACM0.

*** Booting nRF Connect SDK v2.8.0-a2386bfc8401 ***
*** Using Zephyr OS v3.7.99-0bc3393fb112 ***
Hello world from [email protected]/nrf54h20/cpuapp

После запуска процессором приложения PPR загрузится и выведет данные на настроенную консоль, в /dev/ttyACM1.

*** Booting nRF Connect SDK v2.8.0-a2386bfc8401 ***
*** Using Zephyr OS v3.7.99-0bc3393fb112 ***
Hello world from [email protected]/nrf54h20/cpuppr

Приятно видеть применение RISC-V для конкретных операций наряду со знакомыми процессорами Arm, присутствующими во многих микроконтроллерах. С внедрением процессоров VPR во многие новые SoC Nordic становится ясно, что в ближайшие годы мы продолжим наблюдать больше гетерогенных вычислительных ресурсов. Понимание архитектуры системы и взаимодействия между компонентами позволяет более полно использовать возможности этих продуктов. Продолжая изучать процессоры PPR и FLPR VPR, мы увидим, как их можно применять, чтобы расширить функциональность и повысить производительность.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Daniel Mangum: VPR: Nordic’s First RISC-V Processor

Предыдущая статьяЗапись логов в Golang
Следующая статьяПоездка в берлинском метро с графовой БД Memgraph