Semana 5¶
Esta semanas continuaremos explorando el API de FreeRTOS. Ya casi tenemos todos los elementos básicos para comenzar a realizar aplicaciones. Es bueno aclarar en este punto que hay muchos más detalles del API que hemos dejado de lado, por tanto, aún más camino por recorrer en el futuro.
Objetivos¶
- Conocer el funcionamiento del sistea al iniciar y su arquitectura de memoria.
- Aprender algunos mecanismos de sincronización del sistema.
Consideraciones de las semanas anteriores¶
Reto de la semana pasada¶
Una posible solución al reto de la semana pasa:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | #include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "freertos/portmacro.h"
static void vSenderTask( void *pvParameters );
static void vReceiverTask( void *pvParameters );
QueueHandle_t xQueueMessage;
QueueHandle_t xQueueACK;
char * messages[] = {"Hola mundo" ,
"Cruel y despiadado",
"La vida es bella, el feo es uno",
"Hay 10 tipos de personas en el mundo: los que entienden el binario, y los que no",
"Por favor envie todo el spam a mi direccion principal, root@localhost"};
void app_main(void)
{
xQueueMessage = xQueueCreate( 1, sizeof( char * ) );
xQueueACK = xQueueCreate( 1, sizeof( uint8_t ) );
if( (xQueueMessage != NULL) &&(xQueueACK != NULL) )
{
xTaskCreate( vSenderTask, "Sender", 2048, NULL, 2, NULL );
xTaskCreate( vReceiverTask, "Receiver", 2048, NULL, 2, NULL );
}
else
{
printf("The queues could not be created.\r\n");
}
}
/*-----------------------------------------------------------*/
static void vSenderTask( void *pvParameters )
{
UBaseType_t msgIndex = 0;
char buffer[100];
char *pbuffer = buffer;
uint8_t ack = 0;
for( ;; )
{
//printf("Sending message... %d\r\n",msgIndex);
strcpy (buffer,messages[msgIndex]);
msgIndex = (msgIndex + 1) % (sizeof(messages)/sizeof(messages[0]));
xQueueSendToBack( xQueueMessage, &pbuffer, portMAX_DELAY );
xQueueReceive( xQueueACK, &ack, portMAX_DELAY);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
/*-----------------------------------------------------------*/
static void vReceiverTask( void *pvParameters )
{
char * pbuffer;
portCHAR ack = 1;
for( ;; )
{
//printf( "Waiting message...\r\n");
xQueueReceive( xQueueMessage, &pbuffer, portMAX_DELAY);
printf("Message: %s\r\n",pbuffer);
xQueueSendToBack( xQueueACK, &ack, portMAX_DELAY );
}
}
|
El resultados sera:
Message: Hola mundo
Message: Cruel y despiadado
Message: La vida es bella, el feo es uno
Message: Hay 10 tipos de personas en el mundo: los que entienden el binario, y los que no
Message: Por favor envie todo el spam a mi direccion principal, root@localhost
El código anterior tiene varias cosas interesante:
- La línea
xQueueMessage = xQueueCreate( 1, sizeof( char * ) );crea una cola de tamaño 1. El item es de 4 bytes, correspondientes al tamaño de las direcciones en el ESP-32 (sizeof(char *)). En este caso la idea es que el item de la cola almacenará la dirección del buffer con los caráctares del mensaje. msgIndex = (msgIndex + 1) % (sizeof(messages)/sizeof(messages[0]));Incrementa el índice de mensajes haciendo que al llegar a la cantidad de items contenidos en el arreglo de mensajes, se reinicie el conteo a 0 gracias a la función módulo (%).char *pbuffer = buffer;almacena la dirección del buffer con los caracteres.- En
xQueueSendToBack( xQueueMessage, &pbuffer, portMAX_DELAY );&pbufferpasa la dirección de una variable que contendrá el valor del item a enviar. En este caso, el valor es una dirección, es decir, la dirección en memoria donde están los caracteres con el mensaje. - Al
xQueueReceive( xQueueMessage, &pbuffer, portMAX_DELAY);se pasa la dirección de una variable donde se almecenará la dirección en memoria del buffer de caracteres.
Sobre la función app_main¶
Pregunta Juanito: ¿Qué es app_main? app_main es una función llamada por el framework ESP-IDF. Esta función es llamada
por la tarea principal:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | static void main_task(void* args)
{
// Now that the application is about to start, disable boot watchdogs
REG_CLR_BIT(TIMG_WDTCONFIG0_REG(0), TIMG_WDT_FLASHBOOT_MOD_EN_S);
REG_CLR_BIT(RTC_CNTL_WDTCONFIG0_REG, RTC_CNTL_WDT_FLASHBOOT_MOD_EN);
#if !CONFIG_FREERTOS_UNICORE
// Wait for FreeRTOS initialization to finish on APP CPU, before replacing its startup stack
while (port_xSchedulerRunning[1] == 0) {
;
}
#endif
//Enable allocation in region where the startup stacks were located.
heap_caps_enable_nonos_stack_heaps();
//Initialize task wdt if configured to do so
#ifdef CONFIG_TASK_WDT_PANIC
ESP_ERROR_CHECK(esp_task_wdt_init(CONFIG_TASK_WDT_TIMEOUT_S, true))
#elif CONFIG_TASK_WDT
ESP_ERROR_CHECK(esp_task_wdt_init(CONFIG_TASK_WDT_TIMEOUT_S, false))
#endif
//Add IDLE 0 to task wdt
#ifdef CONFIG_TASK_WDT_CHECK_IDLE_TASK_CPU0
TaskHandle_t idle_0 = xTaskGetIdleTaskHandleForCPU(0);
if(idle_0 != NULL){
ESP_ERROR_CHECK(esp_task_wdt_add(idle_0))
}
#endif
//Add IDLE 1 to task wdt
#ifdef CONFIG_TASK_WDT_CHECK_IDLE_TASK_CPU1
TaskHandle_t idle_1 = xTaskGetIdleTaskHandleForCPU(1);
if(idle_1 != NULL){
ESP_ERROR_CHECK(esp_task_wdt_add(idle_1))
}
#endif
app_main();
vTaskDelete(NULL);
}
|
Antes de llegar aquí han ocurrido los siguientes pasos:
- La CPU 0 o PRO_CPU inicia en la posición de memoria
0x40000400correspondiente al vector de reset del chip. - En este punto la CPU 1 o APP_CPU está en estado de reset.
- Una vez inicia la PRO_CPU se ejecuta el primer bootloader, cargado por el fabricante del chip. La función de ese bootloader
es cargar un segundo bootloader, residente en la posición
0x1000, con más funciones que el primero y cuyo código fuente se puede consultar el directoriocomponents/bootloader. El concepto de segundo bootloader permite hacer cosas como leer la tabla de particiones de la flash, implementar estrategias de encriptado de la memoria, ejecutar un proceso de carga segura de la aplicación, secure boot, y hacer actualizaciones del programa tipoover-the-airuOTA. - Espressif entrega como parte del ESP-IDF el bootloader anterior. Por tanto, es posible modificarlo completamente o cambiar su funcionamiento.
- El segundo
bootloaderlee la tabla de particiones y decide cuál aplicación cargar. Aquí es donde ocurre la majia de seleccionar entre una aplicación nueva (actualización mediante OTA) o seguir con la aplicación anterior. - El segundo bootloader también se encarga de cargar partes de la aplicación en la
IRAM(instruction RAM) oDRAM(data RAM) así como configurar las zonas de la FLASH utilizadas como IROM (instruction ROM) o DROM (data ROM). - Finalmente el segundo bootloader le entrega el control a la aplicación.
- El punto de entrada de la aplicación es
call_start_cpu0ubicado encomponents/esp32/cpu_start.c. - En
call_start_cpu0se inicia la APP_CPU que ejecutará la funcióncall_start_cpu1. Finalmente PRO_CPU saltarástart_cpu0y APP_CPU astart_cpu1. Estas de últimas funciones iniciarán el planificador en cada CPU y se creará la tareamain_taskquien finalmente llamará el punto de entrada del código de usuario que será la funciónapp_main. - Una vez se retorne de
app_mainla tareamain_taskterminará y será borrada. - Se pueden leer más detalles de este proceso aquí.
Ejercicios con el API de FreeRTOS¶
Para realizar los siguientes ejercicio es necesario tener a la mano dos documentos:
- Tutorial oficial.
- La implementación de Espressif. ESP-FREERTOS.
Ejercicio 1: memoria de aplicación¶
El código y datos de la aplicación pueden ubicarse en varias regiones de memoria: IRAM, IROM, RTC fast memory,
DRAM, DROM, RTC slow memory.
Para indicarle al enlazador (linker) que ubique códígo en la IRAM se utiliza el atributo IRAM_ATTR:
#include "esp_attr.h"
void IRAM_ATTR gpio_isr_handler(void* arg)
{
// ...
}
La definición de un servicios de atención a interrupción es un uso típico de lo anterior. Otro caso, son las funciones del API de FreeRTOS.
Por otra parte, Si una función, explícitamente, no se ubica en IRAM o en RTC, se colocará en flash, es decir, IROM.
En la región RTC fast memory, debe ubicarse el código que se ejecutará luego de despartar de una condición de
Deep sleep.
La datos constantes de la aplicación se pueden colocan en la DRAM utilizando el atributo DRAM_ATTR:
DRAM_ATTR const char[] format_string = "%p %x";
char buffer[64];
sprintf(buffer, format_string, ptr, val);
Por defecto las constantes se ubican en la DROM. Las constantes literales se embeben en el propio código de la aplicación.
Finalmente, en RTC slow memory se ubican las variables estáticas globales y globales que serán usadas desde la
memoria RTC, así:
RTC_NOINIT_ATTR uint32_t rtc_noinit_data;
Ejericio 2: comunicación entre interrupciones y tareas¶
Para este ejercicio vamos a conectar dos puertos de entrada con dos puertos de salida del microcontrolador.
Los puertos de entrada capturarán el cambio de nivel de voltaje en las salidas así: GPIO5 flancos de subida, GPIO4
flancos de subida y bajada. Los eventos anteriores serán enviados, desde un servicio de atención a interrupción,
a una tarea utilizando una cola. A esta técnica de tratamiento de las interrupciones se le conoce como PROCESAMIENTO
DIFERIDO DE INTERRUPCIONES. El código del ejemplo es el siguiente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | /* GPIO 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 <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "driver/gpio.h"
/**
* Brief:
* This test code shows how to configure gpio and how to use gpio interrupt.
*
* GPIO status:
* GPIO18: output
* GPIO19: output
* GPIO4: input, pulled up, interrupt from rising edge and falling edge
* GPIO5: input, pulled up, interrupt from rising edge.
*
* Test:
* Connect GPIO18 with GPIO4
* Connect GPIO19 with GPIO5
* Generate pulses on GPIO18/19, that triggers interrupt on GPIO4/5
*
*/
#define GPIO_OUTPUT_IO_0 18
#define GPIO_OUTPUT_IO_1 19
#define GPIO_OUTPUT_PIN_SEL ((1ULL<<GPIO_OUTPUT_IO_0) | (1ULL<<GPIO_OUTPUT_IO_1))
#define GPIO_INPUT_IO_0 4
#define GPIO_INPUT_IO_1 5
#define GPIO_INPUT_PIN_SEL ((1ULL<<GPIO_INPUT_IO_0) | (1ULL<<GPIO_INPUT_IO_1))
#define ESP_INTR_FLAG_DEFAULT 0
static xQueueHandle gpio_evt_queue = NULL;
static void IRAM_ATTR gpio_isr_handler(void* arg)
{
uint32_t gpio_num = (uint32_t) arg;
xQueueSendFromISR(gpio_evt_queue, &gpio_num, NULL);
}
static void gpio_task_example(void* arg)
{
uint32_t io_num;
for(;;) {
if(xQueueReceive(gpio_evt_queue, &io_num, portMAX_DELAY)) {
printf("GPIO[%d] intr, val: %d\n", io_num, gpio_get_level(io_num));
}
}
}
void app_main()
{
gpio_config_t io_conf;
//disable interrupt
io_conf.intr_type = GPIO_PIN_INTR_DISABLE;
//set as output mode
io_conf.mode = GPIO_MODE_OUTPUT;
//bit mask of the pins that you want to set,e.g.GPIO18/19
io_conf.pin_bit_mask = GPIO_OUTPUT_PIN_SEL;
//disable pull-down mode
io_conf.pull_down_en = 0;
//disable pull-up mode
io_conf.pull_up_en = 0;
//configure GPIO with the given settings
gpio_config(&io_conf);
//interrupt of rising edge
io_conf.intr_type = GPIO_PIN_INTR_POSEDGE;
//bit mask of the pins, use GPIO4/5 here
io_conf.pin_bit_mask = GPIO_INPUT_PIN_SEL;
//set as input mode
io_conf.mode = GPIO_MODE_INPUT;
//enable pull-up mode
io_conf.pull_up_en = 1;
gpio_config(&io_conf);
//change gpio intrrupt type for one pin
gpio_set_intr_type(GPIO_INPUT_IO_0, GPIO_INTR_ANYEDGE);
//create a queue to handle gpio event from isr
gpio_evt_queue = xQueueCreate(10, sizeof(uint32_t));
//start gpio task
xTaskCreate(gpio_task_example, "gpio_task_example", 2048, NULL, 10, NULL);
//install gpio isr service
gpio_install_isr_service(ESP_INTR_FLAG_DEFAULT);
//hook isr handler for specific gpio pin
gpio_isr_handler_add(GPIO_INPUT_IO_0, gpio_isr_handler, (void*) GPIO_INPUT_IO_0);
//hook isr handler for specific gpio pin
gpio_isr_handler_add(GPIO_INPUT_IO_1, gpio_isr_handler, (void*) GPIO_INPUT_IO_1);
//remove isr handler for gpio number.
gpio_isr_handler_remove(GPIO_INPUT_IO_0);
//hook isr handler for specific gpio pin again
gpio_isr_handler_add(GPIO_INPUT_IO_0, gpio_isr_handler, (void*) GPIO_INPUT_IO_0);
int cnt = 0;
while(1) {
printf("cnt: %d\n", cnt++);
vTaskDelay(1000 / portTICK_RATE_MS);
gpio_set_level(GPIO_OUTPUT_IO_0, cnt % 2);
gpio_set_level(GPIO_OUTPUT_IO_1, cnt % 2);
}
}
|
El resultado es:
cnt: 1
GPIO[4] intr, val: 1
GPIO[5] intr, val: 1
cnt: 2
GPIO[4] intr, val: 0
cnt: 3
GPIO[4] intr, val: 1
GPIO[5] intr, val: 1
cnt: 4
GPIO[4] intr, val: 0
cnt: 5
GPIO[4] intr, val: 1
GPIO[5] intr, val: 1
cnt: 6
GPIO[4] intr, val: 0
cnt: 7
Varias consideraciones:
- La interrupción de más baja prioridad interrumpirá la tarea de más alta prioridad. Las tareas son manejadas por software, mientras que las interrupciones son lanzadas por hardware.
- Debido a lo anterior, es recomendable que las interrupciones sean, en lo posible, muy cortas y el procesamiento diferido.
- El procesamiento de interrupciones diferido consiste en que la interrupción registra la causa de la interrupción y le informa a una tarea acerca de ésta, es decir, la interrupción delega el procesamiento a una tarea. Esto permite salir rápidamente de la interrupción.
- Las interrupciones utilizan funciones del API de FreeRTOS especialmente disañadas para su contexto. El nombre de las
funciones utilizadas por las interrupciones es casi idéntico a las funciones que usan las tareas más la terminación
FromISR. Por ejemplo, en el código anteriorxQueueSendFromISR(gpio_evt_queue, &gpio_num, NULL);. - Algunas funciones del API para las interrupciones incluyen el puntero
pxHigherPriorityTaskWoken. Dichas funciones colocarán enpdTRUEla variable apuntada si el llamado a la función hace que una tarea de más alta prioridad que la tarea que actualmente está corriendo se desbloquee. Por tanto, la variable apuntada porpxHigherPriorityTaskWokendebe inicializarse enpdFALSE.
Ejercicio 3: semáforos binarios, RETO!¶
Los semáforos binarios permiten informarle a una tarea que el evento por el cual esperan ha ocurrido. De esta manera,
mediante el uso de un semáforo binario, es posible sincronizar tareas o una tarea con una interrupción. El API
para utilizar semáforos binarios está definido aquí freertos/include/freertos/semphr.h. Cuando un semáforo binario se
utiliza para sincronizar una interrupción con un tarea, la interrupción ejecutará continuamente una operación give sobre
el semáforo, mientras que la tarea realizará un take. Si el semáforo no está disponible, la operación take
bloqueará la tarea hasta que la interrupción realice la operación give, momento en el cual la tarea bloqueada estará lista
para correr. La siguiente figura ilustra el funcionamiento de un semáforo binario:
El reto consiste en repetir el ejercicio anterior pero sólo con una pareja de puertos de entrada salida y utilizando un semáforo binario para realizar el procesamiento diferido de la interrupción.