ESP32-S3のRISC-VでのULPをArduinoで実行する

概要

ESP32-S3にはRISC-V命令セットのコプロセッサーのULPが搭載されています。ESP-IDF5以降で正式に利用可能になるのですが、一足先にArduinoでの動作実験をしてみました。

先行事例

上記にてULPを検証している人がいました。それ以外だとほとんど情報がないですね。そもそもESP-IDF5からのサポートなのですが、まだv5.0-beta1で正式リリースしていません。セットアップ方法などは上記のブログを参考にして欲しいのです。

ツール上ではベータが出ましたのでmasterではなくて、rekease/v5.0から選ぶのがよいと思います。そしてEspressif-IDEも一緒に入れておくと、プロジェクトの作成やビルドがかんたんだと思います。

Arduino IDEで動かしてみる

個人的にはESP-IDFは使いませんのでArduinoで動かしたいと思います。RISC-Vのビルドをして実行ファイルをArduinoで転送すれば動くと思います。

ただし、ULP系のソースが古かったり、Hファイルだけで実際のCファイルが無かったりしました。。。そこで5系から必要なファイルだけ取ってきて動かしました。

こんな感じで4ファイルをプロジェクトに入れることで動かすことができました。

結果です。48(H)65(e)6c(l)6c(l)6f(o)20( )というアスキーコードが確認できます。

/*
 * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
 *
 * SPDX-License-Identifier: Unlicense OR CC0-1.0
 */
/* ULP-RISC-V example

   This example code is in the Public Domain (or CC0 licensed, at your option.)

   Unless required by applicable law or agreed to in writing, this
   software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
   CONDITIONS OF ANY KIND, either express or implied.

   This code runs on ULP-RISC-V  coprocessor
*/

#include "ulp_riscv.h"
#include "ulp_riscv_utils.h"
#include "ulp_riscv_print.h"

#include "ulp_riscv_uart_ulp_core.h"
#include "sdkconfig.h"

static ulp_riscv_uart_t s_print_uart;

uint32_t uart_tx = CONFIG_EXAMPLE_UART_TXD;

int main (void)
{
    ulp_riscv_uart_cfg_t cfg = {
        .tx_pin = uart_tx,
    };

    ulp_riscv_uart_init(&s_print_uart, &cfg);
    ulp_riscv_print_install((putc_fn_t)ulp_riscv_uart_putc, &s_print_uart);

    int cnt = 0;
    while(1) {

        ulp_riscv_print_str("Hello World from ULP-RISCV!\n");
        ulp_riscv_print_str("Cnt: 0x");
        ulp_riscv_print_hex(cnt);
        ulp_riscv_print_str("\n");

        cnt++;
        ulp_riscv_delay_cycles(1000 * ULP_RISCV_CYCLES_PER_MS);
    }
}

こちらがULPで動いているコードです。”Hello World from ULP-RISCV!\n”と送信しているので、その先頭がオシロスコープで確認できていましたね。

ulp_main.h

// Variable definitions for ESP32ULP
// This file is generated automatically by esp32ulp_mapgen.py utility

#pragma once

uint32_t* ulp_uart_tx = (uint32_t*)0x500002c0;

// https://lang-ship.com/tools/file2data/
// ulp_main

// File Dump
const unsigned char ulp_main_bin[708] PROGMEM = {
0x6f, 0x00, 0xe0, 0x01, 0x13, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 
0x82, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x17, 0x11, 
0x00, 0x00, 0x13, 0x01, 0x21, 0xfe, 0x35, 0x24, 0x19, 0x20, 0x35, 0x2c, 0x01, 0xa0, 0x79, 0x71, 
0x83, 0x27, 0x00, 0x2c, 0x22, 0xd4, 0x13, 0x05, 0x40, 0x2c, 0x6c, 0x00, 0x06, 0xd6, 0x26, 0xd2, 
0x4a, 0xd0, 0x4e, 0xce, 0x52, 0xcc, 0x3e, 0xc6, 0xad, 0x20, 0x93, 0x05, 0x40, 0x2c, 0x13, 0x05, 
0x60, 0x14, 0xb7, 0x04, 0x0b, 0x01, 0x9d, 0x2a, 0x01, 0x44, 0x93, 0x84, 0xc4, 0x74, 0x13, 0x05, 
0x40, 0x29, 0x95, 0x2a, 0x13, 0x05, 0x40, 0x2b, 0xbd, 0x22, 0x22, 0x85, 0x69, 0x2a, 0x13, 0x05, 
0xc0, 0x2b, 0x95, 0x22, 0x05, 0x04, 0xf3, 0x27, 0x00, 0xc0, 0xa6, 0x97, 0x73, 0x27, 0x00, 0xc0, 
0xe3, 0x6e, 0xf7, 0xfe, 0xe9, 0xbf, 0xf3, 0x27, 0x00, 0xc0, 0x31, 0x15, 0x3e, 0x95, 0xf3, 0x27, 
0x00, 0xc0, 0xe3, 0xee, 0xa7, 0xfe, 0x82, 0x80, 0x29, 0x67, 0x13, 0x07, 0x47, 0x40, 0x14, 0x43, 
0x93, 0x07, 0x00, 0x40, 0xb3, 0x97, 0xa7, 0x00, 0x93, 0xf6, 0xf6, 0x3f, 0xd5, 0x8f, 0x1c, 0xc3, 
0x82, 0x80, 0xaa, 0x87, 0x88, 0x41, 0x13, 0x07, 0xc0, 0x71, 0x98, 0xc3, 0xc8, 0xc3, 0xb5, 0x67, 
0x93, 0x87, 0x47, 0x90, 0x98, 0x43, 0xb7, 0x06, 0x00, 0x80, 0x55, 0x8f, 0x98, 0xc3, 0xb7, 0x27, 
0x00, 0x18, 0x93, 0x87, 0x17, 0x12, 0xaa, 0x97, 0x13, 0x97, 0x27, 0x00, 0xb9, 0x66, 0x96, 0x07, 
0xf5, 0x8f, 0x89, 0x66, 0xf1, 0x16, 0x75, 0x8f, 0xd9, 0x8f, 0x21, 0x67, 0xd9, 0x8f, 0x98, 0x43, 
0xb7, 0x06, 0x08, 0x00, 0x55, 0x8f, 0x98, 0xc3, 0x98, 0x43, 0xb7, 0x06, 0xfa, 0xff, 0xfd, 0x16, 
0x75, 0x8f, 0xa9, 0x66, 0x98, 0xc3, 0x93, 0x86, 0x06, 0x41, 0x90, 0x42, 0x13, 0x07, 0x00, 0x40, 
0x33, 0x17, 0xa7, 0x00, 0x13, 0x76, 0xf6, 0x3f, 0x51, 0x8f, 0x98, 0xc2, 0x98, 0x43, 0xb7, 0x06, 
0x00, 0xa0, 0xfd, 0x16, 0x75, 0x8f, 0xb7, 0x06, 0x00, 0x20, 0x55, 0x8f, 0x98, 0xc3, 0x98, 0x43, 
0xb7, 0x06, 0x00, 0x08, 0x55, 0x8f, 0x98, 0xc3, 0x98, 0x43, 0xb7, 0x06, 0x00, 0xf0, 0xfd, 0x16, 
0x75, 0x8f, 0x98, 0xc3, 0x91, 0xbf, 0x29, 0x67, 0x01, 0x11, 0x13, 0x07, 0x87, 0x40, 0x1c, 0x43, 
0x4e, 0xc6, 0x83, 0x29, 0x45, 0x00, 0x4a, 0xc8, 0x13, 0x09, 0x00, 0x40, 0x33, 0x19, 0x39, 0x01, 
0x93, 0xf7, 0xf7, 0x3f, 0x22, 0xcc, 0x26, 0xca, 0x52, 0xc4, 0x56, 0xc2, 0x5a, 0xc0, 0x06, 0xce, 
0xb3, 0xe7, 0x27, 0x01, 0x2a, 0x84, 0x2e, 0x8a, 0x1c, 0xc3, 0x81, 0x44, 0x29, 0x6b, 0xa1, 0x4a, 
0x08, 0x40, 0x13, 0x05, 0xc5, 0xf9, 0x01, 0x37, 0xb3, 0x57, 0x9a, 0x40, 0x85, 0x8b, 0x95, 0xc7, 
0x4e, 0x85, 0x19, 0x37, 0x85, 0x04, 0xe3, 0x95, 0x54, 0xff, 0x08, 0x40, 0x31, 0x15, 0xe5, 0x35, 
0x4e, 0x85, 0xdd, 0x3d, 0x08, 0x40, 0x62, 0x44, 0xf2, 0x40, 0xd2, 0x44, 0x42, 0x49, 0xb2, 0x49, 
0x22, 0x4a, 0x92, 0x4a, 0x02, 0x4b, 0x05, 0x61, 0xf9, 0xb5, 0x13, 0x07, 0x8b, 0x40, 0x1c, 0x43, 
0x93, 0xf7, 0xf7, 0x3f, 0xb3, 0xe7, 0x27, 0x01, 0x1c, 0xc3, 0xe9, 0xb7, 0x93, 0x07, 0xc0, 0x2c, 
0xcc, 0xc3, 0x88, 0xc3, 0x82, 0x80, 0x41, 0x11, 0x26, 0xc2, 0x93, 0x07, 0xc0, 0x2c, 0x9c, 0x43, 
0x06, 0xc6, 0x22, 0xc4, 0x81, 0xcb, 0x2a, 0x84, 0x93, 0x04, 0xc0, 0x2c, 0x83, 0x45, 0x04, 0x00, 
0x05, 0x04, 0x91, 0xe5, 0xb2, 0x40, 0x22, 0x44, 0x92, 0x44, 0x41, 0x01, 0x82, 0x80, 0x9c, 0x40, 
0xc8, 0x40, 0x82, 0x97, 0xe5, 0xb7, 0x01, 0x11, 0x26, 0xca, 0x93, 0x07, 0xc0, 0x2c, 0x9c, 0x43, 
0x06, 0xce, 0x22, 0xcc, 0x4a, 0xc8, 0x4e, 0xc6, 0x9d, 0xc3, 0x2a, 0x84, 0x93, 0x04, 0xc0, 0x2c, 
0x21, 0x49, 0xa5, 0x49, 0x93, 0x57, 0xc4, 0x01, 0x98, 0x40, 0xc8, 0x40, 0x63, 0xe0, 0xf9, 0x02, 
0x93, 0x85, 0x07, 0x03, 0x7d, 0x19, 0x02, 0x97, 0x12, 0x04, 0xe3, 0x15, 0x09, 0xfe, 0xf2, 0x40, 
0x62, 0x44, 0xd2, 0x44, 0x42, 0x49, 0xb2, 0x49, 0x05, 0x61, 0x82, 0x80, 0x93, 0x85, 0x77, 0x05, 
0xd5, 0xb7, 0xa1, 0x67, 0x93, 0x87, 0x47, 0x10, 0x98, 0x43, 0xb7, 0x06, 0xc0, 0xfd, 0xfd, 0x16, 
0x75, 0x8f, 0x98, 0xc3, 0x82, 0x80, 0xa1, 0x67, 0x93, 0x87, 0x47, 0x10, 0x98, 0x43, 0xb7, 0x46, 
0xc0, 0xff, 0xfd, 0x16, 0x75, 0x8f, 0xb7, 0xc6, 0x0f, 0x00, 0x55, 0x8f, 0x98, 0xc3, 0x98, 0x43, 
0xb7, 0x06, 0x00, 0x02, 0x55, 0x8f, 0x98, 0xc3, 0x98, 0x43, 0xb7, 0x06, 0x40, 0x00, 0x55, 0x8f, 
0x98, 0xc3, 0x01, 0xa0, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x20, 
0x66, 0x72, 0x6f, 0x6d, 0x20, 0x55, 0x4c, 0x50, 0x2d, 0x52, 0x49, 0x53, 0x43, 0x56, 0x21, 0x0a, 
0x00, 0x00, 0x00, 0x00, 0x43, 0x6e, 0x74, 0x3a, 0x20, 0x30, 0x78, 0x00, 0x0a, 0x00, 0x00, 0x00, 
0x04, 0x00, 0x00, 0x00, };

肝になるのがこちらのulp_main.hです。こちらは自動生成されたファイルがあったのですが、中身を消してビルドフォルダの中にあるulp_main.binをバイナリ化したデータがはいっています。

uint32_t* ulp_uart_tx = (uint32_t*)0x500002c0;

特徴的なのはRISC-Vのグローバル変数はULPのメモリに確保されます。そしてULPのメモリはESP32のコアからもアクセス可能ですので両方のコアから参照できます。このアドレスは.fdファイルを見ると確認することができます。

inoファイル

/*
   SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD

   SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
/* ULP riscv uart print example

   This example code is in the Public Domain (or CC0 licensed, at your option.)

   Unless required by applicable law or agreed to in writing, this
   software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
   CONDITIONS OF ANY KIND, either express or implied.
*/

#include <stdio.h>
#include "esp_sleep.h"
#include "ulp_riscv.h"
#include "ulp_main.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

static void init_ulp_program(void);

void setup() {
  Serial.begin(115200);
  delay(100);

  esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
  /* not a wakeup from ULP, load the firmware */
  if (cause != ESP_SLEEP_WAKEUP_ULP) {
    Serial.printf("Not a ULP-RISC-V wakeup, initializing it! \n");
    init_ulp_program();
  }

  /* ULP Risc-V read and detected a change in GPIO_0, prints */
  if (cause == ESP_SLEEP_WAKEUP_ULP) {
    Serial.printf("ULP-RISC-V woke up the main CPU! \n");
  }

  /* Go back to sleep, only the ULP Risc-V will run */
  Serial.printf("Entering in deep sleep\n\n");

  /* Small delay to ensure the messages are printed */
  delay(100);

  ESP_ERROR_CHECK( esp_sleep_enable_ulp_wakeup());
  //esp_deep_sleep_start();
}

void loop() {
  uint8_t *val = (uint8_t*)0x50000000;
  for (int i = 0; i < sizeof(ulp_main_bin); i++) {
    Serial.printf("[%04X] %02X\n", i, val[i]);
  }

  Serial.printf("UART TX : %ld\n", *ulp_uart_tx);
  delay(5000);
}

static void init_ulp_program(void) {
  esp_err_t err = ulp_riscv_load_binary(ulp_main_bin, sizeof(ulp_main_bin));
  ESP_ERROR_CHECK(err);

  // UARTの出力ピンをランダムに変更する
  if (random(2)) {
    *ulp_uart_tx = 4;
  } else {
    *ulp_uart_tx = 6;
  }

  /* The first argument is the period index, which is not used by the ULP-RISC-V timer
     The second argument is the period in microseconds, which gives a wakeup time period of: 20ms
  */
  ulp_set_wakeup_period(0, 20000);

  /* Start the program */
  err = ulp_riscv_run();
  ESP_ERROR_CHECK(err);
}

メインのファイルです。

上記のファイルに相当します。ESP-IDFだといい感じにリンクしてULPのプログラムを取り込んでくれますが、Arduinoだとその手が使えません。

  esp_err_t err = ulp_riscv_load_binary(ulp_main_bin, sizeof(ulp_main_bin));

ulp_main.hでデータ化した配列を使って取り込んでいます。

  // UARTの出力ピンをランダムに変更する
  if (random(2)) {
    *ulp_uart_tx = 4;
  } else {
    *ulp_uart_tx = 6;
  }

こんな感じにRISC-V側の変数を変更することができます。

  Serial.printf("UART TX : %ld\n", *ulp_uart_tx);

ULPが動作中でも上記のように参照することができます。動作中にESP32から書き込むこともできますがRISC-Vとバッティングすることがあるので考えて書き換える必要がありそうです。

残りのファイル

上記のファイルを持ってきました。内容的にはULPまわりのみですのでESP-IDFのバージョンが4系のArduinoでも問題なく動かすことができました。

UART送信の仕組み

上記のソースをみるとわかるのですが、、、

void ulp_riscv_uart_putc(const ulp_riscv_uart_t *uart, const char c)
{
    ulp_riscv_gpio_output_level(uart->tx_pin, 0);

    for (int i = 0; i<8; i++) {
        /* Offset the delay to account for cycles spent setting the bit */
        ulp_riscv_delay_cycles(uart->bit_duration_cycles - 100);
        if ( (1 <<  i) & c) {
            ulp_riscv_gpio_output_level(uart->tx_pin, 1);
        } else {
            ulp_riscv_gpio_output_level(uart->tx_pin, 0);
        }

    }

    ulp_riscv_delay_cycles(uart->bit_duration_cycles - 20);
    ulp_riscv_gpio_output_level(uart->tx_pin, 1);
    ulp_riscv_delay_cycles(uart->bit_duration_cycles);
}

んー、8ビット単位でビジーループを使って、GPIOをON/OFFして信号を作っていますね。RISC-VのULPからは便利な機能はADC以外の呼び出せませんので地道に実装する必要があります。

masterブランチをみたところI2Cも実装されていました。昔のアセンブラのI2Cよりはわかりやすいですが、ちょっと読みたくないソースになります。

まとめ

ESP32-S3のULPはバイナリを作って転送することで動かすことができます。バイナリのコンパイルはESP-IDF5が必要ですが、動かすだけであればArduinoで大丈夫でした。

あとはULPだけかんたんにコンパイルする方法があればいいのですが、、、んー、cmakeでいろいろやっているのでなかなか大変そうでした。またIDEでもESP-IDFに合わせた環境設定なので、ULP側のコード補完などが効きません。

他のソースですが、こんな感じの画面になっていて#include <stdio.h>から読み込めないエラーになっています。まったく別プラットフォームのものを同じプロジェクトで管理するとこうなってしまいますよね。

ULPはULPだけのプロジェクトに分離したほうがいいと思いますが、そうするとESP-IDF側への自動反映が面倒なことになります。

まだまだArduinoで使えるようになるのは先な気がしますが、ビルドさえなんとかしてしまえば動かすことは可能そうでした。

コメント