Приветствую всех жителей и гостей кибер-города Датагор! Работа устройства на базе микроконтроллера часто требует двустороннего обмена данными с приложениями на компьютере или смартфоне. Реализации проводного и беспроводного вариантов такого обмена и будет посвящена статья. В данной части рассматривается проводной обмен посредством связки протоколов USB-UART.
Примем в качестве примера устройство: • передающее с заданной периодичностью значение температуры Java-приложению на компьютере, • управляющее RGB-лентой, яркость и цвет светодиодов которой задаётся Java-приложением.
Для его создания нам понадобятся: 1. Микроконтроллер ATmega328p (далее — «МК») с частотой тактирования 8 МГц. 2. RGB-лента WS2812B. 3. Температурный датчик DS18B20. 4. USB-UART адаптер. 5. Подтягивающий резистор на 4.7 kОм. 6. Блок питания от смартфона на 5 В.
Как видно из видео выше, алгоритм обмена данными следующий: а) Каждые 2 секунды МК считывает показания DS18B20 и передаёт 4 байта, содержащие знак температуры, а также десятки, единицы и десятые доли значения температуры для их отражения в лэйбле Java-приложения.
б) При каждом изменении положения ползунка любого из трёх слайдеров Java-приложения цвет текстового поля меняется соответствующим образом, а значения красной, зелёной и синей его составляющих передаются в МК для соответствующей корректировки цвета светодиодов RGB-ленты.
Время 0:00 - Пишем Java-приложение с ипользованием jSerialComm Время 2:15 - Пишем прошивку для МК Время 6:12 - Как это работает. Проводной обмен данными в действии
Программа для МК оформлена на языках Си и ассемблер в Visual Studio Code. Второй вариант прошивки расположен в подвале статьи и расчитан на применение ATMEGA8A.
Контроль за температурным датчиком. Код обмена данными между МК и DS18B20 достаточно подробно пояснялся в в моей предыдущей датагорской статье, поэтому лишь приведу его содержимое.
Считывание значения температуры осуществляется, как уже говорилось выше, с периодичностью в 2 секунды, для чего использовалось прерывание по переполнению счётчика таймера TIMER1.
Как видите: • тактовая частота МК делится на 256, что даёт прерывание раз в 256×65535 / 8000000 = 2 секунды. • в обработчике прерывания поднимается флаг timerFlag, уведомляя основной цикл о наступлении времени измерения температуры.
На Рисунке 2 представлена выдержка из выложенного в архив статьи даташита WS2812B.
Рисунок 2. Параметры записи в WS2812B: а) формат слова, определяющего цвет и яркость светодиода б) тайминг записи бинарных «0» и «1»
Как видно из Рисунка 2а, цвет и яркость каждого отдельного светодиода ленты определяется значением 24-битного числа, запись которого начинается со старшего бита зелёной составляющей и завершается младшим битом синей. Сама запись значения бита (бинарные «0» или «1») осуществляется чередованием высоких и низких состояний на выводе DIN WS2812B (т.е. — на пине PC5 МК), с таймингом согласно Рисунка 2б.
Запись последовательности из N 24-битных чисел приводит к изменению состояния первых N светодиодов в ленте в прямой очерёдности, т. е. первое число записывается в первый от входа DIN светодиод, второе — во второй и. т. д. При этом, промежуток времени между записью двух соседних чисел не должен превышать 280 мкс, иначе контроллер ленты сбросится и вновь начнёт запись с первого светодиода.
Для пояснения сказанного выше, на Рисунке 3 представлены случаи записи последовательности из чисел 0xff0000, 0×00ff00 и 0×0000ff, обуславливающими максимальную интенсивность зелёной, красной и синей составляющей, соответственно, когда интервал времени: а) не превышает 280 мкс во всех случаях, б) превышает 280 мкс между записью второго и третьего числа.
Рисунок 3. Результат записи последовательности чисел в ленту из трёх светодиодов а) интервал между записью чисел не превышает 280 мкс б) интервал между записью второго и третьего чисел превышает 280 мкс
Из вышеизложенного следует, что изменить значение N-го по счёту светодиода в ленте можно, лишь записав в неё N чисел, в т. ч.: • первые (N-1) чисел — текущие значения светодиодов, предшествующих N-му, • последнее число — новое значение N-го светодиода.
В плоскости программы это означает необходимость хранения текущего значения всех светодиодов в ленте. Код заметно упрощается, если разбить 24-битные числа на 8-битные составляющие по каждому цвету. Тогда, буфер для хранения в случае с девятью светодиодами будем выглядеть так:
Обеспечить средствами Си тайминг согласно Рисунка 2б мне не удалось, поскольку период тактового импульса при частоте 8 МГц составляет 125 нс. При этом, установка управляющего пина в высокое, а затем низкое состояние
PORTC |= (1 << PC5);
PORTC &= ~(1 << PC5);
использует более 4-х тактов (т.е. 500 нс), что превышает 380 нс, отведённые даташитом для высокого состояния бинарного числа «0». Как итог — некорректная работа ленты.
С учётом изложенного, было принято решение оформить фрагмент записи в WS2812B на ассемблере. Чтобы не изобретать велосипед, я обратился к интернету, где обнаружил статью Mike Silva — «Driving WS2812 RGB LEDs». Статья написана доступным языком, с подробными комментариями, поэтому сразу приведу окончательный вариант кода с переводом комментариев, а затем дам несколько пояснений.
SREG = 0x3f
OUTBIT = 5 /* управляющий пин - PC5 */
PORTC = 0x08
.text
.global stripRefresh
stripRefresh:
movw r26, r24 /* сохранить адрес буфера grbValue в пару r26-r27 */
movw r24, r22 /* сохранить количество элементов буфера grbValue в пару r24-r25 */
in r22, SREG /* сохранить текущее состояние регистра SREG */
cli /* запретить глобально прерывания */
in r20, PORTC
ori r20, (1 << OUTBIT) /* сохранить маску бинарного "1" - в r20 */
in r21, PORTC
andi r21, ~(1 << OUTBIT) /* сохранить маску бинарного "0" - в r21 */
ldi r19, 7 /* r19 - счётчик записи первых 7 битов */
ld r18, X+ /* скопировать в r18 первый элемент буфера grbValue */
loop1:
out PORTC, r20 /* 1 +0 установить PC5 в 1 */
lsl r18 /* 1 +1 сдвинуть старший бит r18 в бит С регистра SREG */
brcs L1 /* 1/2 +2 если старший бит r18 - 1, перейти к метке L1 */
out PORTC, r21 /* 1 +3 сбросить PC5 в 0 (итого - три такта в состоянии High) */
nop /* 1 +4 */
bst r18, 7 /* 1 +5 сохранить 7-й бит r18 в бит Т регистра SREG для последующей проверки */
subi r19, 1 /* 1 +6 декрементировать r19 */
breq bit8 /* 1/2 +7 если в WS2812B записан 7-й бит перейти к метке bit8 для записи 8-го бита */
rjmp loop1 /* 2 +8 итого - 10 тактов на запись бинарного "0" */
L1:
nop /* 1 +4 */
bst r18, 7 /* 1 +5 сохранить 7-й бит r18 в бит Т регистра SREG для последующей проверки */
subi r19, 1 /* 1 +6 декрементировать r19 */
out PORTC, r21 /* 1 +7 сбросить PC5 в 0 (итого - 7 тактов в состоянии High) */
brne loop1 /* 2/1 +8 итого - 10 тактов на запись бинарного "1" */
bit8:
ldi r19, 7 /* 1 +9 обновить счётчик битов */
out PORTC, r20 /* 1 +0 установить PC5 в 1 */
brts L2 /* 1/2 +1 если последний бит текущего элемента буфера grbValue - 1, перейти к метке L2 */
nop /* 1 +2 */
out PORTC, r21 /* 1 +3 сбросить PC5 в 0 (итого - три такта в состоянии High) */
ld r18, X+ /* 2 +4 загрузить следующий элемент буфера grbValue */
sbiw r24, 1 /* 2 +6 декрементировать счётчик элементов буфера grbValue */
brne loop1 /* 2 +8 если записаны не все элементы, перейти к метке loop1 */
out SREG, r22 /* восстановить значение регистра SREG */
ret
L2:
ld r18, X+ /* 2 +3 загрузить следующий элемент буфера grbValue */
sbiw r24, 1 /* 2 +5 декрементировать счётчик элементов буфера grbValue */
out PORTC, r21 /* 1 +7 сбросить PC5 в 0 (итого - 7 тактов в состоянии High) */
brne loop1 /* 2 +8 если записаны не все элементы, перейти к метке loop1 */
out SREG, r22 /* восстановить значение регистра SREG */
ret
.end
1. Алгоритм работы кода сводится к тому, чтобы уложиться в 10 тактов при записи бита элемента буфера grbValue, в т.ч: • 3 такта (375 нс) в состоянии High и 7 тактов (875 нс) в состоянии Low пина PC5 — для бинарного «0», • 7 тактов (875 нс) в состоянии High и 3 такта (375 нс) в состоянии Low пина PC5 — для бинарного «1».
2. По стандартам Си, первый аргумент фукнции помещается в пару r24–r25, а второй — в пару r22–r23. Именно из этих регистров при вызове из Си-файла
stripRefresh(grbValue, sizeof(grbValue));
ассемблер-код считывает адрес буфера и количество его элементов.
3. В комментариях к коду первый столбец — число тактов на исполнение инструкции, второй — оно же, но с нарастающим результатом.
Далее были созданы хидер- и Си-файлы strip, в которых: а) объявлены: • внешней функция stripRefresh () посредством ключевого слова extern, • вспомогательные переменные redValue, greenValue, blueValue и stripState,
б) прописаны функции: • setLedColor (), которая записывает требуемые значения красной, зелёной и синей составляющих в соответствующие элементы буфера grbValue, • stripSetColor (), устанавливающая все светодиоды ленты в заданный цвет, • stripOff (), гасящая все светодиоды ленты.
Обмен данными с Java-приложением осуществляется по протоколу UART на следующих условиях: а) Скорость обмена — 9600. б) Приём данных — через прерывание RX.
Как видите, в обработчике прерывания поступающий от Java-приложения четырёх-байтный массив сохраняется в буфере receivedByte, а затем поднимается флаг receiveFlag, уведомляющий основной цикл о поступлении новых данных.
Общий контроль за устройством осуществляется из файлов device посредством двух функций: 1. deviceInit () обеспечивает инициализацию: • протокола UART, • термодатчика, • RGB-ленты, • таймера.
2. deviceControl (): • при поднятии флага receiveFlag сбрасывает последний, анализирует значение нулевого из 4-х принятых байтов и, если его значение равно заданному (87), копирует значения первого, второго и третьего байтов в переменные redValue, greenValue и blueValue, соотвественно, а затем обновляет цвет светодиодов RGB-ленты. Указанная проверка нулевого байта введена для исключения ошибок в принятых данных. • при поднятии флага timerFlag сбрасывает последний, считывает текущее значение температуры, раскладывает его на десятки, единицы и десятые доли, а затем передаёт Java-приложению.
Проект приложения разбит для удобства на три класса, которые отвечают: • MyFrame — за графически интерфейс, • MySerial — за обмен данными с МК, • Main — за общий контроль и диспетчеризацию данных между двумя предыдущими классами.
В конструкторе класса MyFrame создаётся фрэйм, включающий следующий элементы: а) Пять ComboBox — для выбора таких параметров обмена, как скорость, количество битов данных, количество стоп-битов, чётности, номера COM-порта. б) Button — кнопка «Open» для соединения с выбранным COM-портом. в) Три Slider — для выбора значения красной, зелёной и синей составляющей цвета RGB-ленты. г) TextField — для отображения выбранного цвета. д) Label — для отображения значения температуры, принимаемого от МК.
Результат работы конструктора представлен на Рисунке 4.
Рисунок 4. Результат работы конструктора класса MyFrame
Методы класса MyFrame: • actionPerformed () при нажатии «Open» поднимает флаг openCloseButtonFlag, • stateChanged () при движении ползунка любого из слайдеров меняет соответствующим образом цвет TextField, а также поднимает флаг slidersFlag. • showMessageDialog () выводит на экран сообщение заданного содержания, в частности — об успешном соединении с COM-портом.
Реакция на поднятие указанных флагов прописана в классе Main и будет рассмотрена ниже.
Класс MySerial содержит четыре метода следующего назначения: а) openPort () и closePort () обеспечивают соединение с COM-портом и отсоединение, соответственно. б) sendColorData () передаёт МК выбранное слайдерами значение цвета. в) SerialEventBasedReading () принимает от МК 4 байта со значением температуры и объединяет их в строковую переменную dataBuffer.
В классе Main создаются экземпляры frame и serial классов MyFrame и MySerial, соответственно, а затем запускается задача, метод run () которой по прерыванию таймера с периодичностью 200 мс: 1. Проверяет флаг openCloseButtonFlag и, если он поднят: • сбрасывает флаг, • в случае, если текущая функция кнопки — соединение с портом, меняет название кнопки с «Open» на «Close», обеспечивает соединение с портом и выдаёт сообщение об удачной/неудачной попытке соединения, • в случае, если текущая функция кнопки — отключение порта, меняет название кнопки с «Close» на «Open», отключает порт и выдаёт соответствующее сообщение,
2. Если переменная dataBuffer — не пустая, отображает её содержимое в Label, а затем очищает.
3. Проверяет флаг slidersFlag и, если он поднят: • сбрасывает флаг, • передаёт МК новое значение цвета RGB-ленты.