pimoroni/trackball-python

Arduino support?

Opened this issue ยท 14 comments

You specifically advertise this as being "perfect for adding navigation or control to your Raspberry Pi or Arduino projects." Yet you have zero Arduino support for it, not even a basic listing of I2C registers and how to use them unless you're prepared to reverse engineer the python library. An Arduino library would be nice but in the absence of that could you as least document the protocol!

To be frank the Python library is 133 lines of code, very concise and is about the most accurate documentation you're likely to see. I can't spare the time to transcribe it into text and I think it'd be a wasted effort since anyone could- I'm pretty certain- find & replace over the Python code to produce a half decent Arduino library.

I mean the registers are all there at the top:

I2C_ADDRESS = 0x0A
I2C_ADDRESS_ALTERNATIVE = 0x0B
CHIP_ID = 0xBA11
VERSION = 1
REG_LED_RED = 0x00
REG_LED_GRN = 0x01
REG_LED_BLU = 0x02
REG_LED_WHT = 0x03
REG_LEFT = 0x04
REG_RIGHT = 0x05
REG_UP = 0x06
REG_DOWN = 0x07
REG_SWITCH = 0x08
MSK_SWITCH_STATE = 0b10000000
REG_USER_FLASH = 0xD0
REG_FLASH_PAGE = 0xF0
REG_INT = 0xF9
MSK_INT_TRIGGERED = 0b00000001
MSK_INT_OUT_EN = 0b00000010
REG_CHIP_ID_L = 0xFA
RED_CHIP_ID_H = 0xFB
REG_VERSION = 0xFC
REG_I2C_ADDR = 0xFD
REG_CTRL = 0xFE
MSK_CTRL_SLEEP = 0b00000001
MSK_CTRL_RESET = 0b00000010
MSK_CTRL_FREAD = 0b00000100
MSK_CTRL_FWRITE = 0b00001000

The only unusual part is the 5 registers that describe left/right/up/down/switch- these contain values describing the amount of left/right/up/down the ball has moved since the last read so X is right-left and Y is down-up.

The switch register has a 7bit count of the number of presses there have been since the last update, and the MSB (MSK_SWITCH_STATE) indicates the current switch state (pressed/released).

Right, now to find a guillotine and whoever mentioned Arduino in the product description! (those two things are totally unrelated honest)

wez commented

@Gadgetoid would you mind describing how the interrupt pin is intended to be used? Is the intent that reading the trackball state clears the interrupt pin, or is the application expected to update the interrupt register after reading the state? Is the interrupt pin high or low when the interrupt register triggered flag is set?
Could you also briefly describe what the control fread and fwrite flags are for?

The interrupt pin is active-low and, from what I can tell, is cleared on read.

I haven't the foggiest clue what FREAD and FWRITE are or were, but they might have been a vestige of when flash IO was a little more transparent to the user. I'd say you can comfortably ignore them.

It's on our TODO list to write a Pico SDK driver for this board, so perhaps that will provide a better reference for Arduino code.

wez commented

Thanks; the main reason I ask about the interrupts is that the device seems to continually assert or toggle (I haven't gotten around to printf debugging precisely which) the interrupt status even when there is no movement on the trackball; I wonder if there is a mode to adjust this; perhaps the sleep control can help?

wez commented

... I had mis-transcribed the interrupt register as 0x9 instead of 0xf9 and now things make more sense!

This is very much an odd duck of a breakout- I appreciate you taking the time to grapple with it!

@wez any luck working with the trackball? I'm trying to figure out my first i2c project with it and an stm32duino (bluepill)
If you've pushed your code anywhere do you mind if I take a look?

If you shim I2C with your own implementation, or just replace the function calls, then you should be able to use this code: https://github.com/pimoroni/pimoroni-pico/blob/0bda2abd2acd4269725efb81ffd3f33621a8e176/drivers/trackball/trackball.cpp

If you shim I2C with your own implementation, or just replace the function calls, then you should be able to use this code: https://github.com/pimoroni/pimoroni-pico/blob/0bda2abd2acd4269725efb81ffd3f33621a8e176/drivers/trackball/trackball.cpp

Thanks for sharing :) I'm on my first i2c and stm project so I'll see if I can figure this out :)
Cheers for that! @Gadgetoid

Making some progress;
One thing I'm not sure of (I've been able to get the light working)
The example code doesn't mention but can you read the current value of the LED register? or do I need the masterI2C device to just keep track?

Thanks!
-F

Any of you use pullup resistors with this?
Getting intermittent failures on i2c; trying to determine if it's my wiring. I'm almost ready to share what I've got with a bit more tweaking

wez commented

I added pullups on my i2c bus; I can't remember what resistor value I ended up using and I'm too lazy to open up the case to look it up; sorry :-)

I added pullups on my i2c bus; I can't remember what resistor value I ended up using and I'm too lazy to open up the case to look it up; sorry :-)

No worries! thanks for verification; which controller board did you end up using? and is your code open?

wez commented

I played with an atsamd21 but am using an nrf52840. The internal pullup strength of those two are different, which is one of the reasons that I ended up added external pullups!

The overall code for my project isn't currently pushed anywhere, but the trackball bits are fairly self-contained:

  • Call Trackball.begin() to set it up
  • Call Tracball.process() in your loop function to read and apply the state.
  • You'll need to adapt the logic that generates the mouse reports to your application
  • Trackball.mode controls whether it generates scroll wheel or mouse movement events
#pragma once
#include "Manuform.h"

class PimoroniTrackball {
public:
  enum Reg {
    LedRed = 0,
    LedGreen = 1,
    LedBlue = 2,
    LedWhite = 3,
    Left = 4,
    Right = 5,
    Up = 6,
    Down = 7,
    Switch = 8,
    UserFlash = 0xd0,
    FlashPage = 0xf0,
    Int = 0xf9,
    ChipIdLow = 0xfa,
    ChipIdHigh = 0xfb,
    Version = 0xfc,
    I2CAddr = 0xfd,
    Ctrl = 0xfe,
  };

  enum CtrlMask {
    Sleep = 1,
    Reset = 2,
    FRead = 4,
    FWrite = 8,
  };

  enum IntMask {
    Triggered = 1,
    OutputEnable = 2,
  };

  enum SwitchStateMask {
    Pressed = 0x80,
  };

  enum MouseMode {
    ScrollWheel,
    Pointer,
  };

  static const constexpr uint16_t kChipId = 0xba11;
  static const constexpr uint8_t kAddress = 0x0a;
  static const constexpr int kInterruptPin =
#ifdef ARDUINO_PARTICLE_XENON
      PIN_D10
#else
      PIN_SERIAL1_RX
#endif
      ;

  static const constexpr bool kUseInterrupt = true;

  void begin() {
    setRegister(Reg::Ctrl, CtrlMask::Reset);
    delay(10);
    pinMode(kInterruptPin, INPUT);
    digitalWrite(kInterruptPin, LOW); // Disable pull up

    if (kUseInterrupt) {
      attachInterrupt(digitalPinToInterrupt(kInterruptPin),
                      PimoroniTrackball::serviceInterrupt,
                      FALLING);
    }

    auto mask = getRegister(Reg::Int);
    setRegister(Reg::Int, mask | IntMask::OutputEnable);
  }

  uint8_t getRegister(Reg reg) {
    Wire.beginTransmission(kAddress);
    Wire.write(uint8_t(reg));
    Wire.endTransmission();
    return Wire.read();
  }

  void setRegister(Reg reg, uint8_t value) {
    Wire.beginTransmission(kAddress);
    uint8_t data[2] = {reg, value};
    Wire.write(data, sizeof(data));
    Wire.endTransmission();
  }

  // https://github.com/pimoroni/trackball-python/issues/5#issuecomment-607951295
  struct State {
    uint8_t left;
    uint8_t right;
    uint8_t up;
    uint8_t down;
    // The number of presses since the last read.
    // SwitchStateMask::Pressed indicates whether it is currently down
    uint8_t button;
    // Corresponds to SwitchStateMask::Pressed from the button field
    bool pressed;

    bool isEmpty() {
      return left == 0 && right == 0 && up == 0 && down == 0 && !pressed;
    }

    int8_t x() {
      return int16_t(right) - int16_t(left);
    }
    int8_t y() {
      return int16_t(down) - int16_t(up);
    }

    void clear() {
      left = 0;
      right = 0;
      up = 0;
      down = 0;
      button = 0;
      pressed = false;
    }
  };

  State read() {
    Wire.beginTransmission(kAddress);
    Wire.write(uint8_t(Reg::Left));
    Wire.endTransmission();

    State state;
    Wire.requestFrom(kAddress, 5, true);
    state.left = Wire.read();
    state.right = Wire.read();
    state.up = Wire.read();
    state.down = Wire.read();
    state.button = Wire.read();
    state.pressed = (state.button & SwitchStateMask::Pressed) != 0;
    state.button &= ~SwitchStateMask::Pressed;

    return state;
  }

  static void serviceInterrupt();

  bool wasInterrupted() {
    if (kUseInterrupt) {
      return interupted;
    }
    return digitalRead(kInterruptPin) == LOW;
  }

  void process() {
    if (wasInterrupted()) {
      updateState();
      applyToMouse();
    } else if (!lastState.isEmpty()) {
      MouseReport empty;
      ConnectedHost::active->mouseReport(empty);
      lastState.clear();
    }
  }

  void updateState() {
    interupted = false;
    lastState = read();
  }

  void applyToMouse() {
    if (lastState.isEmpty()) {
      return;
    }
    cycle();

    MouseReport report;
    report.buttons = lastState.pressed ? 1 : 0;
    switch (mode) {
    case MouseMode::ScrollWheel:
      report.wheel = -lastState.y();
      break;
    case MouseMode::Pointer:
      report.x = accelerated(lastState.x());
      report.y = accelerated(lastState.y());
      break;
    }
    ConnectedHost::active->mouseReport(report);
  }

  static int8_t pow(int8_t v, uint8_t n) {
    int32_t b = int32_t(v);
    int32_t scaled = 3;
    bool preservedSign = n & 1;

    while (n--) {
      scaled *= b;
    }

    if (!preservedSign) {
      if (v < 0) {
        scaled = -scaled;
      }
    }

    if (scaled > 127) {
      return 127;
    }
    if (scaled < -127) {
      return -127;
    }
    return int8_t(scaled);
  }

  static int8_t accelerated(int8_t v) {
    return pow(v, 3);
  }

  void cycle() {
    uint8_t rgbw[5] = {Reg::LedRed, 0, 0, 0, 0};
    switch (++ledState) {
    case 1:
      rgbw[2] = 0x7f;
      break;
    case 2:
      rgbw[1] = 0x7f;
      break;
    case 3:
      rgbw[3] = 0x7f;
      ledState = 0;
      break;
    }
    Wire.beginTransmission(kAddress);
    Wire.write(rgbw, sizeof(rgbw));
    Wire.endTransmission();
  }

  volatile bool interupted{false};
  State lastState;
  volatile int ledState{0};
  MouseMode mode{MouseMode::ScrollWheel};
};

PimoroniTrackball Trackball;

void PimoroniTrackball::serviceInterrupt() {
  Trackball.interupted = true;
}