Калькулятор на клавиатуре и экране — первый проект, где сборка уже не главное: и матрица, и дисплей у вас заведены, а вся работа переезжает в логику. Я собрал такой за вечер по мотивам Project Hub, где это один из самых массовых проектов на мембранке, — и застрял на неожиданном месте: клавиши печатались по одной, а число собрать не получалось вовсе. Символ «1» и число 1 — вещи разного сорта, и между ними лежит одна строчка арифметики, которую готовые скетчи из сети молча проглатывают, не объясняя.
В этом уроке цифры с матрицы склеиваются в число по формуле n = n*10 + цифра, клавиши A, B, C, D станут плюс, минус, умножить и разделить, решётка — равно, звёздочка — сброс. Выражение и итог живут на LCD 16x2: набрали 12, нажали A, набрали 34, нажали решётку — на нижней строке появится 12+34=46. Деление на ноль не роняет программу, а выводит сообщение, как на настоящем калькуляторе.
Дисплей берём I2C и не случайно. Клавиатура уже заняла восемь цифровых пинов D2–D9, параллельному HD44780 нужно ещё шесть линий, а из свободных остаются только D10–D13 — впритык и без запаса. Шина I2C сидит на A4 и A5 и к цифровым пинам не прикасается: подключение экрана по двум проводам мы разбирали в уроке про LiquidCrystal I2C, здесь подключим той же библиотекой. Сканер матрицы берём готовый из урока про клавиатуру 4x4 — он станет функцией opros() на десять строк.
В описаниях клавиатурных калькуляторов на Project Hub отдельно подчёркнуты две вещи, и обе про интерфейс, а не про арифметику: экран повторяет набор по каждой клавише — «как на настоящем калькуляторе» — и недопустимое выражение вроде деления на ноль не считается, а показывается сообщением. Первое — не украшение: без эха пользователь не видит, сколько цифр уже вошло, и теряет счёт на третьей-четвёртой. Второе мы повторим один в один и разберём, за какой строкой кода оно стоит.
Число из клавиш: n = n*10 + цифра
Символ цифры и её значение связаны через код. В таблице символов '0' — это код 48, '7' — код 55, и все десять цифр идут подряд, поэтому вычитание '7' - '0' даёт ровно 7. Дальше включается позиционная запись: нажали 1 — в переменной единица; нажали 2 — старую единицу умножили на десять и прибавили двойку, получилось 12; нажали 3 — стало 123. Одна строка chislo = chislo * 10 + (k - '0') обслуживает числа любой длины, и тот же приём крутится в типах из урока про переменные и типы: всё, что нажимается пальцем, приезжает в скетч символом и превращается в число на месте.
Потолок ввода ставят сознательно. На Uno int — 16 бит, максимум 32 767: произведение 400 и 90 равно 36 000 и уже вылезает за границу, а 9999x9999 улетает за пределы int в три тысячи раз. Поэтому числа в скетче объявлены long — 32 бита, до 2 147 483 647: даже 99 980 001 помещается с запасом. Набор ограничен четырьмя цифрами: клавиша сверх четвёртой игнорируется — и экран не переполняется, и длинный неряшливый ввод не успевает навредить.
После каждой клавиши первая строка перерисовывается: затирается шестнадцатью пробелами и печатается заново — число, знак операции, второе число. Частота у нажатий человеческая, две-три в секунду, так что перерисовка по событию экран не раздражает; мерцание начинается, когда clear() и печать крутятся в каждом проходе loop, — это отдельная болячка с отдельным уроком про мерцание LCD.
Мелкая, но коварная деталь — затирание старых цифр. Напечатали 123, стёрли звёздочкой, набрали 4: без затирания пробелами на месте трёх старых цифр остались бы хвосты. Вторая деталь — флаг nazhatoB: он отличает «второе число ещё не начали набирать» от «набрали ноль», и без него после знака операции на экране висел бы ноль, которого пользователь не вводил. Обе правки — по одной строке, но именно на таких строках готовые скетчи из сети и сыпятся.
#include <Wire.h>#include <LiquidCrystal_I2C.h> LiquidCrystal_I2C lcd(0x27, 16, 2); // экран молчит — попробуйте 0x3F const int STROKI[4] = {2, 3, 4, 5};const int STOLBY[4] = {6, 7, 8, 9};char klavishi[16] = {'1','2','3','A','4','5','6','B','7','8','9','C','*','0','#','D'}; long chisloA = 0;long chisloB = 0;char operaciya = ' ';bool nazhatoB = false;int etap = 0; // 0 — набор первого числа, 1 — набор второго, 2 — результатint derzhim = -1; // индекс зажатой клавиши, -1 — клавиш нет void setup() { for (int r = 0; r < 4; r++) { pinMode(STROKI[r], OUTPUT); digitalWrite(STROKI[r], HIGH); // неактивная строка HIGH: активная — LOW } for (int c = 0; c < 4; c++) { pinMode(STOLBY[c], INPUT_PULLUP); } lcd.init(); lcd.backlight(); lcd.setCursor(0, 0); lcd.print("Kalkulyator"); Serial.begin(9600);} int opros() { for (int r = 0; r < 4; r++) { digitalWrite(STROKI[r], LOW); for (int c = 0; c < 4; c++) { if (digitalRead(STOLBY[c]) == LOW) { digitalWrite(STROKI[r], HIGH); return r * 4 + c; } } digitalWrite(STROKI[r], HIGH); } return -1;} void znakNaEkran() { if (operaciya == 'A') lcd.print("+"); if (operaciya == 'B') lcd.print("-"); if (operaciya == 'C') lcd.print("x"); if (operaciya == 'D') lcd.print("/");} void nabornayaStroka() { lcd.setCursor(0, 0); lcd.print(" "); // затереть 16 позиций lcd.setCursor(0, 0); lcd.print(chisloA); if (operaciya != ' ') znakNaEkran(); if (etap == 1 && nazhatoB) lcd.print(chisloB);} void schitat() { long r = 0; lcd.setCursor(0, 1); lcd.print(" "); lcd.setCursor(0, 1); if (operaciya == 'D' && chisloB == 0) { lcd.print("Delenie na 0!"); Serial.println("Delenie na 0"); etap = 2; return; } if (operaciya == 'A') r = chisloA + chisloB; if (operaciya == 'B') r = chisloA - chisloB; if (operaciya == 'C') r = chisloA * chisloB; if (operaciya == 'D') { r = chisloA / chisloB; lcd.print(chisloA); lcd.print("/"); lcd.print(chisloB); lcd.print("="); lcd.print(r); lcd.print(" ost "); lcd.print(chisloA % chisloB); } else { lcd.print(chisloA); znakNaEkran(); lcd.print(chisloB); lcd.print("="); lcd.print(r); } Serial.print(chisloA); Serial.print(" "); Serial.print(operaciya); Serial.print(" "); Serial.print(chisloB); Serial.print(" = "); Serial.println(r); etap = 2;} void sbros() { chisloA = 0; chisloB = 0; operaciya = ' '; nazhatoB = false; etap = 0; lcd.setCursor(0, 1); lcd.print(" "); nabornayaStroka();} void loop() { int i = opros(); if (i >= 0 && i != derzhim) { char k = klavishi[i]; if (k == '*') { sbros(); } else if (k == '#') { if (etap == 1) schitat(); } else if (k >= '0' && k <= '9') { if (etap == 2) sbros(); // после результата начинаем заново int znachenie = k - '0'; if (etap == 0 && chisloA <= 999) chisloA = chisloA * 10 + znachenie; if (etap == 1 && chisloB <= 999) { chisloB = chisloB * 10 + znachenie; nazhatoB = true; } nabornayaStroka(); } else if (k == 'A' || k == 'B' || k == 'C' || k == 'D') { if (etap == 0 || etap == 1) { operaciya = k; chisloB = 0; nazhatoB = false; etap = 1; nabornayaStroka(); } } delay(30); } derzhim = i; delay(10);}Автомат на три этапа и деление на ноль
Калькулятор — конечный автомат, и это не выпендрёж: одно и то же нажатие решётки должно означать разное — сверять выражение на этапе набора второго числа и ничего не делать на этапе результата. Этапов три: набор первого числа, набор второго, результат; переменная etap их различает, а схема в целом — та же, что в уроке про конечный автомат: светофор менял фазы по таймеру, калькулятор — по клавишам. Память derzhim отсекает удержание: клавиша считается один раз, на фронте.
Деление на ноль ловится до деления: если второе число ноль, на нижней строке появляется Delenie na 0, и операция не выполняется вовсе. Проекты на Project Hub ловят этот случай так же заранее — недопустимое выражение проще не считать, чем объяснять пользователю, почему экран показывает мусор. Живое деление у нас целочисленное: 7 на 2 даёт 3, а остаток печатает оператор % — на экране строка 7/2=3 ost 1. Дроби требуют точки и другого формата вывода — там же, где мы печатали дробные числа на LCD.
Знаков умножения и деления в знакогенераторе HD44780 нет — ни ×, ни ÷, поэтому на экран печатаем x и дробь. Минус у отрицательных результатов забот не добавляет: печать числа print делает сама, разность 3 - 10 покажет как -7 без дополнительных телодвижений.
| Ввод на клавиатуре | Что на LCD | Что происходит внутри |
|---|---|---|
| 1 2 A 3 4 # | 12+34=46 | числа собраны по n*10+цифра, сложение на long |
| 7 D 2 # | 7/2=3 ost 1 | целочисленное деление, остаток даёт % |
| 9 D 0 # | Delenie na 0! | ноль ловится до деления, операция не выполняется |
| 4 0 0 C 9 0 # | 400x90=36000 | int был бы переполнен, long держит до 2,1 млрд |
| * в любой момент | чистый ввод | полный сброс к набору первого числа |
I2C-дисплей: два провода вместо шести
Схема у дисплея минимальная: SDA к A4, SCL к A5, питание 5 В и общий провод; подсветка берёт свои 20–30 мА от шины 5V, а не с пина. Единственная известная категория грабель — адрес: у части модулей 0x27, у части 0x3F, и экран с неверным адресом просто молчит. Адрес — первый аргумент конструктора, смена — дело одной цифры в скетче.
Почему не параллельный LCD, который дешевле: шесть линий против двух, а свободных цифровых после клавиатуры всего четыре. Даже если бы хватило, схема осталась бы без единого запаса под светодиод или сервопривод — а пин 8, кстати, уже занят столбцом клавиатуры, на нём даже зуммер не поселить. I2C оставляет свободными все цифровые пины, кроме восьми клавиатурных, и это главный аргумент в связке «матрица плюс экран».
По таймингам у схемы запас в тысячи раз. Скан матрицы на 16 МГц занимает микросекунды, шина I2C в штатном режиме гоняет байты на 100 кГц — обновление двух строк экрана стоит доли миллисекунды, а между нажатиями проходят сотни миллисекунд. Ток через замкнутую клавишу — доли миллиампера, греться нечему, и единственное слабое место остаётся прежним: шлейф в разъёме, который отгибается от частых перетыканий.
Что дальше. Первый кандидат — дроби: десятичная точка на свободной клавише, счётчик знаков после неё и печать с нужным числом разрядов. Второй — цепочки вида 12+34-7: их либо пересчитывают на каждой операции, либо хранят в стеке, и половина вопросов про клавиатурные калькуляторы на форумах именно про это — два числа и знак хранить легко, цепочка уже нет.
И обратите внимание, сколько из скетча переедет в следующий урок почти без изменений: сканер, сбор числа из клавиш, фронт derzhim, строка экрана с затиранием. Калькулятор — это упражнение на арифметику; та же клавиатура с тем же экраном станет кассой, где числа превращаются в позиции чека. Инструменты одни — меняется только смысл нажатий.
- 1. Нажата клавиша '7'. Как сканер превратит её в число 7 для арифметики?
- 2. Что появится на LCD после ввода 9 D 0 и решётки?
- 3. Почему произведение 400 и 90 требует long, а не int на Uno?
- 4. Сколько цифровых пинов Uno останется свободным после клавиатуры и I2C-экрана?
- 5. Что делает звёздочка в калькуляторе?
- Символ цифры превращается в значение разностью кодов: n = n*10 + (k - '0') собирает число любой длины.
- int на Uno — 16 бит до 32 767; калькулятору нужен long (32 бита) и потолок набора в четыре цифры.
- Деление на ноль ловится до операции: сообщение на экране вместо мусора; остаток целочисленного деления печатает %.
- I2C-экран занимает A4/A5: после клавиатуры D2–D9 параллельному LCD шести линий уже не хватает.
- Нажатия обрабатывает автомат с этапами и памятью derzhim — одно нажатие, одно событие.
Частые вопросы по теме урока
- Нажата клавиша '7'. Как сканер превратит её в число 7 для арифметики?
- Цифры идут в таблице кодов подряд: '0' — это 48, '7' — 55, разность даёт значение. Формула n = n*10 + (k - '0') собирает многозначное число. Вычитанием кода '0' из кода '7' — разность кодов равна 7
- Что появится на LCD после ввода 9 D 0 и решётки?
- Случай ловится до деления: chisloB равно нулю — печатаем сообщение и не считаем. Так же поступают калькуляторы на Project Hub: недопустимое выражение не выполняют. Сообщение о делении на ноль, сама операция не выполнится
- Почему произведение 400 и 90 требует long, а не int на Uno?
- Потолок int на Uno — 32 767; 400x90 = 36 000 вылетает за границу и даёт мусор. long — 32 бита, до 2 147 483 647. int на Uno 16-битный с границей 32 767, а 36 000 уже за ней
- Сколько цифровых пинов Uno останется свободным после клавиатуры и I2C-экрана?
- Клавиатура забирает D2–D9, I2C-экран живёт на аналоговой шине A4/A5. Свободны D10–D13 плюс D0/D1, зарезервированные под UART. Шесть: экран сидит на A4/A5 и цифровые пины не трогает
- Что делает звёздочка в калькуляторе?
- Сброс чистит оба числа, операцию и этап: новое выражение набирается с чистого листа. Умножение — клавиша C. Сбрасывает всё к набору первого числа