MicroPython: Развиваем навыки — #11 Клавиатуры

MicroPython Skill Builders #11 Клавиатуры

Добро пожаловать в одиннадцатый выпуск серии «Развиваем навыки MicroPython» — цикла уроков, цель которого — улучшить ваши навыки программирования на MicroPython, попутно знакомя с новыми компонентами и приёмами написания кода на базе Raspberry Pi Pico!

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

Матричные клавиатуры выпускаются в конфигурациях 3x4 и 4x4. Некоторые модели можно подключать к Pico без пайки. Мембранные клавиатуры недорогие и имеют клейкий слой под защитной бумагой, что позволяет легко крепить их к плоской поверхности.

Что вам понадобится

Предполагается, что вы уже установили Thonny на компьютер и настроили Raspberry Pi Pico с последней прошивкой MicroPython (UF2).

Вам понадобится:

  • Raspberry Pi Pico (или Pico 2) с контактными выводами

  • Кабель Micro-USB (для питания и программирования Pico)

  • Матричная клавиатура:

    • Мембранная версия 3x4 — пайка не требуется

    • Версия 4x4 — требует пайки контактов, но позволяет вводить числа с плавающей точкой

  • Провода-перемычки (папа/папа и папа/мама, желательно разных цветов)

Дополнительно:

  • OLED-дисплей SSD1315 (0.96», 128x64)

  • Беспаечная макетная плата

Целые числа и числа с плавающей точкой

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

В MicroPython существует два типа чисел: целые числа (Integers) и числа с плавающей точкой (Floats).

Целые числа

Могут быть положительными или отрицательными и не содержат десятичной точки (например: -123, 3458, -999).

Числа с плавающей точкой

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

Если они представляют очень большие или очень маленькие значения, они могут быть записаны в научной нотации с символом „e“ (например: -1.23, 0.000000789, 12345.678, 5.23e3, -2.75e-5).

Число после „e“ обозначает степень 10 и указывает, на сколько позиций нужно сдвинуть десятичную точку — вправо для положительных, влево для отрицательных значений.

1.23 e 5 представляет число 123000.0, а -1.23 e -5 представляет -0.0000123.

Для ввода таких чисел мы будем использовать следующие клавиши:

  • „A“ — клавиша «минус»

  • „B“ — клавиша „e“

  • „D“ — клавиша десятичной точки

  • „*“ — стирание/Backspace

  • „#“ — клавиша ENTER/ВВОД, сигнализирующая об окончании ввода числа

Раскладка матричной клавиатуры

Внутренние соединения матрицы

Все эти устройства ввода имеют одинаковую внутреннюю схему подключения.

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

Обратите внимание на маленькие чёрные точки на соединениях R0–R3. Клавиатура 4x4 имеет 4 столбца: C0–C3.

Мембранная версия 3x4

Распиновка мембранной матричной клавиатуры 3x4

Кнопочная версия 4x4

Распиновка кнопочной матричной клавиатуры 4x4

Подключение клавиатуры к Pico

Во всех случаях соединения выполняются следующим образом:

Контакт клавиатуры

GPIO Raspberry Pi Pico

R0

13

R1

12

R2

11

R3

10

C0

9

C1

8

C2

7

C3 (если присутствует)

6

Распиновку для альтернативных клавиатур можно найти в документации на страницах соответствующих товаров — существует несколько различных конфигураций.

Мы выбрали именно эти контакты, чтобы не конфликтовать с SPI GPIO контактами, используемыми для подключения OLED-дисплея SSD1315 (рассмотренного в предыдущих выпусках). Благодаря этому оба устройства можно использовать одновременно (если у вас нет дисплея — просто закомментируйте строки с OLED и смотрите результаты в окне Shell).

Как мы считываем нажатия клавиш

Мы последовательно устанавливаем каждую строку в состояние high, переходя от R0 к R3.

Пока строка установлена в high (3.3В), мы проверяем каждый столбец — не перешёл ли он тоже в high. В большинстве случаев, когда ни одна кнопка не нажата, мы считываем ~0В — состояние low.

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

Код

Этот блок импортирует необходимые библиотеки и настраивает OLED-дисплей:

# Using a Matrix Keypad
# Waveshare 0.96inch OLED Module via SPI (SSD1315)
# Matrix Keypad 4x4 or 4x3
# Tony Goodhew 21th Aug 2023

from machine import Pin, SPI
import time

# ============ Set up the display ==========
# Connect Red to 3.3V and Black to GND
import ssd1306

# Uses SPI port 0
spi_port = 0
MOSI = 19     # blue
CLK = 18      # yellow
CS = 17       # orange
RST = 16      # white   #NB MISO not used
DC = 20       # green

WIDTH = 128
HEIGHT = 64

spi = SPI(
    spi_port,
    baudrate=40000000,
    mosi=Pin(MOSI),
    sck=Pin(CLK))

oled = ssd1306.SSD1306_SPI(WIDTH,HEIGHT,
    spi,
    dc=Pin(DC),
    res=Pin(RST),
    cs=Pin(CS),
    external_vcc=False
    )

oled.fill(0) # Clear display
oled.show()

def block(x,y,w,h,c): # Filled rectangle
    for yy in range(h):
        oled.hline(x,y+yy,w,c)
# ========= Display set up =========

Следующий блок кода создаёт двумерный массив (список списков), чтобы по нажатой кнопке можно было найти соответствующий символ:

# === MAIN PROGRAM ===
# Map characters to keys
# These are lists of lists!
#    A 2 dimensional array

#         C0   C1   C2   C3
keys = [['1', '2', '3', 'A'], # R0\
        ['4', '5', '6', 'B'], # R1\
        ['7', '8', '9', 'C'], # R2\
        ['*', '0', '#', 'D']] # R3

Здесь мы сохраняем списки контактов строк и столбцов и настраиваем GPIO: строки — как ВЫХОДЫ, столбцы — как ВХОДЫ:

# Pin connections left to right
row_pins = [13, 12, 11, 10]
col_pins = [9, 8, 7, 6]

cols = []  # Creat empty lists
rows = []

# Set up rows and columns
# Rows are OUTPUTS / Cols are INPUTS
for x in range(0,4):
    # Rows are OUTPUTs which we set HIGH or LOW
    rows.append(Pin(row_pins[x], Pin.OUT))
    rows[x].value(0) # Set LOW = 0V

    # Columns are INPUTS with internal PULL_DOWNS
    cols.append(Pin(col_pins[x], Pin.IN, Pin.PULL_DOWN))

Этот блок настраивает встроенный светодиод Raspberry Pi Pico для световой индикации нажатий:

# Setup LED to indicate key pressed
led = Pin(25, Pin.OUT)     # Pi Pico    Comment out one of these
#led = Pin("LED", Pin.OUT) # Pi Pico W

# Short flash of LED to indicate program running
led.value(1)
time.sleep(0.5)
led.value(0)

В следующем блоке функция сканирует клавиатуру, ожидая нажатия кнопки — оно обнаруживается по переходу столбца в состояние high (3.3В).

Мы используем цикл while, управляемый переменной scanning, который поочерёдно устанавливает каждую строку в high. Затем поочерёдно опрашиваются столбцы — не установлено ли соединение:

if cols[col].value() == 1: # KEY PRESSED!

Как только нажатие обнаружено, цикл прерывается (переменная running устанавливается в False), и по значениям строки и столбца из таблицы определяется символ нажатой кнопки. Если нажатия нет, строка возвращается в состояние LOW.

Светодиод Pico кратко мигает, сигнализируя о нажатии, и символ возвращается вызывающему коду. Включена небольшая задержка для устранения «дребезга». В самих циклах задержки нет, поэтому сканирование происходит очень быстро:

# Function to scan for a key press  - Returns the character pressed
def scan():
    scanning = True
    while scanning:     # Until key pressed - blocking!
        for row in range(4):
            for col in range(4):
                rows[row].high()     # Equivalent to .value(1)
                if cols[col].value() == 1:
                    scanning = False # Terminate looping
                    k = keys[row][col] # Read character from grid
                    time.sleep(0.3) # Debounce
            rows[row].low()          # Equivalent to .value(0)
    led.value(1)        # Flash LED to indicate key pressed
    time.sleep(0.1)
    led.value(0)
    return k            # Supply values to calling code

Следующая функция, get_value(xx,yy), постепенно строит строку из нажатых символов, добавляя их в переменную ns, которая изначально является пустой строкой.

(xx, yy) — это позиция на экране для отображения формирующейся строки ns. Окончание ввода пользователь обозначает нажатием „#“ — аналогично клавише «ENTER» на клавиатуре компьютера.

Клавиша „*“ работает как «BACKSPACE» и стирает последний введённый символ, уменьшая длину строки на один символ. При изменении строки она выводится на экран и в окне Shell. На экране перед обновлением предыдущая строка перекрывается блоком пикселей цвета фона.

Для обозначения отрицательного числа: на клавиатуре 4x4 можно использовать кнопку „A“. На клавиатуре 4x3 применяется программный приём — если первым нажатием при пустой строке (длина ns равна нулю) является „*“ (стирание), оно интерпретируется как «минус».

Для клавиш „A“, „B“ и „D“ в строку ns добавляются соответствующие символы: „-“, „e“ и „.“.

После выхода из цикла по нажатию „#“ сначала проверяется, не является ли строка пустой — тогда возвращается целое значение 0. Иначе проверяется наличие „.“ или „e“ в строке, что означает FLOAT, и строка конвертируется в соответствующий тип — целое число или число с плавающей точкой.

В конце значение возвращается вызывающему коду:

def get_value(xx,yy): # INPUT a float - includes e-format
    ns =""            # Needs 4 column keypad for full functionality
    while True:
        key = scan()           # Get a chacter from the keypad
        if key == '#':         # ENTER/RETURN
            break              # Break out of loop
        elif (key == "*") :
            if len(ns) == 0:
                ns = ns + "-"  # Minus character (for 3 column displays)
            else:
                ns = ns[0:-1]  # Backspace
        elif key == "C":
            pass               # Not valid
        elif key == "A":
            ns = ns + "-"      # 'Minus' character
        elif key == "D":       # Decimal point
            ns = ns + "."
        elif key == "B":
            ns = ns +"e"       # Allow scientific notation values
        else:
            ns = ns + key      # Add character to string
        print(ns)              # Current string to Shell window
        block(xx,yy,128,10,0)  # Remove outdated number from display
        oled.text(ns,xx,yy,1)  # Display updated number
        oled.show()

    if len(ns) == 0:
        return 0
    if (ns.find(".") > 0) or (ns.find("e") > 0): # This is a FLOAT:
        # It contains an "." OR an "e"
        return float(ns)
    else:
        # Integer
        return int(ns)

Последний блок кода использует написанные функции для ввода значений с клавиатуры и отображает их вместе с их типом. Ввод НУЛЯ останавливает программу и очищает экран:

# ======= Main Loop =========

while True:
    oled.text("N:",0,10) # Prompt
    oled.show()
    n = get_value(16,10)
    oled.text(str(n),5,25,1)
    oled.text(str(type(n)),5,35,1)
    oled.show()
    print("\n", n)
    tp = str(type(n))
    print(tp)
    time.sleep(2)
    oled.fill(0)
    if n == 0:  # HALT if zero
        break

# Tidy up
oled.fill(0)
oled.show()

Запуск кода

Теперь ваша очередь — соберите программу в окне кода Thonny и запустите её, а затем попробуйте вводить числа с помощью клавиатуры. Используйте целые числа, числа с плавающей точкой и числа в научной нотации.

Посмотрите, что произойдёт, если сразу нажать клавишу хэш.

Приложение — Распиновки

У Adafruit есть удобная распиновка для подобных матриц (обратите внимание: Adafruit нумерует строки и столбцы с 1, а не с 0).