Визуальное представление

Рисунок 1 - Условная схема подключения
Рисунок 1 - Условная схема подключения
Рисунок 2 - принципиальная схема
Рисунок 2 - принципиальная схема

Описание

Устройство представляет собой программируемый кнопками S1-S3 Двух фазный генератор от 1Гц до 9999Гц, информация выводится на символьный LCD1602. В программном коде используются оба канала 16-битного таймера (см. рисунок 3 - Timer1). Сдвиг фазы устанавливается у канала В. Arduino UNO можно заменить на NANO или любую другую особых изменений не требует, но будьте внимательны!

Cсылка на проект.

*будете внимательны https://www.tinkercad.com/ нет библиотеки GyverTimers.h.

В схеме предусмотрена регулировка, с помощью "ШИМ", яркости и контрастности (функция DISPLAY_AND_CONTRAST() - используется по необходимости). Так же программно возможно установить сдвиг фазы у обоих каналов, но частота для обоих каналов устанавливается одна (так как используется 1 таймер), Во время настройки, оба канала выключены, после установки необходимых значений каналы включаются, что свидетельствует изменение в правом верхнем углу "ON" и "OFF".

Для регулировки частоты каждого канала - придется привнести некоторые не значительные изменения:

  1. Необходимо для 2 выхода использовать любой другой таймер (рекомендую канал B если не вчитываться в тонкости) ориентируясь по таблице (ссылка от автора GyverTimers.h) - https://alexgyver.ru/gyvertimers/

    Рисунок 3 - таблица таймеров ATmega328p
    Рисунок 3 - таблица таймеров ATmega328p

    *Обратите внимание на минимальную частоту таймеров.

  2. Изменить «контент» на LCD, проще использовать LCD1604, или придумать удобное визуальное представление, для ручной настройки частоты и сдвига обоих таймеров.

    *Обратите внимание, что сдвиг фазы будет относительно самого таймера (и используемого канала), а не между ними (между таймерами). Хоть программно запуская оба таймера условно можно принять на веру их синхронность, однако это будет не совсем корректно.

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

Для увеличения регулируемого диапазона частоты необходимо увеличить "cегментность" (количество разрядов) или произвести переключение с Гц на КГц, но помните что макс частота 1МГц(см. Рисунок 3)., придется так же изменить незначительно программный код, с теми условиями которые необходимы Вам, к примеру:

  1. По нажатию на Hz изменить на KHz,

  2. В коде изменить изначально на KHz

  3. По вашему усмотрению.

ИТОГ

Простое устройство паразитирующее библиотеку "GyverTimers.h", которая сильно упрощает работу с таймером. Программный код разделен на несколько условных блоков "работа с LCD" и "настройка таймера".

Программный код (Arduino C ):

*Обратите внимание что с вкл LCD, питая всю плату о USB порта компьютера, вполне возможно произойдет ситуация, что Arduino не будет прошиваться, отключите LCD, во время загрузки прошивки.

/****************************************************/
/*               2-х фазный генератор               */
/*                                                  */
/*                                                  */
/*                                                  */
/****************************************************/
// подключение библиотек
#include <GyverTimers.h>    // Библиотека GyverTimers (для работы с таймером И! устанавливать сдвиг фазы)
#include <LiquidCrystal.h>  // Подключаем стандартную библиотеку LiquidCrystal

/****************************************************/
// Инициализируем объект-экран, передаём использованные
LiquidCrystal lcd(8, 7, 15, 14, 11, 12);  // RS, E, D4, D5, D6, D7 */

/****************************************************/
// Инициализация пинов
#define LEFT_BUTTON 4                // левая кнопка  
#define RIGHT_BUTTON 2               // права кнопка           
#define CENTRE_BUTTON 3              // центральная кнопка 
#define DISPLAY_CONTRAST_ON 5        // Управление контрасностью ШИМ
#define DISPLAY_LIGHT_ON 6           // Управление подсветкой ШИМ 
#define CHANNEL_TIMER_A 9            // выход канала А 1 таймера
#define CHANNEL_TIMER_B 10           // выход канала В 1 таймера

/****************************************************/
//таймер
#define AMOUNT_VALUE_FREQUENCY_NUMBER 4      // количество знаков значения частоты обоих каналов
#define AMOUNT_VALUE_FREQUENCY_DEVIATION 2   // количество знаков значения отклонения частоты канала В
#define ON_TEXT "ON "                        // Текст на LCD для отображения того что каналлы вкл
#define OFF_TEXT "OFF"                       // Текст на LCD для отображения того что каналлы выкл
  
/****************************************************/
// Положение курсора 
int cur_col = 0;                     // текущая координата указателя (столбец)
int cur_row = 0;                     // текущая координата указателя (строка)  

/****************************************************/
// LCD
// для использования другого необходимо отредактировать:
#define NUMBER_COL_USE_LCD 16        // количетсво столбцов (установить от 1) но в дальнейшем использовать от 0
#define NUMBER_ROW_USE_LCD 2         // количетсво строк (установить от 1) но в дальнейшем использовать от 0
int number_row[]  =                  // номера строк LCD ( используется для курсора)
{ 0, 1};

/****************************************************/
// контент LCD
// **страница 1**
// внешний вид страницы 1
String content_page_one[] =          // массив контента 1 страницы
{ "Ch1:0100Hz ->ON ",                // 0 строка
  "F1:180'"};                        // 1 строка

int position_cursor_page_one[4][4] = // координтаы по которому необходимо перемещать курсор( 1 индекс = строка) (2 индекс = столбец)                  
{
  { 4, 5, 6, 7},                     // Координаты 1 ряда 1 страницы (0-3)
  { 3, 4, 5},                        // Координаты 2 ряда 1 страницы (4-5) 
};  
int amount_position_page_one[] =     // Количество(размер массива) координат на одной линии
  { 3, 2};                           //( так как самого начала он должен быть известен)( sizeoof - работает во время компиляции, а не кода)
     
// Значения(VALUE) используемые на странице 1 (индекс = разряду( начало с конца)
int value_content_one_page[][4] = 
{
  { 0, 1, 0, 0},                     // частота канала A и B
  { 1, 8, 0},                        // сдвиг по фазе канала  B
};
int amount_value_page_one[] =        // Количество(размер массива) изменяемых значений на одной линии
  { 3, 2};  

/****************************************************/
// Символы ASCII начиная с 0 
#define translate_to_ASC 48          // 0  в кодировке ASCII

/****************************************************/
// Работа со временм
#define last_press_For_buttom  200   // условный антидребезг контактов
#define micros_work 300              // время 1 цикла (loop)( перемещение курсора или редактирование)
unsigned long Work;                  // переменная для сравнения истекшего отрезка времени в loop
unsigned long last_press;            // Время Прошлого нажатия
/****************************************************/
// Флаги 
boolean flag_editor_enable ;         // включен редактор одного символа на lcd
/****************************************************/

/****************************************************/
/****************  РАБОТА С LCD  ********************/
/****************************************************/
//--------------------------------------------------//
// функция для перевода int в число из кодировки ASCII 
char func_change_int_to_ASC(int param0){return param0 + translate_to_ASC;}
//--------------------------------------------------//
// функция для перевода из кодировки ASCII в int
int change_ASC_to_int(char param){return param - translate_to_ASC;}
//--------------------------------------------------// 
// функция установки контента на LCD                                          
void load_page_to_lcd(String *array_content){       // массив в котором хранится контент 
  lcd.clear();                                      // очистить LCD
  for (int i=0; i < NUMBER_ROW_USE_LCD ;i++){       // выводим текст равный ( инициализированному значению страницы для LCD)
    lcd.setCursor(0,i);
    lcd.print(array_content[i]);
  }
} 
//--------------------------------------------------//
// функция опроса кнопки 
boolean ask_button(int number_pin){
  boolean is_button_press = false;
  if (!digitalRead(number_pin) && (millis() - last_press > last_press_For_buttom)){  // проверка на нажатие  защита от антидребезг + множетсвенное нажатие
    is_button_press = true;
    last_press = millis();
  }
  return is_button_press;
}
//--------------------------------------------------//
// функция установки курсора
void set_cursor(int param0,int param1){             // param0 = col, param1 = row
  lcd.setCursor(param0,param1);
  lcd.cursor();                 // подчеркивание
}
//--------------------------------------------------//
// функция перемещения курсора
void move_cursor(){
  if(ask_button(LEFT_BUTTON)){       // нажата левая кнопка 
    int cur_index_col = find_index(position_cursor_page_one[cur_row],
      amount_position_page_one[cur_row],cur_col);                       // найти  текущий индекс числа (столбца) 
    cur_index_col --;                                                   // уменьшить текущий индекс
    if(is_index_exp(amount_position_page_one[cur_row],cur_index_col)){  // если надо перенести строку
      cur_row --;                                                       // опустить курсор выше
      if(is_index_exp(NUMBER_ROW_USE_LCD - 1, cur_row)){                // надо ли перейти в начало или конец страницы
        cur_row = number_row[NUMBER_ROW_USE_LCD - 1];                   // установить последнюю строку
      }
      cur_col = position_cursor_page_one[cur_row][              
        amount_position_page_one[cur_row]];                             // получить индекс последнего элемента ({[текущая строка] [размер массива])                       
    }
    else{                                                               // если НЕ надо переносить строку и перехоить в начало или конец
      cur_col= position_cursor_page_one[cur_row][cur_index_col];        // присвоить новое текущее значение
    }
    set_cursor(cur_col,cur_row);     // установить новые координаты для курсора
  }
  
  else if(ask_button(RIGHT_BUTTON)){ // нажата правая кнопка 
    int cur_index_col = find_index(position_cursor_page_one[cur_row],
      amount_position_page_one[cur_row],cur_col);                       // найти  текущий индекс числа (столбца) 
    cur_index_col ++;                                                   // увеличить текущий индекс
    if(is_index_exp(amount_position_page_one[cur_row],cur_index_col)){  // если надо перенести строку
      cur_row ++;                                                       // опустить курсор ниже
      if(is_index_exp(NUMBER_ROW_USE_LCD - 1, cur_row)){                // надо ли перейти в начало или конец страницы
        cur_row = number_row[0];                                        // установить на первую строку
      }
      cur_col = position_cursor_page_one[cur_row][0];                   // индекс первого элемента ({[текущая строка] [размер массива])                         
    }
    else{                                                               // если НЕ надо переносить строку и перехоить в начало или конец
      cur_col= position_cursor_page_one[cur_row][cur_index_col];        // присвоить новое текущее значение
    }
    set_cursor(cur_col,cur_row);     // установить новые координаты для курсора
  }
}
//--------------------------------------------------//
// функция нахождения индекса значения в массиве int( если не найдет вернет 9999 - условная ошибка)
int find_index(int *received_array,int syze_array,int number){
  for(int i=0 ;i <= syze_array;i++)if(received_array[i] == number)return i;
  return 9999;
}
//--------------------------------------------------//
// функция проверки если индекс вышел из диапазона( IndexOutOfRangeException )
boolean is_index_exp(int syze_array, int index){
  if(syze_array < index or index < 0) return true;
  else {return false;}
}
//--------------------------------------------------//
// функция изменения 1 значения(разряда) на LCD
void change_one_segment_on_lcd(){
  if(ask_button(LEFT_BUTTON)){                            // нажата левая кнопка    
    int buff = value_content_one_page[cur_row][
      find_index(position_cursor_page_one[cur_row],
        amount_value_page_one[cur_row],cur_col)];         // взять текущее значение 
    buff++;                                               // увеличить на 1
    buff = func_zero_nine(buff);                          // проверить
    lcd.print(func_change_int_to_ASC(buff));              // отобразить
    set_cursor(cur_row,cur_col);                          // вернуть курсор
    value_content_one_page[cur_row][
      find_index(position_cursor_page_one[cur_row],
        amount_value_page_one[cur_row],cur_col)] = buff;  // сохранить
  }
  else if(ask_button(RIGHT_BUTTON)){                      // нажата правая кнопка
    int buff = value_content_one_page[cur_row][
      find_index(position_cursor_page_one[cur_row],
        amount_value_page_one[cur_row],cur_col)];         // взять текущее значение 
    buff--;                                               // уменьшить на 1
    buff = func_zero_nine(buff);                          // проверить
    lcd.print(func_change_int_to_ASC(buff));              // отобразить
    set_cursor(cur_row,cur_col);                          // вернуть курсор
    value_content_one_page[cur_row][
      find_index(position_cursor_page_one[cur_row],
        amount_value_page_one[cur_row],cur_col)] = buff;  // сохранить
  }
}
//--------------------------------------------------//
// фукция изменения числа(цикла) 0-9 
int func_zero_nine(int number){
  switch(number){
    case 10: return 0; break;
    case -1: return 9; break;
    default: return number; break;
  }
}
//--------------------------------------------------//
void on_off_lcd_for_page_one(String param0){
  lcd.setCursor(13,0);
  lcd.print(param0);
}
//--------------------------------------------------//
// Управление подсветкой и контрастностью ( не используется )
void DISPLAY_AND_CONTRAST()  { 
  //analogWrite(DISPLAY_LIGHT_ON,0);     // Вкл ШИМ подсветки
  //analogWrite(DISPLAY_CONTRAST_ON,0);  // Вкл ШИМ контрасности          
}
/****************************************************/
/*********************  ГЕНЕРАТОР  ******************/
/****************************************************/
//--------------------------------------------------//
// запуск таймера
void start_timer(){
  Timer1.enableISR(CHANNEL_A);                      // запускаем таймер 1 канал А
  Timer1.enableISR(CHANNEL_B);                      // запускаем таймер 1 канал B
  Timer1.setFrequency(get_freq());                  // установить частоту
  Timer1.phaseShift(CHANNEL_B,get_dem_freq());      // установить сдвиг фазы
}
//--------------------------------------------------//
// настройка таймера
void set_timer(){
  Timer1.outputEnable(CHANNEL_A, TOGGLE_PIN);       // в момент срабатывания таймера пин будет переключаться
  Timer1.outputEnable(CHANNEL_B, TOGGLE_PIN);       // в момент срабатывания таймера пин будет переключаться
  Timer1.outputState(CHANNEL_A, HIGH);              // задаём начальное состояние пина 11
  Timer1.outputState(CHANNEL_B, HIGH);              // задаём начальное состояние пина 3
}
//--------------------------------------------------//
// прерывание таймера TIMER1_A
ISR(TIMER1_A) {
  // код по обработке прерывания
  // необходимо объявить даже если тело пустое для GyverTimers.h
}
//--------------------------------------------------//
// прерывание таймера TIMER1_B
ISR(TIMER1_B) {
  // код по обработке прерывания
  // необходимо объявить даже если тело пустое для GyverTimers.h
}
//--------------------------------------------------//
// получение значения частоты
int get_freq(){
  return (value_content_one_page[0][0] * 1000 +
    value_content_one_page[0][1] * 100 +
      value_content_one_page[0][2] * 10 +
        value_content_one_page[0][3]) * 2;
        // неободимо умножать на 2 частоту( если меанд)
        // причина в работе таймера и GyverTimers.h
}
// получение значения сдвига фазы
//--------------------------------------------------//
int get_dem_freq(){
  return value_content_one_page[1][0] * 100 +
    value_content_one_page[1][1] * 10 +
      value_content_one_page[1][2];
}

/****************************************************/
/********************  MAIN  ************************/
/****************************************************/
//--------------------------------------------------//
// SETUP
void setup() {
  lcd.begin(NUMBER_COL_USE_LCD, NUMBER_ROW_USE_LCD);  // Инициализируем дисплей: 
  pinMode(LEFT_BUTTON, INPUT);                        // Инициализируем кнопки:
  pinMode(RIGHT_BUTTON, INPUT);      
  pinMode(CENTRE_BUTTON, INPUT);       
  
  pinMode(DISPLAY_CONTRAST_ON, OUTPUT);               // управление подсветкой 
  pinMode(DISPLAY_LIGHT_ON, OUTPUT);                  // управление контрасностью 

  pinMode(CHANNEL_TIMER_B, OUTPUT);                   // Выход таймера канал B
  pinMode(CHANNEL_TIMER_A, OUTPUT);                   // Выход таймера канал A 

  digitalWrite(14, LOW);                              // ( перевод в цифровое состояние и записать 0) пина 14(А0)
  digitalWrite(15, LOW);                              // ( перевод в цифровое состояние и записать 0) пина 15(А1)
 
  set_timer();                                        // настройка таймера 
  start_timer();                                      // запуск таймера ( с начальной частотой и отклонением )
 
  load_page_to_lcd(content_page_one);                 // загрузить 1(начальную) страницу
  cur_row = number_row[0];                            // задать начальные координаты курсора (строка)
  cur_col = position_cursor_page_one[0][0];           // задать начальные координаты курсора (столбец)
  set_cursor(cur_col,cur_row);                        // установить курсор 
  flag_editor_enable = false;                         // выключить редактирование (включить перемещение курсора)
}
/****************************************************/
//--------------------------------------------------//
// LOOP
void loop() {
  if (micros() - Work > micros_work){     // Выполнять цикл раз в (micros_work)  (Работа с LCD дисплеем)
    if(flag_editor_enable){               // изменение 1 сегмента   
      lcd.blink();                        // выключить мигание
      set_cursor(cur_col,cur_row);        // установить курсор 
      change_one_segment_on_lcd();        // Опрос боковых кнопок ( для изменения)
      if(ask_button(CENTRE_BUTTON)){      // опрос центральной кнопки
        flag_editor_enable = false;       // выключить редактирование
        on_off_lcd_for_page_one(ON_TEXT); // отобразить на дисплее включение
        Timer1.setFrequency(get_freq());  // установить частоту и запустить таймер
        Timer1.phaseShift(
          CHANNEL_B,get_dem_freq());      // установить сдвиг фазы
      }
    }
    else{                                 //перемещение курсора
      lcd.noBlink();                      // включить мигание
      set_cursor(cur_col,cur_row);        // установить курсор 
      move_cursor();                      // Опрос боковых кнопок ( для перемещения)
      if(ask_button(CENTRE_BUTTON)){      // опрос центральной кнопки
        flag_editor_enable = true;        // включить редактирование
        on_off_lcd_for_page_one(OFF_TEXT);// отобразить на дисплее выключение
        //Timer1.pause();                 // приостановить
        Timer1.stop();                    // приостановить и сбросить
      }
    }
    Work = micros();                      // этот цикл завершен
    }  
}

/****************************************************/

Признаю, что все можно сделать лучше, но я себе поставил задачу сделать как можно "more universal" - по минимум изменений. Надеюсь на 3+ выполнил.=)

Комментарии (14)


  1. dlinyj
    09.08.2023 10:37
    +4

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


    Скажите, вы делали проверку частотомером, что это работает корректно или просто доверились коду?


    1. iig
      09.08.2023 10:37

      Ловите тестировщика ;)

      Таки да, целочисленное деление вещь забавная.


      1. dlinyj
        09.08.2023 10:37
        +1

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


    1. RivayMark Автор
      09.08.2023 10:37
      +1

      Добрый день мне они показались крайне простыми, просто упал выбор). I2c платы для LCD у меня под рукой не было подключил по параллельному интерфейсу. Проверку разумеется выполнил так как пользуюсь этим устройством( рисунок ниже ,измерил 2-канальным USB осциллографом )


      1. dlinyj
        09.08.2023 10:37

        Осциллограф — это аналитический прибор, а не измерительный. Но даже с ним можно проверить в разном диапазоне, и оценить погрешность.


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


        1. RivayMark Автор
          09.08.2023 10:37

          Нет проблем! Да, Вы правы, но мне не хотелось вручную крутить магнитную муфту( по сути эмулирую сенсор), подсказали такую вещь. Увидел что это довольно просто( работа с таймером) большая часть кода забита для LCD. Знакомый посоветовал выложить ничем не примечательный "инструмент". Да же не представляю зачем мне это генератор еще необходим=)

          *Это мой первый опыт в "репосте" простой разработки, наверное стоит написать зачем это нужно))))))))


          1. dlinyj
            09.08.2023 10:37

            *Это мой первый опыт в "репосте" простой разработки, наверное стоит написать зачем это нужно))))))))

            Надеюсь не последний и желаю удачи в дальнейших постах :)))


          1. iig
            09.08.2023 10:37
            +1

            наверное стоит написать зачем это нужно))))))))

            Наверное да.

            И назвать это "эмулятор сенсора".


  1. kalapanga
    09.08.2023 10:37

    А в чём практический смысл для пользователя этого устройства в сдвиге фазы сигнала относительно ардуиновского таймера? Вот есть выходная клемма, на ней сигнал, он сдвинут по фазе относительно чего-то там внутри этого устройства. И что с этой информацией делать, как применить?

    Вот сдвиг фазы между каналами - это понятно, и пощупать можно.


    1. iig
      09.08.2023 10:37

      Вот сдвиг фазы между каналами - это понятно, и пощупать можно.

      На схеме нарисовано 2 канала ;)


    1. RivayMark Автор
      09.08.2023 10:37

      День добрый! Наверное стоило написать зачем мне это устройство, но я подумал что не к чему). Касаемо вопроса. 2 Выхода таймера 1. канал В сдвинут на 180 градусов. Практический смысл эмулятор работы, условно не в даваясь в детали - "датчика воды" ( для счетчиков воды) эмулировать их работу соответственно.


  1. REPISOT
    09.08.2023 10:37
    +1

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


    P.S. Ссылка на проект с требованием регистрации. Не надо так.


    1. RivayMark Автор
      09.08.2023 10:37

      Добрый день! Могу понять, правда это не мои правила, этого хочет tinkercad.


      1. Ivanii
        09.08.2023 10:37

        Наверно нужно пользоваться другими ресурсами, не требующими регистрации для чтения?