Немного о программировании ESP8266 на C под FreeRTOS

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

Замотивировавшись ответом от 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).

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

Поделиться публикацией
Никаких подозрительных скриптов, только релевантные баннеры. Не релевантные? Пиши на: adv@tmtm.ru с темой «Полундра»

Зачем оно вам?
Реклама
Комментарии 46
  • –4
    Зачем вообще на микроконтроллере использовать ОС и тратить на нее ограниченные ресурсы, если можно написать компактный и эффективный код под конкретную задачу?
    • 0
      Лично для меня, как для Java программиста, контроллер без OC слишком низкоуровнево и, пока что, геморройно. А так получается, использование FreeRTOS в данном конкретном случае ESP8266 — золотая середина между низкоуровневым программированием без ОС и скриптовыми языками типа LUA. Хотя доводилось в университете программировать 8-битные PIC на ассемблере, и там для ОС точно места нет.
      • +4
        Так там ведь от той ОС планировщик, очередь да таймер по хорошему. Куда уж компактнее
        • +4

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


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

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

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

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

              • 0
                Вне всяких сомнений.
              • 0
                Выложите код на GitHub и можно будет более предметно обсудить преимущества и недостатки ОС в embeded. Сам придерживаюсь подхода — каждой задаче свой инструмент.
                • 0
                  Для своих проектов я использую Mercurial, так что до гитхаба код вряд ли дойдет. Но подумаю о том, чтобы открыть репозиторий на BitBucket.
                • 0
                  даже lwip можно запустить без ОС, но зачем? на самом деле ос — сильно облегчает многозадачность и вопрос стоит по другому — зачем изобретать велосипед, если все это решается ОС. ЗЫ: сейчас делаю проект без ос на STM8S003, потому что реально нет места — используется 7733 байта из 8192 на максимальном уровне оптимизации, и надо будет еще добавлять функционал :(, но если ресурсы позволяют — ось рулит.
                  • 0
                    То что вы описали про свой проект, спокойно умешается в тысяче строк и паре циклов. Есстественно там не нужна ось. А вот когда устройство например имеет свой графический интерфейс (ессно самописный), то без банального планировщика что то многозадачное писать муторно. Хотя я все таки предпочитаю использовать свой планировщик в стиле ентить в HLSDK.
                    • 0
                      Просто вы сформулировали вопрос как утверждение, отсюда и минусы. Отвечая на него — вы, конечно, можете обойтись без ОС. Но в какой-то момент вам понадобится, например, делать длинную обработку своих показаний и, одновременно, скажем, данных с датчика вибрации. Вы решаете, что это не проблема, и пишете в своём главном цикле оркестровку обработок данных из прерываний — мальчики амперметры налево, девочки вибрация направо. Это вносит зависимость между (например) концептуально независимыми потоками данных. Потом вы обнаруживаете, что у вас есть другой датчик, данные от которого надо обработать срочно, сразу по пришествию, но обработка слишком длинная для прерывания, а пускать их в главный цикл — не получается, цикл то и дело занят обработкой амперметра, и надо её как-то прерывать и заниматься срочными данными. Потом вы решаете использовать DMA для записи на SD-карту, у вас появляется ещё один асинхронный процесс (начали пересылку, теперь можно что-то другое делать пока DMA-контроллер не просигналит, что пересылка кончилась), и вы начинаете городить конечный автомат, что само по себе не плохо, но ваш код начинает размазываться по разным состояниям. И в какой-то момент вы приходите к осознанию того, что вы написали собственную ОС. :)

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

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


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

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

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

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

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

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

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

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


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

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


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

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

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

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

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

                      • 0
                        я для своего проекта использовал esp-open-sdk, это было NOSDK версия, но там был хорошо спрятанный баг, который вроде бы решили.
                        • 0
                          Три esp8266 трудятся в так называемом продакшене с freertos у разных заказчиков. Удобно, не напряжно, разделил на модули, модули в таски и все. Правильно автор что freertos заюзал. А кто говорит что без ОС лучше, ну не знаю, может Вы не разобрались… Не те задачи и не те ресурсы что бы спички считать.
                          • 0
                            Подскажите, что почитать с примерами, чтобы разобраться новичку.
                            • 0
                              Смотря в чем разобраться — если конкретно с ESP8266 — в SDK есть много разных примеров…
                              • 0
                                Для начала с чего-нибудь попроще, типа Maple mini. Примеры в том числе с работой с самой SDK.
                                • 0
                                  Я пока только с ESP знаком, сейчас вот ESP32 палочкой с разных сторон тыкаю. С другими контроллерами не разбирался, и посоветовать не могу ничего.
                                  • 0
                                    jonic писал, что «возможно не разобрались с RTOS», я для своих задач плюсов не вижу, но разобраться хочу, поэтому и спросил. Т.е. практически безотносительно железа.
                                    Если тыкнете носом в пошаговое руководство по ESPшной SDK буду тоже благодарен.
                                    • 0
                                      Не знаю даже, есть ли пошаговые руководства вообще. Я просто открывал официальную документацию по SDK, описание API FreeRTOS, lwip и разбирался. Начинал с попыток завести официальный SDK, прошить свой хеллоуворлд. После неудачи с этими инструментами нагуглил ESP Open RTOS, и с ним завелось. На том и остановился.
                                      • +2
                                        Вот побольше бы таких пошаговых писали, а то большинство документации или примеров напоминают мем «как нарисовать сову». У меня хоть и коряво получается писать, но на другом ресурсе пару примеров hello world со скриншотами попытался описать по теме в которой сам немножко разобрался.
                                        • 0
                                          могу дать исходник своего проекта, если интересно, извините за поздний ответ) весь в работе, не слежу
                                    • 0
                                      Мне если честно сейчас больше mtk7688 и платы на ней симпатизируют, потому что не так уж и дороже и openwrt. Например из коробки отлично работают usb звуковые карты с али по 1$, а так же из коробки можно сделать флешку с системой и файлами приличного размера. Да и платы стоят в районе 700-800 рублей, что не дорого считаю за вполне крутые характеристики
                                      • +1
                                        Дороговатые они как-то. И как у них с энергопотреблением?
                                        • +1
                                          Их точно не стоит от батарейки питать, мы сейчас говорим про SoC с вынесенной памятью(до 256мб) и флешем, поэтому рассуждать о потреблении одного чипа было бы не совсем корректным. У меня специфика что постоянное питание присутствует) а 12$ за готовый модуль с 700mhz SoC 32 ROM и 128 RAM я бы не сказал что дорого
                            • 0
                              Спасибо за интересную статью! А MQTT сервер какой использовался?
                              • 0
                                Пока разворачиваю свой, тестирую на www.cloudmqtt.com. Там есть бесплатный план с кучей ограничений, но для тестов хватает.
                                • 0
                                  А данные делите на два потока для чётных и нечётных минут? Зачем так сделано?
                                  • 0
                                    Один поток делает замеры, второй отправляет результаты… Дальше буду подбирать интервал отправки (и добавлю глубокий сон) так, чтобы меньше тратилось энергии…
                                    • 0
                                      Я к тому, что у вас сбор данных раз в минуту, а отправка раз в две минуты как я понял. Почему нельзя было отправлять сразу после измерения? А так принятые пары значений потом придётся специально «разносить» по времени, т.к. все стандартные сервисы берут время приёма mqtt сообщения как я понимаю.
                                      • 0
                                        Время замера сохраняется на самой ESP, которая, в свою очередь, синхронизируется по SNTP… Так что отправлять можно и раз в 30 минут, время каждого замера не потеряется…
                                        • 0
                                          Я понимаю, что не потеряется, но отправляете то вы их через mqtt, соответственно нужно отправлять пару значение-время и потом руками разбирать. Стандартные системы умных домов по-моему такого не поддерживают (я просто из текста не очень понял куда и чем вы эти данные принимаете и складываете).
                                          • 0
                                            Принимаю через MQTT самописным сервером на Java. Насчет стандатрных систем умных домов я не в курсе, даже не исследовал, что вообще в этой области есть.
                              • +3
                                Таким вот нехитрым образом, с помощью языка C и рук с небольшим радиусом кривизны
                                Наверное, подразумевался все же большой радиус кривизны :)

                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.