FreeRTOS для самых маленьких на примере ESP-IDF

в 9:01, , рубрики: embedded, esp32, freertos, http, ruvds_статьи, Программирование, шаговики, шаговые двигатели, шаговый двигатель

FreeRTOS для самых маленьких на примере ESP-IDF - 1


Все мы знаем, что FreeRTOS — это операционная система для проектов, где каждой задаче установлены строгие рамки времени, чтобы любое действие было гарантированно обработано. На этом познания об этой системе у большинства айтишников заканчиваются, по той простой причине, что 99% из нас не собираются никогда в жизни разрабатывать ничего наподобие софта тормозной системы автомобиля или медицинского оборудования. Но я бы хотел сегодня немного рассказать об этой системе, потому как она вполне может пригодиться многим из нас по другим причинам. Ведь FreeRTOS вполне может оказаться отличным следующим шагом для саморазвития после Arduino, потому как поддерживает невероятное количество процессоров и при разрастании проекта немногим сложнее «Hello World» её использование будет проще для написания кода за счёт своей продуманной архитектуры. Её можно использовать на микроконтроллерах, с которыми многие уже привыкли работать в своих проектах выходного дня и которые обходятся в смешные деньги, например AVR, ESP32 и STM32. Сегодня я покажу, насколько просто использовать преимущества FreeRTOS на примере контроллера ESP32 и фреймворка от производителя Espressif — ESP-IDF, для своей линейки микроконтроллеров.

Начнём

В первую очередь нужно принять тот факт, что хотя FreeRTOS и называется операционной системой, она не является универсальной повседневной системой, как, например, Linux или Windows, которые мы устанавливаем на компьютеры. Её нельзя установить на компьютер и даже на микроконтроллер и запускать в ней различный софт. Тут немного другой подход. Но прежде чем углубляться, давайте рассмотрим возможные подходы к решению задачи написания кода для микрокомпьютера или микроконтроллера.

▍ 1. Linux

Установка Linux-дистрибутива и компиляция своей программы или даже софта с github по принципу, похожему на установку софта на обычный компьютер. Такой вариант годится для относительно мощных платформ вроде Raspberry. Linux поддерживает многоядерность, многопоточность, как правило, уже имеет драйверы практически для любого оборудования, можно легко использовать стандартные PCIE, USB и даже SDIO и UART устройства вроде GPS модулей, Bluetooth устройств. С каждым новым ядром списки поддерживаемого оборудования пополняются семимильными шагами. Это отличное, универсальное, но очень дорогое решение, если нужно просто окрашивать кнопочки, моргать светодиодами и крутить моторчики. Цены на такие платы начинаются от 3к. руб. и вверх на луну. Однако, я уже рассказывал, как для запуска Linux можно использовать отработавшее свою жизнь бытовое оборудование в постах про «Одноплатный компьютер из камеры видеонаблюдения» и «Превращаем TV-box в мини-компьютер».

▍ 2. Написание программ с использованием платформы Arduino

Отличный вариант для самых простых микроконтроллеров, чтобы ознакомить детишек с тем, что такое микроконтроллер и как просто поморгать LED. Скрывает сложности как программирования, так и аппаратной части, чем невероятно снижает порог вхождения. За счёт огромного количества наработанного кода это очень популярное решение. Есть оверхед по ресурсам, но сильно расстраиваться не стоит, потому как, например, в ESP32 под капотом Arduino мы найдём тот же FreeRTOS.

▍ 3. Компиляция своей программы без операционки (bare metal)

Пишем на чистом Си или даже ассемблере, используя библиотеки производителя платформы и различные сторонние библиотеки, если нужно задействовать модули WiFi, Bluetooth, протоколы вроде 1wire, I2C e.t.c. Требует полный спектр знаний в аппаратной части и электронике, в цифровой логике, во внутренней периферии микроконтроллера, такой как регистры процессора, таймеры, компараторы, умения работать на низком уровне с прерываниями. Хоть это и даёт программисту полный контроль над системой, позволяя оптимизировать производительность и использовать все возможности аппаратного обеспечения, но код пишется медленно. Для большинства людей сложно, очень сложно.

FreeRTOS для самых маленьких на примере ESP-IDF - 2

Ничего не понятно!

▍ 4. FreeRTOS

Это операционная система, предоставляющая набор компонентов, из которых мы включаем в свой проект только необходимые и используем их в своём коде. Она не устанавливается как Linux! Она собирается в единую прошивку вместе с нашим проектом.

FreeRTOS для самых маленьких на примере ESP-IDF - 3

Часть команды, часть корабля

При этом она корректно использует все необходимые функции аппаратной платформы, такие как поддержка множества ядер процессора, WiFi и BT модули.

FreeRTOS для самых маленьких на примере ESP-IDF - 4

Выбор и настройка компонентов FreeRTOS через 'idf.py menuconfig'

Если нам нужно только получить доступ к GPIO, то нужно подключить компонент driver. Для HTTP-сервера подключаем esp_http_server. А в коде программы подключить заголовки «driver/ledc.h» и «esp_http_server.h». Но чтобы HTTP-сервер обрёл смысл на нашей железке, нам обязательно понадобится ещё и сеть, а это компонент esp_wifi, который можно использовать как WiFi-клиент или точку доступа. Соответственно не забываем и про заголовок «esp_wifi.h». В общем при настройке проекта нужно чётко понимать, какие компоненты будут задействованы. Все настройки конфигуратора будут сохранены в файл «sdkconfig».

Тут некий читатель наверняка начал подозревать, что в проектах с перспективой на разрастание использование FreeRTOS будет не только более профессиональным, но и более простым решением за счёт отличной структурированности этой системы. И такой читатель чертовски прав!

Давайте сначала я покажу, как поморгать светодиодом, а потом перейдём к более интересной задаче, которая будет не намного сложнее, но ближе к практическому применению: используем встроенный в ESP32 WiFi-модуль, чтобы подключиться к сети, прямо на ESP32 поднимем HTTP-сервер, который будет обрабатывать запросы по пути /feed и позволит нам управлять шаговым двигателем через интернет. И всё это на популярной плате ESP-WROOM-32, которая у большинства тех читателей, кто дочитал до этого места, наверняка валяется в загашнике. Цена этой платы, близкая к цене чашечки кофе в три сотни рубчиков, просто смешна. А если учесть, что на борту не только двухъядерный процессор частотой до 240Mhz, а также модули WiFi и BT со встроенной или подключаемой внешней антенной, то эта плата просто вне конкуренции для подобных проектов. Кроме того, на плате уже есть встроенный UART-адаптер, который позволяет просто воткнуть плату в USB, без программатора прошить прошивку и даже получать отладочную информацию нашего приложения через консоль. Кстати, есть более дешёвые модули без встроенного UART-контроллера, которые по размерам очень малы и, соответственно, более энергоэффективны без питания этой микросхемы. Такие удобнее использовать уже в готовых устройствах, которые достаточно прошить один раз через внешний USB UART адаптер.

FreeRTOS для самых маленьких на примере ESP-IDF - 5

ESP32 без UART-контроллера и с IPEX выходом на внешнюю антенну WiFi

Hello world на FreeRTOS в ESP32: моргаем LED

Приборы и материалы:

  1. Компьютер с USB.
  2. ESP-WROOM-32 для простоты с USB-интерфейсом.
  3. LED и желательно резистор на 100-1000 Ом, но не обязательно.
  4. Паяльник или макетные провода.

Для того, чтобы в миллионный раз не описывать одно и то же и не утонуть в подробностях настройки под Linux/Windows/MAC и под любимую IDE, я буду давать ссылки на уже существующие посты. Настраиваем по любой инструкции из интернета ESP-IDF любым подходящим способом.

Заглядываем в загашник и находим какой-то ESP32 микроконтроллер с WiFi на борту, который уже и не вспомните для чего брали. Моя ставка, что это будет что-то вроде ESP-WROOM-32. Но даже если это более простой ESP8266, не расстраивайтесь. На таких тоже можно собрать проект под управлением FreeRTOS, однако придётся установить другой фреймворк ESP8266_RTOS_SDK.

После настройки SDK в системе убеждаемся, что idf.py работает, и запускаем в директории, где хотим создать проект:

$ idf.py create-project habr-esp32-led
cd habr-esp32-led/

Будет создана директория habr-esp32-led. Сильно не радуйтесь, увидев CMakeLists.txt, потому что эти проекты управляются не через cmake, а через idf.py.

У вас есть директория habr-esp32-led, в которой найдётся habr-esp32-led/main/CMakeLists.txt следующего содержания:

idf_component_register(SRCS "habr-esp32-led.c" 
                   INCLUDE_DIRS ".")

Чтобы проект мог использовать GPIO, нам нужно подключить компонент «driver» для работы с GPIO. С добавлением новых Си заголовков всегда нужно подключать соответствующие компоненты в этом файле. Вот так должно получиться после изменений:

idf_component_register(SRCS "habr-esp32-led.c" 
                   INCLUDE_DIRS "." 
                   REQUIRES driver 
                   )

Должна получиться вот такая структура проекта с двумя различными CMakeLists.txt:


habr-esp32-led
  - CMakeLists.txt
  - sdkconfig
  - main
       - CMakeLists.txt
       - habr-esp32-led.c
 

Записываем следующий код в ./habr-esp32-led/main/habr-esp32-led.c:

#include "freertos/FreeRTOS.h" 
#include "freertos/task.h" 
#include "driver/gpio.h" 
#include <stdio.h> 

#define LED_PIN GPIO_NUM_2  // Пин для подключения светодиода 

void app_main(void) 
{ 
   gpio_reset_pin(LED_PIN);  // Сбрасываем настройки пина 
   gpio_set_direction(LED_PIN, GPIO_MODE_OUTPUT);  // Устанавливаем пин как выход 

   while (1) { 
       gpio_set_level(LED_PIN, 1);  // Включаем светодиод 
       vTaskDelay(1000 / portTICK_PERIOD_MS);  // Задержка 1 секунда 

       gpio_set_level(LED_PIN, 0);  // Выключаем светодиод 
       vTaskDelay(1000 / portTICK_PERIOD_MS);  // Задержка 1 секунда 
   } 
}

Уверен, кто уже имеет некий опыт в программировании на Си под микроконтроллеры, по аналогии с Arduino поймёт по комментариям, что тут происходит, но всё равно хоть немного я должен пояснить.

Задаём алиас, который заявляет, что LED_PIN полностью аналогичен использованию GPIO_NUM_2, что в свою очередь говорит контроллеру в дальнейшем работать с пином GPIO под номером 2. Вы можете использовать и другие GPIO-выходы.

void app_main() — это главная функция входа в программу, такая как void loop() в Arduino или void main() в классическом Си. Выход из этой функции не предусмотрен, и она запускается лишь единожды. Соответственно все настройки портов не выносятся в отдельную функцию setup(), а бесконечный цикл организуется через «while (1) {}».

vTaskDelay() блокирует текущий поток на заданное количество тиков, которое зависит от частоты процессора, поэтому для использования более удобных миллисекунд, чтобы не пересчитывать каждый раз при переконфигурировании под различные процессоры или разные частоты текущего процессора, мы переводим миллисекунды в тики, используя константу фреймфорка portTICK_PERIOD_MS.

Можно запустить конфигуратор из директории проекта habr-esp32-led и перенастроить проект, но для текущей задачи менять пока ничего не потребуется:

$ idf.py menuconfig

Выбирайте ваш конкретный контроллер, например для ESP-WROOM-32:

$ idf.py set-target esp32

Вот такие контроллеры поддерживаются в ESP32 IDF:

$ idf.py set-target 
Usage: idf.py set-target [OPTIONS] {esp32|esp32s2|esp32c3|esp32s3|esp32c2|esp32c6|esp32h2|esp32p4|linux|esp32c5|esp32c61} 
Try 'idf.py set-target --help' for help. 

Error: Missing argument '{esp32|esp32s2|esp32c3|esp32s3|esp32c2|esp32c6|esp32h2|esp32p4|linux|esp32c5|esp32c61}'. Choose from: 
       esp32, 
       esp32s2, 
       esp32c3, 
       esp32s3, 
       esp32c2, 
       esp32c6, 
       esp32h2, 
       esp32p4, 
       linux, 
       esp32c5, 
       esp32c61

После следующей команды проект будет несколько минут компилироваться и в конце напишет примерно следующее:

idf.py build
...
habr-esp32-led.bin binary size 0x2d2a0 bytes. Smallest app partition is 0x100000 bytes. 0xd2d60 bytes (82%) free. 

Project build complete. To flash, run: 
idf.py flash 
or 
idf.py -p PORT flash 
or 
python -m esptool --chip esp32 -b 460800 --before default_reset --after hard_reset write_flash --flash_mode dio --flash_size 2MB --flash_freq 40m 0x1000 build/bootloader/bootloader.bin 0x8000 build/partition_table/partition-table.bin 0x10000 build/habr-esp32-led.bin 
or from the "/mnt/raid/devel/habr/habr-esp32-led/build" directory 
python -m esptool --chip esp32 -b 460800 --before default_reset --after hard_reset write_flash "@flash_args"

Это означает, что прошивка собрана и готова к тому, чтобы быть залитой в микроконтроллер:

idf.py flash

Или с указанием нужного USB порта:

idf.py -p /dev/ttyUSB0 flash

Это прошьёт программу в микроконтроллер и наша программа автоматически запустится.

Подключив светодиод к выводам GPIO2 и GND, можно увидеть его мигание. Подключать нужно через токоограничивающий резистор, но если использовать слабый светодиод, то он не нанесёт ущерба вашей плате даже без резистора, если всего несколько раз мигнёт для проверки.

Вот что мы должны увидеть:

FreeRTOS для самых маленьких на примере ESP-IDF - 6

LED на микроконтроллере ESP32 должен мигать

Для отладки можно подключиться к ESP32-консоли через minicom или picocom, так как на платах ESP32 с USB подключением уже есть UART-контроллер:

picocom -b 115200 -r -l /dev/ttyUSB0

Так мы сможем увидеть лог загрузки после нажатия RESET и работы FreeRTOS, а также всё, что выводят наши команды printf().

Пример лога загрузки FreeRTOS и запуска тасков

ets Jul 29 2019 12:21:46

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:6276
load:0x40078000,len:15728
ho 0 tail 12 room 4
load:0x40080400,len:4
load:0x40080404,len:3860
entry 0x4008063c
I (31) boot: ESP-IDF v5.4-dev-3602-ga97a7b0962-dirty 2nd stage bootloader
I (31) boot: compile time Dec 10 2024 15:54:10
I (31) boot: Multicore bootloader
I (35) boot: chip revision: v3.1
I (38) boot.esp32: SPI Speed: 40MHz
I (41) boot.esp32: SPI Mode: DIO
I (45) boot.esp32: SPI Flash Size: 4MB
I (48) boot: Enabling RNG early entropy source…
I (53) boot: Partition Table:
I (55) boot: ## Label Usage Type ST Offset Length
I (62) boot: 0 nvs WiFi data 01 02 00009000 00006000
I (68) boot: 1 phy_init RF data 01 01 0000f000 00001000
I (75) boot: 2 factory factory app 00 00 00010000 00100000
I (81) boot: End of partition table
I (85) esp_image: segment 0: paddr=00010020 vaddr=3f400020 size=21f1ch (139036) map
I (140) esp_image: segment 1: paddr=00031f44 vaddr=3ff80000 size=00018h ( 24) load
I (140) esp_image: segment 2: paddr=00031f64 vaddr=3ffb0000 size=03f38h ( 16184) load
I (150) esp_image: segment 3: paddr=00035ea4 vaddr=40080000 size=0a174h ( 41332) load
I (167) esp_image: segment 4: paddr=00040020 vaddr=400d0020 size=8c988h (575880) map
I (364) esp_image: segment 5: paddr=000cc9b0 vaddr=4008a174 size=0d6bch ( 54972) load
I (397) boot: Loaded app from partition at offset 0x10000
I (397) boot: Disabling RNG early entropy source…
I (408) cpu_start: Multicore app
I (416) cpu_start: Pro cpu start user code
I (416) cpu_start: cpu freq: 160000000 Hz
I (416) app_init: Application information:
I (416) app_init: Project name: esp32shneck
I (420) app_init: App version: 1
I (424) app_init: Compile time: Dec 10 2024 15:54:09
I (429) app_init: ELF file SHA256: 932c20a3e…
I (433) app_init: ESP-IDF: v5.4-dev-3602-ga97a7b0962-dirty
I (439) efuse_init: Min chip rev: v0.0
I (443) efuse_init: Max chip rev: v3.99
I (447) efuse_init: Chip rev: v3.1
I (451) heap_init: Initializing. RAM available for dynamic allocation:
I (457) heap_init: At 3FFAE6E0 len 00001920 (6 KiB): DRAM
I (462) heap_init: At 3FFB81B0 len 00027E50 (159 KiB): DRAM
I (467) heap_init: At 3FFE0440 len 00003AE0 (14 KiB): D/IRAM
I (473) heap_init: At 3FFE4350 len 0001BCB0 (111 KiB): D/IRAM
I (478) heap_init: At 40097830 len 000087D0 (33 KiB): IRAM
I (485) spi_flash: detected chip: generic
I (487) spi_flash: flash io: dio
I (491) main_task: Started on CPU0
I (501) main_task: Calling app_main()
I (521) gpio: GPIO[14]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0
I (521) gpio: GPIO[25]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0
I (521) gpio: GPIO[26]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0
I (531) gpio: GPIO[27]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0
I (541) FEEDER: GPIO is initialised
I (541) FEEDER: Init Wi-Fi…
I (551) main_task: Returned from app_main()
I (561) wifi:wifi driver task: 3ffc2e48, prio:23, stack:6656, core=0
I (571) wifi:wifi firmware version: 0a80d45
I (571) wifi:wifi certification version: v7.0
I (571) wifi:config NVS flash: enabled
I (571) wifi:config nano formatting: disabled
I (571) wifi:Init data frame dynamic rx buffer num: 32
I (581) wifi:Init static rx mgmt buffer num: 5
I (581) wifi:Init management short buffer num: 32
I (591) wifi:Init dynamic tx buffer num: 32
I (591) wifi:Init static rx buffer size: 1600
I (601) wifi:Init static rx buffer num: 10
I (601) wifi:Init dynamic rx buffer num: 32
I (601) wifi_init: rx ba win: 6
I (611) wifi_init: accept mbox: 6
I (611) wifi_init: tcpip mbox: 32
I (611) wifi_init: udp mbox: 6
I (621) wifi_init: tcp mbox: 6
I (621) wifi_init: tcp tx win: 5760
I (621) wifi_init: tcp rx win: 5760
I (621) wifi_init: tcp mss: 1440
I (631) wifi_init: WiFi IRAM OP enabled
I (631) wifi_init: WiFi RX IRAM OP enabled
W (641) wifi:Password length matches WPA2 standards, authmode threshold changes from OPEN to WPA2
I (641) phy_init: phy_version 4840,02e0d70,Sep 2 2024,19:39:07
I (721) wifi:mode: sta (a0:a3:b3:fe:eb:e5)
I (721) wifi:enable tsf
I (721) FEEDER: Wi-Fi is started
I (741) wifi:new:<1,0>, old:<1,0>, ap:<255,255>, sta:<1,0>, prof:1, snd_ch_cfg:0x0
I (741) wifi:state: init -> auth (0xb0)
I (751) wifi:state: auth -> assoc (0x0)
I (761) wifi:state: assoc -> run (0x10)
I (781) wifi:connected with WIFIAP, aid = 3, channel 1, BW20, bssid = a0:f3:c1:6c:e6:93
I (781) wifi:security: WPA2-PSK, phy: bgn, rssi: -76
I (781) wifi:pm start, type: 1

I (781) wifi:dp: 1, bi: 102400, li: 3, scale listen interval from 307200 us to 307200 us
I (801) wifi:<ba-add>idx:0 (ifx:0, a0:f3:c1:6c:e6:92), tid:0, ssn:1, winSize:64
I (881) wifi:dp: 2, bi: 102400, li: 4, scale listen interval from 307200 us to 409600 us
I (881) wifi:AP's beacon interval = 102400 us, DTIM period = 2
I (1821) esp_netif_handlers: sta ip: 192.168.1.171, mask: 255.255.255.0, gw: 192.168.1.1
I (1821) FEEDER: Got IP address: 192.168.1.171

Крутим шаговик по HTTP на заданное количество шагов в ESP32

Для этого проекта нам понадобится уже повозиться подольше.

Добавляем к приборам и материалам:

  1. Микросхема драйвера двигателя L293D или похожая L298.
  2. Макетная плата с проводами dupont или запаять всё на «соплях».
  3. Шаговый мотор, желательно на напряжение 5В (я выкорчевал из CD-привода от древнего ноутбука).
  4. Желательно поставить конденсатор в параллель с питанием примерно 100mF и напряжением выше 10V.

Шаговый двигатель отличается от других типов тем, что подавая попеременно напряжения на различные обмотки мы можем точно поворачивать его на определённый угол, отсчитывая количество сделанных шагов. Выбрал я этот тип двигателя лишь для усложнения проекта, чтобы был практический смысл выделить задачу отсчитывания шагов в отдельный таск (грубо говоря, поток в терминологии FreeRTOS). Ну ещё и потому, что у меня в груде компьютерного хлама шаговики валяются в ассортименте.

К сожалению, как в случае со светодиодом, схалтурить и пустить ток напрямую с микроконтроллера на мотор тут не удастся. Слишком уж большой ток потянет даже самый маленький шаговичёк, но всё равно тока от микроконтроллера не будет достаточно, чтобы его повернуть. Я использовал маломощный драйвер моторов L293D, потому как для моего шаговика от ноутбуковского CD-привода хватает для проверки программы токов и напряжения в 5В, взятых прямо с питания ESP32-контроллера. Если использовать другие шаговые моторы, то нужно проверить их напряжение питания. Скорее всего, придётся подключать к микросхеме повышенное напряжение. Вместо L293D вполне можно использовать с похожей распиновкой L298, которая позволяет крутить мотор токами до 3 ампер на каждый выход при напряжении до 50 вольт. Однако из-за своего расположения ног L298 не становится на макетную плату. На Ali можно найти такие микросхемы в виде готовых модулей L298, которые можно подключать без пайки.

▍ Собираем железо

FreeRTOS для самых маленьких на примере ESP-IDF - 7

Ориентировочная схема подключения

Обратите внимание в документации, что микросхемы драйвера почти всегда имеют два напряжения: ~ 5В для питания внутренней логики (ножка 16 на L293D) и повышенное напряжение для питания моторов (Ножка 8 на L293D). В своей сборке, как я сказал, для питания шаговика было достаточно 5 вольт и я взял напряжение прямо с платы ESP32, которую запитал от USB. Это очень упростило схему и позволило запаять все провода подвесным монтажом. Разница со схемой только в том, что вместо двух моторов DC мы подключаем две обмотки шагового мотора. Нам понадобится шаговик с 4 контактами. Прозвоните 4 выхода и выясните, которые из них образуют замкнутые пары обмоток, каждую из которых следует подключать к выходам драйвера OUT1-OUT2 и OUT3-OUT4 соответственно.

FreeRTOS для самых маленьких на примере ESP-IDF - 8

L293D распиновка

Итак, на input 1-4 мы подаём маломощный сигнал с указанных в нижеприведённом коде GPIO микроконтроллера, а к output 1-4 подключаем обмотки шагового мотора. Для того, чтобы разрешить выходной сигнал, оба пина enable (Enable1,2, Enable3,4) замыкаем на VCC +5v. В этом проекте мы не будем управлять ими через GPIO, как нарисовано в ориентировочной схеме.

Однако если вы питаете схему напряжения выше 5 вольт, то используйте любой понижающий преобразователь, который на схеме выглядит как LM7805. Выходные 5 вольт будут питать ESP32 и логику драйвера мотора, а высокое напряжение без понижения через драйвер запитает моторы.

FreeRTOS для самых маленьких на примере ESP-IDF - 9

Итог сборки железа

▍ Код

По инструкции из начала поста создайте новый проект в директории esp32_shneck.

Меняем содержимое ./esp32_shneck/main/CMakeLists.txt на следующее:

                    PRIV_REQUIRES spi_flash
                    INCLUDE_DIRS ""
                    REQUIRES driver esp_wifi esp_netif nvs_flash esp_http_server
)

Опция REQUIRES подключает в проект следующие модули ESP-IDF:

— driver — поддержка базовых функций таких как GPIO, SPI, I2C, UART, PWM,
— esp_wifi — WiFi клиент,
— esp_netif — WiFi интерфейс,
— nvs_flash — работа с флэш памятью, которая нужна для корректной работы драйвера WiFi,
— esp_http_server — HTTP сервер.

Основная программа нашего проекта на Си ./esp32_shneck/main/esp32shneck.c:

Код esp32shneck.c

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_http_server.h"
#include "freertos/queue.h"
#include "driver/ledc.h"

#define TAG "FEEDER"
#define WIFI_SSID "WifiAP"
#define WIFI_PASSWORD "WifiPassword"

#define IN1_GPIO GPIO_NUM_25 // IN1 L293
#define IN2_GPIO GPIO_NUM_26 // IN2 L293
#define IN3_GPIO GPIO_NUM_27 // IN3 L293
#define IN4_GPIO GPIO_NUM_14 // IN4 L293
#define STEP_DELAY_MS 10

const int step_sequence[8][4] = {
    {0, 1, 0, 0},
    {0, 1, 0, 1},
    {0, 0, 0, 1},
    {1, 0, 0, 1},
    {1, 0, 0, 0},
    {1, 0, 1, 0},
    {0, 0, 1, 0},
    {0, 1, 1, 0}
};


QueueHandle_t shneckQueue;

void step_motor_gpio_init() {
    gpio_config_t io_conf = {
        .pin_bit_mask = (1ULL << IN1_GPIO) | (1ULL << IN2_GPIO) |
                        (1ULL << IN3_GPIO) | (1ULL << IN4_GPIO),
        .mode = GPIO_MODE_OUTPUT,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .pull_up_en = GPIO_PULLUP_DISABLE,
        .intr_type = GPIO_INTR_DISABLE
    };
    gpio_config(&io_conf);

    ESP_LOGI(TAG, "GPIO is initialised");
}

void step_motor_step(int step) {
    gpio_set_level(IN1_GPIO, step_sequence[step][0]);
    gpio_set_level(IN2_GPIO, step_sequence[step][1]);
    gpio_set_level(IN3_GPIO, step_sequence[step][2]);
    gpio_set_level(IN4_GPIO, step_sequence[step][3]);
}

void schneck_rotate_task(void *pvParameters) {
    int direction = 1;  // 1 - clockwise, -1 - counterclockwise
    int current_step = 0;
    int steps_left = 0;
    while(1) {
            if (steps_left == 0) {
                if (xQueueReceive(shneckQueue, &steps_left, portMAX_DELAY))
                    ESP_LOGI(TAG, "Got %d steps from queue", steps_left);
                }
            else {
                step_motor_step(current_step);
                current_step = (current_step + direction + 8) % 8;
                steps_left--;
                }
            vTaskDelay(pdMS_TO_TICKS(STEP_DELAY_MS));
    }
}

esp_err_t index_handler(httpd_req_t *req) {
    char *indexhtml = "<!DOCTYPE html><html><head><title>GET</title></head><body><button onclick="fetch('/feed?steps=100')">FEED</button></body></html>";
    httpd_resp_send(req, indexhtml, strlen(indexhtml));
    return ESP_OK;
}

esp_err_t feed_handler(httpd_req_t *req) {
    char query[128];
    char value[32];
    int error = 0;

    if (httpd_req_get_url_query_str(req, query, sizeof(query)) == ESP_OK) {
        ESP_LOGI(TAG, "URL Query: %s", query);

        if (httpd_query_key_value(query, "steps", value, sizeof(value)) == ESP_OK) {
            ESP_LOGI(TAG, "Steps Parameter Value: %s", value);
            int steps = atoi(value);
            ESP_LOGI(TAG, "Steps as Integer: %d", steps);
            if (steps > 0) {
                ESP_LOGI(TAG, "Rotating motor ...");
                if (xQueueSend(shneckQueue, &steps, pdMS_TO_TICKS(100)) != pdPASS) {
                    ESP_LOGW("ShneckTask", "Queue is full...");
                    error = 1;
                }
            } else {
                ESP_LOGW(TAG, "Invalid steps value: %d", steps);
            }
        } else {
            ESP_LOGW(TAG, "Parameter 'steps' not found");
        }
    } else {
        ESP_LOGI(TAG, "No URL query found");
    }
    
    const char *resp_str = "Steps parameter processed";
    httpd_resp_send(req, resp_str, strlen(resp_str));

    if (error == 1) {
        ESP_LOGW(TAG, "Error during queueing");
        return ESP_ERR_NOT_ALLOWED;
    }
    return ESP_OK;
}

 httpd_handle_t start_webserver(void) {
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();

    httpd_handle_t server = NULL;
    if (httpd_start(&server, &config) == ESP_OK) {
        httpd_uri_t write_servo_uri = {
            .uri = "/feed",
            .method = HTTP_GET,
            .handler = feed_handler,
            .user_ctx = NULL
        };
        httpd_register_uri_handler(server, &write_servo_uri);

        httpd_uri_t index_uri = {
            .uri = "/",
            .method = HTTP_GET,
            .handler = index_handler,
            .user_ctx = NULL
        };
        httpd_register_uri_handler(server, &index_uri);

    }
    return server;
}


 void wifi_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) {
    if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
        esp_wifi_connect();
    } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
        ESP_LOGI(TAG, "Wi-Fi disconnected, reconnecting...");
        esp_wifi_connect();
    } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
        ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
        ESP_LOGI(TAG, "Got IP address: " IPSTR, IP2STR(&event->ip_info.ip));
        start_webserver();
    }
}

void http_server_task(void *pvParameters) {
    ESP_LOGI(TAG, "Init Wi-Fi...");
    esp_netif_init();
    esp_event_loop_create_default();
    esp_netif_create_default_wifi_sta();

    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    esp_wifi_init(&cfg);

    esp_event_handler_instance_t instance_any_id;
    esp_event_handler_instance_t instance_got_ip;
    esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL, &instance_any_id);
    esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &wifi_event_handler, NULL, &instance_got_ip);

    wifi_config_t wifi_config = {
        .sta = {
            .ssid = WIFI_SSID,
            .password = WIFI_PASSWORD,
        },
    };
    esp_wifi_set_mode(WIFI_MODE_STA);
    esp_wifi_set_config(WIFI_IF_STA, &wifi_config);
    ESP_ERROR_CHECK(esp_wifi_start());
    ESP_LOGI(TAG, "Wi-Fi is started");
    vTaskDelete(NULL);
}

void app_main(void) {
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);

    shneckQueue = xQueueCreate(10, sizeof(int));
    step_motor_gpio_init();

    xTaskCreate(schneck_rotate_task, "schneck_rotate_task", 2048, NULL, 5, NULL);
    xTaskCreate(http_server_task, "http_server_task", 8192, NULL, 5, NULL);
}

▍ Объяснения по коду

Директивами include подключаются заголовочные файлы с функциями необходимых нам компонентов.

WIFI_SSID и WIFI_PASSWORD задают реквизиты подключения к сети WiFi для работы нашей программы в качестве клиента.

IN1_GPIO, IN2_GPIO, IN3_GPIO, IN4_GPIO задают, на каких GPIO будем выдавать сигналы для нашего шагового мотора.

STEP_DELAY_MS — задержка между шагами мотора. Чем меньше задержка, тем быстрее вращение.

step_sequence — таблица истинности, которая задаёт состояния четырёх выходов микроконтроллера, которые подключены на входы драйвера мотора IN1, IN2, IN3, IN4. Всего 8 строк, каждая из которых описывает состояния обмоток шаговика для циклических 8 шагов. Можно было бы обойтись четырьмя циклическими шагами, но тогда мотор может работать не так плавно и больше шуметь. Если переключаем состояния в сторону увеличения номера строки в таблице состояний, то мотор будет вращаться в одну сторону. Если уменьшать номер строки, то мотор будет вращаться в противоположную. Если мотор не будет вращаться, а начнёт вибрировать на месте, то нужно переключить направление одной или другой обмотки (поменять местами OUT1 и OUT2 или OUT 3 и OUT4).

Для полного понимания FreeRTOS рекомендую божественный курс Владимира Мединцева.

Но для запуска проекта лишь вскользь галопом по Европам. Находим функцию входа в программу app_main() в которой код:

  • инициализирует внутренюю флэш память для корректной работы WiFi драйвера через nvs_flash_init(),
  • создаёт очередь shneckQueue через которую мы будем передавать заказы на вращение шаговика от задачи (потока) HTTP сервера к задаче, работающей с железом
  • инициализируем GPIO step_motor_gpio_init()
  • создаём и запускаем две одновременно работающих задачи FreeRTOS, которые заданы функциями schneck_rotate_task() и http_server_task(). Ни одна из этих функций не должна никогда завершаться, иначе это закрашит всю систему. Каждая из этих функций, чтобы стать задачей, принимает аргумент pvParameters.

▍ schneck_rotate_task()

В этой функции переменная current_step отвечает за текущее состояние выходов для подачи на мотор, которое определяется строкой в таблице step_sequence под номером от 0 до 7. Если на следующем шаге current_step превысит 7, то для текущего состояния выбирается нулевая строка. Между шагами выдерживается пауза, заданная в STEP_DELAY_MS.

steps_left определяет, сколько всего шагов необходимо сделать по указанию пользователя. Изначально это значение равно нулю и мотор находится в состоянии покоя. На каждой итерации бесконечного цикла через функцию xQueueReceive() происходит попытка получить из очереди новый объект для обработки. В нашем случае объект в очереди это целое число, предписывающее сделать указанное количество шагов. Но для более сложных проектов это может быть более сложная структура, имеющая большее количество данных, и я специально сделал это через очередь, чтобы читатель понял удобство межпроцессного взаимодействия в ОС FreeRTOS.

▍ http_server_task()

В этой задаче инициализируется подсистема WiFi и задаётся обработчик событий WiFi wifi_event_handler(), который при успешном подключении к сети запустит start_webserver().

А вот в коде этой функции самое время вкусить прелесть и простоту встроенного в FreeRTOS веб-сервера. Мы задаём два обработчика HTTP-путей:

"/" — запустит index_handler(),
"/feed" — запустит feed_handler().

Это, конечно, не Django и не Laravel, но для сервака за $3 — просто праздник!

index_handler() вываливает пользователю HTML с кнопкой, а по нажатию на кнопку отправляется «заказ» на поворот на 100 шагов по IP адресу нашего ESP32 в сети 192.168.1.10/feed?steps=100.

Аргумент steps мы обрабатываем из GET-запроса функцией httpd_req_get_url_query_str(), а по результатам обработки даже высылаем пользователю HTTP-ответ функцией httpd_resp_send().

▍ Результат

FreeRTOS для самых маленьких на примере ESP-IDF - 10

Вращение шагового мотора от CD-привода ноутбука

Развивая проект за смешные деньги на основе ESP32, FreeRTOS и шаговых двигателей, можно создать программно-аппаратный комплекс для самых разных целей: автоматической кормушки для кота или сельской живности, трекинговой системы для телескопа, фотоаппарата или солнечных панелей, автоматизации теплиц (например, для точного открывания форточек), управления шторами или жалюзи, а также машинками через TCP/IP.

© 2025 ООО «МТ ФИНАНС»

Автор: SergeyNovak

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js