How to make a SN74HC165N shift register keyboard for ESP32 (Arduino framework) using freeRTOS

SN74HC165N Shift Register Pin Configuration

SN74HC165N Shift Register Pin Configuration

Every beginner sooner or later needs to increase the number of I/O ports for his project and MK. In my case – ESP32 devboard. At least, everything was tested on it, and expansion of the ports was planned on a custom board with the same esp32-WROOM module on board. We will not go into the details of the circuit design and pinout for my case; the topic of the article is the implementation of the keyboard on the SN74HC165N in the Arduino framework for esp32 using the freeRTOS functionality in the project (i.e. we will write code with a scheduler and tasks, and not in one loop, also known as “Round Robin”).

If you have already gotten to freeRTOS, I think I don’t need to explain to you how to connect the button and wiring on the breadboard, so it’s short and to the point: we connect the buttons with pull-ups to “+” and I’ll tell you how it turned out for me. The solution is probably not optimal – I would be glad to hear your opinion if it turned out to be better. I’m working on my first electronics project. At one time I did not find any suitable information on this topic, which prompted me to write an article.

Full example code:

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

So, my tasks were the following:

  • implement data transfer from 8 buttons via a shift register (instead of 8 pins we take 3)

  • For each button, a whole set of actions should be recorded: pressing, x2/x3/xY pressing, holding, only releasing, a counter for the number of presses at a time, multi-touch, etc., so that in the future there will be something to play with in the project (not to lose functionality due to for another hardware implementation)

  • and of course, all this should work for freeRTOS, which means it’s implemented via a queue

Libraries used:

  • EncButton — here I found all the necessary functionality for a single button: different types of clicks, holding, etc.

Implemented in C++ as a class, it works for me. Well, don’t forget that for platformio you need to connect Arduino.h itself:

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

That’s it, we have everything else in the basic Arduino.h and we will do things.


Let’s look at some important parts of the code one by one

1.First of all, we define the ports for the shift register: data bus, “latch” a.k.a. shutter, a.k.a. “LATCH”, tact contact. If the meaning of the contacts and the register device is not entirely clear, read the datasheet for SN74HC165.

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

This is the constant for the last parameter of the function xQueueSend()which in our case will determine the frequency of updating keyboard values ​​if the queue is busy for some reason and does not accept values.

#define KEYBOARD_QUEUE_TIMEOUT_TICKS 5000

2. Variable button type enum and the overloads created for it are needed to increase the clarity and readability of the text. Now in a loop for It’s clear to us what’s going on, and overall everything looks neater, but nothing more.

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 is essentially a container structure that can be modified depending on what functionality you need in your project. For a complete understanding, you need to read the opensource documentation of the EncButton library, the link was above.

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. We create a handle (I call it “handbrake”) for our main freeRTOS queue, essentially a pointer to the queue to identify it.

QueueHandle_t keyboard_queue;

Next, we declare the creation of a queue in setup(), set the size and number of elements:

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

4. We create tasks for the keyboard and the receiver task (for me this is master_task) and don’t forget to write delay in loop(), because This is also a freeRTOS task, and if you forget to do this, the watchdog will throw an error.

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. Let’s analyze keyboard_task

After initializing the pins, create a buffer input_buff to write data to the queue. Everything else is internal settings for reading data from buttons from “EncButton”. Adjust them as you wish.

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;

Then the logic is as follows:

We read the data from the shift register and write it to the byte variable:

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

Functions shiftIn() And bitRead() – from the library built into the Arduino core for working with shift registers, you can write them yourself.

We read the data one by one using the function tick() :

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

And then we write all the data we need into the buffer and transfer it to the queue:

//считывание и передача значений с каждой кнопки масссива в структуру-буфер и его(буфера) передача в очередь '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);
    }

As you can see here, each button is passed as a separate element to the queue. You can modify this example to pass an array of buttons at once – I personally did just that for my project.

6. We write a receiver task.

example sink task
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();
  }

I made a receiver here purely to check the correctness of the output, i.e. without payload. If everything is ok, when you release the corresponding button you will see on the screen:

“release BUTTX”

In the same way, you can use all other button parameters in the recipient task.

Thus:

  • We have successfully reduced the number of pins occupied on the esp32 from 8 to 3

  • We received a keyboard with a wide range of functions that can be customized to suit your needs

  • Provided our task with data from the keyboard

In principle, the goal has been achieved. However, I will allow myself to develop the topic a little further. Let’s say you take this example as a reference for your project.

There is a possibility that:

  1. In the designed device, the keyboard, whatever one may say, will not be the only input device

  2. Several tasks must be done at once receive data from queue

Let’s consider the first case. The well-known pic below shows an example of a model that can be described as follows: one common queue for all input devices.

An example of organizing information exchange between tasks.  Kurnitz, Components and Technologies No. 6, 2011

An example of organizing information exchange between tasks. Kurnitz, Components and Technologies No. 6, 2011

The queue element, along with the data from the transmitter task, receives an ID, and then, with its help, the receiver understands what data came from where.

Let’s add an encoder to our keyboard. It has its own transmitter task, and for its parameters we create a new structure:

//в структуре создержатся переменные, покрывающие весь функционал энкодера
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;

Now, as agreed, we will immediately transfer an array of buttons to the general queue. We will also add a variable of type enum, which will serve as an identification variable:

//---------------------------------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 

The main data queue could thus look like this:

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

Everything else remains the same. You configure reading on the receiving task side and there are no problems.

Second case. The situation gets more complicated if you want to have multiple recipient tasks at once. Kurnitz calls this problem nontrivial. I had to solve this problem myself, so I’m raising it here. The solution that the Internet offers in this case is to share the synchronization functions of event groups and view the queue element without deleting it.

xQueuePeek() + xEventGroupSync() + xQueueReceive()

The bottom line: all recipient tasks “snoop” into the queue using the function xQueuePeek() after which they immediately set their synchronization bit (in a specially created event group EventGroupHandle_t syncEvent), letting the system know that it has already received data from the queue and is only waiting for the rest. When all tasks have received data, the synchronization condition is satisfied and the last reader removes the item from the queue using xQueueReceive().

To avoid lowering the priority of one of the recipient tasks, I created a separate synchronization task whose only function is to remove an element from the queue. It has a lower priority than reader tasks:

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);
  }
}

And it worked. Now all your payload tasks have information from all input devices. I would be glad to receive feedback, I hope that the article was useful.

Sources:

1. freeRTOS queue API (queueManagement)

2. Andrey Kurnits series of articles on freeRTOS. Components and Technologies, 2011

3. Third-party button processing library (EncButton)

4. Overloading enumeration operators (enum++)

5. Built-in Ardunio.h functions for working with a shift register (shiftIn() And bitRead() )

6.Datasheet SN74HC165N (can be found online)

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *