Agora que já sabemos programá-lo, vamos colocá-lo para ajudar no co-processamento do sistema principal, onde reside os 2 núcleos principais do ESP32, cada um rodando a 240 MHz. A ideia deste post é tentar simular alguma tarefa pesada para o microcontrolador, em que qualquer desvio de código acarreta em uma visível falha que pode ser relevante ou não.

Um método bem simples de estressar o microcontrolador é um loop infinito sem delay efetuando alguma tarefa. Pensando nisso, faremos um PWM via software que é extremamente pior do que um PWM por hardware como Timers, que não são interferidos pelo código em si, como desvio por interrupções ou delays.

O PWM por software sofrerá com qualquer desvio de programação, inclusive interrupções externas, e é isso que será mostrado.

Vamos primeiramente entender a situação que nosso microcontrolador enfrentará:

Agora que já sabemos como o sistema precisa se comportar, vamos primeiramente testar o método de utilizar uma interrupção no mesmo núcleo que gera o PWM de 4 MHz. Veja o fluxograma que apenas idealiza como o processo é executado:

Fluxograma do código com interrupção.
Figura 1 – Fluxograma do código com interrupção.

Código C++:

void atd()
{
	REG_WRITE(GPIO_OUT_W1TS_REG, BIT2);//Ativa o atuador
}

extern "C" void app_main()
{

	pinMode(13, OUTPUT);//Pino do PWM 4MHz
	pinMode(02, OUTPUT);//Pino do atuador
	attachInterrupt(0, atd, FALLING);//Interrupcao do botao de panico


	while (1)//PWM de 4MHz
	{
		REG_WRITE(GPIO_OUT_W1TS_REG, BIT13);
		asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");
		asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");
		asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");
		asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");
		REG_WRITE(GPIO_OUT_W1TC_REG, BIT13);
		asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");
		asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");
		asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");
		asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");
	}
	
}

Com um analisador lógico podemos visualizar os pinos do microcontrolador numa linha do tempo e ver como o sistema se comportou quando o botão de pânico foi pressionado.

Observações:

Analisador lógico no código com interrupção.
Figura 2 – Analisador lógico no código com interrupção.

É possível mais que claramente ver a ineficiência de tratamento da interrupção e gerência do PWM de alta frequência ao mesmo tempo. Mesmo o ESP32 trabalhando em 240 MHz, seu tratamento convencional de interrupções não é tão bom se comparado com outras arquiteturas de microcontroladores, como AVR, sendo possível ver que desde o botão ser pressionado e o atuador ligar, passaram-se aproximadamente 1,3 us, o que é relativamente lento. Todo o processo de desvio da interrupção até o retorno do PWM durou aproximadamente 3,5 us e isso não é tolerável no projeto. Apesar dos métodos convencionais serem lentos, é possível atribuir, via Assembly, interrupções de baixa latência diretamente na arquitetura da XTensa, mas não vamos tão a fundo por um problema que pode ser resolvido mais facilmente com algum dos outros 2 processadores.

Você pode estar se perguntando por que não atribuí a interrupção ao outro núcleo, já que o ESP32 conta com 2 núcleos principais e podemos deixar as tarefas separadas por núcleo, mas o outro núcleo também está ocupado com outra tarefa, então sobrou o ULP.

Vamos então programá-lo para ajudar o processamento do sistema principal. O ULP ficará encarregado de tratar todos sensores e atuadores do nosso sistema, que nesse caso é apenas um de cada, mas já é suficiente para analisar como é eficiente no co-processamento, visto que quanto mais sensores, mais o sistema principal seria prejudicado.

Fluxograma do código com ULP do ESP32 para co-processamento
Figura 3 – Fluxograma do código com ULP

Código C++:

extern "C"
{
#include <driver/gpio.h>
#include <driver/rtc_io.h>
#include <ulp/ulp.c>
#include <ulp_main.h>
}

extern const uint8_t ulp_main_bin_start[] asm("_binary_ulp_main_bin_start");//Inicio do binario
extern const uint8_t ulp_main_bin_end[] asm("_binary_ulp_main_bin_end");//Fim do binario


void initULP()
{
	//Configura o GPIO0 como entrada no RTC Domain (ULP reside no RTC Domain)
	rtc_gpio_init(GPIO_NUM_0);
	rtc_gpio_set_direction(GPIO_NUM_0, RTC_GPIO_MODE_INPUT_ONLY);


	//Configura o GPIO2 como saida no RTC Domain (ULP reside no RTC Domain)
	rtc_gpio_init(GPIO_NUM_2);
	rtc_gpio_set_direction(GPIO_NUM_2, RTC_GPIO_MODE_OUTPUT_ONLY);



	ulp_load_binary(0, ulp_main_bin_start, (ulp_main_bin_end - ulp_main_bin_start) / sizeof(uint32_t));//Carrega o binario na RTC_SLOW_MEM
	ulp_run((&ulp_main - RTC_SLOW_MEM) / sizeof(uint32_t));//Inicializa o ULP
}


extern "C" void app_main()
{
	initULP();//Funcao que inicializa o ULP

	//Configura o GPIO13 como saida
	gpio_pad_select_gpio(13);
	gpio_set_direction(GPIO_NUM_13, GPIO_MODE_OUTPUT);


	while (1)//PWM 4 MHz
	{
		REG_WRITE(GPIO_OUT_W1TS_REG, BIT13);//GPIO13 = HIGH
		asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");
		asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");
		asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");
		asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");
		REG_WRITE(GPIO_OUT_W1TC_REG, BIT13);//GPIO13 = LOW
		asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");
		asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");
		asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");
		asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");asm("NOP");
	}

}

Código ASM:

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


.bss//Declaracao de variaveis aqui


.text

	.global main
	main://Inicio do codigo (Entry point)
		WRITE_RTC_REG(RTC_GPIO_OUT_W1TC_REG, RTC_GPIO_OUT_DATA_W1TC_S+12, 1, 1)//Desliga o atuador (GPIO2 = LOW)



	loop:

		READ_RTC_REG(RTC_GPIO_IN_REG, RTC_GPIO_IN_NEXT_S+11, 1)//Le o estado do GPIO0 e guarda no R0
		jumpr on, 1, lt//Se o botao for pressionado (0), ativa o atuador

		WRITE_RTC_REG(RTC_GPIO_OUT_W1TC_REG, RTC_GPIO_OUT_DATA_W1TC_S+12, 1, 1)//Caso o botao nao esteja pressionado, mantem o atuador desligado


	jump loop//retorna ao loop


	on://ativa o atuador e retorna ao loop
		WRITE_RTC_REG(RTC_GPIO_OUT_W1TS_REG, RTC_GPIO_OUT_DATA_W1TS_S+12, 1, 1)//Ativa o atuador (GPIO2 = HIGH)
	jump loop

Vamos analisar novamente pelo analisador lógico, como o sistema se comportou quando o botão de pânico foi pressionado.

Observações:

Analisador lógico no código do ULP do ESP32 para co-processamento
Figura 4 – Analisador lógico no código do ULP.

Observe que mesmo durante o evento (pressionar do botão e atuador ativar) o PWM continuou perfeitamente como o esperado (4 MHz), mostrando a eficiência e importância de usar outro núcleo/microcontrolador para ajudar no processamento.

Os co-processadores têm uma trajetória relativamente importante para computação atual, sendo um dos mais famosos a Float Point Unit (FPU), que é um co-processador para efetuar cálculos de ponto flutuante presente na maioria dos dispositivos atuais, inclusive no ESP32. Os co-processadores livram o processador central de alguma tarefa, tornando o sistema, em geral, mais rápido.

O ULP pode ter poucos Mnemônicos (Instruction set limitado), mas se torna importantíssimo em projetos específicos, como nos 2 citados neste artigo. O simples fato de conseguir ler pinos digitais/analógicos e controlar pinos já o torna um aliado interessante para você aprender e utilizar.

Saiba mais

MSP430 – Modos de Low-Power

Idealização de um projeto IoT portátil

Embarcados interview: Jack Ganssle