Unidad 2: hello world¶
Sesión 1¶
En esta sesión haremos lo siguiente:
Demo de la aplicación 1_hello_world funcionando.
¿Cómo obtengo el código del proyecto?
Observaremos el repositorio del fabricante.
¿Cómo hago I/O básica?
Escribir un pin.
Leer un pin.
Interactuar con el usuario usando el puerto serial.
Ejercicios¶
Ejercicios 1: salida digital¶
En este ejercicio vamos a realizar un programa que encienda y apague un LED a 1 Hz.
Lo primero que deberás hacer es conectar al puerto 5 del microcontrolador un LED con la resistencia de 330 ohm. Dependiendo del sistema de desarrollo que hayas comprado es posible que ya tengas el LED.
Ahora escribe el siguiente código y lee los comentarios con los pasos que debes seguir.
1#include <stdio.h>
2
3// 1. Incluir los archivos de cabecera necesarios
4#include "freertos/FreeRTOS.h"
5#include "freertos/task.h"
6#include "driver/gpio.h"
7
8// 2. Definir el PIN de salida ha utilizar
9#define PIN 5
10
11void app_main(void)
12{
13 // 3. asociar el pin del micro con el circuito de GPIO
14 gpio_pad_select_gpio(PIN);
15
16 // 4. Configurar el puerto como salida.
17 gpio_set_direction(PIN, GPIO_MODE_OUTPUT);
18
19 uint8_t portLevel = 0;
20
21 while(1)
22 {
23 // 5. Manda al puerto el valor de salida
24 gpio_set_level(PIN, portLevel);
25 portLevel = !portLevel;
26 // 6. Espera en cada nivel del LED 500 ms
27 vTaskDelay(500/portTICK_PERIOD_MS);
28 }
29}
Ejercicio 2: entrada digital¶
Ahora vas a leer el estado de un pulsador y enviaras dicho estado por el puerto serial. Debes hacer la lectura cada segundo.
Debes asegurarte que tu pulsador tiene patas largas y entra adecuadamente en el protoboard, de lo contrario tendrás lecturas erráticas debido al mal contacto entre el pulsador y el protoboard.
Ahora escribe el siguiente código y lee los comentarios con los pasos que debes seguir.
1#include <stdio.h>
2
3// 1. Incluir los archivos de cabecera necesarios
4#include "freertos/FreeRTOS.h"
5#include "freertos/task.h"
6#include "driver/gpio.h"
7#include "esp_log.h"
8
9// 2. Definir el PIN de entrada a utilizar
10#define PULSADOR_PIN 19
11
12void app_main(void)
13{
14 // 3. asociar el pin del micro con el circuito de GPIO
15 gpio_pad_select_gpio(PULSADOR_PIN);
16
17 // 4. Configurar el puerto como entrada.
18 gpio_set_direction(PULSADOR_PIN, GPIO_MODE_INPUT);
19
20 // 5. Habilitar la resistencia interna de pull up
21 gpio_pullup_en(PULSADOR_PIN);
22
23 int pulsadorState;
24
25 while(1)
26 {
27
28 // 6. Lee el estado del pulsador
29 pulsadorState = gpio_get_level(PULSADOR_PIN);
30
31 // 7. Reportar por el puerto serial
32 printf("Estado del pulsador: %d\n", pulsadorState);
33
34 // 8. Espera un segundo para volver a leer
35 vTaskDelay(1000/portTICK_PERIOD_MS);
36 }
37}
Ejercicio 3: reto¶
Ahora que ya sabes escribir y leer puertos del microcontrolador te propongo que hagas un programa que lea el estado del pulsador y si este está presionado encienda el LED, de lo contrario lo apague.
Ejercicio 4: leer el puerto serial¶
El siguiente programa leerá un mensaje que el usuario escriba en el monitor serial. Si el mensaje es hola, el microcontrolador deberá responder con la palabra mundo, de lo contrario responderá con la frase: no entiendo.
1#include <stdio.h>
2#include "freertos/FreeRTOS.h"
3#include "freertos/task.h"
4#include "driver/gpio.h"
5#include "esp_log.h"
6#include "string.h"
7
8void app_main(void)
9{
10 int c;
11 char message[16];
12 uint8_t counter = 0;
13
14 while(1)
15 {
16 c = getchar();
17
18 if(c != EOF){
19
20 if( '\n' == c)
21 {
22 // Termina el mensaje con 0
23 message[counter] = 0;
24 // Verifica si el mensaje es hola
25 if ( strncmp (message, "hola", strlen("hola") ) == 0)
26 {
27 printf(" mundo\n");
28 }
29 else
30 {
31 printf("no entiendo\n");
32 }
33 counter = 0;
34 }
35 else
36 {
37 // Solo guarda un carácter si hay espacio
38 if( counter < ( sizeof(message) - 1 ) )
39 {
40 message[counter] = c;
41 counter++;
42 }
43 }
44 }
45
46 vTaskDelay(100/portTICK_PERIOD_MS);
47 }
48}
En este programa ten presente los siguientes puntos:
La función
getcharintentará obtener un byte del puerto serial. Si el byte existe lo retornará. Si no existe retornará un EOF (end of file) que corresponde a un-1.La variable en la cual almacenamos el valor retornado por
getchares unint. En el ESP32 losintson variables de 32 bits que puede almacenar número negativos. Esto es necesario para poder contener el EOF; sin embargo, cuando hay un byte en el puerto serial, dicho byte es almacenado en la variableintcomo un entero de 8 bits sin signo correspondiente, precisamente, al byte recibido.Nota que el arreglo
messagepuede almacenar 16 bytes, de los cuales, 15 serán para almacenar los bytes que llegan del puerto serial, excepto elENTERque decidimos no guardar, y uno será para terminar la secuencia de caracteres en 0. Terminar una cadena de caracteres en 0 es fundamental en C para poder identificar el punto final de la secuencia.
Ejercicio 5: reto¶
Ahora que ya sabes escribir y leer un pin del microcontrolador y enviar y recibir mensajes por el puerto serial te voy a proponer que hagas un programa que te permita hacer los siguiente:
Al enviar la cadena de caracteres
ledOnpor el puerto serial enciende un LED.Al enviar la cadena de caracteres
ledOffpor el puerto serial apaga el LED.Al enviar la cadena de caracteres
readButtonpor el puerto serial lee el estado del pulsador y lo escribe en el puerto serial.
Ejercicio 6: reto¶
Ahora vas a añadir más comandos al programa anterior. Abre el ejemplo hello_world que está en la carpeta examples del framework. Con la información que hay allí incorpora los siguientes comandos y escribe el resultado en el puerto serial:
Leer la revisión del chip:
rev.Leer la versión de framework :
idf.Leer si está habilitado el bluetooth clásico.
Leer si está habilitado el bluetooth low energy.
Leer si está habilitado el WiFi.
Leer la cantidad de memoria flash.
Un minireto extra porque deberás buscar en el API del fabricante que función te da esta información:
Leer la dirección MAC del microcontrolador.
Ejercicio 7: build¶
Para entender cómo se hace el BUILD de una aplicación dale una mirada a este enlace. No tienes que leer todo el documento, pero si lo suficiente para que entiendas cómo podrías iniciar un proyecto de cero, es decir, si fueras a incluir uno por uno cada archivo para configurar y construir (build) un proyecto.
Ejercicio 8: build/reto¶
En la unidad anterior te propuse que copiaras uno de los proyectos
que trae como ejemplo el esp-idf (hello_world). Con el conocimiento del ejercicio
anterior, te propongo que intentes crear un proyecto desde cero, es decir, crea una
carpeta y luego incluye uno a uno los archivos necesarios para definir el proyecto.
Realiza un programa simple y comprueba que es posible construir y grabar el programa
en la memoria del microcontrolador.
Ejercicio 9: proyecto de curso¶
Ahora vas a comenzar el proyecto de curso: ESP-Jumpstart. NO HAGAS nada por favor, solo
lee el contenido de este
enlace donde se introduce el proyecto.
Luego lee este otro enlace. POR FAVOR, no hagas nada, solo lee. En los siguientes ejercicios te diré que harás.
Reflexiona con estas preguntas:
En la introducción te dicen que el proyecto que haremos en el curso es un proyecto de producción. ¿Qué deberías cambiar para personalizar tu proyecto?
En el proyecto de curso ¿Vamos a controlar remotamente al microcontrolador?
En el proyecto de curso ¿Vamos a actualizar remotamente el programa grabado en la memoria del microcontrolador?
¿Cuál es la diferencia entre el ESP-IDF y el Toolchain del compilador?
Al instalar el entorno de trabajo para el ESP-IDF te pedí que instalaras la versión 4.2 del ESP-IDF; sin embargo, en la documentación del proyecto sugieren la versión 4.0. Lee de este enlace hasta la sección
Support Periods. ¿Cuál será la diferencia entre la versión 4.0 y la versión 4.2?
Ejercicio 10: versión del ESP-IDF¶
Abre el acceso directo al ESP-IDF que tienes en el escritorio de Windows. Si estás en Linux no olvides ejecutar el alias get_idf para adicionar al path el entorno de trabajo.
Navega hasta el último ejercicio en el que has trabajado. Graba el microcontrolador y ejecuta el programa monitor. Termina el programa monitor. Sube por el terminal hasta encontrar el punto donde lanzaste el monitor:
juanfranco@pop-os:~/esp-idf-course/projects/testsClass$ idf.py monitor
Executing action: monitor
Choosing default port b'/dev/ttyUSB0' (use '-p PORT' option to set a specific serial port)
Running idf_monitor in directory /home/juanfranco/esp-idf-course/projects/testsClass
Executing "/home/juanfranco/.espressif/python_env/idf4.2_py3.8_env/bin/python /home/juanfranco/esp-idf-course/esp-idf/tools/idf_monitor.py -p /dev/ttyUSB0 -b 115200 --toolchain-prefix xtensa-esp32-elf- /home/juanfranco/esp-idf-course/projects/testsClass/build/hello-world.elf -m '/home/juanfranco/.espressif/python_env/idf4.2_py3.8_env/bin/python' '/home/juanfranco/esp-idf-course/esp-idf/tools/idf.py'"...
--- idf_monitor on /dev/ttyUSB0 115200 ---
--- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H ---
ets Jun 8 2016 00:22:57
rst:0x1 (POWERON_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:2
load:0x3fff0030,len:4
load:0x3fff0034,len:7200
ho 0 tail 12 room 4
load:0x40078000,len:13212
load:0x40080400,len:4568
0x40080400: _init at ??:?
entry 0x400806f4
I (30) boot: ESP-IDF v4.2-238-g8cd16b60f 2nd stage bootloader
Observa la última línea:
I (30) boot: ESP-IDF v4.2-238-g8cd16b60f 2nd stage bootloader
Este te dice la versión del esp-idf que está usando tu programa.
Ahora escribe el comando:
idf.py --version
Debería ver algo similar a esto:
ESP-IDF v4.2-238-g8cd16b60f
Nota entonces que las versiones deben coincidir.
¿Pero qué significa lo que ves?
v4.2 es la versión del framework
238 es el número de commits en el repositorio público después de que el fabricante liberara la versión. Eso quiere decir que se han realizado 238 actualizaciones con mejoras o correcciones a bugs.
g8cd16b60f es el SHA (el hash) del commit correspondiente a esta versión del esp-idf que estamos usando.
En este punto te preguntarás. ¿Para qué estamos mirando todo esto? Por que en un ambiente de producción de una aplicación embebida para IoT debes tener control sobre estos aspectos para poder dar el soporte apropiado a tus productos.
Ejercicio 11: actualización del esp-idf¶
Abre de nuevo el acceso directo al ESP-IDF. Verifica que estás en la carpeta del framework. Ejecuta el comando:
git branch
Deberías ver algo así:
* release/v4.2
¿Qué indica esto? Que tu esp-idf está en la versión 4.2, es decir,
que tenemos checkout la versión 4.2 en nuestro computador. ¿En nuestro
computador tenemos más versiones? Tendrás que verificarlo. Ejecuta este comando:
git branch -a
Deberías ver algo así:
* release/v4.2
remotes/origin/HEAD -> origin/master
remotes/origin/audio/stack_on_psram_v3.3
remotes/origin/ble_mesh_release/esp-ble-mesh-v0.6.1
remotes/origin/customer/maintain_v4.0_xiaomi_tsf_issue
remotes/origin/customer/maintain_xiaomi_11kv_v4.0.2
remotes/origin/feature/ftm_support
remotes/origin/ftm
remotes/origin/master
remotes/origin/release/v2.0
remotes/origin/release/v2.1
remotes/origin/release/v3.0
remotes/origin/release/v3.1
remotes/origin/release/v3.2
remotes/origin/release/v3.3
remotes/origin/release/v4.0
remotes/origin/release/v4.1
remotes/origin/release/v4.2
En mi caso, solo tengo en el computador la versión v4.2 y el * al lado
izquierdo de la versión v4.2 indica que es la versión activa. ¿Ves que hay
otras entradas que dicen remotes? estas son otras versiones del esp-idf que
están en la cuenta en GitHub del fabricante, pero localmente, solo tenemos la
4.2.
Ahora escribe
git status
En mi caso el resultado es:
On branch release/v4.2
Your branch is up to date with 'origin/release/v4.2'.
nothing to commit, working tree clean
¿Qué quiere decir lo que ves? quiere decir que la rama activa en este momento es la release/v4.2 y que está actualizada con la rama origin/release/v4.2 que se encuentra en Internet y es administrada por parte del fabricante. ¿Qué implica esto? Implica que el código que tenemos en nuestro computador está sincronizado con el código del fabricante y está actualizado.
¿Y si no fuera así? ¿Si no estuviera sincronizado? Primero debes decidir si realmente quieres la actualización. Una buena razón para querer la actualización es cuando el fabricante ha corregido errores o problemas.
En este punto te voy a recomendar que instales un cliente gráfico de Git. A mi me gusta este pero puedes usar otro si gustas.
Una vez instales el cliente gráfico, abre el repositorio local del esp-idf.
Si tu repositorio local y el remoto del fabricante está actualizado verás algo similar a esto:
Nota cómo el ícono de un computador y el logo del fabricante están en la misma fila. Esto indica que están actualizados. Si no es así vas a darle doble click a la rama release/v4.2 que tiene el ícono del computador para asegurarte que es la rama activa. Luego darás click en el botón pull. Si te pide que envíes algo dale click en enviar. Si todo sale bien verás que ahora los íconos estarán en la misma fila y por tanto tendrás los repositorios sincronizados.
Por último vas a actualizar los submódulos que tenga el repositorio. ¿Qué es un submódulo? Pues es un repositorio dentro de otro repositorio.
No olvides que debes estar en la raíz de la carpeta del esp-idf. Escribe este comando y ya está.
git submodule update --init --recursive
Ejercicio 12: descarga del proyecto del curso¶
Ahora vamos a descargar todo el código del proyecto del curso. La idea es que coloques este código en la carpeta donde has venido guardando todos los códigos de los ejercicios.
Abre la carpeta raíz donde están todos tus ejercicios y escribe:
git clone --recursive https://github.com/espressif/esp-jumpstart
Abre la carpeta esp-jumstart. Ahí estará el proyecto completo del curso dividido en 7 partes:
1_hello_world 3_wifi_connection 5_cloud 7_mfg components LICENSE README.md
2_drivers 4_network_config 6_ota CHANGES.md docs README_cn.md
Ejercicio 13: corre el proyecto 1_hello_world¶
Ahora abre el proyecto 1_hello_world. Realiza el build, flash, monitor.
Analiza el programa. En este punto del curso deberías entender qué hace.
Ejercicio 14: ¿Cómo llegamos a app_main?¶
En este punto ya te has dado cuenta que el programa inicia ejecutando la función app_main. ¿Pero quién llama a esa función? ¿Qué pasa antes de que app_main sea llamada?
Lee este enlace.
La idea no es que entiendas cada detalle que se discute en la lectura, pero si que reflexiones sobre algunas cuestiones importantes para nuestro proyecto de curso.
¿Cuántos core simétricos (similares) tiene el ESP32?
Son dos core. Uno se llama PRO y el otro CPU.
¿Qué es un bootloader?
Es un programa que se encarga de cargar otro programa.
¿Por qué hay dos booloaders?
El primer bootloader viene grabado en una memoria interna del ESP32 (ROM). El objetivo de ese bootloader es cargar un segundo bootloader que puede programar el propio usuario. En tu caso el bootloader ya está programado por el fabricante.
¿Para qué sirve ese segundo booloader?
Sirve para flexibilizar el proceso de carga de tu aplicación. Por ejemplo, considera que encontraste un error en tu programa y deseas hacer una actualización remota para corregir ese error. Para hacerlo, vas a necesitar que el programa que actualmente ejecuta el ESP32 descargue la nueva versión y la grabe en la memoria. En este punto pueden ocurrir dos cosas. La primera es que el programa se descargue y se grabe correctamente en la memoria. La segunda es que por algún motivo no se pueda descargar o grabar correctamente. En el segundo caso, vas a querer que el ESP32 al menos ejecute el programa que tiene ahora, aunque tenga errores. En el primer caso, vas a querer ejecutar el nuevo programa. Y he aquí la pregunta del millón, cuando el ESP32 se reinicie ¿Cuál aplicación se ejecutará? Pues precisamente esa puede ser una función del segundo bootloader, es decir, elegir cuál de las dos versiones ejecutar. Bien, y si no tuvieras dos aplicaciones grabadas en la memoria sino 3 ¿Cómo haría el ESP32 para seleccionar cuál ejecutar? De nuevo, el segundo booloader podría incluir un mecanismo que lea el estado de algunos pines externos del ESP32 que le indiquen cuál aplicación elegir. Entonces ¿Ves el punto? el segundo bootloader te da flexibilidad.
¿Qué es FreeRTOS?
FreeRTOS es un sistema operativo de tiempo real. Una de sus funciones principales es administrar el uso de los recursos de procesamiento del ESP32, es decir, los core PRO y APP. Para hacer uso de esos recursos, se debe crear una abstracción en el programa llamada TAREA. En principio, tu no tiene que crear ninguna tarea porque en el código del esp-idf se crea una tarea por ti denominada MAIN TASK.
¿Qué es una tarea?
Es una abstracción del sistema operativo que permite ejecutar un flujo de instrucciones de manera independiente a otras tareas o flujos de instrucciones.
¿Qué es un flujo de instrucciones?
Es una secuencia de operaciones de bajo nivel (lenguaje de máquina) que debe ejecutar un core de la CPU. Esas instrucciones son producidas en el proceso de construcción de los programas.
¿Qué parte del sistema operativo decide qué tarea será ejecutada y en cuál core de la CPU?
Esa función la tiene el scheduler que es la parte del sistema operativo encargada de
administrar los recursos de procesamiento.
Ahora observa parte del código de MAIN TASK:
1static void main_task(void* args)
2{
3 #if !CONFIG_FREERTOS_UNICORE
4 // Wait for FreeRTOS initialization to finish on APP CPU, before replacing its startup stack
5 while (port_xSchedulerRunning[1] == 0) {
6 ;
7 }
8 #endif
9 .
10 .
11 .
12 .
13
14 #ifdef CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1
15 TaskHandle_t idle_1 = xTaskGetIdleTaskHandleForCPU(1);
16 if(idle_1 != NULL){
17 ESP_ERROR_CHECK(esp_task_wdt_add(idle_1));
18 }
19 #endif
20
21 app_main();
22 vTaskDelete(NULL);
23}
MAIN TASK llama a la función app_main.
Ejercicio 15: lenguaje C- structs y pointers¶
Estos dos conceptos del lenguaje C son fundamentales para comprender el proyecto del curso.
Comencemos con el concepto de estructura o struct. Esta facilidad del lenguaje nos permite crear nuestros propios tipos de datos compuestos
Considera el siguiente código tomado de un ejemplo del fabricante:
1#define GPIO_OUTPUT_IO_0 18
2#define GPIO_OUTPUT_IO_1 19
3#define GPIO_OUTPUT_PIN_SEL ((1ULL<<GPIO_OUTPUT_IO_0) | (1ULL<<GPIO_OUTPUT_IO_1))
4
5void app_main(void)
6{
7 gpio_config_t io_conf;
8 io_conf.intr_type = GPIO_INTR_DISABLE;
9 io_conf.mode = GPIO_MODE_OUTPUT;
10 io_conf.pin_bit_mask = GPIO_OUTPUT_PIN_SEL;
11 io_conf.pull_down_en = 0;
12 io_conf.pull_up_en = 0;
13 gpio_config(&io_conf);
14 .
15 .
16 .
El código anterior muestra cómo configurar el pin 18 y 19 del ESP32 como dos pines de salida.
Para ello nota que estamos definiendo una nueva variable llamada io_conf. Dicha
variable es un STRUCT definida así:
1typedef struct {
2 uint64_t pin_bit_mask; /*!< GPIO pin: set with bit mask, each bit maps to a GPIO */
3 gpio_mode_t mode; /*!< GPIO mode: set input/output mode */
4 gpio_pullup_t pull_up_en; /*!< GPIO pull-up */
5 gpio_pulldown_t pull_down_en; /*!< GPIO pull-down */
6 gpio_int_type_t intr_type; /*!< GPIO interrupt type */
7} gpio_config_t;
Como puedes ver gpio_config_t es el nombre de la struct que te permite
crear un nuevo tipo de dato compuesto por otros tipos de datos: uint64_t,
gpio_mode_t, gpio_pullup_t, gpio_pulldown_t y gpio_int_type_.
Ahora veamos los pointers. Nota la línea de código:
1gpio_config(&io_conf);
La función gpio_config está definida así:
1esp_err_t gpio_config(const gpio_config_t *pGPIOConfig)
gpio_config devuelve un error de tipo esp_err_t y recibe
la dirección de una variable de tipo gpio_config_t.
Entonces un pointer es una variable que sirve para almacenar la dirección de otra variable.
Un pointer lo declaras así:
1gpio_config_t *pGPIOConfig;
pGPIOConfig es el nombre de la variable que almacenará la dirección de una struct
de tipo gpio_config_t. Observa la diferencia con esta declaración:gpio_config_t io_conf;
1gpio_config_t io_conf;
io_conf es una variable que almacena una struct de tipo gpio_config_t.
¿Ves la diferencia? Al colocar un * antes del nombre de la variable, estás
indicando que la variable será un pointer o una variable que almacena direcciones
de variables.
¿Cómo obtengo la dirección de una variable?
Es muy fácil. Solo debes utilizar el operador & antes del nombre de la variable
y listo.
Observa de nuevo:
1gpio_config(&io_conf);
Y
1esp_err_t gpio_config(const gpio_config_t *pGPIOConfig)
&io_conf devuelve la dirección de io_conf que será almacenada
en la variable pGPIOConfig al llamar a la función gpio_config.
Ejercicio 16: un ejemplo más simple de pointers¶
Considera el siguiente fragmento de código:
1int var = 10;
2int *pvar = &var;
3*pvar = *pvar + 10;
4printf("var: %d\n", var);
var es una variable de tipo int. pvar es una variable
que almacena direcciones de variable de tipo int.
La expresión *pvar permite referirnos a la variable. Por tanto,
*pvar permite leer o escribir directamente la variable.
Nota que *pvar es diferente a int *pvar. En *pvar estás
accediendo a la variable cuya dirección está almacenada en pvar
y en int *pvar estás declarando una variable que almacenará
la dirección de otra variable de tipo int.
El resultado del programa programa anterior es que var será
igual a 20.
Ejercicio 17: reto¶
Crea un programa que le pase a una función la dirección de un int.
Antes de llamar la función inicializa el int en 10,
imprime el valor, en la función cámbialo, a 20 y luego de llamar la función
imprime de nuevo el int. Sería algo así:
1var = 10
2Imprimir var
3Llamar función con la dirección de var
4Imprimir var de nuevo
Ejercicio 18: exploremos un poco más las struct y los pointers¶
En el ejercicio 15 llamamos a la función gpio_config:
1gpio_config(&io_conf);
Recuerda cómo está definida la función:
1esp_err_t gpio_config(const gpio_config_t *pGPIOConfig)
En pGPIOConfig guardamos la dirección de la variable `io_conf.
Para acceder al contenido de la variable de tipo struct cuya dirección está almacenada en el puntero usamos una notación especial. Observa esta parte del código interno de la función```gpio_config``:
1if ((pGPIOConfig->mode) & (GPIO_MODE_DEF_OUTPUT)) {
2 if(GPIO_MASK_CONTAIN_INPUT_GPIO(gpio_pin_mask)) {
3 ESP_LOGE(GPIO_TAG, "GPIO can only be used as input mode");
4 return ESP_ERR_INVALID_ARG;
5 }
6}
Nota esta parte (pGPIOConfig->mode). Para acceder a los miembros de
la estructura a través del puntero usamos el operador ->.
Observa el siguiente ejemplo:
1#include <stdio.h>
2#include "freertos/FreeRTOS.h"
3#include "freertos/task.h"
4#include "driver/gpio.h"
5#include "esp_log.h"
6#include "string.h"
7
8typedef struct{
9 uint8_t a;
10 uint8_t b;
11 uint8_t c;
12} myData_t;
13
14void funcMyDataPrint(myData_t *pmyData);
15
16void app_main(void)
17{
18 myData_t myData = {.a = 1, .b = 2, .c = 3};
19 funcMyDataPrint(&myData);
20}
21
22void funcMyDataPrint(myData_t *pmyData){
23 printf("myData.a = %d, myData.b = %d, myData.c = %d\n",pmyData->a,pmyData->b,pmyData->c);
24}
Aquí de nuevo puedes ver cómo mediante pmyData y el operador -> se accede
a los datos la struct a través del puntero.
Ejercicio 19: reto struct y pointers¶
Considera el siguiente programa:
1#include <stdio.h>
2#include "freertos/FreeRTOS.h"
3#include "freertos/task.h"
4#include "driver/gpio.h"
5#include "esp_log.h"
6#include "string.h"
7
8typedef struct{
9 uint8_t a;
10 uint8_t b;
11 uint8_t c;
12} myData_t;
13
14void funcMyDataPrint(myData_t *pmyData);
15
16void funcMyDataChange(myData_t myData);
17
18void app_main(void)
19{
20 myData_t myData = {.a = 1, .b = 2, .c = 3};
21 funcMyDataPrint(&myData);
22 funcMyDataChange(myData);
23 printf("myData.a = %d, myData.b = %d, myData.c = %d\n",myData.a,myData.b,myData.c);
24}
25
26void funcMyDataPrint(myData_t *pmyData){
27 printf("myData.a = %d, myData.b = %d, myData.c = %d\n",pmyData->a,pmyData->b,pmyData->c);
28}
29
30void funcMyDataChange(myData_t myData){
31 myData.a = 4;
32 myData.b = 5;
33 myData.c = 6;
34 printf("In function funcMyDataChange myData.a = %d\n",myData.a);
35}
Explica por qué luego de llamar la función funcMyDataChange al imprimir de nuevo
myData los valores no han cambiado.
RETO: modifica la función funcMyDataChange para que puedas modificar los
valores de myData.
Ejercicio 20: punteros a funciones¶
En una variable no solo puedes guardar la dirección de otra variable. En C también puedes utilizar un puntero para guardar la dirección de una función.
¿Para qué sirve esto? Recuerda que estamos utilizando un framework, el esp-idf. La idea de un framework es que haga muchas cosas por ti para agilizar el desarrollo de tu aplicación. Tu le pides al framework que realice una operación, por ejemplo, conectarse a una red WiFi. Entonces, cuando la operación se completa el framework necesita avisarte. Aquí es donde utilizamos los punteros a función. Tu tendrás que almacenar en una variable controlada por el framework la dirección de una función que será llamada una vez la operación termine. Genial, ¿No?
Considera el siguiente código tomando del fabricante. La idea de este código es configurar qué función debe llamar un componente de software que permite implementar la funcionalidad de un botón. El componente es muy interesante porque es capaz de filtrar el ruido mecánico que produce el botón cuando se presiona y también es puede detectar si dejas el botón presionado por cierto tiempo. Entonces, aquí lo que hacemos es indicarle al componente, escrito por el fabricante, cuál de nuestras funciones debe llamar al momento de detectar que el botón se presionó.
1...
2static bool g_output_state;
3
4static void push_btn_cb(void *arg)
5{
6 app_driver_set_state(!g_output_state);
7}
8
9bool app_driver_get_state(void)
10{
11 return g_output_state;
12}
13
14static void configure_push_button(int gpio_num, void (*btn_cb)(void *))
15{
16 button_handle_t btn_handle = iot_button_create(JUMPSTART_BOARD_BUTTON_GPIO, JUMPSTART_BOARD_BUTTON_ACTIVE_LEVEL);
17 if (btn_handle) {
18 iot_button_set_evt_cb(btn_handle, BUTTON_CB_RELEASE, btn_cb, "RELEASE");
19 }
20}
21
22...
23
24void app_driver_init()
25{
26 ...
27 configure_push_button(JUMPSTART_BOARD_BUTTON_GPIO, push_btn_cb);
28 ...
29}
Comienza observando la función app_driver_init. Nota que estamos
llamando a la función configure_push_button.
Observa la definición de configure_push_button:
1static void configure_push_button(int gpio_num, void (*btn_cb)(void *))
Mira el segundo argumento:
1void (*btn_cb)(void *)
La variable btn_cb es un puntero a función porque ahí vamos a almacenar la dirección de una función.
Ahora observa cómo se declara esa variable:
btn_cb es una variable que almacenará la dirección ( * ) de una función que
recibe como argumento una dirección a cualquier cosa ( void * ``) y no devolverá
nada ( ``void ).
Observa de nuevo:
1configure_push_button(JUMPSTART_BOARD_BUTTON_GPIO, push_btn_cb);
Estamos almacenando en btn_cb la dirección de push_btn_cb. Mira
como está definida la función push_btn_cb:
1static void push_btn_cb(void *arg)
2{
3 app_driver_set_state(!g_output_state);
4}
¿Lo notaste? La función push_btn_cb recibe una dirección a cualquier cosa ( void *)
y no devuelve nada ( void ). Por tanto, cuando el botón se presione el componente
llamará la función definida por nosotros push_btn_cb.
Ahora veamos un ejemplo más sencillo.
Ejercicio 21: punteros a funciones¶
Analiza el siguiente ejemplo:
1#include <stdio.h>
2#include "freertos/FreeRTOS.h"
3#include "freertos/task.h"
4#include "driver/gpio.h"
5#include "esp_log.h"
6#include "string.h"
7
8int suma(int a, int b);
9int resta(int a, int b);
10
11void app_main(void)
12{
13 int (*pfunction) (int,int);
14
15 pfunction = suma;
16 printf("3+2: %d\n", pfunction(3,2));
17
18 pfunction = resta;
19 printf("10-2: %d\n", pfunction(10,2));
20}
21
22int suma(int a, int b)
23{
24 return a + b;
25}
26
27int resta(int a, int b)
28{
29 return a - b;
30}
Ejercicio 22: PRE - reto¶
¿Recuerdas el ejercicio 4? Vas a ejecutarlo de nuevo
pero esta vez vas a eliminar este línea vTaskDelay(100/portTICK_PERIOD_MS);
que está al final y esperarás varios segundos antes de ingresar un mensaje.
Mira de nuevo el código sin la línea:
1#include <stdio.h>
2#include "freertos/FreeRTOS.h"
3#include "freertos/task.h"
4#include "driver/gpio.h"
5#include "esp_log.h"
6#include "string.h"
7
8void app_main(void)
9{
10 int c;
11 char message[16];
12 uint8_t counter = 0;
13
14 while(1)
15 {
16 c = getchar();
17
18 if(c != EOF){
19
20 if( '\n' == c)
21 {
22 // Termina el mensaje con 0
23 message[counter] = 0;
24 // Verifica si el mensaje es hola
25 if ( strncmp (message, "hola", strlen("hola") ) == 0)
26 {
27 printf(" mundo\n");
28 }
29 else
30 {
31 printf("no entiendo\n");
32 }
33 counter = 0;
34 }
35 else
36 {
37 // Solo guarda un carácter si hay espacio
38 if( counter < ( sizeof(message) - 1 ) )
39 {
40 message[counter] = c;
41 counter++;
42 }
43 }
44 }
45 }
46}
¿Qué ocurre?
Notarás que el programa se reinicia. Ahora busca algunos mensajes de error. Verás algo similar a esto:
E (95306) task_wdt: Task watchdog got triggered. The following tasks did not reset the watchdog in time:
E (95306) task_wdt: - IDLE0 (CPU 0)
E (95306) task_wdt: Tasks currently running:
E (95306) task_wdt: CPU 0: main
E (95306) task_wdt: CPU 1: IDLE1
E (95306) task_wdt: Print CPU 0 (current core) backtrace
¿Qué está pasando?
Resulta que el sistema operativo tiene un mecanismo para evitar que una tarea monopolice
por completo algún recurso de procesamiento, es decir, algún core. Cualquier tarea
debe tratar de usar los recursos y devolverlos al sistema operativo. En este caso
la línea vTaskDelay(100/portTICK_PERIOD_MS); hace precisamente eso, es decir,
hace un llamado al sistema operativo diciéndole que por favor la ponga a dormir
100 ms y luego la active de nuevo, devolviendo efectivamente el core en el cual
se ejecuta. Cada que esto ocurre el sistema operativo reiniciará un contador conocido
como Watchdog timer; sin embargo, si este contador no es reiniciado disparará un
reset.
Ejercicio 23: reto¶
No olvides lo que aprendiste en el ejercicio anterior. Este reto consiste entonces en hacer una aplicación que reciba por el puerto serial:
Dos operandos
Una operación: suma, resta, multiplicación.
Luego, debe calcular la operación e imprimirla por el puerto serial así:
1printf("%d %s %d = %d\n", ?,?,?,pfunction(op1,op2) );
Nota que previamente tendrás que configurar el puntero pfunction con
la operación correcta.
Sesión 2¶
En esta sesión vamos a resolver dudas sobre los ejercicios y escuchar aportes, comentarios y/o experiencias de todos.