Урок 48. Обмен данными между платой Ардуино и компьютером через интерфейс UART.

Обмен данными платы Арпдуино с компьютером

В уроке подключим локальный контроллер на базе Ардуино к компьютеру. Для связи будем использовать стандартный последовательный интерфейс UART.

Предыдущий урок     Список уроков     Следующий урок

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

 

Коротко об интерфейсе UART.

Это самый распространенный последовательный интерфейс современных микроконтроллеров. Я достаточно подробно описывал его в уроке 12. Сейчас только подчеркну особенности соединения UART устройств между собой.

Для обмена данными в UART есть 2 сигнала:

  • RX – вход, через который происходит прием данных:
  • TX – выход, через который данные передаются.

Оба сигнала дискретные, имеют логические уровни CMOS, т.е.:

  • уровень  логического 0 около 0 В;
  • уровень логической 1 около 5 В.

Активный уровень сигналов – низкий. В режиме ожидания сигналы находятся в высоком уровне.

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

Для работы с UART интерфейсом в системе Ардуино есть встроенный класс Serial. Использование функций Serial значительно облегчает разработку приложений для  последовательного порта. Класс считывает данные с контроллера UART и записывает их в программный буфер. Это происходит по прерыванию от контроллера, незаметно для основной программы. Передача данных также происходит под управлением класса Serial. Для этого достаточно загрузить данные в программный буфер функцией Serial.write(). Класс Serial подробно описан в уроке 12.

Интерфейс UART радиальный, т.е. по линиям связи одного интерфейса подключаются только 2 устройства. Выход TX одного устройства подключается к входу RX второго. Сигнал TX второго  UART устройства соединяется с входом RX первого.

Схема подключение устройств через UART

У нас UART устройством может быть плата Ардуино или преобразователь интерфейсов USB-UART, подключенный к компьютеру.

 

Схема локального контроллера.

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

Я решил подключить к контроллеру:

  • датчик температуры DS18B20;
  • переменный резистор, с помощью которого на аналоговом входе можно задавать напряжение;
  • кнопку – датчик дискретного сигнала;
  • светодиод – дискретное исполнительное устройство.

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

Для локального контроллера я использовал плату Arduino Nano. Вот моя схема.

Принципиальная схема локального контроллера

Собранный локальный контроллер выглядит так.

Локальный контроллер на Arduiuno Nano

Локальный контроллер на Arduiuno Nano

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

Линии связи с конвертером USB-UART подключаются непосредственно к сигналам TX и RX платы Ардуино. Могут быть два физических варианта реализации связи с компьютером:

  • Использовать конвертер интерфейсов USB-UART, например модуль CH340. В этом случае модуль подключается к USB порту компьютера, а провода связи (3 провода) тянутся к локальному контроллеру.
  • Использовать USB-UART преобразователь, встроенный в плату Ардуино. Соединение сигналов  конвертера интерфейсов и микроконтроллера происходит на плате. Логически этот вариант не отличается от предыдущего, а выглядит это как подключение платы Ардуино к компьютеру через USB порт.

 

Протокол обмена данными через UART.

Минимальная единица информации, которой можно обмениваться по UART это байт. Мы передаем байты, а нам надо передавать числа, команды, текст. То как интерпретировать последовательность байтов при обмене данными определяет протокол.

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

Протоколы бывают числовыми и текстовыми.

Числа, данные в текстовом протоколе передаются как коды символов. Например, число “132” в текстовом протоколе передается как 3 байта 0x31, 0x33 и 0x32. Это коды символов ”1”, ”3” и ”2”. В числовом протоколе это же число передается одним байтом со значением 132.

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

Достоинство текстовых протоколов – возможность использования для контроля и отладки стандартных средств и программ – текстовых терминалов. В качестве примера можно привести монитор последовательного порта Arduino IDE. Данные в нем выводятся в человеческом, понятном виде.

Резюмируя о текстовых и числовых протоколах применительно к интерфейсам передачи данных:

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

Я категоричный сторонник числовых протоколов. Особенно при использовании микроконтроллеров невысокой производительности. Текстовые протоколы просто сожрут все ресурсы дешевых плат Ардуино.

Итак, я выбрал числовой протокол обмена данными.

 

Разработка протокола обмена данными локального контроллера.

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

  • разрабатывать свои протоколы совсем несложно;
  • специализированные протоколы часто на много эффективнее стандартных;
  • протокол можно оптимизировать под свою конкретную задачу.

В общем виде любой протокол обмена выглядит так:

Команда:  Адрес ->  Код операции ->  Данные ->  Контрольный код ->

Ответ:       <- Данные  <- Контрольный код

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

  • Адрес  - адрес устройства, с которым происходит обмен. У нас интерфейс радиальный. Устройство может быть только одно, но при использовании шинных интерфейсов в сети может быть несколько контроллеров.
  • Код операции – информация о том, что надо сделать.
  • Данные – собственно информация обмена.
  • Контрольный код – код позволяющий обнаружить ошибки данных при передаче.

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

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

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

Я собираюсь оптимизировать протокол по критериям: минимум данных обмена и минимум ресурсов микроконтроллера. Т.е. я собираюсь реализовать самый простой протокол обмена.

Какие данные нам надо передавать:

  • температура;
  • напряжение;
  • состояние кнопки.

Получать необходимо состояние светодиода.

Я выбрал такой формат команды.

Номер байта Формат числа Назначение
0 byte Код операции + адрес контроллера (0x10)
1 byte Состояние светодиода (младший бит)
2 byte Контрольный код ( байт 0 ^ байт 1 ^ 0xe5)

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

Контрольный код я рассчитываю очень простым способом – делаю операцию “исключающее или” с двумя байтами и кодом 0xe5. Последнее необходимо, чтобы определить часто встречающуюся ошибочную ситуацию, когда все байты равны нулю.

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

Ответ в моем протоколе выглядит так.

Номер байта Формат числа Назначение
0 … 3 float Температура
4 … 7 float Напряжение
8 byte Состояние кнопки (младший бит)
9 byte Резерв
10, 11 int Контрольная сумма (сумма байтов 0 … 9  ^ 0xa1e3)

Данных здесь больше. Контрольный код представляет собой сумму 10 байтов с последующим ”исключающим или”. Реализуется расчет такого контрольного кода достаточно просто.

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

 

Разработка резидентной программы локального контроллера.

Принципиальный вопрос – я собираюсь реализовать обмен данными с центральным контроллером параллельным процессом.

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

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

Будем строить программу по такому принципу.

Для начала я реализовал обработку всех датчиков. Я сделал это простым способом в цикле loop().  Заметьте, что программа при отработке паузы 0,9 секунд для ожидания измерения датчика DS18B20 не зависает. Но если применить для этого функцию delay() все будет работать.

Временно добавил блок вывода информации на компьютер для проверки.

// локальный контроллер
// радиальный интерфейс UART

#include <MsTimer2.h>
#include <OneWire.h>
#include <Button.h>

OneWire sensTmp (3); // датчик подключен к выводу 3
Button button1(2, 30); // кнопка подключена к выводу 2

float temperature; // температура
float voltage; // напряжение
byte bufSensTmp[9]; // буфер данных датчика температуры
int timeCount; // счетчик времени

void setup() {
MsTimer2::set(1, timerInterrupt); // прерывания по таймеру 1 мс
MsTimer2::start(); // разрешаем прерывание по таймеру
Serial.begin(9600); // скорость 9600
}

void loop() {
//------------------------------- измерение температуры
if (timeCount < 50) {
timeCount= 50;
sensTmp.reset(); // сброс шины
sensTmp.write(0xCC, 1); // пропуск ROM
sensTmp.write(0x44, 1); // инициализация измерения
}
// задержка 0,9 сек
if(timeCount > 950) {
timeCount= 0;
sensTmp.reset(); // сброс шины
sensTmp.write(0xCC, 1); // пропуск ROM
sensTmp.write(0xBE, 1); // команда чтения памяти датчика
sensTmp.read_bytes(bufSensTmp, 9); // чтение памяти датчика, 9 байтов
if ( OneWire::crc8(bufSensTmp, 8) == bufSensTmp[8] ) { // проверка CRC
// данные правильные
temperature= (float)((int)bufSensTmp[0] | (((int)bufSensTmp[1]) << 8)) * 0.0625 + 0.03125;
}
else temperature= -200.; // ошибка измерения температуры
}

//------------------------------- измерение напряжения
voltage= (float)(analogRead(A0)) * 5. / 1024.;

//----------------------------- проверка измерений
Serial.print("T= ");
Serial.print(temperature);
Serial.print(" U= ");
Serial.print(voltage);
if (button1.flagPress == true) Serial.println(" PRESS");
else Serial.println(" FREE");
}

//-------------------------------------- обработчик прерывания 1 мс
void timerInterrupt() {
timeCount++;
button1.scanState(); // вызов метода обработки состояния кнопки
}

Можно загрузить скетч по ссылке sketch_48_1.

Проверил, все работает. Правильно измеряет температуру и напряжение, реагирует на нажатие кнопки.

Проверка работы локального контроллера

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

 

//------------------------------ перегрузка результатов измерений в буфер
noInterrupts;
* (float *)dataSerialBuf = temperature;
* (float *)(dataSerialBuf+4) = voltage;
if (button1.flagPress == true) dataSerialBuf[8]= 1;
else dataSerialBuf[8]= 0;
dataSerialBuf[9]= 0;
interrupts;
}

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

Теперь программа считывает показания датчиков и кладет их в массив dataSerialBuf.

Осталось сделать доступ к массиву dataSerialBuf из сети.

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

//-------------------- обмен данными
timeOutCount++;
n= Serial.available(); // число принятых байтов

if (n == 0) timeOutCount= 0; // данных нет

else if (n == 3) {
// принята команда, 3 байта
// чтение команды в буфер
byte buf[3];
buf[0]= Serial.read();
buf[1]= Serial.read();
buf[2]= Serial.read();
// проверка
if ( (buf[0] == 0x10) && ((buf[0] ^ buf[1] ^ 0xe5) == buf[2]) ) {
// правильно
if ( (buf[1] & 1) == 0) digitalWrite(5, LOW); // управление светодиодом
else digitalWrite(5, HIGH);

// ответ
unsigned int sum= 0; // контрольная сумма
for (int i=0; i<10; i++) {
Serial.write(dataSerialBuf[i]);
sum += dataSerialBuf[i];
}
// контрольная сумма ответа
sum ^= 0xa1e3;
Serial.write( * ((byte *)(& sum)));
Serial.write( * (((byte *)(& sum)) + 1));
}
else {
// неправильно, сброс порта
timeOutCount= 0;
while (true) { if (Serial.read() == 0xffff) break;}
}
}

else if (n > 3) {
// принято больше данных, неправильно, сброс порта
timeOutCount= 0;
while (true) { if (Serial.read() == 0xffff) break;}
}

else {
// не все байты приняты, проверка тайм-аута
if (timeOutCount > TIME_OUT) {
// сброс порта
timeOutCount= 0;
while (true) { if (Serial.read() == 0xffff) break;}
}
}

Модуль управления обменом вызывается с периодом 1 мс.

  • Проверяется, есть ли данные в буфере последовательного порта.
  • Если данных нет, то сбрасывается счетчик тайм-аута приема данных.
  • Если данные есть, то ожидается прием 3 байтов команды. Одновременно отсчитывается время тайм-аута приема. Оно задано псевдо оператором:

#define TIME_OUT  6   // время таймаута приема команды (мс)

  • Если прием байтов растянулся на время более 6 мс, то определяется ошибка приема команды. Все сбрасывается, буфер последовательного порта очищается.
  • Если приняты 3 байта команды, то проверяется контрольная сумма и формируется ответ.

Полностью скетч резидентной программы локального контроллера можете загрузить по ссылке sketch_48_3.

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

Программа верхнего уровня.

Локальный контроллер обменивается данными не с компьютером, а с программой на компьютере.

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

Программа мониторинга состояния локального контроллера

Загрузить программу можно по этой ссылке UART_Arduino_PC.zip. Необходимо разархивировать файл, выбрать COM порт. Все как в программах верхнего уровня из предыдущих уроков.

У меня все работает идеально. Ни одной ошибки обмена.

 

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

Предыдущий урок     Список уроков     Следующий урок

11 комментариев на «Урок 48. Обмен данными между платой Ардуино и компьютером через интерфейс UART.»

  1. а если Arduino с ком портом по uart «общаться» — какой переходник нужен? RS485 => RS232?

    • Если на компьютере стандартный COM порт с уровнями +10/-10 , то нужен преобразователь уровней RS232. Например микросхема MAX232, SP232, ADM232. Через один урок я напишу об этом. RS485 здесь непричем.

  2. Эдуард, возможно ли сделать в будущем пару уроков по программе верхнего уровня? Хотя бы самый минимальный уровень.

    • Здравствуйте!
      Это сложная, объемная, совершенно другая тема. Может быть когда-нибудь инфопродукт сделаю. Подумаю, но в ближайшее время вряд ли.

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

    • Здравствуйте!
      Тайм-аут это нечто другое. В примере урока команда от компьютера состоит из 3 байтов. Представьте, что от компьютера на контроллер было передано 2 байта команды, а третий не пришел. Контроллер будет бесконечно ожидать 3го байта команды. Система приема данных зависнет.
      Чтобы такого не произошло, контроллер отсчитывает время между приходом байтов команды. Если оно превысило время тайм-аута, то команда считается ошибочной и прерывается.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *