diff --git a/examples/system/.build-test-rules.yml b/examples/system/.build-test-rules.yml index 3d1d18ed8a..a88ba92b68 100644 --- a/examples/system/.build-test-rules.yml +++ b/examples/system/.build-test-rules.yml @@ -411,6 +411,12 @@ examples/system/ulp/ulp_riscv/gpio_interrupt: depends_components: - ulp +examples/system/ulp/ulp_riscv/gpio_pulse_counter: + enable: + - if: SOC_RISCV_COPROC_SUPPORTED == 1 + depends_components: + - ulp + examples/system/ulp/ulp_riscv/i2c: enable: - if: SOC_RISCV_COPROC_SUPPORTED == 1 diff --git a/examples/system/ulp/ulp_riscv/gpio_pulse_counter/CMakeLists.txt b/examples/system/ulp/ulp_riscv/gpio_pulse_counter/CMakeLists.txt new file mode 100644 index 0000000000..3c19ba2a0c --- /dev/null +++ b/examples/system/ulp/ulp_riscv/gpio_pulse_counter/CMakeLists.txt @@ -0,0 +1,8 @@ +# The following lines of boilerplate have to be in your project's CMakeLists +# in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.22) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +# "Trim" the build. Include the minimal set of components, main, and anything it depends on. +idf_build_set_property(MINIMAL_BUILD ON) +project(ulp_riscv_pulse_counter_example) diff --git a/examples/system/ulp/ulp_riscv/gpio_pulse_counter/README.md b/examples/system/ulp/ulp_riscv/gpio_pulse_counter/README.md new file mode 100644 index 0000000000..f201358fa3 --- /dev/null +++ b/examples/system/ulp/ulp_riscv/gpio_pulse_counter/README.md @@ -0,0 +1,51 @@ +| Supported Targets | ESP32-S2 | ESP32-S3 | +| ----------------- | -------- | -------- | + +# ULP-RISC-V pulse counter example with GPIO Polling: +This example demonstrates how to program the ULP-RISC-V coprocessor to count the number of pulses being applied on a GPIO pin, and fastest possible frequency that can be counted. + +For the demonstration purpose, HP core is used to generate square wave pulses which are counted by ULP core. + +ULP program written in C can be found in `ulp/main.c`. The build system compiles and links this program, converts it into binary format, and embeds it into the .rodata section of the ESP-IDF application. + +At runtime, the application running inside the main CPU loads ULP program into the `RTC_SLOW_MEM` memory region using `ulp_riscv_load_binary` function. The main code then configures the ULP wakeup period and starts the coprocessor by using `ulp_riscv_run`. + +After the ULP program has started, main program running on HP core starts generating pulses on a GPIO pin. + +ULP program checks for the pulses by continuously polling the GPIO status to determine any state change from LOW to HIGH or vice versa to determine an edge, and increments the count to track how many number of edges have passed so far. + +To speed up execution, the critical part of ULP program is written in an inline assembly format with optimization for speed and lowest possible number of instructions in loop (users can unroll a few iterations of the loop to further improve speed, but that is beyond the scope and simplicity intended for this example code, and hence not shown in assembly code) + +After the pulses have been generated and edges have been counted by ULP core, HP core checks the number of edges counted and stored in ULP memory, divides it by 2, and prints on console whether the count matched as expected or missed any edges. + +## Hardware setup +Only one ESP devkit having ULP riscv core is needed to run this example + +### Pin Assignment: +The following pin assignment is used by default +| ESP variant | GPIO pin for Pulse Input/Output | +| -------------- | ------------------------------- | +| ESP32-S2 | GPIO1 | +| ESP32-S3 | GPIO1 | + +**Note:** If modifying pulse GPIO to another pin, please ensure it is a valid RTC GPIO and supported for usage by ULP core. Also update assembly code in `ulp/main.c` to read correct register for that GPIO. + +### Connections: +- No specific wire connections are required to run this example, as the same GPIO pin is used to generate and detect pulses +- Optionally, user can connect an oscilloscope positive probe to GPIO1 and negative probe to GND pin to check the frequency of pulses that was measured by ULP core + +## Example output +SUCCESS case: +``` +Start generating pulses +Stop generating pulses +Number of pulses generated by HP Core: 500000 +SUCCESS: ULP core was able to count all the pulses correctly, try reducing HP_CORE_PULSE_DELAY to generate pulses faster and see if ULP core can still count the pulses at that speed. +``` +FAILURE case: +``` +Start generating pulses +Stop generating pulses +Number of pulses generated by HP Core: 500000 +FAILURE: ULP core couldn't count all the pulses correctly, try increasing HP_CORE_PULSE_DELAY to generate slower pulses and see if ULP core can still count the pulses at that speed. +``` \ No newline at end of file diff --git a/examples/system/ulp/ulp_riscv/gpio_pulse_counter/main/CMakeLists.txt b/examples/system/ulp/ulp_riscv/gpio_pulse_counter/main/CMakeLists.txt new file mode 100644 index 0000000000..8230872e75 --- /dev/null +++ b/examples/system/ulp/ulp_riscv/gpio_pulse_counter/main/CMakeLists.txt @@ -0,0 +1,27 @@ +# Set usual component variables +set(COMPONENT_SRCS "ulp_riscv_pulse_counter_example_main.c") +set(COMPONENT_ADD_INCLUDEDIRS "") +set(COMPONENT_REQUIRES soc nvs_flash ulp esp_driver_gpio) + +register_component() + +# +# ULP support additions to component CMakeLists.txt. +# +# 1. The ULP app name must be unique (if multiple components use ULP). +set(ulp_app_name ulp_${COMPONENT_NAME}) +# +# 2. Specify all C and Assembly source files. +# Files should be placed into a separate directory (in this case, ulp/), +# which should not be added to COMPONENT_SRCS. +set(ulp_riscv_sources "ulp/main.c") + +# +# 3. List all the component source files which include automatically +# generated ULP export file, ${ulp_app_name}.h: +set(ulp_exp_dep_srcs "ulp_riscv_pulse_counter_example_main.c") + +# +# 4. Call function to build ULP binary and embed in project using the argument +# values above. +ulp_embed_binary(${ulp_app_name} "${ulp_riscv_sources}" "${ulp_exp_dep_srcs}") diff --git a/examples/system/ulp/ulp_riscv/gpio_pulse_counter/main/ulp/main.c b/examples/system/ulp/ulp_riscv/gpio_pulse_counter/main/ulp/main.c new file mode 100644 index 0000000000..e85bc8f4fb --- /dev/null +++ b/examples/system/ulp/ulp_riscv/gpio_pulse_counter/main/ulp/main.c @@ -0,0 +1,63 @@ +/* + * SPDX-FileCopyrightText: 2025 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 +#include +#include +#include "ulp_riscv.h" +#include "ulp_riscv_utils.h" +#include "ulp_riscv_gpio.h" + +uint32_t edge_count = 0; +uint32_t edge_limit = 0; + +int main(void) +{ + /* + * Written pulse counter in assembly to make it optimized and achieve faster speed + * Assembly registers usage + * a2 - Stores GPIO register address + * a3 - Stores previous GPIO level + * a4 - Stores current GPIO level + * a5 - Used to count number of edges + * a6 - Used to check and exit loop after HP_CORE_PULSE_COUNT x 2 edges count limit is reached + * NOTE: While reading GPIO pin, logical AND operation is not used to get exact pin level as it takes additional + * CPU cycles. Instead, GPIO register is right shifted to get the pin level bit at LSB position, + * which means the result stored in a4 will be logical OR of other GPIO pin levels as well, but that + * doesn't matter as we are only interested in change of state on the input pin and the other pins + * are left uninitialized. + */ + __asm__ volatile ( + "lui a2, 0xa\n" /* a2 = 0xa000 (upper 20 bits of GPIO register address) */ + "addi a2, a2, 1060\n" /* a2 = 0xa424 (full GPIO register address for GPIO_PULSE_INPUT) */ + "li a3, 0\n" /* a3 = prev (initialize previous GPIO level to 0) */ + "li a5, 0\n" /* a5 = edge counter (initialize to 0) */ + "lw a6, 0(%[edge_limit_addr])\n" /* a6 = edge count limit (HP_CORE_PULSE_COUNT * 2) */ + "loop:\n" + "lw a4, 0(a2)\n" /* a4 = *(uint32_t*)0xa424 (read GPIO register) */ + "srli a4, a4, 11\n" /* a4 = a4 >> 11 (shift right to get GPIO1 level bit at LSB) */ + "beq a4, a3, loop\n" /* if (a4 == prev) loop (no edge detected) */ + "addi a5, a5, 1\n" /* a5++ (increment edge counter) */ + "mv a3, a4\n" /* prev = a4 (update previous GPIO level) */ + "blt a5, a6, loop\n" /* if (a5 < edge_limit) loop (continue until limit) */ + "sw a5, 0(%[edge_count_addr])\n" /* edge_count = a5 (store final edge count to memory) */ + : + : [edge_count_addr] "r" (&edge_count), [edge_limit_addr] "r" (&edge_limit) + : "a2", "a3", "a4", "a5", "a6", "memory" + ); + + return 0; +} diff --git a/examples/system/ulp/ulp_riscv/gpio_pulse_counter/main/ulp_riscv_pulse_counter_example_main.c b/examples/system/ulp/ulp_riscv/gpio_pulse_counter/main/ulp_riscv_pulse_counter_example_main.c new file mode 100644 index 0000000000..2e27a22455 --- /dev/null +++ b/examples/system/ulp/ulp_riscv/gpio_pulse_counter/main/ulp_riscv_pulse_counter_example_main.c @@ -0,0 +1,120 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +/* ULP riscv DS18B20 1wire temperature sensor 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 +#include "freertos/FreeRTOS.h" +#include "soc/rtc_periph.h" +#include "driver/gpio.h" +#include "driver/rtc_io.h" +#include "esp_sleep.h" +#include "ulp_riscv.h" +#include "ulp_main.h" + +extern const uint8_t ulp_main_bin_start[] asm("_binary_ulp_main_bin_start"); +extern const uint8_t ulp_main_bin_end[] asm("_binary_ulp_main_bin_end"); + +/* Pulses will be generated by HP core, and detected by ULP core on this same pin */ +#define GPIO_PULSE_IO GPIO_NUM_1 + +/* Number of pulses to be generated by HP core */ +#define HP_CORE_PULSE_COUNT 500000 + +/* Delay between two pulses (less value = less delay) */ +#if CONFIG_IDF_TARGET_ESP32S2 +#define HP_CORE_PULSE_DELAY 29 /* ESP32S2 has slower RTC clock, so it needs longer delay. */ +#elif CONFIG_IDF_TARGET_ESP32S3 +#define HP_CORE_PULSE_DELAY 6 /* ESP32S3 has faster RTC clock, so it can count pulses at higher speed. */ +#else +#error "This example is only supported on ESP32-S2 and ESP32-S3" +#endif + +/* NOTE: HP_CORE_PULSE_DELAY has been pre-calculated for each target to achieve maximum frequency + * For ESP32-S2, with HP_CORE_PULSE_DELAY = 27, maximum pulse frequency is ~120 KHz + * For ESP32-S3, with HP_CORE_PULSE_DELAY = 5, maximum pulse frequency is ~250 KHz + * In the above defined macro, some buffer margin has been kept to ensure it works on different devices + * and different conditions. User can try reducing the delay value further to achieve higher pulse frequency + */ + +static void init_ulp_program(void) +{ + esp_err_t err = ulp_riscv_load_binary(ulp_main_bin_start, (ulp_main_bin_end - ulp_main_bin_start)); + ESP_ERROR_CHECK(err); + + /* 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); + + ulp_edge_limit = (uint32_t)(HP_CORE_PULSE_COUNT << 1); + + /* Start the program */ + err = ulp_riscv_run(); + ESP_ERROR_CHECK(err); +} + +static void generate_pulses() +{ + for (uint32_t i = 0; i < HP_CORE_PULSE_COUNT; i++) { + /* Generate a pulse by setting the GPIO high and low with a delay in between */ + rtc_gpio_set_level(GPIO_PULSE_IO, 1); + /* Volatile ensures below loop is not optimized by compiler, as it provides blocking delay */ + for (volatile uint32_t j = 0; j < HP_CORE_PULSE_DELAY; j++); + rtc_gpio_set_level(GPIO_PULSE_IO, 0); + for (volatile uint32_t j = 0; j < HP_CORE_PULSE_DELAY; j++); + } +} + +void app_main(void) +{ + /* If user is using USB-serial-jtag then idf monitor needs some time to + * re-connect to the USB port. We wait 1 sec here to allow for it to make the reconnection + * before we print anything. Otherwise the chip will go back to sleep again before the user + * has time to monitor any output. + */ + vTaskDelay(pdMS_TO_TICKS(1000)); + + /* Initialize selected GPIO as RTC IO, enable input, disable pullup and pulldown */ + rtc_gpio_init(GPIO_PULSE_IO); + rtc_gpio_set_direction(GPIO_PULSE_IO, RTC_GPIO_MODE_INPUT_OUTPUT); + rtc_gpio_pulldown_dis(GPIO_PULSE_IO); + rtc_gpio_pullup_dis(GPIO_PULSE_IO); + + /* Initialize ULP core */ + init_ulp_program(); + + /* Small delay to ensure the ULP is ready to capture pulses */ + vTaskDelay(100); + printf("Start generating pulses\n"); + generate_pulses(); + printf("Stop generating pulses\n"); + + /* Small delay to ensure the ULP pulse counting is complete */ + vTaskDelay(100); + + /* Printing the count summary */ + printf("Number of pulses generated by HP Core: %d\n", HP_CORE_PULSE_COUNT); + if ((ulp_edge_count / 2) == HP_CORE_PULSE_COUNT) { + printf("SUCCESS: ULP core was able to count all the pulses correctly, " \ + "try reducing HP_CORE_PULSE_DELAY to generate pulses faster" \ + " and see if ULP core can still count the pulses at that speed.\n"); + } else { + printf("FAILURE: ULP core couldn't count all the pulses correctly, " \ + "try increasing HP_CORE_PULSE_DELAY to generate slower pulses " \ + "and see if ULP core can still count the pulses at that speed. %ld\n", ulp_edge_count / 2); + } + + printf("\nEntering deep sleep\n"); + /* Entering deep sleep */ + esp_deep_sleep_start(); +} diff --git a/examples/system/ulp/ulp_riscv/gpio_pulse_counter/pytest_ulp_riscv_pulse_counter.py b/examples/system/ulp/ulp_riscv/gpio_pulse_counter/pytest_ulp_riscv_pulse_counter.py new file mode 100644 index 0000000000..82fced286f --- /dev/null +++ b/examples/system/ulp/ulp_riscv/gpio_pulse_counter/pytest_ulp_riscv_pulse_counter.py @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: CC0-1.0 +import time + +import pytest +from pytest_embedded import Dut +from pytest_embedded_idf.utils import idf_parametrize + + +@pytest.mark.generic +@idf_parametrize('target', ['esp32s2', 'esp32s3'], indirect=['target']) +def test_ulp_riscv_pulse_counter(dut: Dut) -> None: + # Wait for the start of pulse generation + dut.expect_exact('Start generating pulses') + # Wait for pulses to be generated + time.sleep(3) + # Wait for the end of pulse generation + dut.expect_exact('Stop generating pulses') + # Wait for ULP to finish counting + time.sleep(1) + # Check the pulse count summary output + dut.expect(r'Number of pulses generated by HP Core: (\d+)') + # Check for SUCCESS message + result = dut.expect_exact(['SUCCESS', 'FAILURE'], timeout=5) + assert result == b'SUCCESS', 'ULP pulse counter did not report SUCCESS' + # Wait for deep sleep entry + dut.expect_exact('Entering deep sleep') + pass diff --git a/examples/system/ulp/ulp_riscv/gpio_pulse_counter/sdkconfig.defaults b/examples/system/ulp/ulp_riscv/gpio_pulse_counter/sdkconfig.defaults new file mode 100644 index 0000000000..e3745e5057 --- /dev/null +++ b/examples/system/ulp/ulp_riscv/gpio_pulse_counter/sdkconfig.defaults @@ -0,0 +1,9 @@ +# Enable ULP +CONFIG_ULP_COPROC_ENABLED=y +CONFIG_ULP_COPROC_TYPE_RISCV=y +CONFIG_ULP_COPROC_RESERVE_MEM=4096 +# Set log level to Warning to produce clean output +CONFIG_BOOTLOADER_LOG_LEVEL_WARN=y +CONFIG_BOOTLOADER_LOG_LEVEL=2 +CONFIG_LOG_DEFAULT_LEVEL_WARN=y +CONFIG_LOG_DEFAULT_LEVEL=2