Geosan

Июл 172019
 

Язык Си (без ++) — один из основных языков для программирования микроконтроллеров, поскольку здесь требуется высокая скорость, а оперативной памяти не бывает много.

Пример простой программы на Си для микроконтроллера AVR

Это текст программы компиляторов типа AvrStudio, CodeVisionAVR и т.п.

 #include // заголовочный файл для ввода-вывода #include #define BV(x) (1 << x) int main(void) { DDRC=0xFF; // порт PORTC настроен на выход PORTC=0xFF; // установка уровней на порте PORTC while(1) // начало цикла { PORTC=~(BV(5)); // переключение 5-го бита порта PORTC _delay_ms(1000); // задержка 1 секунда PORTC=BV(5); // переключение 5-го бита порта PORTC обратно _delay_ms(1000); } return 0; }  

Функция main — это точка входа в программу, с которой компьютер начинает выполнение программы.

Допускается из main возвращать void, хотя это не по стандарту, так что лучше int.

В функцию main можно передавать аргументы командной строки:

int main(int argc, char* argv[]) { }  

Вообще говоря, мы можем писать программу для MK AVR также на языке Processing/Wiring. Это тот же Си, но упрощенный. Но компилироваться это будет только в Arduino IDE или т.п., а потом можно загружать полученный hex в наш микроконтроллер. При этом не обязательно, чтобы МК стоял на плате Arduino. Разницы то нет.

Вот так выглядит аналогичная программа на Processing/Wiring:

int ledPin = 13; void setup() { pinMode(ledPin, OUTPUT); } void loop() { digitalWrite(ledPin, HIGH); delay(1000); digitalWrite(ledPin, LOW); delay(1000); }  

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

http://ar.com/basic/uno

а пока мы вернемся к языку Си.

Общая структура памяти программы на Си

— куча — для динамического выделения памяти

— стек — локальные переменные класса памяти auto (включая аргументы функций)

— DATA — константы

— CODE — исполняемый код, инструкции процессора

Типы данных в Си

-Базовые типы данных: char, int, float, double.

-Модификаторы знака: signed, unsigned.

-Модификаторы знака: long, short.

void — тип без значения

При этом следущие типы равны:

int = signed int = signed // 16 или 32 бит (зависит от платформы ) unsigned = unsigned int char = signed char	// 8 бит (от -128 до 127) (ASCII) unsigned char // 8 бит (от 0 до 255) wchar_t // UNICODE  

В Си логический тип реализован неявно (с помощью int): false = нуль, true = не нуль.

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

typedef тип имя

где тип — любой существующий тип данных, имя — новое имя для этого типа.

Пример: typedef unsigned char byte;

Преобразование типов:

Если операнды операции имеют разные типы, то происходит неявное приведение типов:

double a = 1.222; int i = a; // дробная часть отсекается! double x = 2/5;	// результат будет 0 !  

(чтобы здесь получить 0.4 нужно было бы написать x=2.0/5 или 2/5.0)

Явное приведение типов:

int a=2, b=5; double b = (double)a / b;	// результат будет b=0.4  

Принудительное преобразование типов:

(тип) выражение;

(желательно вообще избегать преобразования типов)

Переменные и константы

Переменная представляет собой блок памяти, на который мы ссылаемся по её имени (идентификатору).

Декларация переменных (вместе с инициализацией):

[класс памяти] [квалификаторы] [модификаторы] тип идентификатор = инициатор;

Например,

static const unsigned char x = 100;  

Здесь «;» — составляющая часть конструкции, завершающая часть.

Допустима (хотя и редко используется) запись: const x = 100; (по умолчанию int).

Квалификаторы (или «модификаторы доступа»): const, volatile.

const — означает, что переменные не могут изменяться во время выполнения программы; инициалиировать можно только при декларации;

volatile — содержимое переменной может измениться само собой (используется в многопоточных программах при взаимодействии процессов)

Возможен вариант const volatile, когда писать могут только снаружи.

Спецификторы хранения (описатель класса памяти): auto, register, extern, static.

auto — локальные переменных (по умолчанию) — программный стек.

register — просьба компилятору положить переменную в регистр ЦПУ (но он эту просьбу редко выполняет);

extern — объявление (declaration) переменных, но не определение (definition) (определение где-то в другом месте); определение может идти ниже по файлу (но как глобальная) или в другом файле.

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

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

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

Описание области действия идентификаторов (имен):

— внутреннее (локальное) — внутри блока {…}

— внешнее (глобальное) — вне всех блоков

Идентификатор, описанный внутри блока, известен только в этом блоке (локальный идентификатор).

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

Стоит избегать использования глобальных имен.

Переменные с классом памяти static видны только в пределах текущего блока (для локальных) или в пределах файла (для объявленных глобально).

Статические переменные хранятся в сегменте данных (data) и по умолчанию инициализируются нулем. Т.е. память под static-переменные выделяется при старте программы и существует до конца программы.

Замечание: Инициализация выполняется одни раз при выделении памяти!

void f(void) { static int a = 1; /* - это инициализация (но не присваивание!), т. е. переменная инициализурется единицей только один раз при старте программы! (или 0 по умолчанию, т.е. если бы было просто static int a;) */ a++; }  

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

Следует различать присваивание и инициализацию:

— Присваивание: имя_переменной = выражение;

— Многочисленное присваивание: x = y = z = 0;

— Инициализация переменных: тип имя_переменной = константа;

Константы

Константы являются частью машинных команд и под них память не выделяется.

Константы бывают:

— целые:

10-я система: 127; -127; +127;

8-я система: 0127; (начинается с нуля — значит 8-ричная!)

16-я система: 0x7F; (x или X, f или F — регистр не влияет)

— вещественные: 3.14; 2. ; .25 (0 можно опускать); 2E3; 2e3; 2E-3; 2.0E+3;

— символьные: 8-битные ASCII: ‘A’, ‘=’, ‘\n’, ‘\t’, ‘\370’, ‘\xF8’ (символ градуса);

— строковые литералы (в двойных кавычках): «Hello, world!\n». Строки заканчиваются нулевым байтом — ‘\0’.

Макроопределения:

 #define WIDTH 80	//(подробнее ниже)  

Операции и операторы

Оператор (инструкция, англ. statement) — это единица выполнения программы.

В языке Си любое выражение, заканчивающееся символом «точка с запятой» (;), является оператором.

Фигурные скобки { } — это составной оператор.

Например,

{y = x; x++;}  

Кроме того { } является отдельным блоком и в нем можно определять локальные переменные.

; — пустой оператор.

Операции:

— Арифметические операторы: — + * / %

— Инкрименты и декрименты: ++a, —a, a++, a— (могут выполняться быстрее)

— Операторы сравнения (отн-ний): > >= < <= == != (возвращают 1 или 0)

— Логические операторы: && || ! (возвращают 1 или 0)

— Битовые операторы: & | ^ ~ >> <<

— Оператор ?: x ? y : z, напр.: r = 10>9 ? 100 : 200

sizeof — унарный оператор для вычисления размера переменной или типа

, — оператор запятая (последовательное вычисление): a = (b=3, b+2);

Порядок выполнения операторов:

— Унарные операторы выполняются справа-налево.

— Бинарные выполняются слева-направо.

— Присваивание выполняется справа-налево.

Порядок можно менять с помощью скобок!

Примеры:

a=10; r=!!a==a; // результат будет 0, т. к. !!a вернет 1;  

Выражение а + b + c интерпретируется как (а + b) + с.

r = (2==2==2); // результат будет 0, т. к. 2==2 вернет 1 и сравнит с 2; - и вычисляется слева направо. 5 < 3 < 2;	// результат будет 1, т. к. 5<3 вернет 0; a = b = c = 2;	// сначала c=2, потом b=c, потом a = b;  

sizeof() — возвращает длину в байтах переменной или типа; sizeof(int); sizeof(a);

sizeof выражение; — само выражение не вычисляется.

Оператор запятая:

x = (y = 3, y+1);  

левая сторона оператора вычисляется как void и не выдаёт значения, переменной x присвается значение выражения в правой стороне, т.е. y+1.

Указатели и ссылки в Си

& — оператор «получение адреса» объекта;

* — доступ к значению объекта по указанному адресу;

p = &number; // - получение адреса переменной number; q = *p; // - получение значения переменной по указанному адресу: st.a; // - обращение к полю структуры st; pst->a;	// - обращение к полю структуры по её указателю;  

Указатели:

Указатель

Июл 172019
 

Форматный вывод на Си для микроконтроллеров.

Алгоритмы и программные решения
Форматированный ввод-вывод применяется очень широко, в первую очередь это, конечно, взаимодействие с пользователем, а так-же отладочный вывод, логи, работа с различными текстовыми протоколами и многое другое. В этой статье рассматриваются особенности применения форматированного вывода (ввод оставим на потом) для микроконтроллеров.
Первая программа написанная для ПК традиционно называется «Hello, world» и естественно пишет в стандартный вывод эту знаменитую фразу:

#include <stdio.h> int main(){ printf("%s", "Hello, world"); return 0; }

Первая программа для микроконтроллера обычно зовётся «Blinky» и она просто мигает светодиодом. Дело в том, что заставить работать традиционный «Hello, world» на микроконтроллере не так уж и просто для начинающего. Во первых, нет стандартного устройства вывода и его функциональность ещё нужно реализовать. Во вторых, не всегда бывает очевидно как подружить стандартные функции вывода с целевым устройством. Ситуация усугубляется тем, что в каждом компиляторе (тулчейне) это делается каким-то своим способом.

Форматный вывод.

Что-же, в общем, за зверь такой форматный вывод? Упрощенно говоря, это — вывод значений различных типов в виде текстовых полей.

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

Требования и особенности ввода-вывода для МК.

1. Гибкость. В отличии от старших братьев, для МК нет и не может быть стандартного устройства ввода-вывода. В каждой системе будет что-то своё, с уникальными требованиями и особенностями. В одной системе будет вывод в USART, во второй — на ЖК дисплей, в третей — в файл на SD карточке, в четвёртой — всё сразу. Значит система форматированного ввода-вывода должна быть достаточно гибкой, настраиваемой и независимой от аппаратных особенностей целевой платформы.

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

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

4. Доступность. Библиотека ввода-вывода должна быть в наличии для целевой платформы.

5. Функциональность часто ставится на последнее место в угоду скорости и компактности.

Стандартная библиотека Си.

Стандартным и единственно доступным средством форматированного вывода в Си является семейство функций: printf, sprintf, snprintf и fprintf.
Рассматривать будем функцию printf, как наиболее типичного и часто используемого представителя функций форматного вывода, при необходимости переходя к другим. Итак фунция printf имеет следующий синтаксис:

int ptintf(const char *fmt, ...);

Это обычная функция с переменным числом параметров. Здесь первый аргумент fmt является форматной строкой, которая содержит, как обычный текст, так и специальные управляющие последовательности. Троеточие (…) обозначает список дополнительных параметров, их может быть от нуля и до сколько поместится в стеке. Да, да все параметры в функцию printf передаются преимущественно в стеке (за исключением архитектуры ARM, где первые 4 параметра передаются в регистрах). Запихивает их в стек вызывающая функция, она-же инициализирует кадр стека перед вызовом и очищает после. Сама printf узнать сколько и каких параметров ей было передано может узнать только из форматной строки. При передачи параметров размером меньше (signed char, unsigned char, char и на некоторых платформах signed/unsigned short), чем int, они будут расширенны соответствующим расширением(знаковым или без-знаковым) до int-a. Это сделано для того, чтобы стек был всегда выровнен по границе машинного слова, а так-же уменьшает количество возможных ошибок при нестыковке размера фактического параметра и ожидаемого из форматной строки. Так-же параметры типа float при передаче в функции с переменным числом аргументов, приводятся к типу double.
Форматная строка содержит как символы непосредственно выводимые в поток, так и специальные управляющие последовательности, которые начинаются со знака «%». Управляющие последовательности имеют следующий формат:

%[флаги][ширина][.точность|длинна]спецификатор[дополнительный модификатор]

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

Флаги определяют дополнительные параметры форматирования (их может быть несколько):
Таблица 2. Флаги.

Ширина — десятичное число, задаёт минимальное количество символов выводимых для соответствующего параметра. Если выводимое значение содержит меньше символов, чем указано, то оно будет дополнено пробелами (или нулями если есть флаг «0») до нужной ширины слева или справа, если указан флаг «-«. Например:

printf("a =%10d", 123); Будет выведено ("_" - подчерк считать пробелом): a_=_ _ _ _ _ _ _123 printf("a =%-10d", 123); a_=123_ _ _ _ _ _ _ printf("a =%010d", 123); a_=0000000123 

Если вместо десятичного числа указать «*», то значение ширины будет считанно из дополнительного целочисленного параметра. Это позволяет задавать значение ширины поля вывода из переменной:

for(int i=1; i < 10; i++) printf("i =%0*d", i, i);

Здесь первый раз i передаётся в качестве ширины поля, второй — значения.
Точность или длинна — десятичное число после знака точки «.». В случае вывода целых чисел этот элемент означает минимальное количество записанных знаков, если выводимое число короче указанной длинны, то оно дополняется ведущими нулями, если число длиннее, то оно не урезается.

printf("a = %.6d", 123); a = 000123

Таким образом есть уже два способа вывести целое с нужным количеством ведущих нулей.
Для чисел с плавающей точкой в форматах «e», «E» и «f» этот элемент означает число знаков после десятичной точки. Результат округляется.

printf("f = %.2f", 12.3456); f = 12.35

Для форматов «g» и «G» это — общее количество выведенных значимых цифр.

printf("f = %.2g", 12.3456); f = 12

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

printf("%.5s\n", "Hello, world"); Hello

Также как и в элемента «ширина», в место точности можно поставить звёздочку и передать её значение в виде дополнительного целочисленного параметра:

char str[] = "Hello, world"; for(unsigned i = 1; i < sizeof(str); i++) printf("%.*s\n", i, str); H He Hel Hell Hello Hello, Hello, Hello, w Hello, wo Hello, wor Hello, worl Hello, world

Дополнительный модификатор служит для указания размерности типа:
h — применяется со спецификаторами i, d, o, u, x и X для знаковых и без-знаковых коротких целых short и unsigned short).
l — совместно со спецификаторами i, d, o, u, x и X означают длинные целые long и unsigned long).
l — совместно со спецификаторами s и c «широкие» многобайтные строку и символ соответственно.
L — обозначает тип long double, применяется со спецификаторами e, E, f, g и G.
В компиляторах поддерживающих тип long long, таких как GCC и IAR, часто есть для него нестандартный модификатор ll.
В стандарте С99 добавлены модификаторы «t» и «Z» для типов ptrdiff_t и size_t соответственно.

Работа над ошибками

Основным недостатком функций семейства printf считается вовсе не громоздкость и неторопливость — размер кода и накладные расходы на запихивание параметров в стек и разбор форматной строки обычно считаются приемлемыми, а подверженность ошибкам кодирования. Самое неприятное в этих ошибках то, что они могут быть неочевидными и проявляться не всегда, или не на всех платформах.
Большинство ошибок связано с несоответствием спецификаторов указанных в форматной строке с количеством и типами фактических аргументов. При этом можно выделить следующие ситуации:
— занимаемый в стеке размер параметров ожидаемых из форматной строки меньше размера фактически переданных. Типы фактические параметров совпадают с ожидаемыми. В этом случае просто выведутся параметры указанные в форматной строке, а лишние будут проигнорированы.
— занимаемый в стеке размер параметров ожидаемых из форматной строки меньше размера фактически переданных. Типы фактические параметров не совпадают с ожидаемыми. В этом случае параметры просто будут интерпретированы в соответствии с форматной строкой и в общем случае будет выведен мусор.
— размер фактических параметров меньше ожидаемых. Здесь поведение не определено и зависит от кучи разных факторов — от платформы, от компилятора, от содержимого стека на момент вызова printf. Поведение printf при этом может быть от внешне корректной работы и до чтения и даже записи произвольных участков памяти со всеми вытекающими последствиями.
Многие ошибки возникают при переносе кода с одной платформы на другую у которых отличаются размеры типов. Например:

printf("A = %x", 0x12345678); 

На платформах, где int имеет 32 бита этот код работает правильно, а где int — 16 бит — будут выведены только 2 младших или старших (в зависимости от порядка следования байт) байта.
К счастью некоторые компиляторы, например GCC, знают printf «в лицо» и выдают предупреждения в случае несовпадения фактических параметров с форматной строкой. Однако это работает только если форматная строка задана литералом. А если она хранится в переменной (например extern указатель инициализируемый в другом модуле), то компилятор бессилен и проследить соответствеи параметров может быто очень не просто.

Особенности реализаций

AVR

AVR достаточно неудобная платформа для работы со строками из-за того, что они могут располагаться в различных адресных пространствах и когда работаешь со строкой всегда нужно помнить где именно она расположена. В первую очередь нас естественно интересуют строки в RAM и во Flash.
Примеры для AVR рассчитаны для запуска на симуляторе sImulavr и используют его механизм отладочного вывода как стандартный вывод. Это позволит не отвлекаться на инициализацию устройств выводи и т. д. К тому-же не надо загружать прошивку в устройство для запуска.

AVR-GCC он-же WinAVR

В AVR-GCC, а точнее в avr-libc самая удачная, на мой взгляд, реализация стандартной библиотеки ввода-вывода Си. В ней имеется возможность выбирать необходимый функционал функций семейства printf. Они прекрасно работают со строками как RAM так и во Flash. Все функции семейства, включая snprintf и fprintf разделяют общий код и очень хорошо оптимизированы как по скорости, таки по объёму кода.
Для поддержки находящихся во Flash памяти строк введен новый спецификатор форматной строки %S — S — заглавная, строчная s по-прежнему означает строку в RAM. Но во Flash памяти может быть и сама форматная строка, для этого есть специальные модификации функций с суффиксом «_P» printf_P, fprintf_P, sprintf_P и т. д., которые ожидают форматную строку во Flash.
Для того чтобы printf заработала, нужно написать функцию вывода символа и определить файловый дескриптор стандартного вывода.

#include <avr/io.h> // для printf #include <stdio.h> // для PROGMEM #include <avr/pgmspace.h> // порт для отладочного вывода - определен в параметрах запуска симулятора #define special_output_port (*((volatile char *)0x24)) // прототип функции вывода символа static int my_putchar(char c, FILE *stream); // определяем дескриптор для стандартного вывода static FILE mystdout = FDEV_SETUP_STREAM( my_putchar, // функция вывода символа NULL, // функция ввода символа, нам сейчас не нужна _FDEV_SETUP_WRITE // флаги потока - только вывод ); // функция вывода символа static int my_putchar(char c, FILE *stream) { special_output_port = c; return 0; } // форматная строка во flash PROGMEM char str1[] ="Format str from flash\nlong = %15ld\nFlash string = %10S\n"; // просто строка во flash PROGMEM char str2[] = "string form flash"; __attribute((OS_main)) int main(void) { // инициализируем стандартный дескриптор stdout = &mystdout; // форматная строка в RAM и выводим строку из RAM printf("Format str from ram\nlong = %15ld\nRam string = %10s\n", 1234567890l , "string from RAM"); // форматная строка в flash и выводим строку из flash printf_P(str1, 1234567890l , str2); return 0; }

Помимо stdout есть стандартные дескрипторы stdin и stderr для стандартного ввода и стандартного вывода ошибок соответственно. Используя функцию fprintf можно явно указывать нужный файловый дескриптор и при необходимости можно определить сколько угодно устройств вывода:

static FILE lcd = FDEV_SETUP_STREAM( my_lcd_putchar, NULL, _FDEV_SETUP_WRITE); static FILE usart= FDEV_SETUP_STREAM( my_usart_putchar, NULL, _FDEV_SETUP_WRITE); … stdout = &lcd; stderr = &usart; … fprintf(stderr, "Hello, world!"); fprintf(stdout, "Hello, world!");

В avr-libc имеется три уровня функциональности библиотеки ввода-вывода:
1. нормальный — поддерживается вся упомянутая выше функциональность, кроме вывода чисел с плавающей точкой. Этот режим включен по умолчанию и каких либо опций для него указывать не надо. Поддержка чисел с плавающей точкой занимает много места, а нужна сравнительно редко. Приведенный выше пример, скомпилированный с этим уровнем функциональности, занимает порядка 1946 байт Flash памяти.
2. минимальный — поддерживаются только самые базовые вещи: целые, строки, символы. Флаги, ширина поля и точность, если они есть в форматной строке, разбираются корректно, но игнорируются, поддерживается только флаг «#». Пример, скомпилированный с этим уровнем функциональности, занимает порядка 1568 байт Flash памяти. Его вполне можно было-бы применить на контроллере с 2 Кб flash памяти. Включается указанием в командной строке компоновщика («Linker options» в AVRStudio, а не компилятора, как расплывчато указано в документации avr-libc) следующих опций:

-Wl,-u,vfprintf -lprintf_min

3. максимальный — полная функциональность, включая поддержку чисел с плавающей точкой. Включается опциями

-Wl,-u,vfprintf -lprintf_flt -lm

Скомпилированный пример занимает при этом 3488 байт.
Функции семейства printf из avr-libc не используют буферизацию, не считая буферов для конвертации чисел, и выводят символы в устройство по мере их обработки. Поэтому, если буферизация нужна, то ее можно реализовать самостоятельно в функции вывода символа, там можно реализовать и кольцевой буфер и все, что угодно. Также в этих функциях не используется динамическая память, что в нашем случае очень хорошо, зато активно используется стек. Попробуем определить максимальное использование стека в них. Для этого в приложенном архиве заходим в каталог AvrGccPrintf, компилируем проект посредством AVRStudio и запускаем симуляцию с помощью runSimul.cmd.

После открывает образовавшийся файл trace.log, находим значение указателя стека (SP) после входа в main (после пролога), находим минимальное значение SP (стек растёт вниз) и вычитаем из первого второе. У меня получилось 0x455 — 0x429 = 0x2c = 44 байта использует сама функция fprintf, плюс еще 8 байт в стеке занимают ее параметры, итого 52 байта. Еще 14 байт занимает один файловый дескриптор и ещё 6 байт три стандартных указателя на файловые дескрипторы (stdout, stdin, stderr). Итого 72 байта RAM только на вызов fprintf, без учета всего остального.
Также из файла trace.log можно узнать общее время выполнения функций и где процессор проводит его больше всего.

Подробнее о стандартной библиотеке ввода-вывода avr-libc можно здесь:
www.nongnu.org/avr-libc/user-manual/group__avr__stdio.html

IAR for AVR

Здесь есть несколько версий Си-шных библиотек, с отличающейся функциональностью:
— CLIB — относительно маленькая, но ограниченная библиотека. Нет поддержки файловых дескрипторов, локалей и многобайтных символов. Считается устаревшей.
— Normal DLIB — более новая. Так-же нет поддержки файловых дескрипторов, локалей и многобайтных символов, но есть некоторые плюшки из стандарта С99.
— Full DLIB — полная библиотека Си. Поддерживает всё согласно стандарту С99, но при этом очень объёмная. Рассматривать этот вариант не буду, так, как размер функции printf отсюда превышает доступные 4 Кб кода для бесплатной версии IAR KickStart for AVR.
В IAR имеется возможность выбирать возможности для printf. Для этого заходим с меню Project->Options далее в диалоге General Options->Library Configuration. В списке «Printf formatter» можно выбрать необходимый уровень функционала. Для CLIB это Large, Small, Tiny, для DLIB добавляется еще Full. По возможностям эти уровни примерно соответствуют аналогичным в avr-gcc, поэтому расписывать их не буду.

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

int putchar(int outchar);

Разберём полный пример. Он также предназначен для запуска на симуляторе.

#define special_output_port (*((volatile char *)0x24)) #define special_exitcode_port (*((volatile char *)0x020)) #include <ioavr.h> #include <stdio.h> // для printf_P #include <pgmspace.h> __flash char str1[] ="Format str from flash\nlong = %15ld\nFlash string = %10S\n"; __flash char str2[] = "Hello"; int putchar(int outchar) { special_output_port = outchar; return outchar; } int main(void) { printf("Format str from ram\nlong = %15ld\nRam string = %10s\n", 1234567890l , "Hello"); printf_P(str1, 1234567890l , str2); special_exitcode_port = 0; return 0; }

Тут выясняются две неприятные особенности. Во-первых функции printf и printf_P видимо не разделяют общий код и printf_P всегда использует максимальный форматтер, независимо от того, что выбрали в настройках для printf, занимая порядка 3500 байт кода. Поэтому приведенный пример не помещается в четыре бесплатных килобайта. Для проверки одну из функций надо закомментировать.
Во-вторых, ни printf, ни printf_P не умеют читать строки из flash памяти — для них нет спецификатора.
Размеры printf для различных уровней функциональности примерно соответствуют аналогичным у avr-gcc, где чуть меньше, где чуть больше — непринципиально. А вот использование стека в разы выше, минимальный размер стека данных, при котором printf заработала, составил 200 байт для DLIB и около 150 для CLIB. Так, что на контроллерах с 2 кб flash, 128 RAM использовать эти функции не получится.
Демо-проект находится в каталоге AvrIarPrintf. Для запуска симуляции, а точнее преобразования генерируемого IAR-ом hex-а в пригодный для потребления simulavr-ом elf, на машине должен быть установлен WinAVR (и прописан в переменной окружения PATH, естественно).

Mspgcc

В стандартной библиотеке mspgcc для форматного вывода реализованы только функции printf и sprintf, плюс еще нестандартная функция uprintf, которая принимает указатель на функцию вывода символа в качестве первого аргумента. Файловых дескрипторов там нет и в помине, выбирать уровень функциональности форматтеров тоже нельзя. При этом printf «весит» порядка двух килобайт так, что запустить её, скажем, на MSP43 Launchpad-е не получится.
Для работы printf нужно, вполне ожидаемо, определить функцию int putchar(int c):

int putchar(int c) { // вывод символа ... return c; } 
IAR for MSP430, IAR for ARM и может еще какой IAR

Вообще в реализациях стандартной библиотеки от IAR Systems всё довольно однообразно для различных платформ, что не может не радовать. Как правило есть минимум две версии стандартной библиотеки — одна урезанная без поддержки файловых дескрипторов, вторая — полная, соответственно, с их поддержкой. Зовутся они как правило «Normal» и «Full» соответственно. Так-же в каждой из них можно выбирать необходимый функционал разбора форматной строки, поддерживаемый функциями семейства printf. Варианты уже уже описаны для IARfor AVR: Large, Small, Tiny и Full.
Если выбрать вариант библиотеки без файловых дескрипторов, то для работы функций вывода нужно определить лишь функцию int putchar(int outchar).
Если используем вариант с дескрипторами, то определить нужно функции __write, __lseek, __close и remove.
Минимальная их реализация, например, для STM32 может выглядеть так:

// Посылает один символ в USART1. static void Putch(uint8_t c) { if(c == '\n') Putch('\r'); while(( USART1->SR & USART_SR_TXE) == 0); USART1->DR = c; } // Вывод буфера в файл/поток, где // handle - номер открытого файла, // buf - буфер с данными, // bufSize - размер буфера, // возвращает количество фактически записанных символов. size_t __write(int handle, const unsigned char *buf, size_t bufSize) { for (int i = 0; i < bufSize; i++) { Putch(buf[i]); } return bufSize; } // Установка текущей позиции в потоке. // Нам сейчас не нужно. int __lseek(int handle, int ptr, int dir) {return 0; } // Закрыть поток. int __close(int handle) { return -1; } // Удалить файл. // Не знаю зачем printf требует определения этой функции. int remove(const char *fname) { return -1; } 
ARM + NewLib

Большинство сборок GCC под ARM используют NewLib в качестве стандартной библиотеки Си. Это достаточно взрослый проект и хорошо соответствует Си-шным стандартам, но при этом она относительно «тяжела» и требовательна к ресурсам. Для настройки библиотеки под свои нужды используется механизм системных вызовов. О нём уже немного писалось тут ispolzuem-libc-newlib-dlya-stm32, поэтому подробно на них останавливаться не буду, а упомяну об особенностях.
Первое это требования к памяти. Все stdio функции из NewLib используют хитрую буферизацию и динамически распределяют память для своих внутренних нужд. А значит им нужна куча и не маленькая, а в целых 3 Кбайт. Плюс примерно 1500 байт на статические структуры и плюс около 500 байт стека. Итого только чтоб напечатать «Hello, world!» нужно порядка 5 Кб оперативки. Что как-бы чуть больше, чем много для STM32-Discovery, на которой я запускал тестовый пример, с её 8 килобайтами. Также при использовании таких функций как printf по зависимостям тянется практически вся stdio плюс функции поддержки плавающей точки. В итоге тестовая прошивка занимает чуть меньше 30 Кб памяти программ. Если отказаться от использования чисел с плавающей точкой, то вместо printf можно использовать её облегченный аналог iprintf. В этом случае объём тестовой прошивки будет около 12 Кб.
Если какая либо из функций stdio не сможет выделить необходимую её память из кучи, то она тихонечко свалится в HardFault без объяснений причин.
Еще один момент это буферизация. Она может несколько запутать. Вызываем:

printf("Hello, World!");

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

printf("Hello, World!\n"); ... printf("Hello again!"); fflush(stdout); 

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

setvbuf(stdout, NULL, _IONBF, 0); 

При этом потребление памяти кучи уменьшится более чем на 1.5 Кб.

Xprintf

Это реализация функций похожих на printf от товарища Chan-а. Качаем отсюда:
elm-chan.org/fsw/strf/xprintf.html
Библиотека содержит следующие функции для форматированного вывода — аналоги функций стандартной библиотеки Си:

void xputc (char c); void xputs (const char* str); void xfputs (void (*func)(unsigned char), const char* str); void xprintf (const char* fmt, ...); void xsprintf (char* buff, const char* fmt, ...); void xfprintf (void (*func)(unsigned char), const char* fmt, ...); 

Все функции написаны целиком на Си и их можно использовать практически на любом микроконтроллере. Однако они не учитывают особенностей некоторых МК, например, нет никакой поддержки строк во Flash памяти для семейства AVR, что не добавляет удобства использования. Для использования xprintf необходимо инийиализировать указатель xfunc_out, присвоив ему адрес функции вывода символа. Рассмотрим пример. Компилятор avr-gcc, проект AvrStudio, рассчитан на запуск в симуляторе simulavr.

#include <avr/io.h> // для PROGMEM и strncpy_P #include <avr/pgmspace.h> #include "xprintf.h" #define special_output_port (*((volatile char *)0x24)) #define special_input_port (*((volatile char *)0x25)) #define special_abort_port (*((volatile char *)0x22)) #define special_exitcode_port (*((volatile char *)0x020)) // функция вывода символа static void my_putchar(unsigned char c) { special_output_port = c; } // строки во Flash памяти PROGMEM char str1[] ="Format str from flash\nlong = %15ld\nFlash string = %10s\n"; PROGMEM char str2[] = "Hello form flash"; // буферы для временного копирования строк char buffer[60]; char buffer2[20]; __attribute((OS_main)) int main(void) { // инициализируем укзатель нашей функцией вывода символа xfunc_out = my_putchar; xprintf("Format str from ram\nlong = %15ld\nRam string = %10s\n", 1234567890l , "Hello"); xfprintf(my_putchar, "Hello, world!\n"); strncpy_P(buffer, str1, sizeof(buffer)); strncpy_P(buffer2, str2, sizeof(buffer2)); xprintf(buffer, 1234567890l , buffer2); special_exitcode_port = 0; return 0; }

Здесь для вывода строк из Flash памяти их приходится предварительно скопировать в оперативку.
Из достоинств этой библиотеки можно выделить компактность кода (~1600 байт для приведённого примера) и лёгкость использования на различных платформах и модифицировать. На этом достоинства заканчиваются. Из недостатков стоит отметить
относительно медленную работу, примерно в полтора-два медленнее чем стандартная printf из avr-libc, и несоответствие стандартам — не поддерживаются некоторые флаги (‘пробел’, ‘ #’, ‘+’ ), спецификаторы (i, p n), не считая флагов для чисел с плавающей точкой и т.д.
Потребление стека порядка 38 байт не считая аргументов.

Описание примеров.

В приложенном архиве находятся следующие примеры:

AvrGccPrintf

Проект для AvrStudio 4. Запускается на симуляторе Simulavr. Чтоб запустить нужно скомпилировать и выполнить скрипт «runSimul.cmd». В логе симуляции «trace.log» можно посмотреть времена выполнения функций и оценить расход стека.

AvrIarPrintf

Тоже, что и в предыдущем примере, только компилируется в IAR for AVR.

AvrXprintf

Проект для AvrStudio 4. Запускается на симуляторе Simulavr. Для вывода используется библиотека xprintf от ELM Chan.

Msp430GccPrintf

Коппилируется mspgcc. Проект для Code::Blocks. Запускать планировалось на MSP430 Launchpad с контроллером MSP430G2231. Пример должен считывать температуру со встроенного датчика и посылать ее через програмый usart. Но к сожалению функция printf из библиотеки mspgcc не помещается в имеющиеся 2 Кб памяти. По этому используется функция puts. которая выводит строчку «Hello, world!».

Msp430IarPrintf

IAR для MSP430. Запускается на MSP430 Launchpad. В отличии от mspgcc, минимальная версия printf умещается в 2Кб флеша MSP430G2231.

Stm32Format

GCC + NewLib. Работает на STM32 Discovery. собирается с помощью makefile. проверял с Yagarto и CodeSourcery.

IarArm

Работает на STM32 Discovery. Собирается с помощью IAR for ARM.

Итоги.

Преимущества использования стандартной библиотеки Си для форматного вывода:
— Стандартность. Этим всё сказано.
— Доступность. В каком-то виде есть практически в любом Си компиляторе.
— Разделение формата вывода и выводимых данных. Форматную строку легко вынести в отдельный файл/модуль, например для дальнейшей локализации.
— Хорошая функциональность.
Недостатки:
— Полностью стандартная реализация в большинстве случаев слишком «тяжела» для микроконтроллеров.
— Использование лишь одной функции, например printf, тянет за собой значительную часть библиотеки вывода, даже если реально используются только ограниченные возможности.
— Неаккуратное использование функций с форматной строкой может привести к трудно обнаруживаемым ошибкам кодирования.
— В каждом компиляторе используется какой-то свой способ для определения низкоуровневых функций вывода.

Июл 172019
 

Текстовые строки в Ардуино. Конвертирование данных в строки и наоборот. Класс String.

02.10.2016 Автор: ЭДУАРД

Язык C

Текстовые строки в Ардуино.

Текстовые строки в Ардуино.

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

В Ардуино признаком конца строки является число 0, в текстовом виде ‘\0’. При объявлении строковых переменных в некоторых случаях необходимо явно указывать признак конца строки, а в некоторых он формируется по умолчанию.

Способы объявления и инициализации текстовых строк.

char myStr1[10];

Объявлен символьный массив определенного размера. При заполнении его символами необходимо позаботиться о записи в конце строки байта со значением 0 – признака окончания строки.

char myStr2 [6] = {‘S’, ‘t’, ‘a’, ‘r’, ‘t’};

Объявлен массив и присвоено значение элементам. В конце строки компилятор прибавит признак конца строки автоматически.

char myStr3[6] = {‘S’, ‘t’, ‘a’, ‘r’, ‘t’, ‘/0’};

То же самое, только завершающий признак мы объявили явно.

char myStr4 [ ] = “Start”;

Массив инициализирован строковой константой. Компилятор автоматически задаст размер массива и добавит завершающий символ.

char myStr5 [6 ] = “Start”;

То же самое, только размер массива указан явно.

char myStr 6[20 ] = “Start”;

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

  • Строковые константы объявляются внутри двойных кавычек (”Start”).
  • Отдельные символы задаются внутри одинарных (‘S’).

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

char myStr7 [] = “Текстовая строка может быть”
“ объявлена”
“ таким образом”;

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

char * myStrArray = { “Сообщение 1”, “Сообщение 2”, “Сообщение 3”, “Сообщение 4”, “Сообщение 5”, “Сообщение 6”};

Массивы строк объявляют как массив указателей. Это связано с тем, что строки могут иметь разную длину, и приходится резервировать двумерный массив данных, рассчитанный на самую длинную строку. А указатели требуют одинаковое количество ячеек памяти.

Управляющие символы.

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

Код символа в HEX (DEC) Название Обозначение Описание
0 (0) Конец строки \0 Признак конца строки
0D (13) Возврат каретки \r Перемещает курсор в крайнюю левую позицию
0A (10) Перевод строки \n Перемещает курсор на одну строку вниз

Для того чтобы вывести текст с новой строки необходимо использовать символы ‘\r’ и ‘\n’.

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

// управляющие символы
void setup() {
Serial.begin(9600); // скорость 9600
}

void loop() {
Serial.println(«\r\n Begin \r\n next string»
«\r\n 3 empty strings \r\n\r\n\r\n»); // вывод сообщения в нескольких строках
delay(1000);
}

На экране монитора последовательного порта увидим:

Сообщение
Часто управляющие символы ‘\r’ и ‘\n’ применяют для завершения команды в символьном виде, например AT команды. Такой способ управления будем использовать в следующем уроке для драйвера шагового двигателя.

Конвертирование различных типов данных Ардуино в текстовую строку.

Задача преобразования различных типов данных в символьный вид и наоборот возникает:

  • при выводе данных на дисплей или индикаторы;
  • передаче данных на другие устройства, например компьютер;
  • некоторые электронные компоненты требуют обмена данными с помощью AT команд в символьном виде, например GSM модемы, WiFi модули и т.п.

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

Конвертирование данных в строку через встроенные функции классов ввода-вывода.

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

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

Преобразование Функция класса Sreial Описание
int в DEC текст print(int d) Преобразует переменную int в строку с десятичным представлением числа
int в DEC текст print(int d, DEC) Преобразует переменную int в строку с десятичным представлением числа
int в HEX текст print(int d, HEX) Преобразует переменную int в строку с шестнадцатеричным представлением числа
int в OCT текст print(int d, OCT) Преобразует переменную int в строку с восьмеричным представлением числа
int в BIN текст print(int d, BIN) Преобразует переменную int в строку с двоичным представлением числа
float в текст print(float d) Преобразует переменную float в строку с двумя знаками после запятой
float в текст print(float d, N) Преобразует переменную float в строку с N знаками после запятой

Например, конвертирование числа int в строку будет выглядеть так.

int x= 24562;
Serial.print(x);

Преобразование переменной float в строку можно выполнить так.

float x= 12.657;
Serial.print(x);

Преобразование будет выполнено при передаче данных на другое устройство. Функции класса Serial подробно описаны в уроке 12.

Если выводите данные на LCD дисплей с помощью библиотеки LiquidCristal, то для преобразования данных типа int можно использовать метод print (урок 23).

Преобразование Функция класса LiquidCristal Описание
int в DEC текст print(int d, DEC) Преобразует переменную int в строку с десятичным представлением числа
int в HEX текст print(int d, HEX) Преобразует переменную int в строку с шестнадцатеричным представлением числа
int в OCT текст print(int d, OCT) Преобразует переменную int в строку с восьмеричным представлением числа
int в BIN текст print(int d, BIN) Преобразует переменную int в строку с двоичным представлением числа

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

Конвертирование целочисленных данных в строку через функции itoa, ltoa, ultoa.

Функции простые, позволяют конвертировать числа целых форматов в текстовую строку.

itoa (int data, char* string, int radix); // преобразование int

ltoa (long data, char* string, int radix); // преобразование long

ultoa (unsigned long data, char* string, int radix); // преобразование unsigned long

  • data – это конвертируемая переменная;
  • char* string – указатель на строку (имя массива);
  • radix – система исчисления результата в строке:
    • 10 для DEC;
    • 8 для OCT;
    • 16 для HEX;
    • 2 для BIN.

Например, конвертирование переменой x типа int в строку myStr1 можно сделать так.

itoa(x, myStr1, 10); // в десятичном виде
itoa(x, myStr1, 8); // в восьмеричном виде
itoa(x, myStr1, 16); // в шестнадцатеричном виде
itoa(x, myStr1, 2); // в двоичном виде

Вот программа для проверки работы этих функций.

// проверка преобразования числа в текстовую строку
int x=0; // переменная, которая выводится
char myStr[20]; // текстовый массив

void setup() {
Serial.begin(9600); // скорость 9600
}

void loop() {
// подготовка буфера строки
for (int i=0; i<20; i++) {myStr[i]=’ ‘;} // заполнение пробелами
myStr[18]=’\r’; // возврат каретки
myStr[19]=’\n’; // перевод строки

// преобразование переменной int x
itoa(x, myStr, 10); // int -> DEC
//itoa(x, myStr, 8); // int -> OCT
//itoa(x, myStr, 16); // int -> HEX
//itoa(x, myStr, 2); // int -> BIN

// преобразование переменной long x
//ltoa(x, myStr, 10); // long -> DEC
//ltoa(x, myStr, 8); // long -> OCT
//ltoa(x, myStr, 16); // long -> HEX
//ltoa(x, myStr, 2); // long -> BIN

// преобразование переменной unsigned long x
//ultoa(x, myStr, 10); // long -> DEC
//ultoa(x, myStr, 8); // long -> OCT
//ultoa(x, myStr, 16); // long -> HEX
//ultoa(x, myStr, 2); // long -> BIN

Serial.write(myStr, 20);
x++;
delay(500);
}

В цикле каждые 0,5 секунд происходит:

  • Текстовая строка myStr заполняется пробелами, в конце добавляются управляющие символы возврат каретки и перевод строки.
  • Переменная x конвертируется одной из функцией. Результат оказывается в буфере myStr.
  • Функция Serial.write(myStr, 20); передает через последовательный порт 20 байтов массива myStr в виде байтов.
  • Прибавляется 1 к переменной x.

Чтобы проверить нужную функцию необходимо освободить ее от признака комментарий. Я проверил все.

Для вывода чисел с плавающей запятой можно опять предложить метод из урока 20.

Конвертирование данных в строку с помощью функции sprintf.

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

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

int sprintf( char *string, const char *format , argument1, argument2 … )

Функция возвращает число преобразованных символов. В случае ошибки возвращает число – 1.

  • argument – это переменные, которые необходимо преобразовать;
  • format – управляющая строка:

% [флаг] [ширина] тип_формата

Флаг и ширина — необязательные поля.

Тип формата Тип выходных данных
c Символ
s Символьная строка
d, i Целое десятичное число
u Целое без знаковое десятичное число
o Целое восьмеричное число
x Целое шестнадцатеричное число

Флаги.

Знак Действие
Выравнивание результата влево
+ Выводит знак числа
Пробел Выводит знак пробел перед положительными числами
0 Заполняет поле 0

Ширина – минимальный размер поля для вывода символов. Если длина числа меньше, то добавляются пробелы. Если перед шириной стоит 0, то добавляются нули.

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

int x= 125; int y= 34;

Функция Выведет в myStr
sprintf(myStr2,»%d»,x ); 125
sprintf(myStr2,»%5d»,x ); 125
sprintf(myStr2,»%05d»,x ); 00125
sprintf(myStr2,»%+05d»,x ); +00125
sprintf(myStr2,»Цикл %d закончен»,x ); Цикл 125 закончен
sprintf(myStr2,»Цикл %d закончен y= %d»,x,y ); Цикл 125 закончен y= 34
sprintf(myStr2,»%o»,x ); 175
sprintf(myStr2,»%x»,x ); 7d

Можете загрузить следующую программу и проверить работу sprintf в реальном контроллере Ардуино.

// проверка преобразования числа в текстовую строку
// с помощью sprintf
int x=0; // переменная, которая выводится
char myStr[20]; // текстовый массив

void setup() {
Serial.begin(9600); // скорость 9600
}

void loop() {
// подготовка буфера строки
for (int i=0; i<20; i++) {myStr[i]=’ ‘;} // заполнение пробелами
myStr[18]=’\r’; // возврат каретки
myStr[19]=’\n’; // перевод строки

// преобразование переменной int x в строку
sprintf(myStr,»%d»,x ); // int -> DEC
//sprintf(myStr,»%5d»,x ); // int -> DEC
//sprintf(myStr,»%05d»,x ); // int -> DEC
//sprintf(myStr,»%+05d»,x ); // int -> DEC
//sprintf(myStr,»Cycle %d is over»,x ); // int -> DEC с текстом
//sprintf(myStr,»%o»,x ); // int -> OCT
//sprintf(myStr,»%x»,x ); // int -> HEX

Serial.write(myStr, 20);
x++;
delay(500);
}

Конвертирование данных типа float в текстовую строку.

Самый простой способ преобразования float в текстовую строку – использование функции dtostrf.

char* dtostrf(double data, signed char width, unsigned char prec, char *string)

  • data – это конвертируемая переменная;
  • width – число значащих разрядов;
  • prec – число разрядов после запятой;
  • char* string – указатель на строку (имя массива).

float x= 12.728;
dtostrf(x, 2, 3, myStr3); // выводим в строку myStr3 2 разряда до, 3 разряда после запятой

Вот программа для проверки такого способа.

// проверка преобразования числа в текстовую строку
// с помощью dtostrf
float x=0; // переменная, которая выводится
char myStr[20]; // текстовый массив

void setup() {
Serial.begin(9600); // скорость 9600
}

void loop() {
// подготовка буфера строки
for (int i=0; i<20; i++) {myStr[i]=’ ‘;} // заполнение пробелами
myStr[18]=’\r’; // возврат каретки
myStr[19]=’\n’; // перевод строки

// преобразование переменной float x в строку
dtostrf(x, 2, 3, myStr);

Serial.write(myStr, 20);
x+= 0.01;
delay(500);
}

У меня работает.

Конвертирование текстовой строки в различные типы данных.

В следующем разделе речь идет об обратном преобразовании – текстовой строки в число.

Преобразование строки в данные с помощью функций atoi, atol, atof.

Хороший, удобный метод. Функции простые, имеют вид:

int atoi(const char* string); // преобразование в int

long atol(const char* string); // преобразование в long

double atof(const char* string); // преобразование в float

В качестве аргумента функций указывается указатель на строку с числом в десятичном виде. Возвращают – конвертированное значение числа.

Например, преобразование строки myStr4 в переменную x типа int будет выглядеть так.

int x;
x= atoi(myStr4);

Для чисел с плавающей запятой.

float x;
x= atof(myStr4);

Вот программа проверки atoi и atol.

// проверка преобразования текстовой строки в число
// с помощью atoi
char myStr[]= «123»; // текстовый массив

void setup() {
Serial.begin(9600); // скорость 9600
}

void loop() {
Serial.println(atoi(myStr)); // преобразование строки в int
//Serial.println(atol(myStr)); // преобразование строки в long
delay(1000);
}

А в этой программе я проверил работу atof.

// проверка преобразования текстовой строки в число
// с помощью atof
char myStr[]= «123.456»; // текстовый массив

void setup() {
Serial.begin(9600); // скорость 9600
}

void loop() {
Serial.println(atof(myStr),3); // преобразование строки в float
delay(1000);
}

Конвертирование текстовой строки в числа с помощью функции sscanf.

Функция является обратной функцией для sprintf с такими же недостатками и достоинствами. Но она позволяет конвертировать числа в восьмеричном и шестнадцатеричном форматах. Тип float эта функция на Ардуино не поддерживает.

int sscanf( char *string, const char *format , address1, address2 … )

Все аргументы и форматы такие же, как у sprintf. Только указываются адреса переменных (address1, address2 … ).

Конвертирование строки myStr5 в переменную x типа int будет выглядеть так.

int x;
sscanf(myStr5,»%d», &x); // для десятичных чисел
sscanf(myStr5,»%o», &x); // для восьмеричных чисел
sscanf(myStr5,»%x», &x); // для шестнадцатеричных чисел

Вот скетч проверки функции для целочисленных операций.

// проверка преобразования текстовой строки в число
// с помощью sscanf
int x;
char myStr[]= «123»; // текстовый массив

void setup() {
Serial.begin(9600); // скорость 9600
}

void loop() {
sscanf(myStr,»%d», &x);
Serial.println(x); // преобразование строки в int
delay(1000);
}

Класс String Ардуино.

В Ардуино существует класс String. Он предоставляет более широкие возможности для работы с текстовыми строками.

Надо четко различать:

  • char myStr [ ] = “Start”; — строка символов, т.е. массив типа char с завершающим признаком в конце;
  • String MyStr = Start”; — экземпляр класса String.

Принято имена текстовых строк начинать как обычные переменные с маленькой буквы (myStr), а экземпляры String – с большой буквы (MyStr).

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

Несколько основных функций класса String.

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

String()

Конструктор, создает экземпляр класса String. Объект типа String может быть создан из разных типов данных:

String MyStr1 = «Start»; // инициализация строковой константы
String MyStr2 = String(‘d’); // преобразование символа в объект String
String MyStr3 = String(«Start»); // преобразование строковой константы в объект String
String MyStr4 = String(MyStr1 + » in 10 sec»); // конкатенация двух строк
String MyStr5 = String(67); // использование целочисленной константы
String MyStr6 = String(analogRead(1), DEC); // использование int с основанием 10
String MyStr7 = String(346, BIN); // использование int с основанием 2
String MyStr8 = String(89, HEX); // использование int с основанием 16
String MyStr9 = String(millis(), DEC); // использование long с основанием системы счисления

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

toCharArray(*buf, length)

Копирует текст экземпляра класса String в указанный массив.

MyStr.toCharArray(*buf, length)

  • buf – указатель на массив;
  • length – количество символов.

MyStr.toCharArray(myStr, 10); // копируем 10 символов в массив myStr

int length()

Функция возвращает длину строки String в символах без учета завершающего признака нуля.

int len =MyStr.length(); // получаем длину строки MyStr

long toInt()

Функция преобразовывает объект String в целое число.

int x = MyStr.toInt(); // конвертирование строки MyStr в int

Конвертирование данных в строку String.

Для целочисленных форматов все очень просто.

String MyStr;
int x;
MyStr= String(x, DEC); // для десятичных чисел
MyStr= String(x, HEX); // для шестнадцатеричных чисел
MyStr= String(x, BIN); // для двоичных чисел

Программа для проверки.

// проверка преобразования числа в String
int x=0; // переменная, которая выводится
String MyStr;

void setup() {
Serial.begin(9600); // скорость 9600
}

void loop() {

MyStr= String(x, DEC); // int -> DEC
//MyStr= String(x, HEX); // int -> HEX
//MyStr= String(x, BIN); // int -> BIN

Serial.println(MyStr);
x++;
delay(500);
}

Для плавающей запятой надо использовать функцию dtostrf(). С помощью нее получить строку-массив, а затем занести ее в объект String.

float x=2.789;
String MyStr;
char myStr8[10];
dtostrf(x, 2, 3, myStr8); // выводим в строку myStr8 2 разряда до, 3 разряда после запятой
MyStr = myStr8;

Программа для проверки преобразования float.

// проверка преобразования числа в String
float x=0; // переменная, которая выводится
String MyStr;
char myStr[10];

void setup() {
Serial.begin(9600); // скорость 9600
}

void loop() {

dtostrf(x, 2, 3, myStr); // выводим в строку myStr 2 разряда до, 3 разряда после запятой
MyStr = myStr;

Serial.println(MyStr);
x += 0.01;
delay(500);
}

Конвертирование строки String в различные типы данных.

Для целых чисел используем функцию toInt().

String MyStr = «123»;
int x = MyStr.toInt();

Проверяем.

// проверка преобразования String в число
// с помощью toInt
String MyStr = «123»;

void setup() {
Serial.begin(9600); // скорость 9600
}

void loop() {
Serial.println(MyStr.toInt()); // преобразование строки в int
delay(1000);
}

Для плавающей запятой.

Получим данные из объекта String в массив и выполним преобразование функцией atof().

String MyStr = «34.123»;
char myStr8[10];
MyStr.toCharArray(myStr8, MyStr.length()); // копирование String в массив myStr8
float x = atof(myStr8); // преобразование в float

Программа для проверки.

// проверка преобразования String в число
// с помощью toInt
String MyStr = «34.123»;
char myStr[10];

void setup() {
Serial.begin(9600); // скорость 9600
}

void loop() {
MyStr.toCharArray(myStr, MyStr.length());
Serial.println(atof(myStr)); //
delay(1000);
}

Июл 172019
 

Arduino & Modbus

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

Modbus — самый известный и распространенный стандарт в промышленной автоматизации, его поддерживают миллионы устройств по всему миру, эти устройства легко интегрируется в единую сеть и стыкуются с огромным количеством готового программного обеспечения. Попробуем использовать его в нашем проекте?

Что нам необходимо знать об этом стандарте?
Протокол Modbus использует последовательные линии связи (например, RS232, RS485), а протокол Modbus TCP рассчитан на передачу данных по сетям TCP/IP.
Протокол Modbus имеет два режима передачи RTU и ASCII, в режиме ASCII каждый байт передается как два ASCII символа его шестнадцатеричного представления.
В сети Modbus есть только один ведущий, который с заданным интервалом опрашивает несколько ведомых устройств, каждое из которых имеет свой уникальный адрес от 1 до 254, адрес 0 широковещательный и на него отвечают все устройства, так как ведущий в сети один у него нет своего адреса.
В спецификации Modbus определено два типа данных, один бит и 16 битное слово. Данные организованны в четыре таблицы с 16 битной адресацией ячеек, адресация в таблицах начинается с 0. Для доступа к данным из разных таблиц предназначены отдельные команды.

Discrete Inputs 1 бит только чтение
Coils 1 бит чтение и запись
Input Registers 16 бит только чтение
Holding Registers 16 бит чтение и запись

Как нам подключить Modbus устройство к OpenHAB? За это отвечает модуль Modbus Tcp Binding, этот модуль работает в режиме ведущего и обеспечивает подключение нескольких ведомых устройств через последовательный порт или TCP/IP сеть.
Для того чтобы связать с ним Arduino нам необходимо реализовать в контроллере ведомое Modbus устройство, воспользуемся для этого библиотекой Modbus-Master-Slave-for-Arduino.

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

Рассмотрим на примере нашего скетча основные шаги необходимые для работы с этой библиотекой.

Все функции библиотеки реализованы в одном файле ModbusRtu.h.
Для взаимодействия с ней, в программе нужно создать объект, задав в его конструкторе Modbus адрес, номер последовательного порта, номер выхода, управляющего передачей (для RS485)

Modbus slave(ID, 0, 0); 

Затем определить массив регистров Modbus

uint16_t au16data[11]; 

После этого, при старте программы настроить последовательный порт ведомого

slave.begin(9600); 

В основном цикле программы необходимо вызывать функцию обработки Modbus сообщений

state = slave.poll(au16data, 11); 

И после этого можно обработать полученные данные и сохранить необходимые переменные в регистрах Modbus.

#include "ModbusRtu.h" #define ID 1 // адрес ведомого #define btnPin 2 // номер входа, подключенный к кнопке #define stlPin 13 // номер выхода индикатора работы // расположен на плате Arduino #define ledPin 12 // номер выхода светодиода //Задаём ведомому адрес, последовательный порт, выход управления TX Modbus slave(ID, 0, 0); boolean led; int8_t state = 0; unsigned long tempus; // массив данных modbus uint16_t au16data[11]; void setup() { // настраиваем входы и выходы io_setup(); // настраиваем последовательный порт ведомого slave.begin( 9600 ); // зажигаем светодиод на 100 мс tempus = millis() + 100; digitalWrite(stlPin, HIGH ); } void io_setup() { digitalWrite(stlPin, HIGH ); digitalWrite(ledPin, LOW ); pinMode(stlPin, OUTPUT); pinMode(ledPin, OUTPUT); pinMode(btnPin, INPUT); } void loop() { // обработка сообщений state = slave.poll( au16data, 11); // если получили пакет без ошибок - зажигаем светодиод на 50 мс if (state > 4) { tempus = millis() + 50; digitalWrite(stlPin, HIGH); } if (millis() > tempus) digitalWrite(stlPin, LOW ); //обновляем данные в регистрах Modbus и в пользовательской программе io_poll(); } void io_poll() { //Копируем Coil[1] в Discrete[0] au16data[0] = au16data[1]; //Выводим значение регистра 1.3 на светодиод digitalWrite( ledPin, bitRead( au16data[1], 3 )); //Сохраняем состояние кнопки в регистр 0.3 bitWrite( au16data[0], 3, digitalRead( btnPin )); //Копируем Holding[5,6,7] в Input[2,3,4] au16data[2] = au16data[5]; au16data[3] = au16data[6]; au16data[4] = au16data[7]; //Сохраняем в регистры отладочную информацию au16data[8] = slave.getInCnt(); au16data[9] = slave.getOutCnt(); au16data[10] = slave.getErrCnt(); } 

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

40f0a878912f4bf4930ded14fe21e707.JPG

Для демонстрации работы с разными регистрами, в процессе работы программы данные из регистра с типом coil будут скопированы в регистр с типом discrete, а из регистров с типом holding в регистры с типом input. Кроме этого состояние кнопки будет сохранено в третий бит регистра au16data[0] (discrete), а значение третьего бита регистра au16data[1] (coil) выведено на светодиод.

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

6ff226483d9448a5b787af24c94286c2.JPG

Пора приступать к испытаниям. Значительно облегчает работу на этом этапе эмулятор Modbus мастер-устройства, в сети есть несколько хороших, при этом бесплатных программ, вот некоторые из них:
www.focus-sw.com/fieldtalk/modpoll.html
qmodbus.sourceforge.net
www.mikont.com/products/EAT-Console.html

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

8184e77240054ca3a681087d35db8388.JPG

Для установки эмулятора нужно скачать архив и распаковать его в папку C:\arduino\EATConsole, затем открыть страницу загрузки Eclipse, скачать Eclipse IDE for Java Developers и распаковать его в папку C:\arduino\eclipse, после этого скопировать файлы из папки C:\arduino\EATConsole\eclipse\plugins в папку C:\arduino\eclipse\plugins.

Для создания конфигурации необходимо запустить C:\arduino\eclipse\eclipse.exe, создать пустой проект, скопировать в него пустой файл C:\arduino\EATConsole\menu.ptmenu и добавить в редакторе пункты в соответствии со следующей таблицей. Если же вы скачали проект из репозитория, то в нём, в папке EATConsole уже есть подготовленный файл menu.ptmenu.

Type Address Bit Name Point Slave
Display Boolean 0 0 DT0 1
Display Boolean 0 1 DT1 1
Display Boolean 0 2 DT2 1
Display Boolean 0 3 BTN 1
Input Boolean 1 0 CL16 1
Input Boolean 1 1 CL17 1
Input Boolean 1 2 CL18 1
Input Boolean 1 3 LED 1
Display Integer 2 INPT3 0 1
Display Integer 3 INPT4 0 1
Display Integer 4 INPT5 0 1
Display Integer 5 HOLD6 0 1
Display Integer 6 HOLD7 0 1
Display Integer 7 HOLD8 0 1

Type — тип элемента меню EATConsole.
Address — адрес регистра данных.
Bit – номер бита в регистре.
Name – название элемента меню.
Point – количество десятичных знаков после точки.
Slave – Modbus адрес контроллера.

add9437d0a2f4e13a987d841bc8c993e.JPG

Теперь сохраним и скопируем файл menu.ptmenu в каталог C:\arduino\EATConsole, для этого можно щёлкнуть правой кнопкой мыши на файле прямо в Eclipse, выбрать в контекстном меню пункт “Copy”, а затем вставить в проводнике в папку C:\arduino\EATConsole.

После этого запустим C:\arduino\EATConsole\EATConsole.exe, настроим последовательное соединение, выбрав пункт меню Файл\Настройки, в диалоговом окне укажем номер порта, скорость 9600, 8 бит данных, 1 стоповый бит.

351170a62856416da0b7a5f82885e5c7.JPG

*Программа работает с портами с 1 по 8 и если USB переходник Arduino встал на порт с большим номером, придётся открыть диспетчер устройств Windows и изменить номер порта для него.

Когда все настройки будут введены, нажмите кнопку “Установить”, сразу после этого программа начнёт опрос устройства и если что-то пошло не так появится сообщение – НЕТ СВЯЗИ. Если же всё было сделано правильно и связь есть в чём можно убедиться по миганию индикатора статуса (светодиод на выводе 13), то пора приступить к испытаниям нашего контроллера.

Попробуем поменять значение в регистрах HLD0…HLD2 и СL0…СL2, при этом должно поменяться значение в соответствующем регистре IN0..IN2 и DT0..DT2, затем нажмём на кнопку макета, при этом должно поменяться значение в поле BTN, щёлкнем по полю LED, при этом должен загореться или потухнуть светодиод.

e458a8cc65be4a059abd2734578c4336.JPG

Что мы получили в результате нашей работы:

1 познакомились с азами протокола Modbus;
2 создали скетч, который превращает Arduino в Modbus slave устройство и позволяет читать и записывать несколько Modbus регистров с разными типами данных;
3 протестировали обмен с контроллером при помощи эмулятора функций Modbus master устройства, для которого создали конфигурацию соответствующую структуре регистров контроллера.

Выводы
Библиотека Modbus-Master-Slave-for-Arduino проста в использовании, позволяет создать ведомое Modbus устройство, которое корректно работает с программным эмулятором. Скомпилированный пример занимает немногим более 5кб памяти программ, так что в контроллере остаётся достаточно места для добавления необходимого функционала.

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

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

В следующий раз займёмся подключением контроллера к платформе OpenHAB.

Июл 172019
 

Как подружить OpenHAB и Arduino

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

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

Arduino подходит для локального контроля и управления в доме, в сети есть масса проектов для этого — отлично, но недостаточно, ведь нужно мыслить глобально! Нам нужен выход в сеть и мобильный интерфейс!

Хорошо, что благодаря этой статье мы уже знакомы с OpenHAB — платформой с открытым исходным кодом, объединяющей большое количество устройства с разными протоколами в единую сеть. OpenHAB реализован на Java, поэтому работает в различных ОС, его можно запустить на одноплатном компьютере и даже роутере, в нём есть мобильный и Web интерфейс. Звучит как хороший набор заклинаний против наших, надоевших уже граблей, проверим?

Вначале установим OpenHAB, откроем страницу загрузки, скачаем Runtime core и Demo setup, распакуем их в C:\openhab. При распаковке Demo setup разрешим замену файла README.txt. Если на компьютере отсутствует Java (проверим командой java –version), то инсталлируем ее в соответствии с инструкцией.

Теперь запустим OpenHAB, выполнив C:\openhab\start.bat, подождём немного и откроем Web интерфейсlocalhost:8080/openhab.app?sitemap=demo
(если нам потребуется остановить OpenHAB — нажмём Ctl+C или закроем консоль программы)

image

Работает! Продолжим. Наша цель поиск простого решения, поэтому начнём с экспериментов. В одном из комментариев к статье предлагалось подключить контроллер к OpenHub через последовательный порт использовав Serial-Binding. Вполне интересное решение, попробуем его реализовать.

Для установки дополнения открываем страницу загрузки, качаем Addons, распаковываем org.openhab.binding.serial-1.6.1.jar (версия плагина может отличаться) в папку C:\openhab\addons. При желании читаем документацию.

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

Качаем и устанавливаем эмулятор com0com, запускаем утилиту image Setup из меню программы и видим, что при установке уже создана виртуальная пара последовательных портов — у меня это COM35 + COM36. Будем использовать их в нашей работе.

image

В качестве терминала я использовал Hercules SETUP utility, это бесплатная программа позволяющая выбрать порты с большими номерами и ввести несколько команд, отправляя их нажатием соответствующей кнопки. Скачиваем её, запускаем hercules_3-2-8.exe, переходим на вкладку “Serial”, выбираем второй порт виртуальной пары (у меня это COM36) и вводим следующие настройки – скорость 9600, 8 бит, 1 стоповый бит, нет контроля четности. Открываем порт.

45e089736561407590c4e72a3a1b8345.JPG

Теперь нужно сконфигурировать openHAB, для этого воспользуемся его дизайнером. Откроем страницу загрузки, скачаем openHAB Designer и распакуем его в папку C:\openhab\designer, запустим C:\openhab\designer\openHAB-Designer.exe и укажем папку, в которой лежит конфигурация — C:\openhab\configurations.

d75109973e294e7d8f0d3228fd21944c.JPG

Сначала создадим элемент (item) и свяжем его с первым в виртуальной паре последовательным портом (у меня это COM35). Для этого добавим следующий код в конец файла demo.items, после чего щёлкнем правой кнопкой мыши в окне редактора и скомандуем “Save”, вместо этого можно просто нажать ctl+S.

String Serial_string "Текст [%s]" { serial="COM35" } 

55844677eb6348e58daa3c751821998f.JPG

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

rule "Test serial string" when System started or Time cron "0 * * * * ?" then Serial_string.sendCommand("Test") end 

3c2e558e31da4803ab1acb084b8405c0.JPG

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

1d840e45d8d24c8dad6b90be8b78529e.JPG

Попробуем теперь отправить команды в OpenHAB. Введём в терминале, в первое поле панели “Send” текст “ON_1”, а во второе “OFF_1”. Нажмём первую кнопку “Send”, затем вторую. В результате этого в консоли появляются следующие сообщения:

ef1720dbf7764275acbf7125049a4057.JPG

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

rule "Serial control" when Item Serial_string received update then if(Serial_string.state=="ON_1") sendCommand(Light_FF_Corridor_Ceiling, ON) if(Serial_string.state=="OFF_1") sendCommand(Light_FF_Corridor_Ceiling, OFF) if(Serial_string.state=="ON_2") sendCommand(Light_GF_Kitchen_Ceiling, ON) if(Serial_string.state=="OFF_2") sendCommand(Light_GF_Kitchen_Ceiling, OFF) end 

Откроем WEB интерфейс, щёлкнем по пункту “First Floor” затем по пункту “Corridor” или скопируем в браузер ссылку: localhost:8080/openhab.app?sitemap=demo#_FF_Corridor, отправим текст “ON_1”, а потом OFF_1” из терминала. Проконтролируем выключатель в WEB приложении, его состояние должно изменяться.

c76ab6d5c0714466af9ae0759c6eddec.JPG

Теперь нажмём “Home”, выберем пункт “Ground Floor” затем пункт “Kitchen” или скопируем в браузер ссылку: localhost:8080/openhab.app?sitemap=demo#_GF_Kitchen, отправим текст “ON_2”, а затем “OFF_2” из терминала. Проконтролируем выключатель в WEB приложении, его состояние также должно изменяться.

a1406434fb484363a0771592a4592952.JPG

Результат нашего эксперимента можно наблюдать в консоли приложения, в которой появятся следующие сообщения:

e780a15d9096473f8b77e7dc81830063.JPG

На компьютере всё работает, пора подключаться к реальному миру!

В большинство плат Arduino уже встроен переходник с COM порта на USB, поэтому у нас не возникнет вопросов с присоединением к компьютеру или ноутбуку. Из подручных средств соберём макет, для этого нам понадобится контроллер Arduino, макетная плата, провода, кнопка, светодиод, резисторы 10кОм и 1кОм.

43d3ff91a9f140aeb677cf2ec27a132c.JPG

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

Откроем редактор, добавим в него следующий код и сохраним скетч в файле btn.ino.

// задаем константы const int buttonPin = 2; // номер входа, подключенный к кнопке const int ledPin = 13; // номер выхода светодиода // переменные int buttonState = 3; // переменная для хранения состояния кнопки int buttonCnt = 0; // переменная для защиты от дребезга кнопки void setup() { // инициализируем пин, подключенный к светодиоду, как выход pinMode(ledPin, OUTPUT); // инициализируем пин, подключенный к кнопке, как вход pinMode(buttonPin, INPUT); // настраиваем последовательный порт Serial.begin(9600); } void loop(){ // обработка дребезга кнопки delay(1); if (buttonState != digitalRead(buttonPin)) buttonCnt++; else buttonCnt = 0; if (buttonCnt > 100) { // считываем значения с входа кнопки buttonState = digitalRead(buttonPin); buttonCnt = 0; // проверяем нажата ли кнопка // если нажата, то buttonState будет LOW: if (buttonState == LOW) { // включаем светодиод digitalWrite(ledPin, HIGH); // отправляем команду Serial.print("ON_1"); } else { // выключаем светодиод digitalWrite(ledPin, LOW); // отправляем команду Serial.print("OFF_1"); } } } 

c37e5c7d6e6f4701b713ea12f9813023.JPG

Компилируем и загружаем скетч в Arduino, открываем монитор порта, выбираем скорость 9600 и пробуем нажать кнопку на плате. В результате мы можем наблюдать получение команд:

2969056c698445fa92846315c407a974.JPG

Теперь закроем монитор порта и настроим связь OpenHAB с контроллером, для этого отредактируем настройки нашего элемента (в моём случае нужно поменять значение на COM18 так как USB переходник Arduino встал на этот порт).

String Serial_string "Текст [%s]" { serial="COM18" } 

В завершении проконтролируем прохождение команд при помощи консоли и WEB интерфейса.

Что мы получили в результате нашей работы:

1 установили, настроили OpenHAB, научились добавлять элементы (item) и автоматизировать обработку при помощи правил (rule);
2 научились тестировать обмен при помощи программных средств. Эти инструменты помогут в дальнейшем при поиске неисправностей и в постановке собственных экспериментов;
3 сделали простейший скетч для Arduino, подключили его к OpenHAB, создали правило обрабатывающее текстовые команды, протестировали возможность управления через последовательный порт.

Выводы:

Отправка команд в текстовом виде обычное дело в практике Arduino, обработку таких команд можно реализовать в OpenHAB поэтому такой способ взаимодействия заслуживает внимания и обсуждения.

Недостатком полученного решения является то, что команды ничем не разделены между собой кроме паузы, это может приводить к их “слипанию” и ненадёжной обработке. Для решения этой проблемы необходимо доработать скетч и правило обработки команд либо внести изменения в модуль Serial-Binding.

Многие назовут такой вариант взаимодействия с OpenHAB любительским, поэтому прежде чем продолжать работу над ним попробуем связать OpenHAB и Arduino через протокол Modbus.

Об этом в следующих публикациях:
Arduino & Modbus habrahabr.ru/post/249043
Arduino & OpenHAB habrahabr.ru/post/252555
Открытый контроллер умного дома на базе Arduino vk.com/myremoter

Июл 072019
 

Test Example Code

by Phillip Burgess

We have example code ready to go for these displays. It’s compatible with the Arduino Uno or Mega…but not other boards like the Leonardo, nor “Arduino-like” boards such as Netduino…programming gurus might be able to port it to other microcontrollers by adapting the C++ source, but as written it does some pretty low-level, non-portable things.

Continue reading »

Операция «пучеглазка». (RGB LED Matrix Panels Test)

 Arduino  Комментарии к записи Операция «пучеглазка». (RGB LED Matrix Panels Test) отключены
Июл 022019
 

В данной заметке речь пойдёт о светодиодных панелях, используемых в рекламных вывесках и архитектурных ТВ экранах.
Вот примерно таких:
bea69c.jpg

Continue reading »

si4463 описание

 Arduino  Комментарии к записи si4463 описание отключены
Мар 292019
 

Описание АТ команд модулей:

Для настройки модуля необходимо перевести его в командный режим, для этого необходимо притянуть контакт «SET» к массе и подождать ~40ms. Контакт «SET» имеет подтягивающий резистор на 10к. В командном режиме последовательный порт сконфигурирован на 9600bps. Если параметры модуля были изменены, после выхода из командного режима, они будут применены через ~80ms.

Continue reading »