Разработка на языке Си для контроллеров СМАРТ

ООО «Миконт»

ООО «Миконт»

Версия 1.2.0 для СМАРТ-ОС v2.8.0

1 Настройка IDE Qt Creator

При создании программного обеспечения для контроллеров СМАРТ мы используем компилятор gcc. Разрабатывать ПО можно разными способами. Здесь мы покажем, как настроить IDE Qt Creator для Windows, чтобы создавать программное обеспечение для контроллеров СМАРТ.

1.1 Установка дистрибутивов

Для начала скачайте саму среду разработки Qt Creator, либо на сайте производителя https://download.qt.io/official_releases/qtcreator/, либо на нашем сайте (Qt Creator). Установите её. Для установки нужно будет зарегистрироваться.

Теперь нужно скачать компилятор GCC.

Зайдите на https://developer.arm.com/downloads/-/gnu-rm, выберите:

Установите/распакуйте GCC.

1.2 Настройка компилятора

Запустите Qt Creator.

Сначала необходимо включить модуль BareMetal. Выберите пункт меню «Help» → «About Plugins…» («Справка» → «О модулях…»). Отметьте «Load on startup» («Загружать при запуске») для пункта «BareMetal» в разделе «Device Support». Перезапустите Qt Creator.

BareMetal

Теперь добавьте устройство BareMetal.

Выберите пункт меню «Edit» → «Preferences…» («Правка» → «Настройки…»). Выберите слева раздел «Devices» («Устройства»).

Нажмите кнопку «Add…» («Добавить…») и добавьте «Bare Metal Device» («Устройство на голом железе»).

Добавление Bare Metal Device

Теперь нужно добавить компиляторы.

Выберите слева раздел «Kits» («Комплекты»), справа — вкладку «Compilers» («Компиляторы»).

Нажмите кнопку «Add» («Добавить») → «GCC» → «C».

Введите название «GCC». Укажите компилятор arm-none-eabi-gcc.exe в поле «Compiler path» («Путь к компилятору»).

Компилятор GCC-C

Аналогичным образом добавьте компилятор C++. В качестве исполняемого файла укажите arm-none-eabi-g++.exe.

Компилятор GCC-C++

Перейдите на вкладку «Debuggers» («Отладчики») и нажмите кнопку «Add» («Добавить»).

Введите название «GDB» и укажите файл arm-none-eabi-gdb.exe в поле «Path» («Путь»).

Отладчик GCC

Наконец, перейдите на вкладку «Kits» («Комплекты»). Нажмите кнопку «Add» («Добавить»).

Укажите следующие значения параметров:

Параметр Значение
Name: gcc-arm
Device type: Bare Metal Device
Device: Bare Metal Device (default for Bare Metal)
Compiler C: GCC
Compiler C++: GCC
Debugger: GDB
Qt version: None
Параметр Значение
Название gcc-arm
Тип устройства: Устройство на голом железе
Устройство: Устройство на голом железе (по умолчанию для Bare Metal)
Компилятор C: GCC
Компилятор C++: GCC
Отладчик: GDB
Профиль Qt: Нет
Комплект «gcc-arm»

Если все шаги выполнены успешно, то для комплекта «gcc-arm» будет установлен значок .

2 Компоненты программы

Чтобы начать разрабатывать программное обеспечение для контроллера СМАРТ, скачайте заготовку проекта smart2-app: со всеми примерами или пустой на сайте micont.ru/soft/smart-c/.

Откройте в Qt Creator файл проекта smart-project/smart2-app/smart2-app.qbs. Сконфигурируйте проект, отметив «gcc-arm» и сборку «Release».

Конфигурирование проекта

2.1 Основные функции программы

Проверьте, что проект компилируется и собирается. Для этого выберите пункт меню «Build» → «Build Project “smart2-app”» («Сборка» → «Собрать проект „smart2-app“») или нажмите Ctrl+B.

Проект smart2-app

В начале файла определяются структуры журнала, уставок, входов/выходов, структуры доступа по протоколу Modbus, http-сервера. Все они описаны в следующих разделах.

Управление осуществляется тремя функциями с предопределёнными именами:

  1. smart_app_init(void)

Вызывается один раз при запуске контроллера, инициализирует систему. В частности в ней задаётся цикл работы контроллера, то есть период вызова функции smart_app_task():

smart_set_cycle(uint32_t ms); // ms — цикл работы в мс.
  1. smart_app_task(void)

Собственно функция прикладной задачи.

  1. int smart_object_frame(void *p)

Функция для задачи пользовательского интерфейса (пункт главного системного меню «Объект»). В стандартном случае в этой функции будет только вызов главного пользовательского меню:

menu_handler(&object_menu);
return 0;

object_menu — структура, описывающая главное пользовательское меню, см. раздел «Пользовательский интерфейс».

Программная документация по СМАРТ-ОС в формате HTML вызывается запуском файла smart-project/doc/os-doc-html/index.html или ярлыком
"smart-project/doc/SMART OS Documentation.lnk".

Также она есть на сайте micont.ru/soft/smart-c/doc/os-doc-html/.

2.2 Журнал

В контроллерах СМАРТ журнал используется для периодического сохранения значений 32-битных переменных (float/int32_t/uint32_t). Кроме того, переменные могут сохраняться при отключении питания.

Для каждой переменной указывается 4-символьное имя (в однобайтной кодировке), режимы сохранения и её тип.

Существует 4 комбинации режимов сохранения:

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

Существует два базовых типа переменных: TP_FLOAT (0x00) (float) и TP_INT32 (0x01) (uint32_t). Дополнительно существует тип TP_SIGNED_INT32 (0x03) (int32_t).

Переменные журнала могут быть доступны по сети с помощью Modbus RTU-сервера или Modbus TCP-сервера.

2.2.1 Структуры

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

// Определение структуры оперативных (журнальных) переменных.
// Может состоять из 32-битных переменных (float/int32_t/uint32_t),
// может иметь вложенные структуры и массивы.
struct
{
    float    var0;
    int32_t  var1;
    uint32_t var2;
//  ...
} vars;

Затем нужно определить массив описаний этой структуры:

// Определение структуры метаданных оперативных (журнальных) переменных.
const var_desc_t workvars_meta[] = {
    { .name = "var0", .sv = SV_NORMAL | SV_POWER | SV_RESTORE,
                      .tp = TP_FLOAT },
    { .name = "var1", .sv = SV_NORMAL | SV_POWER | SV_RESTORE,
                      .tp = TP_SIGNED_INT32 },
    { .name = "var2", .sv = SV_NORMAL | SV_POWER | SV_RESTORE,
                      .tp = TP_INT32 },
};

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

// Определение системной структуры, описывающей оперативные (журнальные)
// переменные.
const workvars_t workvars = {
    .meta_ptr = workvars_meta,
    .meta_count = sizeof(workvars_meta) / sizeof(var_desc_t),
    .block_ptr = (var_t *) &vars,
    .count = sizeof(vars) / sizeof(var_t)
};

2.2.2 Функции

В основной функции инициализации smart_app_init() необходимо инициализировать журнал:

smart_journal_init();

Периодическое сохранение журнала (переменных с флагом SV_NORMAL) выполняется в основной функции прикладной задачи smart_app_task() примерно так:

rtc_t rtc;
uint8_t current_hour = 24;

void smart_app_task(void)
{
    rtc_get(&rtc); // Получаем текущую дату/время.
    if (current_hour != rtc.hour) { // Контролируем смену часа.
        current_hour = rtc.hour;
        smart_journal_save(); // Сохраняем журнал.
    }

//  ...
}

2.3 Сохраняемые переменные (уставки)

2.3.1 Структуры

Похожий механизм (см. «Журнал») используется для хранения настроечных переменных (уставок). Описывается произвольная структура с переменными, значения которых нужно сохранять в энергонезависимой FLASH-памяти и структура с метаописанием этих переменных.

// Определение структуры сохраняемых/настроечных переменных (уставок).
// Может состоять из 32-битных переменных (float/int32_t/uint32_t),
// может иметь вложенные структуры и массивы.
struct
{
    float    set0;
    int32_t  set1;
    uint32_t set2;
} conf;

Метаописания уставок может состоять из нескольких секций. Разделение на секции сделано исключительно для удобства. Метаописания нужны только для возможности редактирования переменных (см. ITEM_TYPE_SETTING).

Примечание 1. В отличие от журнала, в структуре уставок можно сохранять 64-битные переменные: double, int64_t, uint64_t. Ограничение состоит в том, что их не получится описать в структуре метаописаний, поэтому для изменения таких переменных придётся написать собственный функционал.

Примечание 2. На дисплей 64-битные переменные выводятся следующим образом: для double используется стандартный форматный вывод "%f", для int64_t/uint64_t используются функции из библиотеки «Extended Functions».

Пример структуры секции метаописаний:

// Определение секции метаданных уставок.
const var_meta_t section0_meta[] = {
    DECLARE_INT_SETTING("", &conf.set1, 5, 0, 3600),
    DECLARE_INT_SETTING("", &conf.set2, 50, 10, 90),
    DECLARE_FLOAT_SETTING("", &conf.set0, 0, 0, 100.0f),
};

Примечание 3. Заметьте, порядок описаний не зависит от порядка переменных в структуре уставок conf, в отличие от журнальных переменных.

Для описания уставки существуют два макроса: DECLARE_FLOAT_SETTING для float и DECLARE_INT_SETTING для int32_t/uint32_t. У этих макросов 5 параметров:

Далее нужно определить массив, содержащий ссылки на все секции метаописаний:

// Определение массива всех секций метаданных уставок.
const settings_section_t sections[] = {
    { .name = "section0", .meta_ptr = section0_meta,
      .meta_count = sizeof(section0_meta) / sizeof(var_meta_t) },
//  { .name = "sectionN", .meta_ptr = sectionN_meta,
//    .meta_count = sizeof(sectionN_meta) / sizeof(var_meta_t) },
};

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

// Определение системной структуры, описывающей сохраняемые переменные.
const settings_t settings = {
    .sections_ptr = sections,
    .sections_count = sizeof(sections) / sizeof(settings_section_t),
    .block_ptr = (var_t *) &conf,
    .count = sizeof(conf) / sizeof(var_t)
};

2.3.2 Функции

В случае использования ITEM_TYPE_SETTING для изменения значения уставки операционная система сама предложит сохранить изменения. Если вы меняете значение другим способом (или используете 64-битные значения уставок), то необходимо явно вызвать функцию сохранения:

// Объявляем функцию сохранения, определённую в операционной системе.
extern void smart_save_settings(void);

... Изменяем значение/значения уставок ...

// Вызываем функцию сохранения.
smart_save_settings();

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

2.4 Ввод/вывод

На данный момент операционная система поддерживает 3 типа контроллеров:

А также 5 типов модулей:

2.4.1 Структуры

Для контроллера нужно определить следующую структуру (.dev_addr обязательно должен быть равен 33):

// Структура, определяющая контроллер СМАРТ-В02М2.
mod_smart_b02_t smart_b02 = {
    .generic = { .new = mod_smart_b02_new, .dev_addr = 33 }
};

// Структура, определяющая контроллер СМАРТ-В03.
// mod_smart_b03_t smart_b03 = {
//     .generic = { .new = mod_smart_b03_new, .dev_addr = 33 }
// };

// Структура, определяющая контроллер СМАРТ-В04.
// mod_smart_b04_t smart_b04 = {
//     .generic = { .new = mod_smart_b04_new, .dev_addr = 33 }
// };

Далее нужно определить список для шины UART, состоящий из одного элемента — указателя на вышеопределённую структуру контроллера:

// Список, состоящий из одного элемента,
// ссылающегося на структуру определения контроллера СМАРТ-В02М2.
void *const smart_b02_uart[] = {
    &smart_b02
};

Наконец, необходимо определить структуру для операций ввода/вывода. В качестве интерфейса обязательно указываем UART. Скорость обязательно задаём 115200 бод.

// Структура ввода/вывода для контроллера СМАРТ-В02М2.
io_t io_b02 = {
   .iface_handle = SERIF_UART, // Для контроллера обязательно UART.
   .default_baudrate = SERIF_BAUD_115200, // Обязательно 115200.
   .cycle_time = 0,
   .inter_query_delay = 0,
   .mod_list = smart_b02_uart,
   .mod_count = 1
};

Теперь то же самое нужно сделать для модулей. Предположим, что все модули подключены к одному порту RS-485-1.

Контроллер СМАРТ
// Структуры, определяющие модули:

// СМАРТ-321-16DI;
mod_smart_b321_t smart321 = {
    .generic = { .new = mod_smart_b321_new, .dev_addr = 1 }
};

// СМАРТ-322-16DO;
mod_smart_b322_t smart322 = {
    .generic = { .new = mod_smart_b322_new, .dev_addr = 2 }
};

// СМАРТ-323-8DI.8DO (СМАРТ-323-8DI.4DO.4DR);
mod_smart_b323_t smart323 = {
    .generic = { .new = mod_smart_b323_new, .dev_addr = 3 }
};

// СМАРТ-331-8AI2U;
mod_smart_b331_t smart331 = {
    .generic = { .new = mod_smart_b331_new, .dev_addr = 4 }
};

// СМАРТ-332-6AO.
mod_smart_b332_t smart332 = {
    .generic = { .new = mod_smart_b332_new, .dev_addr = 5 }
};

Для адреса модуля .dev_addr можно использовать значения от 1 до 128. На каждом модуле этот адрес нужно выставить с помощью DIP-переключателей JP2.

Когда все переключатели выключены (находятся в положении «вверх»), то у модуля установлен адрес по умолчанию 33. Если 8-й переключатель включен (находится в положении «вниз»), то оставшиеся 7 переключателей задают адрес в двоичном виде.

Расположение переключателей JP2

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

Адрес 1 2 3 4 5 6 7 8
33
1
2
3
4
33
128

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

Далее нужно определить список для шины RS-485-1, состоящий из указателей на вышеопределённые структуры:

// Список из 5 модулей для шины RS-485-1.
void *const smart_modules_485_1[] = {
    &smart321,
    &smart322,
    &smart323,
    &smart331,
    &smart332
};

Наконец, необходимо определить структуру для операций ввода/вывода. Указываем шину RS-485-1. Скорость обязательно задаём 115200 бод, модули по умолчанию настроены на эту скорость.

// Структура ввода/вывода для модулей.
io_t io_modules = {
   .iface_handle = SERIF_RS485_1, // Шина RS-485-1.
   .default_baudrate = SERIF_BAUD_115200, // Обязательно 115200.
   .cycle_time = 0,
   .inter_query_delay = 0,
   .mod_list = smart_modules_485_1,
   .mod_count = sizeof(smart_modules_485_1) / sizeof(void *)
};

2.4.2 Функции

В основной функции инициализации smart_app_init() необходимо провести процедуру инициализации для операций ввода/вывода:

io_init(&io_b02);
io_init(&io_modules);

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

io_get_channel(
    io_t *io,         // Указатель на структуру io_t.
    uint32_t mod,     // Порядковый номер модуля
                      // в списке модулей .mod_list.
    uint32_t channel, // Номер канала (начальный номер группы каналов
                      // + номер канала в рамках модуля).
    void *value       // Указатель на переменную для значения.
);

Начальные номера групп каналов:

Имя Значение Описание
MOD_CH_DIN_OFFSET 100 Начальный номер канала для дискретных входов
MOD_CH_AIN_OFFSET 300 Начальный номер канала для аналоговых (токовых) входов
MOD_CH_DOUT_OFFSET 500 Начальный номер канала для дискретных выходов
MOD_CH_AOUT_OFFSET 700 Начальный номер канала для аналоговых (токовых) выходов

Номер канала в рамках модуля всегда начинается с 0. Максимальное значение равно количеству каналов − 1.

Например, для модуля СМАРТ-321-16DI диапазон номеров каналов для дискретных входов — (MOD_CH_DIN_OFFSET + 0) ÷ (MOD_CH_DIN_OFFSET + 15); для модуля СМАРТ-332-6AO диапазон номеров каналов для аналоговых выходов — (MOD_CH_AOUT_OFFSET + 0) ÷ (MOD_CH_AOUT_OFFSET + 5).

Для дискретных входов и выходов в качестве параметра value передаётся указатель на переменную типа uint8_t. Возможные значения — 0 или 1.

Для аналоговых входов и выходов в качестве параметра value передаётся указатель на переменную типа float.

Кроме этого, существуют два специальных канала:

Имя Значение Описание
MOD_CH_STATE 0 Состояние связи с модулем
MOD_CH_LAST_ERROR 1 Детализация ошибки связи с модулем

Для канала MOD_CH_STATE в качестве параметра value передаётся указатель на переменную типа uint8_t. Возможные значения *value:

Значение Описание
0 Ошибка связи с модулем
1 Есть связь с модулем (нет ошибки)

Для канала MOD_CH_LAST_ERROR в качестве параметра value передаётся указатель на переменную типа uint32_t. Возможные значения *value:

Значение Ошибка
0 Нет ошибки (MOD_CH_STATE == 1)
1 Таймаут (неверный адрес модуля или скорость != 115200)
2 Ошибка CRC
3 Другая ошибка (например, несоответствие типа модуля)

Примеры:

uint8_t din;
// Получаем 16-й DI-сигнал с модуля СМАРТ-321-16DI.
io_get_channel(&io_modules, 0, MOD_CH_DIN_OFFSET + 15, &din);

float ain;
// Получаем значение 2-го AI-сигнала с контроллера СМАРТ-В02М2.
io_get_channel(&io_b02, 0, MOD_CH_AIN_OFFSET + 1, &ain);
uint32_t last_error;
// Получаем код ошибки связи с модулем СМАРТ-331-8AI2U (0 — нет ошибки).
io_get_channel(&io_modules, 3, MOD_CH_LAST_ERROR, &last_error);

Для записи значений в дискретные и аналоговые выходы используется функция:

io_set_channel(
    io_t *io,         // Указатель на структуру io_t.
    uint32_t mod,     // Порядковый номер модуля
                      // в списке модулей .mod_list.
    uint32_t channel, // Номер канала (начальный номер группы каналов
                      // + номер канала в рамках модуля).
    void *value       // Указатель на переменную для значения.
);

Для дискретных выходов в качестве параметра value передаётся указатель на переменную типа uint8_t. Возможные значения — 0 или 1.

Для аналоговых выходов в качестве параметра value передаётся указатель на переменную типа float. В качестве значения указывается значение тока 4 ÷ 20 мА.

Примеры:

uint8_t dout = 1;

// Устанавливаем 8-й DO-сигнал на модуле СМАРТ-323-8DI.8DO.
io_set_channel(&io_modules, 2, MOD_CH_DOUT_OFFSET + 7, &dout);

float aout = 15.4;

// Записываем значение 15,4 мА во 2-й аналоговый выход.
// на модуле СМАРТ-332-6AO.
io_set_channel(&io_modules, 4, MOD_CH_AOUT_OFFSET + 1, &aout);

Примечание. Везде, где в качестве параметра value требуется указатель на переменную типа uint8_t, можно использовать указатель на переменную типа uint32_t или int32_t.

2.5 Modbus RTU Server

Контроллеры СМАРТ поддерживают протокол Modbus. Для интерфейсов RS-232 и RS-485 реализованы протоколы Modbus RTU Server и Modbus RTU Client. Для Ethernet реализован Modbus TCP Server.

Контроллер СМАРТ

2.5.1 Структуры

Сначала нужно определить структуру, к переменным которой будет доступ по Modbus. В структуре можно определять переменные типов, которые кратны 2 байтам: int16_t, uint16_t, int32_t, uint32_t, int64_t, uint64_t, float, double.

// Определение структуры переменных для доступа по Modbus.
#pragma pack(push, 2) // Выравнивание по 2 байта.
struct
{
    int16_t  modbus_i16;
    int32_t  modbus_i32;
    uint64_t modbus_u64;
    float modbus_float;
//  ...
} modbus_data;
#pragma pack(pop)

Так же, как и для уставок, доступные по Modbus переменные можно разбить на несколько регионов — произвольных областей регистров хранения («Holding Registers»), которые ссылаются на разные структуры.

Для каждого региона указывается адрес в пределах от 0 до 49000 (не включительно).

// Описание регионов для доступа по Modbus.
const modbus_region_t modbus_regions[] = {
    {
        .addr = 0,
        .count = sizeof(modbus_data) / sizeof(uint16_t),
        .type = MODBUS_REG_HOLDING, // Только такое значение.
        .data_ptr = (uint16_t *) &modbus_data
    },
//  {
//      .addr = 100,
//      .count = sizeof(another_modbus_vars) / sizeof(uint16_t),
//      .type = MODBUS_REG_HOLDING,
//      .data_ptr = (uint16_t *) &another_modbus_vars
//  },
};

Ну и собственно описание структуры Modbus RTU-сервера:

// Описание структуры Modbus RTU-сервера.
modbus_server_t modbus_rtu_server = {
    .iface_handle = SERIF_RS232, // Можно также использовать
                                 // RS485_1 и RS485_2.
    .default_baudrate = SERIF_BAUD_921600, // Можно использовать любую
                                           // доступную скорость.
    .dev_id = 1, // Идентификатор контроллера, используется на верхнем уровне.
    .map = { .flags = (modbus_server_flags_t)
             (MODBUS_SERVER_FLAG_JOURNAL | MODBUS_SERVER_FLAG_SETTINGS),
        .regions = modbus_regions,
        .regions_count = sizeof(modbus_regions) / sizeof(modbus_region_t)
    }
};

В качестве интерфейса можно использовать либо RS-232, либо один из двух интерфейсов RS-485. Доступна любая скорость от 9600 до 921600 бод. Адрес (идентификатор) устройства может принимать значения от 1 до 247.

Можно разрешить доступ к переменным журнала и/или уставок. Для этого используются флаги MODBUS_SERVER_FLAG_JOURNAL и MODBUS_SERVER_FLAG_SETTINGS.

На верхнем уровне журнальные переменные будут доступны, начиная с адреса 49000 в том порядке, в котором они описаны в структуре vars и workvars_meta. Уставки будут доступны, начиная с адреса 50000 для секции 0, 51000 для секции 1, и. т. д.

В Modbus используется 16-битная адресация, в то время как и уставки, и журнальные переменные являются 32-битными. Поэтому адреса для них будут кратны 2. Так, определённые в структуре журнальные переменные var0, var1, var2 будут доступны по адресам 49000, 49002, 49004 соответственно.

Переменные в структуре modbus_data разного размера, поэтому адреса у них будут следующие: modbus_i16 — адрес 0, modbus_i32 — адрес 1, modbus_u64 — адрес 3, modbus_float — адрес 7. Итого, значение modbus_regions[0].count равно девяти 16-битным регистрам.

2.5.2 Функции

В основной функции инициализации smart_app_init() сервер Modbus RTU инициализируется функцией modbus_server_init():

// Инициализируем Modbus RTU-сервер.
modbus_server_init(&modbus_rtu_server);

2.6 Modbus RTU Client

Для клиента Modbus RTU доступны запросы на чтение регистров хранения («Holding Registers») и входных регистров («Input Registers»), а также на запись регистров хранения («Holding Registers»).

2.6.1 Структуры

Сначала необходимо определить массив структур запросов. Для каждого запроса (блока регистров) указывается адрес устройства, к которому будет осуществляться запрос (dev_id), адрес блока регистров (addr), количество 16-битных регистров (count), тип регистров (type, см. ниже), признак включения (работы) запроса (flags, может принимать значение MODBUS_REQUEST_FLAG_ENABLE, что означает включение запроса в работу, или 0, что запрещает обмен данными), ну и, наконец, адрес блока переменных (data_ptr).

Возможные типы регистров:

Тип Регистр
MODBUS_READ_HOLDING_REGS Чтение регистров хранения («Holding Registers»)
MODBUS_WRITE_HOLDING_REGS Запись регистров хранения («Holding Registers»)
MODBUS_READ_INPUT_REGS Чтение входных регистров («Input Registers»)

Пример:

int32_t mc_var0, mc_var1, mc_var2;

static modbus_request_t request_list[] = {
    { .dev_id = 1, .addr = 0, .count = 2, .type = MODBUS_READ_HOLDING_REGS,
    .flags = MODBUS_REQUEST_FLAG_ENABLE, .data_ptr = (uint16_t *) &mc_var0 },
    { .dev_id = 1, .addr = 2, .count = 2, .type = MODBUS_WRITE_HOLDING_REGS,
    .flags = MODBUS_REQUEST_FLAG_ENABLE, .data_ptr = (uint16_t *) &mc_var1 },
    { .dev_id = 1, .addr = 0, .count = 2, .type = MODBUS_READ_INPUT_REGS,
    .flags = MODBUS_REQUEST_FLAG_ENABLE, .data_ptr = (uint16_t *) &mc_var2 },
};

Затем нужно определить собственно структуру Modbus RTU-клиента:

// Описание структуры Modbus RTU-клиента.
static modbus_client_t modbus_rtu_client = {
    .iface_handle = SERIF_RS485_2,
    .default_baudrate = SERIF_BAUD_115200,
    .timeout = 50,
    .cycle_time = 10,
    .inter_query_delay = 0,
    .req_list = request_list,
    .req_count = sizeof(request_list) / sizeof(modbus_request_t),
};

В качестве интерфейса можно использовать либо RS-232, либо один из двух интерфейсов RS-485. Доступна любая скорость от 9600 до 921600 бод.

Параметры задержек и таймаутов:

Название параметра Описание
timeout Максимальное время ожидания ответа, мс
cycle_time Минимальное время цикла запросов, мс
inter_query_delay Задержка между запросами, мс

2.6.2 Функции

В основной функции инициализации smart_app_init() Modbus RTU-клиент инициализируется функцией modbus_client_init():

// Инициализируем Modbus RTU-клиент.
modbus_client_init(&modbus_rtu_client);

Для Modbus-клиента существует набор специальных параметров.

Название Тип Описание
MODBUS_REQUEST_PAR_ENABLE uint8_t Признак работы запроса
MODBUS_REQUEST_PAR_STATUS uint8_t Признак успешного обмена данными
MODBUS_REQUEST_PAR_LAST_ERROR uint8_t Детализация ошибки обмена данными
MODBUS_REQUEST_PAR_CNT_OK uint32_t Счётчик успешных запросов
MODBUS_REQUEST_PAR_CNT_ERROR uint32_t Счётчик неуспешных запросов

Функция получения параметра:

uint32_t modbus_client_get_param(const modbus_client_t *self,
    uint32_t request, modbus_request_param_t param, void *value);

Параметры функции:

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

Для этого используется функция modbus_client_set_param():

uint32_t modbus_client_set_param(const modbus_client_t *self,
    uint32_t request, modbus_request_param_t param, const void *value);

Параметры функции аналогичны параметрам для функции modbus_client_get_param().

2.7 Modbus TCP Server

2.7.1 Структуры

Для Modbus TCP-сервера точно так же, как и для Modbus RTU-сервра, объявляются структуры переменных и регионов.

Структура Modbus TCP-сервера объявляется следующим образом:

// Описание структуры Modbus TCP-сервера.
const modbus_tcp_server_t modbus_tcp_server = {
    .server_id = 1, // Идентификатор контроллера (обычно не используется).
    .tcp_port = 502, // Стандартный номер порта для Modbus TCP.
    .map = {
        .flags = (modbus_server_flags_t)
            (MODBUS_SERVER_FLAG_JOURNAL | MODBUS_SERVER_FLAG_SETTINGS),
        .regions = modbus_regions,
        .regions_count = sizeof(modbus_regions) / sizeof(modbus_region_t)
    }
};

Порт Modbus TCP обычно имеет значение 502. Остальные параметры такие же, как и для Modbus RTU-сервера.

2.7.2 Функции

В основной функции инициализации smart_app_init() сервер Modbus TCP инициализируется функцией modbus_tcp_server_init():

// Инициализируем Modbus TCP-сервер.
modbus_tcp_server_init(&modbus_tcp_server);

2.8 HTTP-сервер

В контроллерах СМАРТ реализован небольшой RESTful-сервер, поддерживающий запрос GET для получения значений переменных.

2.8.1 Структуры

Сначала необходимо создать т. н. путь («route») со списком описаний переменных. Здесь не нужно определять специальную структуру переменных, достаточно указателей на уже существующие переменные.

Название переменной может занимать максимум 11 байтов (+ завершающий 0). В кодировке UTF-8, используемой в СМАРТ-ОС, один символ может занимать один байт, а может состоять из двух, трёх или четырёх байтов. Это нужно учитывать при вводе имени для переменной. Ниже представлен пример, где для третьего описания имя состоит из 5 символов, которые занимают все 11 байтов (русская буква «П» — 2 байта, символ номера «№» — 3 байта, смайлик «😀» — 4 байта, символ дефиса «-» и единицы «1» — по 1 байту).

Поле .flags указывает тип переменной: float (VAR_META_FLOAT) или int32_t/uint32_t (VAR_META_INT). Поле .value содержит указатель на саму переменную.

 
// HTTP-путь /route-0.
const http_var_t http_route_0[] = {
       { .name = , .value = &vars.var0,
           .flags = VAR_META_FLOAT },
       { .name = , .value = &conf.set2,
           .flags = VAR_META_INT },
       { .name = 😀, .value = &modbus_data.modbus_i32,
           .flags = VAR_META_INT },
};

Далее определяется список всех путей (имя тоже должно содержать максимум 11 байтов):

// Список всех определённых путей.
const http_route_t http_routes[] = {
    { .name = "route-0", .vars = http_route_0,
      .count = sizeof(http_route_0) / sizeof(http_var_t) },
//  { .name = "route-1", .vars = http_route_1,
//    .count = sizeof(http_route_1) / sizeof(http_var_t) },
//  ...
//  { .name = "route-N", .vars = http_route_N,
//    .count = sizeof(http_route_N) / sizeof(http_var_t) },
};

Сама структура HTTP-сервера определяется следующим образом:

// Описание структуры HTTP-сервера.
const http_server_t http_server = {
    .tcp_port = 5000,
    .flags = (http_server_flags_t)
        (HTTP_SERVER_FLAG_JOURNAL | HTTP_SERVER_FLAG_SETTINGS),
    .routes = http_routes,
    .count = sizeof(http_routes) / sizeof(http_route_t)
};

Значение для TCP-порта по умолчанию 5000, можно указать другой. Для доступа к журнальным переменным и/или уставкам используются флаги HTTP_SERVER_FLAG_JOURNAL и HTTP_SERVER_FLAG_SETTINGS соответственно.

Строка запроса по протоколу HTTP в общем виде:

http://<ip-адрес контроллера>:<порт>/<название пути>

IP-адрес задаётся на контроллере. Для этого зайдите в меню контроллера «Настройки» → «Сеть» → «Ethernet», установите IP-адрес в вашей подсети, а также маску и шлюз. Настройки применятся после перезагрузки.

Настройки Ethernet на контроллере СМАРТ

Для описанного выше пути «/route-0» строка запроса будет такой:

http://192.168.77.100:5000/route-0

Ответ приходит в формате JSON (заголовок HTTP-ответа «Content-Type: application/json»):

 
[
       { : , :0.101325 },
       { : , : 4294967295 },
       { : 😀,: -2147483648 }
]

При установленном флаге HTTP_SERVER_FLAG_JOURNAL журнальные переменные будут доступны по адресу http://<ip-адрес контроллера>:<порт>/vars.

При установленном флаге HTTP_SERVER_FLAG_SETTINGS уставки доступны по адресу http://<ip-адрес контроллера>:<порт>/settings/<номер секции>; для секции 0, например: http://192.168.77.100:5000/settings/0.

2.8.2 Функции

В основной функции инициализации smart_app_init() HTTP-сервер инициализируется функцией http_server_init():

// Инициализируем HTTP-сервер.
http_server_init(&http_server);

В приложении А содержится код файлов smart-c.html и smart-c.js (также сами файлы находятся в папке smart-project/). На странице smart-c.html выводятся 3 журнальных значения с обновлением каждые 3 секунды.

Примечание. Подразумевается, что это те переменные, которые были описаны выше в структуре http_route_0, а для HTTP-сервера отмечен флаг HTTP_SERVER_FLAG_JOURNAL. URL запроса http://192.168.77.100:5000/vars, его можно скорректировать в smart-c.js.

В основной функции прикладной задачи smart_app_task() значения нужно инициализировать:

smart_app_task(void)
{
// ...

       vars.var0 = 0.101325f;
       conf.set2 = 4294967295u;
       modbus_data.modbus_i32 = (~0x7fffffff); // -2147483648

// ...
}

2.9 Версия и описание ПО

! Этот функционал применим для СМАРТ-ОС v2.8.0 и последующих версий.

! Реализация изменилась по сравнению с предыдущей версией!

Для программы вы можете указать версию и краткое описание.

Найдите структуру app_info в файле app.c.

// Версия данной прикладной программы и её описание.
const appver_t app_info = {
    .ver = { 0, 0, 1 }, // Версия ПО.
    .desc = {
        // Максимальное количество символов 18.
        // 3     10      18
        // Информация о прикладном ПО
        // (меню «Настройки» -> «О системе» -> «Прикладное ПО»):
        "Тестовая программа",
        "smart-project",
        "------------------",
        "------------------",
        // 3     10      18
        "------------------",
        "------------------",
        "------------------",
        "------------------",
        "------------------",
        // 3     10      18
        "------------------",
        "------------------",
        "------------------",
        "------------------",
        "------------------"
    }
};

В поле app_info.ver укажите версию ПО в формате, состоящем из трёх чисел (например, major.minor.release).

Описание ПО состоит из 14 строк. В журнале сохраняются максимум 18 символов на строку.

Версию и первые 3 строки можно увидеть в меню «Настройки» → «О системе» → «Прикладное ПО».

3 Пользовательский интерфейс

В СМАРТ-ОС при разработке пользовательского интерфейса в основном используется декларативный подход. Для удобства в заголовочном файле операционной системы smart-project/smart2-app/lib/inc/smart_hmi.h введены макроопределения, с помощью которых и описывается интерфейс пользователя.

Основным элементом описания является меню (menu), состоящее из элементов (items). Одно из таких меню является главным для прикладной задачи, оно вызывается при выборе пункта «Объект» главного меню операционной системы.

Главное меню операционной системы

В файле проекта smart-project/smart2-app/src/menu.c приведены примеры описаний меню.

3.1 Создание меню

Для создания меню используются макросы BEGIN_MENU(name) и END_MENU(name, text), где name — название меню, а text — заголовок, отображаемый в верхней строке экрана.

Между этими двумя макросами определяются элементы меню с помощью макроса
MENU_ITEM(text, type, flags, param, ptr).

Параметры макроса:

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

Тип элемента меню. Типы элементов описаны в следующем разделе.

Флаги элемента меню. Флаги ITEM_FLAG_RO и ITEM_FLAG_RW взаимоисключающие, в то время как ITEM_FLAG_SU может объединяться с одним из двух вышеперечисленных с помощью операции побитового «или» («|»).

  1. ITEM_FLAG_RO

    Флаг «только для чтения». Он не разрешает изменять значение уставки (тип ITEM_TYPE_SETTING), переменной (типы ITEM_TYPE_INT и ITEM_TYPE_FLOAT), комбобокса (тип ITEM_TYPE_COMBO), чекбокса (тип ITEM_TYPE_CHECK). Указывается для всех остальных типов элементов.

  2. ITEM_FLAG_RW

    Флаг «чтение/запись». Позволяет изменять значение уставки (тип ITEM_TYPE_SETTING), переменной (типы ITEM_TYPE_INT и ITEM_TYPE_FLOAT), комбобокса (тип ITEM_TYPE_COMBO) или чекбокса (тип ITEM_TYPE_CHECK). Для других типов установка этого флага не имеет значения.

  3. ITEM_FLAG_SU

    Флаг требует ввода пароля изготовителя (пункт «Доступ» главного меню операционной системы, см. описание меню СМАРТ-ОС). Для типов ITEM_TYPE_SETTING, ITEM_TYPE_INT, ITEM_TYPE_FLOAT, ITEM_TYPE_COMBO, ITEM_TYPE_CHECK  он устанавливается совместно с ITEM_FLAG_RW. Для типов ITEM_TYPE_ACTION, ITEM_TYPE_SUBMENU устанавливается совместно с ITEM_FLAG_RO.

Параметр типа uint32_t. Смысловое значение параметра и область допустимых значений зависят от типа элемента.

Указатель на данные. Тип данных также зависит от типа элемента.

3.2 Типы элементов меню

Для указания типа элемента меню используется перечисляемый тип menu_item_type_t. Здесь описаны все возможные идентификаторы, которые можно использовать для прикладных задач.

ITEM_TYPE_SETTING

Используется для отображения и (при наличии флага ITEM_FLAG_RW) изменения значения уставки. В поле ptr передаётся указатель на элемент массива метаописания уставок (см. структуру section0_meta).

В форматной строке вывода нужно указать спецификатор, соответствующий типу уставки (%d, %u, %f). Параметр param не используется.

MENU_ITEM("i32: %d", ITEM_TYPE_SETTING, ITEM_FLAG_RW, 0, &section0_meta[0]),
MENU_ITEM("u32: %u", ITEM_TYPE_SETTING, ITEM_FLAG_RW, 0, &section0_meta[1]),
MENU_ITEM("f: %0.1f", ITEM_TYPE_SETTING, ITEM_FLAG_RW, 0, &section0_meta[2]),

ITEM_TYPE_INT

Используется для отображения и (при наличии флага ITEM_FLAG_RW) изменения значения 32-битной целочисленной переменной. Указатель на переменную должен находиться в поле ptr.

Можно отобразить до 3-х последовательно расположенных целочисленных значений (то есть три последовательных значения целочисленного массива или три расположенных последовательно в структуре 32-битных значений типа int32_t/uint32_t). В этом случае в поле param указывается фактическое количество переменных.

int count;
uint32_t hms[3];
...
MENU_ITEM("Количество: %d", ITEM_TYPE_INT, ITEM_FLAG_RW, 0, &count),
MENU_ITEM("%02u час %02u мин %02u сек", ITEM_TYPE_INT,
    ITEM_FLAG_RO, 3, &hms[0]),

ITEM_TYPE_FLOAT

Используется для отображения и (при наличии флага ITEM_FLAG_RW) изменения значения переменной типа float. Указатель на переменную должен находиться в поле ptr.

Можно отобразить до 3-х последовательно расположенных значений типа float (то есть три последовательных значения массива или три расположенных последовательно в структуре значений типа float). В этом случае в поле param указывается фактическое количество переменных.

struct { float min; float max; } min_max;
...
MENU_ITEM("min:%5.2f max:%5.2f", ITEM_TYPE_FLOAT,
    ITEM_FLAG_RO, 2, &min_max.min),

ITEM_TYPE_BIT

Вывод значения одного бита 32-битной переменной, указать которую нужно в поле ptr. В поле param требуется указать номер бита от 0 до 31. В форматной строке достаточно спецификатора %d для вывода цифры 0 или 1.

ITEM_TYPE_BIT8

Вывод 8 младших битов 32-битной переменной. В форматной строке укажите 8 спецификаторов %d. Можно сгруппировать их, например, по четыре:

uint32_t bits;
...
MENU_ITEM("Bits: %d%d%d%d %d%d%d%d", ITEM_TYPE_BIT8, ITEM_FLAG_RO, 0, &bits),

ITEM_TYPE_STRING

Вывод на экран строки в кодировке UTF-8. Достаточно просто указать строку в качестве форматного вывода.

Дополнительно можно вывести строку в качестве аргумента. Для этого необходимо в строке форматного вывода указать спецификатор %s, а указатель на строку передать в ptr.

char str[] = "Тест123";
...
// Простой вывод строки.
MENU_ITEM("15°C м³ Δt 12€ √→", ITEM_TYPE_STRING, ITEM_FLAG_RO, 0, NULL),

// Строка str в качестве аргумента.
MENU_ITEM("Строка: %s", ITEM_TYPE_STRING, ITEM_FLAG_RO, 0, str),

Набор символов, которые можно отобразить на экране, зависит от используемого шрифта (см. библиотеку «Extended Functions»). Для стандартного шрифта набор отображаемых символов следующий:

 
!"#$%&’()*+,-./0123456789:;<=>?@
ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`
abcdefghijklmnopqrstuvwxyz{|}~
§©«®°±»×÷Δπ
АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ
абвгдеёжзийклмнопрстуфхцчшщъыьэюя
⁰¹²³⁴⁵⁶⁷⁸⁹⁺⁻₀₁₂₃₄₅₆₇₈₉₊₋
—…€₽№™←↑→↓↔︎↕−√∞≈≠≡≤≥▲►▼◄●

Для стандартного шрифта максимальное количество символов в строке равно 20. Если строка выходит за пределы экрана, то в конце строки отображается многоточие «…».

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

ITEM_TYPE_CHECK

Элемент чекбокс. Служит для управления состоянием (реже — просто отображения) одного бита 32-битной переменной. В ptr передаётся указатель на 32-битную целочисленную переменную, в param — номер управляемого бита.

В сочетании в флагом ITEM_FLAG_SU требует ввода пароля изготовителя.

MENU_ITEM("Состояние:", ITEM_TYPE_CHECK, ITEM_FLAG_RW, 2, &hms[0]),

ITEM_TYPE_COMBO

Элемент комбобокс. Позволяет выбирать один из нескольких вариантов. В сочетании в флагом ITEM_FLAG_SU требует ввода пароля изготовителя.

Для начала требуется определить список доступных вариантов с помощью макроопределений BEGIN_STRING_LIST и END_STRING_LIST.

BEGIN_STRING_LIST(variant_list)
    "Один",
    "Два",
    "Три",
    "Четыре",
    "Пять"
END_STRING_LIST

Затем нужно определить собственно объект комбобокса с помощью макроопределения DECLARE_COMBOBOX.

DECLARE_COMBOBOX(variant_combo, &conf.set1, variant_list)

В качестве первого параметра задаётся название комбобокса.

В качестве второго параметра передаётся указатель на 32-битную переменную, которая будет хранить индекс выбранного варианта. Как правило, для этого используется хранимая переменная (уставка).

Третьим параметром передаётся список доступных вариантов.

Теперь можно использовать комбобокс в меню.

// Дополнительно требуется ввод пароля изготовителя.
MENU_ITEM("Вариант: %s", ITEM_TYPE_COMBO, ITEM_FLAG_RW | ITEM_FLAG_SU,
    0, &variant_combo),

ITEM_TYPE_HEADER

Разделитель (заголовок) групп элементов меню. Представляет собой простой вывод строки.

MENU_ITEM("=== Группа 2 ===", ITEM_TYPE_HEADER, ITEM_FLAG_RO, 0, NULL),

ITEM_TYPE_SUBMENU

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

В ptr передаётся указатель на меню, которое требуется вызвать. Можно использовать макрос MENU_PROTOTYPE для extern-объявления меню.

// extern-объявление меню «Настройки».
extern MENU_PROTOTYPE(settings_menu)

// Вызов меню «Настройки» с вводом пароля.
MENU_ITEM("Настройки...", ITEM_TYPE_SUBMENU, ITEM_FLAG_RO | ITEM_FLAG_SU,
    0, &settings_menu),

// Описание меню «Настройки».
BEGIN_MENU(settings_menu)
    MENU_ITEM(...),
    MENU_ITEM(...),
    ...
END_MENU(settings_menu, "Настройки")

ITEM_TYPE_ACTION

Вызов произвольной функции.

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

// Сигнатура функции, вызываемой элементом типа ITEM_TYPE_ACTION.
typedef uint32_t (*action_t)(uint32_t param);

В качестве параметра функции передаётся параметр param.

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

extern uint32_t draw_custom_menu(uint32_t param);
...
MENU_ITEM("Собственное меню...", ITEM_TYPE_ACTION, ITEM_FLAG_RO,
    0, &draw_custom_menu),

3.3 Подключение обработчика пользовательского меню

Как уже было написано в главе «Основные функции программы», чтобы подключить пользовательское меню к системе, нужно вызвать обработчик главного пользовательского меню object_menu из функции задачи пользовательского интерфейса smart_object_frame:

extern MENU_PROTOTYPE(object_menu)

// Системная функция пользовательского интерфейса.
int smart_object_frame(void *p)
{
    menu_handler(&object_menu);
    return 0;
}

3.4 Редактируемые меню

Почти все объекты меню, описываемые в системе, являются константными. Таким образом, они не занимают место в оперативной памяти, а хранятся во FLASH.

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

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

Для примера создайте меню со списком трубопроводов и универсальное меню для трубопровода, которое будет меняться динамически:

extern uint32_t select_pipe(uint32_t param);

// Меню «Трубопроводы».
BEGIN_MENU(pipe_list_menu)
    MENU_ITEM("Трубопровод 1", ITEM_TYPE_ACTION, ITEM_FLAG_RO,
        0, &select_pipe), // param = 0 (Трубопровод 1)
    MENU_ITEM("Трубопровод 2", ITEM_TYPE_ACTION, ITEM_FLAG_RO,
        1, &select_pipe), // param = 1 (Трубопровод 2)
    MENU_ITEM("Трубопровод 3", ITEM_TYPE_ACTION, ITEM_FLAG_RO,
        2, &select_pipe), // param = 2 (Трубопровод 3)
END_MENU(pipe_list_menu, "Выбор трубопровода")

// Меню «Трубопровод N».
BEGIN_EDITABLE_MENU(pipe_menu)
    MENU_ITEM("Уставка: %0.1f", ITEM_TYPE_SETTING, ITEM_FLAG_RW, 0, NULL),
END_EDITABLE_MENU(pipe_menu, "Трубопровод N")

Здесь использованы макросы BEGIN_EDITABLE_MENU и END_EDITABLE_MENU из файла menu.h.

Добавьте по одной уставке pipe1, pipe2, pipe3 в структуру уставок conf и соответствующие им метаописания в ранее определённую секцию метаданных уставок section0_meta, где до этого уже были описаны три другие уставки:

// Ранее определённая секция метаданных уставок.
const var_meta_t section0_meta[] = {
    // Предыдущие уставки.
    DECLARE_INT_SETTING("", &conf.set1, 3, 0, 3600),
    DECLARE_INT_SETTING("", &conf.set2, 50, 10, 90),
    DECLARE_FLOAT_SETTING("", &conf.set0, 0, 0, 100.0f),

    // Новые уставки.
    DECLARE_FLOAT_SETTING("pipe1", &conf.pipe1, 1, -100.0f, 100.0f),
    DECLARE_FLOAT_SETTING("pipe2", &conf.pipe2, 2, -100.0f, 100.0f),
    DECLARE_FLOAT_SETTING("pipe3", &conf.pipe3, 3, -100.0f, 100.0f),
};

Теперь создайте список заголовков для каждого трубопровода:

BEGIN_STRING_LIST(pipe_titles)
    "Трубопровод 1",
    "Трубопровод 2",
    "Трубопровод 3",
END_STRING_LIST

В меню «Трубопроводы» вместо типа ITEM_TYPE_SUBMENU используется вызов функции ITEM_TYPE_ACTION. Смысл состоит в том, чтобы вызвать функцию select_pipe, которая предварительно настроит нужные параметры, а затем уже вызовет обработчик меню «Трубопровод N».

Собственно функция настройки меню трубопровода:

// В param находится номер трубопровода.
uint32_t select_pipe(uint32_t param)
{
    // Устанавливаем заголовок в зависимости от номера трубопровода.
    pipe_menu.title = STRING_LIST(pipe_titles)[param];

    // Устанавливаем указатель на соответствующую трубопроводу уставку.
    pipe_menu_items[0].ptr = &section0_meta[3 + param];

    // Вызываем обработчик меню «Трубопровод N».
    menu_handler((const menu_t *) &pipe_menu);

    return 0;
}

Последним шагом добавьте меню «Трубопроводы» в главное прикладное меню:

MENU_ITEM("Трубопроводы...", ITEM_TYPE_SUBMENU, ITEM_FLAG_RO, 0,
    &pipe_list_menu),

В файле menu.h также есть макрос для описания редактируемого комбобокса DECLARE_EDITABLE_COMBOBOX.

3.5 Собственный пользовательский интерфейс

В СМАРТ-ОС есть возможность самостоятельно реализовать отрисовку пользовательского интерфейса. Для рисования графических примитивов и вывода текста различными шрифтами используется библиотека «Extended Functions».

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

extern uint32_t custom_handler(uint32_t param);
...
MENU_ITEM("Собственное меню...", ITEM_TYPE_ACTION, ITEM_FLAG_RO,
    0, &custom_handler), // Обработчик собственного интерфейса.

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

Для начала нужно инициализировать буфер экрана функцией s_set_display_buffer и запустить цикл обработки (for (;;)).

uint32_t custom_handler(uint32_t param)
{
    keycode_t key;

    // Инициализируем буфер экрана.
    extern uint8_t lcd_buffer[LCD_BUFFER_SIZE];
    s_set_display_buffer(lcd_buffer);

    for (;;) { // Запускаем цикл обработки.

Далее нужно считать статус нажатых клавиш и очистить буфер экрана.

        // Считываем статус нажатых клавиш.
        key = kbd_get();
        
        // Очищаем буфер экрана.
        lcd_buffer_clear();

Как правило, клавиша ESC должна возвращать управление операционной системе:

        if (key == KB_ESC)
            break;

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

После всего нужно отобразить всё на экране и вызвать две системные функции.

        // Вывод на экран.
        lcd_fill();

        // Системные вызовы.
        hmi_wait();
        hmi_service();
    } // Конец цикла обработки.

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

    // Возвращаем стандартный шрифт и стиль для системы.
    s_text_set_style(true, false, S_TEXT_DEFAULT);
    s_text_set_font(FONT_06X08);

    return 0;
}

4 Библиотека «Extended Functions»

Библиотека содержит дополнительный функционал:

Подключение библиотеки:

#include "smart_ext.h"

Программная документация по библиотеке в формате HTML вызывается запуском файла smart-project/doc/ext-doc-html/index.html или ярлыком
"smart-project/doc/Extended Functions Documentation.lnk".

Также она есть на сайте micont.ru/soft/smart-c/doc/ext-doc-html/.

В составе проекта есть программа FontViewer (smart-project/doc/fonts/font-viewer/). С помощью неё можно посмотреть набор доступных для отображения символов для каждого из 14 шрифтов (файлы smart-project/doc/fonts/*.inc).

5 Меню СМАРТ-ОС

Главное меню операционной системы.

Главное меню операционной системы

Главное меню прикладной задачи.

Управление работой контроллера. Здесь можно запустить прикладную задачу и остановить её.

Управление журналом. Позволяет выгрузить журнал на USB-устройство, а также очистить журнал.

В данный момент в СМАРТ-ОС существует два уровня доступа к функционалу: «Оператор» и «Изготовитель». Для уровня доступа «Изготовитель» требуется ввод пароля. По умолчанию пароль 0000.

С помощью меню «Сменить ключ» можно поменять пароль.

Важно! Если вы забыли пароль, то единственный способ сбросить его — замкнуть джампером специальные контакты внутри корпуса.

Настройки

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

Позволяет задать контраст дисплея, а также отключить звук клавиш контроллера.

Позволяет настраивать параметры портов RS-232, RS-485-1, RS-485-2 и Ethernet.

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

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

Меню разработчика.

Содержит информацию о версии СМАРТ-ОС, о прикладном ПО, а также позволяет задать заводской номер контроллера.

6 Загрузка программы в контроллер

Результатом успешной сборки проекта является файл программы smart.bin в каталоге сборки. Вы можете загрузить файл smart.bin в контроллер двумя способами.

6.1 USB

Запишите файл на флеш-накопитель, вставьте его в USB-порт контроллера СМАРТ и перезагрузите контроллер с зажатыми клавишами «влево»  и «вправо» .

На экране контроллера отобразится следующая информация:

bootloader v0.3
usb init...done
mount...done
search smart.bin
found

Затем программа загрузится, после чего контроллер автоматически перезагрузится.

6.2 TFTP

Подключите контроллер к сети Ethernet. Зайдите в меню контроллера «Настройки» → «Сеть» → «Ethernet», установите IP-адрес контроллера в вашей подсети, а также маску и шлюз. Перезагрузите контроллер с зажатыми клавишами «влево»  и «вправо» .

На экране контроллера должна отобразиться следующая информация:

bootloader v0.3
usb init...done
mount...done
search smart.bin
not found
eth link...up
ipv4:<ip-адрес контроллера>
tftpd listen :69

Теперь воспользуйтесь любым клиентом TFTP.

1. Можно использовать TFTP-клиент, входящий в состав Windows 7, 8, 10. По умолчанию он не устанавливается, поэтому для установки откройте диалог «Выполнить», используя сочетание клавиш Win+R, введите команду OptionalFeatures и отметьте пункт «Клиент TFTP».

Установка клиента TFTP

Для загрузки программы выполните в командной строке (находясь в каталоге, где находится smart.bin):

tftp.exe -i <ip-адрес контроллера> PUT smart.bin

В случае успешной загрузки будет выведено следующее сообщение (на русскоязычной Windows):

Успешная передача: <размер smart.bin> байт за 2 сек., <количество байт/с>

2. Можно использовать свободно распространяемый (freeware) TFTP-клиент от WinAgents Software Group, также его можно скачать с нашего сайта tftp.exe. (А также он находится в папке проекта smart-project).

Скопируйте tftp.exe в папку, где находится smart.bin, например, и выполните в командной строке:

tftp.exe -i -t1 <ip-адрес контроллера> PUT smart.bin

В случае успешной загрузки будет выведено примерно следующее сообщение:

File smart.bin was transferred successfully.
<размер smart.bin> bytes transfered for 3 seconds, 0 bytes/second

6.3 Автоматизация загрузки

Можно заменить операцию запуска проекта «Build» → «Run» («Сборка» → «Запустить») на процедуру загрузки программы в контроллер.

Выберите в Qt Creator слева пункт «Projects» → «Run».

Конфигурирование запуска

Введите в качестве исполняемого файла (Executable):

%{CurrentBuild:QbsBuildRoot}\..\..\tftp.exe

Аргументы командной строки (Command line arguments), заменив IP-адрес и папку smart2-app.baed4fec на свои:

-i -t1 192.168.77.100 PUT smart2-app.baed4fec\smart.bin

Рабочая папка (Working directory):

%{CurrentBuild:QbsBuildRoot}

Теперь по нажатию Ctrl+R файл smart.bin будет отправляться по TFTP на контроллер.

Приложение А (проверка HTTP-сервера)

Файл smart-c.js

function ToInteger(x) {
    x = Number(x);
    return x < 0 ? Math.ceil(x) : Math.floor(x);
}

function modulo(a, b) {
    return a - Math.floor(a/b)*b;
}

function toUint32(x) {
    return modulo(ToInteger(x), Math.pow(2, 32));
}

function toInt32(x) {
    var uint32 = toUint32(x);
    if (uint32 >= Math.pow(2, 31)) {
        return uint32 - Math.pow(2, 32)
    } else {
        return uint32;
    }
}
async function get_vars() {
    const response = await fetch("http://192.168.77.100:5000/route-0");
    const json = await response.json();

    i = 0;
    for (const { name, value } of json) {
        document.getElementById("name" + i).innerHTML = name;
        var varTag = document.getElementById("value" + i);
        switch (i) {
            case 0:
                varTag.innerHTML = value;
                break;
            case 1:
                varTag.innerHTML = toUint32(value);
                break;
            case 2:
                varTag.innerHTML = toInt32(value);
                break;
            default:
                break;
        }

        i++;
    }
}

function on_timer() {
    let get_var_promise = Promise.resolve();
    get_var_promise.then(() => get_vars());
}

on_timer();
setInterval(on_timer, 3000);

Файл smart-c.html

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="utf-8" />
    <title>SMART</title>
    <script src="smart-c.js"></script>
</head>
<body>
    <div>
        <h2><u>route-0</u></h2>
        <p style="font-size: 100%;"><span
            id="name0">var0</span> = <span id="value0">0</span></p>
        <p style="font-size: 100%;"><span
            id="name1">var1</span> = <span id="value1">0</span></p>
        <p style="font-size: 100%;"><span
            id="name2">var2</span> = <span id="value2">0</span></p>
    </div>
</body>
</html>

Список изменений

Версия документации Версия СМАРТ-ОС Описание
1.0.0 v2.6.4 Первая версия документации.
1.1.0 v2.7.0 Добавлено описание «Modbus RTU Client».
1.2.0 v2.8.0 Добавлен тип контроллера СМАРТ-В04.
Изменился способ описания прикладного ПО и его версии.