luni
Well-known member
Here you go@luni, perhaps EncoderTool needs it's own thread, don't want to highjack this one, have an idea for a new setter.
A few links:
Last edited:
Here you go@luni, perhaps EncoderTool needs it's own thread, don't want to highjack this one, have an idea for a new setter.
Either way is a counter, decade counter rolls over at different count.
Suggesting Ring counter if one exists. Component availability is a key design factor no matter the method.
n_col = n_pin/2
n_row = n_pin/4
n_enc = n_col X n_row = n_pin^2 / 8;
#include <MIDI.h>
MIDI_CREATE_INSTANCE(HardwareSerial, Serial3, MIDI);
//*****************************************************
#include "EncoderTool.h"
#define NUM_ENCS 8
using namespace EncoderTool;
constexpr unsigned QH_A = 17; //output pin QH of shift register B - Encoder B
constexpr unsigned QH_B = 16; //output pin QH of shift register A - Encoder A
constexpr unsigned pinLD = 4; //load pin for all shift registers)
constexpr unsigned pinCLK = 5; //clock pin for all shift registers
#define NUM_ENCS 8
EncPlex74165 encoders(NUM_ENCS, pinLD, pinCLK, QH_A, QH_B );
//****************************************************
struct EncData {
uint16_t CurVal; // Could be a Sysex byte, CC or NRPN value
uint16_t OldVal; // Could be a Sysex byte, CC or NRPN value
uint16_t MinVal;
uint16_t MaxVal;
uint16_t ControlNum; // For CC#, NRPN#. Relevant bits are masked in the MidiSend() function
byte MidiChannel;
int CommandType; // 0 = disabled, 1 = CC, 2= CC14bit, 3 = NRPN, 4 = Roland Single and 5 = Roland Two byte SYSEX messages
byte Address4; // Sysex address
byte Address3; //
byte Address2; //
byte Address1; //
};
EncData EncData[8] = {
{0, 0, 1, 127, 7, 1, 1, 0, 0, 0, 0},
{0, 0, 1, 127, 11, 2, 1, 0, 0, 0, 0},
{0, 0, 1, 255, 25, 3, 2, 0, 0, 0, 0},
{0, 0, 1, 511, 270, 4, 3, 0, 0, 0, 0},
{0, 0, 0, 1, 0, 1, 4, 0x00, 0x00, 0x00, 0x00}, // Panel Mode
{0, 0, 0, 4, 0, 1, 4, 0x30, 0x00, 0x10, 0x50}, // Tone 1 Filter Type
{0, 0, 0, 127, 0, 1, 4, 0x03, 0x00, 0x10, 0x51}, // Tone 1 Filter freq
{0, 0, 1, 255, 0, 1, 5, 0x00, 0x00, 0x00, 0x04}, // Patch Number
};
//************************************************************************************
// Roland JV series specific. These two are kind of templates. The first 5 bytes are sent verbatim
// and address the Synth.
// The next four bytes are the Parameter address, MSB - LSB and the following byte contains the 7-bit parameter value
byte JV_7BitSysex[12] = {0xF0, 0x41, 0x10, 0x6A, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF7};
// This one has two parameter value bytes, each with a range 0x0 - 0xF allowing param value range 0-255.
byte JV_8BitSysex[13] = {0xF0, 0x41, 0x10, 0x6A, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF7};
// The second last byte of both the above contain the Checksum value.
// Address, Value and Checksum are inserted in the MidiSend() function.
uint16_t sum;
uint16_t idx;
uint16_t checkSum;
//************************************************************************************
int Cable = 0;
elapsedMillis stopwatch = 0;
void setup() {
delay(1000);
MIDI.begin(MIDI_CHANNEL_OMNI);
MIDI.turnThruOff();
encoders.begin(CountMode::half);
for (unsigned i = 0; i < NUM_ENCS; i++) {
encoders[i].setLimits(EncData[i].MinVal, EncData[i].MaxVal);
encoders[i].setValue(EncData[i].CurVal);
EncData[i].OldVal = EncData[i].CurVal;
}
}
void loop() {
ReadEncoders();
SendMidi();
}
void sendMidi2() {
for (unsigned i = 0; i < NUM_ENCS; i++) {
if (EncData[i].CurVal != EncData[i].OldVal) {
EncData[i].OldVal = EncData[i].CurVal;
usbMIDI.sendControlChange(EncData[i].ControlNum, EncData[i].CurVal, EncData[i].MidiChannel);
MIDI.sendControlChange(EncData[i].ControlNum, EncData[i].CurVal, EncData[i].MidiChannel);
}
}
}
//********************************************************************
void SendMidi() {
for (int i = 0; i < NUM_ENCS; i++) {
if (EncData[i].CommandType != 0) {
if (EncData[i].CurVal != EncData[i].OldVal) {
EncData[i].OldVal = EncData[i].CurVal;
uint16_t CurrentValue = EncData[i].CurVal;
uint16_t _Control = EncData[i].ControlNum;
uint16_t value14 = (CurrentValue & 0b00000000000000000011111111111111);
uint16_t value21 = (CurrentValue & 0b00000000000111111111111111111111);
byte valueLSB = (CurrentValue & 0b00000000000000000000000001111111);
byte valueMSB = (CurrentValue & 0b00000000000000000011111110000000) >> 7;
uint16_t control14 = (_Control & 0b00000000000000000011111111111111);
byte controlLSB = (_Control & 0b00000000000000000000000001111111);
byte controlMSB = (_Control & 0b00000000000000000011111110000000) >> 7;
byte Channel = EncData[i].MidiChannel;
switch (EncData[i].CommandType) {
case 0:
// Do nothing.
break;
case 1:
usbMIDI.sendControlChange (controlLSB, valueLSB, Channel, Cable);
MIDI.sendControlChange (controlLSB, valueLSB, Channel);
break;
case 2:
usbMIDI.sendControlChange (controlLSB, valueMSB, Channel, Cable);
usbMIDI.sendControlChange (controlLSB + 32, valueLSB, Channel, Cable);
MIDI.sendControlChange (controlLSB, valueMSB, Channel);
MIDI.sendControlChange (controlLSB + 32, valueLSB, Channel);
break;
case 3:
usbMIDI.beginNrpn(control14, Channel, Cable);
usbMIDI.sendNrpnValue(valueLSB, Channel, Cable);
usbMIDI.endRpn(Channel, Cable);
MIDI.beginNrpn(control14, Channel);
MIDI.sendNrpnValue(valueLSB, Channel);
MIDI.endNrpn(Channel);
break;
case 4:
JV_7BitSysex[9] = valueLSB & 0b01111111;
JV_7BitSysex[8] = EncData[i].Address1 & 0b01111111;
JV_7BitSysex[7] = EncData[i].Address2 & 0b01111111;
JV_7BitSysex[6] = EncData[i].Address3 & 0b01111111;
JV_7BitSysex[5] = EncData[i].Address4 & 0b01111111;
sum = 0;
idx = 5;
checkSum = 0;
for (sum = 0; idx < 10; ++idx) {
sum += (byte)JV_7BitSysex[idx];
}
checkSum = 128 - ((sum % 128) & 0b01111111);
checkSum = (checkSum & 0b01111111);
JV_7BitSysex[10] = checkSum;
usbMIDI.sendSysEx(12, JV_7BitSysex, true, Cable);
MIDI.sendSysEx(12, JV_7BitSysex, true);
break;
case 5:
JV_8BitSysex[10] = EncData[i].CurVal & 0b00000000000000000000000000001111;
JV_8BitSysex[9] = (EncData[i].CurVal & 0b00000000000000000000000011110000) >> 4;
JV_8BitSysex[8] = EncData[i].Address1 & 0b01111111;
JV_8BitSysex[7] = EncData[i].Address2 & 0b01111111;
JV_8BitSysex[6] = EncData[i].Address3 & 0b01111111;
JV_8BitSysex[5] = EncData[i].Address4 & 0b01111111;
sum = 0;
idx = 5;
checkSum = 0;
for (sum = 0; idx < 11; ++idx) {
sum += (byte)JV_8BitSysex[idx];
}
checkSum = 128 - ((sum % 128) & 0b01111111);
checkSum = (checkSum & 0b01111111);
JV_8BitSysex[11] = (byte)checkSum;
usbMIDI.sendSysEx(13, JV_8BitSysex, true, Cable);
MIDI.sendSysEx(13, JV_8BitSysex, true);
break;
}
break;
}
}
}
}
//**************************************************************************************
void ReadEncoders() {
encoders.tick();
if (stopwatch > 10) // Look at sending Midi values every 10 ms
{
for (unsigned i = 0; i < NUM_ENCS; i++)
{
EncData[i].CurVal = encoders[i].getValue();
}
stopwatch = 0;
}
}
I was thinking of a simple shift register, but then I thought why bother with external parts, when a Teensy can directly read out the matrix even faster.
View attachment 28413
A quick calculation gives the following formulas for the number of encoders and the optimal matrix arrangement for a given number of available pins
where n_col is the number of columns, n_row the number of rows, n_enc the number of encoders and n_pin the number of available pins.Code:n_col = n_pin/2 n_row = n_pin/4 n_enc = n_col X n_row = n_pin^2 / 8;
Assuming communication with the main controller via UART (2 pins) and using 1 pin to signal when the encoder values changed we have 21 pins left on a T-LC. Thus, a LC can read out n_enc = 21*21/8 = 55 encoders as shown in the schematic above. A T4.1 has 39 pins available for the matrix which gives 190 encoders.
Here a very quick feasibility calculation regarding readout speed:
Let's assume some 100ns to read and process one pin (which is really long for a T4). Then, a complete scan of 190 encoders would be done in 0.1µs/pin X 2*190pins = 38µs. If we poll the matrix with >5kHz (which is fast for mechanical encoders) this would generate 5000 1/s * 38µs = 0.2 = 20% processor load only. All in all this does not look bad![]()
Thinking of doing a PCB, what would be a reasonable spacing between the encoders? Would it make sense to pre-cut or pre-mill the board such that one could break off an arbitrary number of encoder positions from the board to meet the needs of a project?
#include "Arduino.h"
#include "EncoderTool.h"
#include "DirectMux.h"
using namespace EncoderTool;
DirectMux<3, 5> matrix;
// define used pins
constexpr unsigned AR0 = 2, BR0 = 3, SR0 = 4; // row 0
constexpr unsigned AR1 = 5, BR1 = 6, SR1 = 7; // row 1
constexpr unsigned AR2 = 8, BR2 = 9, SR2 = 10; // row 2
constexpr unsigned C0 = 23, C1 = 22, C2 = 21, C3 = 20, C4 = 19; // columns
void onEncoderChanged(uint8_t channel, int value, int delta)
{
Serial.printf("CH%d: %d\n", channel, value);
}
void setup()
{
pinMode(LED_BUILTIN, OUTPUT);
matrix.begin({AR0, AR1, AR2}, {BR0, BR1, BR2}, {SR0, SR1, SR2}, {C0, C1, C2, C3, C4});
matrix.attachCallback(onEncoderChanged);
}
void loop()
{
matrix.tick();
}
#pragma once
#include "Multiplexed/EncPlexBase.h"
#include <array>
namespace EncoderTool
{
template <size_t rows, size_t cols>
class DirectMux : public EncPlexBase
{
// some typedefs for less verbose code
using cArr_t = std::array<uint_fast8_t, cols>;
using rArr_t = std::array<uint_fast8_t, rows>;
public:
DirectMux() : EncPlexBase(rows * cols) {}
void begin(rArr_t arPins, rArr_t brPins, rArr_t srPins, cArr_t cPins, CountMode mode = CountMode::quarter)
{
EncPlexBase::begin(mode); // setup the base class
this->arPins = arPins; // copy passed in pin numbers
this->brPins = brPins;
this->srPins = srPins;
this->cPins = cPins;
for (auto pin : arPins) pinMode(pin, INPUT_PULLUP);
for (auto pin : brPins) pinMode(pin, INPUT_PULLUP);
for (auto pin : srPins) pinMode(pin, INPUT_PULLUP);
for (auto pin : cPins)
{
pinMode(pin, OUTPUT);
digitalWriteFast(pin, HIGH); // board is implemented as active LOW
}
}
inline void tick() // call as often as possible
{
unsigned curEnc = 0;
for (auto cPin : cPins)
{
digitalWriteFast(cPin, LOW);
delayNanoseconds(500);
for (unsigned row = 0; row < rows; row++)
{
uint_fast8_t A = digitalReadFast(arPins[row]);
uint_fast8_t B = digitalReadFast(brPins[row]);
uint_fast8_t S = digitalReadFast(srPins[row]);
int delta = encoders[curEnc].update(A, B, S);
if (delta != 0 && callback != nullptr)
{
callback(curEnc, encoders[curEnc].getValue(), delta); // if something changed, invoke the callback
}
curEnc++;
}
digitalWriteFast(cPin, HIGH);
}
}
protected:
rArr_t arPins, brPins, srPins;
cArr_t cPins;
};
I like the idea, but the encoder tool is pretty much platform independent. So setting the counter to int64 would be quite some overhead for smaller boards. Best would be to make the counter type a template parameter which defaults to 'int'. One could then change the counter type if needed. Shouldn't be too difficult, but currently my time is super limited (currently renovating a house), so it might take some time...
Here the line which defines the type of the counter to 'int'
https://github.com/luni64/EncoderTo...e773756f9778d803762c22f/src/EncoderBase.h#L63
You can change this to int64 but you need to make sure to change e.g.getValue, setValue etc to reflect the change.
Here the line which defines the type of the counter to 'int'
https://github.com/luni64/EncoderTo...e773756f9778d803762c22f/src/EncoderBase.h#L63
You can change this to int64 but you need to make sure to change e.g.getValue, setValue etc to reflect the change.
using encCallback_t = void (*)(int value, int delta);
using encCallback_t = void (*)(int64_t value, int64_t delta);
clinker8 said:I guess that means, I have to maintain this for a while - at least until you update the library.
#include "EncoderTool.h"
using namespace EncoderTool;
//using Encoder = Encoder_tpl<int>; // this is already defined by the library
using Encoder64 = Encoder_tpl<int64_t>; // e.g. use a 64bit counter
using smallEnc = Encoder_tpl<uint8_t>; // e.g. use an unsigned 8bit counter (no negative values possible)
Encoder e1;
Encoder64 e2;
smallEnc e3;
void setup()
{
e1.begin(1, 2);
e2.begin(4, 5);
e3.begin(7, 8);
}
void loop()
{
if (e1.valueChanged()) Serial.printf("e1 (int32_t): %d\n", e1.getValue());
if (e2.valueChanged()) Serial.printf("e2 (int64_t): %lld\n", e2.getValue());
if (e3.valueChanged()) Serial.printf("e3 (uint8_t): %d\n", e3.getValue());
}
I pushed an experimental version with user selectable counter type to the GitHub repository https://github.com/luni64/EncoderTool/tree/counterType (use the branch "counterType").
The standard API did not change. I.e., the default counter is of type "int", regardless of how large that is for the controller you are using. E.g. for the Teensies "int" translates to "int32_t". Additionally you can use any integral type for the counter. This works for the interrupt based, the polled and the multiplexed encoders. Here a basic usage example showing how to use the standard 32bit encoder, a 64bit encoder and a tiny, unsigned 8bit encoder:
Code:#include "EncoderTool.h" using namespace EncoderTool; //using Encoder = Encoder_tpl<int>; // this is already defined by the library using Encoder64 = Encoder_tpl<int64_t>; // e.g. use a 64bit counter using smallEnc = Encoder_tpl<uint8_t>; // e.g. use an unsigned 8bit counter (no negative values possible) Encoder e1; Encoder64 e2; smallEnc e3; void setup() { e1.begin(1, 2); e2.begin(4, 5); e3.begin(7, 8); } void loop() { if (e1.valueChanged()) Serial.printf("e1 (int32_t): %d\n", e1.getValue()); if (e2.valueChanged()) Serial.printf("e2 (int64_t): %lld\n", e2.getValue()); if (e3.valueChanged()) Serial.printf("e3 (uint8_t): %d\n", e3.getValue()); }
@clinker8: I would very much appreciate if you could test it thoroughly before I pull it into the main branch
It usually is a good idea to git clone libraries. You can then switch between branches / versions without hassle.Been refusing library updates for a little bit. Would be good to keep everything up to date again.
Is everything else the same? Disable the callback using nullptr? What type is "delta"? I had to edit the type of my version of the library to be int64_t as well, when I was surprised with a very large positive number when the spindle reversed direction.
Good point, I need to change the callback types also. I'll update the repo as soon as this is working as well.
Encoder_tpl<int64_t> spindleEnc;
void onEncoderChanged(int64_t value, int64_t delta)
{
Serial.println(value);
}
void setup()
{
spindleEnc.begin(1, 2);
spindleEnc.attachCallback(onEncoderChanged);
}
void loop()
{
}
I updated the callback signatures (value and delta) to reflect the underlying counter type. Here an example for a 64 bit counter:
Code:Encoder_tpl<int64_t> spindleEnc; void onEncoderChanged(int64_t value, int64_t delta) { Serial.println(value); } void setup() { spindleEnc.begin(1, 2); spindleEnc.attachCallback(onEncoderChanged); } void loop() { }
I also cleaned up the API a bit and disabled unsigned types for the underlying counter since they might generate unexpected behavior.
In file included from /home/pi/Arduino/ELS/ELS.ino:1:0:
ELS.h:12: error: 'Encoder_tpl' does not name a type
Encoder_tpl<int64_t> SpindleEnc;
#include "Arduino.h"
//#include "QuadEncoder.h"
#include <EncoderTool.h>
#include "TeensyTimerTool.h"
#include "touchdisplay.h"
#include "threadchart.h"
using namespace TeensyTimerTool;
using namespace EncoderTool;
//using Encoder64 = Encoder_tpl<int64_t>; // e.g. use a 64bit counter
Encoder_tpl<int64_t> SpindleEnc;
Encoder_tpl<int64_t> DROZ_Enc;
Encoder_tpl<int64_t> DROX_Enc;
#define PUL 5
#define DIR 6
#define ENA 7
// Rotary Encoder Pins