В уроке подключим локальный контроллер на базе Ардуино к компьютеру. Для связи будем использовать стандартный последовательный интерфейс 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 устройством может быть плата Ардуино или преобразователь интерфейсов USB-UART, подключенный к компьютеру.
Схема локального контроллера.
Разработаем схему локального контроллера, которую будем использовать и в последующих уроках. Только придется изменять схему интерфейса связи. В этом варианте используется стандартный интерфейс UART.
Я решил подключить к контроллеру:
- датчик температуры DS18B20;
- переменный резистор, с помощью которого на аналоговом входе можно задавать напряжение;
- кнопку – датчик дискретного сигнала;
- светодиод – дискретное исполнительное устройство.
Я постарался подключить достаточно разнообразные устройства: сложный датчик с цифровым интерфейсом, аналоговый датчик, дискретный датчик.
Для локального контроллера я использовал плату Arduino 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(); // вызов метода обработки состояния кнопки
}
Можно загрузить скетч по ссылке:
Проверил, все работает. Правильно измеряет температуру и напряжение, реагирует на нажатие кнопки.
Дальше я удалил блок вывода данных на компьютер и сделал перегрузку измеренных данных в массив 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 байта команды, то проверяется контрольная сумма и формируется ответ.
Полностью скетч резидентной программы локального контроллера можете загрузить по ссылке:
Проверить работу локального контроллера можно только подключив его к компьютеру со специальной программой верхнего уровня. Не поверите, но у меня контроллер заработал сразу. Я не сделал ни одной ошибки. Наверное, благодаря тому, что очень простой протокол.
Программа верхнего уровня.
Локальный контроллер обменивается данными не с компьютером, а с программой на компьютере.
Я написал очень простую программу мониторинга параметров контроллера. Она отображает состояние всех датчиков, позволяет управлять светодиодом контроллера, показывает состояние обмена данными, фиксирует каждую ошибку обмена.
Загрузить программу можно по этой ссылке:
Необходимо разархивировать файл, выбрать COM порт. Все как в программах верхнего уровня из предыдущих уроков.
У меня все работает идеально. Ни одной ошибки обмена.
В следующем уроке будем управлять этим локальным контроллером не от компьютера, а от другой платы Ардуино.
Здорово, понравилось!!!Очень ценю ваш фундаментальный подход!
Спасибо за добрые слова.
Спасибо за уроки, просто и понято.
а если Arduino с ком портом по uart «общаться» — какой переходник нужен? RS485 => RS232?
Если на компьютере стандартный COM порт с уровнями +10/-10 , то нужен преобразователь уровней RS232. Например микросхема MAX232, SP232, ADM232. Через один урок я напишу об этом. RS485 здесь непричем.
Эдуард, возможно ли сделать в будущем пару уроков по программе верхнего уровня? Хотя бы самый минимальный уровень.
Здравствуйте!
Это сложная, объемная, совершенно другая тема. Может быть когда-нибудь инфопродукт сделаю. Подумаю, но в ближайшее время вряд ли.
Здравствуйте, большое спасибо за уроки. Очень интересная тема по программам для компа верхнего уровня. Не могли бы Вы для начала выложить исходник программы UART_Arduino_PC.exe? Кому интересно, тот сам разберется что там к чему. Спасибо.
Здравствуйте!
У меня есть планы сделать инфопродукт по разработке программ верхнего уровня. Но когда до этого дойдут руки — не знаю. Сейчас очень занят.
А пока исходник выложите 🙂
Эдуард. а насколько сложно подключить к СКАДА системе Trace Mode ?
Наверное, все возможно. Я с системой СКАДА не работал. Разбираться надо.
Обратите внимание на проект «Мажордомо», если интересует система «Умный дом» своими руками. Активное сообщество, целая куча наработок и т.д. и т.п.
Здравствуйте, я не очень понял про время таймаута, вот например если я хочу чтобы данные передавались не каждую секунду, а каждые 2 мс, то какое тогда необходимо время таймаута, и как в таком случае необходимо изменить программу,
Здравствуйте!
Тайм-аут это нечто другое. В примере урока команда от компьютера состоит из 3 байтов. Представьте, что от компьютера на контроллер было передано 2 байта команды, а третий не пришел. Контроллер будет бесконечно ожидать 3го байта команды. Система приема данных зависнет.
Чтобы такого не произошло, контроллер отсчитывает время между приходом байтов команды. Если оно превысило время тайм-аута, то команда считается ошибочной и прерывается.
Замечательные уроки!
Спасибо вам большое, нашел уйму полезной информации!
Хотел бы узнать, в какой среде разработки вы делаете программы верхнего уровня? Сложно ли это?
Здравствуйте!
Спасибо за добрые слова.
Программы верхнего уровня я пишу в среде Borland C++ Builder.
Сложно ли это, не знаю. Все на свете и сложно и просто. Информации, конечно, надо знать много.
Антон, погугли среду разработки Small Basic, с её помощью очень несложно, даже проще ардуины, мне кажется.
Что лучше использовать
MsTimer2::start(),MsTimer2::stop() или interrupts,noInterrupts?
Здравствуйте!
Только первый вариант. noInterrupts запрещает все прерывания, в том числе и системные.
Добрый день. Эдуард подскажите. А если есть необходимость подключить два модуля к NANO. Можно ли как-то использовать другие пины в качестве Rx Tx?
Заранее спасибо.
Здравствуйте!
В принципе можно, но UART будет программный и параллельной задачей обмен не реализовать. Для локальной сети лучше использовать аппаратный UART.
Что означает в коде:
* ((byte *)(& sum))
?
Спасибо
Здравствуйте!
* ((byte *)(& sum))
Об этом написано в уроке 15.
С помощью указателей формируется младший байт от переменой sum типа int.
& sum — получается адрес переменной sum
(byte *)(& sum) — адрес преобразуется в адрес переменной типа byte
* ((byte *)(& sum)) считывается значение по этому адресу.
Добрый день, Эдуард!
Можете ли Вы поделиться исходным кодом программы для компьютера к этому уроку, exe-файл которой Вы выложили?
Заранее спасибо!
Виктор.
Здравствуйте!
Это совершенно другая, сложная тема. Я подумываю написать инфопродукт об этом, но сейчас не хватает времени.
Доброго времени суток!
Спасибо за скорый ответ.
Уроки к Ардуино отличные!
Буду с нетерпением ждать продолжения, желательно к delphi!
С уважением ,
Виктор
День добрый. Урок прекрасный, но возникла сложность. Внутри обработчика прерывания по таймеру «не работает» чтение данных c UART. Т.е. данных в буфере нет (Serial.available не возвращает положительного результата). В сети пишут, что это из-за того, что получение данных Serial само реализуется через прерывания «и вообще не нужно ничего по UART в обработчике слать». Но у вас, судя по всему, всё работает. Подскажите, пожалуйста, что может быть не так?
Здравствуйте!
Все правильно. В обработчике прерывания Serial работать не будет. При вызове любого прерывания, остальные запрещаются. Разрешаются только при выходе из функции-обработчика. А зачем вам использовать Serial внутри обработчика? Функции обработки прерываний должны быть как можно короче, занимать меньше времени.
Тогда я, видимо, не совсем поняла урок. «Естественно модуль управления обменом по сети расположен в обработчике прерывания по таймеру. Вот его скетч». И в полном варианте программы по ссылке вроде бы тоже весь обмен по UART реализован именно в обработчике прерывания timerInterrupt()
Здравствуйте!
Может я неправильно выразился. Я имел ввиду, что в обработчике прерывания нельзя ждать завершения каких-либо процессов класса Serial. Можно только проанализировать состояние и выйти из обработчика.
Эдуард, здравствуйте. У Вас потрясающие уроки!
Дабы получше усвоить материал, пытаюсь написать свою модификацию протокола и программу верхнего уровня (параллельно учу Python). Но возникла проблема: ардуино Мега 2560 возвращает всегда нулевое значение светодиода, независимо, горит он или нет. Чтобы исключить возможные ошибки логики убрал из скетча все лишнее, оставил только кнопку и один светодиод. Ситуация не изменилась, в логе одно и тоже нулевое значение, при этом светодиод отрабатывает идеально.
Ссылка на файлы проекта: https://www.dropbox.com/sh/xoxraiatujf1xfv/AAA3nHeTgRpGe-cpUDlg6ISda?dl=0
В процессе написания комментария родилась мысль писать в буфер не состояние кнопки, как в Вашем уроке, а состояние светодиода. И все заработало. Но очевидны недостатки. Хочется читать сенсор/датчик, а не промежуточную переменную.
Здравствуйте!
Спасибо за оценку уроков.
Вопроса я не понял. С одной стороны вы пишите «ардуино Мега 2560 возвращает всегда нулевое значение светодиода, независимо, горит он или нет». С другой — «писать в буфер не состояние кнопки, как в Вашем уроке, а состояние светодиода. И все заработало».
Если сомневаетесь, то проверьте работу светодиода в цикле loop. Занесите в него 0, считайте и передайте через последовательный порт. Затем тоже самое с 1.
Да, наверно действительно несколько непонятно написал. Проблема в том, что по какой то причине, конструкция
////////////////////////
noInterrupts();
if (button1.flagPress == true) dataSerialBuf[0] = 1;
else dataSerialBuf[0] = 0;
interrupts();
////////////////////////
не пишет в буфер состояние кнопки, простейшая программа верхнего уровня показывает в порту нули, независимо, жму я на кнопку или нет.
При замене проверки состояния кнопки на проверку состояния светодиода, которым рулит кнопка, программа верхнего уровня начинает показывать и единицы. Но первоначальной задачей было все же отслеживать состояние кнопки
Не понятно. Все должно работать. А простая программа в цикле loop() работает?
Зачем вы прерывания запрещаете? Прерывания надо запрещать если проверяете тип данных длиной более одного байта. Часть данных может измениться в прерывании. Получится половина данных новая, половина старая.
Хотелось бы посмотреть исходник программы для компьютера
Здравствуйте…..у меня почему_то пояснение в виде каких то иероглифов , как их сделать читаемыми? подскажите пожалуйста…….
Здравствуйте! Это ваш браузер так выводит.
Нажмите на ссылку. Откроется окно с иероглифами. Правой кнопкой мыши, выбрать сохранить как. Будет сохранено в исходном виде.
Доброго времени)
Вы делаете в основном цикле «погрузку» данных в массив с предварительной отменой прерываний и последующим их возобновлением.
А если операцию погрузки в массив делать в самом прерывании, непосредственно перед отправкой, на что это повлияет?
Здравствуйте!
Смысл в том, чтобы развязать операции формирования данных массива и отправки. Вы просто кладете данные в массив, а параллельный процесс эти данные по запросу отсылает на компьютер. Основная программа не знает, что существует параллельный процесс. Он может отослать данные в любой момент. Для того, чтобы не было отослано половина старых, а половина новых данных и запрещается прерывания.
Спасибо!
Для чего запрещаются прерывания я понимаю, просто в моем случае я сначала жду массив данных, а в ответ отправляю такой же массив других данных. И если соответствующие операции извлечения данных и их погрузки делать в самом прерывании, я бы мог обойтись одним массивом, потому и спросил, как лучше:)
Еще такая проблема — я похожую связку сделал между Attiny45 и ESP8266 NodeMCU, и ESP у меня при обрыве связи начинает бесконечно перезагружаться в том случае, если я очистку буфера вашим способом делаю. Без очистки всё идеально, можно обрывать и восстанавливать связь, ничего не зависнет. С чем может быть такое связано? C особенностями ESP?
Много причин может быть. Например, вы с размером массива промахиваитесь и портите какую нибудь переменную.
Всё, нашел причину)
Дело всё-таки в ESP. Ваш способ очистки буфера /* while (true) { if (mySerial.read() == 0xffff) break;} */ на ней работать не хочет, зато сработал вот такой:
/* while (Serial.available()) { Serial.read(); } */
Хотя на других платах Ваша очистка отлично отрабатывает
здраствуйте! у меня такая проблема==>
‘timerInterrupt’ was not declared in this scope
почему такая ошибка не понял
Здравствуйте!
Прежде всего проверьте, что у вас в пути к файлам Ардуино нет папок с именами на кириллице.
добрый день! решил эту проблемуб но у меня вопрос есть.я хочу передавать инвормайию с ардуино на другой микроконтролер по uart ,как сообщение должно быть длиной 10 символов(10 байт) если оно отправляется с ардуино платы на другой и 13 символов если оно принимается ардуино платой с другой.
Первый символ всегда 0x1. Он означает начало сообщения.
Второй символ указывает на тип сообщения, 0x20 для сообщения отправляемого ардуино на другой плату, и 0x21 для сообщения отправляемого 2.платой на ардуино.
Следующие 8 символов содержат в себе любые данные которые нужно передать. в моцй задаче это измерения двух датчиков.вот я не очень знаю как протокол написать по этому поводу.я иностранный студент в Москве и это задача часть моей дипломной работы и мне надо ваш помощь.есть другой человек он пишет коды для 2. микроконтролер, моя задача коду ардуино.ни понятие веши вот протокол как писат и понимат,ваш урок похож видимо решить мою задачу.вы можете помогать?
А вы не хотите использовать библиотеку протокола ModBus из последующих уроков? Ей очень удобно работать, обмен делается очень просто.
если бы я использоыал датчик DHT11 ,то я должен использовать команды (0xBE),(0x44, 1),(0xСС, 1)
???
или все это команды для DS18B20?
Здравствуйте!
Конечно. В схеме установлен датчик DS18B20, значит и протокол для него.
значить все это команды использовать ,но только библиотеку менять на?
Другой датчик, другой протокол, другая библиотека, другие команды.
Вы все-таки, если есть возможность, примите мой совет использовать для обмена протокол ModBus и библиотеки для нее из последующих уроков. Будет проще реализовывать обмен данными.
ок спасибо болшое!
Урок 58. Обмен данными между платами Ардуино через UART по протоколу ModBus. Библиотека Tiny_ModBusRTU_Master.
Урок 61. Аппаратная реализация интерфейса RS-485. Объединение плат Ардуино в локальную сеть RS-485.
и мне просьба ,вы можете готовить урок ,как пишиться собственные протоколы для обмена данных между двумя микроконтролерами и их коды что зачем конкретна ,потому что для начинаюших это трудно или если у вас есть докуманты об этом можете разпубликовать?за все инфо спасибо большое!
Добрый день, Эдуард. Не поможете ли разжевать одну фразу из кода (участок перегрузки данных в буфер):
* (float *)(dataSerialBuf+4) = voltage;
Что в ней означает каждый символ, по такому примеру:
temperature= (float)((int)bufSensTmp[0] | (((int)bufSensTmp[1]) << 8)) * 0.0625 + 0.03125;
переменной "temperature" типа float (занимает в памяти микроконтроллера 4 байта) присвоить значение типа float, полученное путём побитового "ИЛИ" двух байтов типа int, с предварительным побитовым сдвигом второго байта влево на 8 бит, взятых из "0" и "1" ячеек буфера bufSensTmp[] соответственно. И затем умноженное на число 0.0625 для того-то, и просуммировано с числом 0.03125 для того-то.
Здравствуйте!
dataSerialBuf — это имя массива, а значит указатель на него.
+ 4 — смещение указателя на нужные нам данные.
(float *) — явное преобразование указателя на указатель данных типа float.
* — запись данного по указателю типа float, т.е. запись данного типа float.
Урок 15 посвящен указателям.
Спасибо за столь скорый ответ.
Поправьте пожалуйста, если я допустил неточность. Очень-очень надо. Скопировать кусок чужого кода в свой проект легко, но мне непременно надо разобраться, как самому написать этот кусок кода. Поэтому такая дотошность.
В программе объявлена глобальная переменная типа «массив» с именем dataSerialBuf[], которая имеет 10 ячеек по 1 байту и расположена в памяти контроллера по адресу, назначенному (компилятором?). Пусть это будут ячейки с адресом от 15600 (для первого байта) и до 15600+9 для последнего (всего 10). Верно? Когда из программы есть обращение к имени массива — это равносильно что напрямую называть адрес первой ячейки, т.е. 15600, в которой лежит первый байт массива, а именно dataSerialBuf[0]. Использование указателей позволяет не заморачиваться с ТОЧНЫМ расположением в памяти микроконтроллера переменных, достаточно назвать её имя. И указатель, как стрелочник, добросовестно направит к адресу с первым байтом. Если это переменная типа int, то она имеет два байта и для неё компилятором зарезервировано два байта в памяти. Если float, то четыре. Здесь я лично для себя прибег к методу ассоциаций и представил переменную типа float, как четыре вагона поезда. В каждый вагон мы можем положить 8 бит(записать), и в каждом вагоне можем посмотреть что там лежит (прочитать). Таким образом, переменную dataSerialBuf[] можно представить в виде 10-ти вагонов. Нумерация вагонов начинается с нуля. С «0» по «3» (четыре вагона-байта) это для переменной temperature типа float, с «4» по «7» (ещё четыре вагона-байта) для переменной voltage типа float, один вагон (№8) для того, чтоб передать состояние светодиода всего одним битом из восьми, один вагон (№9) зарезервирован и полезного груза пока не несет.
Тогда запись вида: * (float *)(dataSerialBuf+4) = voltage; можно интерпретировать как, в вагон №4 состава dataSerialBuf[] положить переменную voltage, которая имеет тип float, а потому займет 4 вагона с №№ 4-7.
Сильно не пинайте за очевидные для Вас вещи. Я, как и многие, учусь по урокам на сайте. За сайт и возможность поучиться у более сильного программиста, выражаю Вам отдельную благодарность.
Да, все правильно. Добавить нечего.
Если вы реализовываете практическую задачу, то будет проще сделать это с помощью библиотек, поддерживающих протокол ModBus. Этому посвящены несколько последующих уроков, и в разделе Умный дом практически реализовывается обмен по ModBus.
Спасибо.
Да собственно MODBUS это и есть моя цель. Точнее MODBUS RTU на RS-485. Так что здесь для меня просто кладезь информации. Потом мне надо ещё построить мост MODBUS_RTU — Mosquitto.
Или может здесь применена какая-то разрешенная в С форма записи вроде такой: sum ^= 0xa1e3; тоже самое что и sum = sum ^ 0xa1e3;
Здравствуйте
Подскажите пожалуйста, что означает конструкция
while (true) { if (Serial.read() == 0xffff) break;} ?
и что вообще означает понятие «сброс порта»?
Здравствуйте!
В буфере порта могли остаться несчитанные данные. Эта конструкция очищает буфер. Считывает данные, пока не встретится код 0xffff, что означает, что буфер пуст.
Здравствуйте, еще можно вопрос?
Формула для контрольной суммы (сумма байтов 0 … 9 ^ 0xa1e3) как-то зависит от количества байт? Другими словами, если будет 5 байт или 15 байт — формула изменится (ну кроме количества слагаемых)?
При данном способе не зависит. Это просто сумма с переполнением.
Эдуард, подскажите, пожалуйста такой момент. Контрольная сумма объявлена как unsigned int sum. А если слагаемые могут быть отрицательными (по логике передаваемых данных) — нужно ли объявить контрольную сумму как знаковую (int)? Или логика передаваемых данных не влияет на подсчет контрольной суммы?
Здравствуйте!
Объявляйте сумму, как беззнаковую переменную. Хотя при суммировании не важно, как объявлены слагаемые и переменная суммирования. Результат будет одним и тем же. Используются знаковые или беззнаковые переменные повлияет только при интерпретации результата — сравнение больше меньше и т.п.
Правильнее слагаемые явно преобразовать в беззнаковые byte.
Эдуард, всю голову сломал. Подскажите пожалуйста — при каком значении n программа попадет вот сюда:
else {
// не все байты приняты, проверка тайм-аута
if (timeOutCount > TIME_OUT) {…
Вообще — при каких обстоятельствах она туда может попасть?
Обычно такие конструкции отрабатывают тайм-ауты, т.е. зависание обмена по непредсказуемым причинам. Например, программа ждет 5 байтов, а пришло только 3. Провод оборвался не вовремя или помеха на линию связи. Без отработки такой ситуации программа зависнет навечно.
Большое спасибо за ответы
Эдуард, подскажите пожалуйста — что за беда. Если программа уже загружена в МК и там благополучно работает, то при подключении к порту любым терминалом, в том числе стандартным монитором порта Arduino IDE, МК перегружается! Т.е. картина выглядит так: программа работает, чего-то там делает, открываем монитор порта — МК перегружается! Плата Arduino UNO. Это так надо или у меня лыжи не едут?!
Здравствуйте!
Да, при запуске монитора последовательного порта микроконтроллер перезапускется. Вырабатывается аппаратный сброс с помощью сигнала DTR (можете почитать об этом в уроке 46).
Мне кажется это очень удобно. Программа перезапускается и данные порта монитор не пропускает.
Т.е это штатный режим?! Вот это засада… Т.е. работает себе программа, работает, тут внешняя программ решила подключиться чтобы считать данные. Подключилась… а тут Reset. Я прочитал что можно это обойти путем соединения пинов reset и 5 В через резистор 120 Ом или конденсатора (не помню на какие пины). Но как-то страшно, не сгорит ли…
А у нано — такая-же засада?
Почему внешняя программа. Сброс формируется только монитором последовательного порта. Если программа не трогает DTR, то сброса не будет.
Используйте другой монитор. Мне нравится CoolTerm. В уроке 3 уроках STM32 есть ссылка.
Не только монитором порта. Подключаюсь например программкой Terminal v 1.9 — тоже самое: как только жму кнопочку «Соединиться с портом» — МК перезагружается. Подозреваю что так же будет со всеми терминалами.
А у нано такая же засада, не знаете?
Здравствуйте!
В Nano будет тоже самое. В PRO Mini если сигнал DTR не подключать такого эффекта не будет.
Нужен терминал, который не использует DTR сигнал. Попробуйте CoolTerm. Я не помню, если там есть такая возможность, то установите DTR в неактивное состояние.
Эдуард, здравствуйте
Такая ситуация. Пакет данных, который я пытаюсь передать от МК на ПК достаточно большой. Если я не ошибаюсь буфер ограничен 64 байтами? Ну даже не в этом дело, если пакет большой, как его «искусственно» разделить на несколько пакетов? По другому сформулирую — если бы в вашем примере dataSerialBuf был размером 200 байт — то нужно было бы как-то разделять его передачу?
Здравствуйте!
Решайте сами. Никто вам не мешает передавать пакеты любой длины. Все ограничено ресурсами микроконтроллера.
При больших объемах пакетов ошибка передачи «выбивает» сразу много данных. Так что, длина пакетов еще зависит от помехозащищенности сети. Если вероятность ошибки велика, то лучше разбивать пакеты на небольшие порции.
Подскажите пожалуйста — а как разбить пакеты? Вот есть у меня 100 байт которые нужно передать. Я решаю что логично было бы их разбить на 60 и 40 (по логике передаваемых данных). Предположим это массивы buf1 и buf2
for (i=0; i<60; i++)
Serial.write(buf1[i]);
for (i=0; i<40; i++)
Serial.write(buf2[i]);
Что нужно сделать между этими двумя циклами чтобы данные ушли двумя разными пакетами и принимающая сторона приняла их как 2 разных пакета?
Здравствуйте!
Добавьте в начале посылки признак пакета. Я уже не помню, как там сделано. Если есть код операции, то можно в него бит добавить.
Добрый день. Как всё таки вычисляется 0xa1e3? И нужно ли его менять если увеличить количество передаваемых байт с ваших 10 до, скажем 20?
Если можно, дайте пожалуйста, подробный и развернутый ответ, с примерами, для совсем чайников вроде меня.
Из всего урока, только этот момент не понятен (
Здравствуйте!
Нужно пакет данных сопроводить контрольным кодом, чтобы определить на приемной стороне, что данные не исказились.
Как его вычислить. ? Лучший вариант использовать циклические коды, но они сложно вычисляются. Поэтому я просто суммировал байты и сумму передал в качестве контрольного кода.
Если все передаваемые байты будут равны 0 (а это ситуация возможная при аппаратных ошибках), то их сумма тоже будет равна 0, и данные восприняты, как правильные. Поэтому я добавил операцию исключающего или. Я выбрал код a1e3, но ничего не мешает использовать другой.
#define TIME_OUT 6 // время таймаута приема команды (мс)
Если прием байтов растянулся на время более 6 мс, то определяется ошибка приема команды.» — а почему именно 6мс? Если мне потребуется передавать 4 байта данных (температура, влажность, ток нагрузки, «авария/норма»)?
Здравствуйте!
Это время не длительности пакета, а отсутствия данных. Ошибка формируется, если прием пакета начался, а очередного данного нет более 6 мс, т.е. нарушена целостность пакета.
Помогите. Очень нужна помощь спецов. Есть игра-авиасимулятор, есть контроллер ARCC к которому подключены кнопки, и программа, в которой можно назначить на эти кнопки разные функции авиасимулятора. И дальше в процессе игры эта программа связывает контроллер с симулятором. Но эта программа заточена только под контроллер ARCC. А надо подружить программу с ардуинкой — написать что то типа модуля, поддерживающего интерфейс программы. Исходники программы есть. Там же есть примеры, как это сделано для контроллера ARCC.
Меня интересовал исходник программы верхнего уровня, работа с СОМ портом, разбор посылки с контроллера, а не exe-файл!
Здравствуйте!
Но это уроки программирования микроконтроллера, а не компьютера. Планы были написать уроки по программам верхнего уровня, но времени не хватает.
Может исходник на почту бросите, я хоть и мало но заплатил, а тут облом.
Эдуард, нужно принять данные с борта модели на ноутбук в соответствующее приложение (посредством радиомодулей, подключенных к контроллеру и COM порту ), пожалуйста помогите с программой верхнего уровня. Третьим лицам не передам. В разумных пределах информацию оплачу. Сильно надо!
Здравствуйте Эдуард.
Объястните пожалуйста следущие строки:
Вопрос 1
//————————-перегрузка результатов измерений в буфер———
* (float *)dataSerialBuf = temperature;
* (float *)(dataSerialBuf+4) = voltage;
температура использует первые 4 байта массива (0,1,2,3). Вы написали строку
* (float *)dataSerialBuf = temperature; будет ли она эквивалентна, если запишем так
* (float *)dataSerialBuf[0,1,2,3] = temperature;?
Ну и следующая строка:
* (float *)(dataSerialBuf+4) = voltage; == * (float *)(dataSerialBuf[0,1,2,3]+[4,5,6,7]) = voltage;?
Вопрос 2
Строка if ( (buf[0] == 0x10) && ((buf[0] ^ buf[1] ^ 0xe5) == buf[2]) )
Вы пишите «Контрольный код представляет собой сумму 10 байтов с последующим ”исключающим или”.
Реализуется расчет такого контрольного кода достаточно просто.»
А где здесь сумма 10ти байтов? Если мы приняли только три байта с центрального контроллера
» else if (n == 3) {
//принята команда, 3 байта
//чтение команды в буфер
byte buf[3];
buf[0]= Serial.read();
buf[1]= Serial.read();
buf[2]= Serial.read(); »
Вопрос 3
Числа(?) 0xe5 и 0xa1e3 в человеческом виде чему равны? Может имеется какая -то таблица с их переводом?
Вопрос 4
Строка sum += buf[i]; переменые в buf[i] прибавляются к sum c каждым циклом и пока
не получится сумма 0xa1e3 в строке sum ^= 0xa1e3; «контрольная сумма ответа»?
Что означаеют символы ^= ?
Спасибо.
Здравствуйте!
Посмотрите урок 15 о работе с указателями.
У нас есть переменные, которые надо передать через последовательный интерфейс. Переменные разного типа, например первая temperature типа float. Через последовательный порт мы можем передавать только байтами. Поэтому надо все переменные преобразовать в байты и положить последовательно в буфер. А потом из него передать байтами. На приемной стороне сделать обратную операцию.
* (float *)dataSerialBuf = temperature;
Указатель на массив типа byte (указатель на массив равен его имени) dataSerialBuf преобразовали в указатель на тип float.
По этому указателю положили число типа float, которое занимает 4 байта.
* (float *)(dataSerialBuf+4) = voltage;
Тоже самое с переменной voltage, только сместили на 4 байта. и т.д.
Что касается второго вопроса. Я не помню формат протокола. Значит принимается 2 байта и 3й байт это контрольный код.
3 вопрос. Откройте калькулятор на компьютере. Выберите режим программиста. Набираете в одной системе исчисления, переключаете на другую и получаете результат. 0xe5 это в шестнадцатеричном виде.
4 вопрос. ^ Это операция ИСКЛЮЧАЮЩЕЕ ИЛИ. Побитное сложение, при котором если биты равны, то результат 0, если не равны, то результат бита 1. В данном случае применяется, чтобы при всех нулевых байтах контрольный код не был равен нулю. Такая ситуация возникает после сброса микроконтроллера.
Не обижайтесь. Почти все вопросы у вас начального уровня. Я периодически задумываюсь открыть на форуме раздел для таких вопросов и отвечать на них там более развернуто.
Здравствуйте Эдуард.
Спасибо за ответ.
Какая тут обида.
Где же ещё узнать и у кого как не на Вашем сайте.
А в учебниках типа Блума, Петина, Белова и таких же авторов об этом не пишут и не разясняют, что почём, от куда и куда.
Спасибо.
Эдуард -Вы просто гений.Спасибо за Ваши уроки.
Спасибо, конечно. Но вы преувеличиваете.
Здравствуйте, перегрузка результатов измерений в буфер, что должна всегда в каждом цикле происходить или это просто пример?
Здравствуйте!
Буфер нужен всегда.
Два процесса: измерения и передачи данных. Между ними данные где-то должны храниться.
Еще. Данные должны цельными. Приходит запрос на передачу данных, а часть из них от нового измерения, часть — от прошлого. Поэтому происходит копирование данных в буфер с запретом прерываний.
Спасибо, я раньше пробовал, отправлял в Serial , только когда менялись данные и просто в цикле, и получал то нули или вообще не получал. Пробую разобраться как в цикле прерывания всё происходит, но пока что-то нет полной картинки.
Просто в цикле вроде освоил отладку, ставлю флаги или задержку и …. А здесь как? тоже флаги? или перевести этот код для понимания в цикл или может просто, то что новое и заморочился. Если можете по поводу чтения кода в прерывании подскажите или это всё едино.
Здравствуйте!
При использовании класса Serial нет необходимости работать с флагами и прерываниями.
Функцией available() проверяете, есть ли данные. Считываете с помощью read(), отсылаете с помощью write().
Класс Serial сам обрабатывает прерывания, проверяет и сбрасывает флаги, а данные помещает в приемный буфер или берет их из передающего буфера.
Посмотрите в уроке, какой протокол я выбрал и, как он реализуется в программе.
Здравствуйте, вопрос:
Числа 0xe5 и 0xa1e3 с потолка взяты или есть какая-то методика их выбора?
Спасибо
Здравствуйте!
В принципе с потолка, но с разным подряд количеством 1 и 0.