Objetivos del proyecto
De alguna manera resultó que construí mi casa, un esqueleto. En mi aul de lujo no hay gas y no se espera en un futuro cercano, por eso elegí un esqueleto; todo lo demás, para mí, sería muy costoso calentar con electricidad. Bueno, también porque es una de las tecnologías más baratas.
Ok, tiré tuberías alrededor de la casa, colgué baterías, una caldera, parecía caliente, pero algo estaba mal.
Después de escucharme, me di cuenta de que este es un sapo que no me gusta, que mientras no estoy en casa (12-16 horas al día), la calefacción funciona. Y puede que no funcione, enciéndalo solo antes de llegar, ya que el esqueleto tiene una ligera inercia y le permite elevar rápidamente la temperatura. La misma situación cuando en algún lugar durante mucho tiempo para salir de casa. Bueno, en general, correr, girar la manija de la caldera con cambios de temperatura en la calle de alguna manera no es kosher.
Quedó claro que sin automatización, en ninguna parte, la caldera es la más simple, pero tiene contactos para conectar un relé de control externo. Por supuesto, podría comprar inmediatamente una caldera con todas las funciones necesarias, pero para mí, esas calderas son de alguna manera inhumanas. Además, quería hacer sentadillas con cerebro, orinar algo para el alma, aprender un poco de C, aunque en la versión arduino.
En realidad sobre los requisitos:
- control de temperatura de consigna
- control de temperatura del refrigerante dependiendo de la temperatura exterior o manual
- zonas horarias con diferentes configuraciones, más frío durante el día, más caliente durante la noche
- modo automático, con transición automática día-noche
- modo manual, sin transiciones automáticas, para el fin de semana
- modo no automático, donde puede configurar manualmente cualquier temperatura del refrigerante y encender / apagar la caldera
- control de calefacción local, desde botones y pantalla y a través del sitio web / aplicación móvil
Fue al principio, y luego sufrí y agregué:
- control de farola (foco LED)
- sistema de alarma basado en sensor de movimiento, sirena y farola
- medir la energía consumida por la caldera por día / mes / año + para cada mes del año
- modo de alarma solo por parpadeo lento de una lámpara
- modo de señalización mediante un parpadeo rápido de una lámpara y pitidos cortos de una sirena
- modo de señalización mediante el parpadeo rápido de una lámpara y el aullido constante de una sirena
El propósito de este artículo es compartir experiencias, describir algo en ruso que no pude encontrar en Internet. Creo que este artículo será útil para principiantes Arduino hágalo usted mismo que ya están un poco familiarizados con la programación, como cosas absolutamente básicas que no describí. Traté de escribir el código lo más claro posible, espero haber tenido éxito.
Lo que estaba al principio
Inicialmente, el proyecto se implementó en un grupo salvaje de Arduino Nano + ESP8266, pero ESP no es como un escudo, sino como un dispositivo separado. Por qué Sí, porque ya tenía todo esto, pero no había dinero de la palabra en absoluto, por lo que, en principio, no quería comprar hierro nuevo. ¿Por qué ESP no es como un escudo? Ahora ni lo recuerdo.
Arduino manejó todos los procesos porque tenía la cantidad requerida de GPIO, y ESP envió todos los datos al servidor Blynk, porque conocía Internet y no tenía suficiente GPIO. Se conectaron a través de UART y enviaron JSON con datos entre sí. El esquema es inusual, pero funcionó durante un año casi sin quejas. Cualquier persona interesada puede ver el códec .
Haré una reserva de inmediato, no sabía mucho cómo programar (e incluso ahora me gustaría más), así que es mejor que las mujeres embarazadas y los niños no lo vean. Además, todo fue escrito en el IDE de Arduino, no será recordado por la noche, lo que es muy limitado en términos de refactorización, todo es muy primitivo allí.
Hierro
Entonces, ha pasado un año, las finanzas permitidas para comprar ESP32 devkit v1, que tiene suficiente GPIO, pueden acceder a Internet y, en general, a un súper controlador. Además de los chistes, me gustó mucho al final del trabajo.
Lista de hierro:
- ESP32 devkit v1 noname China
- 3 sensores de temperatura DS18B20, temperatura dentro de la casa, exterior y temperatura del refrigerante en las tuberías
- bloque de 4 relés
- sensor pir HC-SR501
No dibujaré un esquema, creo que todo quedará claro a partir de las macros con los nombres de los pines.
Por qué FreeRTOS y Arduino Core
Hay un montón de bibliotecas escritas en Arduino, en particular el mismo Blynk, por lo que no se alejará de Arduino Core.
FreeRTOS porque le permite organizar el trabajo de una pequeña pieza de hierro similar al trabajo de un controlador industrial completo. Cada tarea puede moverse a su propia tarea, detenerse, iniciarse, crearse cuando sea necesario, eliminarse; todo esto es mucho más flexible que escribir una larga maraña de código Arduino, cuando al final todo se hace a su vez en la función de bucle.
Al usar FreeRTOS, cada tarea se ejecutará en un momento estrictamente especificado, si solo la potencia del procesador es suficiente. Por el contrario, en Arduino todo el código se ejecuta en una función, en un hilo, si algo se ralentiza, el resto de las tareas se retrasarán. Esto es especialmente notable cuando se gestionan procesos rápidos, en este proyecto este parpadeo de una linterna y un pitido de sirena se discutirán a continuación.
Acerca de la lógica
Acerca de las tareas de FreeRTOS
→ Enlace a todo el códec del proyecto
Entonces, cuando se usa FreeRTOS, la función de configuración desempeña el papel de la función principal, el punto de entrada a la aplicación, las tareas de FreeRTOS (en adelante tareas) se crean en ella, la función de bucle no se puede usar en absoluto.
Considere una pequeña tarea para calcular la temperatura del refrigerante:
void calculate_water_temp(void *pvParameters) { while (true) { if (heating_mode == 3) {} else { if (temp_outside > -20) max_water_temp = 60; if (temp_outside <= -20 && temp_outside > -25) max_water_temp = 65; if (temp_outside <= -25 && temp_outside > -30) max_water_temp = 70; if (temp_outside <= -30) max_water_temp = 85; } vTaskDelay(1000 / portTICK_RATE_MS); } }
Se declara como una función que debe tomar _void pvParameters
, se organiza un bucle sin fin dentro de la función, utilicé while (true)
.
Se realiza un cálculo de temperatura simple (si el modo operativo lo permite) y luego vTaskDelay(1000 / portTICK_RATE_MS)
durante 1 segundo. En este modo, no consume tiempo del procesador, las variables con las que trabajó la tarea, en otras palabras, el contexto, se guardan en la pila para sacarlas de allí cuando llegue el momento.
La siguiente tarea debe crearse en la configuración. Esto se hace llamando al método xTaskCreate
:
xTaskCreate(calculate_water_temp, "calculate_water_temp", 2048, NULL, 1, NULL);
Hay muchos argumentos, pero para nosotros, Calculate_water_temp es significativo: el nombre de la función que contiene el código de la tarea y 2048 es el tamaño de la pila en bytes.
El tamaño de la pila inicialmente configuró a todos en 1024 bytes, luego calculé el método deseado escribiendo, si el controlador comenzó a caer con un desbordamiento de la pila (como se puede ver en la salida en uart), solo aumenté el tamaño de la pila 2 veces, si no ayudaba, 2 veces, etc. hasta que funcione Por supuesto, esto no ahorra demasiado memoria, pero ESP32 tiene suficiente, en mi caso no podría molestarse con esto.
También puede especificar un identificador para la tarea, un identificador con el que puede controlar la tarea después de la creación, por ejemplo, eliminar. Este es el último NULL en el ejemplo. Un identificador se crea así:
TaskHandle_t slow_blink_handle;
A continuación, al crear una tarea, xTaskCreate
pasa un puntero al xTaskCreate
al parámetro xTaskCreate:
xTaskCreate(outside_lamp_blinks, "outside_lamp_blynk", 10000, (void *)1000, 1, &slow_blink_handle);
Y si queremos eliminar la tarea, hacemos esto:
vTaskDelete(slow_blink_handle);
Se puede ver cómo se usa esto en el código de panic_control
panic_control.
FreeRTOS Mutex Pros
Mutex se utiliza para eliminar conflictos entre tareas al acceder a recursos como uart, wifi, etc. En mi caso, necesitaba mutexes para wifi y acceso a memoria flash.
Cree un enlace al mutex:
SemaphoreHandle_t wifi_mutex;
En la setup
cree un mutex:
wifi_mutex = xSemaphoreCreateMutex();
Además, cuando necesitamos acceso al recurso de la tarea, se necesita el mutex, lo que permite que el resto de las tareas sepan que el recurso está ocupado y no debemos intentar trabajar con él:
xSemaphoreTake(wifi_mutex, portMAX_DELAY);
portMAX_DELAY
: espere indefinidamente hasta que el recurso y el mutex sean liberados por otras tareas, todo este tiempo la tarea se suspenderá.
Después de trabajar con el recurso, le damos el mutex para que otros puedan usarlo:
xSemaphoreGive(wifi_mutex);
Puede ver el código con más send_data_to_blynk
en la send_data_to_blynk
send_data_to_blynk.
En la práctica, no se usaban mutexes cuando el controlador funcionaba, pero durante la depuración JTAG, los errores desaparecían constantemente y desaparecían después de usar mutexes.
Breve descripción de tasok
get_temps
: recibe la temperatura de los sensores, cada 30 segundos, más a menudo no es necesario.
get_time_task
: obtiene el tiempo de los servidores NTP. Anteriormente, llegó el momento del módulo RTC DS3231, pero comenzó a fallar después de un año de trabajo, así que decidí deshacerme de él. Decidí que para mí esto no tiene ninguna consecuencia especial, principalmente el tiempo afecta el cambio de la zona horaria de calefacción, de día o de noche. Si Internet desaparece durante el funcionamiento del controlador, la hora simplemente se congelará, la zona horaria simplemente permanecerá igual. Si el controlador se apaga y después de encenderlo no hay Internet, entonces la hora siempre será 0:00:00 - modo de calefacción por la noche.
calculate_water_temp
: considerado anteriormente.
detect_pir_move
: recibe una señal de movimiento del sensor HC-SR501. El sensor forma una unidad lógica + 3.3V cuando se detecta movimiento, que se detecta usando digitalRead
, por cierto, el pin para la detección de este sensor debe estar levantado a GND - pinMode(pir_pin, INPUT_PULLDOWN);
heating_control
- cambio de modos de calentamiento.
out_lamp_control
- control de una farola.
panic_control
: controla la sirena y el foco cuando se detecta movimiento. Para crear el efecto de sirenas y luces intermitentes, se utilizan tareas separadas, outside_lamp_blinks
y siren_beeps
. Cuando se usa FreeRTOS, el parpadeo y los pitidos funcionan perfectamente, exactamente en los intervalos establecidos, otras tareas no afectan su trabajo, porque ellos viven en corrientes separadas. FreeRTOS garantiza que el código en la tarea se ejecutará a la hora especificada. Al implementar estas funciones en el loop
todo funcionó no tan bien, porque influenciado por la ejecución de otro código.
guard_control
: control de los modos de guardia.
send_data_to_blynk
: envía datos a la aplicación Blynk.
run_blynk
: tarea para iniciar Blynk.run()
como lo requiere el manual para usar Blynk. Según tengo entendido, esto es necesario para obtener datos de la aplicación al controlador. En general, Blynk.run()
debería estar en un loop
, pero básicamente no quería poner nada allí y lo convertí en una tarea separada.
write_setting_to_pref
: graba la configuración y los modos de funcionamiento para capturarlos después de un reinicio. Sobre pref se describirá a continuación.
count_heated_hours
- contando el tiempo de operación de la caldera. Lo hice simplemente, si la caldera se enciende en el momento del inicio de la tarea (una vez cada 30 segundos), en la memoria flash, el valor de la clave deseada se incrementa en uno.
send_heated_hours_to_app
: en esta tarea, los valores se extraen y multiplican por 0.00833 (1/120 horas), las horas de funcionamiento recibidas de la caldera se envían a la aplicación Blynk.
feed_watchdog
- alimenta a Watchdog. Tuve que escribir watchdog, porque una vez cada pocos días, el controlador podría congelarse. Lo que está conectado no está claro, puede haber algún tipo de interferencia con la fuente de alimentación, pero el uso de watchdog resuelve este problema. Watchdog timer 10 segundos, está bien si el controlador no está disponible durante 10 segundos.
heart_beat
: tarea con pulso. Cuando paso el controlador, quiero saber que funciona bien. Porque en mi placa no hay LED incorporado, tuve que usar el LED UART: instalar Serial.begin(9600);
y escribe una cadena larga en UART. Funciona bastante bien
Nivelación de desgaste ESP32 NVS
Las siguientes descripciones son bastante crudas, literalmente en los dedos, solo para transmitir la esencia del problema. Más detalles
Arduino usa memoria EEPROM para almacenar datos en memoria no volátil. Esta es una pequeña memoria en la que cada byte se puede escribir y borrar por separado, mientras que la memoria flash se borra solo por sectores.
No hay EEPROM en ESP32, pero generalmente hay una memoria flash de 4 Mb en la que puede crear particiones para el firmware del controlador o para almacenar datos del usuario. Las secciones para los datos del usuario son de varios tipos: NVS, FATFS, SPIFFS. Debe seleccionarse en función del tipo de datos destinados a la grabación.
Porque Todos los datos que se escriben en este proyecto son del tipo Int. Elegí NVS - Almacenamiento no volátil. Este tipo de partición es muy adecuada para almacenar datos pequeños, a menudo sobrescribibles. Para entender por qué, debe profundizar un poco más en la organización de NVS.
Al igual que EEPROM y FLASH, existen restricciones para sobrescribir datos, los bytes en EEPROM se pueden sobrescribir de 100,000 a 1,000,000 de veces, y el sector FLASH es el mismo. Si escribimos datos una vez por segundo, obtenemos 60 segundos x 60 minutos x 24 horas = 86,400 veces / día. Es decir, en este modo, el byte durará 11 días, lo cual es un poco. Después de lo cual el byte no estará disponible para escribir y leer.
Para solucionar este problema, las funciones update()
put()
de la biblioteca Arduino EEPROM solo escriben datos cuando cambian. Es decir, puede escribir cada segundo algunas configuraciones y códigos de modo que cambian muy raramente.
NVS utiliza un método diferente para controlar la nivelación del desgaste. Como se mencionó anteriormente, los datos en el sector flash se pueden escribir en partes, pero solo se puede borrar todo el sector. Por lo tanto, el registro de datos en NVS se lleva a cabo en una especie de diario, este diario se divide en páginas, que se colocan en un sector de la memoria flash. Los datos se escriben en pares de clave: valor. De hecho, es incluso más fácil que con EEPROM, porque trabajar con un nombre significativo es más fácil que con una dirección en la memoria. Upd: ¡ la longitud de la clave no tiene más de 15 caracteres!
Si primero escribe el valor 1
en la tecla somekey
tecla, y luego escribe el valor 2
en la misma tecla, el primer valor no se eliminará, solo se marcará como eliminado (Borrado) y se agregará una nueva entrada al registro:

Si intenta leer datos con somekey
último valor de esta clave. Porque Como el registro es común, los valores de las diferentes claves se almacenan uno al lado del otro a medida que se escriben.
La página tiene un estado, Vacío: vacío, sin entradas, Activo: los datos se están escribiendo actualmente, Completo: está lleno, no puede escribir en él. Tan pronto como la página se quede sin espacio, ella es de
Activo va a Completo, y la siguiente página Vacía se vuelve Activa.

Según tengo entendido de la documentación en el sitio web de Espressif y en varios foros, la limpieza de páginas comienza cuando las páginas gratuitas llegan a su fin. Para ser más precisos, de acuerdo con esto , el borrado ocurrirá cuando solo quede 1 página libre.
Si es necesario borrar la página, los registros actuales (no borrados) se mueven a otra página y se sobrescribe la página.
Por lo tanto, la operación de borrado de escritura para cada página en particular es bastante rara, cuantas más páginas, con menos frecuencia. En base a esto, aumenté el tamaño de la partición NVS a 1 MB, a mi velocidad de grabación es suficiente durante 170 años, que en general es suficiente. Acerca de cambiar el tamaño de la sección NVS será el siguiente.
Para un trabajo conveniente con NVS, ESP32 para Arduino Core tiene una práctica biblioteca de Preferencias escritas, aquí se escribe cómo trabajar con ellas.
Un poco sobre VisualGDB
Tan pronto como comencé a trabajar con Arduino IDE, me sorprendió de inmediato la miserable funcionalidad en comparación con Visual Studio. Dicen que VS tampoco es una fuente, aunque me conviene, pero escribir algo más de 50 líneas en el IDE de Arduino es dolorosamente doloroso y dolorosamente largo. Por lo tanto, surgió la cuestión de elegir un IDE para el desarrollo. Porque Estoy familiarizado con VS, me decidí por VisualGDB .
Después del Arduino IDE, el desarrollo del ESP32 es simplemente un paraíso. ¿Cuál es la transición a la definición, la búsqueda de llamadas en el proyecto y la capacidad de cambiar el nombre de la variable?
Cambiar la tabla de particiones ESP32 con VisualGDB
Como se mencionó anteriormente, la tabla se puede cambiar con la partición ESP32; consideraremos cómo se puede hacer esto.
La tabla se edita como un archivo csv, de forma predeterminada VisualGDB escribe la siguiente tabla:
Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xe000, 0x2000, app0, app, ota_0, 0x10000, 0x140000, app1, app, ota_1, 0x150000,0x140000, spiffs, data, spiffs, 0x290000,0x170000,
Aquí vemos una sección en NVS, dos secciones para aplicaciones y algunas secciones más. De los matices, se puede notar que app0 (su aplicación) siempre debe escribirse en el desplazamiento 0x10000, comenzando desde la dirección cero, de lo contrario, el gestor de arranque no lo detectará. Además, las compensaciones deben seleccionarse para que las secciones no se "superpongan" entre sí. La tabla de particiones se escribe en el desplazamiento 0x8000. Como puede ver, el tamaño del NVS en este caso es 0x5000 - 20KB, que no es mucho.
Modifiqué la tabla de particiones de la siguiente manera:
Name, Type, SubType, Offset, Size, Flags app0, app, ota_0, 0x10000, 0x140000, nvs, data, nvs, , 1M, otadata, data, ota, , 0x2000, spiffs, data, spiffs, , 0x170000,
No olvide agregar una cuadrícula antes de Nombre, si usa esta tabla, necesita que esta línea se considere un comentario.
Como puede ver, el tamaño del NVS se incrementa a 1 MB. Si no especifica desplazamientos, la sección comenzará inmediatamente después del anterior, por lo que es suficiente para indicar el desplazamiento solo para app0. Los archivos CSV se pueden editar en el bloc de notas como txt y luego cambiar el permiso a csv para el archivo guardado.
A continuación, la tabla de particiones debe convertirse a un binario, porque entra al controlador de esta forma. Para hacer esto, ejecute el convertidor:
c:\Users\userName\Documents\ArduinoData\packages\esp32\hardware\esp32\1.0.3\tools\gen_esp32part.exe part_table_name.csv part_table_name.bin
. El primer parámetro es su CSV, el segundo parámetro es el binario de salida.
El binario resultante debe colocarse en c:\Users\userName\Documents\ArduinoData\packages\esp32\hardware\esp32\1.0.3\tools\partitions\part_table_name.csv
, después de lo cual es necesario especificar que fue él quien fue tomado para construir la solución, y sin tabla de partición predeterminada. Puede hacerlo escribiendo el nombre de su tabla en el archivo c:\Users\userName\Documents\ArduinoData\packages\esp32\hardware\esp32\1.0.3\boards.txt
. En mi caso, esto es esp32doit-devkit-v1.build.partitions=part_table_name
Después de estas manipulaciones, VisualGDB al compilar la aplicación tomará exactamente su tabla de particiones y la colocará en
~project_folder_path\Output\board_name\Debug\project_name.ino.partitions.bin
, desde donde ya se habrá vertido en el tablero.
Depurador JTAG CJMC-FT232H
Hasta donde sé, este es el depurador más barato que puede funcionar con ESP32, me costó alrededor de 600 rublos, hay muchos de ellos en Aliexpress.

Cuando conecta el depurador, Windows instala en él controladores inadecuados que deben cambiarse utilizando el programa Zadig, todo es simple allí, no lo describiré.
Se conecta a ESP32 devkit-v1 de la siguiente manera:
FT232H - ESP32
AD0 - GPIO13
AD1 - GPIO12
AD2 - GPIO15
AD3 - GPIO14
Luego, en Project -> VisualGDB Project Properties
debe realizar la siguiente configuración:

Luego haga clic en Prueba. A veces sucede que la conexión no se establece la primera vez, el proceso parece congelarse, luego debe interrumpir y repetir la Prueba. Si todo está en orden, el proceso de prueba de la conexión dura unos 5 segundos.
Por lo general, ensamblé el proyecto y lo cargué a través de USB al ESP32 en sí (no a través del depurador), después de lo cual comencé a depurar usando Debug -> Attach to Running Embedded Firmware
. En el código, puede establecer puntos de interrupción, ver los valores de las variables en el momento del desglose y en la ventana Debug -> Windows -> Threads
puede ver en qué tarea de FreeRTOS se detuvo el código, lo que es útil si se produce un error durante la depuración. Estas funciones del depurador fueron suficientes para que yo trabajara cómodamente.
Cuando comencé a trabajar con NVS, la depuración se interrumpía constantemente por errores oscuros. Según tengo entendido, esto se debe a que el depurador debe crear algo así como un volcado en la sección NVS predeterminada, pero en este momento el controlador ya está utilizando el NVS. Por supuesto, esto podría evitarse creando 2 particiones NVS, una con el nombre predeterminado para la depuración y la otra para sus propias necesidades. Pero no había nada complicado allí, en el código agregado, funcionó la primera vez, así que no lo revisé.
Glitches ESP32
Al igual que cualquier dispositivo con Aliexpress, mi placa ESP32 tenía su propia falla, que no se describe en ninguna parte. Cuando llegó, le di de comer a la periferia algunos periféricos que funcionaban en I2C, pero después de un tiempo, la placa comenzó a reiniciarse si algún equipo que consumía o incluso un condensador estaba conectado a la pata de + 5V. Por qué es tan completamente incomprensible.
Ahora enciendo la placa desde la carga china 0.7A, los sensores ds18b20 desde el pie de la placa de 3.3V, y el relé y el sensor de movimiento desde otra carga de 2A. El pie GND de la placa está, por supuesto, conectado a los pines GND del resto de la plancha. Barato y alegre es nuestra opción.
Sobre los resultados del proyecto
Tuve la oportunidad de controlar de manera flexible la calefacción de la casa, ahorrando dinero y el sudor ganado. Por el momento, si la calefacción se mantiene 23 grados durante todo el día a -5 - -7 afuera, es alrededor de 11 horas de funcionamiento de la caldera. Si durante el día se mantiene a 20 grados y se calienta a 23 solo por la noche, entonces ya son 9 horas de funcionamiento de la caldera. La capacidad de la caldera es de 6 kW, con un precio actual de kilovatios de 2.2 rublos, esto es aproximadamente 26.4 rublos por día. La duración de la temporada de calefacción en nuestra área es de 200 días, la temperatura promedio en la temporada de calefacción es de aproximadamente -5 grados. Por lo tanto, obtenemos aproximadamente 5000r de ahorro para la temporada de calefacción.
El costo del equipo no excede 2000r, es decir, los costos se rechazarán en unos meses, sin mencionar el hecho de que un sistema listo para usar de tal automatización costaría al menos 20,000r. Otra cosa es que pasé aproximadamente una semana de tiempo de trabajo puro escribiendo firmware y depuración, pero en el curso del trabajo, por ejemplo, finalmente me di cuenta de qué punteros hay en C ++ y obtuve mucha otra experiencia (por ejemplo, la experiencia de muchas horas de depuración de fallas incomprensibles). Y la experiencia, como saben, es difícil de sobreestimar.
Capturas de pantalla de la aplicación móvil Blynk:



Por supuesto, el código en el proyecto no es una obra maestra, pero lo escribí en condiciones de falta de tiempo y me enfoqué principalmente en la legibilidad. Simplemente no hay tiempo para refactorizar. En general, tengo muchas excusas por las que mi código da tanto miedo, pero este es mi favorito, así que me detendré en ello, no desarrollaré el tema más.
Si mi garabato ayuda a alguien, me alegraré sinceramente. Estaré encantado de cualquier comentario y sugerencia.