add config_keybrd.h with typedef read_pins_t, read_pins_mask_t
This commit is contained in:
parent
04ab6ebe72
commit
48a3a41db1
@ -7,10 +7,17 @@ keybrd version 1.0.0 will be released when the public API is stable.
|
||||
|
||||
## [Unreleased][unreleased]
|
||||
|
||||
## [0.3.2] - 2016-06-21
|
||||
### Added
|
||||
config_keybrd.h for size configurations.
|
||||
RowScanner_SPI-ShiftRegisters for compact split keyboards up to 32 keys per matrix.
|
||||
LED_PinNumber for controlling indicator lights by pin number.
|
||||
|
||||
## [0.3.2] - 2016-06-10
|
||||
### Changed
|
||||
* Changed uC from scanning port arrays to scanning Arduino pins.
|
||||
Thereby added support for Arduino boards, Teensy 3, and Teensy LC micro controllers.
|
||||
* Changed uC from scanning port arrays to scanning Arduino pins, thereby adding support for:
|
||||
Arduino boards, Teensy 3, and Teensy LC micro controllers
|
||||
up to 31x31 matrix capability
|
||||
* Changed IOE from scanning port arrays to scanning single ports.
|
||||
* Moved scanner and debouncer into their own classes.
|
||||
|
||||
|
@ -2,16 +2,14 @@ planned_features is a view of where the keybrd project is headed.
|
||||
|
||||
Top priority
|
||||
============
|
||||
Add support for shift registers, for compact split keyboards up to 32 keys per matrix.
|
||||
MCP23S18 I/O expander with Serial Peripheral Interface (SPI)
|
||||
|
||||
Med priority
|
||||
============
|
||||
Add 16x16 matrix capability (currently limited to 8x8 matrices)
|
||||
Add matrix-to-layout mapping array (to decouple matrix from layout)
|
||||
|
||||
Low priority
|
||||
============
|
||||
Add matrix-to-layout mapping array (to decouple matrix from layout)
|
||||
|
||||
Update tutorials:
|
||||
* Currently tutorial sketches are obsolete and won't compile
|
||||
* Change tutorial sketches from teensy 2.0 and PCA9655E-D IOE to Teensy LC and MCP23018 IOE
|
||||
|
@ -1,12 +1,14 @@
|
||||
#ifndef DEBOUNCERINTERFACE_H
|
||||
#define DEBOUNCERINTERFACE_H
|
||||
|
||||
#include <config_keybrd.h>
|
||||
|
||||
/* DebouncerInterface is an interface class.
|
||||
debounce() takes rawSignal and returns debounced signal. Signals are bitwise.
|
||||
*/
|
||||
class DebouncerInterface
|
||||
{
|
||||
public:
|
||||
virtual uint8_t debounce(const uint8_t rawSignal, uint8_t& debounced)=0;
|
||||
virtual read_pins_t debounce(const read_pins_t rawSignal, read_pins_t& debounced)=0;
|
||||
};
|
||||
#endif
|
||||
|
@ -4,10 +4,10 @@ where each sample contains the switch states for a row of switches, one bit per
|
||||
|
||||
Debounce uses Dr. Marty's debounce algorithm from
|
||||
http://drmarty.blogspot.com.br/2009/05/best-switch-debounce-routine-ever.html
|
||||
I2C and TWI protocols do not include any Packet Error Checking (PEC).
|
||||
SPI, I2C, and TWI protocols do not include any Packet Error Checking (PEC).
|
||||
The goal of Marty's debounce routine is to reject spurious signals,
|
||||
which is useful when connecting split keyboards with a cable using I2C or TWI.
|
||||
Was tested on split keyboard with 3-meter long telephone wire to I/O expander
|
||||
which is useful when connecting split keyboards with a cable using SPI, I2C, or TWI.
|
||||
This class was tested on split keyboard with a 3-meter long telephone wire to I/O expander.
|
||||
|
||||
Dr. Marty's debounce algorithm:
|
||||
Periodically read samples and update the state when a number consecutive sample bits are equal.
|
||||
@ -23,11 +23,11 @@ There is a latency equal to SAMPLE_COUNT, between button press and debounced sig
|
||||
samples[SAMPLE_COUNT] is a ring buffer. samplesIndex is it's current write index.
|
||||
SAMPLE_COUNT is the number of consecutive equal samples needed to debounce.
|
||||
SAMPLE_COUNT is a macro because it defines samples[SAMPLE_COUNT] array size at compile time.
|
||||
SAMPLE_COUNT should be at lease 1.
|
||||
SAMPLE_COUNT is defined in config_keybrd.h and should be at lease 1.
|
||||
|
||||
SAMPLE_COUNT = 4 is very reliable for a keyboard.
|
||||
Keyboards with a long I2C wire or in environment with strong electromagnetic interference (EMI)
|
||||
may need a larger SAMPLE_COUNT for reliability.
|
||||
Split keyboards with a long connecting wire or in environment with
|
||||
strong electromagnetic interference (EMI) may need a larger SAMPLE_COUNT for reliability.
|
||||
*/
|
||||
#include "Debouncer_4Samples.h"
|
||||
|
||||
@ -35,11 +35,11 @@ may need a larger SAMPLE_COUNT for reliability.
|
||||
For parameters, 1 means pressed, 0 means released.
|
||||
For return, 1 means debounced changed.
|
||||
*/
|
||||
uint8_t Debouncer_4Samples::debounce(const uint8_t rawSignal, uint8_t& debounced)
|
||||
read_pins_t Debouncer_4Samples::debounce(const read_pins_t rawSignal, read_pins_t& debounced)
|
||||
{
|
||||
uint8_t previousDebounced; //bitwise, 1 means pressed, 0 means released
|
||||
uint8_t all_1 = ~0; //bitwise
|
||||
uint8_t all_0 = 0; //bitwise
|
||||
read_pins_t previousDebounced; //bitwise, 1 means pressed, 0 means released
|
||||
read_pins_t all_1 = ~0; //bitwise
|
||||
read_pins_t all_0 = 0; //bitwise
|
||||
|
||||
samples[samplesIndex] = rawSignal; //insert rawSignal into samples[] ring buffer
|
||||
|
||||
|
@ -2,21 +2,20 @@
|
||||
#define DEBOUNCER_4SAMPLES_H
|
||||
#include <Arduino.h>
|
||||
#include <inttypes.h>
|
||||
#include <config_keybrd.h>
|
||||
#include <DebouncerInterface.h>
|
||||
|
||||
#define SAMPLE_COUNT 4 //number of consecutive equal bits needed to change a debounced bit
|
||||
|
||||
/* Debouncer_4Samples
|
||||
Configuration: #define SAMPLE_COUNT in this header file.
|
||||
Configuration: #define SAMPLE_COUNT in config_keybrd.h
|
||||
*/
|
||||
class Debouncer_4Samples : public DebouncerInterface
|
||||
{
|
||||
private:
|
||||
uint8_t samples[SAMPLE_COUNT]; //bitwise, one bit per key, most recent readings
|
||||
read_pins_t samples[SAMPLE_COUNT]; //bitwise, one bit per key, most recent readings
|
||||
uint8_t samplesIndex; //samples[] current write index
|
||||
public:
|
||||
Debouncer_4Samples(): samplesIndex(0) {}
|
||||
virtual uint8_t debounce(const uint8_t rawSignal, uint8_t& debounced);
|
||||
virtual read_pins_t debounce(const read_pins_t rawSignal, read_pins_t& debounced);
|
||||
};
|
||||
#endif
|
||||
|
||||
|
@ -5,9 +5,9 @@ process() scans the row and calls any newly pressed or released keys.
|
||||
void RowBase::process()
|
||||
{
|
||||
//these variables are all bitwise, one bit per key
|
||||
uint8_t rowState; //1 means pressed, 0 means released
|
||||
uint16_t rowEnd; //1 bit marks positioned after last key of row
|
||||
uint8_t debouncedChanged; //1 means debounced changed
|
||||
read_pins_t rowState; //1 means pressed, 0 means released
|
||||
read_pins_mask_t rowEnd; //1 bit marks positioned after last key of row
|
||||
read_pins_t debouncedChanged; //1 means debounced changed
|
||||
|
||||
wait();
|
||||
rowState = scan(rowEnd);
|
||||
@ -56,11 +56,11 @@ pressRelease() calls key's press() or release() function if it was pressed or re
|
||||
Both parameters are bitwise.
|
||||
rowEnd bit marks positioned immediatly after last key of row.
|
||||
*/
|
||||
void RowBase::pressRelease(const uint16_t rowEnd, const uint8_t debouncedChanged)
|
||||
void RowBase::pressRelease(const read_pins_mask_t rowEnd, const read_pins_t debouncedChanged)
|
||||
{
|
||||
uint8_t isFallingEdge; //1 means falling edge
|
||||
uint8_t isRisingEdge; //1 means rising edge
|
||||
uint16_t rowMask; //bitwise, active col bit is 1 (same type as rowEnd)
|
||||
read_pins_t isFallingEdge; //bitwise, 1 means falling edge
|
||||
read_pins_t isRisingEdge; //bitwise, 1 means rising edge
|
||||
read_pins_t rowMask; //bitwise, active col bit is 1 (same type as rowEnd)
|
||||
uint8_t col; //index for ptrsKeys[col] array
|
||||
|
||||
//bit=1 if last debounced changed from 1 to 0, else bit=0
|
||||
|
@ -2,6 +2,7 @@
|
||||
#define ROWBASE_H
|
||||
#include <Arduino.h>
|
||||
#include <inttypes.h>
|
||||
#include <config_keybrd.h>
|
||||
#include <Key.h>
|
||||
|
||||
/* RowBase is an abstract base class.
|
||||
@ -14,14 +15,14 @@ class RowBase
|
||||
|
||||
virtual void keyWasPressed();
|
||||
protected:
|
||||
uint8_t debounced; //bitwise, 1 means pressed, 0 means released
|
||||
read_pins_t debounced; //bitwise, 1 means pressed, 0 means released
|
||||
|
||||
void wait();
|
||||
void pressRelease(const uint16_t rowEnd, const uint8_t debouncedChanged);
|
||||
void pressRelease(const read_pins_mask_t rowEnd, const read_pins_t debouncedChanged);
|
||||
public:
|
||||
RowBase(Key *const ptrsKeys[]) : ptrsKeys(ptrsKeys), debounced(0) { }
|
||||
virtual void process();
|
||||
virtual uint8_t scan(uint16_t& rowEnd)=0;
|
||||
virtual uint8_t debounce(const uint8_t rowState, uint8_t& debounced)=0;
|
||||
virtual read_pins_t scan(read_pins_mask_t& rowEnd)=0;
|
||||
virtual read_pins_t debounce(const read_pins_t rowState, read_pins_t& debounced)=0;
|
||||
};
|
||||
#endif
|
||||
|
@ -3,11 +3,12 @@
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <inttypes.h>
|
||||
#include <config_keybrd.h>
|
||||
|
||||
class RowScannerInterface
|
||||
{
|
||||
public:
|
||||
virtual uint8_t scan(uint16_t& rowEnd)=0;
|
||||
virtual read_pins_t scan(read_pins_mask_t& rowEnd)=0;
|
||||
};
|
||||
#endif
|
||||
|
||||
|
@ -39,9 +39,9 @@ https://www.arduino.cc/en/Reference/DigitalWrite
|
||||
https://www.arduino.cc/en/Reference/DigitalRead
|
||||
https://www.arduino.cc/en/Reference/Constants > Digital Pins modes: INPUT, INPUT_PULLUP, and OUTPUT
|
||||
*/
|
||||
uint8_t RowScanner_PinsArray::scan(uint16_t& rowEnd)
|
||||
read_pins_t RowScanner_PinsArray::scan(read_pins_mask_t& rowEnd)
|
||||
{
|
||||
uint8_t rowState = 0;
|
||||
read_pins_t rowState = 0; //bitwise
|
||||
rowEnd = 1;
|
||||
|
||||
//strobe row on
|
||||
|
@ -2,11 +2,14 @@
|
||||
#define ROWSCANNER_PINSARRAY_H
|
||||
#include <Arduino.h>
|
||||
#include <inttypes.h>
|
||||
#include <config_keybrd.h>
|
||||
#include <RowScannerInterface.h>
|
||||
#include <RowPort.h>
|
||||
#include <ColPort.h>
|
||||
|
||||
/* RowScanner_PinsArray class uses Arduino pin numbers (not port pin numbers).
|
||||
The maximum keys per row is 31, because Arduino's largest type is 32 bits and rowEnd consumes the last bit.
|
||||
Constructor is in RowScanner_PinsArray.cpp
|
||||
*/
|
||||
class RowScanner_PinsArray : public RowScannerInterface
|
||||
{
|
||||
@ -18,8 +21,8 @@ class RowScanner_PinsArray : public RowScannerInterface
|
||||
public:
|
||||
RowScanner_PinsArray(const uint8_t strobePin,
|
||||
const uint8_t readPins[], const uint8_t READ_PIN_COUNT);
|
||||
virtual uint8_t scan(uint16_t& rowEnd);
|
||||
uint8_t getRowState(uint16_t& rowEnd);
|
||||
virtual read_pins_t scan(read_pins_mask_t& rowEnd);
|
||||
//read_pins_t getRowState(read_pins_mask_t& rowEnd);
|
||||
};
|
||||
#endif
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
Strobes the row and reads the columns.
|
||||
Sets rowEnd and returns rowState.
|
||||
*/
|
||||
uint8_t RowScanner_PinsBitwise::scan(uint16_t& rowEnd)
|
||||
read_pins_t RowScanner_PinsBitwise::scan(read_pins_mask_t& rowEnd)
|
||||
{
|
||||
//strobe row on
|
||||
if (activeHigh)
|
||||
@ -39,7 +39,7 @@ rowEnd is a bitwise row mask, one col per bit, where active col bit is 1.
|
||||
At end of function, 1 bit marks place immediatly after last key of row.
|
||||
rowEnd is a larger type than portMask so that it can not overflow.
|
||||
*/
|
||||
uint8_t RowScanner_PinsBitwise::getRowState(uint16_t& rowEnd)
|
||||
uint8_t RowScanner_PinsBitwise::getRowState(read_pins_mask_t& rowEnd)
|
||||
{
|
||||
uint8_t rowState = 0; //bitwise, one key per bit, 1 means key is pressed
|
||||
uint8_t portMask; //bitwise, 1 bit is a colPortState position
|
||||
|
@ -7,6 +7,7 @@
|
||||
#include <ColPort.h>
|
||||
|
||||
/* RowScanner_PinsBitwise uses bit manipulation to read all pins of one port.
|
||||
The maximum keys per row is 8, because ports have a maximum of 8 pins each.
|
||||
*/
|
||||
class RowScanner_PinsBitwise : public RowScannerInterface
|
||||
{
|
||||
@ -20,7 +21,7 @@ class RowScanner_PinsBitwise : public RowScannerInterface
|
||||
: refRowPort(refRowPort), strobePin(strobePin),
|
||||
refColPort(refColPort) {}
|
||||
static const bool activeHigh; //logic level of strobe pin: 0=activeLow, 1=activeHigh
|
||||
virtual uint8_t scan(uint16_t& rowEnd);
|
||||
uint8_t getRowState(uint16_t& rowEnd);
|
||||
virtual read_pins_t scan(read_pins_mask_t& rowEnd);
|
||||
uint8_t getRowState(read_pins_mask_t& rowEnd);
|
||||
};
|
||||
#endif
|
||||
|
@ -9,10 +9,10 @@ void RowScanner_SPIShiftRegisters::begin()
|
||||
/*
|
||||
Sets rowEnd and returns rowState.
|
||||
*/
|
||||
uint8_t RowScanner_SPIShiftRegisters::scan(uint16_t& rowEnd)
|
||||
read_pins_t RowScanner_SPIShiftRegisters::scan(read_pins_mask_t& rowEnd)
|
||||
{
|
||||
//todo rowEnd, rowState, return int size depend on BYTE_COUNT, like in send()
|
||||
uint8_t rowState;
|
||||
read_pins_t rowState = 0;
|
||||
|
||||
digitalWrite(SS, LOW);
|
||||
digitalWrite(SS, HIGH);
|
||||
|
@ -8,6 +8,7 @@
|
||||
#include <ColPort.h>
|
||||
|
||||
/* RowScanner_SPIShiftRegisters reads all shift registers in a daisy chain.
|
||||
The maximum keys per row is 31, because Arduino's largest type is 32 bits and rowEnd consumes the last bit.
|
||||
//todo delete: Assumes only one row of shift registers is connected (no Slave Select).
|
||||
*/
|
||||
class RowScanner_SPIShiftRegisters : public RowScannerInterface
|
||||
@ -17,9 +18,9 @@ class RowScanner_SPIShiftRegisters : public RowScannerInterface
|
||||
const uint8_t BYTE_COUNT; //number of shift registers
|
||||
const uint8_t KEY_COUNT; //number of keys in row
|
||||
public:
|
||||
RowScanner_SPIShiftRegisters(const uint8_t SS, uint8_t BYTE_COUNT, uint16_t KEY_COUNT)
|
||||
RowScanner_SPIShiftRegisters(const uint8_t SS, uint8_t BYTE_COUNT, uint8_t KEY_COUNT)
|
||||
: SS(SS), BYTE_COUNT(BYTE_COUNT), KEY_COUNT(KEY_COUNT) {}
|
||||
virtual uint8_t scan(uint16_t& rowEnd);
|
||||
virtual read_pins_t scan(read_pins_mask_t& rowEnd);
|
||||
void begin();
|
||||
};
|
||||
#endif
|
||||
|
@ -1,11 +1,11 @@
|
||||
#include "Row_IOE.h"
|
||||
|
||||
uint8_t Row_IOE::scan(uint16_t& rowEnd)
|
||||
read_pins_t Row_IOE::scan(read_pins_mask_t& rowEnd)
|
||||
{
|
||||
return scanner.scan(rowEnd);
|
||||
}
|
||||
|
||||
uint8_t Row_IOE::debounce(const uint8_t rowState, uint8_t& debounced)
|
||||
read_pins_t Row_IOE::debounce(const read_pins_t rowState, read_pins_t& debounced)
|
||||
{
|
||||
return debouncer.debounce(rowState, debounced);
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ class Row_IOE : public RowBase
|
||||
Row_IOE( RowPort& refRowPort, const uint8_t strobePin,
|
||||
ColPort& refColPort, Key *const ptrsKeys[])
|
||||
: RowBase(ptrsKeys), scanner(refRowPort, strobePin, refColPort) { }
|
||||
uint8_t scan(uint16_t& rowEnd);
|
||||
uint8_t debounce(const uint8_t rowState, uint8_t& debounced);
|
||||
read_pins_t scan(read_pins_mask_t& rowEnd);
|
||||
read_pins_t debounce(const read_pins_t rowState, read_pins_t& debounced);
|
||||
};
|
||||
#endif
|
||||
|
@ -5,12 +5,12 @@ void Row_ShiftRegisters::begin()
|
||||
scanner.begin();
|
||||
}
|
||||
|
||||
uint8_t Row_ShiftRegisters::scan(uint16_t& rowEnd)
|
||||
read_pins_t Row_ShiftRegisters::scan(read_pins_mask_t& rowEnd)
|
||||
{
|
||||
return scanner.scan(rowEnd);
|
||||
}
|
||||
|
||||
uint8_t Row_ShiftRegisters::debounce(const uint8_t rowState, uint8_t& debounced)
|
||||
read_pins_t Row_ShiftRegisters::debounce(const read_pins_t rowState, read_pins_t& debounced)
|
||||
{
|
||||
return debouncer.debounce(rowState, debounced);
|
||||
}
|
||||
|
@ -29,10 +29,10 @@ class Row_ShiftRegisters : public RowBase
|
||||
RowScanner_SPIShiftRegisters scanner;
|
||||
Debouncer_4Samples debouncer;
|
||||
public:
|
||||
Row_ShiftRegisters(const uint8_t SS, uint8_t BYTE_COUNT, Key *const ptrsKeys[], uint16_t KEY_COUNT)
|
||||
Row_ShiftRegisters(const uint8_t SS, uint8_t BYTE_COUNT, Key *const ptrsKeys[], uint8_t KEY_COUNT)
|
||||
: RowBase(ptrsKeys), scanner(SS, BYTE_COUNT, KEY_COUNT) { }
|
||||
void begin();
|
||||
uint8_t scan(uint16_t& rowEnd);
|
||||
uint8_t debounce(const uint8_t rowState, uint8_t& debounced);
|
||||
read_pins_t scan(read_pins_mask_t& rowEnd);
|
||||
read_pins_t debounce(const read_pins_t rowState, read_pins_t& debounced);
|
||||
};
|
||||
#endif
|
||||
|
@ -1,11 +1,11 @@
|
||||
#include "Row_uC.h"
|
||||
|
||||
uint8_t Row_uC::scan(uint16_t& rowEnd)
|
||||
read_pins_t Row_uC::scan(read_pins_mask_t& rowEnd)
|
||||
{
|
||||
return scanner.scan(rowEnd);
|
||||
}
|
||||
|
||||
uint8_t Row_uC::debounce(const uint8_t rowState, uint8_t& debounced)
|
||||
read_pins_t Row_uC::debounce(const read_pins_t rowState, read_pins_t& debounced)
|
||||
{
|
||||
return debouncer.debounce(rowState, debounced);
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ class Row_uC : public RowBase
|
||||
Row_uC(const uint8_t strobePin, const uint8_t readPins[], const uint8_t READ_PIN_COUNT,
|
||||
Key *const ptrsKeys[])
|
||||
: RowBase(ptrsKeys), scanner(strobePin, readPins, READ_PIN_COUNT) { }
|
||||
uint8_t scan(uint16_t& rowEnd);
|
||||
uint8_t debounce(const uint8_t rowState, uint8_t& debounced);
|
||||
read_pins_t scan(read_pins_mask_t& rowEnd);
|
||||
read_pins_t debounce(const read_pins_t rowState, read_pins_t& debounced);
|
||||
};
|
||||
#endif
|
||||
|
35
src/config_keybrd.h
Normal file
35
src/config_keybrd.h
Normal file
@ -0,0 +1,35 @@
|
||||
#ifndef CONFIG_KEYBRD_H
|
||||
#define CONFIG_KEYBRD_H
|
||||
#include <inttypes.h>
|
||||
|
||||
/* size of read_pins_t and read_pins_mask_t depends on the maximum number of pins scanned by RowScanner.
|
||||
By default, read_pins_t and read_pins_mask_t are set to the largest type.
|
||||
If your 8-bit AVR is running low on memory, using a smaller type saves SRAM.
|
||||
Using smaller types on a 32-bit uC (Teensy LC) would accomplish nothing.
|
||||
*/
|
||||
|
||||
/* Uncomment a typedef read_pins_t that covers all col pins of the RowScanner object with the most col pins i.e.
|
||||
For RowScanner_PinsArray, RowScanner_PinsArray::READ_PIN_COUNT
|
||||
For RowScanner_SPIShiftRegisters, RowScanner_SPIShiftRegisters::KEY_COUNT
|
||||
For RowScanner_PinsBitwise, cover the last 1 bit in RowScanner_PinsBitwise::strobePin
|
||||
*/
|
||||
typedef uint8_t read_pins_t;
|
||||
//typedef uint16_t read_pins_t;
|
||||
//typedef uint32_t read_pins_t;
|
||||
|
||||
/* read_pins_mask_t is only used for rowEnd, which extends one bit beyond the last col pin.
|
||||
uncomment typedef that covers one bit beyond the last col pin.
|
||||
This could be the same typedef as read_pins_t, or the next larger typedef.
|
||||
*/
|
||||
typedef uint8_t read_pins_mask_t;
|
||||
//typedef uint16_t read_pins_mask_t;
|
||||
//typedef uint32_t read_pins_mask_t;
|
||||
|
||||
/* SAMPLE_COUNT = 4 is very reliable for a keyboard.
|
||||
Split keyboards with a long connecting wire or in environment with
|
||||
strong electromagnetic interference (EMI) may need a larger SAMPLE_COUNT for reliability.
|
||||
SAMPLE_COUNT is used in Debouncer_Samples.h
|
||||
*/
|
||||
#define SAMPLE_COUNT 4 //number of consecutive equal bits needed to change a debounced bit
|
||||
|
||||
#endif
|
@ -7,8 +7,8 @@ All the tutorial example sketches run on breadboard keyboards that have 2 to 8 k
|
||||
Breadboard keyboards have row-column matrices and diodes just like the big keyboards.
|
||||
|
||||
A breadboard is the easiest way to learn keyboard electronics.
|
||||
A novice won't get everything right the first time.
|
||||
It's easy to get some detail wrong with electronics.
|
||||
You won't get everything right the first time.
|
||||
There is a learning curve.
|
||||
Compared to PCBs, breadboard keyboards are easier to learn on because:
|
||||
* Mistakes are easily corrected; no soldering and desoldering
|
||||
|
Reference in New Issue
Block a user