Замотивировавшись ответом от 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
#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 */
#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;
}
}
#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 */
#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 (обновление прошивки по воздуху).
Немного результатов работы данного кода:
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)
iig
22.09.2017 11:15+1Под ESP8266 можно писать на C(++), сомнений нет. Какая роль в этом проекте FreeRTOS — не понял :(
kosmonaFFFt Автор
22.09.2017 12:20Когда я выбирал на чем писать, выбор был из 3-х вариантов:
— LUA/MicroPython с прошивкой NodeMCU
— ESP без OS
— ESP с FreeRTOS
т. к. проект just for fun и для самообучения, я выбрал вариант 3, в котором помимо самой ESP я познакомлюсь еще и с FreeRTOS…iig
22.09.2017 12:39У каждого свои критерии выбора, не о том вопрос.
Например, программа для ESP в Arduino-style выгдядит так:
void setup () { } void main() { while (1) { do_something(); yield(); // выполнять код, который остался "под капотом". Сеть, таймеры.. } }
Стоит в главном цикле запустить "тяжелый" код — можно получить побочные эффекты (обрывы сети как минимум).
RTOS как-то позволяет обходить это ограничение?
Если ошибаюсь — поправьте.
kosmonaFFFt Автор
22.09.2017 13:23В FreeRTOS, насколько я знаю, нет определяемого программистом «главного цикла». Просто несколько задач с разными, или не очень, приоритетами, которые управляются планировщиком, +прерывания.
Barnaby
22.09.2017 17:50Вопрос в том что будет если код выполняется очень долго, в Arduino для этого надо расставлять yield (или delay), иначе отвалится вайфай а потом придет wdt reset.
ixamilion
25.09.2017 11:28FreeRTOS это система вытесняющего типа, т.е. переключение происходит не по прямой команде пользователя, а планировщиком. Проверка необходимости переключения происходит каждый системный такт (1мс по умолчанию). Если в такой такт обнаруживается задача готовая к выполнению с приоритетом не ниже текущей, то происходит переключение. Про ваш пример с wdt — оптимально сделать его задачей высшего приоритета, которая будет в постоянном ожидании семафора на выполнение. Получив такой, обнулять таймер и снова в ожидание. А семафоры раздавать в остальных задачах, что будет свидетельствовать об отсутствии зацикленности там где ее не ждете.
Falstaff
25.09.2017 13:06+1Не ради спора, а просто уточнения ради, хочу дополнить что у FreeRTOS, во-первых, режим вытесняющей многозадачности конфигурируется (настройка
configUSE_PREEMTION
воFreeRTOSConfig.h
), а во-вторых у пользователя есть возможность руками переключить контекст (вызвавtaskYIELD()
, хотя при включённой вытесняющей многозадачности это по идее никогда не понадобится). Ну и, конечно, проверка необходимости переключения просходит в системных вызовах — когда вы отдаёте семафор, пишете в очередь, ставите флаг и т.п., так что если вы запишете в очередь в прерывании, то вернуться вы можете уже в освободившуюся задачу, а не в ту, которую это прерывание прервало. Это просто чтобы не создалось впечатление, что переключение происходит только на тактах.
de1m
22.09.2017 13:42я для своего проекта использовал esp-open-sdk, это было NOSDK версия, но там был хорошо спрятанный баг, который вроде бы решили.
jonic
22.09.2017 17:24Три esp8266 трудятся в так называемом продакшене с freertos у разных заказчиков. Удобно, не напряжно, разделил на модули, модули в таски и все. Правильно автор что freertos заюзал. А кто говорит что без ОС лучше, ну не знаю, может Вы не разобрались… Не те задачи и не те ресурсы что бы спички считать.
Mogwaika
23.09.2017 14:47Подскажите, что почитать с примерами, чтобы разобраться новичку.
kosmonaFFFt Автор
23.09.2017 20:20Смотря в чем разобраться — если конкретно с ESP8266 — в SDK есть много разных примеров…
Mogwaika
23.09.2017 20:44Для начала с чего-нибудь попроще, типа Maple mini. Примеры в том числе с работой с самой SDK.
kosmonaFFFt Автор
23.09.2017 20:50Я пока только с ESP знаком, сейчас вот ESP32 палочкой с разных сторон тыкаю. С другими контроллерами не разбирался, и посоветовать не могу ничего.
Mogwaika
23.09.2017 21:58jonic писал, что «возможно не разобрались с RTOS», я для своих задач плюсов не вижу, но разобраться хочу, поэтому и спросил. Т.е. практически безотносительно железа.
Если тыкнете носом в пошаговое руководство по ESPшной SDK буду тоже благодарен.kosmonaFFFt Автор
25.09.2017 00:10Не знаю даже, есть ли пошаговые руководства вообще. Я просто открывал официальную документацию по SDK, описание API FreeRTOS, lwip и разбирался. Начинал с попыток завести официальный SDK, прошить свой хеллоуворлд. После неудачи с этими инструментами нагуглил ESP Open RTOS, и с ним завелось. На том и остановился.
Mogwaika
25.09.2017 01:26+1Вот побольше бы таких пошаговых писали, а то большинство документации или примеров напоминают мем «как нарисовать сову». У меня хоть и коряво получается писать, но на другом ресурсе пару примеров hello world со скриншотами попытался описать по теме в которой сам немножко разобрался.
Alex_Sa
23.09.2017 00:01Спасибо за интересную статью! А MQTT сервер какой использовался?
kosmonaFFFt Автор
23.09.2017 02:12Пока разворачиваю свой, тестирую на www.cloudmqtt.com. Там есть бесплатный план с кучей ограничений, но для тестов хватает.
Mogwaika
23.09.2017 14:49А данные делите на два потока для чётных и нечётных минут? Зачем так сделано?
kosmonaFFFt Автор
23.09.2017 20:19Один поток делает замеры, второй отправляет результаты… Дальше буду подбирать интервал отправки (и добавлю глубокий сон) так, чтобы меньше тратилось энергии…
Mogwaika
23.09.2017 20:43Я к тому, что у вас сбор данных раз в минуту, а отправка раз в две минуты как я понял. Почему нельзя было отправлять сразу после измерения? А так принятые пары значений потом придётся специально «разносить» по времени, т.к. все стандартные сервисы берут время приёма mqtt сообщения как я понимаю.
kosmonaFFFt Автор
23.09.2017 20:48Время замера сохраняется на самой ESP, которая, в свою очередь, синхронизируется по SNTP… Так что отправлять можно и раз в 30 минут, время каждого замера не потеряется…
Mogwaika
23.09.2017 21:54Я понимаю, что не потеряется, но отправляете то вы их через mqtt, соответственно нужно отправлять пару значение-время и потом руками разбирать. Стандартные системы умных домов по-моему такого не поддерживают (я просто из текста не очень понял куда и чем вы эти данные принимаете и складываете).
kosmonaFFFt Автор
24.09.2017 23:57Принимаю через MQTT самописным сервером на Java. Насчет стандатрных систем умных домов я не в курсе, даже не исследовал, что вообще в этой области есть.
Alexander_vrn
25.09.2017 00:28+2Таким вот нехитрым образом, с помощью языка C и рук с небольшим радиусом кривизны
Наверное, подразумевался все же большой радиус кривизны :)
Ksiw
Зачем вообще на микроконтроллере использовать ОС и тратить на нее ограниченные ресурсы, если можно написать компактный и эффективный код под конкретную задачу?
kosmonaFFFt Автор
Лично для меня, как для Java программиста, контроллер без OC слишком низкоуровнево и, пока что, геморройно. А так получается, использование FreeRTOS в данном конкретном случае ESP8266 — золотая середина между низкоуровневым программированием без ОС и скриптовыми языками типа LUA. Хотя доводилось в университете программировать 8-битные PIC на ассемблере, и там для ОС точно места нет.
WerewolfPrankster
Так там ведь от той ОС планировщик, очередь да таймер по хорошему. Куда уж компактнее
Alex_ME
Начиная с определенной сложности задачи, использование легковесных ОС вроде FreeRTOS оправдано и активно применяется в Embedded разработке. И даже на более слабых контроллерах, а ESP8266 обладает довольно внушительными ресурсами (существенно превышает "народные" ATMega или STM32F1xx). Правда с памятью там проблемы были, вроде как.
А задача работы WiFi и IP стеком уже весьма серьезная. Так аналог ESP8266 — RTL87xx/RTL95xx (и прочие модули серии) имеют в SDK FreeRTOS по-умолчанию и никак иначе.
Ksiw
Шибко грамотные специалисты переполнившись ядом негодования, кинулись минусить безобидный вопрос менее опытного.
Должен пояснить, что в своем проекте использую СТМку, которая снимает показания амперметров с трёх фаз, меряет обороты двигателя, щёлкает релюшкой и отправляет показания/принимает команды по mqtt, и чудесно со всем этим справляется без дополнительной прослойки в виде ОС.
Спасибо ответившим.
iig
А вас точно интересует ответ? ;)
Ksiw
Вне всяких сомнений.
AndyKorg
Выложите код на GitHub и можно будет более предметно обсудить преимущества и недостатки ОС в embeded. Сам придерживаюсь подхода — каждой задаче свой инструмент.
kosmonaFFFt Автор
Для своих проектов я использую Mercurial, так что до гитхаба код вряд ли дойдет. Но подумаю о том, чтобы открыть репозиторий на BitBucket.
avf1906
даже lwip можно запустить без ОС, но зачем? на самом деле ос — сильно облегчает многозадачность и вопрос стоит по другому — зачем изобретать велосипед, если все это решается ОС. ЗЫ: сейчас делаю проект без ос на STM8S003, потому что реально нет места — используется 7733 байта из 8192 на максимальном уровне оптимизации, и надо будет еще добавлять функционал :(, но если ресурсы позволяют — ось рулит.
Solexid
То что вы описали про свой проект, спокойно умешается в тысяче строк и паре циклов. Есстественно там не нужна ось. А вот когда устройство например имеет свой графический интерфейс (ессно самописный), то без банального планировщика что то многозадачное писать муторно. Хотя я все таки предпочитаю использовать свой планировщик в стиле ентить в HLSDK.
Falstaff
Просто вы сформулировали вопрос как утверждение, отсюда и минусы. Отвечая на него — вы, конечно, можете обойтись без ОС. Но в какой-то момент вам понадобится, например, делать длинную обработку своих показаний и, одновременно, скажем, данных с датчика вибрации. Вы решаете, что это не проблема, и пишете в своём главном цикле оркестровку обработок данных из прерываний —
мальчикиамперметры налево,девочкивибрация направо. Это вносит зависимость между (например) концептуально независимыми потоками данных. Потом вы обнаруживаете, что у вас есть другой датчик, данные от которого надо обработать срочно, сразу по пришествию, но обработка слишком длинная для прерывания, а пускать их в главный цикл — не получается, цикл то и дело занят обработкой амперметра, и надо её как-то прерывать и заниматься срочными данными. Потом вы решаете использовать DMA для записи на SD-карту, у вас появляется ещё один асинхронный процесс (начали пересылку, теперь можно что-то другое делать пока DMA-контроллер не просигналит, что пересылка кончилась), и вы начинаете городить конечный автомат, что само по себе не плохо, но ваш код начинает размазываться по разным состояниям. И в какой-то момент вы приходите к осознанию того, что вы написали собственную ОС. :)В общем, разным задачам — разные решения. На определённых задачах и ОС вам понадобится.
Ksiw
Спасибо за глубокий ответ. Действительно, примерно что-то такое и началось, когда в довесок ко всему решил прикрутить вывод на экран 5010.
Вы лучше некуда разъяснили ситуацию.
Сколь много ресурсов железяки отнимает freertos(прошивка/озу)?
Жаль, не могу истово плюсануть.
Falstaff
Не за что. :) По поводу ресурсов — на практике всё различается, но чтобы просто представить себе порядок цифр, можно поглядеть на их официальный FAQ по потреблению памяти. Если вкратце, то:
По опыту — больше всего оперативной займут стеки задач, если задач много и им приходится давать много стека (например,
printf()
много стека ест при вызове) то расходов действительно будет порядком. Но всё в ваших руках, если иерархия вызовов не очень глубокая и ничего жадного до стека не используется (вызовы вроде printf(), большие локальные структуры или массивы), то стеки могут быть маленькими. Для задач вроде интерфейса (лампочки, пищалки и т.п.) можно использовать корутины. Они ограничены простыми задачами, но собственного стека у них нет (все корутины бегут на выбранной вами задаче, обычно idle task, и используют её стек), поэтому они совсем легковесные.Mogwaika
А на быстродействие как влияет?
Falstaff
Вопрос с влиянием на быстродействие не очень однозначный. RTOS, в отличие от привычных крупных настольных ОС, в принципе не обязана чем-то шуршать в отсутствие внешних раздражителей, затрачивать процессорное время на поддержание себя самой. Большая часть накладных расходов будет приходиться на переключение контекста. Если упрощённо, то переключение контекста может произойти в таких случаях:
В том же официальном FAQ по ссылке выше есть пример для определённых условий (Keil, Cortex-M3, без stack overflow checking, без статистики, оптимизация на скорость). При этом на переключение контекста уходит 84 такта (на самом деле — на работу планировщика, даже если он решил не переключать контекст). Можно (очень грубо) прикинуть сценарии и сколько при этом будет накладных расходов. Например:
Это очень примерно, я мог что-то не учесть (если что, надеюсь, меня поправят), на других машинах/компиляторах/настройках тоже будет различаться, так что это для того, чтобы общее ощущение создалось. Пример тоже слегка экстремальный, обычно такой яростной активности нет, к тому же есть много способов не переключаться так часто (например, если вы можете в прерывании буферизировать данные и посылать в задачу кусками побольше). Режим tickless idle позволит сэкономить на системном таймере при необходимости, чтобы в отсутствие активности накладных расходов совсем не было. В общем, примерно такая картина.
Vladislav_Dudnikov
FreeRTOS — это система реального времени. Без систем реального времени многие задачи будет почти невозможно выполнять.
Простой пример в виде задачи. Есть контроллер, которые меряет n каналов АЦП и обрабатывает их (скажем 10 мс на одну выборку). Есть RS485 по которому нужно передавать обработанные данные с минимальной задержкой (максимальное время ответа — 1 мс).
Такую задачу решить с RTOSом — раз плюнуть.
Тем более суффикс OS не говорит о том, что там сверхсложная система, которая требует кучу памяти, ресурсов и т.п. FreeRTOS, например, требует мало ресурсов.