Как стать автором
Обновить

Как сделать клавиатуру на сдвиговом регистре SN74HC165N для ESP32 (Arduino framework) с использованием FreeRTOS

Уровень сложностиСредний
Время на прочтение11 мин
Количество просмотров6.4K
Конфигурация выводов сдвигового регистра SN74HC165N
Конфигурация выводов сдвигового регистра SN74HC165N

У каждого новичка рано или поздно возникает необходимость увеличить количество портов ввода-вывода для своего проекта и МК. В моем случае — ESP32 devboard. По крайней мере, на ней все тестировалось, а расширение портов планировалось на кастомной плате с тем же модулем esp32-WROOM на борту. В детали схемотехники и распиновки для моего случая вдаваться не будем, тема статьи — реализация клавиатуры на SN74HC165N в Arduino-фреймворке для esp32 с использованием функционала freeRTOS в проекте (т.е. будем писать код с планировщиком и задачами, а не в одном цикле, так же известном как «Round Robin»).

Если вы уже добрались до freeRTOS, думаю, как подключать кнопку и проводки на breadboard мне объяснять вам не требуется, поэтому кратко и по делу: подключаем кнопки с подтяжкой к "+" и расскажу, как получилось у меня. Решение, наверное, не оптимальное — буду рад услышать ваше мнение, если получилось сделать лучше. Работаю над своим первым проектом в электронике. В свое время не нашел подходящей информации на эту тему, что и сподвигло меня на написание статьи.

Полный код примера:

main.cpp
/*
//каждая клавиша передается в очередь как отдельная структура со своими параметрами

*/
#include "main.h"

void setup() {
  Serial.begin(9600);
  keyboard_queue = xQueueCreate(8, sizeof(gg_button));
  xTaskCreate(keyboard_task, "KEYBOARD", 1500, NULL, 1, NULL);
  xTaskCreate(master_task, "MASTER", 1500, NULL, 1, NULL);
}

void loop() {
   delay(1000);   
}


void keyboard_task(void* paramBaram) {
  //init
  pinMode(DATA_PIN, INPUT);     // инициализация пинов
  pinMode(CLOCK_PIN, OUTPUT);
  pinMode(LATCH_PIN, OUTPUT);
  digitalWrite(LATCH_PIN, HIGH);
  gg_button input_buff;
  uint8_t x_clicks = 5;
  uint16_t timeout_button = 60000; // после взаимодействия с кнопкой прошло указанное время, мс [событие]
  uint16_t press_for_timeout = 2000;  // время, которое кнопка удерживается (с начала нажатия), мс
  uint16_t hold_for_timeout = 2000;  // время, которое кнопка удерживается (с начала удержания), мс
  uint16_t step_for_timeout = 2000;
  while(1) {
    digitalWrite(LATCH_PIN, LOW);   // щелкнули защелкой
    digitalWrite(LATCH_PIN, HIGH);
    byte b = shiftIn(DATA_PIN, CLOCK_PIN, MSBFIRST);      // считываем

  //опрос для каждой кнопки в массиве
    for (button = BUTT0; button <= BUTT7; button++) {
      buttons[button].tick(!bitRead(b,  button)); //необходимо инвертировать вывод bitRead(), т.к. либа настроена на работу с подтяжкой к земле, а у меня к +
    }

  //считывание и передача значений с каждой кнопки масссива в структуру-буфер и его(буфера) передача в очередь 'keyboard_queue'
    for (button = BUTT0; button <= BUTT7; button++) {
      input_buff.id = button;
      input_buff.press = buttons[button].press();
      input_buff.release = buttons[button].release();
      input_buff.click = buttons[button].click();
      input_buff.pressing = buttons[button].pressing();
      input_buff.hold = buttons[button].hold();
      input_buff.holding =  buttons[button].holding();
      input_buff.step = buttons[button].step();
      input_buff.has_clicks = buttons[button].hasClicks();
      input_buff.has_x_clicks = buttons[button].hasClicks(x_clicks);
      input_buff.get_clicks = buttons[button].getClicks();
      input_buff.get_steps = buttons[button].getSteps();
      input_buff.release_hold = buttons[button].releaseHold();
      input_buff.release_step = buttons[button].releaseStep();
      input_buff.timeout = buttons[button].timeout(timeout_button);
      input_buff.press_for = buttons[button].pressFor();
      input_buff.press_for_t = buttons[button].pressFor(press_for_timeout);
      input_buff.hold_for = buttons[button].holdFor();
      input_buff.hold_for_t = buttons[button].holdFor(hold_for_timeout);
      input_buff.step_for =  buttons[button].stepFor();
      input_buff.step_for_t = buttons[button].stepFor(step_for_timeout);

       xQueueSend(keyboard_queue, &input_buff, KEYBOARD_QUEUE_TIMEOUT_TICKS);
    }
    portYIELD();
     //отдать управление планировщику
  }
}

void master_task(void* paramBaram) {
  gg_button output_buff;
  while(1) {
    xQueueReceive(keyboard_queue, &output_buff, portMAX_DELAY);
    
    
    switch(output_buff.id) {
      case BUTT0:
        //print_button(&output_buff);
        if(output_buff.release == 1) {
          std::cout << "release BUTT0\n";
        }
      break;
      
      case BUTT1:
        if(output_buff.release == 1) {
          std::cout << "release BUTT1\n";
        }
      break;

      case BUTT2:
        if(output_buff.release == 1) {
          std::cout << "release BUTT2\n";
        }
      break;

      case BUTT3:
        if(output_buff.release == 1) {
          std::cout << "release BUTT3\n";
        }
      break;

      case BUTT4:
        if(output_buff.release == 1) {
          std::cout << "release BUTT4\n";
        }
      break;

      case BUTT5:
        if(output_buff.release == 1) {
          std::cout << "release BUTT5\n";
        }
      break;

      case BUTT6:
        if(output_buff.release == 1) {
          std::cout << "release BUTT6\n";
        }
      break;

      case BUTT7:
        if(output_buff.release == 1) {
          std::cout << "release BUTT7\n";
        }
      break;
    }
    
    portYIELD();
  }
}

main.h
#ifndef MAIN_H
#define MAIN_H

#include <Arduino.h>
#include <EncButton.h>

#define DATA_PIN    12  // пин данных
#define LATCH_PIN   27  // пин защелки
#define CLOCK_PIN   14  // пин тактов синхронизации

#define KEYBOARD_QUEUE_TIMEOUT_TICKS 5000
#define SIZE_KEYBOARD 8  //массив кнопок (общее количество кнопок для обработки)

enum buttons_list {
  BUTT0,
  BUTT1,
  BUTT2,
  BUTT3,
  BUTT4,
  BUTT5,
  BUTT6,
  BUTT7
};

enum buttons_list button;

//массив из 8 кнопок
VirtButton buttons[SIZE_KEYBOARD];

//структура для передачи состояния кнопки в очередь: тут почти все фичи из класса 'VirtButton'
typedef struct gg_button {
  buttons_list id; 
  bool press;
  bool release;
  bool click;
  bool pressing;
  bool hold;
  bool holding;
  bool step;
  bool has_clicks;
  bool has_x_clicks;
  uint8_t get_clicks;
  uint16_t get_steps;
  bool release_hold;
  bool release_step;
  bool timeout;
  uint16_t press_for;
  bool press_for_t;
  uint16_t hold_for;
  bool hold_for_t;   
  uint16_t step_for;
  bool step_for_t;
} gg_button;

QueueHandle_t keyboard_queue;

//tasks
void keyboard_task(void* paramBaram);
void master_task(void* paramBaram);


//enum не может неявно преобразовываться в другие типы в C++
//поэтому нужна перегрузка '++' (он нужен для использования enum в операторе for)

//префиксный
buttons_list operator++(buttons_list& b) {
    b = static_cast<buttons_list>(static_cast<int>(b) + 1);
    return b;
}
// Постфиксный инкремент
buttons_list operator++(buttons_list& b, int) {
    buttons_list old = b;
    b = static_cast<buttons_list>(static_cast<int>(b) + 1);
    return old;
}

#endif

Итак, задачи у меня были следующие:

  • реализовать передачу данных с 8 кнопок через сдвиговый регистр (вместо 8 выводов занимаем 3)

  • для каждой кнопки должен фиксироваться целый набор действий: нажатие, x2/x3/xY нажатие, удержание, только отпускание, счетчик количества нажатий за один раз, мультитач и проч., чтобы в будущем в проекте было с чем поиграться (не терять функционал из-за другой аппаратной реализации)

  • ну и разумеется, все это должно работать для FreeRTOS, а значит — реализовано через очередь

Использованные библиотеки:

  • EncButton — здесь нашел весь необходимый функционал для отдельно взятой кнопки: разные виды нажатий, удержание и т.д.

Реализовано на c++ как класс, мне пойдет. Ну и не забывайте, что для platformio нужно саму Arduino.h подключить:

#include <Arduino.h>
#include <EncButton.h>

Вот и все, все остальное у нас есть в базовой Arduino.h и будем делать вещи.


Разберем некоторые важные части кода по отдельности

1.Первым делом определяем порты для сдвигового регистра: шина данных, «защелка» а.к.а. затвор, а.к.а. «LATCH», тактовый контакт. Если значение контактов и устройство регистра не совсем ясно, читаем даташит для SN74HC165.

#define DATA_PIN    12  // пин данных
#define LATCH_PIN   27  // пин защелки
#define CLOCK_PIN   14  // пин тактов синхронизации

Это константа для последнего параметра функции xQueueSend(), который в нашем случае будет определять частоту обновления значений клавиатуры в случае, если очередь по какой-то причине занята и не принимает значения.

#define KEYBOARD_QUEUE_TIMEOUT_TICKS 5000

2.  Переменная button типа enum и созданные для нее перегрузки нужны для повышения понятности и удобочитаемости текста. Теперь в цикле for у нас понятно что происходит, и в целом все выглядит более аккуратно, но не более того.

enum buttons_list
enum buttons_list {
  BUTT0,
  BUTT1,
  BUTT2,
  BUTT3,
  BUTT4,
  BUTT5,
  BUTT6,
  BUTT7
};

enum buttons_list button;

//enum не может неявно преобразовываться в другие типы в C++
//поэтому нужна перегрузка '++' (он нужен для испольования enum в операторе for)

//префиксный
buttons_list operator++(buttons_list& b) {
    b = static_cast<buttons_list>(static_cast<int>(b) + 1);
    return b;
}
// Постфиксный инкремент
buttons_list operator++(buttons_list& b, int) {
    buttons_list old = b;
    b = static_cast<buttons_list>(static_cast<int>(b) + 1);
    return old;
}

gg_button по сути структура-контейнер, которую можно модифицировать в зависимости от того, какой функционал вам необходим в проекте. Для полного понимания нужно ознакомиться с документацией opensource библиотеки «EncButton», ссылка была выше.

struct gg_button
//структура для передачи состояния кнопки в очередь: тут почти все фичи из класса 'VirtButton'
typedef struct gg_button {
  buttons_list id; 
  bool press;
  bool release;
  bool click;
  bool pressing;
  bool hold;
  bool holding;
  bool step;
  bool has_clicks;
  bool has_x_clicks;
  uint8_t get_clicks;
  uint16_t get_steps;
  bool release_hold;
  bool release_step;
  bool timeout;
  uint16_t press_for;
  bool press_for_t;
  uint16_t hold_for;
  bool hold_for_t;   
  uint16_t step_for;
  bool step_for_t;
} gg_button;

3.  Создаем handle (я его называю «ручник») для нашей главной очереди freeRTOS, по сути — указатель на очередь для ее индентификации.

QueueHandle_t keyboard_queue;

Далее объявляем о создании очереди в setup(), устанавливаем размер и количество элементов:

keyboard_queue = xQueueCreate(8, sizeof(gg_button));

4. Создаем задачи для клавиатуры и задачи-приемника (у меня это master_task) и не забываем прописать в loop() delay, т.к. это тоже freeRTOS задача, и если это забывать сделать, watchdog будет выдавать ошибку.

void setup() {
  Serial.begin(9600);
  keyboard_queue = xQueueCreate(8, sizeof(gg_button));
  xTaskCreate(keyboard_task, "KEYBOARD", 1500, NULL, 1, NULL);
  xTaskCreate(master_task, "MASTER", 1500, NULL, 1, NULL);
}

void loop() {
   delay(1000);   
}

5.  Разберем keyboard_task

После инициализации пинов создаем буффер input_buff для записи данных в очередь. Все остальное — внутренние настройки считывания данных с кнопок из «EncButton». Регулируйте их по своему усмотрению.

gg_button input_buff;
  uint8_t x_clicks = 5;
  uint16_t timeout_button = 60000; // после взаимодействия с кнопкой прошло указанное время, мс [событие]
  uint16_t press_for_timeout = 2000;  // время, которое кнопка удерживается (с начала нажатия), мс
  uint16_t hold_for_timeout = 2000;  // время, которое кнопка удерживается (с начала удержания), мс
  uint16_t step_for_timeout = 2000;

Далее логика следующая:

считываем данные со сдвигового регистра и записываем в переменную byte:

  digitalWrite(LATCH_PIN, LOW);   // щелкнули защелкой
  digitalWrite(LATCH_PIN, HIGH);
  byte b = shiftIn(DATA_PIN, CLOCK_PIN, MSBFIRST);      // считываем

Функции shiftIn() и bitRead() — из встроенной в ядро ардуино библиотеки для работы со сдвиговыми регистрами, вы можете написать их сами.

Поочередно считываем данные с помощью функции tick() :

//опрос для каждой кнопки в массиве
    for (button = BUTT0; button <= BUTT7; button++) {
      buttons[button].tick(!bitRead(b,  button)); //необходимо инвертировать вывод bitRead(), т.к. либа настроена на работу с подтяжкой к земле, а у меня к +
    }

А затем записываем все нужные нам данные в буффер и передаем в очередь:

//считывание и передача значений с каждой кнопки масссива в структуру-буфер и его(буфера) передача в очередь 'keyboard_queue'
    for (button = BUTT0; button <= BUTT7; button++) {
      input_buff.id = button;
      input_buff.press = buttons[button].press();
      input_buff.release = buttons[button].release();
      input_buff.click = buttons[button].click();
      input_buff.pressing = buttons[button].pressing();
      input_buff.hold = buttons[button].hold();
      input_buff.holding =  buttons[button].holding();
      input_buff.step = buttons[button].step();
      input_buff.has_clicks = buttons[button].hasClicks();
      input_buff.has_x_clicks = buttons[button].hasClicks(x_clicks);
      input_buff.get_clicks = buttons[button].getClicks();
      input_buff.get_steps = buttons[button].getSteps();
      input_buff.release_hold = buttons[button].releaseHold();
      input_buff.release_step = buttons[button].releaseStep();
      input_buff.timeout = buttons[button].timeout(timeout_button);
      input_buff.press_for = buttons[button].pressFor();
      input_buff.press_for_t = buttons[button].pressFor(press_for_timeout);
      input_buff.hold_for = buttons[button].holdFor();
      input_buff.hold_for_t = buttons[button].holdFor(hold_for_timeout);
      input_buff.step_for =  buttons[button].stepFor();
      input_buff.step_for_t = buttons[button].stepFor(step_for_timeout);

       xQueueSend(keyboard_queue, &input_buff, KEYBOARD_QUEUE_TIMEOUT_TICKS);
    }

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

6. Пишем задачу-приемник.

пример задачи-приемника
gg_button output_buff;
  while(1) {
    xQueueReceive(keyboard_queue, &output_buff, portMAX_DELAY);
    
    switch(output_buff.id) {
      case BUTT0:
        //print_button(&output_buff);
        if(output_buff.release == 1) {
          std::cout << "release BUTT0\n";
        }
      break;
      
      case BUTT1:
        if(output_buff.release == 1) {
          std::cout << "release BUTT1\n";
        }
      break;

      case BUTT2:
        if(output_buff.release == 1) {
          std::cout << "release BUTT2\n";
        }
      break;

      case BUTT3:
        if(output_buff.release == 1) {
          std::cout << "release BUTT3\n";
        }
      break;

      case BUTT4:
        if(output_buff.release == 1) {
          std::cout << "release BUTT4\n";
        }
      break;

      case BUTT5:
        if(output_buff.release == 1) {
          std::cout << "release BUTT5\n";
        }
      break;

      case BUTT6:
        if(output_buff.release == 1) {
          std::cout << "release BUTT6\n";
        }
      break;

      case BUTT7:
        if(output_buff.release == 1) {
          std::cout << "release BUTT7\n";
        }
      break;
    }
    
    portYIELD();
  }

Я здесь сделал приемник чисто для проверки корректности вывода, т.е. без полезной нагрузки. Если все ок, при отпускании соответствующей кнопки вы увидите на экране:

«release BUTTX»

Таким же образом вы можете использовать в задаче-получателе все остальные параметры кнопки.

Таким образом:

  • Мы успешно сократили количество занимаемых выводов на esp32 с 8 до 3

  • Получили клавиатуру с широким набором функций, которую можно настроить под свои нужды

  • Обеспечили нашу задачу данными с клавиатуры

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

Есть вероятность, что:

  1. В проектируемом устройстве клавиатура, как не крути, будет являться не единственным устройством ввода

  2. Сразу несколько задач должны получать данные из очереди

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

Пример организации обмена информацией между задачами. Курниц, Компоненты и технологии №6, 2011
Пример организации обмена информацией между задачами. Курниц, Компоненты и технологии №6, 2011

Элемент очереди вместе с данными из задачи-передатчика получает ID, а затем с его помощью приемник понимает какие данные откуда пришли.

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

//в структуре создержатся переменные, покрывающие весь функционал энкодера
typedef struct {
  bool left;
  bool right;
  bool leftH;
  bool rightH;
  int dir;
  bool press;
  bool pressing;
  int clicks;
  int click;
  bool fast;
  int counter;
  bool release;
  bool hold;
  uint16_t holdFor;
  uint16_t step;
  uint16_t action;

} EncData;

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

//---------------------------------ALL DATA ENUM---------------------------------------------
//data types for correct data queue read/write 
enum queue_data_type{ ENCODER_DATA, KEYBOARD_DATA};
//--------------------------------------------------------------------------------------------
//enum var for data type identificaion (joystick, encoder, etc.) in one queue 

Главная очередь данных таким образом может выглядеть так:

typedef struct {
  EncData enc_data;
  gg_button_t keyboard_data[SIZE_KEYBOARD];
  queue_data_type id;
} ggData;

Все остальное остается так же. Вы настраиваете чтение на стороне задачи-приемника и проблем нет.

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

"xQueuePeek() + xEventGroupSync() + xQueueRecieve()"

Суть: все задачи-получатели "подсматривают" в очередь с помощью функции xQueuePeek() после чего сразу устанавливают свой бит синхронизации (в специально созданной для этого группе событий EventGroupHandle_t syncEvent), давая понять системе, что она уже получила данные из очереди и ждет только остальных. Когда все задачи получили данные, условие синхронизации выполняется и последний читатель удаляет элемент из очереди с помощью xQueueRecieve().

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

void sync_task(void* pvParameters) {
  ggData delete_buff;
  EventBits_t sync_return_check;
  while(1) {
    sync_return_check = xEventGroupSync(syncEvent, SYNC_START_BIT, ALL_SYNC_BITS, portMAX_DELAY);  
    if( (sync_return_check & ALL_SYNC_BITS) == ALL_SYNC_BITS) 
      xQueueReceive(ggDataQueue, &delete_buff, portMAX_DELAY);
  }
}

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

Источники:

1. API очередей freeRTOS (queueManagement)

2. Андрей Курниц цикл статей по freeRTOS. Компоненты и технологии, 2011

3.  Сторонняя библиотека обработки кнопок (EncButton)

4. Перегрузка операторов перечисления (enum++)

5. Встроенные функции Ardunio.h для работы со сдвиговым регистром (shiftIn() и bitRead() )

6.Datasheet SN74HC165N (можно найти в сети)

Теги:
Хабы:
Всего голосов 4: ↑4 и ↓0+4
Комментарии16

Публикации

Истории

Ближайшие события

Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн
Антиконференция X5 Future Night
Дата30 мая
Время11:00 – 23:00
Место
Онлайн
Конференция «IT IS CONF 2024»
Дата20 июня
Время09:00 – 19:00
Место
Екатеринбург