Схема подключения матричной клавиатуры к контроллеру. Учебный курс

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

Для Arduino чаще всего используются такие клавиатуры:

Самыми распространенными являются 16 кнопочные клавиатуры 4x4. Принцип их работы достаточно прост, Arduino поочередно подает логическую единицу на каждый 4 столбцов, в этот момент 4 входа Arduino считывают значения, и только на один вход подается высокий уровень. Это довольно просто, если знать возможности управления портами вывода в Arduino , а так же портами входа/ввода.

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

Подключаем клавиатуру в любые порты ввода/вывода.

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

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

Int PinOut {5, 4, 3, 2}; // пины выходы int PinIn {9, 8, 7, 6}; // пины входа int val = 0; const char value { {"1", "4", "7", "*"}, {"2", "5", "8", "0" }, {"3", "6", "9", "#"}, {"A", "B", "C", "D"} }; // двойной массив, обозначающий кнопку int b = 0; // переменная, куда кладется число из массива(номер кнопки) void setup() { pinMode (2, OUTPUT); // инициализируем порты на выход (подают нули на столбцы) pinMode (3, OUTPUT); pinMode (4, OUTPUT); pinMode (5, OUTPUT); pinMode (6, INPUT); // инициализируем порты на вход с подтяжкой к плюсу (принимают нули на строках) digitalWrite(6, HIGH); pinMode (7, INPUT); digitalWrite(7, HIGH); pinMode (8, INPUT); digitalWrite(8, HIGH); pinMode (9, INPUT); digitalWrite(9, HIGH); Serial.begin(9600); // открываем Serial порт } void matrix () // создаем функцию для чтения кнопок { for (int i = 1; i <= 4; i++) // цикл, передающий 0 по всем столбцам { digitalWrite(PinOut, LOW); // если i меньше 4 , то отправляем 0 на ножку for (int j = 1; j <= 4; j++) // цикл, принимающих 0 по строкам { if (digitalRead(PinIn) == LOW) // если один из указанных портов входа равен 0, то.. { Serial.println(value); // то b равно значению из двойного массива delay(175); } } digitalWrite(PinOut, HIGH); // подаём обратно высокий уровень } } void loop() { matrix(); // используем функцию опроса матричной клавиатуры }

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

#include const byte ROWS = 4; const byte COLS = 3; char keys = { {"1","2","3"}, {"4","5","6"}, {"7","8","9"}, {"#","0","*"} }; byte rowPins = {5, 4, 3, 2}; byte colPins = {8, 7, 6}; Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS); void setup(){ Serial.begin(9600); } void loop(){ char key = keypad.getKey(); if (key != NO_KEY){ Serial.println(key); } }

  1. Изучить особенности работы параллельных портов микроконтроллера.
  2. Изучить схемы подключения кнопок и матричной клавиатуры к микроконтроллеру.
  3. Научиться определять состояние кнопок при помощи программы.
  4. Изучить способы отладки программ на лабораторном стенде LESO1.
  5. Изучить принцип работы матричной клавиатуры.

2 Предварительная подготовка к работе

  1. По конспекту лекций и рекомендуемой литературе изучить схемы параллельных портов микроконтроллеров.
  2. По конспекту лекций и рекомендуемой литературе изучить схемы подключения кнопок и клавиатуры к параллельным портам.
  3. Изучить архитектуру микроконтроллера ADuC842 .
  4. Изучить принципиальную схему лабораторного стенда LESO1.
  5. Составить алгоритм работы программы: при нажатии на кнопку, согласно варианту, загорается комбинация светодиодов, соответствующая в бинарном виде номеру кнопки; при отпускании кнопки, светодиоды должны погаснуть.
  6. Составить программу на языке программирования С.

3 Краткие теоретические сведения

3.1 Применение матричной клавиатуры для ввода информации в микропроцессорную систему

Для реализации взаимодействия пользователя с микропроцессорной системой используют различные устройства ввода-вывода информации. В самом простом случае в роли устройства ввода может выступать кнопка, представляющая собой элементарный механизм, осуществляющий замыкание-размыкание контактов под действием внешней механической силы. Схема подключения кнопки к линии ввода параллельного порта ввода микроконтроллера показана на рисунке 1. Когда контакты кнопки S1 разомкнуты через резистор R1 на вход контроллера поступает высокий логический уровень "1", когда же контакты замкнуты, то вход оказывается соединенным с общим проводом, что соответствует логическому уровню "0". Если параллельный порт микроконтроллера имеет встроенный генератор тока, то в схеме можно обойтись без резистора R1.

Рисунок 1 – Подключение одиночной кнопки к параллельному порту

Недостаток приведенной схемы заключается в том, что для подключения каждой кнопки требуется отдельная линия параллельного порта. Так как часто требуется вводить информацию с большого количества кнопок, то для уменьшения количества линий ввода-вывода используется клавиатура, представляющая собой двухмерную матрицу кнопок, организованных в ряды и столбцы (рисунок 2).

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

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


Рисунок 2 – Подключение матричной клавиатуры к параллельному порту

Временная диаграмма напряжений на портах вывода при выполнении программы опроса клавиатуры приведена на рисунке 3.


Рисунок 3 – Временные диаграммы работы порта вывода

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

Программа для микроконтроллера жестко зависит от принципиальной схемы разрабатываемого устройства. Невозможно написать программу для микроконтроллерного устройства не имея перед глазами его схемы. Поэтому, перед началом работы по принципиальной схеме учебного стенда LESO1 следует изучить способ подключения клавиатуры и светодиодов к микроконтроллеру: определить, к каким портам подключены светодиоды, столбцы и строки клавиатуры. Затем по таблице SFR нужно узнать адреса регистров задействованных портов ввода-вывода.

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

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

4 Задание к работе в лаборатории

  1. По принципиальной схеме установите, к каким портам микроконтроллера подключены светодиоды, а также столбцы и строки клавиатуры.
  2. По таблице регистров специальных функций (SFR) определите адреса регистров требуемых портов.
  3. Войдите в интегрированную среду программирования Keil-C.
  4. Создайте и настройте должным образом проект.
  5. Введите текст программы в соответствии с заданием: При нажатии на кнопку, согласно варианту, загорается комбинация светодиодов, соответствующая в бинарном виде номеру кнопки; при отпускании кнопки, светодиоды должны погаснуть.
  6. Оттранслируйте программу, и исправьте синтаксические ошибки.
  7. Загрузите полученный *.hex файл в лабораторный стенд LESO1 .
  8. Убедитесь, что программа функционирует должным образом.

5 Указания к составлению отчета

Отчет должен содержать:

  1. Цель работы.
  2. Принципиальную схему подключения клавиатуры к микроконтроллеру.
  3. Графическую схему алгоритма работы программы.
  4. Исходный текст программы.
  5. Содержимое файла листинга программного проекта.
  6. Выводы по выполненной лабораторной работе.

Схемы, а также отчет в целом, выполняются согласно нормам ЕСКД.

«. Сегодня подключаем матричную клавиатуру к плате Arduino, а также рассматриваем интересные схемы с ней. Сделать такую клавиатуру можно и самому из кнопок и печатной платы. В статье видео-инструкция, листинги программ, схемы подключения и необходимые компоненты.

Большая часть текста содержит объяснение программного кода, его можно скачать либо посмотреть видео под статьей.

Сделать такую клавиатуру можно и самому. Для этого понадобится печатная плата, 12 или 16 обычных кнопок и соединительные провода. Я же буду использовать готовую.

Для чего нужна матричная клавиатура?

Для примера возьмем обычную кнопку. Как вы знаете, это простейшее электромеханическое устройство. Чтобы подключить ее к плате, нужно использовать стягивающий резистор, а также задействовать по одному контакту питания и «земли». Нажатием такой кнопки можно выполнить определенное действие, например можно управлять светодиодом, различными приводами, механизмами и так далее. А что, если нам необходимо подключить несколько кнопок? Тогда придется задействовать больше контактов и большее число проводов, да и без макетной платы уже не обойтись, плюс еще резисторы придется использовать в большем количестве.

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

Подключать ее к плате следует 8 выводами, каждый из них считывает значения с определенных строк и столбцов. Подключать их следует к выводам на панели Digital. Я подключу, например, к выводам от 2 до 9 включительно. Нулевой и первый трогать не желательно, поскольку они предназначены для UART интерфейса (например, для подключения блютуз-модуля). Рациональнее оставить их свободными.

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

После того, как вы установили в библиотеку, можно зайти в Ардуино IDE (программа с сайта Arduino) и посмотреть примеры скетчей.

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

#include // подключаем нашу библиотеку




{"2","5","8","0"},
{"3","6","9","#"},
{"A","B","C","D"}
};




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

if (customKey){
Serial.println(customKey);
}
}

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

Если это сделать неправильно, то, например, при нажатии цифры 4, в порт выйдет цифра 6 или любой другой символ. Это можно определить опытным путем и расположить символы, как они расположены на клавиатуре.

В функции void setup указываем скорость последовательного соединения с монитором порта 9600 бод . Функция нужна только для подачи питания на модули. В функции Void Loop прописываем условие. Переменная Char используется для хранения только одного символа, например, 1, А или 5, что подходит к ситуации. Если нажатие зафиксировано, то происходит вывод символа в монитор порта с помощью функции Serial Print. В скобках нужно указывать, какую переменную выводим в порт. Если все сделано верно, в мониторе порта получим символ, на который нажимали. Не забудьте в мониторе порта внизу справа указать скорость передачи данных такую же, как в скетче.

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

Давайте выведем данные на дисплей.

Я использую дисплей, сопряженный с модулем I2C, который упрощает подключение. Для работы с дисплеем с шиной I2C необходимо установить еще одну библиотеку. Скачать ее можно .

Далее нужно указать размерность дисплея. Используемый в примере дисплей вмещает по 16 символов в каждой из 2-ух строк, это я и указываю. В функции Void Setup нужно подать питание на дисплей и включить подсветку. Делается это с помощью двух функций: lcd.begin и lcd.backlight .

#include // подключаем нашу библиотеку
#include
LiquidCrystal_I2C lcd(0x27, 16, 2);
const byte ROWS = 4; //число строк у нашей клавиатуры
const byte COLS = 4; //число столбцов у нашей клавиатуры
char hexaKeys = {
{"S","4","7","*"}, // здесь мы располагаем названия наших клавиш, как на клавиатуре,для удобства пользования
{"O","5","8","0"},
{"S","6","9","#"},
{"I","B","C","D"}
};
byte rowPins = {5, 4, 3, 2}; //к каким выводам подключаем управление строками
byte colPins = {9, 8, 7, 6}; //к каким выводам подключаем управление столбцами
//initialize an instance of class NewKeypad
Keypad customKeypad = Keypad(makeKeymap(hexaKeys), rowPins, colPins, ROWS, COLS);
void setup(){
Serial.begin(9600);
lcd.begin(); // Инициализируем экран
lcd.backlight();
}
void loop(){
char customKey = customKeypad.getKey();
if (customKey){
Serial.println(customKey);
lcd.setCursor(1,4); //устанавливаем курсор
lcd.print(customKey);
}
}

В функции Voil Loop нужно в самом условии прописать строчку lcd.print для вывода данных на дисплей. И еще нужно предварительно установить положение курсора. В скобках идут 2 цифры: первая — это номер символа, а вторая — номер строки. Нужно помнить, что у этого дисплея отсчет строк и столбцов начинается не с единицы, а с нуля. То есть здесь имеются строчки под номерами 0 и 1, а не 1 и 2, как может показаться сначала. Затем загрузим код в плату и посмотрим, что будет.

Так как дисплей работает по интерфейсу I2C, подключаем к аналоговым выводам. Выходы SDA и SCL соответственно подключаем к А4 и А5, а остальные два — это уже питание и «земля».

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

Чтобы стереть строчку, вспомним калькулятор. Когда нужно было удалить значение, мы нажимали на кнопку сброса. Нажмем на такую кнопку в плате и можем заново набирать символы.

Подключение клавиатуры к Arduino и управляющее действие

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

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

Подключение светодиода

Я буду использовать макетную плату и резистор (желательно использовать от 150 до 220 Ом). Двумя перемычками замкну схему, подключив их к пинам питания и земли на плате Ардуино.

Схема будет работать так: при нажатии на 1 включается светодиод, при нажатии на 2 — выключается.

Светодиод в примере подключен к пину 8 на плате Ардуино.

#include
const byte ROWS = 4; // Four rows
const byte COLS = 4; // Three columns
char keys = { // Define the Keymap
{"1","4","7","*"}, // здесь мы располагаем названия наших клавиш, как на клавиатуре,для удобства пользования
{"2","5","8","0"},
{"3","6","9","#"},
{"A","B","C","D"}
};
byte rowPins = { 5, 4, 3, 2 };// Connect keypad ROW0, ROW1, ROW2 and ROW3 to these Arduino pins.
byte colPins = { 9, 8, 7 ,6}; // Connect keypad COL0, COL1 and COL2 to these Arduino pins.
Keypad kpd = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);// Create the Keypad
#define ledpin 8
void setup()
{
pinMode(ledpin,OUTPUT);
Serial.begin(9600);
}
void loop()
{
char key = kpd.getKey();
if(key) // Check for a valid key.
{
switch (key)
{
case "1":
digitalWrite(ledpin, HIGH);
break;
case "2":
digitalWrite(ledpin, LOW);
break;
}
}
}

Давайте разберем скетч.

Возьмем первый скетч урока и просто его дополним. В начале с помощью полезной функции Define присвоим название подключенному к пину 8 светодиоду ledpin . В функции Void setup указываем сигнал со светодиода как выход.

Если бы не библиотека Кейпад для клавиатуры, пришлось бы прописывать то же самое для 8 пинов, с которыми связана клавиатура. В функции void loop условие. Нажатие определенной клавиши «приравнивается» к значению переменной key. Оператор Switch сравнивает значения переменной key и «дает» определенные команды в зависимости от этого значения. Состоит он из двух вспомогательных операторов Case и Break . Проще говоря, если будет найдено значение переменной, равное 1 , то будет выполняться действие. Оператор break служит командой выхода из оператора Case .

Соответственно при нажатии на 1 будет выполняться максимальная подача напряжения на светодиод и он будет гореть. При нажатии на 2 он гореть не будет. Это указывается в функции Digitat write , где в скобках задается название переменной и «указание» ей. Можно таким образом написать определенные команды для каждой кнопки и управлять большим количеством светодиодов или создать команду для включения всех светодиодов сразу.

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

Первым делом выбирается тема изучения. Эта статья — про матричную клавиатуру. Далее нужно разобраться, что это такое, основной источник — википедия. Если там нет информации — гугл. После прочтения 3-5 ссылок, главное понять что оно из себя представляет и зачем вообще нужно. Ответ на первый вопрос должен быть примерно таким — это набор кнопок, включенных и опрашиваемых особым образом. Так почему не использовать обычное включение кнопок? Это ответ на второй вопрос — подобное включение, при использовании 16 и более кнопок, позволяет сэкономить ножки микроконтроллера. Все очень просто, на первом этапе этой информации более чем достаточно.

Теперь следующий шаг, изучение схемы. Во время изучения схемы микроконтроллер представляем черным ящиком, который может либо дрыгать ножками, либо читать информацию с ножек. Смотрим на особенности схемы, на одну ножку приходится 4 кнопки по горизонтали (строки), аналогично 4 кнопки приходится на одну ножку по вертикали (столбцы). Кнопки в пределах одной строки/столбца объединены общим проводом. Получается матрица 4х4.

Теперь нужно понять принцип работы более детально. Ножки, подключенные к общему проводу строк, настраиваются как вход, те что подключены к столбцам настроены как выход. Когда активен только первый столбец, то мы однозначно знаем, что нажаты могут быть только кнопки с 1 по 4. Далее, переключаемся на второй столбец, сканируем кнопки с 5 по 8 и т.д. Остается лишь читать состояния входов. Диоды нужны, чтобы защитить входы микроконтроллера, если нажато несколько кнопок одновременно. Этой информации должно быть достаточно для того, чтобы написать собственную прошивку. Написать свою прошивку это интересно и главное, ты знаешь как она работает. Поэтому я стараюсь все прошивки писать с нуля. Перейдем к программной части.

Начнем с того, что опрос кнопок, сам по себе, не является целью. Возможно в основном цикле будет программа, использующая клавиатуру, поэтому логично производить опрос независимо от основного цикла, через равные промежутки времени. Поможет нам в этом прерывание таймера по совпадению. Его суть в том, что когда таймер сделает заданное количество тиков основная программа встанет на «паузу» и выполнится код записанный отдельно от основного цикла. Этот код и будет нашим опросом.

Можно использовать Таймер1, но он имеет множество полезных функций и может пригодиться для других вещей, поэтому задействуем таймер2. С какой периодичностью производить опрос? С такой, чтобы не пропустить самые быстрые нажатия. Для надежности, решил взять 20 раз в секунду, т.е. прерывание должно происходить раз в 50мс. Таймер2 не очень годится для длинных отсчетов, потому что максимальное число, которое можно записать в регистр сравнения 0xFF = 255. При минимальной частоте таймера 7813Гц, один тик будет длиться 1/7813=0,000128сек, т.е. максимальный промежуток между прерываниями 0,000128*255=32,6мс. В данном случае не принципиально 50 или 32, поэтому этот результат меня устроил.

За одно прерывание нужно опросить 4 столбца, для этого сделаем цикл for, переменная i хранит номер опрашиваемого столбца.

int i= 0 ; < 4 ; i++ ) { } }

int i=0; // Timer 2 output compare interrupt service routine interrupt void timer2_comp_isr(void) { for(i=0; i<4; i++) { } }

Прерывание работает, цикл крутится, нужно добавить переключения столбцов. Для этого, проще всего, создать массив, в котором будут заранее сконфигурированы состояния PORTD, эти состояния будут по очереди записываться в порт.

int i= 0 ; char portState[ 4 ] = { 0xEF , 0xDF , 0xBF , 0x7F } ; // Timer 2 output compare interrupt service routine interrupt [ TIM2_COMP] void timer2_comp_isr(void ) { for (i= 0 ; i< 4 ; i++ ) { PORTD= portState[ i] ; } }

int i=0; char portState= {0xEF,0xDF,0xBF,0x7F}; // Timer 2 output compare interrupt service routine interrupt void timer2_comp_isr(void) { for(i=0; i<4; i++) { PORTD=portState[i]; } }

С переключениями порта разобрались, теперь нужно в течение каждого переключения, проверить каждую строку — не замкнута ли кнопка, т.е. произвести опрос PIND.0-PIND.3. Для этого сделаем в одном цикле еще один цикл, используем for, переменная j хранит номер строки.

int i= 0 , j= 0 ; char portState[ 4 ] = { 0xEF , 0xDF , 0xBF , 0x7F } ; interrupt [ TIM2_COMP] void timer2_comp_isr(void ) { for (i= 0 ; i< 4 ; i++ ) { PORTD= portState[ i] ; for (j= 0 ; j< 4 ; j++ ) { } } }

int i=0,j=0; char portState= {0xEF,0xDF,0xBF,0x7F}; interrupt void timer2_comp_isr(void) { for(i=0; i<4; i++) { PORTD=portState[i]; for(j=0; j<4; j++) { } } }

Теперь нужно добавить сам опрос входа, но если писать if(PIND.0==0), то получится много условий, код будет не компактным, поэтому, вспоминаем про «логическое и»: если число 1 и число 2 равны единице, то результат будет равен единице, во всех остальных случаях результат 0. Добавляем массив, в котором записаны номера пинов от 0 до 3. Выделяем из всего PIND, при помощи «логического и», нужный пин и проверяем равен ли он нулю.

int i= 0 , j= 0 ; char portState[ 4 ] = { 0xEF , 0xDF , 0xBF , 0x7F } ; char inputState[ 4 ] = { 0x01 , 0x02 , 0x04 , 0x08 } ; interrupt [ TIM2_COMP] void timer2_comp_isr(void ) { for (i= 0 ; i< 4 ; i++ ) { PORTD= portState[ i] ; for (j= 0 ; j< 4 ; j++ ) { if (((PIND& inputState[ j] ) == 0 ) ) { } } } }

int i=0,j=0; char portState= {0xEF,0xDF,0xBF,0x7F}; char inputState={0x01,0x02,0x04,0x08}; interrupt void timer2_comp_isr(void) { for(i=0; i<4; i++) { PORTD=portState[i]; for(j=0; j<4; j++) { if(((PIND&inputState[j])==0)) { } } } }

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

int i= 0 , j= 0 ; char portState[ 4 ] = { 0xEF , 0xDF , 0xBF , 0x7F } ; char inputState[ 4 ] = { 0x01 , 0x02 , 0x04 , 0x08 } ; char symbol[ 4 ] [ 4 ] = { { "1" , "2" , "3" , "4" } , { "5" , "6" , "7" , "8" } , { "9" , "A" , "B" , "C" } , { "D" , "E" , "F" , "D" } } ; // Timer 2 output compare interrupt service routine interrupt [ TIM2_COMP] void timer2_comp_isr(void ) { for (i= 0 ; i< 4 ; i++ ) { PORTD= portState[ i] ; for (j= 0 ; j< 4 ; j++ ) { if (((PIND& inputState[ j] ) == 0 ) ) { lcd_putchar(symbol[ i] [ j] ) ; } } } }

int i=0,j=0; char portState= {0xEF,0xDF,0xBF,0x7F}; char inputState={0x01,0x02,0x04,0x08}; char symbol={{"1","2","3","4"}, {"5","6","7","8"}, {"9","A","B","C"}, {"D","E","F","D"}}; // Timer 2 output compare interrupt service routine interrupt void timer2_comp_isr(void) { for(i=0; i<4; i++) { PORTD=portState[i]; for(j=0; j<4; j++) { if(((PIND&inputState[j])==0)) { lcd_putchar(symbol[i][j]); } } } }

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

#include // Alphanumeric LCD Module functions #asm .equ __lcd_port= 0x18 ; PORTB #endasm #include int i= 0 , j= 0 ; char portState[ 4 ] = { 0xEF , 0xDF , 0xBF , 0x7F } ; char inputState[ 4 ] = { 0x01 , 0x02 , 0x04 , 0x08 } ; char mass2[ 4 ] [ 4 ] = { { "1" , "2" , "3" , "4" } , { "5" , "6" , "7" , "8" } , { "9" , "A" , "B" , "C" } , { "D" , "E" , "F" , "D" } } ; // Timer 2 output compare interrupt service routine interrupt [ TIM2_COMP] void timer2_comp_isr(void ) { for (i= 0 ; i< 4 ; i++ ) { PORTD= portState[ i] ; for (j= 0 ; j< 4 ; j++ ) { if (((PIND& inputState[ j] ) == 0 ) ) { while ((PIND& inputState[ j] ) != inputState[ j] ) { } ; lcd_putchar(mass2[ i] [ j] ) ; } } } } void main(void ) { // Port C initialization // Func6=In Func5=In Func4=In Func3=In Func2=In Func1=In Func0=In // State6=T State5=T State4=T State3=T State2=T State1=P State0=P PORTC= 0x03 ; DDRC= 0x00 ; // Port D initialization // Func7=Out Func6=Out Func5=Out Func4=Out Func3=In Func2=In Func1=In Func0=In // State7=1 State6=1 State5=1 State4=1 State3=P State2=P State1=P State0=P PORTD= 0xFF ; DDRD= 0xF0 ; // Timer/Counter 2 initialization // Clock source: System Clock // Clock value: 7,813 kHz // Mode: CTC top=OCR2 // OC2 output: Disconnected ASSR= 0x00 ; TCCR2= 0x0F ; TCNT2= 0x00 ; OCR2= 0xC3 ; // Timer(s)/Counter(s) Interrupt(s) initialization TIMSK= 0x80 ; lcd_init(8 ) ; // Global enable interrupts #asm("sei") while (1 ) { if (PINC.0== 0 ) { lcd_clear() ; } } ; }

#include // Alphanumeric LCD Module functions #asm .equ __lcd_port=0x18 ;PORTB #endasm #include int i=0,j=0; char portState= {0xEF,0xDF,0xBF,0x7F}; char inputState={0x01,0x02,0x04,0x08}; char mass2={{"1","2","3","4"}, {"5","6","7","8"}, {"9","A","B","C"}, {"D","E","F","D"}}; // Timer 2 output compare interrupt service routine interrupt void timer2_comp_isr(void) { for(i=0; i<4; i++) { PORTD=portState[i]; for(j=0; j<4; j++) { if(((PIND&inputState[j])==0)) { while((PIND&inputState[j])!=inputState[j]){}; lcd_putchar(mass2[i][j]); } } } } void main(void) { // Port C initialization // Func6=In Func5=In Func4=In Func3=In Func2=In Func1=In Func0=In // State6=T State5=T State4=T State3=T State2=T State1=P State0=P PORTC=0x03; DDRC=0x00; // Port D initialization // Func7=Out Func6=Out Func5=Out Func4=Out Func3=In Func2=In Func1=In Func0=In // State7=1 State6=1 State5=1 State4=1 State3=P State2=P State1=P State0=P PORTD=0xFF; DDRD=0xF0; // Timer/Counter 2 initialization // Clock source: System Clock // Clock value: 7,813 kHz // Mode: CTC top=OCR2 // OC2 output: Disconnected ASSR=0x00; TCCR2=0x0F; TCNT2=0x00; OCR2=0xC3; // Timer(s)/Counter(s) Interrupt(s) initialization TIMSK=0x80; lcd_init(8); // Global enable interrupts #asm("sei") while (1) { if(PINC.0==0) { lcd_clear(); } }; }

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

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

Update: Зачем нужны диоды? Допустим некий момент времени, когда опрашиваем первый столбец, соответственно на остальных столбцах логическая единица. Если диодов нет и нажать кнопку 1 и 5, то будет короткое замыкание.

Пора бы рассказать как организовать опрос такой клавы. Напомню, что клава представляет из себя строки, висящие на портах и столбцы, которые сканируются другим портом. Код написан для контроллера ATMega8535 , но благодаря тому, что все там указано в виде макросов его можно быстро портировать под любой другой контроллер класса Mega , а также под большую часть современных Tiny . Хотя в случае с Tiny может быть некоторый затык ввиду неполного набора команд у них. Придется чуток дорабатывать напильником.

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

Теперь коротко о файлах:
keyboard_define.inc — файл конфигурации клавиатуры.
В этом файле хранятся все макроопределения используемые клавиатурой. Здесь мы задаем какие ножки микроконтроллера к какой линии подключены. Одна тонкость — выводы на столбцы (сканирующий порт ) должны быть последовательным набором линий одного порта. То есть, например, ножки 0,1,2,3 или 4,5,6,7 , или 3,4,5,6 . Неважно какого порта, главное чтобы последовательно.
С определением ножек, думаю проблем не возникнет, а вот по поводу параметра KEYMASK я хочу рассказать особо.
Это маска по которой будет выделяться сканируемый порт. В ней должны быть 6 единиц и один 0. Ноль выставляется в крайне правую позицию сканирующего порта.

Пример:
У меня сканирующий порт висит на битах 7,6,5,4 крайне правый бит сканирующего порта это бит 4, следовательно маска равна 0b11101111 — ноль стоит на 4й позиции. Если сканирующие линии будут висеть на ножках 5,4,3,2, то маска уже будет 0b11111011 — ноль на второй позиции. Зачем это все будет объяснено ниже.

Также есть маска активных линий сканирующего порта — SCANMSK . В ней единицы стоят только напротив линий столбцов. У меня столбцы заведены на старшую тетраду порта, поэтому сканирующая маска имеет вид 0b11110000 .

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

Вот так у меня выглядел тестовый код:

Main: SEI ; Разрешаем прерывания.

RCALL KeyScan ; Сканируем клавиатуру
CPI R16,0 ; Если вернулся 0 значит нажатия не было
BREQ Main ; В этом случае переход на начало
RCALL CodeGen ; Если вернулся скан код, то переводим его в
; ASCII код.

MOV R17,R16 ; Загружаем в приемный регистр LCD обработчика
RCALL DATA_WR ; Выводим на дисплей.

RJMP Main ; Зацикливаем все нафиг.

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

Теперь расскажу как работает процедура KeyScan

Def COUNT = R18
KeyScan: LDI COUNT,4 ; Сканим 4 колонки
LDI R16,KEYMASK ; Загружаем маску на скан 0 колонки.

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

KeyLoop: IN R17,COL_PORT ; Берем из порта прежнее значение
ORI R17,SCANMSK ; Выставляем в 1 биты сканируемой части.


Вначале загружаем данные из регистра порта , чтобы иметь на руках первоначальную конфигурацию порта. Также нам нужно выставить все сканирующие биты порта в 1, это делается посредством операции ИЛИ по сканирующей маске. В той части где стояли единицы после операции ИЛИ по маске 11110000 (мое значение SCANMASK ) все биты станут единицами, а где был ноль останутся без изменений.

AND R17,R16 ; Сбрасываем бит сканируемого столбца
OUT COL_PORT,R17 ; Выводим сформированный байт из порта.


Теперь мы на сформированный байт накладываем маску активного столбца . В ней вначале ноль на первой позиции, а все остальные единицы. В результате, другие значения порта не изменятся, а вот в первом столбце возникнет 0. Потом маска сдвинется, а вся операция повторится снова. В результате ноль будет уже в следующем столбце и так далее. Таким образом, мы организуем «бегающий» нолик в сканирующем порте, при неизменности других, посторонних, битов порта. А дальше сформированное число загружается в регистр порта и ножки принимают соответствующие уровни напряжений.

NOP ; Задержка на переключение ноги.
NOP
NOP
NOP

SBIS ROW0_PIN,ROW0 ; Проверяем на какой строке нажата
RJMP bt0

SBIS ROW1_PIN,ROW1
RJMP bt1

SBIS ROW2_PIN,ROW2
RJMP bt2

SBIS ROW3_PIN,ROW3
RJMP bt3


Серия NOP нужна для того, чтобы перед проверкой дать ножке время на то, чтобы занять нужный уровень. Дело в том, что реальная цепь имеет некоторое значение емкости и индуктивности, которое делает невозможным мгновенное изменение уровня , небольшая задержка все же есть. А на скорости в 8Мгц и выше процессор щелкает команды с такой скоростью, что напряжение на ноге еще не спало, а мы уже проверяем состояние вывода. Вот я и влепил несколько пустых операций. На 8Мгц все работает отлично. На большую частоту, наверное, надо будет поставить еще штук пять шесть NOP или влепить простенький цикл. Впрочем, тут надо поглядеть на то, что по байтам будет экономичней.
После циклов идет четыре проверки на строки. И переход на соответствующую обработку события.

ROL R16 ; Сдвигаем маску сканирования
DEC COUNT ; Уменьшаем счетчик столбцов
BRNE KeyLoop ; Если еще не все перебрали делаем еще одну итерацию

CLR R16 ; Если нажатий не было возвращаем 0
RET
.undef COUNT

Вот тут происходит сдвиг маски влево командой циклического сдвига ROL . После чего мы уменьшаем счетчик итераций (изначально равен четырем, так как у нас четыре столбца). Если нажатий не было, то по окончании всех четырех итераций мы вываливаемся из цикла, обнуляем регистр R16 и возвращаемся.


bt0: ANDI R16,SCANMSK ; Формируем скан код
ORI R16,0x01 ; Возвращаем его в регистре 16
RET

А вот один из возможных концов при нажатии. Тут формируется скан код который вернется в регистре R16. Я решил не заморачиваться, а как всегда зажать десяток байт и сделать как можно быстрей и короче. Итак, что мы имеем по приходу в этот кусок кода. А имеем мы один из вариантов сканирующего порта (1110,1101,1011,0111 ), а также знаем номер строки по которой мы попали сюда. Конкретно в этот кусок можно попасть только из первой строки по команде RJMP bt0.
Так давай сделаем скан код из сканирующей комбинации и номера строки! Сказано — сделано! Сначала нам надо выделить из значения порта сканирующую комбинацию — она у нас хранится в регистре R16 , поэтому выковыривать из порта ее нет нужды. Продавливаем операцией И значение R16 через SCANMASK и все что было под единичками прошло без изменений, а где были нули — занулилось. Опа, и у нас выведен сканирующий кусок — старший полубайт. Теперь вклеим туда номер строки — операцией ИЛИ . Раз, и получили конструкцию вида [скан][строка]
Вот ее и оставляем в регистре R16 , а сами выходим прочь! Также и с остальными строками. Погляди в исходнике, я их не буду тут дублировать.

Декодирование скан кода.
Отлично, скан код есть, но что с ним делать? Его же никуда не приткнуть. Мы то знаем, что вот эта шняга вида 01110001 это код единички, а какой нибудь LCD экран или стандартная терминалка скорчит нам жуткую кракозябру и скажет, нам все что она думает о нашей системе обозначений — ей видите ли ASCII подавай. Ладно, будет ей ASCII.

Как быть? Прогнать всю конструкцию по CASE где на каждый скан код присвоить по ASCII коду меня давит жаба — это же сколько надо проверок сделать! Это же сколько байт уйдет на всю эту тряхомудию? А память у нас не резиновая, жалкие восемь килобайт, да по два байта на команду, это в лучшем случае. Я мог все это сделать прям в обработчике клавиатуры. НЕТ!!! В ТОПКУ!!! Мы пойдем своим путем.
Ок, а что у нас есть в запасе? Метод таблиц перехода не катит, по причине жуткой неупорядоченности скан кодов. Почесал я тыковку, пошарился по квартире… и тут меня осенило. Конечно же!!! Брутфорс!!!

Брутфорсим скан код.
Итак, у нас есть жутко несваримый скан код, а также стройная таблица ASCII символов. Как скрестить ужа с ежом? Да все просто! Разместим в памяти таблицу символов в связке [скан код]: , а потом каждый нужный скан код будем прогонять через эту таблицу и при совпадении подставлять на выходе нужный ASCII из связки. Классический пример программизма — потеряли во времени, зато выиграли в памяти.

Вот так это выглядит:

CodeGen:LDI ZH,High(Code_Table*2) ; Загрузил адрес кодовой таблицы
LDI ZL,Low(Code_Table*2) ; Старший и младший байты

Тут мы загрузили в индексный регистр адрес нашей таблицы. Умножение на два для того, чтобы адрес был в байтах, т.к. в среде компилятора пространство кода адресуется в словах.

Brute: LPM R17,Z+ ; Взял из таблицы первый символ — скан код

CPI R17,0xFF ; Если конец таблицы
BREQ CG_Exit ; То выходим

CPI R16,0 ; Если ноль,
BREQ CG_Exit ; то выходим

CP R16,R17 ; Сравнил его со скан кодом клавиши.
BREQ Equal ; Если равен, то идем подставлять ascii код

Загружаем из таблицы первый скан код и нычим его в регистр R17 , попутно увеличиваем адрес в регистре Z (выбор следующей ячейки таблицы) и первым делом сравниваем его с FF — это код конца таблицы. Если таблица закончилась, то выходим отсюда. Если мы не всю таблицу перебрали, то начинаем сравнивать входное значение (в регистре R16 ) вначале с нулем (нет нажатия), если ноль тоже выходим. И со скан кодом из таблицы. Если скан таблицы совпадает со сканом на входе, то переходим на Equal .

LPM R17,Z+ ; Увеличиваем Z на 1
RJMP Brute ; Повтор цикла

А в случае если ничо не обнаружено, то мы повторно вызываем команду LPM R17,Z+ лишь для того, чтобы она увеличила Z на единичку — нам же надо перешагнуть через ASCII код и взять следующий скан код из таблицы. Просто INC Z не прокатит, так как Z у нас двубайтный . ZL и ZH . В некторых случаях достаточно INC ZL , но это в случае когда мы точно уверены в том, что адрес находится недалеко от начала и переполнения младшего байта не произойдет (иначе мы вместо адреса 00000001:00000000 получим просто 00000000:0000000, что в корне неверно), а команда LPM все сделает за нас, так что тут мы сэкономили еще пару байт. Потом мы вернемся в начало цикла, а там будет опять LPM которая загрузит уже следующий скан код.

Equal: LPM R16,Z ; Загружаем из памяти ASCII код.
RET ; Возвращаемся

Если же было совпадение, то в результате LPM Z+ у нас Z указывает на следующую ячейку — с ASCII кодом. Ее мы и загружаем в регистр R16 и выходим наружу.

CG_Exit: CLR R16 ; Сбрасываем 0 = возвращаем 0
RET ; Возвращаемся

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



; STATIC DATA
;========================================
Code_Table: .db 0x71,0x31 ;1
.db 0xB1,0x32 ;2
.db 0xD1,0x33 ;3
.db 0x72,0x34 ;4
.db 0xB2,0x35 ;5
.db 0xD2,0x36 ;6
.db 0x73,0x37 ;7
.db 0xB3,0x38 ;8
.db 0xD3,0x39 ;9
.db 0x74,0x30 ;0
.db 0xFF,0 ;END

Тут просто табличка статичных данных, на границе памяти. Как видишь данные сгруппированы по два байта — сканкод/ASCII

Вот посредством таких извратов вся программа, с обработкой клавиатуры, декодированием скан кода, чтением/записью в LCD индикатор и обнулением оперативки (нужно для того, чтобы точно быть увереным, что память равна нулю) заняло всего 354 байта . Кто сможет меньше?