По следам черного самурая, или делаем игру «Sokoban» своими руками (Часть 2 из 3).



Анекдот-эпиграф.
Закончилась посадка на суперлайнер ИЛ-2086. В салон выходит стюардесса:
Дамы и господа, для того, чтобы помочь вам скоротать время полета,
на борту нашего лайнера имеются библиотека, кинозал, три бара,
ресторан, бассейн и два теннисных корта. А теперь я попрошу вас
пристегнуть ремни безопасности, потому что сейчас вместе со всей этой
фигней мы попытаемся взлететь!

В прошлой части я рассказал о выбранных схемотехнических решениях, которые вошли в данный проект. Были рассмотрены и выбраны схемы основных модулей, сделана печатная плата и все это “хозяйство” размещено в корпус. А в этой части статьи я расскажу о важных, с моей точки зрения, программных вопросах работы с этими модулями.

Итак, наша игровая консоль состоит из:
1) Экрана;
2) Кнопок;
3) Системы энергоснабжения;
4) Звуковой подсистемы.

Зарядная часть и Step-Up преобразователь — автономные компоненты и никак программно не управляются. А вот про работу с экраном, клавиатурой и про воспроизведение мелодии поговорим подробнее.

Итак, важнейшая часть консоли — экран.
Напомню, в нашем распоряжении имеется матричный монохромный дисплей со светодиодной подсветкой. Разрешение экрана — 128х64 точек и фактически он состоит из двух половинок размерностью 64х64 точки. Левая и правая части дисплея управляются отдельными контроллерами типа KS0108. Каждый контроллер имеет внутреннее ОЗУ емкостью 512 байт (4096 бит).

Можно представить себе, что память экрана — некий склад с открытыми стеллажами. И загруженные в него данные, как предметы на стеллажах, можно РЕАЛЬНО увидеть. В прямом смысле: без всяких преобразований, пересчета, кодов и т.п. Каждый установленный бит в байте — это горящая точка на экране.

На дисплей передается по 8 бит данных одновременно. Эти 8 бит будут отображены в 8 “пикселях” матрицы экрана вертикально: верхним будет бит 0, нижним – бит 7. Перед вводом байта данных, необходимо задать начальные координаты экрана, в которых отобразится бит 0. Поместим в эту память 0xFF (b11111111) и можем лицезреть на экране вертикальную линию на 8 пикселей 😉
Данные записываются в блоки по 64 байта. И таких блоков — 8 штук на каждый контроллер. Их иногда называют страницами.

Карта дисплея выглядит так:

Напомню, что подключение экрана в этом проекте не совсем обычное.

Такое подключение буквально на чуть-чуть усложняет момент выставления байта на шине данных:

//выставление на шине данных через сдвиговый регистр
void lcd_port_data (byte data)
{
   digitalWrite(lckPin,LOW);
   shiftOut(dataPin, clockPin, MSBFIRST, data);
   digitalWrite(lckPin,HIGH);
} //end lcd_port_data

Чуть расскажу о назначении выводов:
CS1 и CS2 — выбираем контроллер (или другими словами, половинку экрана), для которого предназначаются данные или команды. Можно выбрать сразу оба. Кстати, бывают варианты дисплеев с прямым или инверсным управлением (в зависимости от модели индикатора и его производителя).
D0-D7 — шина данных.
DI — уровень определяет, что будем отправлять в дисплей (1-данные, 0 — команда).
E — тактирующий сигнал для чтения и записи. По перепаду логического уровня с высокого в низкий на этом выводе происходит чтение или запись данных и команд.

В общем случае управление дисплеем представляет собой следующую последовательность операций:

1) Управляющим уровнем на CS1 и CS2 выбираем нужную половинку экрана.
2) Выставляем на выводы DB0-DB7 данные или команду, а на DI — тип информации.
3) Устанавливаем высокий уровень на выводе E.
4) Сбрасываем в низкое состояние уровень на выводе E. Данные (или команда) отправлены.

Ниже таблица команд, которые я использовал при написании кода:

Мы не планируем зажигать отдельные точки (т.к. это требует дополнительных расчетов и в данном проекте нам это не нужно), а будем работать с целым знакоместом 8х8 точек, или 8 байт. Вроде его называют спрайтом. Может это неправильное название, но я далее в тексте буду его использовать как синоним одного символа размером 8х8 пикселей. Это будет минимально отображаемая сущность. И, соответственно, далее будем оперировать “укрупненной” системой координат:

Каждая клетка содержит 8 байт данных — это одно знакоместо (или спрайт).
Набор функций работы с дисплеем (для “квадратно-гнездового” метода) минимален:
Инициализация дисплея

void LCD_Init()

инициализация пинов подключения, переключение дисплея на вывод информации и установка начальных координат в X=0 и Y=0.

Очистка экрана

void LCD_Clr()

заполнение видеопамяти “0”. Делается в один проход, одновременно производя запись в оба контроллера.

Вывод символа в нужной позиции

void LCD_out_XY(byte x, byte y, byte n_out)

Вывод спрайта с кодом n_out в “укрупненных”координатах.
В принципе все. База есть.

Теперь немного расскажу о принципах формирования минимально отображаемого спрайта. Уверен, что для получения байтовой последовательности существует огромное количество специализированных программ. Я их даже не искал. Дело в том, что у меня очень давно “живет” на винчестере обычный EXEL-евский документ Font-Calculator. Даже не припомню где я его взял. Я немного его изменил под свои нужды, и в таком виде он служит мне верой и правдой. С его помощью я уже нарисовал немало разных шрифтов и картинок.

Думаю, что принцип работы будет понятен из картинки.
Я сделал над собой героическое усилие и нарисовал САМУЮ НУЖНУЮ часть ASCII таблицы: символы от пробела (код 32) до буквы Z (код 90), которые идут подряд. Так будет удобно находить начало символа в массиве знакогенератора. Для этого отнимаем от кода символа соответствующую константу-смещение и умножаем на 8 — именно столько байт занимает один символ.
Пример. Если массив знакогенератора начинается с символа пробел (код 32) , константа-смещение =32. Таким образом, для вывода знака “!” (код 33) мы должны читать подряд 8 байт из массива-знакогенератора начиная с (33-32)*8=8-ой позиции.
Фу-уу… пока на этом этапе с экраном остановимся.
Ниже небольшой скетч, который просто заполняет экран цифрами:

/*
GLCD with 74HC595 Adapter Demo
For Game-console Sokoban
Демо: Заполнение экрана цифрами :)
Cyberspring 2015 (robocraft.ru)
(с) Ghost D. 2015
*/
//Пины подключения 74HC595
byte lckPin=9;
byte clockPin=8;
byte dataPin=10;

//пины для LCD
byte DI_pin=4;
byte E_pin=6;
byte CS1_pin=11;
byte CS2_pin=7;
byte RST_pin=3;

// массив символов
byte my_char[]={
    62,81,73,69,62,0,0,0,   //0
    0,66,127,64,0,0,0,0,    //1
    66,97,81,73,70,0,0,0,    //2
    33,65,69,75,49,0,0,0,    //3
    24,20,18,127,16,0,0,0,    //4
    39,69,69,69,57,0,0,0,    //5
    60,74,73,73,48,0,0,0,    //6
    1,113,9,5,3,0,0,0,      //7
    54,73,73,73,54,0,0,0,  //8
    6,73,73,41,30,0,0,0,    //9
    0,0,0,0,0,0,0,0        //null
};

//=====================================
//выставление на шине данных через сдвиговый регистр
void lcd_port_data (byte data)
{
 digitalWrite(lckPin,LOW);
  shiftOut(dataPin, clockPin, MSBFIRST, data);
  digitalWrite(lckPin,HIGH);
} //end lcd_port_data
//=====================================
//посылка команды дисплею
void LCD_Com(unsigned char cmd)
{
                lcd_port_data(cmd);//выводим команду на DB0-DB7
                delayMicroseconds(1);
        digitalWrite (E_pin, HIGH);//устанавливаем "1" на E
                delayMicroseconds(1);
                digitalWrite (E_pin, LOW);// сбрасываем Е
                delayMicroseconds(1);
}
//=====================================
//посылка данных дисплею
void LCD_Data(unsigned char data)
{
                lcd_port_data(data);//данные на DB0-DB7
                digitalWrite(DI_pin,HIGH);//устанавливаем "1" на DI - передаются данные
                delayMicroseconds(1);
                digitalWrite (E_pin, HIGH);//устанавливаем "1" на E
                delayMicroseconds(1);
                digitalWrite (E_pin, LOW);// сбрасываем Е
                digitalWrite(DI_pin, LOW);// сбрасываем DI
}
//=====================================
//очистка дисплея
void LCD_Clr()
{
                digitalWrite(CS1_pin,HIGH);//Вкл оба контроллера, чтоб два раза не бегать
                digitalWrite(CS2_pin,HIGH);
                for(unsigned char i=0; i<8; i++)//X
                {
                  LCD_Com(0xB8+i);
                      for(unsigned char j=0; j<64; j++)//Y
                               {
                                 LCD_Com(0x40+j);
                                 LCD_Data(0x0);//записываем везде нули
                               }
                }

                digitalWrite(CS1_pin, LOW);//Выкл оба контроллера
                digitalWrite(CS2_pin, LOW);
}

//=====================================
//Инициализация дисплея
void LCD_Init()
{
  pinMode (E_pin, OUTPUT);
  pinMode (DI_pin, OUTPUT);
  pinMode (CS1_pin, OUTPUT);
  pinMode (CS2_pin, OUTPUT);

  digitalWrite(DI_pin,LOW);
  digitalWrite(E_pin,HIGH);
  digitalWrite(CS1_pin, HIGH);
  digitalWrite(CS2_pin, HIGH);
  digitalWrite(RST_pin, HIGH);

delayMicroseconds(40);
  LCD_Com(0x3F);//Вкл дисплей
  LCD_Com(0x40);//Y=0
  LCD_Com(0xB8);//X=0
//Начальная область отображения
  LCD_Com(0xC0);
}

//=====================================
//вывод одного символа в нужном месте
//позиция x 0..15 (0..7 - CSEL1, 8-15 - CSEL2)
//позиция y 0..8
void LCD_out_XY(byte x, byte y, byte n_out)
{
  if (x>7)
  {
    //если запрошенная позиция принадлежит другому контроллеру..переключаемся
    digitalWrite(CS1_pin, LOW);
    digitalWrite(CS2_pin, HIGH);
    x=x-8;
  }
  else
  {
    digitalWrite(CS1_pin, HIGH);
    digitalWrite(CS2_pin, LOW);
  }

  LCD_Com(0x40+(x*8));
  LCD_Com(0xB8+y);

//вывод на дисплей 8-ми байт
  for (byte j=0;j<8;j++)
  {
    LCD_Data(my_char[(n_out*8)+j]);
  } //end for
}

//=====================================
void setup() {

  pinMode(lckPin, OUTPUT);
  pinMode(clockPin, OUTPUT);
  pinMode(dataPin, OUTPUT);
  LCD_Init();
} //end setup
//=====================================
void loop() {
byte tmp; //выводимый символ
LCD_Clr();

for (byte j=0;j<8;j++)
{
  for (byte  z=0;z<16;z++)
  {
    LCD_out_XY(z,j,tmp);
    tmp++;
    delay(150);
    if (tmp>9) tmp=0;
  } //end z
  }//end j
delay(1000);
}

Теперь добавим немного звукового сопровождения. Куда же без него:)
Начал я свои поиски отсюда: http://www.arduino.cc/en/Tutorial/PlayMelody.
Как-то не интересно. Большинство музыкальных скетчей на Ардуино обычно заточены под конкретную мелодию, и для ее описания заводят два массива:

int melody[] = {  C,  b,  g,  C,  b,  e,  R,  C,  c,  g, a, C };
int beats[]  = { 16, 16, 16,  8,  8,  16, 32, 16, 16, 16, 8, 8 }; 

В первом указаны ноты, а во втором — соответствующие им длительности звучания. Ого, столько возни для одной мелодии!
Помню в детстве, для озвучки программ на бейсике, я пользовался такой чудесной функцией, как PLAY() . В качестве параметров в эту функцию передавалась “строка-мелодия”, в которой были указания о длительности и октаве ноты, темп воспроизведения и т.д в более-менее понятном виде. Я попробовал поискать описание и формат данных для этой функции, но информации в интернете не очень много. Да и с мелодиями как-то напряжно.

А кто нибудь помнит мобильные телефоны, где можно было вручную (кнопками) вводить мелодии? Так вот, готовых мелодий для телефонов даже сейчас навалом — на любой вкус и цвет. Причем для разных телефонов: для Siemens, Motorolla, Nokia, Alcatel и т.д.

Мне, как гитаристу, больше понравился вариант нотной записи для Нокии. Погуглил. Выяснилось, что правильно этот формат называется RTTTL. Описание.
RTTTL (RingTone Text Transfer Language) — это формат предназначенный для передачи мелодий на телефоны Nokia. По сути дела — это текстовый файл, содержащий имя мелодии, управляющую секцию и секцию описывающую ноты.

Как можно заметить, формат имеет вид :
[название:длительность,октава,скорость в минуту(BPM):сама мелодия].

“d=4” длительность ноты по умолчанию. Это значит, что в записи самой мелодии, все ноты, для которых нет особых указаний, будут иметь длительность по умолчанию, т.е. 4. Здесь же отмечу, что если сравнивать длительность RTTTL и длительность ноты в музыкальном понимании, то идет простое соответствие: 1/длительность RTTTL. Таким образом, «d=4» в записи RTTTL означает, что мы играем ноты по умолчанию с длительностью «одна четвертая». Если d=8, то «одна восьмая» и так далее.

“o=2” октава по умолчанию.
Ниже таблица частот нот для разных октав.

В большинстве случаев, для воспроизведения несложных мелодий достаточно 1-2 октав. А если посмотреть на эту таблицу с калькулятором в руках? Заметили, что нота ДО второй октавы по частоте РОВНО в два раза выше, чем та же нота ДО первой октавы? 261.63х2=523.26 Hz. И эта закономерность прослеживается по всей таблице. А значит, с достаточной для наших целей точностью, мы можем хранить частоты нот только для первой октавы. А остальные значения легко получить простым умножением. Дробные значения теряются, но думаю этим можно пренебречь.

“b=125”. Темп или скорость в минуту (BPM). BPM — это количество четвертных нот в минуту. Например: 120 BPM означает, что в минуту играется 120 четвертных нот. Следовательно, 2 четверти в секунду или 120 четвертных ударов метронома в минуту.

Дальше начинается сама мелодия. Из описания следует, что формат записи нот в общем случае такой:

[ДлительностьНотаОктаваДоп.знак]

В качестве разделителя между нотами выступают запятые.
p” — означает паузу.
c,d,e,f,g,a,b” — это ноты.
Дополнительным знаком может быть «.» (точка) — увеличение длительности ноты в полтора раза.

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

Можно массив частот нот заполнить не в соответствии последовательности нот (До-Ре-Ми и т.д.), а согласно алфавиту. Получиться так: Ля-Си-До-Ре-Ми-Фа-Соль. Что это дает? При таком варианте заполнения массива будет легко находить частоту звучания ноты, просто отняв от кода символа, обозначающего ноту, число 65 (если ноты записаны большими буквами) или 97 — если ноты записаны маленькими.

Пример: нота Ми обозначается латинской “E”. Код символа “E” = 69. Считаем: 69-65=4. В массиве по индексу =4 находим частоту этой ноты — 329! То, что нужно!!!
Теперь насчет полутонов или нот со значком #. Нот всего 7, а полутонов 5. Чтобы обыграть этот момент, расширим массив частот полутонами. Если у ноты полутона нет (это ноты Ми и Си), то в массиве просто укажем частоту основной ноты. При обработке таких случаев, в программе будем сдвигать указатель индекса массива на 7.
Вот, что получилось:

Ах да, чаще всего для обозначения нот в RTTTL используются маленькие буквы.

В теории немного подковались. Приступим к практике.
По меркам микроконтроллера, сердце которого стучит с частотой 16 миллионов раз в секунду, воспроизведение одной ноты, даже самой короткой — это вечность. И это время можно использовать для других целей.

Мелодия будет проигрываться в фоновом режиме. Как же это сделать?
Применим принцип “Хорошая домохозяйка”.
Кратко суть этого принципа. Хозяйке нужно сделать уборку и приготовить обед. Первым делом она заполняет кастрюлю продуктами для первого блюда и ставит ее на плиту. Понятно, что глупо сидеть у плиты и ждать пока блюдо приготовится. Она заводит будильник так, чтобы он подал сигнал через полчаса (или сколько там нужно для приготовления этого блюда). И занимается уборкой. По прошествии заданного времени хозяйка отвлекается от уборки и ставит на плиту следующее блюдо. И вновь заводит будильник. Так до тех пор, пока все блюда не будут готовы. По итогу — и обед готов и уборка сделана! Такой вот чудесный принцип многозадачности 🙂

Такая же система будет лежать в основе и нашего фонового воспроизведения мелодии.
Программная реализация:
1) Есть процедура «ОБРАБОТКИ НОТЫ», которая читает одну ноту (или паузу) и запускает ее воспроизведение. И рассчитывает время (текущее время+длительность звучания ноты) следующего своего вызова.
2) В основной программе постоянно отслеживается время запуска описанной выше процедуры. И выполняется ее своевременный запуск.
И так до тех пор, пока строка-мелодия не закончится.

Естественно, чтобы мелодия проигрывалась без «заиканий», НЕ РЕКОМЕНДУЕТСЯ в основном цикле использовать функцию

delay()

или МЕГА-ТЯЖЕЛЫЕ вычисления, чтобы не прошляпить своевременный запуск.

В будущем надеюсь «прикрутить» сюда прерывания.
Ниже пример-исходник:

/*
Проигрывание RTTTL мелодий (beta version)
описание формата http://wingedshadow.com/rtttl_spec.txt
а тут склад готовых  мелодий http://www.cellringtones.com
Ghost D. 2015
*/

// Пин подключения динамика
#define SoundPin 11
// строки-мелодии
// ФОРМАТ - ОЧЕНЬ ВАЖЕН! Ошибки не контролируются и в конце не должно быть паузы :(
// звездочка - говорит о том, что это указатель на начало соотв. строки
char *song ="MissionImp:d=16,o=6,b=240:32d,32d#,32d,32d#,32d,32d#,32d,32d#,32d,32d,32d#,32e,32f,32f#,32g,g,8p,g,8p,a#,p,c7,p,g,8p,g,8p,f,p,f#,p,g,8p,g,8p,a#,p,c7,p,g,8p,g,8p,f,p,f#,p,a#,g,2d,32p,a#,g,2c#,32p,a#,g,2c,a#5,8c,2p,32p,a#5,g5,2f#,32p,a#5,g5,2f,32p,a#5,g5,2e,d#,8d";
char *song1="Abdelazer:d=4,o=5,b=160:2d,2f,2a,d6,8e6,8f6,8g6,8f6,8e6,8d6,2c#6,a6,8d6,8f6,8a6,8f6,d6,2a6,g6,8c6,8e6,8g6,8e6,c6,2a6,f6,8b,8d6,8f6,8d6,b,2g6,e6,8a,8c#6,8e6,8c6,a,2f6,8e6,8f6,8e6,8d6,c#6,f6,8e6,8f6,8e6,8d6,a,d6,8c#6,8d6,8e6,8d6,2d6";
char *song2="Bumer:d=4,o=2,b=125:8e2,4g2,2p,8g2,4e2,2p,8a2,8g2,8a2,8g2,8a2,8g2,8a2,8g2,8a2,4b2";
char *song3="Queen:d=4,o=6,b=140:32e,16p,32p,1e,p,8c,8p,d,8e,1e,8d,8e,f,8g,f,e,2d,d,e,f,8g,f,e,d";

// Для быстрого доступа к соотв. мелодии, будем хранить в индексном масиве
char *music[]={song,song1,song2,song3};

// В массиве частоты нот первой октавы. Нот семь, полутонов 5. Для Получения полутона к индексу массива добавляем 7
//                a     b      c     d     e     f      G        A#   B   C#  D#    E    F#   G#
const int PROGMEM note_tone[]={440,  493,    261,  294,  329,  349,  392,     466, 493,277, 311, 329, 370,415};
//^^^^^^^^^^^^^^^^ указываем, что эти констатнты нужно хранить во Flash

int index=0; //Индекс строки мелодии. Глобальная переменная
long int changeTime=0; //глобальная переменная время следующего изменения ноты
int def_note_len=0; //длительность ноты. Т.к. задается для всех последующих нот, делаем ее глобальной
byte def_octava=1;//октава по умолчанию
long k_note_len=0; //длительность целой ноты в миллисекундах, зависит от значения BPM
boolean playMusic=true; //вообще, играть музыку или нет
boolean repeatMusic=false; //режим циклического воспроизведения
byte num_melody=0; //номер исполняемой мелодии

void setup()
{
  changeTime=millis();
}

void loop()
{
  if (millis()>=changeTime && playMusic) play_one_note(music[num_melody]);//пришло время поменять ноту?
  if (!playMusic) //если музыка выключена, делаем паузу, ставим указатель в начало и включаем
  {
    delay(5000);
    index=0;
    playMusic=true;
    num_melody++;
    if (num_melody==4) num_melody=0;
  }
// тут будет жить основная программа

}//end main loop

//================================================================
// процедура проигрывания одной ноты или паузы. Процедура сама двигает указатель очередного
// символа в строке мелодии. Плюс считает время своего следующего вызова, в зависимости от
// длительности ноты или паузы
//========================================================================
void play_one_note(char *sound)
{
   // byte tmp_char; //сюда считываем один байт из строки мелодии
    byte tmp=0; //временная переменная используется для вычисления тек. длительности и октавы
    byte index_note=0; //индекс ноты в массиве
    int temp_music=120;
    int cur_note_len=1;
//================================================
//====== проверка на окончание строки мелодии =========
if ((*(sound+index))==0) //Если строка мелодии закончилась
  {
    noTone(SoundPin); //выключаем звук
    playMusic=false;//если достигли конца строки - выключаем воспроизведение звука
    //index=0;
    //delay(2000);
  }
//================================================
//====== считывание начальных параметров =========
    if ((index==0) && playMusic)//если только первый раз вызывается, то нужно проигнорировать название
    {

      while (*(sound+index) != ':') index++; //пропускаем название пока не встретим : (hex=0x3a)
      index++; //теперь пропускаем знак :
      // тут обработаем общие установка. Будем подразумевать, что они задаются в таком порядке
      //d=xx,o=x,b=xx

// ====== обрабатываем установку длительности нот по умолчанию ==========
      if ((*(sound+index))=='d')
      {
       index++; index++; //пропускаем символы 'd=', теперь значение *(sound+index) должно указывать на цифру
       def_note_len=readDigit(sound); //пересчет длительности в человеческий вид
       index++; //пропускаем запятую
      } //end if установки длительности

// ====== обрабатываем установку октавы умолчанию ==========
      if ((*(sound+index))=='o')
        {
          index++; index++; //пропускаем символы 'o='
          def_octava=readDigit(sound);
          index++;//пропускаем запятую
         } //end if установки октавы

// ====== обрабатываем установку длительности нот в зависимости от темпа исполнения ==========
      if ((*(sound+index))=='b')
      {
        index++; index++; //пропускаем символы 'b='
        temp_music=readDigit(sound);
/*
BPM — это количество четвертных нот в минуту, например, 120 BPM означает, что в минуту играется 120 четвертных нот
(следовательно, 2 четверти в секунду), или 120 четвертных ударов метронома в минуту.
считаем длительность в миллисекундах целой ноты. Минута=60х1000 = 60000 милисекунд.
*/
           k_note_len=(60*1000L/temp_music)*4;

      } //конец обработки BPM

index++; //пропускаем разделительный ':'
}
 //================================================
 //end  начальной инициализации
 //================================================

//============== а сюда будет попадать если index>0 ==================
//фу, добрались собственно до мелодии
//проверяем прямую установку длительности, т.е., если первые символы - цифры (до 2х знаков)

   cur_note_len = k_note_len/def_note_len; //длительности ноты ставим предварительно по умолчанию
   tmp=readDigit(sound);
//если было прямо указана длительность - пересчитываем
   if (tmp>0) cur_note_len=k_note_len/tmp;
//эту же переменную будем далее использовать для сдвига октавы
   tmp=def_octava; //пока ставим по умолчанию

//==============================================
//если указана пауза
      if ((*(sound+index))=='p')
      {
        if ((*(sound+index+1))=='.') //признак увелич. длит. в полтора раза
          {
            cur_note_len+=cur_note_len/2; //увеличиваем
            index++; //сдвигаем указатель
          }
        changeTime=millis()+(cur_note_len); //считаем время следующего изменения
        noTone(SoundPin); //выключаем звук
        index++;  //пропускаем символ ','. Тут и притаился маленький "косяк", если пауза стоит последней в строке
      } //end обработки паузы

//==============================================
// а тут ловим ноты (т.е. буквы "a".."g" нижнем регистре)
      if (((*(sound+index))>0x60) && ((*(sound+index))<0x68)) //это ноты
    {
      index_note=(*(sound+index))-0x61; //прочитали ноту

      if ((*(sound+index+1))=='#') //второй знак может быть признаком полутога
        {
          index_note=index_note+7;
          index++; //если обработали полутон, сдвигаем указатель на этот полутон
        }

        if ((*(sound+index+1))=='.' or (*(sound+index+1))=='_') //или может указывать на увеличение длительности в полтора раза
        {
          cur_note_len+=cur_note_len/2;
          index++; //если обработали точку, сдвигаем указатель на след. символ
        }

        if ((*(sound + index+1)>0x2F) && (*(sound + index+1)<0x3A))// если цифра - то это прямое указание октавы
           {
             index++;
             tmp=(*(sound + index)-0x30); //
           }

      //выводим текущую ноту
      // формат команды Tone (Пин, частота)
      // Это место в дальнейшем можно заменить на что-то поинтереснее
          if (tmp==4) tmp=8; //для 4-октавы умножаем на 8
          if (tmp==3) tmp=4; //для получения частоты нот для 3-ей октавы нужно умножить значение частоты для 1-ой октавы на 4
          tone(SoundPin, (pgm_read_word (&note_tone[index_note]))*tmp);//читаем word из флеш

          changeTime=millis()+(cur_note_len); //время, при наступлении которого нужно сменить ноту
          //index++;
   }//конец обработки ноты

index++;//сдвигаем указатель на запятую

}// end play_one_note

//===================================
// небольшая оптимизация, считывание цифр со сдвигом указателя
 int readDigit(char *sound_tmp)
{
  int tmpRead=0;
  while ((*(sound_tmp + index)>0x2F) && (*(sound_tmp + index)<0x3A))// если цифра
    {
      tmpRead=tmpRead*10+(*(sound_tmp + index)-0x30); //
      index++;
    }
  return tmpRead;
}

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

Кнопки.
Напомню, выход клавиатуры подключен к аналого-цифровому преобразователю (АЦП) в микроконтроллере Arduino. АЦП имеет разрядность десять бит, и может возвращать численное значение от 0 до 1023, которое связано с аналоговым напряжением от 0 до 5 вольт.Для определения какая нажата кнопка читаем значение с АЦП (функция analogRead ()).
Возьмем маленький тестовый скетч, который просто выводит в СОМ-порт значения с АЦП.

/*
Утилита получения значений с АЦП
при работе с клавишами
 */
int keyPin = A0;
int keyValueADC=0;

void setup()
{
   Serial.begin(9600);
}

void loop() {
  // читаем значения с АЦП
  keyValueADC = analogRead(keyPin);
  Serial.println(keyValueADC); //выводим в ком-порт
  delay(50);
}

Нажимая нужную кнопку смотрим, что вышло.
У меня получились такие значения:

// Значения АЦП, при нажатии соотв. кнопки
#define adc_key_1 940
#define adc_key_2 850
#define adc_key_3 700
#define adc_key_4 620
#define adc_key_5 570

Следует иметь ввиду. Со временем номинал резистора может "уплыть" (от температуры или старости, АЦП тоже может дрейфовать ну и т.д.). Поэтому, в программе-обработчике нужно выполнять сравнение с "плюс-минус" диапазоном около полученной точки. Я неспроста, их расположил в порядке убывания.

В моем случае, я расширил диапазон на 20 значений вниз от замера и проверку делаю так:

if (temp_read>920) return rightButt;
if (temp_read>830) return selButt;
if (temp_read>680) return upButt;
if (temp_read>600) return leftButt;
if (temp_read>550) return downButt;

Это электрический уровень. Он достаточно простой. А вот дальше идет логический фантик-обертка.
Что можно вообще сделать с кнопкой:
- нажать и отпустить (CLICK)
- нажать два раза (DOUBLE CLICK)
- нажать и долго подержать
На самом деле способов управления с помощью кнопки гораздо больше. Но в данном проекте они нам не нужны.
Ограничимся вариантами: НЕ НАЖАТА, НАЖАТА И ОТПУЩЕНА и НАЖАТА И ОТПУЩЕНА СПУСТЯ БОЛЕЕ ОДНОЙ СЕКУНДЫ. Кроме того, необходимо помнить про антидребезг и отсекать ложные срабатывания.

Я находил на просторах интернета разные варианты построения программы-обработки кнопок. Но, либо они были чересчур мудреными (с кучей странных переменных и непонятных If-ов), либо с применением паразитного “delay()”.
Мне более по душе (может потому, что я в этом варианте неплохо разобрался) - “Автомат состояний” или FSM.
Код получается очень лаконичным и компактным. Его легко можно расширять и упрощать (добавляя или удаляя нужные состояния) в зависимости от текущей необходимости. Простыми словами это работает так:

Есть одна функция, которая может находится в одном из 4 состояний (в зависимости от того, что происходило до этого). При вызова функции оператором CASE происходит переход к нужному состоянию.
Для автомата определим четыре состояния:
* НИЧЕГО (none)
* АНТИДРЕБЕЗГ (deBounce)
* НАЖАТИЕ (Click)
* ДОЛГОЕ НАЖАТИЕ (LongPress)
Есть заданные интервалы времени, по прошествии которых меняется состояние.

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

Ниже упрощенный граф состояний.

Пояснение на пальцах (вдруг кому-то пригодится).
Вызов функции. При первом входе - состояние none. Считываем статус кнопки. Кнопка нажата? Да. Ставим следующее состояние deBounce и завершаем свою работу.

При следующем вызове мы попадаем в состояние deBounce- смотрим - Кнопка все еще нажата? Да. А время для отслеживания дребезга контактов вышло? Нет. Завершение работы функции.

Следующий вызов функции. Состояние все еще deBounce. Кнопка все еще нажата? Да. А время для отслеживания дребезга контактов вышло? Да, уже вышло. Ну, значит кнопка точно нажата намеренно. Следующее состояние Click.

Вызов функции. Состояние Click. Кнопка все еще нажата? Нет. Ага, кнопка была 100% нажата и отпущена. Устанавливаем флаг “КОРОТКОЕ НАЖАТИЕ”
и т.д.

Зелененькими облачками на графе изображены СОБЫТИЯ, которые нужно ловить в основной программе.
Обработчик нажатия кнопок в таком виде живет у меня давно и кочует из проекта в проект где нужна эта обработка.

//отладочный вывод в ком-порт
#define debug 1
//===========================
//блок для обработки кнопок
// Это состояния автомата (FSM)
//статус НИЧЕГО
#define bs_none 0
//статус АНТИДРЕБЕЗГ
#define bs_deBounce 1
//Статус КОРОТКОЕ нажатие
#define bs_click 2
//Статус долгое нажатие
#define bs_longPress 4

//Это значения ФЛАГА СОСТОЯНИЯ КНОПКИ
//НИЧЕГО
#define bf_none 0
//КОРОТКОЕ НАЖАТИЕ
#define bf_click 1
//ДЛИННОЕ НАЖАТИЕ
#define bf_longPress 3

//временные задержки (в мс) для обработки нажатия кнопок
//для антидребезга
#define deBounceTime 50
//длительное удержание (секунда)
#define longTime 1000

// Значения АЦП, при нажатии соотв. кнопки
#define adc_key_1  920
#define adc_key_2  830
#define adc_key_3  680
#define adc_key_4  600
#define adc_key_5  550

// кнопки номера
#define noButt 0
#define selButt 1
#define rightButt  2
#define upButt  3
#define downButt  4
#define leftButt  5

byte buttonPin=A0; //пин подключения кнопки (На АЦП получаем: кнопка не нажата=0, кнопка 1=840,кнопка 2=699
byte buttonState=0; //переменная СТАТУС КНОПКИ
byte curButton=0;   // ТЕКУЩАЯ КНОПКА
long timerButton=0; // Время нажатия кнопки
byte pressBut=0; //номер нажатой кнопки
byte buttonFlag=0; //флаг состояния кнопки
//конец блока для обработки кнопок


void setup()
{
#if debug
  Serial.begin(9600);
#endif
} //end setup

//------ начало основного цикла ----------
void loop(){


 checkButton(); //опрашиваем состояние кнопок

   if (buttonFlag!=bf_none){
#if debug
    Serial.print("Press key # ");
    Serial.print (pressBut);
#endif
    switch (buttonFlag)
    {

      case (bf_click):
#if debug
      Serial.println(" -> Shot Press");
#endif
      buttonFlag=bf_none;
      break;
      case (bf_longPress):
#if debug
      Serial.println(" -> Long Press");
#endif
      buttonFlag=bf_none;
      break;

    } //end switch

  } //end if


}//end main loop
//------ конец основного цикла ----------


//---------------- start ReadKey ----------------
// Функция возвращает номер нажатой клавиши
byte ReadKey() //функция считывает значение с АЦП и преобразует в номер нажатой кнопки
{
int temp_read;
temp_read=analogRead(buttonPin);
if (temp_read>adc_key_1) return rightButt;
if (temp_read>adc_key_2) return selButt;
if (temp_read>adc_key_3) return upButt;
if (temp_read>adc_key_4) return leftButt;
if (temp_read>adc_key_5) return downButt;
return noButt;
}//---------------- end ReadKey ----------------

//------------------------
// Start checkButton
//------------------------
void checkButton() {
  switch (buttonState) {
//------------------------------------------
    case (bs_none): //если ничего не нажато
    curButton=ReadKey(); //считываем номер нажатой кнопки
    if (curButton!=noButt) //Если кнопка нажата (т.е., значение <>0)
    {
      buttonState=bs_deBounce; //следующий статус антидребезг
      timerButton=millis(); //запоминаем время
    }
      break;
//------------------------------------------
  case (bs_deBounce): //Антидребезг. Значит уже что-то нажали
    if ((ReadKey()==curButton))//если кнопка все еще нажата
    {
       if (millis()-timerButton>deBounceTime) //и прошло достаточно времени для устаканивания помех
       {
          buttonState=bs_click; //ставим статус FSM
          timerButton=millis(); //опять запоминаем время
          pressBut=curButton; //устанавливаем значение нажатой кнопки
       }
    }
     else
     {
       buttonState=bs_none; //а если кнопка уже не нажата или нажата другая ставим статус НИЧЕГО
     }
  break;
//------------------------------------------
  case (bs_click): //Ага, кнопка ТОЧНО нажата
    if ((ReadKey()==curButton)) //если кнопка все еще нажата
    {
       if (millis()-timerButton>longTime)//если кнопка удерживается более времени для длительного нажатия
       {
          buttonState=bs_longPress; //ставим статус FSM
          timerButton=millis(); //запоминаем время
       }
    }
     else  // если кнопку отпустили, но мы уже попали в этот режим
      {
        buttonFlag=bf_click; // флаг сработки = КОРОТКОЕ НАЖАТИЕ
        buttonState=bs_none;
      }
  break;
//------------------------------------------
  case (bs_longPress): //Длительное нажатие
    if ((ReadKey()!=curButton)) //если кнопка отпущена
    {
      buttonFlag=bf_longPress; //флаг сработки = ДЛИТЕЛЬНОЕ нажатие
      buttonState=bs_none; // а если кнопка уже не нажата или нажата другая ставим статус НИЧЕГО
    }
  break;
//------------------------------------------
  } //end switch
} //end checkButton
//------------------------

Эта часть статьи получилось САМОЙ объемной, но вроде как все, что хотел рассказать- рассказал.
Итак, основные программные моменты рассмотрены. Все исходники, с подробнейшими комментариями я выложу в финальной части. Чуть-чуть терпения 🙂


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

Arduino

Что такое Arduino?
Зачем мне Arduino?
Начало работы с Arduino
Для начинающих ардуинщиков
Радиодетали (точка входа для начинающих ардуинщиков)
Первые шаги с Arduino

Разделы

  1. Преимуществ нет, за исключением читабельности: тип bool обычно имеет размер 1 байт, как и uint8_t. Думаю, компилятор в обоих случаях…

  2. Добрый день! Я недавно начал изучать программирование под STM32 и ваши уроки просто бесценны! Хотел узнать зачем использовать переменную типа…

3D-печать AI Android Arduino Bluetooth CraftDuino DIY IDE iRobot Kinect LEGO OpenCV Open Source Python Raspberry Pi RoboCraft ROS swarm ИК автоматизация андроид балансировать бионика версия видео военный датчик дрон интерфейс камера кибервесна манипулятор машинное обучение наше нейронная сеть подводный пылесос работа распознавание робот робототехника светодиод сервомашинка собака управление ходить шаг за шагом шаговый двигатель шилд юмор

OpenCV
Робототехника
Будущее за бионическими роботами?
Нейронная сеть - введение