HDD Motor Scrollwheel - working but room for improvement

Hi all,

I have my project working but there is room for improvement. I'm posting my project here "tutorial style" for anyone interested in building it. At the same time it provides insight into the workings that might help answering question about improvements.

I'll post my questions as a reply.

Disclaimer: I built this project as described and it works for me. I'm not claiming it's 100% perfect and flawless, but it works.

What does it do and why?
I use a Wacom tablet and I missed a convenient scrolling device. So I decided to build one from a hard disk motor. When turning the motor by hand it generates pulses on the leads. These pulses are read into an Arduino Pro Micro which sends the scroll commands over USB using the mouse library.

What do you need?
-- A hard disk motor with 4 leads
-- An LM324 quad opamp to amplify motor pulses to logic level
-- An Arduino Pro Micro or another board with a 32U4 chip

Hooking it up.
My circuit/schematic drawing skills might be a bit odd but you should get the idea

NOTE: When hooking up the leads it does not matter which lead goes to which pin. The sequence can easily be changed in the code. Just make sure to find the 3 signal pins on the motor and connect these to pins 3, 5 and 10 on the LM324. To find the signal pins, get your multimeter, measure resistance and google a bit.

-- Turning the motor will generate millivolt pulses on leads A, B and C
-- The negative inputs on the LM324 are tied to ground
-- If a pulse on a lead is only slightly above ground the LM324 will "amplify" it to a logic level on the corresponding output (3.3 or 5V depending on your board)
-- The logic level triggers an interrupt and the code will process it. On the Pro Micro I used a library to use regular inputs as interrupt pins. Not exacly sure why pin 7 didn't work but 16 is fine too.

The signals.
First you need to find the right sequence. To do so use this sketch:
HDDScrollwheel__PinSequence.ino (705 Bytes)
In this sketch, change around the pin numbers until the output is:
...4,6,2,3,1,5,4,6... for one direction and
...6,4,5,1,3,2,6,4...(reversed) for the other direction

NOTE: (Counter)ClockWise, scrolling up down could be just the other way around for you. It depends on the pin sequence and code, just fiddle around with it.

Now for the processing. If you turn the motor at a good speed the signals can be visualized as follows:


When a signal on one of the leads rises or falls, an interrupt is triggered. Immediately after the interrupt the binary state of the leads is converted to an integer.

So if the current state is 3, the previous state was 2 and the state before that was 6
or T0=3 and T-1=2 and T-2=6 >> Your motor is making a valid spin.

The way my code works:
-- The current state, previous state and the state before that are stored in an array
-- The current state is shifted in at the back, pushing values to the left
-- An integer is calculated from that array, for example:
{3,2,6} > 100 * 3 + 10 * 2 + 6 = 326, This would be a valid sequence when rotating
-- The integer (sequence) is check against valid sequences stored in constant arrays for CW and CCW
-- There are 6 valid sequences for each direction:
{326,264,645,451,513,132} and
{231,315,154,546,462,623}

I found that storing 3 values works fine. Storing only 2 can lead to irregular behavior. Especially when just slightly moving the disc or rotating too slowly.

That is the basic concept. This is the complete code:
HDD_Motor_Scrollwheel.ino (4.9 KB)

View the complete code in the next reply.

Thanks for making it this far!

if(you think you have tips on improvement or different concepts)
{
   pleasePost();
}

An improvement I would like to make is the scrolling.

Currently:
My scrollwheel behaves like a mouse scrolling, jumpy and discrete

I also have my Wacom tablet. If I put that into touch mode and scroll with two fingers, I get very smooth and precise scrolling. I would really like to implement that.

I have been reading about USB HID, HID descriptors and possibly changing the HID descriptor of the mouse library. But I'm totally lost on that subject, does anyone have a lead?

I would like to send smooth scrolling commands over USB, can that be done?

...
...
This is the complete code for now:


#include <Mouse.h>                                    // To emulate a USB mouse
#include <PinChangeInterrupt.h>                       // Library to use NON-interrupt pins as interrupt pins

const byte L1 = 16;                                   // HDD Motor > LM324 > arduino pins
const byte L2 = 9; 
const byte L3 = 8;

int currentState;                                     // Stores the decimal value derived from binary status of L1 L2 L3
const int numOfStatesToSave = 3;                      // Number of states to save
int previousStates[numOfStatesToSave];
int combinedStates;                                   // An integer that combines the current and previous states in one value
int scrollDir;                                        // Scrolling direction
int pulseCount;
int multiplier;    

const int validCW[]  = {326,264,645,451,513,132};     // Arrays to check correct rotation ClockWise and CounterClockWise 
const int validCCW[] = {231,315,154,546,462,623};     // Checking sequence also prevents irregular scrolling behaviour
   

unsigned long peviousPulse = 0;
unsigned long pulseInterval;                          // Interval between interrupts


void setup()
{ 
  pinMode(L1, INPUT);                                         // Set pins to inputs
  pinMode(L2, INPUT);
  pinMode(L3, INPUT);

  pinMode(LED_BUILTIN_TX,INPUT);                              // Turn off onboard TX/RX leds
  pinMode(LED_BUILTIN_RX,INPUT);
  
  attachPCINT(digitalPinToPCINT(L1), handlePulse, CHANGE);    // Attach interrupts for rising and falling edges
  attachPCINT(digitalPinToPCINT(L2), handlePulse, CHANGE);
  attachPCINT(digitalPinToPCINT(L3), handlePulse, CHANGE); 
}


void loop(){}                                                 // Nothing to see here, move along


void handlePulse()
{
  currentState = 4 * digitalRead(L1) +                        // A decimal value is calculated from the binary state
                 2 * digitalRead(L2) +                        // of leads L1, L2 and L3
                     digitalRead(L3);

  if(currentState < 7)                                        // when starting and stopping the disk currentState can be 7 shortly, ignore
  {
    pulseInterval = millis() - peviousPulse;                  // Interval between interrupts to determine how fast the disk is turned
    peviousPulse  = millis();                                 // mark the time of the current interrupt
    
    saveState(currentState);                                  // Push the current reading to the back of previousStates[] shifting values left
  
    combinedStates = 100*previousStates[0] +                  // Calculate a checkvalue (integer) to check for validity
                      10*previousStates[1] + 
                         previousStates[2];                   // If previousStates is {5,1,3} the result is 500+10+3 = 513

    if       (inCW (combinedStates)) {scrollDir = -1;}        // Set direction by checking if the checkvalue is either in CW, CCW nor both
    else if  (inCCW(combinedStates)) {scrollDir =  1;}
    else                             {scrollDir =  0;}

    multiplier = 1;                                           // If the disk is turned fast, a multiplier is set
    if (pulseInterval < 30){multiplier = 2;}

    if(pulseInterval > 400)                                   // When turning the disc from stationary it should react immediately, not using pulsecount
    {                                                         
        Mouse.move(0, 0, scrollDir);
    }
    else
    {
      pulseCount++;                                                    
      if(pulseCount == 2)
      {                                                       // one full turn of the disc fires 24 interrupts, scrolling on each interrupt would be too fast
        Mouse.move(0, 0, scrollDir * multiplier);             // when using pulsecount 2, scrolling only occurs every second pulse
        pulseCount = 0;
      }
    }
  }                                                           // end if(currentState < 7)
}                                                             // end handlePulse()


void saveState(int newState)
{
  for(int i = 0; i < numOfStatesToSave - 1; i++)              // Shift values to the left starting at the first element
  {previousStates[i] = previousStates[i + 1];}                // Element 1 (at index 0) gets the value of element 2, 2 gets the value of 3
  previousStates[numOfStatesToSave - 1] = newState;           // Place the new value at the last index
}

bool inCW(int sequence)                                       // Function to check if sequence is in validCW[]
{
  for(int i=0; i<6; i++)
  {if(validCW[i] == sequence){return true;}}
  return false;
}

bool inCCW(int sequence)                                      // Function to check if sequence is in validCCW[]
{
  for(int i=0; i<6; i++)
  {if(validCCW[i] == sequence){return true;}}
  return false;
}

Today I found that a different motor outputs different sequences.
Use HDDScrollwheel__PinSequence.ino to determine the sequence of your motor

	BIN:	4	2	1		DEC							
									Valid:					
CW	000		0	0	0		0		013	137	376	764	640	401
	001		0	0	1		1							
	011		0	1	1		3		const int validCW[]  = {013,137,376,764,640,401};					
	111		1	1	1		7							
	110		1	1	0		6							
	100		1	0	0		4							
														
									Valid					
CCW	000		0	0	0		0		046	467	673	731	310	104
	100		1	0	0		4							
	110		1	1	0		6		const int validCCW[]  = {046,467,673,731,310,104};					
	111		1	1	1		7							
	011		0	1	1		3							
	001		0	0	1		1							

And about the USB HID question. That's still open. Anyone's got a hint to achieve true smooth scrolling?

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.