Lendo sensores analógicos em Deep Sleep com ULP - ESP32

Leitura de sensores analógicos pelo ADC com ULP

Neste tutorial, aprenderemos a ler um sensor analógico através do ADC no ESP32 em Deep Sleep, utilizando o ULP. Esse método de leitura, permite uma economia de bateria gigantesca, já que não precisamos acordar o microcontrolador para efetuar a leitura, situação na qual o gasto é bem maior do que quando mesmo procedimento é feito pelo ULP.

Figura 1 - ULP.

[toc]

kit robotica educacional com Arduino ESP ou Microbit

Por que utilizar?

Imagine que em seu projeto portátil, é preciso ler o sensor de temperatura a cada 30 segundos e para isso, logicamente, precisamos acordar o microcontrolador para efetuar essa tarefa, entretanto, o consumo do ESP32 nesse caso seria de 40mA apenas para ler o sensor. Com o ULP, podemos fazer essa leitura em Deep Sleep e o consumo enquanto o ULP está funcionando não passaria de 150uA, com uma média de consumo dependendo do duty cycle do ULP. Com isso, economizaríamos nesse caso de 150uA (100% duty cycle), aproximadamente 270x mais bateria do que se acordássemos o microcontrolador para essa mesma tarefa.

É fácil perceber o aumento incrível na duração da bateria que poderia ser obtido apenas ao usar o ULP para ler o sensor, as aplicações desse guerreiro são muito grandes, mas o foco é para Sleep.

Se você ainda não conhece o ULP, clique aqui para ver a introdução sobre este incrível coprocessador de baixo consumo presente no ESP32.


Mãos a obra - Lendo um sensor de temperatura em Deep Sleep

Componentes necessários

Códigos do projeto

- Main code (C ou C++), responsável pela programação do ESP32 em si.

#include <C:/msys32/ESP32/ESP32/components/arduino/cores/esp32/Arduino.h>
#include <C:/msys32/ESP32/esp-idf/components/driver/include/driver/rtc_io.h>
#include <C:/msys32/ESP32/esp-idf/components/driver/include/driver/adc.h>
#include <C:/msys32/ESP32/esp-idf/components/ulp/ulp.c>
#include <C:/msys32/ESP32/ESP32/build/main/ulp_main.h>
extern "C"
{
#include <C:/msys32/ESP32/esp-idf/components/esp32/include/esp_clk.h>
}
//Pode ser preciso arrumar os diretorios das bibliotecas
//Pode ser preciso remover o "extern 'C'{}" e definir a biblioteca fora dele, alguns usuarios relatam erro sem o uso do extern

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");
void ulp();


extern "C" void app_main()
{
	initArduino();//inicia configuracoes do arduino, caso nao use o Arduino component, remova essa linha
	pinMode(2, OUTPUT);

	if (esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_ULP)//se o wakeup for por causa do ULP, tomara alguma atitude
	{
		digitalWrite(2, 1);
		delay(500);
		digitalWrite(2, 0);
	}
	else//se nao, iniciara o ULP
	{
		ulp();//configura e inicializa o ulp
	}
	

	esp_sleep_enable_ulp_wakeup();//habilita o wakeup pelo ULP
	esp_deep_sleep_start();//entra em deep sleep eterno
}




void ulp()
{
	adc1_config_channel_atten(ADC1_CHANNEL_4, ADC_ATTEN_11db);
	adc1_config_width(ADC_WIDTH_12Bit);
	adc1_ulp_enable();
	//configura o ADC1 #4 (GPIO32) para 3.3V 12bit e atribui o uso ao ULP
	
	ulp_set_wakeup_period(0, 10000000);//ativa o timer de wakeup do ULP apos cada HALT para 10seg

	ulp_load_binary(0, ulp_main_bin_start, (ulp_main_bin_end - ulp_main_bin_start) / sizeof(uint32_t));//carrega os arquivos
	ulp_run((&ulp_main - RTC_SLOW_MEM) / sizeof(uint32_t));//inicia o ULP
}

- ULP code (Assembly .S), responsável pela programação do ULP em si.

#include "soc/soc_ulp.h"
#include "soc/rtc_io_reg.h"
#include "soc/sens_reg.h"
#include "soc/rtc_cntl_reg.h"


.bss//secao das variaveis


.text//secao do codigo


	.global main
	main:

		move r0, 0//r0 = 0
		move r1, 0//r1 = 0
		stage_rst//stage_cnt = 0

		leitura:
			stage_inc 1//stage_cnt++
			adc r1, 0, 4+1//r1 = leitura ADC GPIO32
			add r0, r0, r1//r0 = r0+r1
			jumps leitura, 4, lt//loop responsavel pelas leituras, equivale a um FOR()

		rsh r0, r0, 2//calcula a media das 4 leituras r0 = r0/4
		jumpr wakeup, 496, ge//se a media da leitura que esta em r0 for maior ou igual que 496 (40 graus celsius), acorda o mcu
		halt//coloca o ULP em sleep e ativa o timer de wakeup (definido com 10 segundos no main code)

	wakeup:
		wake//acorda o mcu
		halt

Entendendo a fundo

Software

leitura:
			stage_inc 1
			adc r1, 0, 4+1
			add r0, r0, r1
			jumps leitura, 4, lt

		rsh r0, r0, 2
		jumpr wakeup, 496, ge
		halt

A parte interessante e diferente do código é justamente a leitura do canal ADC, nós poderíamos fazer mais simples sem utilizar uma estrutura de repetição FOR(), entretanto, é sempre interessante fazer uma média de leituras (inclusive com delay's) para evitar ruídos e coisas do tipo. Já ensinamos a fazer o laço FOR() no tutorial introdutório do ULP.

  1. É feito a leitura do canal ADC no GPIO32 4x e a cada leitura, o valor é somado no registrador R0.
  2. Após as 4 leituras precisamos tirar a média entre eles, entretanto, não há mnemônicos simples para divisão como "DIV". Usaremos os operadores de BitWise (RSH - Right Shift Bit) para dividir o valor por 4. Por causa da divisão facilitada com o RSH na base 2 (2, 4, 8, 16...), também faremos uma quantidade de leituras na base 2.

Não se esqueça de conferir o Set de Instruções do ULP nas referências em caso de dúvidas.

-Mnemônico ADC

adc r1, 0, 4+1

Efetua a leitura do ADC no GPIO32 e atribui o valor ao registrador R1.

O segundo e terceiro operador (parâmetro) deste mnemônico se refere a tabela abaixo, onde:

Segundo operador: Controlador ADC (ADC1 = 0 ou ADC2 = 1). Usamos "0" pois o GPIO32 faz parte do ADC1.

Terceiro operador: MUX+1. O GPIO32 esta descrito como "...CH4", logo foi usado seu canal+1 (4+1).

Figura 2 - Pinagem MUX do ADC.

-Mnemônico RSH

rsh r0, r0, 2

O Right Shift Bit foi usado para calcular a média das 4 leituras. Lembre-se que os valores decimais (após a vírgula) são excluídos, restando apenas os inteiros.

-Mnemônico JUMPR

jumpr wakeup, 496, ge

Esse é o nosso "IF a moda antiga", que pula para a label "wakeup" se o valor do ADC for maior ou igual que 496. Ocasionando no Wake up do ESP32 pelo ULP.

Pelo fato de operações aritméticas não serem tão simples neste Assembly do ULP, em vez da condicional (jumpr) que faz o wake up do ESP32 usar valores como 32°C, onde é preciso efetuar varias contas, usaremos o valor direto do ADC que economiza processamento e tempo. O valor 496 no ADC com o LM35 equivale a 40°C .

Mais informações sobre o LM35 podem ser vistas clicando aqui.


Considerações finais

Mesmo que as aplicações do ULP estejam voltadas a Sleep, podemos usa-lo até com o ESP32 ligado, para por exemplo ler o canal ADC enquanto o ESP32 faz outra tarefa em que não se possa "perder tempo" lendo os lentos canais de ADC. Também é possível criar funções ISR para criar interrupções entre ULP e Main Core, deixando a brincadeira em um nível muito mais sério e interessante.


Desafios

O desafio desta vez é criar a rotina de interrupção citada acima (ISR) para uma comunicação extremamente rápida e eficiente entre ULP e Main Core. Você pode procurar no datasheet sobre o registrador (bit) que o comando WAKE do ULP ativa quando o ESP32 não esta em Sleep e criar a ISR.

Referências

http://esp-idf.readthedocs.io/en/latest/api-guides/ulp_instruction_set.html

https://portal.vidadesilicio.com.br/ultra-low-power-coprocessor-ulp-esp32/


Ultra Low Power coprocessor (ULP) - ESP32

Ultra Low Power coprocessor 

Neste tutorial, você irá aprender a instalar e usar o incrível ULP do ESP32, uma ferramenta extremamente poderosa e útil em projetos portáteis onde se faz o uso de baterias. Para programar o ULP, você vai precisar dos seguintes itens que não serão ensinados aqui:

  • Conhecimento em Assembly e registradores.
  • ESP-IDF instalada.

Deixaremos todas referencias sobre esses itens e os do tutorial ,como por exemplo, instalação do ULP, ao fim do tutorial.

[toc]

kit robotica educacional com Arduino ESP ou Microbit

O que é o ULP e como funciona?

Ultra Low Power coprocessor (ULP) - ESP32
Figura 1 - Coprocessador

O ULP é um coprocessador que podemos utilizar no ESP32 como um terceiro processador, entretanto, este foi projetado para uso em Deep Sleep, visto que seu consumo é extremamente baixo (150uA). Com ele, podemos fazer por exemplo, leituras de sensores ADC e a partir disso, acordar o MCU para tomar alguma decisão mais complexa, como enviar o dado para um banco de dados. Também é possível utilizar I2C e manipular os GPIO do RTC Domain.

Este pequeno processador acessa a região da memória chamada RTC_SLOW_MEM (8kB), onde é armazenado seu código ASM e também variáveis globais que são compartilhadas entre todos processadores. Com essas variáveis, podemos fazer a comunicação entre o ULP e os processadores principais (Main Core). Lembre-se que essas variáveis são mantidas entre os Deep Sleep's, então podemos guardar valores nessa região para usar após o Wake-UP.

O ULP contém 4 registradores de 16bit para uso geral (R0, R1, R2, R3) e mais um registrador de 8bit especifico para contagens (STAGE_CNT), como em loops com FOR(). Para programa-lo ,utilizamos a linguagem Assembly, uma linguagem de baixo nível extremamente rápida, eficiente e econômica em  termos de memória, mas não em linhas (hehehe).

Principais aplicações do ULP

  • Monitorar sensores digitais ou analógicos em Deep Sleep ocasionando um aumento gigantesco no tempo de duração de uma bateria.
  • Ajudar no processamento principal, caso necessário um poder de processamento maior.
  • Efetuar tarefas secundárias, como manipulação de GPIO's, sensores, criação de ISR's e etc, enquanto o Main Core faz uma tarefa pesada ou Time Sensitive.

Mãos a obra - Piscando um LED com o ULP em Assembly com um ESP32

Instalando o Core do ULP na ESP-IDF

Antes de sair usando o ULP loucamente, precisamos instalar os arquivos do Core (arquivos do sistema) para que o ULP funcione e seja compilado corretamente pelo Assembler da IDF (GNU) e após a instalação, precisamos ativa-lo e atribuir uma memória (ao mesmo), no "menuconfig".Para realizar estes procedimentos, siga os passos a seguir:

Se você ainda não tem a ESP-IDF instalada, instale-a antes de prosseguir: https://portal.vidadesilicio.com.br/instalando-esp-idf-no-windows-esp32/

1-) Baixe o .ZIP que contem os arquivos já prontos para serem colocados na IDF: https://github.com/espressif/binutils-esp32ulp/wiki

Figura 2 - Download do Core.

2-) Abra o .ZIP, acesse a pasta "esp32ulp-elf-binutils" e dentro dessa haverá outras tres pastas, copie-as para dentro do diretório "\msys32\mingw32\" da sua IDF.

Figura 3 - Copiando os arquivos do ULP pra IDF.

Já instalamos os arquivos, agora precisamos adicionar algumas linhas ao "component.mk" da pasta do seu projeto. Você precisa fazer os passos abaixo para todos projetos que pretende usar o ULP.

Você deve usar os diretórios do seu projeto.

3-) Abra o arquivo "component.mk" que se encontra na pasta "main" do seu projeto com o bloco de notas ou similar.

Figura 4 - Component.mk.

4-) Adicione o seguinte trecho ao arquivo:

ULP_APP_NAME ?= ulp_$(COMPONENT_NAME)
ULP_S_SOURCES = $(COMPONENT_PATH)/ulp/ulp.S
ULP_EXP_DEP_OBJECTS := main.o
include $(IDF_PATH)/components/ulp/component_ulp_common.mk
Figura 5 - Trecho adicionado ao mk.

5-) Abra o mingw32, vá ao diretório do seu projeto e use o "make menuconfig" para habilitar o ULP. No nosso caso, para ir ao diretório do projeto com o mingw32 é "cd /esp32/esp32".

Figura 6 - Abrindo o diretório.

Vá em "component config > esp32 specific" e habilite-o. Ao habilitar, aparecerá uma opção de configurar a memória reservada ao ULP, deixaremos o padrão por enquanto (512B).

Figura 7 - Habilitando o ULP.

Salve e saia do "menuconfig".

6-) Na pasta "main" do seu projeto, crie uma pasta "ulp" e dentro dessa, crie um arquivo "ulp.S".

Nota: você pode criar e editar (programar) o arquivo "ulp.S" com o bloco de notas caso não tenha uma IDE como Visual Studio (usada no tutorial).

Esses nomes precisam ser iguais ao trecho adicionado no "componente.mk".

Ultra Low Power coprocessor (ULP) - ESP32
Figura 8 - Arquivo do ULP.

Após a criação desse arquivo, podemos finalmente programar o incrível ULP. Esse arquivo é onde iremos programar o Assembly à ele.

Haverão dois códigos, um para o Main Core (main.c ou main.cpp) e outro pro ULP (ulp.S). O que faremos é basicamente atribuir o código Assembly ao trecho da memória RTC_SLOW_MEM e liga-lo. Após isso, o ESP32 entrará em Deep Sleep enquanto o ULP piscará um LED para mostrar funcionamento. Não se esqueça de ler o "Entendendo a fundo" que há explicações sobre o código Assembly e o funcionamento do ULP.

Estamos usando a ESP-IDF com Arduino component, isso nos permite usar tanto as bibliotecas do Arduino quanto da IDF. Se você usar a IDF sem o Arduino component, o código ficará praticamente igual, mudando apenas alguns detalhes como a alteração de extern "C" void app_main()  para void app_main() , em caso de dúvidas, comente!

Componentes necessários

  • 1x - ESP32 (Usaremos o NodeMCU32).
  • 1x - LED (Usaremos o LED Onboard).

Código do projeto

main.cpp

#include <C:/msys32/ESP32/ESP32/components/arduino/cores/esp32/Arduino.h>
#include <C:/msys32/ESP32/esp-idf/components/driver/include/driver/rtc_io.h>
#include <C:/msys32/ESP32/esp-idf/components/ulp/ulp.c>
#include <C:/msys32/ESP32/ESP32/build/main/ulp_main.h>
//Pode ser preciso arrumar os diretorios das bibliotecas

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");

void ulp();

extern "C" void app_main()
{
	initArduino();//Inicia algumas configuracoes do Arduino Core, se voce nao usa Arduino component, apague essa linha
	ulp();//carrega e inicia o ULP

	ESP.deepSleep(1800000000);//entra em deep sleep
}

void ulp()
{
	rtc_gpio_init(GPIO_NUM_2);//inicia o GPIO2 no RTC DOMAIN
	rtc_gpio_set_direction(GPIO_NUM_2, RTC_GPIO_MODE_OUTPUT_ONLY);//Define o GPIO2 como saida

	ulp_load_binary(0, ulp_main_bin_start, (ulp_main_bin_end - ulp_main_bin_start) / sizeof(uint32_t));//Carrega o binario do codigo assembly na memoria
	ulp_run((&ulp_main - RTC_SLOW_MEM) / sizeof(uint32_t));//Inicia o ULP
}

ulp.S

#include "soc/soc_ulp.h"
#include "soc/rtc_io_reg.h"
#include "soc/sens_reg.h"
#include "soc/rtc_cntl_reg.h"


.bss//Variaveis sao declaradas dentro da secao .bss


.text//O codigo é feito dentro da secao .text

	.global main
	main://O codigo e iniciado aqui, equivale ao void setup()

		

	jump loop//Isso nao e necessario, mas foi colocado para organizacao
 
	loop:
		
		WRITE_RTC_REG(RTC_GPIO_OUT_W1TS_REG, RTC_GPIO_OUT_DATA_W1TS_S+12, 1, 1)//GPIO2 HIGH
		stage_rst
		1: stage_inc 1
		wait 32000
		jumps 1b, 125, lt//delay 500ms
		

		WRITE_RTC_REG(RTC_GPIO_OUT_W1TC_REG, RTC_GPIO_OUT_DATA_W1TC_S+12, 1, 1)//GPIO2 LOW
		stage_rst
		1: stage_inc 1
		wait 32000
		jumps 1b, 125, lt//delay 500ms

		
		jump loop//volta ao inicio do loop

Colocando para funcionar

Após programar, basta dar upload para o ESP32 pelo próprio mingw32 e a mágica irá acontecer.

Ultra Low Power coprocessor (ULP) - ESP32
GIF - Blink.

Entendendo a fundo

ULP

Apesar de termos apresentado apenas uma ideia sobre o ULP, devemos entender que este é muito mais complexo que isso devido ao fato de ter como aplicação alvo o Deep sleep.

Algumas características do ULP:

  • Clock (pode ser alterado e calibrado): 8.5MHz ±7%.
  • Memória: RTC_SLOW_MEM com 8kB.
  • Timer de Wake-UP (pode ser desativado): 150kHz.
  • 4 Registradores de 16bit (R0-R3).
  • 1 Registrador de 8bit (STAGE_CNT).
  • Funciona tanto nos modos de operações normais até Deep sleep.
  • Capacidade de fazer leituras de sensores analógicos/digitais externos, I2C e vários outros itens.

Funcionamento e dicas do ULP

O ULP é um coprocessador FSM (Finite state machine) que podemos usar para praticamente qualquer objetivo, entretanto é preciso tomar alguns cuidados como o timer de Wake-UP. Esse timer que vem por padrão ativado, serve para acordar o ULP periodicamente após encontrar o comando HALT no código Assembly. No nosso exemplo não foi usado HALT para encerrar o código do ULP pois usamos ele em um loop infinito. Se você não tomar cuidado esse comando e esquecer o timer ativado (é possível desativa-lo para garantir o funcionamento em certos casos), provavelmente o ULP não irá funcionar da forma que você espera.

Após o binário ser carregado e o ULP iniciado pelo código principal, o ULP irá iniciar a execução do código na label "main" que foi definida no ulp_run() , a partir desse ponto, será executado linha após linha até encontrar o comando HALT ou cair em um loop infinito como mostrado nesse tutorial.

Ao usar o comando WAKE o ESP32 irá acordar e executar todo o código novamente do começo, isso pode gerar problemas com o ulp_run() uma vez que o ULP já esta em execução e será reiniciado, para isso você deve adicionar uma condicional de Wake-UP que verifica o motivo do ESP32 ter acordado e caso não tenha sido pelo ULP, deve ter sido queda de energia ou similares, logo, precisamos inicia-lo novamente.

Toda explicação de funcionamento do ULP, comandos do Assembly, registradores e etc, estão na referencia técnica do ESP32 que se encontra no fim do tutorial.

Software

ulp.S

-Macro WRITE_RTC_REG()

WRITE_RTC_REG(RTC_GPIO_OUT_W1TC_REG, RTC_GPIO_OUT_DATA_W1TC_S+12, 1, 1)//GPIO2 LOW

Essa macro escreve um valor no registrador, sendo bem parecido com o tutorial que já fizemos sobre: Manipulando os registradores do ESP32, Porem, é usado os registrador do RTC Domain, por isso ativamos os pino 12 do RTC, que equivale ao GPIO2.

-Delay em Assembly

stage_rst
1: stage_inc 1
wait 32000
jumps 1b, 125, lt//delay 500ms

O blink faz com que o LED fique 500ms acesso e 500ms apagado, mas como que isso foi feito? Foi usado os ciclos do clock do ULP que é por padrão 8.5MHz ±7%, vamos arredondar para 8MHz ou seja, a cada 1 segundo, o clock irá oscilar 8 milhões de vezes. Pensando nisso, podemos calcular a quantidade de ciclos necessário para efetuar um delay.

Para fazer o delay, iremos usar 3 itens que nos ajudarão a "pular" ciclos do clock, fazendo com que o processador fique em espera:

  • NOP (No Operation): pula 1 ciclo do clock.
  • WAIT X: equivale a usar vários NOP's, limite de 65535.
  • Registrador: guardará um valor auxiliar na contagem.

Veja os passos para efetuar o delay de 500ms no Assembly do ULP com o registrador STAGE_CNT:

1-) Segundos * clock = ciclos para pular

0.5 * 8000000 = 4000000

2-) Cada WAIT pode conter no máximo 65535, que não é nem perto do que precisamos (4000000), logo, vamos utilizar uma variável (registrador) auxiliar que irá se comportar igual ao FOR(). Será usado WAIT 32000, mas você pode escolher qualquer um desde que esteja dentro dos limites.

ciclos para pular / wait = total de operações do loop

4000000 / 32000 = 125 operações

Agora, basta fazer um FOR() em Assembly para que se assemelhe a isso:

for (int i = 0; i < 125; i++)
{
    //blabla
}

Usaremos o registrador STAGE_CNT que é indicado para contagens:

stage_rst//Reseta o registrador (STAGE_CNT = 0)
1: stage_inc 1//Incrementa o registrador em +1 (STAGE_CNT++)
wait 32000//Equivale a 32000 NOP's
jumps 1b, 125, lt//Pula para label "1before" enquanto o registrador for menor que 125

Considerações finais

Infelizmente pelo ESP32 ainda não "cair em uso" aqui no Brasil, só encontramos informações de fora e principalmente no Datasheet. Espero que com esse "ponta pé inicial" mais pessoas comecem a postar materiais sobre ULP, uma vez que as aplicações para esse pequeno campeão que não encontramos em qualquer lugar são gigantescas e muito importantes em projetos portáteis, onde permite o processamento de informações enquanto todo o resto do microcontrolador permanece em Sleep para economia da bateria. Bons estudos e aproveite essa ferramenta extraordinária.

Se você já conseguiu usar o ULP, veja este tutorial para leitura de sensores analógicos ou digitais em Deep Sleep com o ULP no ESP32.

https://portal.vidadesilicio.com.br/lendo-sensores-analogicos-deep-sleep-ulp-esp32/

Referencias

https://esp-idf.readthedocs.io/en/v3.0-rc1/api-guides/ulp.html

https://esp-idf.readthedocs.io/en/v3.0-rc1/api-guides/ulp_instruction_set.html

http://espressif.com/sites/default/files/documentation/esp32_technical_reference_manual_en.pdf

https://portal.vidadesilicio.com.br/instalando-esp-idf-no-windows-esp32/