Forum Rule: Always post complete source code & details to reproduce any issue!
Results 1 to 7 of 7

Thread: EncoderTool

  1. #1
    Senior Member
    Join Date
    Apr 2014
    Location
    Germany
    Posts
    1,836

    EncoderTool

    Quote Originally Posted by MatrixRat View Post
    @luni, perhaps EncoderTool needs it's own thread, don't want to highjack this one, have an idea for a new setter.
    Here you go :-)

    A few links:
    Last edited by luni; 05-16-2022 at 10:47 AM.

  2. #2
    Senior Member
    Join Date
    Apr 2014
    Location
    Germany
    Posts
    1,836
    Inspired by this https://forum.pjrc.com/threads/70233...ontrol-surface and the apparent lack of 23S17 IO Expanders I was thinking of yet another encoder multiplexing scheme which might handle the readout of the required 150+ encoders.

    It uses the 74HC4040 (https://www.ti.com/product/CD74HC4040). This is a 12 stage counter with attached multiplexer. I.e. on each count pulse it sets one (the next) of its 12 outputs to high:
    Click image for larger version. 

Name:	Screenshot 2022-05-16 174639.png 
Views:	7 
Size:	39.9 KB 
ID:	28386

    The idea is to use the outputs of the 4040 to supply the common pins of 12 encoders. The A/B/SW pins of the encoders can then be 'ored' with simple diodes. The controller would generate a count pulse which activates one of the encoders, read out the A/B/SW pins and supply the levels to the library as usual. Potentially this method should be much faster than reading out an IO Expander or using the slow analog multiplexers. The readout time should only be limited by the speed of the counter (>80MHz) and the digitalReadFast speed (~10ns).

    Count and reset pulse can be shared by many counters. Each counter requires 2 (3 with switch) pins on the Teensy. So, with a T4.1 one could control some 20 counters which would be 240 encoders. CLK and Reset would probably need a buffer for that amount of counters.

    Here a first draft of a schematic supporting 24 encoders (pdf attached)

    Click image for larger version. 

Name:	Screenshot 2022-05-16 175246.png 
Views:	14 
Size:	80.6 KB 
ID:	28388

    Any feedback on the schematic? Did I overlook something? Better ideas?

    Ups, looks like I misinterpreted the datasheet of the 4040. It seems not to set one pin high at a time like a decade counter. Back to the drawing board...
    Attached Files Attached Files
    Last edited by luni; 05-16-2022 at 04:51 PM.

  3. #3
    Senior Member
    Join Date
    Aug 2019
    Location
    Melbourne Australia
    Posts
    304
    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.

  4. #4
    Senior Member
    Join Date
    Apr 2014
    Location
    Germany
    Posts
    1,836
    Quote Originally Posted by MatrixRat View Post
    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.
    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.

    Click image for larger version. 

Name:	Screenshot 2022-05-18 191616.jpg 
Views:	22 
Size:	169.8 KB 
ID:	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

    Code:
    n_col = n_pin/2
    n_row = n_pin/4
    n_enc = n_col X n_row = n_pin^2 / 8;
    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.

    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?
    Attached Files Attached Files

  5. #5
    Senior Member
    Join Date
    Aug 2019
    Location
    Melbourne Australia
    Posts
    304
    Yeah, mechanicals. Currectly using 19mm grid to keep the whole thing compact and portable, no knobs.

    End use case is cutting loose on a Synth so with that spacing is easy to nudge a neighbor and mess with the flow. Migrating from dev to use-case will be looking at maybe 35mm grid with knobs.

    I like the snap up board idea. A user can get up and running quickly. Couple of thoughts:-

    Reasonably large pads, hole size = desolder with minimal damage. Metalwork floating in case user wants to play with cap sense.

    Serial ports can get gobbled up quick with Midi so usb comms with host of main MCU is likely a good tool in the box.

    Down side is would be easy to get totally lost on it as a user interface.

    A 16 encoder modular approach with a means to set a module's address is worthy of thought.
    Must do 100Km shopping trek, good time to think.

    My idea for another setter is for a means to change encoder resolution on-the-fly. Have it working in external code with SetDeadzone(x) where x = number of counts to skip before value change. Does a great emulation of a rotary switch with 96CPR. Bear in mind that a preset change may see the same physical encoder at full resolution. Have not managed to kludge it into the library yet.

  6. #6
    Senior Member
    Join Date
    Apr 2014
    Location
    Germany
    Posts
    1,836
    I did a draft of a Encoder Matrix Proto board. The idea is that one can start developing without the tedious wiring of a lot of encoders. After development the board can be easily cut or broken or extended to fit the mechanic design of the device. The board accepts any ARM Teensy board. With a T4.1 it should be able to support some 190 Encoders.


    Here some pictures:
    Click image for larger version. 

Name:	Screenshot 2022-05-22 174538.png 
Views:	9 
Size:	37.8 KB 
ID:	28444

    Click image for larger version. 

Name:	Screenshot 2022-05-22 174207.png 
Views:	10 
Size:	481.5 KB 
ID:	28445

    Click image for larger version. 

Name:	Screenshot 2022-05-22 174229.png 
Views:	6 
Size:	266.8 KB 
ID:	28446

    Click image for larger version. 

Name:	Screenshot 2022-05-22 174503.png 
Views:	6 
Size:	9.2 KB 
ID:	28447

    Maybe this is of use for someone.

  7. #7
    Senior Member
    Join Date
    Aug 2019
    Location
    Melbourne Australia
    Posts
    304
    Ears have been burning again, been re-writing project codebase. Noted a thread about encoders and Midi.
    Here is the essence of what my project does with EncPlex. Variations tested on T3.2 - T4.1, Usb Type = MIDI. Messages are sent concurrently via usbMIDI and MIDI.


    Code:
    #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;
      }
    }

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •