Nieco inne rozwiązanie wykorzystujące sprzętową magistralę SPI zamiast ręcznego machania bitami, jak w przykładzie ze strony Arduino.
Zakładam, że mamy ileś tam układów CD4021 połączonych w łańcuch wg poniższego schematu.
W programie definiujemy ilość układów:
const int CD4021_liczba_ukladow = 2; // - musi byc wieksza lub rowna 1
Przetestowałem układ na 2 sztukach, bo tyle akurat miałem pod ręką. Każdy z układów występuje w programie jako "bank" 8 wejść.
Program automatycznie odczytuje stany wejść, odrwaca je (wejścia są zwierane do masy, taki układ jest bardziej odporny na ew. pomyłki i uszkodzenia) filtruje je (eliminacja drgań styków) i podaje w postaci tablicy 8 bitowych wartości "CD4021_bank_przyciskow_filtr". Indeks w tej tablicy odpowiada bankowi lub numerowi układu - numeracja jest od zera, a poszczególne bity stanom wejść w tymże układzie.
Czyli np., żeby pobrać sobie stan przycisku nr 3 (numeracja jest od 0 do 7) z drugiego CD4021 w kolejce i zastosować go w bibliotece joystika możemy użyć takiego kodu:
if (CD4021_bank_przyciskow_filtr[1] & (1<<3) joystick.pressButton(3);
else joystick.releaseButton(3);
Można też użyc przeznaczonej do tego funkcji:
uint8_t CD4021_odczytaj_stan_przycisku(uint8_t bank, uint8_t numer_przycisku)
która zwraca 1 jeśli przycick jest wciśnięty lub 0 jeśli nie. Tą funkcję można również bezpośrednio użyć z biblioteką joystick.h:
joystick.setButton(3,CD4021_odczytaj_stan_przycisku(1, 3));
Do obsługi HATów napisałem dodatkową funkcję, która przetwarza wejścia stanów góra, lewo, dól, prawo na wartość kątową.
int16_t CD4021_odczytaj_hat(uint8_t bank, uint8_t pinUp, uint8_t pinR, uint8_t pinDwn, uint8_t pinL)
Podajemy "bank", czyli do którego scalaka 4021 podpięty mamy HAT i bity, pod które podłączone są poszczególne wyjścia. W zamian dostajemy kąt, który wstawia się bezpośrednio do
Joystick.setHatSwitch(0,CD4021_odczytaj_hat(1, 0,1,2,3));
Kod napisany jest w większości po polsku ze sporą ilością komentarzy. Mam nadzieję, że będzie zrozumiały i prosty w modyfikowaniu pod własne potrzeby.
Wymagane są dwie biblioteki: Joystick i TimerOne. Linki do nich podane są w nagłówku programu.
Program obsługuje dodatkowo 8 wejść analogowych. Niestety, ale z biblioteką Joystick chyba jest coś nie tak, bo nie udało mi się uzyskać pełnych 8 osi. Windows widzi tylko 7. Problem wystapił nie tylko u mnie - zgłaszali go również inni użytkownicy na githubie.
Schemat:

Pełny kod programu:
#include <Arduino.h>
#include <SPI.h>
#include <TimerOne.h>
#include "Joystick.h"
/*
* Przykladowy program uzywajacy rejestrow CD4021 jako wejsc przyciskow
* (c) 2017 Piotr Zapart
*
Wymagane biblioteki:
Joystick: https://github.com/MHeironimus/ArduinoJoystickLibrary
TimerOne: https://github.com/PaulStoffregen/TimerOne
*/
// ############################## OSIE ANALOGOWE #############################################################
// zakladamy, ze max dostepnych bedzie do 8 osi analogowych: A0, A1, A2, A3, A6, A7, A8, A9
const int k_Ravg_size = 7; //stopien usredniania wartosci ADC (2^n -1)
const uint16_t k_LPfltFeedforward = 10; //prog zadzialania filtra
// definiujemy kolejnosc skanowania osi analogowych
int ADC_kolSkanowania[] = {A0, A1, A2, A3, A6, A7, A8, A9};
const int ADC_iloscOsi = sizeof(ADC_kolSkanowania) / sizeof(ADC_kolSkanowania[0]);
static volatile uint16_t ADC_rawData[ADC_iloscOsi]; //dane odczytane prosto z ADC
static volatile uint16_t ADC_filteredData[ADC_iloscOsi]; //
void ADCLowPassFilter(void);
// #############################################################################
#define WLACZ_FILTR_OSI 1 //1= filtrowanie osi wlaczone, 0 = wylaczone
// #############################################################################
void ADC_odczytajOsie(void)
{
for (int i = 0; i < ADC_iloscOsi; ++i)
{
ADC_rawData[i] = analogRead(ADC_kolSkanowania[i]);
}
}
//##############################################################################
// Martwa strefa w centrum skali
uint16_t applyDeadZone(uint16_t data16bit, uint8_t deadZone)
{
uint16_t output;
uint32_t t32;
if (deadZone > 14) deadZone = 14;
if (data16bit & (1 << 15))
{
data16bit = data16bit - 0x7FFF;
t32 = ((uint32_t)data16bit) * data16bit;
output = t32 >> 15;
if (data16bit > (uint16_t)(1 << deadZone))
{
output = map(data16bit, (1 << deadZone), 0x7FFF, output, 0x7FFF);
}
output += 0x7FFF;
}
else
{
t32 = ((uint32_t)data16bit) * data16bit;
output = (data16bit << 1) - (t32 >> 15); //2*data - (data^2)
if (data16bit < (0x7FFF - (uint16_t)(1 << deadZone)))
{
output = map(data16bit, 0, (0x7FFF - (1 << deadZone)), 0, output);
}
}
return output;
}
//##############################################################################
void ADCLowPassFilter(void)
{
static volatile uint32_t filt[ADC_iloscOsi];
uint16_t temp16;
uint8_t i;
for (i = 0; i < ADC_iloscOsi; ++i)
{
temp16 = ADC_rawData[i];
if ((temp16 > (filt[i] + k_LPfltFeedforward)) ||
((filt[i] > k_LPfltFeedforward) &&
(temp16 < (filt[i] - k_LPfltFeedforward))))
{
filt[i] = temp16;
}
else
{
filt[i] *= k_Ravg_size;
filt[i] += temp16;
filt[i] /= (k_Ravg_size + 1);
}
ADC_filteredData[i] = filt[i];
}
}
// ############################## PRZYCISKI I HATy ###########################################################
const int CD4021_liczba_ukladow = 2; // - musi byc wieksza lub rowna 1
#define PIN_SCK 15 // SPI clock - podlaczony do wejsc 10 w CD4021 (CLK)
#define PIN_MISO 14 // SPI data input - podlaczony do wyjscia pierwszego CD4021 (Pin 3)
#define PIN_MOSI 16 // SPI data output - nie uzywany, nie podlaczac do niczego
#define PIN_LOAD 7 // do pin9 we wszystkich CD4021
volatile uint8_t CD4021_bank_przyciskow_filtr[CD4021_liczba_ukladow];//tablica zawierajaca stan wejsc po eliminacji drgan stykow (filtr cyfrowy)
//deklaracje funkcji
void CD4021_odczytaj_wejscia(void);
uint8_t CD4021_odczytaj_stan_przycisku(uint8_t bank, uint8_t numer_przycisku);
/* HAT dekoder
bit0 = UP
bit1 = RIGHT
bit2 = DOWN
bit3 = LEFT
*/
const int16_t CD4021_hat_decoder[13] PROGMEM =
{
-1, // B0000 OFF
0, // B0001 UP
90, // B0010 RIGHT
45, // B0011 UP+RIGHT
180, // B0100 DOWN
-1, // B0101 OFF
135, // B0110 DOWN+RIGHT
-1, // B0111 OFF
270, // B1000 LEFT
315, // B1001 UP+LEFT
-1, // B1010 OFF
-1, // B1011 OFF
225 // B1100 DOWN+LEFT
};
Joystick_ Joystick( JOYSTICK_DEFAULT_REPORT_ID, //hidReportId
JOYSTICK_TYPE_JOYSTICK, //joystickType
32, //ilosc przyciskow
2, //ilosc HATow
true, //includeXAxis
true, //includeYAxis
true, //includeZAxis
true, //includeRxAxis
true, //includeRyAxis
true, //includeRzAxis
true, //includeRudder
true, //includeThrottle
true, //includeAccelerator
true, //includeBrake
true); //includeSteering
//#############################################################################################################
void CD4021_odczytaj_wejscia(void)
{
uint8_t index, i, input;
static uint8_t count0[CD4021_liczba_ukladow], count1[CD4021_liczba_ukladow];
digitalWrite(PIN_LOAD, LOW);
SPI.beginTransaction(SPISettings(2000000, MSBFIRST, SPI_MODE0)); //taktowanie SPI 2MHz
for (index = 0; index < (CD4021_liczba_ukladow); index++)
{
input = SPI.transfer(0x00);
i = CD4021_bank_przyciskow_filtr[index] ^ ~input;
count0[index] = ~( count0[index] & i );
count1[index] = count0[index] ^ (count1[index] & i);
i &= count0[index] & count1[index];
CD4021_bank_przyciskow_filtr[index] ^= i;
}
SPI.endTransaction();
digitalWrite(PIN_LOAD, HIGH);
}
//#############################################################################################################
// numeracja przyciskow zaczyna sie od zera, czyli dla 24 przyciskow numery sa 0...23
uint8_t CD4021_odczytaj_stan_przycisku(uint8_t bank, uint8_t numer_przycisku)
{
if (bank > CD4021_liczba_ukladow) return 0; //zabezpieczenie
if (CD4021_bank_przyciskow_filtr[bank] & (1 << numer_przycisku)) return 1; //stan wysoki=OFF
else return 0; //stan niski = ON
}
//#############################################################################################################
/*
Funkcja obslugujaca przelaczniki HAT. Jako argumenty podajemy:
bank : czyli numer ukladu CD4021 do ktorego podlaczony jest dany HAT
pinUp...pinL: wejscia CD4021 do ktorych podlaczone sa poszczegolne kierunki.
Funkcja zwraca wartosc kata, ktora bezposrednio mozna uzyc w bibliotece Joystick.h
Przyklad: HAT0 podlaczony jest do drugiego CD4021 w kolejnosci (bank 1) do pinow 0, 1 2 3
Numer HATa bank piny
| | |
Joystick.setHatSwitch(0,CD4021_odczytaj_hat(1, 0,1,2,3));
*/
int16_t CD4021_odczytaj_hat(uint8_t bank, uint8_t pinUp, uint8_t pinR, uint8_t pinDwn, uint8_t pinL)
{
uint8_t hat_index = 0;
if (bank > CD4021_liczba_ukladow) return -1; //zabezpieczenie
if (CD4021_bank_przyciskow_filtr[bank] & (1 << pinUp)) hat_index |= (1 << 0); else hat_index &= ~(1 << 0);
if (CD4021_bank_przyciskow_filtr[bank] & (1 << pinR)) hat_index |= (1 << 1); else hat_index &= ~(1 << 1);
if (CD4021_bank_przyciskow_filtr[bank] & (1 << pinDwn)) hat_index |= (1 << 2); else hat_index &= ~(1 << 2);
if (CD4021_bank_przyciskow_filtr[bank] & (1 << pinL)) hat_index |= (1 << 3); else hat_index &= ~(1 << 3);
return (int16_t)(pgm_read_word(&CD4021_hat_decoder[hat_index]));
}
//#############################################################################################################
/*
Pomocnicza funkcja do testowania stanow wejsc rejestrow CD4021.
Wywolana w petli glownej wydrukuje stan wejsc ukladow jest nastapi jakakolwiek zmiana.
Dane wysylane sa na sprzetowy port Serial1 - wymagane jest podlaczenie konwertera UART/USB:
konwerter GND - Arduino GND
konwerter RX - Arduino TX (pin 0)
*/
void CD4021_printSerial(void)
{
static uint8_t stanCD4021_hist[CD4021_liczba_ukladow];
for (uint8_t i = 0; i < CD4021_liczba_ukladow; i++)
{
if (CD4021_bank_przyciskow_filtr[i] != stanCD4021_hist[i])
{
Serial1.print(F("Bank#")); Serial1.print(i); Serial1.print(F("= "));
for (uint8_t mask = 0x80; mask; mask >>= 1)
{
Serial1.print(mask & CD4021_bank_przyciskow_filtr[i] ? '1' : '0');
}
Serial1.println();
}
stanCD4021_hist[i] = CD4021_bank_przyciskow_filtr[i];
}
}
//#############################################################################################################
void setup()
{
Serial1.begin(115200);
pinMode(PIN_SCK, OUTPUT);
pinMode(PIN_MOSI, OUTPUT);
pinMode(PIN_MISO, INPUT_PULLUP);
pinMode(PIN_LOAD, OUTPUT);
digitalWrite(PIN_LOAD, LOW); //LOAD ustawiony na zero powoduje, ze wejscia CD4021 sa caly czas "skanowane"
// Ustawiamy zakres wartosci dla osi analogowych
Joystick.setXAxisRange(0, 1023);
Joystick.setYAxisRange(0, 1023);
Joystick.setZAxisRange(0, 1023);
Joystick.setThrottleRange(0, 1023);
Joystick.setRudderRange(0, 1023);
Joystick.begin(false);
Timer1.initialize(15000); //15ms
Timer1.attachInterrupt(CD4021_odczytaj_wejscia); //funkcja wywolywana co 15ms: odczytuje i filtruje wejscia z lancucha CD4021
}
//#############################################################################################################
void loop()
{
// Dane z lancucha rjestrow CD4021 odbierane sa automatycznie w cyklicznych przerwaniach co 15ms
// Odklocanie zabiera 4 cykle, co daje w sumie 60ms.
//dla przykladu, obslugujemy pojedynczy przcisk, np nr 4 z banku 0
//Joystick.setButton(4,CD4021_odczytaj_stan_przycisku(0,4));
/* Przykladowo mamy podlaczone dwa CD4021:
uklad 0: 8 CD4021_bank_przyciskow
uklad 1: dwa HATy
*/
//odczyt 8 przyciskow z ukladu (bank) 0
for (int i = 0; i < 8; i++)
{
Joystick.setButton(i, CD4021_odczytaj_stan_przycisku(0, i));
}
//HAT0 podlaczony do wejsc 1,2,3,4 ukladu nr 1 (numeracja pinow od zera)
// bank, piny
Joystick.setHatSwitch(0, CD4021_odczytaj_hat(1, 0, 1, 2, 3));
//HAT1 podlaczony do wejsc 5,6,7,8 ukladu nr 1
// bank, piny
Joystick.setHatSwitch(1, CD4021_odczytaj_hat(1, 4, 5, 6, 7));
// CD4021_printSerial(); // w ramach podgladu stan wejsc wysylany jest na port Serial1 (115200 8N1)
/* Osie analogowe.
Dane z przetwornikow ADC sa usredniane (redukcja szumu) i ew. dodana
jest martwa strefa. Nastepnie tak obrobione dane wpisywane sa jako koncowe
wartosci dla osi analogowych joysticka
*/
ADC_odczytajOsie(); //zbiera dane z wejsc analogowych
#if (WLACZ_FILTR_OSI==1)
ADCLowPassFilter();
Joystick.setXAxis( ADC_filteredData[0] ); // os X = A0
Joystick.setYAxis( ADC_filteredData[1] ); // os Y = A1
Joystick.setZAxis( ADC_filteredData[2] ); // A2
Joystick.setRxAxis( ADC_filteredData[3] ); // A3
Joystick.setRyAxis( ADC_filteredData[4] ); // A6
Joystick.setRzAxis( ADC_filteredData[5] ); // A7
Joystick.setThrottle( ADC_filteredData[6] ); // A8
Joystick.setAccelerator( ADC_filteredData[7] ); // A9
#else
Joystick.setXAxis( ADC_rawData[0] ); // os X = A0
Joystick.setYAxis( ADC_rawData[1] ); // os Y = A1
Joystick.setZAxis( ADC_rawData[2] ); // A2
Joystick.setRxAxis( ADC_rawData[3] ); // A3
Joystick.setRyAxis( ADC_rawData[4] ); // A6
Joystick.setRzAxis( ADC_rawData[5] ); // A7
Joystick.setThrottle( ADC_rawData[6] ); // A8
Joystick.setAccelerator ( ADC_rawData[7] ); // A9
#endif
// w tym momencie mamy juz uaktualnione wszystkie dane i mozemy je wyslac do PC via USB
Joystick.sendState();
}