Тут должна быть КДПВ, но на нее не хватило бюджета.

Замотивировавшись ответом от Tarson на мой комментарий к Программирование и обмен данными с «ARDUINO» по WI-FI посредством ESP8266, решил написать про основы программирования ESP8266 на C под FreeRTOS. Подробности под катом.

Шаг 0 — устройство

Для начала надо обзавестись устройством c ESP8266, желательно, чтобы там был разведен USB to UART, чтобы не пришлось городить программатор. Я свои бесчеловечные опыты провожу на NodeMCU.

Итак, шаг 1 — собираем тулчейн

Для начала надо обзавестись компьютером с установленным на нем дистрибутивом Linux (у меня OpenSUSE Leap). Идем на гитхаб по ссылке тыц, читаем инструкцию по сборке, устанавливаем необходимые зависимости, клонируем репозиторий, и собираем. Я клонировал в /opt/ESP и перед сборкой правил Makefile, выставив переменные:

STANDALONE = n
VENDOR_SDK = 2.1.0

Далее можно в ~/.bashrc добавить в PATH путь к бинарникам тулчейна:

export PATH=/opt/ESP/esp-open-sdk/xtensa-lx106-elf/bin:$PATH

Шаг 2 — получаем SDK

Идем на гитхаб (тыньк), читаем инструкции, клонируем (например в /opt/ESP). Далее задаем любимым способом (например через ~/.bashrc) переменную окружения ESP8266_SDK_PATH:

export ESP8266_SDK_PATH=/opt/ESP/esp-open-rtos

Шаг 3 — создаем проект

Заходим в директорию examples в директории с SDK и копируем любой понравившийся пример. Импортируем/открываем проект в любимой среде разработки, мазохисты могут использовать текстовый редактор. Я предпочитаю NetBeans — у него неплохая поддержка C/C++ проектов, в том числе на основе Makefile. Собирается проект с помощью make, прошивается с помощью make flash. В файле local.mk можно настроить параметры для прошивки своего устройства (размер и режим обращения к флеш памяти, например).

Шаг 4 — программируем

Проводим анализ требований, предметной области, составляем ТЗ согласно ГОСТ 34.602-89, после чего можно начинать писать код :) Светодиодами мигать не буду, т. к. их у меня нет, поэтому в качестве HelloWorld будет чтение данных с датчика AM2302 (он же DHT22) и отправка их по протоколу MQTT на сервер.

Для того, чтобы использовать дополнительные модули, например MQTT или DHT, их необходимо добавить в Makefile:

PROGRAM=fffmeteo
EXTRA_COMPONENTS = extras/paho_mqtt_c extras/dht
include $(ESP8266_SDK_PATH)/common.mk

main.h
#ifndef MAIN_H
#define MAIN_H

#include <stdio.h>
#include <stdint.h>
#include <limits.h>

#include <FreeRTOS.h>
#include <task.h>
#include <queue.h>
#include <semphr.h>

#define DEBUG

#ifdef DEBUG
#define debug(args...) printf("--- "); printf(args)
#define SNTP_DEBUG_ENABLED true
#else
#define debug(args...)
#define SNTP_DEBUG_ENABLED false
#endif

#define WIFI_SSID "kosmonaFFFt"
#define WIFI_PASS "mysupermegapassword"

#define MQTT_HOST "m11.cloudmqtt.com"
#define MQTT_PORT 16464
#define MQTT_USER "kosmonaFFFt"
#define MQTT_PASS "mysupermegapassword"
#define MQTT_TOPIC "/meteo"

#define NTP_SERVER "pool.ntp.org"

#define UART0_BAUD 9600

#define STACK_SIZE 512
#define INIT_TASK_PRIORITY (configTIMER_TASK_PRIORITY + 1)
#define MEASUREMENT_TASK_PRIORITY (INIT_TASK_PRIORITY + 1)
#define SENDING_DATA_TASK_PRIORITY (MEASUREMENT_TASK_PRIORITY + 1)

#define MEASUREMENTS_PERIOD_S 59
#define MAX_MEASUREMENTS_COUNT 16

#define SEND_PERIOD_S 120
#define RUN_SNTP_SYNC_PERIOD 5

#define MS(x) (x / portTICK_PERIOD_MS)

#define AM2302_PIN 5

#ifdef __cplusplus
extern "C"
{
#endif

#ifdef __cplusplus
}
#endif

#endif /* MAIN_H */


main.c
#include "main.h"
#include "sntp.h"

#include <esp/uart.h>

#include <espressif/esp_common.h>

#include <paho_mqtt_c/MQTTESP8266.h>
#include <paho_mqtt_c/MQTTClient.h>

#include <dht/dht.h>

//-----------------------------------------------------------------------------+
//                           Measurements task section.                        |
//-----------------------------------------------------------------------------+

struct measurement_results
{
    time_t timestamp;
    int am2302_humidity;
    int am2302_temperature;
};

static QueueHandle_t measurements_queue;

void measurement_task(void *arg)
{
    int16_t humidity;
    int16_t temperature;

    struct measurement_results measurements;

    while (true)
    {
        debug("MEASUREMENTS: Start measurements...\n");
        measurements.timestamp = time(NULL);

        bool success = dht_read_data(DHT_TYPE_DHT22, AM2302_PIN, &humidity, &temperature);
        if (success && temperature >= -500 && temperature <= 1500 && humidity >= 0 && humidity <= 1000)
        {
            measurements.am2302_humidity = humidity;
            measurements.am2302_temperature = temperature;
        }
        else
        {
            debug("MEASUREMENT: Error! Cannot read data from AM2302!!!\n");
            measurements.am2302_humidity = INT_MIN;
            measurements.am2302_temperature = INT_MIN;
        }

        debug("MEASUREMENTS: Measurements finished...\n");

        xQueueSendToBack(measurements_queue, &measurements, MS(250));
        vTaskDelay(MS(MEASUREMENTS_PERIOD_S * 1000));
    }

    vTaskDelete(NULL);
}

//-----------------------------------------------------------------------------+
//                           Sending data task section.                        |
//-----------------------------------------------------------------------------+

static uint8_t mqtt_buf[512];
static uint8_t mqtt_readbuf[128];

void sending_data_task(void *arg)
{
    mqtt_network_t network;
    mqtt_network_new(&network);

    mqtt_client_t client = mqtt_client_default;
    mqtt_packet_connect_data_t data = mqtt_packet_connect_data_initializer;

    uint8_t sntp_sync_counter = 0;
    while (true)
    {
        debug("MQTT: ConnectNetwork...\n");
        int err = mqtt_network_connect(&network, MQTT_HOST, MQTT_PORT);
        if (err)
        {
            debug("MQTT: Error!!! ConnectNetwork ERROR!\n");
            vTaskDelay(MS(5 * 1000));
            continue;
        }
        else
        {
            debug("MQTT: ConnectNetwork success...\n");
        }

        // TODO: add check for errors!!!
        // TODO: replace magic constants!!!
        mqtt_client_new(&client, &network, 5000, mqtt_buf, 100, mqtt_readbuf, 100);

        data.willFlag = 0;
        data.MQTTVersion = 3;
        data.clientID.cstring = "fff";
        data.username.cstring = MQTT_USER;
        data.password.cstring = MQTT_PASS;
        data.keepAliveInterval = 10;
        data.cleansession = 0;

        err = mqtt_connect(&client, &data);
        if (err)
        {
            debug("MQTT: Error!!! MQTTConnect ERROR!\n");
            vTaskDelay(MS(5 * 1000));
            continue;
        }
        else
        {
            debug("MQTT: MQTTConnect success...\n");
        }

        struct measurement_results msg;
        while (xQueueReceive(measurements_queue, &msg, 0) == pdTRUE)
        {
            if (msg.am2302_humidity == INT_MIN || msg.am2302_temperature == INT_MIN)
            {
                debug("MQTT: Got invalid message, no publishing!!!\n");
                continue;
            }

            debug("MQTT: Got message to publish...\n");
            debug("      timestamp: %ld\n", msg.timestamp);
            debug("      am2302_humidity: %.1f\n", msg.am2302_humidity / 10.0);
            debug("      am2302_temperature: %.1f\n", msg.am2302_temperature / 10.0);

            msg.timestamp = htonl(msg.timestamp);
            msg.am2302_humidity = htonl(msg.am2302_humidity);
            msg.am2302_temperature = htonl(msg.am2302_temperature);

            mqtt_message_t message;
            message.payload = &msg;
            message.payloadlen = sizeof (msg);
            message.dup = 0;
            message.qos = MQTT_QOS1;
            message.retained = 0;

            err = mqtt_publish(&client, MQTT_TOPIC, &message);
            if (err)
            {
                debug("MQTT: Error!!! Error while publishing message!\n");
            }
            else
            {
                debug("MQTT: Successfully publish message...\n");
            }
        }

        mqtt_disconnect(&client);
        mqtt_network_disconnect(&network);

        ++sntp_sync_counter;
        if (sntp_sync_counter == RUN_SNTP_SYNC_PERIOD)
        {
            sntp_sync(NTP_SERVER, NULL, arg);
            sntp_sync_counter = 0;
        }

        vTaskDelay(MS(SEND_PERIOD_S * 1000));
    }

    vTaskDelete(NULL);
}

//-----------------------------------------------------------------------------+
//                           Init task section.                                |
//-----------------------------------------------------------------------------+

/**
 * This semaphore is taken during sntp sync and released after it finished.
 */
static SemaphoreHandle_t init_task_sem;

/**
 * Set time and free init task semaphore.
 * @param error unused
 * @param arg unused
 */
void init_sntp_callback(int8_t error, void* arg)
{
    time_t ts = time(NULL);
    debug("TIME: %s", ctime(&ts));
    xSemaphoreGive(init_task_sem);
}

/**
 * Connection parameters.
 */
static struct sdk_station_config STATION_CONFIG = {
    .ssid = WIFI_SSID,
    .password = WIFI_PASS,
};

void init_task(void* arg)
{
    debug("INIT: setting pins...\n");
    gpio_set_pullup(AM2302_PIN, false, false);

    debug("INIT: Set station parameters...\n");
    sdk_wifi_station_set_auto_connect(false);
    sdk_wifi_station_set_config(&STATION_CONFIG);
    debug("Station parameters has been set.\n");

    debug("INIT: Connecting to AP...\n");
    sdk_wifi_station_connect();
    while (sdk_wifi_station_get_connect_status() != STATION_GOT_IP)
    {
        vTaskDelay(MS(1000));
    }
    debug("INIT: Connection to AP has been estabilished...\n");

    debug("INIT: Start SNTP synchronization...\n");

    init_task_sem = xSemaphoreCreateMutex();
    if (!init_task_sem)
    {
        debug("INIT: Cannot create init task semaphore!!!");
        return;
    }

    xSemaphoreTake(init_task_sem, 0);

    sntp_init();
    sntp_sync(NTP_SERVER, init_sntp_callback, arg);

    BaseType_t result = pdFALSE;
    while (true)
    {
        debug("INIT: Trying to take init task semaphore...\n");
        result = xSemaphoreTake(init_task_sem, MS(5 * 1000));
        if (result == pdTRUE)
        {
            debug("INIT: Init task semaphore is taken...\n");
            break;
        }
    }

    measurements_queue = xQueueCreate(MAX_MEASUREMENTS_COUNT, sizeof (struct measurement_results));
    if (!measurements_queue)
    {
        debug("INIT: ERROR!!! Cannot create queue for measurements!\n");
        goto fail;
    }

    result = xTaskCreate(measurement_task, "measurement_task", STACK_SIZE, NULL, MEASUREMENT_TASK_PRIORITY, NULL);
    if (result == pdFAIL)
    {
        debug("INIT: Measurement task creation failed!!!\n");
        goto fail;
    }
    debug("INIT: Measurement task created...\n");

    result = xTaskCreate(sending_data_task, "send_data_task", STACK_SIZE, NULL, SENDING_DATA_TASK_PRIORITY, NULL);
    if (result == pdFAIL)
    {
        debug("INIT: Send task creation failed!!!\n");
        goto fail;
    }
    debug("INIT: Send data task created...\n");

fail:
    vSemaphoreDelete(init_task_sem);
    vTaskDelete(NULL);
}

//-----------------------------------------------------------------------------+
//                           Application entry point.                          |
//-----------------------------------------------------------------------------+

void user_init(void)
{
    debug("USER_INIT: SDK version: %s\n", sdk_system_get_sdk_version());
    debug("USER_INIT: sizeof (int): %d\n", sizeof (int));
    debug("USER_INIT: sizeof (float): %d\n", sizeof (float));
    debug("USER_INIT: sizeof (time_t): %d\n", sizeof (time_t));
    uart_set_baud(0, UART0_BAUD);

    BaseType_t result = xTaskCreate(init_task, (const char * const) "init_task", STACK_SIZE, NULL, INIT_TASK_PRIORITY, NULL);
    if (!result)
    {
        debug("USER_INIT: Cannot create init task!!!");
        return;
    }
}



sntp.h
#ifndef SNTP_H
#define SNTP_H

#include <time.h>
#include <stdint.h>

#ifdef __cplusplus
extern "C"
{
#endif

#define SNTP_ERR_OK 0
#define SNTP_ERR_CONTEXT -1
#define SNTP_ERR_DNS -2
#define SNTP_ERR_UDP_PCB_ALLOC -3
#define SNTP_ERR_PBUF_ALLOC -4
#define SNTP_ERR_SEND -5
#define SNTP_ERR_RECV_ADDR_PORT -6;
#define SNTP_ERR_RECV_SIZE -7
#define SNTP_ERR_RECV_MODE -8
#define SNTP_ERR_RECV_STRATUM -9

typedef void (*sntp_sync_callback)(int8_t error, void *arg);

void sntp_init();
void sntp_sync(char *server, sntp_sync_callback callback, void *callback_arg);

time_t sntp_get_rtc_time(int32_t *us);
void sntp_update_rtc(time_t t, uint32_t us);

#ifdef __cplusplus
}
#endif

#endif /* SNTP_H */


sntp.c
#include "main.h"
#include "sntp.h"

#include <time.h>
#include <string.h>

#include <lwip/ip_addr.h>
#include <lwip/err.h>
#include <lwip/dns.h>
#include <lwip/udp.h>

#include <esp/rtc_regs.h>

#include <espressif/esp_common.h>

#define TIMER_COUNT RTC.COUNTER

/**
 * Daylight settings.
 * Base calculated with value obtained from NTP server (64 bits).
 */
#define SNTP_BASE (*((uint64_t*) RTC.SCRATCH))

/**
 * Timer value when base was obtained.
 */
#define SNTP_TIME_REF (RTC.SCRATCH[2])

/**
 * Calibration value.
 */
#define SNTP_CALIBRATION (RTC.SCRATCH[3])

/**
 * SNTP modes.
 */
#define SNTP_MODE_CLIENT 0x03
#define SNTP_MODE_SERVER 0x04
#define SNTP_MODE_BROADCAST 0x05

/**
 * Kiss-of-death code.
 */
#define SNTP_STRATUM_KOD 0x00

#define SNTP_OFFSET_LI_VN_MODE 0
#define SNTP_OFFSET_STRATUM 1
#define SNTP_OFFSET_RECEIVE_TIME 32

#define DIFF_SEC_1900_1970 (2208988800UL)

struct sntp_message
{
    uint8_t li_vn_mode;
    uint8_t stratum;
    uint8_t poll;
    uint8_t precision;
    uint32_t root_delay;
    uint32_t root_dispersion;
    uint32_t reference_identifier;
    uint32_t reference_timestamp[2];
    uint32_t originate_timestamp[2];
    uint32_t receive_timestamp[2];
    uint32_t transmit_timestamp[2];
} __attribute__ ((packed));

struct sntp_sync_context
{
    ip_addr_t ip_address;
    sntp_sync_callback callback;
    void* callback_arg;
};

void sntp_init()
{
    SNTP_BASE = 0;
    SNTP_CALIBRATION = 1;
    SNTP_TIME_REF = TIMER_COUNT;
}

void on_dns_found(const char* name, ip_addr_t* ipaddr, void* arg);
void on_udp_recv(void* arg, struct udp_pcb* pcb, struct pbuf* p, ip_addr_t* addr, u16_t port);

void sntp_sync(char* server, sntp_sync_callback callback, void* callback_arg)
{
    int result = ERR_OK;

    debug("SNTP: Start SNTP synchronization, allocating memory for context...\n");
    struct sntp_sync_context* context = malloc(sizeof (struct sntp_sync_context));
    if (!context)
    {
        debug("SNTP: Error!!! Cannot allocate memory for context!\n");
        result = SNTP_ERR_CONTEXT;
        goto fail;
    }
    context->callback = callback;
    context->callback_arg = callback_arg;
    debug("SNTP: Context successfully allocated...\n");

    debug("SNTP: Start DNS lookup...\n");
    err_t err = dns_gethostbyname(server, &(context->ip_address), on_dns_found, context);
    if (!(err == ERR_OK || err == ERR_INPROGRESS))
    {
        debug("SNTP: Error!!! DNS lookup error!\n");
        result = SNTP_ERR_DNS;
        goto fail;
    }
    return;

fail:
    if (context)
    {
        free(context);
    }

    if (callback)
    {
        callback(result, callback_arg);
    }
}

//
//==============================================================================================================================================================
//

void on_dns_found(const char* name, ip_addr_t* ipaddr, void* arg)
{
    debug("SNTP: Start DNS lookup successfully finished...\n");

    int result = ERR_OK;
    struct sntp_sync_context* context = arg;
    sntp_sync_callback callback = context->callback;
    void* callback_arg = context->callback_arg;

    debug("SNTP: Creating upd_pcb...\n");
    struct udp_pcb* sntp_pcb = udp_new();
    if (!sntp_pcb)
    {
        debug("SNTP: Error!!! Cannot allocate udp_pcb!\n");
        result = SNTP_ERR_UDP_PCB_ALLOC;
        goto fail;
    }
    debug("SNTP: Successfully created upd_pcb...\n");

    debug("SNTP: Allocating pbuf...\n");
    struct pbuf* p = pbuf_alloc(PBUF_TRANSPORT, sizeof (struct sntp_message), PBUF_RAM);
    if (!p)
    {
        debug("SNTP: Error!!! DNS lookup error!\n");
        result = SNTP_ERR_PBUF_ALLOC;
        goto fail;
    }
    struct sntp_message* message = p->payload;
    memset(message, 0, sizeof (struct sntp_message));
    message->li_vn_mode = 0b00100011; // li = 00, vn = 4, mode = 3
    debug("SNTP: Pbuf allocated successfully...\n");

    debug("SNTP: Sending data to server...\n");
    udp_recv(sntp_pcb, on_udp_recv, context);
    err_t err = udp_sendto(sntp_pcb, p, ipaddr, 123);
    pbuf_free(p);
    if (err != ERR_OK)
    {
        debug("SNTP: Error!!! data sending error!\n");
        result = SNTP_ERR_SEND;
        goto fail;
    }
    debug("SNTP: Data sent...\n");
    return;

fail:
    if (context)
    {
        free(context);
    }

    if (sntp_pcb)
    {
        udp_remove(sntp_pcb);
    }

    if (callback)
    {
        callback(result, callback_arg);
    }
}

void on_udp_recv(void* arg, struct udp_pcb* pcb, struct pbuf* p, ip_addr_t* addr, u16_t port)
{
    debug("SNTP: Response has successfully received...\n");

    int result = ERR_OK;
    struct sntp_sync_context* context = arg;
    sntp_sync_callback callback = context->callback;
    void* callback_arg = context->callback_arg;

    debug("SNTP: Checking response size...\n");
    if (p->tot_len < sizeof (struct sntp_message))
    {
        debug("SNTP: Error!!! Invalid response size!\n");
        result = SNTP_ERR_RECV_SIZE;
        goto fail;
    }
    debug("SNTP: Response size is OK...\n");

    debug("SNTP: Checking mode...\n");
    u8_t mode = 0x0;
    pbuf_copy_partial(p, &mode, sizeof (mode), SNTP_OFFSET_LI_VN_MODE);
    mode &= 0b00000111;
    if (mode != SNTP_MODE_SERVER && mode != SNTP_MODE_BROADCAST)
    {
        debug("SNTP: Error!!! Invalid mode!\n");
        result = SNTP_ERR_RECV_MODE;
        goto fail;
    }
    debug("SNTP: Mode is OK...\n");

    debug("SNTP: Checking stratum...\n");
    u8_t stratum = 0x0;
    pbuf_copy_partial(p, &stratum, sizeof (stratum), SNTP_OFFSET_STRATUM);
    if (stratum == SNTP_STRATUM_KOD)
    {
        debug("SNTP: Error!!! Kiss of death!\n");
        result = SNTP_ERR_RECV_STRATUM;
        goto fail;
    }
    debug("SNTP: Stratum is OK...\n");

    debug("SNTP: Updating system timer...\n");
    uint32_t receive_time[2];
    pbuf_copy_partial(p, &receive_time, 2 * sizeof (uint32_t), SNTP_OFFSET_RECEIVE_TIME);
    time_t t = ntohl(receive_time[0]) - DIFF_SEC_1900_1970;
    uint32_t us = ntohl(receive_time[1]) / 4295;
    sntp_update_rtc(t, us);
    debug("SNTP: System timer updated...\n");

fail:
    if (context)
    {
        free(context);
    }

    if (pcb)
    {
        udp_remove(pcb);
    }

    if (callback)
    {
        callback(result, callback_arg);
    }
}

/**
 * Check if a timer wrap has occurred. Compensate sntp_base reference
 * if affirmative.
 * TODO: think about multitasking and race conditions.
 */
inline void sntp_check_timer_wrap(uint32_t current_value)
{
    if (current_value < SNTP_TIME_REF)
    {
        // Timer wrap has occurred, compensate by subtracting 2^32 to ref.
        SNTP_BASE -= 1LLU << 32;
    }
}

/**
 * Return secs. If us is not a null pointer, fill it with usecs
 */
time_t sntp_get_rtc_time(int32_t *us)
{
    time_t secs;
    uint32_t tim;
    uint64_t base;

    tim = TIMER_COUNT;
    // Check for timer wrap.
    sntp_check_timer_wrap(tim);
    base = SNTP_BASE + tim - SNTP_TIME_REF;
    secs = base * SNTP_CALIBRATION / (1000000U << 12);
    if (us)
    {
        *us = base * SNTP_CALIBRATION % (1000000U << 12);
    }
    return secs;
}

/**
 * Update RTC timer. Called by SNTP module each time it receives an update.
 */
void sntp_update_rtc(time_t t, uint32_t us)
{
    // Apply daylight and timezone correction
    // DEBUG: Compute and print drift
    int64_t sntp_current = SNTP_BASE + TIMER_COUNT - SNTP_TIME_REF;
    int64_t sntp_correct = (((uint64_t) us + (uint64_t) t * 1000000U) << 12) / SNTP_CALIBRATION;
    debug("RTC Adjust: drift = %ld ticks, cal = %d\n", (time_t) (sntp_correct - sntp_current), SNTP_CALIBRATION);

    SNTP_TIME_REF = TIMER_COUNT;
    SNTP_CALIBRATION = sdk_system_rtc_clock_cali_proc();
    SNTP_BASE = (((uint64_t) us + (uint64_t) t * 1000000U) << 12) / SNTP_CALIBRATION;
}

/**
 * Syscall implementation. doesn't seem to use tzp.
 */
int _gettimeofday_r(struct _reent* r, struct timeval* tp, void* tzp)
{
    // Syscall defined by xtensa newlib defines tzp as void*
    // So it looks like it is not used. Also check tp is not NULL
    if (tzp || !tp)
    {
        return EINVAL;
    }

    tp->tv_sec = sntp_get_rtc_time((int32_t*) & tp->tv_usec);
    return 0;
}


Лирическое отступление по поводу наличия своего кода синхронизации времени по SNTP: в extensions из SDK уже есть такой модуль, но мне он почему-то не понравился (давно было, уже не помню почему), поэтому я тот код нагло скопипастил и доработал под себя.

Работает все просто: при старте контроллера запускается задача инициализации, которая подключается к точке доступа, синхронизирует время по SNTP, запускает задачи измерения температуры с влажностью и отправки данных на сервер, после чего самоубивается. Задачка измерения опрашивает датчик раз в 59 секунд и складывает результаты в очередь, задача отправки запускается раз в 2 минуты, читает данные из очереди и отправляет на MQTT сервер.

Теоретически, можно писать и на C++.

Шаг 5 — заключение, куда же без него

Таким вот нехитрым образом, с помощью языка C и рук с небольшим радиусом кривизны можно запрограммировать контроллер ESP8266. Основное преимущество данного подхода перед скриптовыми решениями (например LUA или MicroPython) в полном контроле над составом и ресурсами прошивки, и возможность впихнуть больше функциональности при ограниченных ресурсах контроллера. Так же есть вариант использования RTOS SDK или NONOS SDK от Espressif, но с первым у меня не срослось, а второй не пробовал использовать. Если кому-то будет интересно, а так же когда сам разберусь, могу написать следующий туториал про OTA (обновление прошивки по воздуху).

Немного результатов работы данного кода:

Данные, полученные с сервера MQTT, и залитые в БД


Отладочный выхлоп контроллера в UART
SDK version: 0.9.9                                                                                                               
--- USER_INIT: sizeof (int): 4                                                                                                                                                               
--- USER_INIT: sizeof (float): 4                                                                                                                                                             
--- USER_INIT: sizeof (time_t): 4                                                                                                                                                            
mode : sta(18:fe:34:d2:c5:a7)                                                                                                                                                                
add if0                                                                                                                                                                                      
--- INIT: setting pins...                                                                                                                                                                    
--- INIT: Set station parameters...                                                                                                                                                          
--- Station parameters has been set.                                                                                                                                                         
--- INIT: Connecting to AP...                                                                                                                                                                
scandone                                                                                                                                                                                     
add 0                                                                                                                                                                                        
aid 2                                                                                                                                                                                        
cnt                                                                                                                                                                                          

connected with kosmonaFFFt, channel 1                                                                                                                                                        
dhcp client start...                                                                                                                                                                         
ip:192.168.1.21,mask:255.255.255.0,gw:192.168.1.1                                                                                                                                            
--- INIT: Connection to AP has been estabilished...                                                                                                                                          
--- INIT: Start SNTP synchronization...                                                                                                                                                      
--- SNTP: Start SNTP synchronization, allocating memory for context...                                                                                                                       
--- SNTP: Context successfully allocated...                                                                                                                                                  
--- SNTP: Start DNS lookup...                                                                                                                                                                
--- INIT: Trying to take init task semaphore...                                                                                                                                              
--- SNTP: Start DNS lookup successfully finished...                                                                                                                                          
--- SNTP: Creating upd_pcb...                                                                                                                                                                
--- SNTP: Successfully created upd_pcb...                                                                                                                                                    
--- SNTP: Allocating pbuf...                                                                                                                                                                 
--- SNTP: Pbuf allocated successfully...                                                                                                                                                     
--- SNTP: Sending data to server...                                                                                                                                                          
--- SNTP: Data sent...                                                                                                                                                                       
--- SNTP: Response has successfully received...                                                                                                                                              
--- SNTP: Checking response size...                                                                                                                                                          
--- SNTP: Response size is OK...                                                                                                                                                             
--- SNTP: Checking mode...                                                                                                                                                                   
--- SNTP: Mode is OK...                                                                                                                                                                      
--- SNTP: Checking stratum...                                                                                                                                                                
--- SNTP: Stratum is OK...                                                                                                                                                                   
--- SNTP: Updating system timer...                                                                                                                                                           
--- RTC Adjust: drift = 1220897578 ticks, cal = 1                                                                                                                                            
--- SNTP: System timer updated...                                                                                                                                                            
--- TIME: Thu Sep 21 19:20:36 2017                                                                                                                                                           
--- INIT: Init task semaphore is taken...                                                                                                                                                    
--- MEASUREMENTS: Start measurements...                                                                                                                                                      
--- MEASUREMENTS: Measurements finished...                                                                                                                                                   
--- INIT: Measurement task created...                                                                                                                                                        
--- MQTT: ConnectNetwork...                                                                                                                                                                  
--- INIT: Send data task created...                                                                                                                                                          
--- MQTT: ConnectNetwork success...                                                                                                                                                          
--- MQTT: MQTTConnect success...                                                                                                                                                             
--- MQTT: Got message to publish...                                                                                                                                                          
---       timestamp: 1506021636                                                                                                                                                              
---       am2302_humidity: 55.8                                                                                                                                                              
---       am2302_temperature: 23.4                                                                                                                                                           
--- MQTT: Successfully publish message...                                                                                                                                                    
--- MEASUREMENTS: Start measurements...                                                                                                                                                      
--- MEASUREMENTS: Measurements finished...                                                                                                                                                   
--- MEASUREMENTS: Start measurements...                                                                                                                                                      
--- MEASUREMENTS: Measurements finished...                                                                                                                                                   
--- MQTT: ConnectNetwork...                                                                                                                                                                  
--- MQTT: ConnectNetwork success...                                                                                                                                                          
--- MQTT: MQTTConnect success...
--- MQTT: Got message to publish...
---       timestamp: 1506021694
---       am2302_humidity: 55.2
---       am2302_temperature: 23.8
--- MQTT: Successfully publish message...
--- MQTT: Got message to publish...
---       timestamp: 1506021751
---       am2302_humidity: 56.5
---       am2302_temperature: 24.4
--- MQTT: Successfully publish message...
--- MEASUREMENTS: Start measurements...
--- MEASUREMENTS: Measurements finished...
--- MEASUREMENTS: Start measurements...
--- MEASUREMENTS: Measurements finished...
--- MQTT: ConnectNetwork...
--- MQTT: ConnectNetwork success...
--- MQTT: MQTTConnect success...
--- MQTT: Got message to publish...
---       timestamp: 1506021807
---       am2302_humidity: 53.0
---       am2302_temperature: 24.7
--- MQTT: Successfully publish message...
--- MQTT: Got message to publish...
---       timestamp: 1506021863
---       am2302_humidity: 52.3
---       am2302_temperature: 24.8
--- MQTT: Successfully publish message...
--- MEASUREMENTS: Start measurements...
--- MEASUREMENTS: Measurements finished...
--- MEASUREMENTS: Start measurements...
--- MEASUREMENTS: Measurements finished...
--- MQTT: ConnectNetwork...
--- MQTT: ConnectNetwork success...
--- MQTT: MQTTConnect success...
--- MQTT: Got message to publish...
---       timestamp: 1506021919
---       am2302_humidity: 52.0
---       am2302_temperature: 24.9
--- MQTT: Successfully publish message...
--- MQTT: Got message to publish...
---       timestamp: 1506021975
---       am2302_humidity: 53.3
---       am2302_temperature: 25.2
--- MQTT: Successfully publish message...


P.S. Для работы с UART на PC рекомендую использовать minicom (консоль), или cutecom (GUI).

Полезные ссылки:

Комментарии (42)


  1. Ksiw
    21.09.2017 23:41
    -4

    Зачем вообще на микроконтроллере использовать ОС и тратить на нее ограниченные ресурсы, если можно написать компактный и эффективный код под конкретную задачу?


    1. kosmonaFFFt Автор
      21.09.2017 23:50

      Лично для меня, как для Java программиста, контроллер без OC слишком низкоуровнево и, пока что, геморройно. А так получается, использование FreeRTOS в данном конкретном случае ESP8266 — золотая середина между низкоуровневым программированием без ОС и скриптовыми языками типа LUA. Хотя доводилось в университете программировать 8-битные PIC на ассемблере, и там для ОС точно места нет.


    1. WerewolfPrankster
      22.09.2017 00:28
      +4

      Так там ведь от той ОС планировщик, очередь да таймер по хорошему. Куда уж компактнее


    1. Alex_ME
      22.09.2017 00:29
      +4

      Начиная с определенной сложности задачи, использование легковесных ОС вроде FreeRTOS оправдано и активно применяется в Embedded разработке. И даже на более слабых контроллерах, а ESP8266 обладает довольно внушительными ресурсами (существенно превышает "народные" ATMega или STM32F1xx). Правда с памятью там проблемы были, вроде как.


      А задача работы WiFi и IP стеком уже весьма серьезная. Так аналог ESP8266 — RTL87xx/RTL95xx (и прочие модули серии) имеют в SDK FreeRTOS по-умолчанию и никак иначе.


    1. Ksiw
      22.09.2017 01:44
      +2

      Шибко грамотные специалисты переполнившись ядом негодования, кинулись минусить безобидный вопрос менее опытного.
      Должен пояснить, что в своем проекте использую СТМку, которая снимает показания амперметров с трёх фаз, меряет обороты двигателя, щёлкает релюшкой и отправляет показания/принимает команды по mqtt, и чудесно со всем этим справляется без дополнительной прослойки в виде ОС.

      Спасибо ответившим.


      1. iig
        22.09.2017 11:09

        А вас точно интересует ответ? ;)


        1. Ksiw
          23.09.2017 00:26

          Вне всяких сомнений.


      1. AndyKorg
        22.09.2017 12:14

        Выложите код на GitHub и можно будет более предметно обсудить преимущества и недостатки ОС в embeded. Сам придерживаюсь подхода — каждой задаче свой инструмент.


        1. kosmonaFFFt Автор
          22.09.2017 12:21

          Для своих проектов я использую Mercurial, так что до гитхаба код вряд ли дойдет. Но подумаю о том, чтобы открыть репозиторий на BitBucket.


      1. avf1906
        22.09.2017 12:15

        даже lwip можно запустить без ОС, но зачем? на самом деле ос — сильно облегчает многозадачность и вопрос стоит по другому — зачем изобретать велосипед, если все это решается ОС. ЗЫ: сейчас делаю проект без ос на STM8S003, потому что реально нет места — используется 7733 байта из 8192 на максимальном уровне оптимизации, и надо будет еще добавлять функционал :(, но если ресурсы позволяют — ось рулит.


      1. Solexid
        22.09.2017 12:31

        То что вы описали про свой проект, спокойно умешается в тысяче строк и паре циклов. Есстественно там не нужна ось. А вот когда устройство например имеет свой графический интерфейс (ессно самописный), то без банального планировщика что то многозадачное писать муторно. Хотя я все таки предпочитаю использовать свой планировщик в стиле ентить в HLSDK.


      1. Falstaff
        22.09.2017 13:16

        Просто вы сформулировали вопрос как утверждение, отсюда и минусы. Отвечая на него — вы, конечно, можете обойтись без ОС. Но в какой-то момент вам понадобится, например, делать длинную обработку своих показаний и, одновременно, скажем, данных с датчика вибрации. Вы решаете, что это не проблема, и пишете в своём главном цикле оркестровку обработок данных из прерываний — мальчики амперметры налево, девочки вибрация направо. Это вносит зависимость между (например) концептуально независимыми потоками данных. Потом вы обнаруживаете, что у вас есть другой датчик, данные от которого надо обработать срочно, сразу по пришествию, но обработка слишком длинная для прерывания, а пускать их в главный цикл — не получается, цикл то и дело занят обработкой амперметра, и надо её как-то прерывать и заниматься срочными данными. Потом вы решаете использовать DMA для записи на SD-карту, у вас появляется ещё один асинхронный процесс (начали пересылку, теперь можно что-то другое делать пока DMA-контроллер не просигналит, что пересылка кончилась), и вы начинаете городить конечный автомат, что само по себе не плохо, но ваш код начинает размазываться по разным состояниям. И в какой-то момент вы приходите к осознанию того, что вы написали собственную ОС. :)

        В общем, разным задачам — разные решения. На определённых задачах и ОС вам понадобится.


        1. Ksiw
          22.09.2017 17:54

          Спасибо за глубокий ответ. Действительно, примерно что-то такое и началось, когда в довесок ко всему решил прикрутить вывод на экран 5010.
          Вы лучше некуда разъяснили ситуацию.
          Сколь много ресурсов железяки отнимает freertos(прошивка/озу)?
          Жаль, не могу истово плюсануть.


          1. Falstaff
            22.09.2017 23:28

            Не за что. :) По поводу ресурсов — на практике всё различается, но чтобы просто представить себе порядок цифр, можно поглядеть на их официальный FAQ по потреблению памяти. Если вкратце, то:


            • 5-10 Кб флэша в минимальной конфигурации на STR71x с полной оптимизацией и четырьмя приоритетами задач. На других машинках может быть и меньше; если добавлять фич, то больше — по опыту, разбухает не очень сильно.
            • Оперативной памяти, на той же машинке:
              • 236 байт на само ядро.
              • 76 байт на каждую очередь, плюс размер очереди (сколько данных хочется там хранить, сами выбираете).
              • 64 байта на каждую задачу (при максимальной длине имени задачи 4 символа), плюс сколько отведёте на стек задачи. Задач из коробки будет одна или две (idle task и, если сконфигурируете, timer task).

            По опыту — больше всего оперативной займут стеки задач, если задач много и им приходится давать много стека (например, printf() много стека ест при вызове) то расходов действительно будет порядком. Но всё в ваших руках, если иерархия вызовов не очень глубокая и ничего жадного до стека не используется (вызовы вроде printf(), большие локальные структуры или массивы), то стеки могут быть маленькими. Для задач вроде интерфейса (лампочки, пищалки и т.п.) можно использовать корутины. Они ограничены простыми задачами, но собственного стека у них нет (все корутины бегут на выбранной вами задаче, обычно idle task, и используют её стек), поэтому они совсем легковесные.


            1. Mogwaika
              23.09.2017 14:43

              А на быстродействие как влияет?


              1. Falstaff
                24.09.2017 00:43

                Вопрос с влиянием на быстродействие не очень однозначный. RTOS, в отличие от привычных крупных настольных ОС, в принципе не обязана чем-то шуршать в отсутствие внешних раздражителей, затрачивать процессорное время на поддержание себя самой. Большая часть накладных расходов будет приходиться на переключение контекста. Если упрощённо, то переключение контекста может произойти в таких случаях:


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

                В том же официальном FAQ по ссылке выше есть пример для определённых условий (Keil, Cortex-M3, без stack overflow checking, без статистики, оптимизация на скорость). При этом на переключение контекста уходит 84 такта (на самом деле — на работу планировщика, даже если он решил не переключать контекст). Можно (очень грубо) прикинуть сценарии и сколько при этом будет накладных расходов. Например:


                • Машинка работает на тактовой 16 МГц, частота системного таймера 100 Гц (гранулярность 10 мс). Если всё в покое, прерывания не вызываются, ничего кроме системного таймера не бежит, то планировщик вызывается 100 раз в секунду, на сумму в 8400 тактов. Это примерно 0.05% процессорного времени. Если частота системы ниже, то накладные расходы в сравнении больше — при 1 МГц будет уже 0.84%.
                • То же самое, плюс каждые 5 мс вызывается прерывание контроллера DMA, оно посылает блок данных задаче-обработчику. Каждый посланный блок вызывает два переключения контекста (прерывание вызывает переключение из idle task в обработчик, обработчик закругляется с данными и засыпает в ожидании новых — контект переключается обратно в idle task). Теперь в секунду происходит 500 переключений контекста, 42000 тактов накладных расходов. Это уже 0.26% процессорного времени на накладные расходы (на 1 МГц — 4.2%).

                Это очень примерно, я мог что-то не учесть (если что, надеюсь, меня поправят), на других машинах/компиляторах/настройках тоже будет различаться, так что это для того, чтобы общее ощущение создалось. Пример тоже слегка экстремальный, обычно такой яростной активности нет, к тому же есть много способов не переключаться так часто (например, если вы можете в прерывании буферизировать данные и посылать в задачу кусками побольше). Режим tickless idle позволит сэкономить на системном таймере при необходимости, чтобы в отсутствие активности накладных расходов совсем не было. В общем, примерно такая картина.


    1. Vladislav_Dudnikov
      22.09.2017 14:44
      +1

      FreeRTOS — это система реального времени. Без систем реального времени многие задачи будет почти невозможно выполнять.
      Простой пример в виде задачи. Есть контроллер, которые меряет n каналов АЦП и обрабатывает их (скажем 10 мс на одну выборку). Есть RS485 по которому нужно передавать обработанные данные с минимальной задержкой (максимальное время ответа — 1 мс).
      Такую задачу решить с RTOSом — раз плюнуть.

      Тем более суффикс OS не говорит о том, что там сверхсложная система, которая требует кучу памяти, ресурсов и т.п. FreeRTOS, например, требует мало ресурсов.


  1. iig
    22.09.2017 11:15
    +1

    Под ESP8266 можно писать на C(++), сомнений нет. Какая роль в этом проекте FreeRTOS — не понял :(


    1. kosmonaFFFt Автор
      22.09.2017 12:20

      Когда я выбирал на чем писать, выбор был из 3-х вариантов:
      — LUA/MicroPython с прошивкой NodeMCU
      — ESP без OS
      — ESP с FreeRTOS
      т. к. проект just for fun и для самообучения, я выбрал вариант 3, в котором помимо самой ESP я познакомлюсь еще и с FreeRTOS…


      1. iig
        22.09.2017 12:39

        У каждого свои критерии выбора, не о том вопрос.
        Например, программа для ESP в Arduino-style выгдядит так:


        void setup ()
        {
        }
        void main()
        {
            while (1) {
            do_something(); 
            yield(); // выполнять код, который остался "под капотом". Сеть, таймеры..
           }
        }

        Стоит в главном цикле запустить "тяжелый" код — можно получить побочные эффекты (обрывы сети как минимум).
        RTOS как-то позволяет обходить это ограничение?


        Если ошибаюсь — поправьте.


        1. kosmonaFFFt Автор
          22.09.2017 13:23

          В FreeRTOS, насколько я знаю, нет определяемого программистом «главного цикла». Просто несколько задач с разными, или не очень, приоритетами, которые управляются планировщиком, +прерывания.


          1. Barnaby
            22.09.2017 17:50

            Вопрос в том что будет если код выполняется очень долго, в Arduino для этого надо расставлять yield (или delay), иначе отвалится вайфай а потом придет wdt reset.


            1. ixamilion
              25.09.2017 11:28

              FreeRTOS это система вытесняющего типа, т.е. переключение происходит не по прямой команде пользователя, а планировщиком. Проверка необходимости переключения происходит каждый системный такт (1мс по умолчанию). Если в такой такт обнаруживается задача готовая к выполнению с приоритетом не ниже текущей, то происходит переключение. Про ваш пример с wdt — оптимально сделать его задачей высшего приоритета, которая будет в постоянном ожидании семафора на выполнение. Получив такой, обнулять таймер и снова в ожидание. А семафоры раздавать в остальных задачах, что будет свидетельствовать об отсутствии зацикленности там где ее не ждете.


              1. Falstaff
                25.09.2017 13:06
                +1

                Не ради спора, а просто уточнения ради, хочу дополнить что у FreeRTOS, во-первых, режим вытесняющей многозадачности конфигурируется (настройка configUSE_PREEMTION во FreeRTOSConfig.h), а во-вторых у пользователя есть возможность руками переключить контекст (вызвав taskYIELD(), хотя при включённой вытесняющей многозадачности это по идее никогда не понадобится). Ну и, конечно, проверка необходимости переключения просходит в системных вызовах — когда вы отдаёте семафор, пишете в очередь, ставите флаг и т.п., так что если вы запишете в очередь в прерывании, то вернуться вы можете уже в освободившуюся задачу, а не в ту, которую это прерывание прервало. Это просто чтобы не создалось впечатление, что переключение происходит только на тактах.


  1. de1m
    22.09.2017 13:42

    я для своего проекта использовал esp-open-sdk, это было NOSDK версия, но там был хорошо спрятанный баг, который вроде бы решили.


  1. jonic
    22.09.2017 17:24

    Три esp8266 трудятся в так называемом продакшене с freertos у разных заказчиков. Удобно, не напряжно, разделил на модули, модули в таски и все. Правильно автор что freertos заюзал. А кто говорит что без ОС лучше, ну не знаю, может Вы не разобрались… Не те задачи и не те ресурсы что бы спички считать.


    1. Mogwaika
      23.09.2017 14:47

      Подскажите, что почитать с примерами, чтобы разобраться новичку.


      1. kosmonaFFFt Автор
        23.09.2017 20:20

        Смотря в чем разобраться — если конкретно с ESP8266 — в SDK есть много разных примеров…


        1. Mogwaika
          23.09.2017 20:44

          Для начала с чего-нибудь попроще, типа Maple mini. Примеры в том числе с работой с самой SDK.


          1. kosmonaFFFt Автор
            23.09.2017 20:50

            Я пока только с ESP знаком, сейчас вот ESP32 палочкой с разных сторон тыкаю. С другими контроллерами не разбирался, и посоветовать не могу ничего.


            1. Mogwaika
              23.09.2017 21:58

              jonic писал, что «возможно не разобрались с RTOS», я для своих задач плюсов не вижу, но разобраться хочу, поэтому и спросил. Т.е. практически безотносительно железа.
              Если тыкнете носом в пошаговое руководство по ESPшной SDK буду тоже благодарен.


              1. kosmonaFFFt Автор
                25.09.2017 00:10

                Не знаю даже, есть ли пошаговые руководства вообще. Я просто открывал официальную документацию по SDK, описание API FreeRTOS, lwip и разбирался. Начинал с попыток завести официальный SDK, прошить свой хеллоуворлд. После неудачи с этими инструментами нагуглил ESP Open RTOS, и с ним завелось. На том и остановился.


                1. Mogwaika
                  25.09.2017 01:26
                  +1

                  Вот побольше бы таких пошаговых писали, а то большинство документации или примеров напоминают мем «как нарисовать сову». У меня хоть и коряво получается писать, но на другом ресурсе пару примеров hello world со скриншотами попытался описать по теме в которой сам немножко разобрался.


  1. Alex_Sa
    23.09.2017 00:01

    Спасибо за интересную статью! А MQTT сервер какой использовался?


    1. kosmonaFFFt Автор
      23.09.2017 02:12

      Пока разворачиваю свой, тестирую на www.cloudmqtt.com. Там есть бесплатный план с кучей ограничений, но для тестов хватает.


      1. Mogwaika
        23.09.2017 14:49

        А данные делите на два потока для чётных и нечётных минут? Зачем так сделано?


        1. kosmonaFFFt Автор
          23.09.2017 20:19

          Один поток делает замеры, второй отправляет результаты… Дальше буду подбирать интервал отправки (и добавлю глубокий сон) так, чтобы меньше тратилось энергии…


          1. Mogwaika
            23.09.2017 20:43

            Я к тому, что у вас сбор данных раз в минуту, а отправка раз в две минуты как я понял. Почему нельзя было отправлять сразу после измерения? А так принятые пары значений потом придётся специально «разносить» по времени, т.к. все стандартные сервисы берут время приёма mqtt сообщения как я понимаю.


            1. kosmonaFFFt Автор
              23.09.2017 20:48

              Время замера сохраняется на самой ESP, которая, в свою очередь, синхронизируется по SNTP… Так что отправлять можно и раз в 30 минут, время каждого замера не потеряется…


              1. Mogwaika
                23.09.2017 21:54

                Я понимаю, что не потеряется, но отправляете то вы их через mqtt, соответственно нужно отправлять пару значение-время и потом руками разбирать. Стандартные системы умных домов по-моему такого не поддерживают (я просто из текста не очень понял куда и чем вы эти данные принимаете и складываете).


                1. kosmonaFFFt Автор
                  24.09.2017 23:57

                  Принимаю через MQTT самописным сервером на Java. Насчет стандатрных систем умных домов я не в курсе, даже не исследовал, что вообще в этой области есть.


  1. Alexander_vrn
    25.09.2017 00:28
    +2

    Таким вот нехитрым образом, с помощью языка C и рук с небольшим радиусом кривизны
    Наверное, подразумевался все же большой радиус кривизны :)