Русский
Русский
English
Статистика
Реклама

Встраиваемые системы

Espressif IoT Development Framework 71 выстрел в ногу

20.01.2021 16:09:28 | Автор: admin

0790_Espressif_IoT_Development_Framework_ru/image1.png
Один из наших читателей обратил наше внимание на Espressif IoT Development Framework. Он нашёл ошибку в коде проекта и поинтересовался, смог бы её найти статический анализатор PVS-Studio. Именно эту ошибку анализатор пока найти не может, зато нашёл множество других. По мотивам этой истории и найденных ошибок, мы решили написать классическую статью про проверку открытого проекта. Приятного изучения того, из-за чего IoT устройства могут "выстрелить вам в ногу".


Программно-аппаратные системы


Отец языка C++ Бьярне Страуструп как-то сказал:


"Си" позволяет очень просто выстрелить себе в ногу. На "Си++" сделать это сложнее, но, когда вы это делаете, отрывает всю ногу.

В нашем случае, заявление начинает приобретать несколько иной смысл. Из идеи, что программист может ошибиться и программа будет функционировать как-то неправильно, мы подходим к ситуациям, когда это может причинять реальный физический вред.


Такие проекты, как Espressif IoT Development Framework, служат для реализации программно-аппаратных систем, взаимодействующих с человеком и управляющие объектами в реальном мире. Всё это накладывает дополнительные требования к качеству и надёжности программного кода. Именно отсюда берут основы такие стандарты как MISRA или AUTOSAR. Впрочем, это уже другая тема.


Вернёмся к Espressif IoT Development Framework (исходный код на сайте GitHub: esp-idf). Вот его краткое описание:


ESP-IDF is Espressif's official IoT Development Framework for the ESP32 and ESP32-S series of SoCs. It provides a self-sufficient SDK for any generic application development on these platforms, using programming languages such as C and C++. ESP-IDF currently powers millions of devices in the field, and enables building a variety of network-connected products, ranging from simple light bulbs and toys to big appliances and industrial devices.

Думаю, читателям интересно посмотреть, уделяется ли разработчиками этого проекта достаточно внимания к качеству и надёжности. К сожалению, такой уверенности, нет. Познакомившись со статьей и описанием замеченных дефектов, вы поймете мои опасения. Итак, заваривайте чай/кофе, вас ждёт много текста и кода.


Предыстория


Ещё хочется рассказать, как появилась эта статья. Мне написал Юрий Попов (Hardcore IoT fullstack dev & CTO), который с интересом следит за нашими публикациями. Незадолго до этого он самостоятельно вручную нашёл ошибку в Espressif IoT Development Framework и поинтересовался, может ли выявить этот дефект PVS-Studio. Ошибка связана с опечаткой коде, а PVS-Studio всегда славился тем, что хорошо выявляет подобные ошибки.


Некорректный код находился в файле mdns.c:


mdns_txt_linked_item_t * txt = service->txt;while (txt) {  data_len += 2 + strlen(service->txt->key) + strlen(service->txt->value);  txt = txt->next;}

Происходит обход списка и определённым образом должны суммироваться длины строк, на которые ссылаются различные объекты, хранящиеся в списке. Вот только всё время считается длина строк, хранящаяся в первом объекте.


Правильный код:


data_len += 2 + strlen(txt->key) + strlen(txt->value);

К обоюдному разочарованию меня и читателя Юры, PVS-Studio не смог заметить эту ошибку. Он просто не знает про такой паттерн ошибки. Собственно, и наша команда не знала про такой паттерн. PVS-Studio, как и любой другой анализатор, умеет замечать только то, на что его запрограммировали :).


Что ж, жаль, но не страшно. Это один из источников, где можно черпать идеи по развитию PVS-Studio. Пользователи и клиенты присылают различные паттерны ошибок, которые они выявили в коде своих проектов, но про которые не знает PVS-Studio. И мы постепенно создаём новые диагностические правила. Так произойдёт и с рассмотренным выше паттерном. Мы уже выписали этот пример в TODO и реализуем новое диагностическое правило для выявления схожих ситуаций в одной из следующих версий анализатора.


По итогам всего этого, Юра сам написал небольшую заметку про эту ошибку, как он её искал и про PVS-Studio: "Баг в ESP-IDF: MDNS, Wireshark и при чём тут единороги". Плюс он уведомил авторов проекта о найденной ошибке: Spurious MDNS collision detection (IDFGH-4263).


На этом история не закончилось. Юра предложил нашей команде проверить проект и написать заметку о результатах. Мы не стали отказываться, так как весьма часто делаем подобные публикации для популяризации методологии статического анализа кода и заодно инструмента PVS-Studio :).


Правда проверку мы повели достаточно неуклюже. К сожалению, нет примера "собрать всё". Ну или мы не разобрались. Мы начали с getting_started\hello_world. Вроде бы он использует часть фреймворка, но не полностью. Так что можно найти и другие ошибки, добившись компиляции большего количества файлов фреймворка. Другими словами, то, что в статье будет описана только 71 ошибка, это наша недоработка :).


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


К счастью, у Юрия Попова, который заварил всю эту кашу, гораздо больше энтузиазма, чем у меня. Он сообщил, что смог добиться более полной компиляции фреймворка и проверил гораздо больше файлов. И, очень вероятно, вслед за моей статьей, выйдет его статья, где он рассмотрит дополнительную порцию ошибок.


Примеры, откуда берутся ложные/бессмысленные срабатывания


Всех исследователей, которые захотят проверить Espressif IoT Development Framework, я хочу предупредить, что понадобится предварительная настройка анализатора. Без неё вы утоните в большом количестве ложных/бесполезных срабатываний. Но анализатор не виноват.


В коде проекта очень активно используются директивы условной компиляции (#ifdef) и макросы. Такой стиль кодирования запутывает анализатор и порождает множество однотипных бесполезных предупреждений. Чтобы было понятнее, как и почему это происходит, рассмотрим пару примеров.


Предупреждение PVS-Studio: V547 Expression 'ret != 0' is always true. esp_hidd.c 45


esp_err_t esp_hidd_dev_init(....){  esp_err_t ret = ESP_OK;  ....  switch (transport) {#if CONFIG_GATTS_ENABLE  case ESP_HID_TRANSPORT_BLE:    ret = esp_ble_hidd_dev_init(dev, config, callback);    break;#endif /* CONFIG_GATTS_ENABLE */  default:    ret = ESP_FAIL;    break;  }  if (ret != ESP_OK) {    free(dev);    return ret;  }  ....}

Выбран такой режим компиляции, при котором макрос CONFIG_GATTS_ENABLE не объявлен. Поэтому, для анализатора этот код выглядит так:


esp_err_t ret = ESP_OK;....switch (transport) {default:  ret = ESP_FAIL;  break;}if (ret != ESP_OK) {

Анализатор вроде прав, что условие всегда истинно. С другой стороны, пользы от этого предупреждения нет, так как мы понимаем, что код совершенно корректен и имеет смысл. Подобные ситуации встречаются крайне часто, что затрудняет просмотр отчёта. Это такая неприятная плата за активное использование условной компиляции :).


Рассмотрим другой пример. В коде активно используется своя разновидность assert-макросов. К сожалению, они тоже сбивают анализатор с толку. Предупреждение PVS-Studio: V547 Expression 'sntp_pcb != NULL' is always true. sntp.c 664


#define LWIP_PLATFORM_ASSERT(x) do \  {printf("Assertion \"%s\" failed at line %d in %s\n", \    x, __LINE__, __FILE__); fflush(NULL); abort();} while(0)#ifndef LWIP_NOASSERT#define LWIP_ASSERT(message, assertion) do { if (!(assertion)) { \  LWIP_PLATFORM_ASSERT(message); }} while(0)#else  /* LWIP_NOASSERT */#define LWIP_ASSERT(message, assertion)#endif /* LWIP_NOASSERT */sntp_pcb = udp_new_ip_type(IPADDR_TYPE_ANY);LWIP_ASSERT("Failed to allocate udp pcb for sntp client", sntp_pcb != NULL);if (sntp_pcb != NULL) {

Анализатор видит, что код в которой раскрывается LWIP_ASSERT остановит выполнение программы (см. вызов функции abort), если указатель sntp_pcb будет нулевой. Поэтому PVS-Studio предупреждает, что следующая проверка (sntp_pcb != NULL) не имеет смысла.


С одной стороны, анализатор прав. Но всё поменяется, если макрос в другом режиме компиляции будет раскрыт в "ничто". В этом случае проверка уже будет иметь смысл. Да, при втором сценарии анализатор ругаться не будет, но сути это уже не меняет. В первом то случае у нас лишнее предупреждение.


Впрочем, всё это не страшно. Если взяться за проект всерьез, то можно настроить анализатор так, что большинство бессмысленных сообщений исчезнет. Ещё в ряде мест можно улучшить ситуацию, изменив стиль написания кода и макросов. Но это уже выходит за рамки данной статьи. Дополнительно можно использовать механизм подавления предупреждений в конкретных местах, в макросах и т.д. Есть ещё и механизм массовой разметки. Про всё это подробнее можно почитать в статье "Как внедрить статический анализатор кода в legacy проект и не демотивировать команду".


Security


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


Для удобства классификации слабостей кода можно использовать CWE (Common Weakness Enumeration). В PVS-Studio можно включить отображение CWE ID для предупреждений. Для предупреждений этой главы я дополнительно приведу соответствующий CWE ID.


Подробнее тема поиска потенциальных уязвимостей раскрыта в статье "Статический анализатор кода PVS-Studio как защита от уязвимостей нулевого дня".


Ошибка N1; Порядок аргументов


Предупреждение PVS-Studio: V764 Possible incorrect order of arguments passed to 'crypto_generichash_blake2b__init_salt_personal' function: 'salt' and 'personal'. blake2b-ref.c 457


int blake2b_init_salt_personal(blake2b_state *S, const uint8_t outlen,                               const void *personal, const void *salt);intblake2b_salt_personal(uint8_t *out, const void *in, const void *key,                      const uint8_t outlen, const uint64_t inlen,                      uint8_t keylen, const void *salt, const void *personal){  ....  if (blake2b_init_salt_personal(S, outlen, salt, personal) < 0)    abort();  ....}

При вызове функции blake2b_init_salt_personal перепутаны местами аргументы personal и salt. Мне кажется, вряд ли это задумано специально и, скорее всего, это ошибка, возникшая по невнимательности. Я не ориентируюсь в коде проекта и в криптографии, но что-то мне подсказывает, что такая путаница может иметь нехорошие последствия.


Согласно CWE эта ошибка классифицируется как CWE-683: Function Call With Incorrect Order of Arguments.


Ошибка N2; Отбрасывание значащих бит


Предупреждение PVS-Studio: V642 Saving the 'memcmp' function result inside the 'unsigned char' type variable is inappropriate. The significant bits could be lost breaking the program's logic. mbc_tcp_master.c 387


static esp_err_t mbc_tcp_master_set_request(  char* name, mb_param_mode_t mode, mb_param_request_t* request,  mb_parameter_descriptor_t* reg_data){  ....  // Compare the name of parameter with parameter key from table  uint8_t comp_result = memcmp((const char*)name,                               (const char*)reg_ptr->param_key,                               (size_t)param_key_len);  if (comp_result == 0) {  ....}

Сохранять результат работы функции memcmp в однобайтовую переменную это очень плохо. Это дефект, который вполне может превратиться в реальную уязвимость, подобную этой: CVE-2012-2122. Подробнее, почему так писать нельзя, описано в документации к диагностике V642.


Если совсем кратко, то некоторые реализации функция memset могут возвращать в случае несовпадения блоков памяти не только значения 1 или -1. Функция, например, может вернуть значение 1024. А это число, записанное в переменную типа uint8_t превратится в 0.


Согласно CWE эта ошибка классифицируется как CWE-197: Numeric Truncation Error.


Ошибка N3 N20; Приватные данные остаются в памяти


Предупреждение PVS-Studio: V597 The compiler could delete the 'memset' function call, which is used to flush 'prk' buffer. The memset_s() function should be used to erase the private data. dpp.c 854


#ifndef os_memset#define os_memset(s, c, n) memset(s, c, n)#endifstatic int dpp_derive_k1(const u8 *Mx, size_t Mx_len, u8 *k1,       unsigned int hash_len){  u8 salt[DPP_MAX_HASH_LEN], prk[DPP_MAX_HASH_LEN];  const char *info = "first intermediate key";  int res;  /* k1 = HKDF(<>, "first intermediate key", M.x) */  /* HKDF-Extract(<>, M.x) */  os_memset(salt, 0, hash_len);  if (dpp_hmac(hash_len, salt, hash_len, Mx, Mx_len, prk) < 0)    return -1;  wpa_hexdump_key(MSG_DEBUG, "DPP: PRK = HKDF-Extract(<>, IKM=M.x)",      prk, hash_len);  /* HKDF-Expand(PRK, info, L) */  res = dpp_hkdf_expand(hash_len, prk, hash_len, info, k1, hash_len);  os_memset(prk, 0, hash_len);             // <=  if (res < 0)    return -1;  wpa_hexdump_key(MSG_DEBUG, "DPP: k1 = HKDF-Expand(PRK, info, L)",                  k1, hash_len);  return 0;}

Очень распространённая ошибка. Компилятор вправе в целях оптимизации удалить вызов функции memset, так как после заполнения буфера нулями, он больше не используется. В результате приватные данные на самом деле не затираются, а продолжат болтаться где-то в памяти. Подробности можно узнать в статье "Безопасная очистка приватных данных".


Согласно CWE эта ошибка классифицируется как CWE-14: Compiler Removal of Code to Clear Buffers.


Другие ошибки этого типа:


  • V597 The compiler could delete the 'memset' function call, which is used to flush 'prk' buffer. The memset_s() function should be used to erase the private data. dpp.c 883
  • V597 The compiler could delete the 'memset' function call, which is used to flush 'prk' buffer. The memset_s() function should be used to erase the private data. dpp.c 942
  • V597 The compiler could delete the 'memset' function call, which is used to flush 'psk' buffer. The memset_s() function should be used to erase the private data. dpp.c 3939
  • V597 The compiler could delete the 'memset' function call, which is used to flush 'prk' buffer. The memset_s() function should be used to erase the private data. dpp.c 5729
  • V597 The compiler could delete the 'memset' function call, which is used to flush 'Nx' buffer. The memset_s() function should be used to erase the private data. dpp.c 5934
  • V597 The compiler could delete the 'memset' function call, which is used to flush 'val' buffer. The memset_s() function should be used to erase the private data. sae.c 155
  • V597 The compiler could delete the 'memset' function call, which is used to flush 'keyseed' buffer. The memset_s() function should be used to erase the private data. sae.c 834
  • V597 The compiler could delete the 'memset' function call, which is used to flush 'keys' buffer. The memset_s() function should be used to erase the private data. sae.c 838
  • V597 The compiler could delete the 'memset' function call, which is used to flush 'pkey' buffer. The memset_s() function should be used to erase the private data. des-internal.c 422
  • V597 The compiler could delete the 'memset' function call, which is used to flush 'ek' buffer. The memset_s() function should be used to erase the private data. des-internal.c 423
  • V597 The compiler could delete the 'memset' function call, which is used to flush 'finalcount' buffer. The memset_s() function should be used to erase the private data. sha1-internal.c 358
  • V597 The compiler could delete the 'memset' function call, which is used to flush 'A_MD5' buffer. The memset_s() function should be used to erase the private data. sha1-tlsprf.c 95
  • V597 The compiler could delete the 'memset' function call, which is used to flush 'P_MD5' buffer. The memset_s() function should be used to erase the private data. sha1-tlsprf.c 96
  • V597 The compiler could delete the 'memset' function call, which is used to flush 'A_SHA1' buffer. The memset_s() function should be used to erase the private data. sha1-tlsprf.c 97
  • V597 The compiler could delete the 'memset' function call, which is used to flush 'P_SHA1' buffer. The memset_s() function should be used to erase the private data. sha1-tlsprf.c 98
  • V597 The compiler could delete the 'memset' function call, which is used to flush 'T' buffer. The memset_s() function should be used to erase the private data. sha256-kdf.c 85
  • V597 The compiler could delete the 'memset' function call, which is used to flush 'hash' buffer. The memset_s() function should be used to erase the private data. sha256-prf.c 105

Ошибка N21; Не удаляется буфер с приватными данными


Предупреждение PVS-Studio: V575 The null pointer is passed into 'free' function. Inspect the first argument. sae.c 1185


static int sae_parse_password_identifier(struct sae_data *sae,           const u8 *pos, const u8 *end){  wpa_hexdump(MSG_DEBUG, "SAE: Possible elements at the end of the frame",        pos, end - pos);  if (!sae_is_password_id_elem(pos, end)) {    if (sae->tmp->pw_id) {      wpa_printf(MSG_DEBUG,           "SAE: No Password Identifier included, but expected one (%s)",           sae->tmp->pw_id);      return WLAN_STATUS_UNKNOWN_PASSWORD_IDENTIFIER;    }    os_free(sae->tmp->pw_id);    sae->tmp->pw_id = NULL;    return WLAN_STATUS_SUCCESS; /* No Password Identifier */  }  ....}

Если с паролем что-то не так и указатель pw_id не нулевой, то выводится отладочное предупреждение и функция завершает свою работу. Что интересно, далее происходит попытка освободить буфер, используя нулевой указатель. Более того, в нулевой указатель вновь записывается NULL. Всё это не имеет смысла. Скорее всего, строчки освобождения памяти находятся не на своём месте. И мне кажется, код должен быть таким:


if (!sae_is_password_id_elem(pos, end)) {  if (sae->tmp->pw_id) {    wpa_printf(MSG_DEBUG,         "SAE: No Password Identifier included, but expected one (%s)",         sae->tmp->pw_id);    os_free(sae->tmp->pw_id);    sae->tmp->pw_id = NULL;    return WLAN_STATUS_UNKNOWN_PASSWORD_IDENTIFIER;  }  return WLAN_STATUS_SUCCESS; /* No Password Identifier */}

Во-первых, наверное, это устранит утечку памяти. Во-вторых, приватные данные перестанут где-то зря храниться длительное время в памяти.


Согласно CWE, эта формально ошибка классифицируется как CWE-628: Function Call with Incorrectly Specified Arguments. Так её классифицирует PVS-Studio, но, по сути и последствиям, это какая-то другая слабость кода.


Ошибка N22, N23; Использование неинициализированного буфера в качестве ключа


Предупреждение PVS-Studio: V614 Uninitialized buffer 'hex' used. Consider checking the second actual argument of the 'memcpy' function. wps_registrar.c 1657


int wps_build_cred(struct wps_data *wps, struct wpabuf *msg){  ....  } else if (wps->use_psk_key && wps->wps->psk_set) {    char hex[65];    wpa_printf(MSG_DEBUG,  "WPS: Use PSK format for Network Key");    os_memcpy(wps->cred.key, hex, 32 * 2);    wps->cred.key_len = 32 * 2;  } else if (wps->wps->network_key) {  ....}

Неинициализированный буфер hex используется для инициализации какого-то ключа. Зачем так сделано непонятно. Возможно, это попытка заполнить ключ какими-то случайным значением, но всё равно это очень плохой способ.


В любом случае этот код нуждается во внимательной проверке.


Согласно CWE, эта ошибка классифицируется как CWE-457: Use of Uninitialized Variable.


Аналогичная ошибка: V614 Uninitialized buffer 'hex' used. Consider checking the second actual argument of the 'memcpy' function. wps_registrar.c 1678


Опечатки и Copy-Paste


Ошибка N24; Copy-Paste классический


Предупреждение PVS-Studio: V523 The 'then' statement is equivalent to the 'else' statement. timer.c 292


esp_err_t timer_isr_register(....){  ....  if ((intr_alloc_flags & ESP_INTR_FLAG_EDGE) == 0) {    intr_source = ETS_TG1_T0_LEVEL_INTR_SOURCE + timer_num;  } else {    intr_source = ETS_TG1_T0_LEVEL_INTR_SOURCE + timer_num;  }  ....}

Есть подозрение, что строчку скопировали, но забыли что-то в ней изменить. В результате, независимо от условия, в переменную intr_source записывается одно и тоже значение.


Примечание. Бывает конечно, что так и задумано. Например, если пока, значения действительно должны совпадать (т.е. это "todo-код"). Но тогда такой код явно стоит снабдить поясняющим комментарием.


Ошибка N25; Не там поставлена скобка


Предупреждение PVS-Studio: V593 Consider reviewing the expression of the 'A = B != C' kind. The expression is calculated as following: 'A = (B != C)'. esp_tls_mbedtls.c 446


esp_err_t set_client_config(....){ .... if ((ret = mbedtls_ssl_conf_alpn_protocols(&tls->conf, cfg->alpn_protos) != 0)) {   ESP_LOGE(TAG, "mbedtls_ssl_conf_alpn_protocols returned -0x%x", -ret);   ESP_INT_EVENT_TRACKER_CAPTURE(tls->error_handle, ERR_TYPE_MBEDTLS, -ret);   return ESP_ERR_MBEDTLS_SSL_CONF_ALPN_PROTOCOLS_FAILED; } ....}

Приоритет оператора сравнения выше, чем приоритет оператора присваивания. Поэтому условие вычисляется следующим образом:


TEMP = mbedtls_ssl_conf_alpn_protocols(....) != 0;if ((ret = TEMP))  PRINT(...., -ret);

В принципе, ошибочная ситуация поймается и обработается в коде, но не так, как задумано. Предполагалось распечатывать статус ошибки, который хранится в переменной ret. Но значение ret всегда будет равно 0 или 1. Поэтому, если что-то пойдёт не так, всегда будет распечатываться только одно значение (-1).


Ошибка возникла из-за того, что не там поставлена скобочка. Правильный код:


if ((ret = mbedtls_ssl_conf_alpn_protocols(&tls->conf, cfg->alpn_protos)) != 0)

Теперь всё будет вычисляться как нужно:


ret = mbedtls_ssl_conf_alpn_protocols(....);if (ret != 0)  PRINT(...., -ret);

Рассмотрим ещё один очень похожий случай.


Ошибка N26; MP_MEM превращается в MP_YES


V593 Consider reviewing the expression of the 'A = B != C' kind. The expression is calculated as following: 'A = (B != C)'. libtommath.h 1660


В начале рассмотрим некоторые константы. Они пригодятся нам чуть ниже.


#define MP_OKAY       0   /* ok result */#define MP_MEM        -2  /* out of mem */#define MP_VAL        -3  /* invalid input */#define MP_YES        1   /* yes response */

Далее следует сказать, что существует функция mp_init_multi, которая может возвращать значения MP_OKAY и MP_MEM:


static int mp_init_multi(mp_int *mp, ...);

И теперь собственно код с ошибкой:


static intmp_div(mp_int * a, mp_int * b, mp_int * c, mp_int * d){  ....  /* init our temps */  if ((res = mp_init_multi(&ta, &tb, &tq, &q, NULL) != MP_OKAY)) {     return res;  }  ....}

Рассмотрим проверку более тщательно:


if ((res = mp_init_multi(....) != MP_OKAY))

Вновь не там поставлена скобка. Поэтому в начале вычисляется:


TEMP = (mp_init_multi(....) != MP_OKAY);

Значение TEMP может быть только 0 или 1. Этим числам соответствуют константы MB_OKAY и MP_YES.


Далее выполняется присваивание и одновременно проверка:


if ((res = TEMP))   return res;

Видите подвох? Статус ошибки MP_MEM (-2) вдруг превратился в статус MB_YES (1). Последствия предсказать не могу, но ничего хорошего в этом нет.


Ошибка N27; Забыли разыменовать указатель


Предупреждение PVS-Studio: V595 The 'outbuf' pointer was utilized before it was verified against nullptr. Check lines: 374, 381. protocomm.c 374


static int protocomm_version_handler(uint32_t session_id,                                     const uint8_t *inbuf, ssize_t inlen,                                     uint8_t **outbuf, ssize_t *outlen,                                     void *priv_data){    protocomm_t *pc = (protocomm_t *) priv_data;    if (!pc->ver) {        *outlen = 0;        *outbuf = NULL;                                  // <=        return ESP_OK;    }    /* Output is a non null terminated string with length specified */    *outlen = strlen(pc->ver);    *outbuf = malloc(*outlen);                           // <=    if (outbuf == NULL) {                                // <=        ESP_LOGE(TAG, "Failed to allocate memory for version response");        return ESP_ERR_NO_MEM;    }    memcpy(*outbuf, pc->ver, *outlen);    return ESP_OK;}

Сообщение анализатора, на первый взгляд, может показаться непонятным. Давайте разбираться.


Если указатель pc->ver является нулевым, то функция досрочно завершает свою работу и при этом записывает значение по адресу, хранящегося в указателе outbuf:


*outbuf = NULL;

Запись по этому адресу происходит и далее:


*outbuf = malloc(*outlen);

А не нравится анализатору то, что затем этот указатель проверяется:


if (outbuf == NULL)

Действительно, это неправильно, в начале разыменовывать указатель, а только потом его проверять. Ляп в том, что на самом то деле должны были проверять не сам указатель, а то, что в него записали. Здесь просто в проверке опечатались и пропустили оператор разыменования (*).


Правильный код:


*outbuf = malloc(*outlen);if (*outbuf == NULL) {  ESP_LOGE(TAG, "Failed to allocate memory for version response");  return ESP_ERR_NO_MEM;}

Ошибка N28; Повторное присваивание


Предупреждение PVS-Studio: V519 The 'usRegCount' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 186, 187. mbfuncholding.c 187


eMBExceptioneMBFuncReadHoldingRegister( UCHAR * pucFrame, USHORT * usLen ){  ....  USHORT          usRegCount;  ....  usRegCount = ( USHORT )( pucFrame[MB_PDU_FUNC_READ_REGCNT_OFF] << 8 );  usRegCount = ( USHORT )( pucFrame[MB_PDU_FUNC_READ_REGCNT_OFF + 1] );  ....}

Код явно писался методом Copy-Paste. Строчку скопировали, но изменили только частично. По соседству есть вот такой осмысленный код:


usRegCount = ( USHORT )( pucFrame[MB_PDU_FUNC_WRITE_MUL_REGCNT_OFF] << 8 );usRegCount |= ( USHORT )( pucFrame[MB_PDU_FUNC_WRITE_MUL_REGCNT_OFF + 1] );

Видимо, и в рассмотренном коде с ошибкой, следовало в первой строке использовать оператор =, а во второй оператор |=.


Логические ошибки


Ошибка N29 N31; Неправильная работа с кодами возврата (Rare)


Предупреждение PVS-Studio: V547 Expression is always false. linenoise.c 256


static int getColumns(void) {  ....  /* Restore position. */  if (cols > start) {    char seq[32];    snprintf(seq,32,"\x1b[%dD",cols-start);    if (fwrite(seq, 1, strlen(seq), stdout) == -1) {      /* Can't recover... */    }    flushWrite();  }  ....}

Это безобидный вариант неправильной обработки статуса, возвращаемого функцией. Ошибка безобидна в том смысле, что никакой обработки и не предусмотрено. Не получилось записать строчку, так не получилось :). Хотя, как я уже сказал, этот безобидный вариант, такой стиль написания программ явно не является образцом для подражания.


Суть же самой ошибки в том, что функция fwrite не возвращает статус -1. Это физически невозможно, так как функция fwrite возвращает значение целочисленного типа size_t:


size_t fwrite( const void *restrict buffer, size_t size, size_t count,               FILE *restrict stream );

А вот что возвращает эта функция:


The number of objects written successfully, which may be less than count if an error occurs.

If size or count is zero, fwrite returns zero and performs no other action.

Таким образом, проверка статуса является неверной.


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


  • V547 Expression is always false. linenoise.c 481
  • V547 Expression is always false. linenoise.c 569

Ошибка N32, N33; Неправильная работа с кодами возврата (Medium)


Предупреждение PVS-Studio: V547 Expression is always false. linenoise.c 596


int linenoiseEditInsert(struct linenoiseState *l, char c) {  ....  if (fwrite(&c,1,1,stdout) == -1) return -1;  ....}

Хотя перед нами та же ошибка, что и в предыдущем случае, она более серьезна. Если не удаётся записать символ в файл, то функция linenoiseEditInsert должна прекратить свою работу и вернуть статус -1. Но этого не произойдёт, так как fwrite никогда не вернёт значение -1. Перед нами логическая ошибка обработки ситуации, когда не удаётся что-то записать в файл.


Аналогичную ошибку можно найти здесь: V547 Expression is always false. linenoise.c 742.


Ошибка N34; Неправильная работа с кодами возврата (Well Done)


Предупреждение PVS-Studio: V547 Expression is always false. linenoise.c 828


static int linenoiseEdit(char *buf, size_t buflen, const char *prompt)  ....  while(1) {    ....    if (fread(seq+2, 1, 1, stdin) == -1) break;    ....  }  ....}

Ошибка в том, что, как и в случае с fwrite, функция fread не возвращает в качестве статуса значение -1.


size_t fread( void *restrict buffer, size_t size, size_t count,              FILE *restrict stream );

Return value

Number of objects read successfully, which may be less than count if an error or end-of-file condition occurs.

If size or count is zero, fread returns zero and performs no other action.

fread does not distinguish between end-of-file and error, and callers must use feof and ferror to determine which occurred.

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


Ошибка N35; Использование оператора || там, где нужен оператор &&


Предупреждение PVS-Studio: V547 Expression is always true. essl_sdio.c 209


esp_err_t essl_sdio_init(void *arg, uint32_t wait_ms){  ....  // Set block sizes for functions 1 to given value (default value = 512).  if (ctx->block_size > 0 || ctx->block_size <= 2048) {    bs = ctx->block_size;  } else {    bs = 512;  }  ....}

Эту ошибку можно, конечно, отнести просто к опечаткам, но, мне кажется, по духу она ближе к логическим ошибкам. Думаю, читатель понимает, что деление ошибок по категориям часто является достаточным условным.


Итак, перед нами всегда истинное условие. Ведь некая переменная всегда или больше 0 или меньше 2048. Из-за этого размер какого-то блока не будет ограничен значением 512.


Правильный вариант кода:


if (ctx->block_size > 0 && ctx->block_size <= 2048) {  bs = ctx->block_size;} else {  bs = 512;}

Ошибка N35 N38; Переменная не изменяется


Предупреждение PVS-Studio: V547 Expression 'depth <= 0' is always false. panic_handler.c 169


static void print_backtrace(const void *f, int core){  XtExcFrame *frame = (XtExcFrame *) f;  int depth = 100;                                          // <=  //Initialize stk_frame with first frame of stack  esp_backtrace_frame_t stk_frame =    {.pc = frame->pc, .sp = frame->a1, .next_pc = frame->a0};  panic_print_str("\r\nBacktrace:");  print_backtrace_entry(esp_cpu_process_stack_pc(stk_frame.pc),                        stk_frame.sp);  //Check if first frame is valid  bool corrupted =    !(esp_stack_ptr_is_sane(stk_frame.sp) &&      (esp_ptr_executable((void *)esp_cpu_process_stack_pc(stk_frame.pc)) ||       /* Ignore the first corrupted PC in case of InstrFetchProhibited */       frame->exccause == EXCCAUSE_INSTR_PROHIBITED));  //Account for stack frame that's already printed  uint32_t i = ((depth <= 0) ? INT32_MAX : depth) - 1;      // <=  ....}

Переменной depth присваивается значение 100, и до момента проверки этой переменной её значение нигде не изменяется. Это весьма подозрительно. Где-то что-то забыли сделать?


Аналогичные случаи:


  • V547 Expression 'xAlreadyYielded == ((BaseType_t) 0)' is always true. event_groups.c 260
  • V547 Expression 'xAlreadyYielded == ((BaseType_t) 0)' is always true. tasks.c 1475
  • V547 Expression 'xAlreadyYielded == ((BaseType_t) 0)' is always true. tasks.c 1520

Ошибка N39; Использование неинициализированного буфера


Предупреждение PVS-Studio: V614 Potentially uninitialized buffer 'k' used. Consider checking the second actual argument of the 'sae_derive_keys' function. sae.c 854


int sae_process_commit(struct sae_data *sae){  u8 k[SAE_MAX_PRIME_LEN];  if (sae->tmp == NULL ||      (sae->tmp->ec && sae_derive_k_ecc(sae, k) < 0) ||      (sae->tmp->dh && sae_derive_k_ffc(sae, k) < 0) ||      sae_derive_keys(sae, k) < 0)    return ESP_FAIL;  return ESP_OK;}

Ошибка в логике. Предположим, что указатели ec и dh являются нулевыми. В этом случае массив k не инициализируется, но функция sae_derive_keys всё равно начнёт его обрабатывать.


Ошибка N40; Всегда ложное условие


Предупреждение PVS-Studio: V547 Expression 'bit_len == 32' is always false. spi_flash_ll.h 371


static inline void spi_flash_ll_set_usr_address(spi_dev_t *dev, uint32_t addr,                                                int bit_len){  // The blank region should be all ones  if (bit_len >= 32) {    dev->addr = addr;    dev->slv_wr_status = UINT32_MAX;  } else {    uint32_t padding_ones = (bit_len == 32? 0 : UINT32_MAX >> bit_len);    dev->addr = (addr << (32 - bit_len)) | padding_ones;  }}

Как легко увидеть, условие bit_len == 32 всегда даст ложный результат. Возможно, выше следовало написать не больше-или-равно (>=), а просто больше (>).


Ошибка N41; Повторное присваивание


Предупреждение PVS-Studio: V519 The '* pad_num' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 46, 48. touch_sensor_hal.c 48


void touch_hal_get_wakeup_status(touch_pad_t *pad_num){  uint32_t touch_mask = 0;  touch_ll_read_trigger_status_mask(&touch_mask);  if (touch_mask == 0) {    *pad_num = -1;  }  *pad_num = (touch_pad_t)(__builtin_ffs(touch_mask) - 1);}

Код явно ошибочен и возможно здесь не хватает оператора else. Я не уверен, но, возможно, код должен выглядеть так:


void touch_hal_get_wakeup_status(touch_pad_t *pad_num){  uint32_t touch_mask = 0;  touch_ll_read_trigger_status_mask(&touch_mask);  if (touch_mask == 0) {    *pad_num = -1;  } else {    *pad_num = (touch_pad_t)(__builtin_ffs(touch_mask) - 1);  }}

Выход за границу массива


Ошибка N42; Неправильная граничная проверка


Предупреждение PVS-Studio: V557 Array overrun is possible. The value of 'frame->exccause' index could reach 16. gdbstub_xtensa.c 132


int esp_gdbstub_get_signal(const esp_gdbstub_frame_t *frame){  const char exccause_to_signal[] =    {4, 31, 11, 11, 2, 6, 8, 0, 6, 7, 0, 0, 7, 7, 7, 7};  if (frame->exccause > sizeof(exccause_to_signal)) {    return 11;  }  return (int) exccause_to_signal[frame->exccause];}

Возможен выход за границу массива на 1 элемент. Для правильной проверки следует использовать не оператор больше, а больше-или-равно:


if (frame->exccause >= sizeof(exccause_to_signal)) {

Ошибка N43; Длинный пример ошибки :)


В рассматриваемой функции выход за границу массива может произойти в двух местах, поэтому и предупреждений анализатора сразу два:


  • V557 Array overrun is possible. The value of 'other_if' index could reach 3. mdns.c 2206
  • V557 Array overrun is possible. The '_mdns_announce_pcb' function processes value '[0..3]'. Inspect the first argument. Check lines: 1674, 2213. mdns.c 1674

Приготовьтесь, это будет сложный случай. Для начала взглянем на следующие именованные константы:


typedef enum mdns_if_internal {    MDNS_IF_STA = 0,    MDNS_IF_AP = 1,    MDNS_IF_ETH = 2,    MDNS_IF_MAX} mdns_if_t;

Обратите внимание, что значение константы MDNS_IF_MAX равно 3.


Теперь взглянем на определение структуры mdns_server_s. Здесь нам важно, что массив interfaces состоит из 3 элементов:


typedef struct mdns_server_s {    struct {        mdns_pcb_t pcbs[MDNS_IP_PROTOCOL_MAX];    } interfaces[MDNS_IF_MAX];    const char * hostname;    const char * instance;    mdns_srv_item_t * services;    SemaphoreHandle_t lock;    QueueHandle_t action_queue;    mdns_tx_packet_t * tx_queue_head;    mdns_search_once_t * search_once;    esp_timer_handle_t timer_handle;} mdns_server_t;mdns_server_t * _mdns_server = NULL;

Это ещё не всё. Нам понадобится заглянуть внутрь функции _mdns_get_other_if. Обратите внимание, что она может вернуть константу MDNS_IF_MAX. Т.е. она может вернуть значение 3.


static mdns_if_t _mdns_get_other_if (mdns_if_t tcpip_if){  if (tcpip_if == MDNS_IF_STA) {    return MDNS_IF_ETH;  } else if (tcpip_if == MDNS_IF_ETH) {     return MDNS_IF_STA;  }  return MDNS_IF_MAX;}

И вот, наконец, мы добрались до ошибок:


static void _mdns_dup_interface(mdns_if_t tcpip_if){    uint8_t i;    mdns_if_t other_if = _mdns_get_other_if (tcpip_if);    for (i=0; i<MDNS_IP_PROTOCOL_MAX; i++) {        if (_mdns_server->interfaces[other_if].pcbs[i].pcb) {        // <=            //stop this interface and mark as dup            if (_mdns_server->interfaces[tcpip_if].pcbs[i].pcb) {                _mdns_clear_pcb_tx_queue_head(tcpip_if, i);                _mdns_pcb_deinit(tcpip_if, i);            }            _mdns_server->interfaces[tcpip_if].pcbs[i].state = PCB_DUP;            _mdns_announce_pcb(other_if, i, NULL, 0, true);          // <=        }    }}

Итак, мы знаем, что функция _mdns_get_other_if может вернуть тройку. Значит переменная other_if может быть равна трём. И вот первый потенциальный выход за границу массива:


if (_mdns_server->interfaces[other_if].pcbs[i].pcb)

Второе место, где опасно используется переменная other_if, - это вызов функции _mdns_announce_pcb:


_mdns_announce_pcb(other_if, i, NULL, 0, true);

Заглянем в эту функцию:


static void _mdns_announce_pcb(mdns_if_t tcpip_if,                               mdns_ip_protocol_t ip_protocol,                               mdns_srv_item_t ** services,                               size_t len, bool include_ip){  mdns_pcb_t * _pcb = &_mdns_server->interfaces[tcpip_if].pcbs[ip_protocol];  ....}

Опять может использоваться индекс 3 для доступа к массиву, состоящего из 3 элементов. А максимальный доступный индекс это двойка.


Нулевые указатели


Ошибка N44 N47; Ошибка очерёдности проверки указателей


Предупреждение PVS-Studio: V595 The 'hapd->wpa_auth' pointer was utilized before it was verified against nullptr. Check lines: 106, 113. esp_hostap.c 106


bool hostap_deinit(void *data){  struct hostapd_data *hapd = (struct hostapd_data *)data;  if (hapd == NULL) {    return true;  }  if (hapd->wpa_auth->wpa_ie != NULL) {    os_free(hapd->wpa_auth->wpa_ie);  }  if (hapd->wpa_auth->group != NULL) {    os_free(hapd->wpa_auth->group);  }  if (hapd->wpa_auth != NULL) {    os_free(hapd->wpa_auth);  }  ....}

Неправильная последовательность проверки указателей:


if (hapd->wpa_auth->group != NULL)....if (hapd->wpa_auth != NULL)

Если указатель hapd->wpa_auth окажется нулевым, то всё плохо. Последовательность действий нужно поменять местами и сделать вложенной:


if (hapd->wpa_auth != NULL){  ....  if (hapd->wpa_auth->group != NULL)  ....}

Аналогичные ошибки:


  • V595 The 'hapd->conf' pointer was utilized before it was verified against nullptr. Check lines: 118, 125. esp_hostap.c 118
  • V595 The 'sm' pointer was utilized before it was verified against nullptr. Check lines: 1637, 1647. esp_wps.c 1637
  • V595 The 'sm' pointer was utilized before it was verified against nullptr. Check lines: 1693, 1703. esp_wps.c 1693

Ошибка N48 N64; Нет проверки указателя после выделения памяти


В целом, в проекте принято проверять, удалось выделить память или нет. Т.е. много кода с подобными проверками:


dhcp_data = (struct dhcp *)malloc(sizeof(struct dhcp));if (dhcp_data == NULL) {  return ESP_ERR_NO_MEM;}

Но местами про проверки забыли.


Предупреждение PVS-Studio: V522 There might be dereferencing of a potential null pointer 'exp'. Check lines: 3470, 3469. argtable3.c 3470


TRex *trex_compile(const TRexChar *pattern,const TRexChar **error,int flags){  TRex *exp = (TRex *)malloc(sizeof(TRex));  exp->_eol = exp->_bol = NULL;  exp->_p = pattern;  ....}

Этот вид ошибки сложнее и опаснее, чем может показаться на первый взгляд. Подробнее эта тема разбирается в статье "Почему важно проверять, что вернула функция malloc".


Другие места, где отсутствуют проверки:


  • V522 There might be dereferencing of a potential null pointer 's_ledc_fade_rec[speed_mode][channel]'. Check lines: 668, 667. ledc.c 668
  • V522 There might be dereferencing of a potential null pointer 'environ'. Check lines: 108, 107. syscall_table.c 108
  • V522 There might be dereferencing of a potential null pointer 'it'. Check lines: 150, 149. partition.c 150
  • V522 There might be dereferencing of a potential null pointer 'eth'. Check lines: 167, 159. wpa_auth.c 167
  • V522 There might be dereferencing of a potential null pointer 'pt'. Check lines: 222, 219. crypto_mbedtls-ec.c 222
  • V522 There might be dereferencing of a potential null pointer 'attr'. Check lines: 88, 73. wps.c 88
  • V575 The potential null pointer is passed into 'memcpy' function. Inspect the first argument. Check lines: 725, 724. coap_mbedtls.c 725
  • V575 The potential null pointer is passed into 'memset' function. Inspect the first argument. Check lines: 3504, 3503. argtable3.c 3504
  • V575 The potential null pointer is passed into 'memcpy' function. Inspect the first argument. Check lines: 496, 495. mqtt_client.c 496
  • V575 The potential null pointer is passed into 'strcpy' function. Inspect the first argument. Check lines: 451, 450. transport_ws.c 451
  • V769 The 'buffer' pointer in the 'buffer + n' expression could be nullptr. In such case, resulting value will be senseless and it should not be used. Check lines: 186, 181. cbortojson.c 186
  • V769 The 'buffer' pointer in the 'buffer + len' expression could be nullptr. In such case, resulting value will be senseless and it should not be used. Check lines: 212, 207. cbortojson.c 212
  • V769 The 'out' pointer in the 'out ++' expression could be nullptr. In such case, resulting value will be senseless and it should not be used. Check lines: 233, 207. cbortojson.c 233
  • V769 The 'parser->m_bufferPtr' pointer in the expression equals nullptr. The resulting value of arithmetic operations on this pointer is senseless and it should not be used. xmlparse.c 2090
  • V769 The 'signature' pointer in the 'signature + curve->prime_len' expression could be nullptr. In such case, resulting value will be senseless and it should not be used. Check lines: 4112, 4110. dpp.c 4112
  • V769 The 'key' pointer in the 'key + 16' expression could be nullptr. In such case, resulting value will be senseless and it should not be used. Check lines: 634, 628. eap_mschapv2.c 634

Ошибка N65, N66; Нет проверки указателя после выделения памяти (показательный случай)


Следующий код содержит точно такую же ошибку, как мы рассматривали выше, но она более показательная и яркая. Обратите внимания, что для выделения памяти используется функция realloc.


Предупреждение PVS-Studio: V701 realloc() possible leak: when realloc() fails in allocating memory, original pointer 'exp->_nodes' is lost. Consider assigning realloc() to a temporary pointer. argtable3.c 3008


static int trex_newnode(TRex *exp, TRexNodeType type){  TRexNode n;  int newid;  n.type = type;  n.next = n.right = n.left = -1;  if(type == OP_EXPR)    n.right = exp->_nsubexpr++;  if(exp->_nallocated < (exp->_nsize + 1)) {    exp->_nallocated *= 2;    exp->_nodes = (TRexNode *)realloc(exp->_nodes,                                      exp->_nallocated * sizeof(TRexNode));  }  exp->_nodes[exp->_nsize++] = n; // NOLINT(clang-analyzer-unix.Malloc)  newid = exp->_nsize - 1;  return (int)newid;}

Во-первых, если функция realloc вернёт NULL, то будет потеряно предыдущее значение указателя exp->_nodes. Возникнет утечка памяти.


Во-вторых, если функция realloc вернёт NULL, то запись значения произойдёт вовсе не по нулевому указателю. Имеется в виду эта строка:


exp->_nodes[exp->_nsize++] = n;

Значение exp->_nsize++ может быть любым, и, если запись произойдёт в какую-то случайную область памяти, доступную для записи, то программа продолжит своё выполнение, как ни в чём не бывало. При этом будут разрушены структуры данных, что приведёт к непредсказуемым последствиям.


Ещё одна такая ошибка: V701 realloc() possible leak: when realloc() fails in allocating memory, original pointer 'm_context->pki_sni_entry_list' is lost. Consider assigning realloc() to a temporary pointer. coap_mbedtls.c 737


Прочие ошибки


Ошибка N67; Лишний или неверный код


Предупреждение PVS-Studio: V547 Expression 'ret != 0' is always false. sdio_slave.c 394


esp_err_t sdio_slave_start(void){  ....  critical_exit_recv();  ret = ESP_OK;  if (ret != ESP_OK) return ret;  sdio_slave_hal_set_ioready(context.hal, true);  return ESP_OK;}

Это странный код, который можно сократить до:


esp_err_t sdio_slave_start(void){  ....  critical_exit_recv();  sdio_slave_hal_set_ioready(context.hal, true);  return ESP_OK;}

Есть здесь ошибка или нет, мне сказать сложно. Возможно, здесь написано совсем не то, что задумано. А возможно, этот код появился в процессе неудачного рефакторинга и, на самом деле, является корректным. В этом случае его действительно достаточно немного упростить, чтобы он смотрелся красивей и понятней. Одно точно этот код заслуживает внимания и проверки автором.


Ошибка N68; Лишний или неверный код


Предупреждение PVS-Studio: V547 Expression 'err != 0' is always false. sdio_slave_hal.c 96


static esp_err_t sdio_ringbuf_send(....){  uint8_t* get_ptr = ....;  esp_err_t err = ESP_OK;  if (copy_callback) {    (*copy_callback)(get_ptr, arg);  }  if (err != ESP_OK) return err;  buf->write_ptr = get_ptr;  return ESP_OK;}

В общем-то, всё то же самое, что и в предыдущем случае. Переменная err является лишней или её забыли изменить.


Ошибка N69; Использование потенциально неинициализированного буфера


Предупреждение PVS-Studio: V614 Potentially uninitialized buffer 'seq' used. Consider checking the first actual argument of the 'strlen' function. linenoise.c 435


void refreshShowHints(struct abuf *ab, struct linenoiseState *l, int plen) {    char seq[64];    if (hintsCallback && plen+l->len < l->cols) {        int color = -1, bold = 0;        char *hint = hintsCallback(l->buf,&color,&bold);        if (hint) {            int hintlen = strlen(hint);            int hintmaxlen = l->cols-(plen+l->len);            if (hintlen > hintmaxlen) hintlen = hintmaxlen;            if (bold == 1 && color == -1) color = 37;            if (color != -1 || bold != 0)                snprintf(seq,64,"\033[%d;%d;49m",bold,color);            abAppend(ab,seq,strlen(seq));                       // <=            abAppend(ab,hint,hintlen);            if (color != -1 || bold != 0)                abAppend(ab,"\033[0m",4);            /* Call the function to free the hint returned. */            if (freeHintsCallback) freeHintsCallback(hint);        }    }}

Буфер seq может быть заполнен, а может быть и не заполнен! Он заполняется только при выполнении условия:


if (color != -1 || bold != 0)  snprintf(seq,64,"\033[%d;%d;49m",bold,color);

Логично предположить, что условие может быть не выполнено, и тогда буфер останется неинициализированным. В этом случае его нельзя использовать для добавления к строке ab.


Чтобы исправить ситуацию, стоит изменить код следующим образом:


if (color != -1 || bold != 0){  snprintf(seq,64,"\033[%d;%d;49m",bold,color);  abAppend(ab,seq,strlen(seq));}

Ошибка N70; Странная маска


Предупреждение PVS-Studio: V547 Expression is always false. tasks.c 896


#ifndef portPRIVILEGE_BIT  #define portPRIVILEGE_BIT ( ( UBaseType_t ) 0x00 )#endifstatic void prvInitialiseNewTask(...., UBaseType_t uxPriority, ....){  StackType_t *pxTopOfStack;  UBaseType_t x;  #if (portNUM_PROCESSORS < 2)  xCoreID = 0;  #endif  #if( portUSING_MPU_WRAPPERS == 1 )    /* Should the task be created in privileged mode? */    BaseType_t xRunPrivileged;    if( ( uxPriority & portPRIVILEGE_BIT ) != 0U )    {      xRunPrivileged = pdTRUE;    }    else    {      xRunPrivileged = pdFALSE;    }  ....}

Константа portPRIVILEGE_BIT имеет значение 0. Поэтому странно использовать его как маску:


if( ( uxPriority & portPRIVILEGE_BIT ) != 0U )

Ошибка N71, Утечка памяти


Предупреждение PVS-Studio: V773 The function was exited without releasing the 'sm' pointer. A memory leak is possible. esp_wpa2.c 753


static int eap_peer_sm_init(void){  int ret = 0;  struct eap_sm *sm;  ....  sm = (struct eap_sm *)os_zalloc(sizeof(*sm));  if (sm == NULL) {    return ESP_ERR_NO_MEM;  }  s_wpa2_data_lock = xSemaphoreCreateRecursiveMutex();  if (!s_wpa2_data_lock) {    wpa_printf(MSG_ERROR, ".......");  // NOLINT(clang-analyzer-unix.Malloc)    return ESP_ERR_NO_MEM;             // <=  }  ....}

Если функция xSemaphoreCreateRecursiveMutex не сможет создать мьютекс, то функция eap_peer_sm_init завершит свою работу и при этом произойдёт утечка памяти. Как я понимаю, следует добавить вызов функции os_free для очистки памяти:


  s_wpa2_data_lock = xSemaphoreCreateRecursiveMutex();  if (!s_wpa2_data_lock) {    wpa_printf(MSG_ERROR, ".......");    os_free(sm);    return ESP_ERR_NO_MEM;  }

Что интересно, компилятор Clang тоже предупреждает об этой ошибке. Однако автор кода почему-то проигнорировал и даже специально подавил соответствующее предупреждение:


// NOLINT(clang-analyzer-unix.Malloc)

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


Заключение


Спасибо за внимание. Как видите, ошибок весьма много. А это ведь был только беглый просмотр неполного отчёта. Надеюсь, Юрий Попов примет эстафету и опишет ещё больше ошибок в своей последующей статье :).


Используйте статический анализатор PVS-Studio регулярно. Это позволит:


  1. Находить многие ошибки на раннем этапе, что существенно сократит расходы на их обнаружение и исправление;
  2. Находя и исправляя глупые опечатки и прочие ляпы с помощью статического анализа, вы высвободите время, которое можно потратить на более высокоуровневый обзор кода и алгоритмов;
  3. Лучше контролировать качество кода новичков и быстрее обучать их писать красивый надежный код;
  4. Если речь идёт о программном обеспечении для встраиваемых устройств, то очень важно устранить как можно больше ошибок до выпуска устройств в эксплуатацию. Поэтому любая дополнительно найденная ошибка с помощью анализатора кода, это здорово. Каждая незамеченная ошибка в программно-аппаратном устройстве потенциально несёт репутационные риски и затраты на обновление прошивок.

Приглашаю скачать и попробовать демонстрационную версию анализатора PVS-Studio. Также напоминаю, что если вы разрабатываете открытый проект или используете анализатор в академических целях, то для таких случаев мы предлагаем несколько вариантов бесплатных лицензий. Не ждите, когда коварный баг съест вашу ногу, начните использовать PVS-Studio прямо сейчас.


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. Espressif IoT Development Framework: 71 Shots in the Foot.

Подробнее..

Разработка защищённого WEB интерфейса для микроконтроллеров

19.04.2021 12:12:03 | Автор: admin

Упомянутые выше параметры вызваны целевым назначением устройства, пока абстрактного устройства.
А WEB интерфейс в данном случае предназначен для настройки параметров и некоего управления в локальной но при этом небезопасной сети со смартфонов или других мобильных гаджетов.

Выбор аппаратной платформы.

Платформа в виде платы должна иметь чип или модуль Wi-Fi, микроконтроллер, желательно SD карту или чип внешней памяти ёмкостью не менее нескольких мегабайт.

Выбор RTOS

В проектируемом устройстве WEB сервер играет вторичную роль. Кроме него будут работать ещё десятки задач. Организовать WEB сервер так чтобы его присутствие не влияло на работу остальной функциональности можно с помощью RTOS.
От middleware RTOS нам нужен развитый набор сервисов синхронизации, вытесняющая многозадачность, надёжный менеджер динамической памяти, быстрая файловая система для SD или eMMC карт, стек TCP/IP протоколов и очень желательно сервисные инструменты отладки.
Все это есть в Azure RTOS. Эта RTOS имеет очень длинную и славную историю с тех пор, когда ещё называлась ThreadX. Она нашла применение в нескольких миллиардах устройств. Её отличает надёжность, компактность и хорошая документация. Долгое время была коммерческой и очень дорогой. Впервые в открытом виде появилась в пакете ПО Synergy от Renesas для микроконтроллеров Synergy на базе ARM Cortex-M4, и сразу получила широкую популярность благодаря исключительно богатой палитре предоставляемого middleware. Теперь портированное middleware Azure RTOS доступно бесплатно и для STM32.

Выбор WEB сервера.

Верхним протоколом, на котором непосредственно базируется WEB сервер является HTTP.
Azure RTOS уже имеет в своём составе HTTP сервер, работающий поверх стека сетевых протоколов Azure RTOS NetX Duo. HTTP Azure, согласно документации, требует всего от 3.0 KB до 9.5 KB FLASH и от 0.5 KB до 2 KB ОЗУ. Ниже будет дана более реалистичная оценка необходимого объема ОЗУ.
Поскольку NetX Duo обладает интерфейсом BSD, то есть возможность достаточно легко адаптировать другие сторонние WEB сервера, хотя тесная связь таких серверов с нижележащим API делает такой выбор малоэффективным.

Свойства WEB сервера реализованного в Azure RTOS:
- Поддержка 2-х типов авторизации: basic (передача пароля в открытом виде) и digest (передача хэша пароля),
- Работа по протоколам HTTP и HTTPS (HTTP защищённый с помощью TLS)
- Чтение файлов страниц с SD карты или другого носителя с FAT32
- Обработка запросов: GET, POST, HEAD, PUT, DELETE
- Поточная передача без указания размера данных: Content-Length
- Работа нескольких подключений одновременно.

Полностью документация на WEB сервер здесь.

Технология работы WEB интерфейса

Способ работы сервера очень простой. Устройство получает через TCP соединение текстовую строку запроса от браузера пользователя сформированную согласно спецификации HTTP.
Устройство также должно ответить строкой по спецификации HTTP. Поэтому WEB сервер в Azure RTOS называется HTTP сервером. HTTP сервер Azure RTOS сразу готов отдавать по запросам браузера файлы с SD карты устройства. Но статические страницы мало интересны.

Самой распространённой технологией в WEB серверах встраиваемых устройств является технология Server Side Includes (SSI). В Azure WEB сервере такой технологии нет. Видимо она считается устаревшей. Действительно, SSI позволял придать динамичность страницам, когда в браузере была отключена возможность исполнять JavaScript. Теперь же JavaScript включён повсеместно, без него сложно добиться адаптируемости.
Более продвинутым считается способ взаимодействия браузера и сервера с помощью технологии AJAX. Т.е. браузер с помощью JavaScript после загрузки страницы в отдельном потоке запрашивает дополнительные данные из устройства. Сервер в устройстве на лету формирует блок запрашиваемых данных и отправляет браузеру в таком же виде как он посылал страницы т.е. по спецификации HTTP.

Никто, конечно, не мешает реализовать и SSI в сервере Azure. Это делается очень просто и выльется в конечном итоге в длинную цепочку операторов if else с проверками на вхождение строк-директив. Но такой подход вызовет слишком тесную связность между страницами и кодом в микроконтроллере.
Гораздо привлекательней выглядит AJAX, причём с передачей файлов в JSON кодировке.
Дело в том, что JSON является форматом внутреннего представления объектов JavaScript в WEB страницах. Нет ничего проще для WEB разработчика чем передавать и принимать данные в формате JSON. В JSON можно выполнить сериализацию буквально всего: параметров, таблиц параметров, баз данных параметров, иерархических деревьев параметров, представления параметров в виде виджетов и прочее.

JSON кодировка довольно простая. JSON в конечном счёте просто строка. Внутри неё нельзя использовать байт 0, поэтому эта строка легко интерпретируется как C-и строка. Одновременно это же обстоятельство позволяет без перекодировки вставлять JSON в HTTP строку ответа.
Немного сложнее дела обстоят с парсингом JSON. Парсинг JSON необходим когда браузер клиента пришлёт устройству запрос например с отредактированными настройками. Простые JSON строки можно парсить и средствами языка C-и, но большие JSON строки уже требуют серьёзных парсеров.
Здесь можно посоветовать проект Jansson. Надо только знать что Jansson активно использует динамическую память, и если передавать в JSON около сотни числовых и строковых параметров, то для парсера может понадобиться около 50 Кбайт ОЗУ, зависит от длины имён переменных и длины самих переменных.

Дополнительные замечания по HTTP серверу Azure.

MIME типы
Сервер Azure HTTP не поддерживает HTTP pipelining, но поддерживает передачу файлов один за другим в одном TCP соединении. WEB страницы как правило содержат ссылки на файлы стилей, скриптов, картинок и прочего. Все эти файлы скачиваются последовательно один за другим браузером клиента. Чтобы получить файл браузер посылает HTTP запрос. Сервер на старте отправки каждого файла отправляет HTTP заголовок, например такой:

HTTP/1.1 200 OKContent-Type: text/htmlConnection: keep-aliveContent-Length: 13210Date: Sun, 17 May 2020 00:59:19 GMTCache-Control: max-age=1Last-Modified: Sun, 17 May 2021 00:59:19 GMT

Здесь имеет большое значение содержание поля Content-Type. Если его указать неправильно браузер может неправильно отобразить страницу. Сервер Azure содержание Content-Type устанавливает в соответствии с расширением передаваемого файла. Но список самих таких известных расширений у сервера небольшой, поэтому в файле nx_web_http_server.c дополняем массив _nx_web_http_server_mime_maps следующим образом:

/* Define basic MIME maps. */static NX_WEB_HTTP_SERVER_MIME_MAP _nx_web_http_server_mime_maps[] ={    {"html",     "text/html"},    {"htm",      "text/html"},    {"txt",      "text/plain"},    {"css",      "text/css"},    {"js",       "application/javascript"},    {"gif",      "image/gif"},    {"jpg",      "image/jpeg"},    {"png",      "image/png"},    {"ico",      "image/x-icon"},};

Способы размещения контента. Для более удобного и быстрого размещения статического контента и сопутствующих файлов на WEB сервере устройства можно применить FTP сервер. FTP сервер имеется в поставке Azure RTOS. Такие среды разработки как Adobe Dreamweaver способны автоматически обновлять контент на целевом FTP сервере содержащем контент WEB сайта.

Ограничение видимости для WEB сервера Azure.
По умолчанию корневой директорией HTTP сервера Azure является корневая директория SD карты.
Чтобы сервер мог считывать файлы только из определенной поддиректории ему надо установить функцией fx_directory_local_path_set локальный путь для задачи сервера. Это удобно делать при первом вызове в callback функции перехватчика запросов сервера. Указатель на callback функцию передаётся в задачу сервера при его создании с помощью функции nx_web_http_server_create. Весь HTTP сервер выполняется в одной задаче, поэтому такой метод работает.

Сжатие контента. Современные браузеры поддерживают компрессию передаваемых страниц и сопутствующих файлов. Компрессия типичных HTML файлов, скриптов и стилей позволяет уменьшит объем передаваемых данных в 3-4 раза. Сервер HTTP Azure не поддерживает компрессию на лету, для микроконтроллеров компрессия может оказаться более продолжительным процессом чем передача несжатого контента. Есть возможность выполнить компрессию статических файлов сразу же при подготовке контента. И хранить файлы на SD карте уже сжатыми, однако HTTP сервер Azure не поддерживает распознавание сжатых файлов и соответствующую модификацию заголовков HTTP.
Проблема не так актуальна как может показаться. Например одностраничные приложения выполненные во фреймворках скачивают статичный контент всего один раз, и гораздо больший трафик может создать динамический контент. Статический контент также можно разместить на сторонних быстрых серверах способных сжимать. И можно конечно же реализовать распознавание сжатых файлов в HTTP сервере собственными силами если сжатие действительно сильно улучшит отзывчивость сервера.

Неточность в исходниках Azure HTTP сервера при базовой авторизации.
Исходные тексты содержат неточность в файле nx_web_http_server.c после строки 3689.
В таком виде базовая авторизация затягивается на 10 сек., поскольку не разрывается соединение после неавторизированного запроса. Туда следует вставить вот такой фрагмент:

if (status == NX_WEB_HTTP_BASIC_AUTHENTICATE)        {          _nx_web_http_server_connection_reset(server_ptr,server_ptr -> nx_web_http_server_current_session_ptr , NX_WEB_HTTP_SERVER_TIMEOUT_SEND);          return;        }

После такого исправления пользователь сразу, а не через 10 сек. увидит диалог с вводом имени и пароля.
Не забыть также вставить этот фрагмент для методов POST и DELETE.

Выбор WEB фреймворка

Выбор фреймворка очень важен. Фреймворки это такое сочетание JavaScript библиотек и файлов стилей CSS. Бывает просто одна библиотека, как например jQuery, бывает библиотека со стилями, как например jQuery UI. Фреймворк значительно облегчает создание адаптируемых, интуитивных и привлекательных WEB страниц, в противовес использованию голого HTML и JavaScript с архаичными стилями. С помощью фреймворков даже самый примитивный WEB сервер выполненный на Arduino, может предоставить удивительно стильный WEB интерфейс. Фреймворки автоматически адаптируют страницы так чтобы они оставались в приемлемом качестве на экранах любых размеров и в любых браузерах.

Но фреймворков очень много. В нашем случае прежде всего интересует сколько файлов и какого размера требуется для фреймворка, насколько удобен фреймворк в использовании неспециалисту, насколько богат и гибок набор стилей и виджетов. Надо помнить что файлы фреймворка скачиваются браузером вместе со страницами WEB интерфейса из устройства. Слишком большой объем фреймворка замедлит время открытия страниц.
Охватить анализом все фреймворки и библиотеки практически невозможною Они возникают как грибы после дождя. Поэтому тут я привёл таблицу как попытку оценить несколько фреймворков которые есть на слуху:

Большинство фреймворков вводят новые элементы синтаксиса разметки в страницы HTML, добиваясь таким образом новых способов выражения дизайна. Это влечёт за собой необходимость дополнительно изучать кроме HTML, JavaScript, CSS и DOM модели ещё и уникальный язык и архитектурный подход фреймворка. Это ещё более усложняет задачу выбора. Поэтому об оптимальном выборе речь не идёт.
Как локальный оптимум выбираем jQuery mobile. Он использует одну из старейших и проверенных библиотек jQuery и интегрирован в WYSIWYG среду разработки Adobe Dreamweaver, что позволяет более удобно конструировать интерфейс по сравнению со способами без WYSIWYG. jQuery mobile как следует из названия предназначен в первую очередь для мобильных устройств, и это как раз то что нужно, поскольку большинство пользователей скорее всего захотят работать с WEB интерфейсом через мобильные устройства

Защита WEB сервера, генерация и инсталляция сертификатов.

Защищённый WEB сервер работает по протоколу HTTPS на порту 433. В этом случае используется протокол шифрования и аутентификации Transport Layer Security (TLS). В Azure RTOS реализован протокол TLS версии 1.3 и также более ранние его версии.

По умолчанию сервер просит от клиента вот такой набор алгоритмов:
Cipher Suite: TLS_RSA_WITH_AES_128_CBC_SHA (0x002f)

Для того чтобы WEB c TLS работал устройство должно ещё иметь в своих недрах сертификат сервера и секретный ключ сервера. Здесь предлагается их хранить во Flash памяти в виде массивов. Сертификат сервера должен содержать подпись доверенного центра (это стоит денег и времени), поэтому проще всего сгенерировать самоподписанный сертификат.

И ключ и сертификат должны быть представлены в двоичном формате DER.

Для примера: ключ занимает 1192 байта, один самоподписанный сертификат занимает 879 байт.

Чтобы сгенерировать самоподписанный сертификат и секретный ключ надо скачать файлы openssl.exe, libeay32.dll, ssleay32.dll и выполнить в их директории несколько команд:

1. Сгенерировать корневой секретный ключ ca.key -
openssl genrsa -out ca.key 2048

2. Сгенерировать корневой сертификат CA.crt -
openssl req -config CA.conf -new -x509 -sha256 -key ca.key -days 3650 -out CA.crt

3. Сгенерировать секретный ключ сервера srv.key
openssl genrsa -out srv.key 2048

4. Сгенерировать запрос сертификата сервера с использованием корневого сертификата srv.csr -
openssl req -new -config Server.conf -out srv.csr -key srv.key

5. Верифицировать и сгенерировать сертификат сервера srv.crt -
openssl x509 -req -in srv.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out srv.crt -days 3650

6. Конвертировать сертификаты и ключ в формат DER -
openssl x509 -in CA.crt -out CA.der -outform DER
openssl x509 -in srv.crt -out srv.der -outform DER
openssl rsa -inform pem -in srv.key -outform der -out srv_key.der

На HTTP сервер устанавливаем файлы srv.der и srv_key.der предварительно сконвертировав их в массивы. Причём у нас остаётся корневой сертификат, благодаря чему даже если злоумышленники взломают устройство и похитят сертификат сервера они не смогут сгенерировать новые сертификаты после того как похищенный сертификат буде аннулирован.

Сколько ОЗУ потребует защищённый WEB сервер

Задача http сервера в Azure RTOS называется TCPSERVER Thread.
Про этому названию её легко найти в списке запущенных задач в отладчике IDE IAR Embedded Workbench for Arm.
По наблюдениям на реальном проекте размер занятого этой задачей стека не превышал 2500 байта.
Движок TLS требует нескольких объёмных структур данных:
Структура NX_SECURE_X509_CERT имеет размер 304 байта
Массив crypto_metadata должен иметь размер не менее 17596 байта на каждую сессию. У нас выбрано до 2 сессий одновременно. Следовательно надо 35192 байта
Массив tls_packet_buffer требует не менее 2000 байт и не более 64 Кбайт. Задаём ему 4000 байта. Его размер определяется размером сертификата сервера. Сертификат может сопровождаться цепочкой сертификатов поэтому надо определять этот размер каждый раз по обстоятельствам.

Итого защищённый сервер потребует не менее 42 Кбайт. Это сравнительно немного. В целом на весь TCP стек с сервером потребуется около 100 Кбайт ОЗУ. Это с учётом буфера пакетов TCP/IP пакетов, JSON парсера для минимальных структур и прочих расходов на протоколы. Если иметь в виду ещё файловую систему, то размер возрастёт на 30-70 Кбайт в зависимости от того какое быстродействие хотим получить.

Пример разработанной страницы WEB интерфейса.

Используя фреймворк jQuery mobile сконструирована вот такая страница:

Если бы фреймворка не было, то эта же страница выглядела бы так:

Содержимое HTML страницы
<!doctype html><html lang="en">  <head>    <meta charset="utf-8">    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">    <title>IoT Logger</title>    <link href="jquery-mobile/jquery.mobile.theme-1.3.0.min.css" rel="stylesheet" type="text/css">    <link href="jquery-mobile/jquery.mobile.structure-1.3.0.min.css" rel="stylesheet" type="text/css">    <script src="jquery-mobile/jquery-1.11.1.min.js">    </script>    <script src="jquery-mobile/jquery.mobile-1.3.0.min.js">    </script>    <style>        @media only screen and (device-width: 300px), only screen and (max-width:300px) {            .css-element {                yourcsscode:;            }        }        input.cb_larger {            width: 25px;            height: 25px;            margin: -12px 0 0 -10px;        }        .tbl_hdr {            font-size: 14px;            font-style: oblique;            font-weight: normal;            text-align: left;            background-color: #DEDEDE        }    </style>  </head>  <body>    <div align="center" data-role="page" id="page">      <div data-role="header" data-position="fixed">        <p id="page_header1" style="padding: 0px 0px 0px 0px; margin: 5px 0px 0px 0px ">IoT Logger</p>        <p style="font-size: 10px; padding: 0px 0px 0px 0px; margin: 0px 0px 5px 5px; text-align: left ">        <span>ID:</span>        <em id="page_header2" style="color:deepskyblue">?</em>        <span id="page_header3" style="margin-left: 10px">?</span>        </p>        <div data-role="navbar" style="width: 400px">          <ul>            <li></li>            <li></li>            <li><li><button type="submit" id="reset_log" onclick="location.assign('device_log.html');" data-theme="b">Show log</button></li></li>          </ul>        </div>      </div>      <div data-role="content">        <div style="overflow:auto;">        <table id="ap_list" style="width:800px">          <caption style="font-size: 16px; font-weight: bold; text-align: left">          Available Access Points list for Station mode          </caption>          <tr>            <th class="tbl_hdr"></th><th class="tbl_hdr">Enable</th>            <th class="tbl_hdr">Access Point SSID</th>            <th class="tbl_hdr">Password</th>            <th class="tbl_hdr">Use DHCP</th>            <th class="tbl_hdr">IP address</th>            <th class="tbl_hdr">IP mask</th>            <th class="tbl_hdr">IP gateway</th>          </tr>          <tr>            <td>1</td>            <td><input type="checkbox" class="cb_larger" id="ssiden1" name="SSIDEN1"></td>            <td><input type="text" id="ssid1" name="SSID1" value=""></td>            <td><input type="password" id="pass1" name="PASS1" value=""></td>            <td><input type="checkbox" class="cb_larger"  id="dhcp1" name="DHCP1"></td>            <td><input type="text"    id="ipaddr1"    name="IPADDR1" value=""></td>            <td><input type="text"    id="ipmask1"    name="IPMASK1" value=""></td>            <td><input type="text" id="ipgateway1" name="IPGATEWAY1" value=""></td>          </tr>          <tr>            <td>2</td>            <td><input type="checkbox" class="cb_larger" id="ssiden2" name="SSIDEN2"></td>            <td><input type="text" id="ssid2" name="SSID2" value=""></td>            <td><input type="password" id="pass2" name="PASS2" value=""></td>            <td><input type="checkbox" class="cb_larger"  id="dhcp2" name="DHCP2"></td>            <td><input type="text"    id="ipaddr2"    name="IPADDR2" value=""></td>            <td><input type="text"    id="ipmask2"    name="IPMASK2" value=""></td>            <td><input type="text" id="ipgateway2" name="IPGATEWAY2" value=""></td>          </tr>          <tr>            <td>3</td>            <td><input type="checkbox" class="cb_larger" id="ssiden3" name="SSIDEN3"></td><td>            <input type="text" id="ssid3" name="SSID3" value=""></td>            <td><input type="password" id="pass3" name="PASS3" value=""></td>            <td><input type="checkbox" class="cb_larger"  id="dhcp3" name="DHCP3"></td>            <td><input type="text"    id="ipaddr3"    name="IPADDR3" value=""></td>            <td><input type="text"    id="ipmask3"    name="IPMASK3" value=""></td>            <td><input type="text" id="ipgateway3" name="IPGATEWAY3" value=""></td>          </tr>          <tr>            <td>4</td>            <td><input type="checkbox" class="cb_larger" id="ssiden4" name="SSIDEN4"></td>            <td><input type="text" id="ssid4" name="SSID4" value=""></td>            <td><input type="password" id="pass4" name="PASS4" value=""></td>            <td><input type="checkbox" class="cb_larger"  id="dhcp4" name="DHCP4"></td>            <td><input type="text"    id="ipaddr4"    name="IPADDR4" value=""></td>            <td><input type="text"    id="ipmask4"    name="IPMASK4" value=""></td>            <td><input type="text" id="ipgateway4" name="IPGATEWAY4" value=""></td>          </tr>          <tr>            <td>5</td>            <td><input type="checkbox" class="cb_larger" id="ssiden5" name="SSIDEN5"></td>            <td><input type="text" id="ssid5" name="SSID5" value=""></td>            <td><input type="password" id="pass5" name="PASS5" value=""></td>            <td><input type="checkbox" class="cb_larger"  id="dhcp5" name="DHCP5"></td>            <td><input type="text"    id="ipaddr5"    name="IPADDR5" value=""></td>            <td><input type="text"    id="ipmask5"    name="IPMASK5" value=""></td>            <td><input type="text" id="ipgateway5" name="IPGATEWAY5" value=""></td>          </tr>        </table>        </div>        <table id="ap_selector">          <tr>            <th>            <div style="text-align:left ; padding-top: 10px">              Selected operational mode            </div>            </th>          </tr>          <tr>            <td>            <input type="radio" id="md_sta" name="wifi_mode" value="0">            <label id="md_sta_lb" for="md_sta">            Station mode <br>            <span style="font-weight: normal; font-size: 14px">            (The device will connect to the someone <br>            of access points listed in the table above)            </span>            </label>            </td>          </tr>          <tr>            <td>            <input type="radio" id="md_ap" name="wifi_mode" value="1">            <label id="md_ap_lb" for="md_ap">            Access Point mode<br>            <span style="font-weight: normal; font-size: 14px">(The device will wait for someone to join with it)</span>            </label>            </td>          </tr>        </table>        <fieldset class="ui-grid-a" style="width: 300px">          <div class="ui-block-a">            <button type="submit" id="submit_data" data-theme="a">Submit</button>          </div>          <div class="ui-block-b">            <button type="submit" id="reset_dev" data-theme="e">Reset device</button>          </div>        </fieldset>        <div data-role="footer" data-position="fixed">          <p id="status_msg" style="font-weight:normal" hidden=true>          </p>        </div>      </div>    </div>  </body>  <script>    // Глобальные переменные для хранения полученной из дивайса информации    var params;    var dev;    var apl;    var wrl;    // Вывод строки статуса выполнения операции по нажатию кнопок    function Show_cmd_status(data, status)    {      $("#status_msg").text("Data sending status: " + status); $("#status_msg").show();      setTimeout(function() { $("#status_msg").hide(); }, 2000);    }    // Функция извлечение интересующих параметров из объекта data и помещение их в поля ввода    function Data_accept(data, status)    {      params = data;                  var v = params.find(function(item, i) { if (item["Client_AP_list"] != undefined) return true; });      apl = v["Client_AP_list"];      if (apl != undefined)      {        $("#ssid1").val(apl[0][1]);        $("#pass1").val(apl[0][2]);        $("#ipaddr1").val(apl[0][4]);        $("#ipmask1").val(apl[0][5]);        $("#ipgateway1").val(apl[0][6]);        $("#ssid2").val(apl[1][1]);        $("#pass2").val(apl[1][2]);        $("#ipaddr2").val(apl[1][4]);        $("#ipmask2").val(apl[1][5]);        $("#ipgateway2").val(apl[1][6]);        $("#ssid3").val(apl[2][1]);        $("#pass3").val(apl[2][2]);        $("#ipaddr3").val(apl[2][4]);        $("#ipmask3").val(apl[2][5]);        $("#ipgateway3").val(apl[2][6]);        $("#ssid4").val(apl[3][1]);        $("#pass4").val(apl[3][2]);        $("#ipaddr4").val(apl[3][4]);        $("#ipmask4").val(apl[3][5]);        $("#ipgateway4").val(apl[3][6]);        $("#ssid5").val(apl[4][1]);        $("#pass5").val(apl[4][2]);        $("#ipaddr5").val(apl[4][4]);        $("#ipmask5").val(apl[4][5]);        $("#ipgateway5").val(apl[4][6]);        if (apl[0][0] == 1) $("#ssiden1").attr("checked", true);        if (apl[1][0] == 1) $("#ssiden2").attr("checked", true);        if (apl[2][0] == 1) $("#ssiden3").attr("checked", true);        if (apl[3][0] == 1) $("#ssiden4").attr("checked", true);        if (apl[4][0] == 1) $("#ssiden5").attr("checked", true);        if (apl[0][3] == 1) $("#dhcp1").attr("checked", true);        if (apl[1][3] == 1) $("#dhcp2").attr("checked", true);        if (apl[2][3] == 1) $("#dhcp3").attr("checked", true);        if (apl[3][3] == 1) $("#dhcp4").attr("checked", true);        if (apl[4][3] == 1) $("#dhcp5").attr("checked", true);      }      v = params.find(function(item, i) { if (item["Parameters"] != undefined) return true; });      if (v != undefined)      {        wrl = v["Parameters"].find(function(item, i) { if (item[0] == "wifi_role") return true; });        if (wrl != undefined)        {          if (wrl[1] == 0)          {            $("#md_sta_lb").click();          } else          {            $("#md_ap_lb").click();          }        }      }      v = params.find(function(item, i) { if (item["Device"] != undefined) return true; });      dev = v["Device"];      if (dev["HW_Ver"] != undefined)      {        $("#page_header1").html(dev["HW_Ver"]);        $("#page_header2").html(dev["CPU_ID"]);        $("#page_header3").html(dev["CompDate"] + " " + dev["CompTime"]);      }    }    // Считываем параметры из полей ввода, записываем их в объект JSON    // Сериализируем объект и отправляем устройству методом POST    function Data_send()    {            apl[0][1] = $("#ssid1").val();      apl[0][2] = $("#pass1").val();      apl[0][4] = $("#ipaddr1").val();      apl[0][5] = $("#ipmask1").val();      apl[0][6] = $("#ipgateway1").val();      apl[1][1] = $("#ssid2").val();      apl[1][2] = $("#pass2").val();      apl[1][4] = $("#ipaddr2").val();      apl[1][5] = $("#ipmask2").val();      apl[1][6] = $("#ipgateway2").val();      apl[2][1] = $("#ssid3").val();      apl[2][2] = $("#pass3").val();      apl[2][4] = $("#ipaddr3").val();      apl[2][5] = $("#ipmask3").val();      apl[2][6] = $("#ipgateway3").val();      apl[3][1] = $("#ssid4").val();      apl[3][2] = $("#pass4").val();      apl[3][4] = $("#ipaddr4").val();      apl[3][5] = $("#ipmask4").val();      apl[3][6] = $("#ipgateway4").val();      apl[4][1] = $("#ssid5").val();      apl[4][2] = $("#pass5").val();      apl[4][4] = $("#ipaddr5").val();      apl[4][5] = $("#ipmask5").val();      apl[4][6] = $("#ipgateway5").val();      if ($("#ssiden1").prop("checked") == true) apl[0][0] = 1;      else apl[0][0] = 0;      if ($("#ssiden2").prop("checked") == true) apl[1][0] = 1;      else apl[1][0] = 0;      if ($("#ssiden3").prop("checked") == true) apl[2][0] = 1;      else apl[2][0] = 0;      if ($("#ssiden4").prop("checked") == true) apl[3][0] = 1;      else apl[3][0] = 0;      if ($("#ssiden5").prop("checked") == true) apl[4][0] = 1;      else apl[4][0] = 0;      if ($("#dhcp1").prop("checked") == true) apl[0][3] = 1;      else apl[0][3] = 0;      if ($("#dhcp2").prop("checked") == true) apl[1][3] = 1;      else apl[1][3] = 0;      if ($("#dhcp3").prop("checked") == true) apl[2][3] = 1;      else apl[2][3] = 0;      if ($("#dhcp4").prop("checked") == true) apl[3][3] = 1;      else apl[3][3] = 0;      if ($("#dhcp5").prop("checked") == true) apl[4][3] = 1;      else apl[4][3] = 0;      if ($("#md_sta").prop("checked") == true) wrl[1] = "0";      else wrl[1] = "1";      // Преобразуем объект JavaScript в строку JSON      json_str = JSON.stringify(params);      // Отправляем устройству методом POST строку JSON с отредактированными параметрами      $.post("data.json", json_str, Show_cmd_status);    }    // Перейти на страницу отображения лога     function Show_log()    {      location.assign("device_log.html");    }    // По клику на кнопке с id = "submit_data" посылаем отредактированные данные обратно устройству      $("#submit_data").click(Data_send);    // По клику на кнопке с id = "reset_dev" посылаем команду сброса устройства    $("#reset_dev").click(function() {$.post("reset", "", Show_cmd_status)});    // Здесь сразу запрашиваем у устройства по протоколу AJAX текущие настройки     $.get("data.json", Data_accept);  </script></html>

Как видно в заголовке страница вместе с файлом HTML будут скачаны еще 4-е файла принадлежащие фреймворку. На самом деле будет скачано больше, поскольку файлы фреймворка скачивают сами ещё несколько нужных им файлов.

Содержимое JSON файла c данными
[ {  "Device": {   "CPU_ID": "5301646835393735C86643535454227D",   "SW_Ver": "V0.0.2",   "HW_Ver": "IoT Logger 1.0.0",   "CompDate": "Apr 16 2021",   "CompTime": "13:03:54"  } }, {  "Parameters": [   [    "leds_mode",    "1"   ],   [    "wifi_role",    "1"   ]  ] }, {  "Client_AP_list": [   [    1,    "SSID",    "PASS",    0,    "192.168.1.1",    "255.255.255.0",    "192.168.1.254"   ],   [    1,    "SSID",    "PASS",    0,    "192.168.1.1",    "255.255.255.0",    "192.168.1.254"   ],   [    1,    "SSID",    "PASS",    0,    "192.168.1.1",    "255.255.255.0",    "192.168.1.254"   ],   [    1,    "SSID",    "PASS",    0,    "192.168.1.1",    "255.255.255.0",    "192.168.1.254"   ],   [    1,    "SSID",    "PASS",    0,    "192.168.1.1",    "255.255.255.0",    "192.168.1.254"   ]  ] }]

И наконец посмотрим отзывчивость WEB интерфейса на примере платформы Sinergy

Диаграмма времени закачки страницы по протоколу HTTP Диаграмма времени закачки страницы по протоколу HTTP Диаграмма времени закачки страницы по протоколу HTTPSдерДиаграмма времени закачки страницы по протоколу HTTPSдер

Здесь надо отметить что для Sinergy у Azure RTOS есть драйвер аппаратного криптографического модуля, поэтому скорость HTTP и HTTPS отличаются всего лишь в два раза.
В случае программной реализации отличие будет более значительным.
Как обстоят дела с драйверами для криптографической периферии STM32 ещё предстоит выяснить.

Итак, загрузка адаптированной под мобильные гаджеты, зашифрованной с помощью TLS страницы вместе со всеми файлами фреймворка и файлом данных JSON в среднем займет не более 800 миллисекунд, что вполне приемлемо для интерфейса встраиваемого устройства.

Подробнее..

Заказная разработка контроллеров для IIoT

04.12.2020 20:21:06 | Автор: admin
В большинстве проектов промышленного Интернета вещей (IIoT) заказчики используют контроллеры, с которыми работали раньше или рекомендованные поставщиками систем верхнего уровня. При этом счет IIoT контроллеров на рынке, из которых можно выбирать, идет на тысячи.


Не всем известна опция разработки оборудования на заказ. Для большинства промышленных контроллеров не нужен дальний космос в виде уникального функционала или дизайнерского корпуса. При удачном выборе подрядчика, имеющего необходимые наработки, за 2-3 месяца можно сделать опытные образцы, а еще через пару месяцев начать серийное производство. Разработка может окупиться за счет использования недорогой элементной базы и реализации нужного количества функций под конкретный проект. Комплекс оборудования на объекте будет состоять из минимального количества устройств (часто одного), а значит будет меньше работ по сборке, установке и пуско-наладке.

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


Шаг 1. А есть ли готовое?


Можно серьезно сэкономить, подобрав оборудование под проект, например, на этом сервисе.

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

Поиск на данном этапе может оказаться неудачным. Тогда он превратится в обзор рынка, что точно не повредит. Особенно, если в итоге будет принято решение начать разработку собственного устройства. А удачные технические решения, применяемые в готовых изделиях, стоит необходимо брать за основу. Ведь по-настоящему классных идей не так много.

Шаг 2. Выбор подрядчика: КогдаСколькоПочем и защита от итальянской забастовки


Если решено разработать собственное оборудование и под боком нет исполнителей, пора искать подрядчика. Оптимально начать с компаний, уже выпускающих то, что нужно. Важно понять, нужны ли права на разработку, исходники конструкторской документации (КД) и встроенного программного обеспечения (ВПО), либо достаточно иметь эксклюзив (на территорию внедрения, дизайн, ). Компании-разработчики могут отказаться от работы на условиях, интересующих заказчика, либо предложить заградительные цены. Самым дешевым вариантом может оказаться не полноценная разработка, а доработка или OEM поставка оборудования под вашим брендом. Однако, и этот вариант несет в себе риск можно вырастить себе конкурента.

Если договориться не получилось, надо искать подрядчика по рекомендациям, либо в Сети по запросу контрактная разработка электроники. В помощь к работе с кандидатами вопросы из чек-листа ниже.

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

Со встречи с потенциальным партнером надо уходить с ответом на вопрос когдасколькопочем?. Об этом часто забывают. Ожидания заказчика (цены, сроки) могут оказаться сильно меньше запросов подрядчика. Если в итоге их удалось синхронизировать, можно переходить к согласованию:

  • тезисной концепции: ТЗ крупными мазками, сроки, стоимость разработки,
  • цен на изделия в партиях, сроков поставки,
  • условий договора (права, исходники, эксклюзив).

Не все можно зафиксировать в договоре. Подрядчик может объявить итальянскую забастовку, за которую его нельзя будет наказать. Также могут возникнуть непредвиденные сложности или новые требования, по которым придется работать больше, чем договаривались. Для того, чтобы защититься от перечисленных проблем, можно использовать простые принципы:

  • При выборе подрядчика обязателен человеческий контакт. Идеальных заказчиков и подрядчиков не бывает, но совершенно реально найти партнера с которым будет комфортно работать именно вам.
  • Отлично, если подрядчик проявляет инициативу и искренне болеет за дело (можно понять по ответу на вопрос как решали серьезные проблемы в прошлом?).
  • Подрядчик должен быть управляемым. Соответственно, он должен слушать и слышать заказчика, а не только себя.

Чек-лист выбора подрядчика


Требование Оценка
Предварительное попадание в сроки и бюджет
Человеческий контакт достигнута ли синергия с заказчиком или постоянно на разных волнах
Готовность предоставления исходников (КД, тексты ПО), прав, эксклюзива по продаже (по необходимости)
Соответствие портфолио / услуг (опыта подрядчика) задаче
Наличие достойных примеров борьбы с проблемами
Наличие необходимых специалистов в команде
Наличие шаблонов документов (договор, ТЗ, руководства, )
Увлеченность делом: интересные идеи, инициативность, ответственность
Желание разобраться в специфике, умение слушать

Шаг 3. Согласование ТЗ на IIoT контроллер


Только полное соответствие результата разработки ТЗ является основанием для приемки работ и окончательной оплаты. Внесение изменений в требования после заключения договора может быть болезненным, поэтому очень важно ответственно поработать над текстом ТЗ с обеих сторон. Цена ошибки на последующих этапах будет существенно выше.

Согласование типа конструктива


Как известно, устройство должно работать не в принципе, а в корпусе. Старт обсуждения ТЗ с корпуса, позволит обеим сторонам представить с первых минут, что за устройство получится в итоге.

Для каждого применения и проекта оптимален свой форм-фактор:

  • Для энергетики, промышленной автоматики и учета ресурсов используют корпуса на DIN рейку 35мм. Пожалуй, это самый популярный формат для промышленного IoT, однако, не является серебряной пулей для всех случаев;
  • Для систем связи используют модули в 19 стойку. Они бывают различной высоты, которую измеряют в U (44,45 мм).
  • В 19 стойку также могут устанавливать крейты (или кассеты). В них вставляют нужное количество модулей, конструктив которых объединяют понятием Евромеханика.
  • Для контроля доступа и размещения отдельно стоящих компактных устройств применяют корпуса в настольном и/или настенном исполнении,
  • Существует специализированные корпуса, в том числе: антивандальные, защищенные от воды и пыли (по международным кодам IP), приборные и т.п.
  • Наконец, бывают устройства без корпуса, устанавливаемые внутрь бОльших устройств, либо устройства, состоящие из платы и передней панели (в частности, модули Евромеханики).


Часто конструктив выбирают по аналогии с готовыми устройствами (с рынка). В некоторых случаях это является ошибкой, поскольку дорогой фирменный корпус (с обработкой, маркировкой, системой соединителей и пр.) может стоить до 50% от себестоимости изделия. Для справки: аналогичная доля для бюджетного корпуса может составлять менее 5%.

Выбор процессорного ядра


В бюджетных устройствах обычно используют однокристальные микроконтроллеры (MCU), с оперативной памятью (RAM) и ПЗУ (Flash) в одном корпусе. Большинство устройств тянет компактную операционную систему (ОС) типа FreeRTOS или TNKernel, а может работать и без ОС. Будем называть их RTOS контроллерами.

В более мощных контроллерах используется процессор (CPU) с внешними микросхемами RAM и Flash. Большинство таких устройств использует различные версии ОС Linux (Linux контроллеры) или менее распространенные ОС типа VхWorks или Windows CE (здесь не рассматриваем). Сделать плату на современном процессоре не так просто: на плате от 4-х до 10-ми слоев нужно расположить несколько BGA корпусов с достаточно строгими требованиями к питанию, геометрии и длинам дорожек. Для упрощения жизни разработчиков предлагаются сотни процессорных модулей, которые могут быть выполнены в виде дочерней платы с разъемами или краевыми контактами под пайку (см. ниже).


Также на рынке появляются микросхемы System on chip (SoC), содержащие процессор и память большого объема, достаточную для работы Linux. Разводка SoC значительного проще, чем набора CPU+RAM+FLASH. Кроме этого, SoC-и могут быть очень бюджетными.

Ниже приведены типовые характеристики и цены для нескольких примеров процессорных ядер от ARM, которые могут быть использованы в IIoT контроллерах.


Часто оправдано использование в одном контроллере двух процессоров: мощного для ресурсоемких приложений и небольшого однокристального для простых приложений реального времени.


Согласование требований к системе питания


В зависимости от типа объектов определяются требования к блоку питания, который может быть, как внешним, так и встроенным в устройство:

  • домашнее/офисное применение, энергетика ~220/380В,
  • телеком 3672В (станционное питание) и PoE,
  • промышленная автоматизация 18...36В,

Изолированный блок питания часто выходит из строя из-за высыхания электролитических конденсаторов. Был случай, когда массовая поломка оборудования произошла буквально через полгода после начала эксплуатации. По этой причине подрядчик должен иметь опыт по разработке систем питания и знать их болевые точки, либо использовать дорогие преобразователи, качество которых гарантировано именем производителя.

Для многих задач требуется резервное питание от одной минуты (краткосрочный резерв, чтобы послать сигнал о пропадании питания) до нескольких часов/дней (охрана и промышленная безопасность). Для реализации краткосрочного резервирования сегодня популярны суперконденсаторы со сроком жизни до 15-ти лет и устойчивостью к отрицательным температурам. Для длительного резервирования нужны аккумуляторы, как правило, на основе лития.


Для всех российских устройств необходимы сертификаты ЕАС на электробезопасность и электромагнитную совместимость. Для прохождения испытаний нужно уметь проектировать фильтры и топологию плат, а также правильно выбирать компоненты.

Порты связи


Распространенные интерфейсы, используемые в IIoT контроллерах, показаны в таблице ниже. Выбор типов и количеств под задачу и бюджет.

Для связи через IP сеть Для связи через промежуточный HUB Для локальной связи на объекте
*Wired Ethernet *RS485 / 422 RS232
Cell 2G/GPRS 4G/LTE *CAN USB
Optical Ethernet PLC (G3, Prime) 1-wire, s-wire (для цифровых датчиков)
Optical GPON Radio: LoRA, NB-Fi (Rus), UNB Radio: Zigbee, 6loWPAN, ISM 433/868/2400 Mhz

* может использоваться и для локальной связи c оборудованием на объекте

Входы и выходы


Для подключения датчиков контроллеры оснащаются дискретными, счетными и аналоговыми входами. Аналоговые входы могут быть потенциальными (например, на 0..10VDC или изолированными на 220VAC), либо токовыми (4..20mA, NAMUR, пожарными). Для реализации выходов используют реле (обычные и полупроводниковые, например, оптосимисторы), а также транзисторы, включенные по схеме открытый коллектор (ОК).

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

Для уменьшения габаритов и экономии разъемов используют универсальные порты, выполняющие разные функции в зависимости от настроек, и использующие одни и те же контакты. Например, дискретный вход с функцией выхода ОК.

Индикация


Продолжительное время в IIoT устройствах было достаточно использовать несколько светодиодов. В более продвинутых контроллерах использовали телевизоры линейки семисегментных LED индикаторов, электролюминесцентные, текстовые или графические LCD индикаторы. Но от телевизоров чаще все-таки отказывались из-за их дороговизны и небольшой пользы при эксплуатации.

Сегодня телевизоры стали модными буквально везде: от автомобилей до умных домов. Появляется все больше конкурсов, в которых наличие экрана обязательно и для IIoT устройств.


Хорошая новость в том, что стоимость LCD или OLED индикаторов уменьшается, а мощность процессоров, необходимая для графического вывода, растет. По этой причине телевизор перестает быть дорогой опцией.

Модельный ряд


Хорошей практикой является разработка не одного устройства, а целой линейки. В минимуме для этого требуется разработка всего одной платы, рассчитанной на максимальную комплектацию. Другие, более бюджетные, версии будут паяться на той же плате, но из меньшего количества деталей. Часть платы будет пустой, но это не проблема (текстолит стоит недорого).

Советую добавить этот пункт в ТЗ.

Требования к встроенному программному обеспечению


Разработка ВПО может занимать до 80% от времени реализации всего проекта. Поскольку данный пост про железо, ограничусь перечислением основных функций, которые должны быть реализованы практически в любом IIoT контроллере:

  • Обмен данными с системой верхнего уровня, в том числе, передача экстренных оповещений;
  • Обмен данными с нижестоящими устройствами и датчиками;
  • Управление исполнительными механизмами;
  • Обновление ВПО с защитой от перебоев питания и связи;
  • Хранение настроек и их восстановление;
  • Администрирование устройства с защитой от несанкционированного доступа;
  • Ведение журналов событий;
  • Анализ данных и формирования воздействий и событий (Edge логика);
  • Синхронизация системного времени;

Проектная работа с подрядчиком


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

Разработка нового IIoT контроллера это сравнительно небольшой проект, для успеха которого, тем не менее, нужна слаженная работа сотрудников заказчика и исполнителя. Со стороны заказчика сразу нужен руководитель проекта, а позже инженер(ы) тестирования (без независимого тестирования продукта не сделать). Более того, обычно разработка передается на условиях As is. После подписания актов и оплаты работ доказать необходимость исправления по гарантии сложно.

Про проектное управление написаны сотни книг. В разрезе разработки контроллеров я выделяю следующие обязательные моменты:

  • В начале работ нужен устав проекта (цели, проектная команда, деньги, сроки, ), и календарный план;
  • В процессе реализации необходим периодический контроль календарного плана и его корректировка;
  • Если в процессе работ внедряются хорошие идеи или новые требования, которых не было в ТЗ, то их надо вносить в реестр изменений. Ценность таких идей может значительно превышать дополнительную стоимость (обычно до 10% от первоначальной стоимости работ).
  • По результатам испытаний нужны протоколы, которые будут использоваться, как основание для доработок и оплат.
  • Хорошей практикой является подведение итога по проекту после его завершения. Часто этот момент пропускают, хотя итоговый отчет прекрасная страховка от граблей в будущих аналогичных проектах, а также основание для наказания невиновных награждения героев.

Хорошие формы документов по проектному управлению здесь.

Заключение


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


Одна из таких возможностей для IIoT проекта применение заказных контроллеров. Для ее реализации заказчику нужно пройти путь из трех быстрых шагов:

  • анализ рынка,
  • выбор подрядчика (например, нас),
  • согласование ТЗ.

Далее предстоит работа с выбранным подрядчиком: разработка опытных образцов, производство и поддержка. Цены на заказные контроллеры, их монтаж и пуско-наладку могут быть значительно ниже по сравнению с использованием готового оборудования. Дополнительными ценностями для заказчика будут:

  • развитие собственного бренда,
  • реализация специфического набора аппаратных функций и
  • получение статуса отечественного производителя (актуально для участия в конкурсах).
Подробнее..

Перевод Интригующие возможности С 20 для разработчиков встраиваемых систем

07.01.2021 08:23:49 | Автор: admin

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

Используя соответствующие возможности С++ можно написать код, который не будет уступать по своей эффективности коду аналогичного приложения, написанного на Си, а в ряде случаев он будет даже более эффективным, поскольку рядовому программисту может быть достаточно утомительно реализовывать некоторый функционал на Си, который гораздо проще реализуется с помощью дополнительных возможностей С++.

С++20 это седьмая итерация С++, которой предшествовали, например, С++17, С++14 и С++11. Каждая итерация добавляла новые функциональные возможности и при этом влияла на пару функций, добавленных ранее. Например, ключевое слово auto С++14.

Прим. перев.:

В С++14 были введены новые правила для ключевого слова auto. Ранее выражения auto a{1, 2, 3}, b{1};были разрешены и обе переменные имели тип initializer_list<int>. В С++14 auto a{1, 2, 3}; приводит к ошибке компиляции, а auto b{1};успешно компилируется, но тип переменной будет int а не initializer_list<int>. Эти правила не распространяются на выражения auto a = {1, 2, 3}, b = {1};, в которых переменные по-прежнему имеют тип initializer_list<int>.

В С++11 были внесены значительные изменения, и она является наиболее распространённой версией стандарта, которая применяется во встраиваемых системах, так как разработчики встраиваемых систем не всегда используют самый современный инструментарий. Проверенные и надежные решения имеют решающее значение при разработке платформы, которая может работать десятилетиями.

Так уж получилось, что в С++20 было добавлено довольно много новых функциональных возможностей. Новые итераторы и поддержка форматирования строк будут полезны с новой библиотекой синхронизации. У всех на слуху оператор трехстороннего сравнения, он же оператор космический корабль. Как и большинство функциональных возможностей, описание этого оператора выходит за рамки данной статьи, но если кратко, то сравнение типа x < 20, сначала будет преобразовано в x.operator<=>(20) < 0. Таким образом, поддержка сравнения, для обработки операторов типа <, <=, >= и >, может быть реализована с помощью одной или двух операторных функций, а не дюжины. Выражение типа x == yпреобразуется в operator<=>(x, y) == 0.

Прим. перев.:

Более подробную информацию об операторе космический корабль смотрите в статье @Viistomin Новый оператор spaceship (космический корабль) в C++20

Но прейдём к более интересным вещам и рассмотрим функциональные возможности С++20 которые будут интересны разработчикам встраиваемых систем, а именно:

Константы этапа компиляции

Разработчикам встраиваемых систем нравится возможность делать что-то на этапе компиляции программы, а не на этапе её выполнения. С++11 добавил ключевое слово constexpr позволяющее определять функции, которые вычисляются на этапе компиляции. С++20 расширил эту функциональность, теперь она распространяется и на виртуальные функции. Более того, её можно применять с конструкциями try/catch. Конечно есть ряд исключений.

Новое ключевое слово consteval тесно связано с constexpr что, по сути, делает его альтернативой макросам, которые наряду с константами, определенными через директиву #define, являются проклятием Си и С++.

Сопрограммы

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

Прим. перев.:

Более подробную информацию о сопрограммах и о том для чего они нужны, смотрите в статье @PkXwmpgN C++20. Coroutines и в ответе на вопрос, заданный на stackoverflow.

С++20 поддерживает функционал сопрограмм с помощью coroutine_traits и coroutine_handle. Сопрограммы являются стеконезависимыми и сохраняют свое состояние в куче. Могут передавать управление и при необходимости предоставлять результат своей работы себе же, когда выполнение сопрограммы возобновляется.

Концепты и ограничения

Концепты и ограничения были экспериментальной функциональной возможностью С++17, а теперь являются стандартной. Поэтому можно предположить, что эксперимент прошел успешно. Если вы надеялись на контракты Ada и SPARK, то это не тот случай, но концепты и ограничения C++20 являются ценными дополнениями.

Плюсом ограничений является то, что они позволяют обнаруживать ошибки на этапе компиляции программы. Ограничения могут накладываться на шаблоны классов, шаблоны функций и обычные функции. Ограничение это предикат (прим. перев.: утверждение), который проверяется на этапе компиляции программы. Именованный набор таких определений называется концептом. Пример кода, взятый с cppreference.com, показывает синтаксис и семантику этой функциональной возможности:

#include <string>#include <cstddef>#include <concepts>using namespace std::literals; // Объявляем концепт "Hashable", которому удовлетворяет// любой тип 'T' такой, что для значения 'a' типа 'T',// компилируется выражение std::hash{}(a) и его результат преобразуется в std::size_ttemplate <typename T>concept Hashable = requires(T a) {    { std::hash{}(a) } -> std::convertible_to<std::size_t>;}; struct meow {}; template <Hashable T>void f(T); // Ограниченный шаблон функции С++20 // Другие способы применения того же самого ограничение:// template<typename T>//    requires Hashable<T>// void f(T); // // template <typename T>// void f(T) requires Hashable<T>;  int main() {  f("abc"s); // OK, std::string удовлетворяет Hashable  f(meow{}); // Ошибка: meow не удовлетворяет Hashable}

Концепты и ограничения являются наиболее значимой функциональной возможностью С++20, которая ориентирована на создание безопасных и надежных приложений, для таких областей, как транспорт, авиация и медицина.

Модули

Повсеместный #include заменяется модулями. Ключевые слова import и export находятся там, где когда-то находился #include.

Директива #include упростила написание компиляторов. Когда компилятор её встречает он просто выполняет чтение файла, указанного в ней. По сути получается, что включаемый файл как бы является частью оригинального файла. Такой подход зависит от порядка, в котором объединяются файлы. В целом это работает, однако такое решение плохо масштабируется при работе с большими и сложными системами, особенно с учетом всех взаимосвязей, которые могут принести шаблоны и классы С++.

Системы сборки на основе модулей широко распространены, и такие языки, как Java и Ada уже используют подобную систему. По сути, модульная система позволяет загружать модули только один раз на этапе компиляции, даже если они используются несколькими модулями. Порядок импортирования модулей не важен. Более того, модули могут явно предоставлять доступ к своим внутренним компонентам, что невозможно при применении директивы #include.

Существующие стандартные библиотеки теперь будут поддерживать включение в виде модулей, но для обеспечения обратной совместимости поддержка директивы #include остается. Модули хорошо работают с существующей поддержкой пространства имен.

Я по-прежнему склонен программировать на Ada и SPARK, но новые изменения в C++20 делают его лучшей платформой C++ для разработки безопасного и надежного программного обеспечения. На самом деле мне нужно поработать с новыми функциями C++20, чтобы увидеть, как они влияют на мой стиль программирования. Я старался избегать сопрограмм, поскольку они были нестандартными. Хотя концепты и ограничения будут немного сложнее, они будут более полезны в долгосрочной перспективе.

Положительным моментом является то, что компиляторы с открытым исходным кодом поддерживают C++20, а также то, что в сети интернет появляется всё больше коммерческих версий компиляторов, поддерживающих С++20.

Подробнее..

Разработчики встраиваемых систем не умеют программировать

02.05.2021 18:15:06 | Автор: admin

Заслуженно распространена точка зрения, что типичный разработчик высокоуровневого прикладного ПО настолько свыкся с доступностью системных ресурсов и мягкостью требований реального времени, что ожидать от него оптимизации кода в угоду снижения ресурсоёмкости приложения можно лишь в крайних случаях, когда этого прямо требуют интересы бизнеса. Это и логично, ведь в задачах прикладной автоматизации самым дорогим ресурсом остаётся ресурс человеческий. Более того, снижение когнитивных затрат на возню с байтами оставляет внимание разработчика свободным для задач первоочередной важности, таких как обеспечение функциональной корректности программы.

Редко когда речь заходит об обратной проблеме, имеющей место в куда более узких кругах разработчиков встраиваемых систем, включая системы повышенной отказоустойчивости. Есть основания полагать, что ранний опыт использования MCS51/AVR/PIC оказывается настолько психически травмирующим, что многие страдальцы затем продолжают считать байты на протяжении всей карьеры, даже когда объективных причин для этого не осталось. Это, конечно, не относится к случаям, где жёсткие ценовые ограничения задают потолок ресурсов вычислительной платформы (микроконтроллера). Но это справедливо в случаях, где цена вычислительной платформы в серии незначительна по сравнению со стоимостью изделия в целом и стоимостью разработки и верификации его нетривиального ПО, как это бывает на транспорте и сложной промышленной автоматизации. Именно о последней категории систем этот пост.

Обычно здесь можно встретить упрёк: "Ты чё пёс А MISRA? А стандарты AUTOSAR? Ты, может, и руководства HIC++ не читал? У нас тут серьёзный бизнес, а не эти ваши побрякушки. Кран на голову упадёт, совсем мёртвый будешь." Тут нужно аккуратно осознать, что адекватное проектирование ПО и практики обеспечения функциональной корректности в ответственных системах не взаимоисключающи. Если весь ваш софт проектируется по V-модели, то вы, наверное, в этой заметке узнаете мало нового хотя бы уже потому, что ваша методология содержит пункт под многозначительным названием проектирование архитектуры. Остальных эмбедеров я призываю сесть и подумать над своим поведением.

Не укради

Что, в конечном итоге, говорят нам вышеупомянутые стандарты в кратком изложении? Примерно вот что:

  • Не своевольничай. Наличие каждой строчки кода должно быть однозначно обосновано низкоуровневыми техническими требованиями и проектной документацией.

  • Пиши простой код. У простого меньше шансов сломаться или выстрелить тебе в ногу, чем у сложного.

  • Делай свои намерения явными и избегай неявных предположений. Это касается проверки инвариантов, исключения платформно-зависимых конструкций, исключения UB, unsafe и схожих граблей, заботливо разложенных языком программирования и средой исполнения.

  • Не забывай об асимптотической сложности. Ответственные системы обычно являются системами реального времени. Адептов C++ призывают воздержаться от злоупотреблений RTTI и использования динамической памяти (хотя последнее к реальному времени относят ошибочно, потому что подобающим образом реализованные malloc() и free() выполняются за постоянное время и даже с предсказуемой фрагментацией кучи).

  • Не игнорируй ошибки. Если что-то идёт не так, обрабатывай как следует, а не надейся на лучшее.

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

Видите пункт "игнорируй широко известные принципы проектирования сложных информационных систем"? Я тоже нет. Но подавленные воспоминания о том, как кросс-компилятор под дохлые однокристалки унижает разработчика в его собственном доме не дают человеку в полной мере осмыслить свои действия и их долгосрочные последствия. В итоге мы получаем спагетти без архитектуры, которое невозможно поддерживать и тестировать, но при этом оно носит гордую метку MISRA-совместимости как (сомнительное) свидетельство качества.

Я имел несчастье ознакомиться с некоторым количеством встраиваемого ПО реального времени, к надёжности которого предъявляются повышенные требования, и в пугающем числе случаев я ощущал, как у меня шевелятся на голове волосы. Меня, например, сегодня уже не удивляет старая байка об ошибках в системе управления Тойоты Приус, или байка чуть поновее про Boeing 737MAX (тот самый самолёт, который проектировали клоуны под руководством обезьян). В нашем новом дивном мире скоро каждая первая система станет программно-определяемой, что (безо всякой иронии) здорово, потому что это открывает путь к решению сложных проблем затратой меньших ресурсов. Но с повальной проблемой качества системоопределяющего ПО нужно что-то делать.

Если взять типичную встраиваемую систему достаточно высокой сложности, можно выделить примерно следующие сгустки логики:

  • Класс-бог, отвечающий за всё сущее.

  • Несколько драйверов периферии и коммуникационных протоколов, ко внутренним состояниям которых зачастую класс-бог обращается непосредственно, минуя хилые абстракции.

  • Utils или helpers, без них никуда.

Что в итоге: модульное тестирование невозможно, потому что нет модулей; толковая поддержка невозможна, потому что всё сложно; о гарантиях функциональной корректности трудно рассуждать всерьёз, нам бы для начала научиться выполнять вменяемую декомпозицию задачи, а не решать все проблемы в одном цикле сразу. Это зачастую преподносится как неотвратимая данность, нельзя по-другому, потому что это же не десктоп система с неограниченными ресурсами, вы не понимаете, это другое.

Инфоцыгане

Косвенным образом масла в огонь подливают некоторые поставщики программных инструментов для разработчиков встраиваемого ПО: Mbed, Arduino, и т.п. Их маркетинговые материалы вполне могут заставить начинающего специалиста поверить, что суть этой работы заключается в низкоуровневом управлении железом, потому что именно на этом аспекте диспропорционально фокусируются упомянутые поставщики ПО. Вот у меня на соседнем рабочем столе открыт в CLion проект ПО для одной встраиваемой системы; проект собирается из чуть более чем ста тысяч строк кода. Из этой сотни примерно три тысячи приходятся на драйверы периферии, остальное приходится на бизнес-логику и всякий матан. Моя скромная практика показывает, что за исключением простых устройств сложность целевой бизнес-логики приложения несопоставима с той его частью, что непосредственно работает с железом.

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

Смотри, что я нашёл! Есть крутая новая система, Mbed называется, значит, для эмбедеров. Гляди, как можно быстро прототипы лепить! Клац, клац, и мигалка готова! Вот же, на видео. А ты, Илья, свой алгоритм оптимизации CAN фильтров пилишь уже неделю, не дело это, давай переходить на Mbed.

Я, разумеется, не хочу сказать, что права на существование сии продукты не имеют. Конечно, имеют, и более того: они чрезвычайно полезны, когда бизнес-логика проста и сосредоточена вокруг интеграции нескольких компонентов. За примерами ходить не надо, просто откройте любой их рекламный проспект это то, для чего они созданы. Но у меня подгорает, когда я слышу о менеджере, продвигающем эмбеддед-фреймворк для проекта, где работа с периферией и её отладка едва ли займёт 1% внимания программиста.

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

Когда один бэкэндер лучше двух эмбедеров

Ранее я публиковал большую обзорную статью о нашем открытом проекте UAVCAN (Uncomplicated Application-level Vehicular Computing And Networking), который позволяет строить распределённые вычислительные системы (жёсткого) реального времени в бортовых сетях поверх Ethernet, CAN FD или RS-4xx. Это фреймворк издатель-подписчик примерно как DDS или ROS, но с упором на предсказуемость, реальное время, верификацию, и с поддержкой baremetal сред.

Для организации распределённого процесса UAVCAN предлагает предметно-ориентированный язык DSDL с помощью которого разработчик может указать типы данных в системе и базовые контракты, и вокруг этого затем соорудить бизнес-логику. Это работает примерно как REST эндпоинты в вебе, XMLRPC, вот это вот всё. Если взять одного обычного бэкендера человека, измученного сервис-ориентированным проектированием и поддержкой сложных распределённых комплексов и объяснить ему суть реального времени, то он в короткие сроки начнёт выдавать хорошие, годные интерфейсы на UAVCAN.

Хрестоматийный пример интеграция системы воздушных сигналов, или хотя бы одного лишь датчика воздушной скорости. Наученный тяжким опытом разработки и сопровождения продакшена, бэкендер тщательно осмыслит следующий вопрос: "какую бизнес-задачу мы решаем?"

Допустим, ответ подопытного является вариацией на тему "измерение воздушной скорости, барометрической высоты и статического давления". Тогда на свет появляются примерно следующие строки DSDL:

# Calibrated airspeeduavcan.time.SynchronizedTimestamp.1.0 timestampuavcan.si.unit.velocity.Scalar.1.0    calibrated_airspeedfloat16                               error_variance
# Pressure altitudeuavcan.time.SynchronizedTimestamp.1.0 timestampuavcan.si.unit.length.Scalar.1.0      pressure_altitudefloat16                               error_variance
# Static pressure & temperatureuavcan.time.SynchronizedTimestamp.1.0 timestampuavcan.si.unit.pressure.Scalar.1.0    static_pressureuavcan.si.unit.temperature.Scalar.1.0 outside_air_temperaturefloat16[3] covariance_urt# The upper-right triangle of the covariance matrix:#   0 -- pascal^2#   1 -- pascal*kelvin#   2 -- kelvin^2

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

Знакомый с физикой полёта читатель здесь уместно поинтересуется: откуда оконечному устройству (что поставляет сервис воздушных сигналов) знать калибровочные параметры датчика воздушной скорости? Ведь это предполагает осведомлённость датчика о собственном положении на летательном аппарате и об его аэродинамических свойствах. Принципы инкапсуляции и разделения ответственности предписывают, что соответствующие параметры настраиваются на самом поставщике сервиса воздушных сигналов (т.е. сетевом узле датчика) с целью сокрытия деталей реализации сервиса от его потребителей.

В БПЛА некоторых типов практикуется автокалибровка, где выборка измерений с приёмника воздушного давления за значительный промежуток времени сопоставляется с наземной скоростью, чтобы таким образом определить калибровочные параметры эмпирически. В рамках сервис-ориентированного подхода это решается превращением системы воздушных сигналов в потребителя данных о наземной скорости. Проще говоря, наш сетевой узел, что измеряет воздушную скорость, подписывается на топик, в котором есть данные о наземной скорости летательного аппарата, получая таким образом доступ к необходимому контексту для выполнения автокалибровки.

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

uint16 differential_pressure_readinguint16 static_pressure_readinguint16 outside_air_temperature_reading

Это, конечно, нельзя использовать непосредственно, поэтому наше конечное устройство превращается в пассивный сенсор, который сообщает измерения на центральный узел, и тот затем выполняет вычисления и публикует их результаты в сеть в узкоспециализированных форматах, по одному под конечного потребителя. Например, если гироподвес и привод предкрылков нуждаются в воздушной скорости, то к каждому будет приделан отдельный топик своего типа. Своими глазами видел.

В итоге мы получаем всё то же спагетти с классом-богом, только вместо класса у нас центральный узел, а вместо спагетти сотня топиков без архитектуры. Нетрудно заметить, что этот подход также может увеличить время доставки данных и загрузку сети, а заодно и, возможно, снизить отказоустойчивость ввиду централизации процессов.

Художника каждый может обидеть

Не следует думать, что я хочу определить всех эмбедеров в биореактор. Я, в конце концов, и сам один из них. Но я склонен к мысли, что легче из хорошего прикладного программиста сделать толкового эмбедера, чем дождаться вменяемого кода от последнего.

Коллеги, одумайтесь.

Я вижу, как нашим микроскопом заколачивают ржавые гвозди, и представляю, сколько ещё подобного происходит за пределами моего поля зрения. В прошлом году уровень отчаяния в нашей скромной команде был столь высок, что мы опубликовали наноучебник, где объясняется, как выглядит сетевой сервис здорового человека: UAVCAN Interface Design Guidelines. Это, конечно, капля в море, но в один прекрасный день я всё-таки переведу его на русский язык ради подъёма уровня профессиональной грамотности.

Непонимание основ организации распределённых вычислений затрудняет внедрение новых стандартов на замену устаревших подходов. Наши наработки в рамках стандарта DS-015 (созданного в коллаборации с небезызвестными NXP Semiconductors и Auterion AG) встречают определённое сопротивление ввиду своей непривычности для целевой аудитории, в то время как ключевые принципы, на которых они основаны, известны индустрии информационных технологий уже не одно десятилетие. Этот разрыв должен быть устранён.

Желающие принять участие в движении за архитектурную чистоту и здравый смысл могут причаститься в телеграм-канале uavcan_ru или на форуме forum.uavcan.org.

Подробнее..

Разработка универсального счетчика импульсов

09.02.2021 14:04:30 | Автор: admin

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

Оптические датчики и их особенности

Чаще всего при подсчете продукции или отслеживании механического перемещения внешним датчиком используются оптические датчики барьерного, рефлекторного или диффузионного типа.

Рисунок 1. Типы датчиков (И - источник, П - приемник, О - объект).Рисунок 1. Типы датчиков (И - источник, П - приемник, О - объект).

Принцип работы барьерного датчика очень простой - имеются разнесенные в пространстве источник света и приемник, между ними проходят объекты, тем самым закрывая источник света от приемника. Полученный сигнал с фотоприемника коррелирует с прохождением объекта и практически не требует обработки. Однако системы с таким датчиком не лишены недостатков - обычно это несколько модулей, требуется прокладка кабелей и юстировка оптической системы. В случае рефлекторного и диффузионного типа датчиков установка проще, так как источник и приемник находятся в одном корпусе. Рефлекторный датчик принимает отраженный от объекта или специальной отражающей метки свет, а диффузионный - рассеянный, учитывая при этом его интенсивность. Но для датчиков этих типов полученный сигнал требует дальнейшей обработки. Вне зависимости от выбранного типа датчика, для счета обычно используется независимый микрокомпьютерный или микропроцессорный блок как отдельное устройство.

Первый прототип датчика

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

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

Принципиальная схема
Рисунок 2. Первый прототип датчика - принципиальная схема.Рисунок 2. Первый прототип датчика - принципиальная схема.

Испытания показали работоспособность в диапазоне 10-30 см с очень контрастными метками (использовали световозвращающий скотч). Но такой результат нас еще не устраивает, так так датчик на просвет работает на значительно больших расстояниях. Также система оказалась чувствительна к включению/выключению освещения в помещении, и при использовании подобного подхода от этого недостатка избавиться не получится.

Второй прототип датчика

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

Принципиальная схема
Рисунок 3. Второй прототип датчика - принципиальная схема.Рисунок 3. Второй прототип датчика - принципиальная схема.

Второй прототип работал лучше предыдущего - увеличилось расстояние срабатывания и снизились требования к отражающим характеристикам меток. Однако все еще имеется ряд проблем. Во-первых, на низкой скорости движения объектов был замечен пропуск импульсов. Это связано с тем, что схема плавающего порога успевала подстроиться под изменения. Во-вторых, при глянцевой поверхности объекта счетчик давал много ложных срабатываний, так как не хватало гистерезиса. Но поднимать гистерезис бесконечно нельзя, система просто перестанет реагировать на обычные метки. И в-третьих, что самое печальное, в некоторых случаях счетчик ловил пульсацию бюджетного освещения в производственных помещениях.

Третий прототип датчика

В результате проведенных опытов стало понятно, что нельзя обойтись без дополнительной настройки системы, которую можно осуществить только с помощью микроконтроллера. Также для исключения влияния помех от фонового освещения решили добавить модуляцию опорного сигнала и преобразование Фурье на приемнике. Корпус уже был разработан и изготовлен на предыдущих этапах, и нам хотелось вписаться в его габариты. Так выбор пал на практически единственный вариант - STM32G030J6M6 Cortex - M0+ c ADC 2.5Msps в корпусе SOIC-8. Отличное решение для непрерывной обработки данных от АЦП. Общение с микроконтроллером осуществляется по шине I2C.

Принципиальная схема
Рисунок 4. Третий прототип датчика - принципиальная схема.Рисунок 4. Третий прототип датчика - принципиальная схема.

На операционном усилителе собран трансимпедансный усилитель тока фотодиода. Лазер модулируется дискретным сигналом от таймера, потому что в данном случае нет необходимости получать чистый синус. Для совместимости с предыдущими решениями был сделан дискретный вывод для использования аппаратного счетчика событий (1й пин разъема P1), а конфигурация осуществляется один раз при старте системы. Таким образом, сохраняется полная преемственность с уже написанным ПО.

В микроконтроллере реализованы генерация сигнала ШИМ, обработка оцифрованных данных и общение по I2C. За генерацию ШИМ отвечает таймер, синхронизированный с АЦП. Данные передаются в память по DMA и обрабатываются по половинам - пока заполняется первая половина буфера, вторая анализируется. Сам алгоритм обработки данных получится следующий:

Рисунок 5. Алгоритм обработки данныхРисунок 5. Алгоритм обработки данных

Микрокомпьютер

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

  • возможность запускать программу на Python 3,

  • место для пары сетевых библиотек,

  • интерфейсы Ethernet и Wi-Fi для связи с сервером,

  • питание по micro USB или PoE,

  • производительность - не критично,

  • время включения - не более 2 минут,

  • хранилище данных не требуется, так как мы хотим передавать их на сервер, и буфера в оперативной памяти будет достаточно.

Сначала мы использовали Orange Pi zero, однако, учитывая их немалые габариты и невозможность нормально сделать PoE, решено было поискать другие варианты. Так взгляд пал на одноплатный компьютер VoCore, характеристики которого полностью подходили под задачу. Изучив предложения на китайском рынке, был найден очень похожий вариант выпускаемый массово - процессор RT5350, 32Mb RAM, 8/16Mb Flash.

Рисунок 6. Одноплатный компьютер VoCore.Рисунок 6. Одноплатный компьютер VoCore.

Он немного больше, чем оригинальный VoCore, зато под модулем остается место для размещения компонентов, а также у модуля есть удобный разъем для подключения к основной плате. Схематика незначительно отличается от оригинального VoCore, так что конфигурацию от VoCore можно легко адаптировать под китайского товарища.

Конструктив

Рисунок 7. 3Д модель счетчика.Рисунок 7. 3Д модель счетчика.

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

Рисунок 8. Поворотная часть корпуса.Рисунок 8. Поворотная часть корпуса.

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

Рисунок 9. Основная часть корпуса.Рисунок 9. Основная часть корпуса.

Так как партии пока относительно небольшие корпус изготавливается методом SLS печати.

Итак, в итоге у нас получилась следующая архитектура устройства:

  • вычислительный модуль (одноплатный компьютер),

  • основная плата, на которой расположены разъемы Ethernet, USB, I2C, светодиоды и кнопка,

  • плата питания (устройство может питаться как от microUSB так и от PoE).

Подсчет срабатываний

Теперь пара слов о том, как реализован подсчет срабатываний датчика. Независимо от типа датчика, алгоритм подсчета импульсов остается одинаковым. Выход датчика подключается к GPIO процессора. Количество импульсов подсчитывалось через GPIO interrupt. Для этого требуется настроить GPIO на вход и включить прерывания. Об этом хорошо написано, например, тут. Число срабатываний можно посмотреть командой cat /proc/interrupts | grep gpiolib. Если же требуется реагировать на каждое событие или записывать время его срабатывания, то уже придется написать простую программу. Данный подход хорошо себя зарекомендовал и является необходимым и достаточным источником данных для подобного класса датчиков. В случае датчика с микроконтроллером, нужно перед началом работы загрузить необходимые параметры по I2C.

Заключение

Итак, что мы имеем на выходе? Компактное устройство для подсчета импульсов с оптическим датчиком и готовой реализацией отправки данных на сервер по Ethernet или WiFi. Была реализована передача данных по MQTT. Адаптивная архитектура также позволяет легко подключать практически любой другой датчик по I2C или SPI через переходник. На данный момент имеются такие варианты счетчиков: лазерный с аналоговой обработкой сигналов, лазерный с цифровой обработкой сигналов, а также индукционный счетчик для подключения внешнего промышленного индукционного датчика. Разработанный корпус позволил осуществлять поворот оптического модуля, а также его замену на другой тип датчика. В ближайших планах хотим подключить тепловизионный датчик для мониторинга нагруженных узлов в производстве.

Подробнее..

Квалификация инструментов для разработки встраиваемого ПО

25.12.2020 08:10:45 | Автор: admin
Привет, хабр! В этой статье я хочу максимально просто и доступно рассказать про то, как доказывается, что ваши средства разработки и верификации подходят для создания систем повышенной надежности. Это очень важный и далеко не самый простой вопрос, и моя цель ответить на него как можно более понятным языком. В самой статье я обобщил указания из отраслевых стандартов, таких как КТ-178 или Р-331 (встраиваемое ПО в авиации), ГОСТ Р ИСО 26262-8 (встраиваемое ПО в автомобилестроении). Так что добро пожаловать под кат


Квалификация зачем она?


Отраслевые стандарты, такие как КТ-178 или ИСО 26262, описывают процессы создания ПО повышенной надежности. Если следовать этим описаниям, то создание такого ПО превратится в бюрократический ад, который будет длиться вечно. Но существует программное обеспечение, которое позволяет автоматизировать значительную часть этих процессов. Такое ПО называется инструментом. И если вы применяете инструмент, то он должен быть надежным (ИСО 26262 даже вводит термин уверенность в инструменте). Для подтверждения надежности инструмента проводится его квалификация.

Квалификация теория


В отраслевых стандартах есть понятие уровня безопасности. В разных стандартах они называются по-разному: Уровень ПО в КТ-178, Уровни Полноты Безопасности автомобиля в ИСО 26262. А для средств разработки применяются уровни квалификации инструментов (КТ-178) или уровни классификации инструментов (ИСО 26262). Эти уровни назначаются по критичности инструментов чем больше влияния оказывает инструмент на разработку, тем более высокий уровень квалификации будет ему назначен. При этом одним из главных критериев по определению влияния инструмента является мера его влияния на результирующее ПО.
В качестве примера можно рассмотреть генератор исходного кода и статический анализатор кода. Сгенерированный код попадает в прошивку устройства, которое будет установлено на борт самолета или автомобиля. Таким образом, генератор кода оказывает непосредственное влияние на результирующее ПО. Так как генератор кода сложная штука, и может генерировать код с ошибками, то к качеству этого генератора кода предъявляются строгие требования и уровень его квалификации будет максимальным. Другое дело статический анализатор, результат работы которого не попадает в бортовое ПО и степень его влияния минимальна. Поэтому для статического анализатора уровень квалификации будет ниже, чем для генератора кода.
А еще уровень квалификации напрямую влияет на трудозатраты: так, для авиации, для квалификации инструмента по наивысшему уровню КТ-178С, требуется выполнение 76 контрольных мероприятий, а по низшему только 14.
Еще один важный момент квалификация инструментов проводится не разработчиком инструмента, а непосредственно разработчиком ПО, причем квалификация должна проводиться для каждого проекта!

Квалификация заметки по практике


Как было сказано в теоретической части, квалификация инструментов это затратный процесс, однако он упрощается несколькими способами:

  • Поддержка процесса квалификации производителями инструментов (вендоров)
  • Указания по квалификации инструментов из стандартов


Рассмотрим их по порядку. В первую очередь, сами вендоры поставляют пакеты поддержки квалификации инструментов. Во вторую отраслевые стандарты содержат указания по квалификации инструментов, которые могут сократить объем работ.

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

Для квалификации инструментов верификации стандарты предписывают демонстрацию их поведения в нормальных условиях. На практике это выглядит следующим образом:

  1. Вендор поставляет набор шаблонов документов, нормативных документов и опорных тестов для инструмента и их эталонные результаты.
  2. Вы заполняете шаблоны документов и запускаете предоставленные тесты в своем окружении.
  3. Результаты тестов, запущенных вами, сравниваются с эталонами, и при расхождении результатов вы устраняете расхождение.


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

Инструменты MathWorks и их квалификация


Такие инструменты как Simulink, DSP Toolbox, Control System Toolbox это стандарт индустрии для разработки систем управления, цифровой обработки сигналов. Неудивительно, что их применяют в авиации, автомобилестроении и прочих отраслях. Из разработанных моделей генерируется C/C++ код, который ездит и летает. Естественно, что перед разработчиками встает вопрос квалификации инструментов. И квалификация инструментов MathWorks для КТ-178С осуществляется для инструментов верификации моделей и кода:



А для ISO 26262 поставляются сертификаты для:

  • Simulink Check
  • Simulink Coverage
  • Simulink Requirements
  • Simulink Design Verifier
  • Simulink Test
  • Simulink Report Generator
  • Polyspace Bug Finder
  • Polyspace Code Prover
  • Embedded Coder
  • HDL Coder
  • PLC Coder


В зависимости от отрасли предоставляются пакеты поддержки квалификации инструментов DO Qualification Kit для авиации или IEC Certification Kit для автомобильной, железнодорожной и других отраслей.

Вместо выводов


В данной статье был дан обобщенный обзор процесса квалификации инструментов в соответствии с отраслевыми стандартами. В ходе написания данной статьи, я руководствовался следующими источниками:
  1. Р-330, Руководство по квалификации программных инструментов, в частности:

  • п. 2.0 Назначение квалификации инструмента
  • п. 3.1. Уровни квалификации
  • Справочные материалы D, вопрос D7

  1. ГОСТ Р ИСО 26262-8, Глава 11, Уверенность в использовании инструментального программного обеспечения

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

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru