Буфет в нашем офисе жил на честном слове: банка для денег и листок, на котором продавец ставил палочки. К концу месяца листок расходился с банкой на пару сотен, и я собрал терминал: мембранка на стенке, у каждой позиции свой номер, продавец жмёт номер — товар уезжает в чек. Клавиатура из прошлого кластера здесь идеальный ввод: десять цифр, человеку не надо ничего объяснять, номер товара — это одна клавиша.
В этом уроке цены лежат в массиве прямо в скетче — таблица товаров в RAM, номер превращается в цену одним обращением по индексу. Каждое нажатие прибавляет позицию: счётчик товара растёт, итог ползёт вверх. Решётка печатает чек в Serial — по строке на позицию, с количеством и стоимостью, в конце итог — и зажигает светодиод «оплачено» на D12. Звёздочка объявляет новый чек: счётчики обнуляются, светодиод гаснет.
Почему таблица в RAM и это честно: цены прошиты в скетче и переживают питание, а чек — вещь сеанса, его жизнь равна жизни продажи. Выключили питание — чек исчез вместе с переменными, и это правильно: следующий покупатель должен получить чистый лист. Настройки, которые обязаны выживать — калибровки, пороги, коды, — живут в EEPROM по другим правилам, разобранным в уроке про три паттерна памяти проектов.
Ассортимент я вписал буфетный: кофе 45, чай 30, вода 25, сэндвич 120, шоколад 65, сок 55 — шесть позиций, у каждой номер на мембранке. Цены живут в массиве наверху скетча одной строкой: подорожал кофе — правка одной цифры и перезаливка. Названия на экран не выводятся принципиально: у продавца перед носом бирка с номером и ценой, а терминал считает — разделение труда, при котором скетч остаётся компактным.
Таблица товаров: массив вместо шестнадцати if-ов
Товары — это два параллельных массива: tseny[7] с ценами и kolichestvo[7] со счётчиками. Нулевые ячейки пустуют сознательно: номера товаров идут с единицы, и номер совпадает с индексом — не нужен ни минус единица в формуле, ни лишний шанс промахнуться. Цена товара 4 — это tseny[4], и никакой цепочки if (n == 1) cena = 45; else if... — массив как таблица закрывает вопрос одной строкой.
Добавление позиции — три действия: проверить границу, увеличить счётчик, прибавить цену к итогу. Границы здесь не украшение: клавиши 0, 7, 8, 9 и буквы товара не имеют, и без проверки скетч прочитает ячейку за краем массива — мусор вместо цены. Счётчик капнут на 99: строка чека с трёхзначным количеством в 16 символов не помещается, да и стоимости в int хватало бы впритык.
Итог держим в long: шесть позиций по 99 штук дорогого товара дают сумму за пределами int. Тип тот же, что защищал калькулятор из прошлого урока, и по той же причине: int на Uno — до 32 767.
Массив на семь ячеек выглядит расточительно, но это семь из двух килобайт SRAM Uno — три десятых процента. Попытка ужать его до шести и вычитать единицу при каждом обращении экономит байт, а платит за неё лишняя операция в горячем цикле и лишний шанс промахнуться. Такой же таблицей ложится и расширение: товар седьмой и восьмой — это две ячейки в каждом массиве и правка одной границы в проверке, остальной скетч не меняется.
const int STROKI[4] = {2, 3, 4, 5};const int STOLBY[4] = {6, 7, 8, 9};const int LED_OPLATA = 12;char klavishi[16] = {'1','2','3','A','4','5','6','B','7','8','9','C','*','0','#','D'}; int tseny[7] = {0, 45, 30, 25, 120, 65, 55}; // [0] пустой: номера товаров с 1int kolichestvo[7] = {0, 0, 0, 0, 0, 0, 0};long itog = 0;int derzhim = -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); } pinMode(LED_OPLATA, OUTPUT); Serial.begin(9600); Serial.println("Terminal: 1-6 tovar, # chek, * novyi");} 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 novyiChek() { for (int n = 0; n < 7; n++) { kolichestvo[n] = 0; } itog = 0; digitalWrite(LED_OPLATA, LOW); Serial.println("--- Novyi chek ---");} void pechatCheka() { Serial.println("========== CHEK =========="); for (int n = 1; n <= 6; n++) { if (kolichestvo[n] > 0) { Serial.print("Tovar "); Serial.print(n); Serial.print(" x "); Serial.print(kolichestvo[n]); Serial.print(" = "); Serial.print(tseny[n] * kolichestvo[n]); Serial.println(" rub"); } } Serial.print("ITOG: "); Serial.print(itog); Serial.println(" rub"); Serial.println("Oplata: svetodiod gORIT");} void loop() { int i = opros(); if (i >= 0 && i != derzhim) { char k = klavishi[i]; if (k == '*') { novyiChek(); } else if (k == '#') { pechatCheka(); digitalWrite(LED_OPLATA, HIGH); } else if (k >= '1' && k <= '6') { int n = k - '0'; if (kolichestvo[n] < 99) { kolichestvo[n] = kolichestvo[n] + 1; itog = itog + tseny[n]; Serial.print("+ Tovar "); Serial.print(n); Serial.print(": "); Serial.print(tseny[n]); Serial.print(" rub, itog "); Serial.println(itog); } else { Serial.println("Bolshe 99 shtuk v stroku cheka ne pomestitsya"); } } else { Serial.println("Takogo tovara net: 1-6"); } delay(30); } derzhim = i; delay(10);}Чек печатается циклом: for по номерам от 1 до 6, и в печать попадают только позиции с ненулевым счётчиком — строка вида Tovar 3 x 2 = 50 rub. Читается она как таблица умножения: номер, штук, стоимость. Итог печатается последним, и в этот же момент зажигается светодиод на D12 — для продавца это и есть сигнал «чек закрыт, деньги приняты».
Новый чек — обратная операция: цикл обнуляет все семь счётчиков, итог падает в ноль, светодиод гаснет. Заметьте, что решётка ничего не обнуляет: по решётке чек печатается, а содержимое терминала остаётся на месте — чек можно перепечатать, и цифры сойдутся. Свойство мелкое, а пригодится, когда бумажный листок у продавца замнётся.
Удержание клавиши отсекается знакомой памятью derzhim из сканера матрицы: пока палец на клавише, позиция добавляется один раз. Без неё пятёрка превратилась бы в очередь из пятёрок — я поймал это на первом запуске, когда положил палец на клавишу и стал смотреть, как счётчик товара улетел за двадцать.
Перед итогом стоит печатать число занятых позиций — ещё один цикл по массиву, только для подсчёта. Строка-формальность, а ловит самый частый инцидент прилавка: продавец начал чек для одного покупателя и по ошибке дописал позиции следующего. Если число строк не сходится с числом покупок, чек стирается звёздочкой целиком и набирается заново — сверка глазами занимает секунды.
| Нажатие | Что делает терминал | Что видите |
|---|---|---|
| 3, 3, 1 | две штуки товара 3 и одна товара 1 | в логе + Tovar 3 и растущий itog |
| # | печатает чек, зажигает D12 | строки позиций, ITOG, светодиод горит |
| # ещё раз | перепечатывает тот же чек | цифры сходятся, ничего не задвоилось |
| 0, 7 или A | сообщает: товара нет | Takogo tovara net в логе |
| * | новый чек | счётчики и итог в ноль, светодиод погас |
Чек в Serial: пять позиций против шестнадцати символов
Чек едет в Serial, и это выбор по размеру: строка Tovar 3 x 2 = 50 rub занимает 21 символ — на 16-колоночном экране она не помещается целиком даже по частям без акробатики. Монитор порта шириной не ограничен, для сверки с банкой его хватает за глаза. На живой точке на экране обычно живут две строки — количество позиций и итог; тот же скетч легко печатает их на LCD, раскладывая по строкам, как в уроке про адресацию строк 20x4.
Формат строк — маленький протокол между скетчем и человеком, тот же подход, что в командном парсере по Serial: фиксированный порядок полей, понятные разделители, итог в самом конце. Как только захотите читать чек другой программой, а не глазами, — формат уже готов и парсится по тем же правилам.
Про скорость. Штатные 9600 бод — это около 960 символов в секунду: чек из восьми строк на 150 символов печатается за шестую долю секунды, продавец задержки не замечает. Но держите в голове цену: каждая строка — это вызовы Serial.print, а числа разворачиваются в символы посимвольно. Если когда-нибудь заведёте печать в каждом проходе loop, она съест отзывчивость клавиатуры — чек печатается по событию, один раз на решётку.
Куда растёт терминал. Первый шаг — выручка за день: сумма закрытых чеков копится в переменной, а на ночь пишется в EEPROM — пара байт на число, износ ячеек при одной записи в сутки не страшен. Второй — журнал чеков по кольцу: кольцевой журнал в EEPROM хранит последние N записей, старые затираются сами.
Третий шаг — экран вместо листка: меню из шести позиций с ценами по две строки за раз, листание — уже знакомые вам клавиши, схема похожа на меню на двух кнопках. Четвёртый — отмена ошибочной позиции: клавиша B минусует последнюю добавленную, для этого в памяти живёт номер последнего товара. Скелет не меняется: массив цен, счётчики, итог — меняется обвязка.
Отдельная тема — сдача: покупатель даёт пятисотку, терминал печатает разность. Для этого нужен ввод суммы от покупателя — те же цифры с решёткой-подтверждением, тот же буфер, что у калькулятора из прошлого урока. Полноценная касса вырастает из этого терминала за один вечер, потому что половина её уже написана.
- 1. Зачем массив цен объявлен на семь ячеек, если товаров шесть?
- 2. Продавец нажал 2, потом 0, потом 2 и решётку. Сколько строк позиций будет в чеке?
- 3. Что происходит при нажатии клавиши 9?
- 4. Почему светодиод зажигается по решётке, а не при первом нажатии цифры?
- 5. Что делает звёздочка?
- Таблица товаров — параллельные массивы: tseny[7] с ценами и kolichestvo[7] со счётчиками, номер совпадает с индексом.
- Каждое нажатие: проверка границ, счётчик плюс один, цена плюс к итогу в long; 0 и 7–9 отклоняются.
- Чек печатается циклом по ненулевым позициям: Tovar n x k = stoimost rub, в конце ITOG.
- Светодиод D12 — статус закрытой продажи: зажигается решёткой, гаснет звёздочкой.
- RAM-чек умирает с питанием — так и задумано; выживающие настройки живут в EEPROM по другим правилам.
Частые вопросы по теме урока
- Зачем массив цен объявлен на семь ячеек, если товаров шесть?
- Номер совпадает с индексом: цена товара 4 — это tseny[4]. Без пустой нулевой ячейки пришлось бы вычитать единицу в каждом обращении. Номера товаров идут с единицы, и нулевая ячейка делает номер совпадающим с индексом
- Продавец нажал 2, потом 0, потом 2 и решётку. Сколько строк позиций будет в чеке?
- Товара 0 не существует, терминал его отклонил; обе штуки товара 2 соберутся в одну строку с количеством 2. В чек идут только ненулевые позиции. Одна: Tovar 2 x 2
- Что происходит при нажатии клавиши 9?
- Проверка границ: номера товаров 1–6, остальное отклоняется. Без проверки скетч читал бы ячейки за краем массива — мусор вместо цены. Сообщение «товара нет», в чек ничего не попадает
- Почему светодиод зажигается по решётке, а не при первом нажатии цифры?
- LED «оплачено» показывает завершённую продажу: чек напечатан, итог зафиксирован. До решётки терминал в наборе. Светодиод — статус закрытой продажи: до решётки чек не закрыт
- Что делает звёздочка?
- Новая продажа начинается с чистого листа: семь счётчиков в ноль, итог в ноль, светодиод погашен. Перепечатать чек — решётка. Обнуляет счётчики и итог, гасит светодиод — новый чек