Como programar o ULP RISC-V coprocessor do ESP32 em C no ESP-IDF


{getToc} $title={Índice}

O ULP RISC-V coprocessor [1] é uma variação de coprocessador de baixo consumo, presente nos ESP32-S2 e ESP32-S3. A grande diferença é que ele possibilita a programação tanto em C como em Assembly, diferente do coprocessador ULP FSM que possibilita apenas a programação em Assembly.

Compilando o código

Para este exemplo, será considerada a seguinte arquitetura:
├── CMakeLists.txt
├── main
│   ├── CMakeLists.txt
│   └── main.c
└── components
      └── app
            ├── include
            │    └── app.h
            ├── ulp
            │    └── ulp_app.c
            ├── app.c
            └── CMakeLists.txt

Inicialmente, as opções CONFIG_ULP_COPROC_ENABLEDCONFIG_ULP_COPROC_TYPE_RISCV devem estar habilitadas no menuconfig.
  1. Deve-se criar uma pasta isolada para o código do ULP dentro da componente que irá controlar o ULP. Nesse exemplo, esta pasta é a ulp, o arquivo fonte do ULP é o ulp_app.c e será usado dentro da componente app.

  2. Chame a função ulp_embed_binary de dentro do CMakeList.txt da componente, após o registro da mesma. Ex.:
    ```CMake
    idf_component_register(SRCS "app.c"
                        INCLUDE_DIRS "include"
                        REQUIRES ulp driver)
    set(ulp_app_name ulp_${COMPONENT_NAME})
    set(ulp_sources "ulp/ulp_app.c")
    set(ulp_exp_dep_srcs "app.c")
    ulp_embed_binary(${ulp_app_name} "${ulp_sources}" "${ulp_exp_dep_srcs}")
    ```
    O primeiro argumento será o nome do binário.
    O segundo são os arquivos fonte a serem compilados para o ULP.
    O terceiro são os arquivos fonte que incluirão o header a ser gerado.

  3. Compile normalmente (idf.py build ou botão de build do VS Code)

Iniciando o programa do ULP RISC-V

Para iniciar o programa do ULP, deve-se carregar o programa chamando a função ulp_riscv_load_binary mostrada a seguir no código da aplicação principal.
```C
extern const uint8_t ulp_bin_start[] asm("_binary_ulp_app_bin_start");
extern const uint8_t ulp_bin_end[]   asm("_binary_ulp_app_bin_end");

void init_ulp_program() {
    esp_err_t err = ulp_riscv_load_binary(ulp_bin_start,
(ulp_bin_end - ulp_bin_start));
    ESP_ERROR_CHECK(err);
}
```
Os nomes de ulp_bin_start e ulp_bin_end dependem diretamente do nome da ulp_app_name, definido no primeiro argumento da função de compilação do código do ULP ulp_embed_binary (tópico 2 da etapa "Compilando o código"). O nome deve estar no padrão "_binary_<ulp_app_name>_bin_start" e "_binary_<ulp_app_name>_bin_end".

Por fim, para executar o binário do ULP RISC-V, basta chamar a função ulp_riscv_run.
```C
ESP_ERROR_CHECK( ulp_riscv_run() );
```

Acessando variáveis do ULP no código principal

Para acessar as variáveis globais do ULP no código principal, as variáveis do coprocessador são externadas em um header gerado automaticamente que será incluído no código fonte principal. Cada variável global do código fonte do ULP é externada no header <ulp_app_name>.h, que neste exemplo é ulp_app.h. Contudo, todas as variáveis são externadas com o tipo uint32_t e possuem o prefixo ulp_, conforme o exemplo a seguir.
Código do ULP RISC-V:
- Arquivo fonte ULP (ulp_app.c):
```C
volatile int var;
volatile char var_string[5];
int main() 
    if(var > 100) var = 0;
    else var++;
}
```
- Header gerado automaticamente (ulp_app.h):
```C
. . .
extern uint32_t ulp_var;
extern uint32_t ulp_var_string;
. . .
```
Aplicação principal
```C
#include "ulp_app.h"
void init_ulp_vars() {
    char *var_string = (char *) &ulp_var_string;
    strcpy(var_string, "");
    ulp_var = 64;
}
uint32_t read_ulp_var() {
return ulp_var;
}
void print_ulp_var_string() {
    char *var_string = (char *) &ulp_var_string;
    printf("String current value: %s", var_string);
}
```
Dessa forma, a variável será compartilhada nos dois arquivos fonte, de forma que pode ser acessada em qualquer um dos contextos.
Caso a variável não seja do tipo uint32_t ou seja um array, basta ler o endereço da variável externada, realizar o casting e utilizá-la normalmente, conforme pode ser visto acima com a variável var_string.
Vale lembrar que estas variáveis não serão desalocadas mesmo que o ULP coprocessor encerre sua tarefa e venha a dormir.

Acordar o coprocessador ULP ciclicamente

Quando a função ulp_riscv_run é chamada, um timer é iniciado e este timer é o responsável por ativar o ULP coprocessor. Caso se deseje ativar a execução cíclica do código do ULP coprocessor, a aplicação pode utilizar a função ulp_set_wakeup_period(), conforme a seguir:

```C
err = ulp_set_wakeup_period(0, 1000000);
ESP_ERROR_CHECK(err);
```
O primeiro argumento é o índice do período, valor não utilizado pelo timer do ULP. O segundo argumento é o período do timer em microssegundos, o que indicará qual o período de despertar do ULP. Nesse exemplo, o valor 1000000 indica um período de wakeup de 1 segundo.

Acordar o processador principal através do ULP

Em uma aplicação de baixo consumo, é muito comum que o processador principal seja acordado apenas esporadicamente, e permaneça em algum modo de sleep durante a maior parte do tempo. Caso se deseje acordar o processador principal por meio do ULP coprocessor, a aplicação deve primeiramente ativar a funcionalidade fazendo a chamada da função esp_sleep_enable_ulp_wakeup(). Vale destacar que sempre que o processador principal reiniciar, a aplicação deve voltar a fazer a chamada desta função para manter a funcionalidade.
Depois, quando a aplicação ULP desejar acordar o processador principal, basta chamar a função ulp_riscv_wakeup_main_processor().

Debug do ULP coprocessor

Por ser um coprocessador extremamente simples, ele não possui diversas funcionalidades de debug como JTAG ou até mesmo printf. Para debugar este coprocessador, uma possibilidade é externar suas variáveis para o processador principal e acessá-las por lá, ou por meio da UART específica do ULP, que possui duas funções de print, uma para strings e outra para variáveis hexadecimais. O valor de baudrate pode ser alterado no menuconfig, mudando o valor da constante ULP_RISCV_UART_BAUDRATE. O exemplo a seguir mostra como configurar e usar as funções de print.
```C
#include "ulp_app.h"
#include "ulp_riscv.h"
#include "ulp_riscv_utils.h"
#include "ulp_riscv_uart_ulp_core.h"
#include "ulp_riscv_print.h"

static ulp_riscv_uart_t s_print_uart;
#define CONFIG_EXAMPLE_UART_TXD 4

volatile uint32_t var;

int main(void) {
    ulp_riscv_uart_cfg_t cfg = {
        .tx_pin = CONFIG_EXAMPLE_UART_TXD,
    };
    ulp_riscv_uart_init(&s_print_uart, &cfg);
    ulp_riscv_print_install((putc_fn_t)ulp_riscv_uart_putc, &s_print_uart);
    ulp_riscv_print_str("ULP: waked up!\r\n");
    ulp_riscv_print_str("ULP: var: 0x");
    ulp_riscv_print_hex(var);
    ulp_riscv_print_str("\r\n");
    return 0;
}
```

Exemplo completo

O código exemplo [2] mostra como pode-se aplicar um pouco de cada um dos conhecimentos deste artigo, e foi validado em um ESP32-S3.

Postar um comentário

Deixe seu comentário ou sua sugestão!

Postagem Anterior Próxima Postagem

Formulário de contato