En el anterior artículo explicaba cómo generar el toolchain para poder compilar programas y subirlos a nuestro módulo ESP8266. En este artículo explicaré cómo hacer un programa en C. Para ello me voy a poner un reto: un firmware que cuando pulse un botón o la tecla p, haga una llamada a una página web que muestre la acción realizada. Con este ejemplo vamos a trabajar con el gpio, puerto serie, wifi, temporizadores y eventos. No hay mucha documentación sobre cómo programar el chip esp8266 más allá de los ejemplos de código, foros y páginas web con artículos dedicados al chip y de la SDK de Espressif que en el momento de escribir este artículo es la 1.0.1.
Empezaré por mostrar un vídeo de lo que he hecho para tener claro desde el principio lo que explicaré aquí.
Lo que veis en el vídeo es que primero programo el esp8266 con el firmware de este artículo. Luego abro una terminal al puerto serie del ESP8266. Pulso 3 veces el botón físico y otras 3 veces la tecla p en la terminal para que se envíe por el puerto serie. Estas acciones provocan en el esp8266 la llamada a una página web que lo registra, mientras en otra página web se visualiza casi en tiempo real lo que he hecho. Cuando compiléis vosotros el ejemplo podéis usar la misma página para hacer vuestras pruebas.
Este es el esquema del circuito para el botón (he obviado las conexiones al puerto serie para simplificarlo):
Vamos a ver cómo lo hecho. Lo primero es crear una carpeta llamada proyectos-esp8266 en nuestra carpeta personal, entra dentro de esta y crear otra llamada reto. Dentro de esta copiaremos el fichero Makefile que modificamos en el anterior artículo. Después crearemos una carpeta llamada user y dentro de esta compiaremos los ficheros uart.h y uart_register.h del ejemplo de IoT que trae la SDK:
1 2 3 4 5 6 |
mkdir -p ~/proyectos-esp8266/reto cd ~/proyectos-esp8266/reto cp ~/esp-open-sdk/source-code-examples/example.Makefile ./Makefile mkdir user cd user cp ~/esp-open-sdk/esp_iot_sdk_v1.0.1/examples/IoT_Demo/include/driver/uart*.h . |
Dentro de la carpeta user crearemos dos ficheros.
user_config.h
1 2 3 4 5 |
#define user_procTaskQueueLen 1 #define SSID "MI_WIFI" #define PASSWORD "MI_CLAVE" #define NOMBRE "SISTEMASORP" #define SERVIDOR "www.sistemasorp.es" |
user_main.c
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 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 |
#include <user_interface.h> #include <os_type.h> #include <ets_sys.h> #include <mem.h> #include <osapi.h> #include <gpio.h> #include <espconn.h> #include "uart.h" #include "user_config.h" extern UartDevice UartDev; os_event_t user_procTaskQueue[user_procTaskQueueLen]; ETSTimer temporizador_boton; struct espconn *conexion; ip_addr_t ip; volatile uint8 accion; volatile bool boton; volatile bool libre; void ICACHE_FLASH_ATTR caracter_recibido(void *parametro) { RcvMsgBuff *pRxBuff = (RcvMsgBuff *)parametro; uint8 caracter; if (UART_RXFIFO_FULL_INT_ST != (READ_PERI_REG(UART_INT_ST(UART0)) & UART_RXFIFO_FULL_INT_ST)) return; WRITE_PERI_REG(UART_INT_CLR(UART0), UART_RXFIFO_FULL_INT_CLR); while (READ_PERI_REG(UART_STATUS(UART0)) & (UART_RXFIFO_CNT << UART_RXFIFO_CNT_S)) { caracter = READ_PERI_REG(UART_FIFO(UART0)) & 0xFF; if(caracter == 'p' || caracter == 'P') { os_printf("Pulsada tecla\n"); if(libre) { system_os_post(USER_TASK_PRIO_0, 1, 0 ); } } } } void ICACHE_FLASH_ATTR boton_pulsado() { boton = FALSE; } void ICACHE_FLASH_ATTR interrupcion_gpio() { uint32 gpio_status = GPIO_REG_READ(GPIO_STATUS_ADDRESS); ETS_GPIO_INTR_DISABLE(); if(boton == FALSE) { os_printf("Pulsado boton\n"); boton = TRUE; os_timer_disarm(&temporizador_boton); os_timer_setfn(&temporizador_boton, (ETSTimerFunc *)boton_pulsado, NULL); os_timer_arm(&temporizador_boton, 250, 0); if(libre) { system_os_post(USER_TASK_PRIO_0, 0, 0 ); } } GPIO_REG_WRITE(GPIO_STATUS_W1TC_ADDRESS, gpio_status & BIT(2)); ETS_GPIO_INTR_ENABLE(); } void ICACHE_FLASH_ATTR interrupcion_wifi(System_Event_t *evt) { os_printf("evento %x\n", evt->event); switch (evt->event) { case EVENT_STAMODE_CONNECTED: os_printf("connectado al ssid %s, canal %d\n", evt->event_info.connected.ssid, evt->event_info.connected.channel); break; case EVENT_STAMODE_DISCONNECTED: os_printf("desconectado del ssid %s, razon %d\n", evt->event_info.disconnected.ssid, evt->event_info.disconnected.reason); break; case EVENT_STAMODE_AUTHMODE_CHANGE: os_printf("modo: %d -> %d\n", evt->event_info.auth_change.old_mode, evt->event_info.auth_change.new_mode); break; case EVENT_STAMODE_GOT_IP: os_printf("ip:" IPSTR ",mascara:" IPSTR ",gateway:" IPSTR, IP2STR(&evt->event_info.got_ip.ip), IP2STR(&evt->event_info.got_ip.mask), IP2STR(&evt->event_info.got_ip.gw)); os_printf("\n"); libre = TRUE; break; case EVENT_SOFTAPMODE_STACONNECTED: os_printf("estacion: " MACSTR "unida, AID = %d\n", MAC2STR(evt->event_info.sta_connected.mac), evt->event_info.sta_connected.aid); break; case EVENT_SOFTAPMODE_STADISCONNECTED: os_printf("estacion: " MACSTR "abandona, AID = %d\n", MAC2STR(evt->event_info.sta_disconnected.mac), evt->event_info.sta_disconnected.aid); break; default: break; } } void ICACHE_FLASH_ATTR inicializa_uart() { ETS_UART_INTR_ATTACH(caracter_recibido, &(UartDev.rcv_buff)); PIN_PULLUP_DIS(PERIPHS_IO_MUX_U0TXD_U); PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0TXD_U, FUNC_U0TXD); uart_div_modify(0, UART_CLK_FREQ / BIT_RATE_9600); WRITE_PERI_REG(UART_CONF0(0), (STICK_PARITY_DIS)|(ONE_STOP_BIT << UART_STOP_BIT_NUM_S)| (EIGHT_BITS << UART_BIT_NUM_S)); SET_PERI_REG_MASK(UART_CONF0(0), UART_RXFIFO_RST|UART_TXFIFO_RST); CLEAR_PERI_REG_MASK(UART_CONF0(0), UART_RXFIFO_RST|UART_TXFIFO_RST); WRITE_PERI_REG(UART_CONF1(0), (UartDev.rcv_buff.TrigLvl & UART_RXFIFO_FULL_THRHD) << UART_RXFIFO_FULL_THRHD_S); WRITE_PERI_REG(UART_INT_CLR(0), 0xffff); SET_PERI_REG_MASK(UART_INT_ENA(0), UART_RXFIFO_FULL_INT_ENA); ETS_UART_INTR_ENABLE(); } void ICACHE_FLASH_ATTR inicializa_gpio() { ETS_GPIO_INTR_DISABLE(); ETS_GPIO_INTR_ATTACH(interrupcion_gpio, 2); PIN_FUNC_SELECT(PERIPHS_IO_MUX_GPIO2_U, FUNC_GPIO2); PIN_PULLUP_EN(PERIPHS_IO_MUX_GPIO2_U); gpio_output_set(0, 0, 0, GPIO_ID_PIN(2)); GPIO_REG_WRITE(GPIO_STATUS_W1TC_ADDRESS, BIT(2)); gpio_pin_intr_state_set(GPIO_ID_PIN(2), GPIO_PIN_INTR_NEGEDGE); ETS_GPIO_INTR_ENABLE(); } void ICACHE_FLASH_ATTR inicializa_wifi() { char ssid[32] = SSID; char password[64] = PASSWORD; struct station_config configuracion; wifi_set_opmode(STATION_MODE); wifi_station_set_auto_connect(TRUE); wifi_station_set_reconnect_policy(TRUE); os_memcpy(&configuracion.ssid, ssid, 32); os_memcpy(&configuracion.password, password, 64); configuracion.bssid_set = 0; wifi_station_set_config(&configuracion); wifi_set_event_handler_cb(interrupcion_wifi); } void ICACHE_FLASH_ATTR conectado(void *argumento) { char cadena_http[100]; struct espconn *socket = (struct espconn *)argumento; os_sprintf(cadena_http, "GET /esp8266/actualiza.php?nombre=%s&accion=%d HTTP/1.0\r\nHost: %s\r\n\r\n", NOMBRE, accion, SERVIDOR); espconn_sent(socket, cadena_http, os_strlen(cadena_http)); } void ICACHE_FLASH_ATTR desconectado(void *argumento) { struct espconn *socket = (struct espconn *)argumento; os_free(socket->proto.tcp); os_free(socket); libre = TRUE; } void ICACHE_FLASH_ATTR recibido(void *argumento, char *datos, unsigned short longitud) { char *buffer = (char *)os_zalloc(longitud + 1); os_memcpy(buffer, datos, longitud); buffer[longitud] = '\0'; os_printf(buffer); os_free(buffer); } void ICACHE_FLASH_ATTR encontrado(const char *nombre, ip_addr_t *ipaddr, void *argumento) { struct espconn *socket = (struct espconn *)argumento; socket->type = ESPCONN_TCP; socket->state = ESPCONN_NONE; socket->proto.tcp = (esp_tcp *)os_zalloc(sizeof(esp_tcp)); socket->proto.tcp->local_port = espconn_port(); socket->proto.tcp->remote_port = 80; os_memcpy(socket->proto.tcp->remote_ip, &ipaddr->addr, 4); espconn_regist_connectcb(socket, conectado); espconn_regist_disconcb(socket, desconectado); espconn_regist_recvcb(socket, recibido); espconn_connect(conexion); } void ICACHE_FLASH_ATTR conecta(os_event_t *eventos) { libre = FALSE; accion = eventos->sig; conexion = (struct espconn *)os_zalloc(sizeof(struct espconn)); espconn_gethostbyname(conexion, SERVIDOR, &ip, encontrado); } void ICACHE_FLASH_ATTR inicializado() { os_printf("Sistema inicializado\n"); } void ICACHE_FLASH_ATTR user_init() { system_init_done_cb(inicializado); inicializa_uart(); inicializa_gpio(); inicializa_wifi(); boton = FALSE; libre = FALSE; system_os_task(conecta, USER_TASK_PRIO_0, user_procTaskQueue, user_procTaskQueueLen); } |
Lo primero que salta a la vista es que no existe una función main eso es porque este chip se programa con una función de inicialización y el resto con eventos/interrupciones.
La función de inicialización es user_init (al final del código), como decía esta función es llamada cuando el módulo se enciende y se encarga de inicializar todo en el esp8266. El modificador de función ICACHE_FLASH_ATTR significa que la función se guarde en la flash en vez de en la RAM y que cuando se vaya a usar sea cacheada en esta última, ahorrando así espacio para poder tener más datos en la memoria. Dentro de esta función hay que destacar:
- system_init_done_cb: Esta función tiene un parámetro que es la función callback que será llamada cuando termine la inicialización.
- system_os_task: Esta función define la ejecución, como tarea, de la función callback pasada como primer parámetro. Podemos tener hasta 4 tareas (cada una con una prioridad distinta). Más adelante hablaré de ello.
La función inicializado es llamada cuando ha terminado la inicialización como comentába antes. Dentro de esta función hay que destacar:
- os_printf: Es como el printf de toda la vida, pero su salida es enviada al puerto serie 0 (el esp8266 tiene un puerto serie con tx y rx y otro con tx sólo para hacer debug).
La función inicializa_uart configura el puerto serie. Dentro de esta función hay que destacar:
- ETS_UART_INTR_ATTACH: Esta función configura a qué función callback se llamará cuando se reciba un caracter por el puerto serie. El segundo parámetro es una estructura que existe por defecto con información del puerto serie y el buffer de recepción, pero yo no lo usaré.
- PIN_PULLUP_DIS: Desactiva la configuración de pull-up del pin indicado (La lista de pines está en el fichero eagle_soc.h de la sdk), en este caso del pin TX.
- PIN_FUNC_SELECT: Configura el pin indicado para comportarse de una manera determinada, en este caso para funcionar como TX del puerto serie.
- uart_div_modify: Configura la velocidad del puerto serie
- WRITE_PERI_REG(UART_CONF0(0), (STICK_PARITY_DIS)|(ONE_STOP_BIT << UART_STOP_BIT_NUM_S)| (EIGHT_BITS << UART_BIT_NUM_S)): Configura la comunicación como 8 bits, sin paridad y un bit de stop.
- SET_PERI_REG_MASK(UART_CONF0(0), UART_RXFIFO_RST|UART_TXFIFO_RST) y CLEAR_PERI_REG_MASK(UART_CONF0(0), UART_RXFIFO_RST|UART_TXFIFO_RST): Limpia la FIFO de TX y RX.
- WRITE_PERI_REG(UART_CONF1(0), (UartDev.rcv_buff.TrigLvl & UART_RXFIFO_FULL_THRHD) << UART_RXFIFO_FULL_THRHD_S): Configura el disparador FIFO de RX.
- WRITE_PERI_REG(UART_INT_CLR(0), 0xffff): Borrar el flag de todas las interrupciones del puerto serie.
- SET_PERI_REG_MASK(UART_INT_ENA(0), UART_RXFIFO_FULL_INT_ENA): Configura la interrupción de recepción del pin RX.
- ETS_UART_INTR_ENABLE: Activa las interrupciones del puerto serie.
La función inicializa_gpio configura el pin GPIO 2 (el único disponible en el ESP-01, pero en otros módulos hay más pines gpio usables). Dentro de esta función hay que destacar:
- ETS_GPIO_INTR_DISABLE: Desactiva la interrupción de los pines GPIO.
- ETS_GPIO_INTR_ATTACH: Esta función configura a que función callback se llamará cuando haya una interrpción en el pin gpio indicado.
- PIN_PULLUP_EN: Activa la configuración de pull-up del pin indicado.
- gpio_output_set: Configura el pin gpio 2 como entrada.
- GPIO_REG_WRITE(GPIO_STATUS_W1TC_ADDRESS, BIT(2)): Borrar el flag de la interrupcion del pin gpio 2.
- gpio_pin_intr_state_set: Indica que en el pin gpio 2 sólo debe activarse la interrupción cuando detecta un flanco de bajada (conecta con masa/GND).
- ETS_GPIO_INTR_ENABLE: Activa las interrupciones gpio.
La función inicializa_wifi configura la conexión a la wifi. Dentro de esta función hay que destacar:
- wifi_set_opmode: Configura el módulo como estación cliente.
- wifi_station_set_auto_connect: Indica que cuando se encienda el módulo se conecte automáticamente a la wifi establecida.
- wifi_station_set_reconnect_policy: Indica que si se pierde la conexión con el punto de acceso, la vuelva a restablecer.
- os_memcpy(&configuracion.ssid, ssid, 32) y os_memcpy(&configuracion.password, password, 64): Copia el nombre de la red wifi y la contraseña a la estructura del tipo station_config. Sirve para copiar datos de una posición de memoria a otra, sería equiparable al memcpy del C.
- configuracion.bssid_set = 0: Para que conecte a cualquier wifi que tenga el nombre proporcionado anteriormente. Si hay varias wifis con el mismo nombre (SSID) puedes indicar a qué punto de acceso quieres acceder poniendo aquí su BSSID (su dirección MAC).
- wifi_station_set_config: Inicializa la wifi con los valores proporcionados en la estructura del tipo station_config.
- wifi_set_event_handler_cb: Esta función configura a que función callback se llamará cuando haya cambios en la wifi (conexión, desconexión, ip recibida…).
La función callback interrupcion_wifi es llamada por el chip cuando un evento en la wifi es generado. Quizá el más importante es cuando recibimos una IP de nuestro router. En el ejemplo pongo una variable a TRUE para indicar que tiene libertad para procesar las pulsaciones del teclado o del botón.
La función callback interrupcion_gpio es llamada por el chip cuando se ha detectado un evento en los pines GPIO. Dentro de esta función hay que destacar:
- GPIO_REG_READ(GPIO_STATUS_ADDRESS): Devuelve el estado de los pones gpio y cual ha sido el que ha causado el evento.
- os_timer_disarm: desactiva un temporizador si este estuviese activo.
- os_timer_setfn: configura un temporizador indicando a qué función callback llamará cuando expire. Se le puede pasar un parámetro (en este caso NULL).
- os_timer_arm: Activa el temporizador indicando cuanto tiempo dura en milisegundos y si debe o no repetirse. La idea de usarlo es para evitar los rebotes físicos cuando el botón es pulsado.
- system_os_post: Indica al chip que, cuando pueda, ejecute la tarea de prioridad 0 que predefinimos en la función user_init con los parámetros 0,0.
La función callback boton_pulsado establece una variable a FALSE para indicar que el tiempo dado para mitigar los rebotes físicos del botón ya ha pasado y de nuevo permite reconocer la pulsación de éste.
La función callback caracter_recibido es llamada por el chip cuando se ha detectado un evento en el puerto serie. Dentro de esta función hay que destacar:
- READ_PERI_REG(UART_INT_ST(UART0): Lee cual ha sido el motivo de la interrupción.
- WRITE_PERI_REG(UART_INT_CLR(UART0): UART_RXFIFO_FULL_INT_CLR): Borra el bit de la interrupción correspondiente a carácter recibido.
- READ_PERI_REG(UART_STATUS(UART0)): Lee el estado del puerto serie.
- READ_PERI_REG(UART_FIFO(UART0)): Lee el caracter propiamente dicho. El chip ya tiene un buffer donde se pueden almacenar los carácteres recibidos, yo no lo he usado pero puede ser interesante.
La función callback conecta es llamada por el chip como una tarea. Inicia el proceso de envío de datos. Dentro de esta función hay que destacar:
- os_zalloc: Sirve para reservar memoria, sería equiparable al malloc del C.
- espconn_gethostbyname: Esta función hace una consulta dns para obtener la ip de un host dado y llamará a una función callback. Usa una estructura del tipo espconn donde se rellenarán todos los datos referentes a la conexión TCP
La función callback encontrado es llamada cuando se ha resuelto un nombre de host. Dentro de esta función hay que destacar:
- espconn_port: Crea un puerto local para poder enganchar el socket con el puerto remoto 80 (http).
- espconn_regist_connectcb: Esta función indica al chip que cuando se establece una conexión TCP, debe llamar a la función callback definida.
- espconn_regist_disconcb: Esta función indica al chip que cuando hay una desconexión, debe llamar a la función callback definida.
- espconn_regist_recvcb: Esta función indica al chip que cuando se reciben datos desde una conexión TCP, debe llamar a la función callback definida.
La función callback conectado es llamada por el chip cuando se establece una conexión TCP. Dentro de esta función hay que destacar:
- os_sprintf: Sirve para rellenar un buffer con datos formateados, sería equiparable al sprintf del C.
- espconn_sent: Envía los datos indicados a través del socket TCP.
- os_strlen: Devuelve la longitud de una cadena, sería equiparable al strlen del C.
La función callback recibido es llamada por el chip cuando se reciben datos de una conexión TCP. En el ejemplo sólo los muestro directamente. Dentro de esta función hay que destacar:
- os_free: Sirve para liberar memoria, sería equiparable al free del C.
La función callback desconectado es llamada por el chip cuando se desconecta la conexión TCP. En el ejemplo libero los buffers de memoria reservados previamente.
Quiero comentar es que no soy un experto en el esp8266, por lo que si que me preguntáis algo puede que no lo sepa responder, por ello os recomiendo ir a la página www.esp8266.com donde hay mucha gente que trabaja con el esp8266 y os podrán resolver vuestras dudas mejor que yo. Todo lo que he juntado aquí ha sido fruto de búsquedas por internet, mil y una pruebas con código fuente, etc. Sin embargo lo que aquí he explicado, aún siendo de las partes más importantes de la programación de un ESP8266, no cubre todo lo que se puede hacer con este chip, por lo que una vez te encuentres cómodo con lo aquí expuesto, te animo a que busques por tu cuenta más funcionalidades.