ULP RISC-V 协处理器编程
ULP RISC-V 协处理器是 ULP 的一种变体,用于 ESP32-S3。与 ULP FSM 类似,ULP RISC-V 协处理器可以在主处理器处于低功耗模式时执行传感器读数等任务。其与 ULP FSM 的主要区别在于,ULP RISC-V 可以通过标准 GNU 工具使用 C 语言进行编程。ULP RISC-V 可以访问 RTC_SLOW_MEM 内存区域及 RTC_CNTL、RTC_IO、SARADC 等外设的寄存器。RISC-V 处理器是一种 32 位定点处理器,指令集基于 RV32IMC,包括硬件乘除法和压缩指令。
安装 ULP RISC-V 工具链
ULP RISC-V 协处理器代码以 C 语言(或汇编语言)编写,使用基于 GCC 的 RISC-V 工具链进行编译。
如果您已依照 快速入门指南 中的介绍安装好了 ESP-IDF 及其 CMake 构建系统,那么 ULP RISC-V 工具链已经被默认安装到了您的开发环境中。
备注
在早期版本的 ESP-IDF 中,RISC-V 工具链具有不同的名称:riscv-none-embed-gcc。
编译 ULP RISC-V 代码
要将 ULP RISC-V 代码编译为某组件的一部分,必须执行以下步骤:
ULP RISC-V 代码以 C 语言或汇编语言编写(必须使用 .S 扩展名),必须放在组件目录中一个独立的目录中,例如 ulp/。
备注
当注册组件时(通过 idf_component_register
),该目录不应被添加至 SRC_DIRS
参数,因为目前该步骤需用于 ULP FSM。如何正确添加 ULP 源文件,请见以下步骤。
注册后从组件 CMakeLists.txt 中调用
ulp_embed_binary
示例如下:... idf_component_register() set(ulp_app_name ulp_${COMPONENT_NAME}) set(ulp_sources "ulp/ulp_c_source_file.c" "ulp/ulp_assembly_source_file.S") set(ulp_exp_dep_srcs "ulp_c_source_file.c") ulp_embed_binary(${ulp_app_name} "${ulp_sources}" "${ulp_exp_dep_srcs}")
ulp_embed_binary
的第一个参数指定生成的 ULP 二进制文件名。生成的其他文件,如 ELF 文件、.map 文件、头文件和链接器导出文件等也可使用此名称。第二个参数指定 ULP 源文件。最后,第三个参数指定组件源文件列表,其中包括生成的头文件。此列表用以正确构建依赖,并确保在构建过程中先生成后编译包含头文件的源文件。请参考下文,查看为 ULP 应用程序生成的头文件等相关概念。
使用常规方法(例如 idf.py app)编译应用程序。
在内部,构建系统将按照以下步骤编译 ULP 程序:
通过 C 编译器和汇编器运行每个源文件。 此步骤在组件编译目录中生成目标文件(.obj.c 或 .obj.S,取决于处理的源文件)。
通过 C 预处理器运行链接器脚本模版。 模版位于
components/ulp/ld
目录中。将目标文件链接到 ELF 输出文件 (
ulp_app_name.elf
)。此步骤生成的 .map 文件默认用于调试 (ulp_app_name.map
)。将 ELF 文件中的内容转储为二进制文件 (
ulp_app_name.bin
),以便嵌入到应用程序中。使用
riscv32-esp-elf-nm
在 ELF 文件中 生成全局符号列表 (ulp_app_name.sym
)。创建 LD 导出脚本和头文件 (
ulp_app_name.ld
和ulp_app_name.h
),包含来自ulp_app_name.sym
的符号。此步骤可借助esp32ulp_mapgen.py
工具来完成。将生成的二进制文件添加到要嵌入应用程序的二进制文件列表中。
访问 ULP RISC-V 程序变量
在 ULP RISC-V 程序中定义的全局符号也可以在主程序中使用。
例如,ULP RISC-V 程序可以定义 measurement_count
变量,此变量可以定义程序从深度睡眠中唤醒芯片之前需要进行的 ADC 测量的次数。
volatile int measurement_count;
int some_function()
{
//read the measurement count for use it later.
int temp = measurement_count;
...do something.
}
构建系统生成定义 ULP 编程中全局符号的 ${ULP_APP_NAME}.h
和 ${ULP_APP_NAME}.ld
文件,使主程序能够访问全局 ULP RISC-V 程序变量。上述两个文件包含 ULP RISC-V 程序中定义的所有全局符号,且这些符号均以 ulp_
开头。
头文件包含对此类符号的声明:
extern uint32_t ulp_measurement_count;
注意,所有符号(包括变量、数组、函数)均被声明为 uint32_t
。函数和数组需要先获取符号地址,再转换为适当的类型。
生成的链接器文本定义了符号在 RTC_SLOW_MEM 中的位置:
PROVIDE ( ulp_measurement_count = 0x50000060 );
要从主程序访问 ULP RISC-V 程序变量,需使用 include
语句包含生成的头文件。这样,就可以像访问常规变量一样访问 ULP RISC-V 程序变量。
#include "ulp_app_name.h"
void init_ulp_vars() {
ulp_measurement_count = 64;
}
互斥
如果想要互斥地访问被主程序和 ULP 程序共享的变量,则可以通过 ULP RISC-V Lock API 来实现:
ULP 中的所有硬件指令都不支持互斥,所以 Lock API 需通过一种软件算法(Peterson 算法 )来实现互斥。
注意,只能从主程序的单个线程中调用这些锁,如果多个线程同时调用,将无法启用互斥功能。
启动 ULP RISC-V 程序
要运行 ULP RISC-V 程序,主程序需要调用 ulp_riscv_load_binary()
函数,将 ULP 程序加载到 RTC 内存中,然后调用 ulp_riscv_run()
函数,启动 ULP RISC-V 程序。
注意,必须在 menuconfig 中启用 CONFIG_ULP_COPROC_ENABLED 和 CONFIG_ULP_COPROC_TYPE_RISCV 选项,以便正常运行 ULP RISC-V 程序。RTC slow memory reserved for coprocessor
选项设置的值必须足够存储 ULP RISC-V 代码和数据。如果应用程序组件包含多个 ULP 程序,RTC 内存必须足以容纳最大的程序。
每个 ULP RISC-V 程序均以二进制 BLOB 的形式嵌入到 ESP-IDF 应用程序中。应用程序可以引用此 BLOB,并以下面的方式加载此 BLOB(假设 ULP_APP_NAME 已被定义为 ulp_app_name
):
extern const uint8_t bin_start[] asm("_binary_ulp_app_name_bin_start");
extern const uint8_t bin_end[] asm("_binary_ulp_app_name_bin_end");
void start_ulp_program() {
ESP_ERROR_CHECK( ulp_riscv_load_binary( bin_start,
(bin_end - bin_start)) );
}
一旦上述程序加载到 RTC 内存后,应用程序即可调用 ulp_riscv_run()
函数启动此程序:
ESP_ERROR_CHECK( ulp_riscv_run() );
ULP RISC-V 程序流
ULP RISC-V 协处理器由定时器启动,调用 ulp_riscv_run()
即可启动定时器。定时器为 RTC_SLOW_CLK 的 Tick 事件计数(默认情况下,Tick 由内部 90 kHz RC 振荡器产生)。Tick 数值使用 RTC_CNTL_ULP_CP_TIMER_1_REG
寄存器设置。启用 ULP 时,使用 RTC_CNTL_ULP_CP_TIMER_1_REG
设置定时器 Tick 数值。
此应用程序可以调用 ulp_set_wakeup_period()
函数来设置 ULP 定时器周期值 (RTC_CNTL_ULP_CP_TIMER_1_REG)。
一旦定时器数到 RTC_CNTL_ULP_CP_TIMER_1_REG
寄存器中设置的 Tick 数,ULP RISC-V 协处理器就会启动,并调用 ulp_riscv_run()
的入口点开始运行程序。
程序保持运行,直至 RTC_CNTL_COCPU_CTRL_REG
寄存器中的 RTC_CNTL_COCPU_DONE
字段被置位或因非法处理器状态出现陷阱。一旦程序停止,ULP RISC-V 协处理器会关闭电源,定时器再次启动。
如需禁用定时器(有效防止 ULP 程序再次运行),请清除 RTC_CNTL_STATE0_REG
寄存器中的 RTC_CNTL_ULP_CP_SLP_TIMER_EN
位,此项操作可在 ULP 代码或主程序中进行。
调试 ULP RISC-V 程序
在对 ULP RISC-V 进行编程时,若程序未按预期运行,有时很难找出的原因。因为其内核的简单性,许多标准的调试方法如 JTAG 或 printf
无法使用。
以下方法可以帮助您调试 ULP RISC-V 程序:
通过共享变量查看程序状态:如 访问 ULP RISC-V 程序变量 中所述,主 CPU 以及 ULP 内核都可以轻松访问 RTC 内存中的全局变量。通过 ULP 向该变量中写入状态信息,然后通过主 CPU 读取状态信息,可帮助您了解 ULP 内核的状态。该方法的缺点在于它要求主 CPU 一直处于唤醒状态,但现实情况可能并非如此。有时,保持主 CPU 处于唤醒状态还可能会掩盖一些问题,因为某些问题可能仅在特定电源域断电时才会出现。
使用 bit-banged UART 驱动程序打印:ULP RISC-V 组件中有一个低速 bit-banged UART TX 驱动程序,可用于打印独立于主 CPU 状态的信息。有关如何使用此驱动程序的示例,请参阅 system/ulp_riscv/uart_print。
陷阱信号:ULP RISC-V 有一个硬件陷阱,将在特定条件下触发,例如非法指令。这将导致主 CPU 被
ESP_SLEEP_WAKEUP_COCPU_TRAP_TRIG
唤醒。
应用示例
主处理器处于 Deep-sleep 状态时,ULP RISC-V 协处理器轮询 GPIO:system/ulp_riscv/gpio。
ULP RISC-V 协处理器使用 bit-banged UART 驱动程序打印: system/ulp_riscv/uart_print.
主处理器处于 Deep-sleep 状态时,ULP RISC-V 协处理器读取外部温度传感器:system/ulp_riscv/ds18b20_onewire。
API 参考
Header File
Functions
-
esp_err_t ulp_riscv_isr_register(intr_handler_t fn, void *arg, uint32_t mask)
Register ULP signal ISR.
备注
The ISR routine will only be active if the main CPU is not in deepsleep
- 参数
fn – ISR callback function
arg – ISR callback function arguments
mask – Bit mask to enable the required ULP RISC-V interrupt signals
- 返回
ESP_OK on success
ESP_ERR_INVALID_ARG if callback function is NULL or if the interrupt bits are invalid
ESP_ERR_NO_MEM if heap memory cannot be allocated for the interrupt
other errors returned by esp_intr_alloc
-
esp_err_t ulp_riscv_isr_deregister(intr_handler_t fn, void *arg, uint32_t mask)
Deregister ULP signal ISR.
- 参数
fn – ISR callback function
arg – ISR callback function arguments
mask – Bit mask to enable the required ULP RISC-V interrupt signals
- 返回
ESP_OK on success
ESP_ERR_INVALID_ARG if callback function is NULL or if the interrupt bits are invalid
ESP_ERR_INVALID_STATE if a handler matching both callback function and its arguments isn’t registered
-
esp_err_t ulp_riscv_config_and_run(ulp_riscv_cfg_t *cfg)
Configure the ULP and run the program loaded into RTC memory.
- 参数
cfg – pointer to the config struct
- 返回
ESP_OK on success
-
esp_err_t ulp_riscv_run(void)
Configure the ULP with default settings and run the program loaded into RTC memory.
- 返回
ESP_OK on success
-
esp_err_t ulp_riscv_load_binary(const uint8_t *program_binary, size_t program_size_bytes)
Load ULP-RISC-V program binary into RTC memory.
Different than ULP FSM, the binary program has no special format, it is the ELF file generated by RISC-V toolchain converted to binary format using objcopy.
Linker script in components/ulp/ld/ulp_riscv.ld produces ELF files which correspond to this format. This linker script produces binaries with load_addr == 0.
- 参数
program_binary – pointer to program binary
program_size_bytes – size of the program binary
- 返回
ESP_OK on success
ESP_ERR_INVALID_SIZE if program_size_bytes is more than 8KiB
-
void ulp_riscv_timer_stop(void)
Stop the ULP timer.
备注
This will stop the ULP from waking up if halted, but will not abort any program currently executing on the ULP.
-
void ulp_riscv_timer_resume(void)
Resumes the ULP timer.
备注
This will resume an already configured timer, but does no other configuration
-
void ulp_riscv_halt(void)
Halts the program currently running on the ULP-RISC-V.
备注
Program will restart at the next ULP timer trigger if timer is still running. If you want to stop the ULP from waking up then call ulp_riscv_timer_stop() first.
-
void ulp_riscv_reset(void)
Resets the ULP-RISC-V core from the main CPU.
备注
This will reset the ULP core from the main CPU. It is intended to be used when the ULP is in a bad state and cannot run as intended due to a corrupt firmware or any other reason. The main core can reset the ULP core with this API and then re-initilialize the ULP.
Structures
-
struct ulp_riscv_cfg_t
ULP riscv init parameters.
Public Members
-
ulp_riscv_wakeup_source_t wakeup_source
ULP wakeup source
-
ulp_riscv_wakeup_source_t wakeup_source
Macros
-
ULP_RISCV_DEFAULT_CONFIG()
-
ULP_RISCV_SW_INT
-
ULP_RISCV_TRAP_INT
Enumerations
Header File
Structures
-
struct ulp_riscv_lock_t
Structure representing a lock shared between ULP and main CPU.
Public Members
-
bool critical_section_flag_ulp
ULP wants to enter the critical sections
-
bool critical_section_flag_main_cpu
Main CPU wants to enter the critical sections
-
ulp_riscv_lock_turn_t turn
Which CPU is allowed to enter the critical section
-
bool critical_section_flag_ulp
Enumerations
Header File
Functions
-
void ulp_riscv_lock_acquire(ulp_riscv_lock_t *lock)
Locks are based on the Peterson’s algorithm, https://en.wikipedia.org/wiki/Peterson%27s_algorithm.
Acquire the lock, preventing the ULP from taking until released. Spins until lock is acquired.
备注
The lock is only designed for being used by a single thread on the main CPU, it is not safe to try to acquire it from multiple threads.
- 参数
lock – Pointer to lock struct, shared with ULP
-
void ulp_riscv_lock_release(ulp_riscv_lock_t *lock)
Release the lock.
- 参数
lock – Pointer to lock struct, shared with ULP