Radio Life - Связь Arduino с внешним миром

Связь Arduino с внешним миром

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

Следует заметить, что это не полноценный RS232, а конвертер RS232->USB. Полноценный RS232 можно организовать с помощью дополнительных элементов, используя сигналы RXD и TXD, но об этом как-нибудь в другой раз.

1. Настройка последовательного порта

Настройка порта очень проста, и заключается в установке скорости передачи и таймаута ожидания приема строки символов. Для этого введем в ранее описанную программу вольтметра функцию void SetSerial().

// Подпрограмма настройки последовательного порта
// ==============================================
void SetSerial() {
  Serial.begin(38400);       // скорость основного порта
  Serial.setTimeout(2);      // таймаут ожидания приема строки символов в мс
}

И изменим функцию PrintTitle(), добавив в нее несколько строк для вывода заголовка программы в последовательный порт. Скорость передачи 38400 выбрана только для примера.

// Подпрограмма печати заголовка программы
// =======================================
void PrintTitle() {
  lcd.begin(16, 2);
  lcd.print(TITLE);                 // печатаем первую строку на дисплее
  lcd.setCursor(0, 1);
  sprintf(adc, "Version %s", Ver);
  lcd.print(adc);                   // печатаем вторую строку на дисплее
  // печатаем в последовательный порт
  Serial.println();                 // новая строка
  Serial.println(TITLE);            // печатаем первую строку на терминал
  Serial.println(adc);              // печатаем вторую строку на терминал
  Serial.print("> ");               // приглашение для ввода
  SER_status = SER_WAITCOMMAND;
}

Дополнительный оператор вывода Serial.println(); обеспечивает вывод заголовка с новой строки. Как это выглядит на экране Putty показано на рис. 1.

Рис. 1.

Полный текст программы приведен по ссылке.

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

2. Обработка ввода с последовательного порта.

Для работы с портом разработаем функцию SerialWork(), которая будет обрабатывать ввод/вывод. Чтобы можно было применить оператор switch, введем слово состояния клавиатуры SER_status, по содержимому которого и будет производиться переключение.

// Подпрограмма работы с последовательным портом
// =============================================
void SerialWork() {
  switch (SER_status) {
    // ожидание ввода команды
    case (SER_WAITCOMMAND): {
      if(Serial.available() > 0) {
        if(Serial.readBytes(ser, 3) == 1) {
          if(ser[0] >= '0' && ser[0] <= '9') {
            Serial.write(ser[0]);
            SER_status = SER_GETCOMMAND; }
          else SerPrintHelp(); } }
      break; }
    // обработка команды
    case (SER_GETCOMMAND): {
      switch (ser[0]) {
        // команда вывода настроек
        case ('1'): {
          SerPrintSetup();
          break; }
        // команда вывода измеряемых величин
        case ('2'): {
          SER_status = SER_PRINTDATA;
          break; }
        default: {
          Serial.print("\r\n> ");
          SER_status = SER_WAITCOMMAND;
          break; } }
      break; }
    case (SER_PRINTDATA): {
      if((SYS_status & SYS_SERPRINT) != 0) {
        SerPrintData();
        SYS_status &= ~SYS_SERPRINT; }
      if(Serial.available() > 0) {
        if(Serial.readBytes(ser, 3) == 1) {
          if (ser[0] == 0x1b) {
            Serial.print("\r\n> ");
            SYS_status &= ~SYS_SERTITLE;
            SER_status = SER_WAITCOMMAND; } } }
      break; }
    default: {
      break; } }

Для того, чтобы выводимые данные не "мигали", в слове состояния программу SYS_status введен еще один бит SYS_SERPRINT. Этот бит устанавливается прерыванием сторожевого таймера, а сбрасывается после вывода данных в порт. Если введен ошибочный символ, то выводится короткий Help. Полный текст программы приведен отдельно по ссылке, а результат работы программы — на рис. 2.

Рис. 2.

Ввод с последовательного порта производится функцией Serial.readBytes, а не Serial.read. Связано это с тем, что ряд клавиш (наприм., функциональные, и т. п.) выдает не один, а три кода клавиши. Поэтому и ожидается прием 3 символов, а время ожидания задается функцией Serial.setTimeout в процессе настройки последовательного порта SetSerial().

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

3. Корректировка параметров вольтметра

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

1. Изменение времени ожидания окончания дребезга кнопки.

2. Калибровка вольтметра отдельно для Vref 1,1 и 5 вольт.

Сначала проведем настройку таймаута дребезга. Текст измененной функции SerialWork() и функции приема символа SerReadByte() приведен ниже:

// Подпрограмма работы с последовательным портом
// =============================================
void SerialWork() {
  switch (SER_status) {
    // ожидание ввода команды
    case (SER_WAITCOMMAND): {
      if(Serial.available() > 0) {
        if(Serial.readBytes(ser, 3) == 1) {
          if(ser[0] >= '0' && ser[0] <= '9') {
            Serial.write(ser[0]);
            SER_status = SER_GETCOMMAND; }
          else SerPrintHelp(); } }
      break; }
    // обработка команды
    case (SER_GETCOMMAND): {
      // команда вывода настроек
      switch (ser[0]) {
        case ('1'): {
          SerPrintSetup();
          break; }
        case ('2'): {
          SER_status = SER_PRINTDATA;
          break; }
        case ('3'): {
          SER_status = SER_CONFIG;
          Serial.println();
          Serial.println("1   - Key timeout");
          Serial.println("2   - Correct Vref = 1.1 V");
          Serial.println("3   - Correct Vref = 5 V");
          Serial.println("ESC - Return to Command Mode");
          Serial.print("CFG>");
          break; }
        default: {
          Serial.print("\r\n> ");
          SER_status = SER_WAITCOMMAND;
          break; } }
      break; }
    case (SER_PRINTDATA): {
      if((SYS_status & SYS_SERPRINT) != 0) {
        SerPrintData();
        SYS_status &= ~SYS_SERPRINT; }
      if(Serial.available() > 0) {
        if(Serial.readBytes(ser, 3) == 1) {
          if (ser[0] == 0x1b) {
            Serial.print("\r\n> ");
            SYS_status &= ~SYS_SERTITLE;
            SER_status = SER_WAITCOMMAND; } } }
      break; }
    case (SER_CONFIG): {
      if (SerReadByte() != 0) {
        switch (ser[0]) {
          case (0x1b): {
            Serial.print("\r\n> ");
            SER_status = SER_WAITCOMMAND;
            break; }
          case ('1'): {
            SER_data = 0;
            Serial.write(ser[0]);
            Serial.println("\n\rESC - Return to Old Value");
            sprintf(adc, "Kkey [%u] (1-255) = ", KEY_delay);
            Serial.print(adc);
            SER_status = SER_CONFIGTIMEOUT;
            break; }
          default: {
            break; } } }
      break; }
    case (SER_CONFIGTIMEOUT): {
      if (SerReadByte() != 0) {
        switch (ser[0]) {
          case (0x1b): {
            Serial.print("\r\nCFG> ");
            SER_status = SER_CONFIG;
            break; }
          case (0xd): {
            if((SER_data > 255) || (SER_data == 0))  {
              Serial.print("\r\nError!"); }
            else {
              KEY_delay = SER_data;
              cli();
              SetTimer2(KEY_delay);
              sei(); }
            Serial.print("\r\nCFG> ");
            SER_status = SER_CONFIG;
            break; }
          default: {
            if(ser[0] >= '0' && ser[0] <= '9')  {
              Serial.write(ser[0]);
              SER_data = SER_data * 10 + (ser[0] - 0x30);  }
            break; } } }
      break; }
    default: {
      break; } } }
// Подпрограмма приема символа
// с последовательного порта
// ===========================
int SerReadByte() {
  if(Serial.available() > 0) {
    if(Serial.readBytes(ser, 3) == 1) {
      return ser[0]; } }
  return 0;
}

Полный текст программы приведен отдельно по ссылке, а результат работы — на рис. 3.

Рис. 3.

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

4. Работа с EEPROM

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

#include <EEPROM.h>

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

// Подпрограмма обращения с EEPROM
// ===============================
void SetEEPROM() {
  // проверка на инициализацию EEPROM
  // по нулевому байту
  if (EEPROM.read(0) == 0x55) {
    // EEPROM уже инициализирована
    // считываем данные
    EEPROM.get(EEPROM_T2_DELAY, KEY_delay);
    EEPROM.get(EEPROM_K11, ADC_K11);
    EEPROM.get(EEPROM_K5, ADC_K5); }
  // инициализация EEPROM
  else {
    EEPROM.write(0, 0x55);
    EEPROM.put(EEPROM_T2_DELAY, KEY_delay);
    EEPROM.put(EEPROM_K11, ADC_K11);
    EEPROM.put(EEPROM_K5, ADC_K5); } }

Дело в том, что начальное состоян ие ячеек EEPROM равно "1". Поэтому если содержимое нулевого байта не равно значению 0x55, то считается, что EEPROM требуется инициальзировать, т. е. записать значения параметров по умолчанию, а в нулевой байт записать 0x55. Соответственно, при дальнейших включениях контроллера значения параметров будут считываться при инициальзации и использоваться при настройке и расчетах.

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

Текст этого варианта программы приведен по ссылке, а полученный результат — на рис. 4.

Рис. 4.

Видно, что при повторном обращении к настройке параметра время задержки не совсем соответствует введенному нами значению. Это связано с тем, что 1 единица счетчика соответствует 64 мксек. Следовательно введенное значение округляется до ближайшей возможной комбинации счетчика, каковая дробной быть не может. Однако в нашем случае этим отклонением можно пренебречь.

5. Работа с GSM-модемом

Последовательный порт ограничивает радиус общения с контроллером допустимой длиной провода. Для работы откуда угодно (точнее — отттуда, где есть сотовая связь и мобильный интернет) можно попробовать использовать GSM-модем.

Возьмем самый простой модем NEOWAY M590. Для его подключения требуется организовать еще один последовательный порт. Схема подключения приведена на рис. 5. Модемы встречаются в разных вариантах исполнения. Тот, который будет использован имеет двухрядный 20-контактный разъем, в котором будем использовать несколько четных контактов. Второй ряд нечетных контактов для простоты не показан.

Рис. 5.увеличенное изображение

Для питания рекомендуется использовать источник с напряжением не выше 5 вольт. Потребляемый модемом ток может достигать (как пишут) 2 А, поэтому возможно потребуется несколько более мощный источник, чем USB-порт.