This is a long post, if you go through it, I would like to thank you for your time upfront :). In short, I am trying to understand how the manufacturer of the original hardware into which this sensor was built uses the signals that drive this sensor to compute a speed and direction of the input, maybe even an absolute position. The questions I have are at the end of the post.
Brief background on the question
I am hacking into an old remote control for my music players hoping to breath new life into it. The manufacturer has discontinued support for it so the firmware in it can no longer be updated and has thus become incompatible with the newer firmware that the manufacturer has released for the player. When I say hacking, what I mean is that I am replacing all the electronics in the remote with custom made boards (for power and logic) but I wish to keep the hardware inputs as those are part of what I like about this remote control. The firmware for the original device is closed anyway and reversing it is beyond my abilities and time availability.
The hardware inputs are a trivial set of buttons and a scroll wheel. The scroll wheel sensor functionality is the focus of the question. The wheel resembles that of an older generation iPod. It has no moving parts, that is, it does not rotate to translate the motion of the user's finger into electrical signals. However, unlike the iPod wheel, there are no active components on this one. The iPod wheel has a digitizer IC that translates the user's input to a digital message. This sensor breaks out 4 lines and appears to be divided into 3 sensing segments. Here are pictures, provided by user Beryl on the pjrc forums, of the sensor removed from the case:
Although it may initially appear to be a capacitive sensor, it turns out that it is in fact resistive. This is the equivalent circuit I have deduced from my attempts to reverse its functionality:
The capacitor is not on the sensor, it is on the PCB that the sensor connects to:
The microcontroller on the original hardware appears to be a Rensas M16C/26. Three of its I/O pins are wired to channels 1-3 and a pin that can be used as an ADC input channel (according to the datasheet) is connected to channel 4. This gives me confidence that the output of the scroll wheel sensor is supposed to be interpreted as an analog voltage level.
The setup above is what I used on the original device to characterize the communications between the M16C and the sensor and try to decode them. As it turns out, it is a train of pulses. The train is made up of two repeating patterns, one I refer to as the wide pattern and the second as the narrow pattern. The wide pattern has a total length of 2ms with 8 pulses in between, each 0.25ms wide. The narrow pattern is 0.2ms wide with 4 pulses, each 0.05ms wide. The wide pattern repeats every 45ms while the narrow pattern repeats every 50ms. The time offset between the two patterns appears to be 3ms at start, which causes the two patterns to shift in time relative to each other (see linked video below).
Have you seen these signal waveforms?
Here is a close up where I managed to capture the both the wide and narrow pulse trains present on the lines for channel 1-3:
Disregard the droop on the signal on channel 3 (magenta), this turned out to be a issue with my wiring.
Video on YouTube of the signals on the scope:
How I Understand This Signals Should Be Interpreted.
Using an Arduino I wrote a short program to replicate the behavior of the input pulse trains from the original hardware. For the wide pattern, the width is 2ms and there are signal changes at every 0.5ms. My program reads the voltage on channel 4 a few cycles after each 0.25ms when there is a step change in the input signals. This results in 8 samples taken each "frame" where the sample on channel 4 is the combination of the voltages drops at each input channel. The relative change of the values can be used to decode the direction of the movement at the sensor and the rate of change in values to decode the speed of the movement (if I am doing this right).
Here is a plot of the data capture of these frames:
Besides the fact that I am probably not reading the signals the way they are meant to be read, I am confused about the need for the 2 patterns. To produce the above capture I chose to drive the inputs on channels 1-3 using the wide pattern alone. If both patterns are present then they overlap every once in a while and appear to distort the output.
Below is the code I wrote to read the sensor using the pulse train inputs as the original hardware appears to do:
#define DELIM "," #define DB 50 // Drive using the narrow pattern //#define RUN_CLOCK // Drive using the wide pattern #define RUN_SIGNAL // Initialize TIMER1, CTC mode, interrupt every 0.05 milliseconds. void timer1_Init() { /* Enable TC1 */ // PRR0 &= ~(1 << PRTIM1); TCCR1A = (0 << COM1A1) | (0 << COM1A0) /* Normal port operation, OCA disconnected */ | (0 << COM1B1) | (0 << COM1B0) /* Normal port operation, OCB disconnected */ | (0 << WGM11) | (0 << WGM10); /* TC16 Mode 4 Normal / CTC, depends on WGM13 and WGM12 on TCCR1B*/ TCCR1B = (0 << WGM13) | (1 << WGM12) /* TC16 Mode 4 CTC */ | 0 << ICNC1 /* Input Capture Noise Canceler: disabled */ | 0 << ICES1 /* Input Capture Edge Select: disabled */ // divide by 8 // effective clock rate is 250KHz | (0 << CS12) | (1 << CS11) | (0 << CS10); /* IO clock divided by 8 */ // at 16MHz count to 100 (desired time step is 50uS which is 20,000 Hz) OCR1A = ((F_CPU / 8UL) / 20000UL) - 1; // Count to 100 TCNT1 = 0; TIFR1 = 0; // Enable the interrupt TIMSK1 = 0 << OCIE1B /* Output Compare B Match Interrupt Enable: disabled */ | 1 << OCIE1A /* Output Compare A Match Interrupt Enable: enabled */ | 0 << ICIE1 /* Input Capture Interrupt Enable: disabled */ | 0 << TOIE1; /* Overflow Interrupt Enable: disabled */ } // Narrow pattern repeats every 50ms #define CLOCK_INTERVAL 1000 // Wide pattern repeats every 45ms. #define SIGNAL_INTERVAL 900 uint16_t dtC = 0, dtS = 6, dtS2 = 0; uint8_t clk = 0, sig = 0; // Bit patterns used to drive the pulses for both the narrow and wide patterns uint8_t CLOCK[4] = { B00000111, B00000011, B00000101, B00000110 }; uint8_t SIGNAL[8] = { B00000110, B00000101, B00000011, B00000011, B00000101, B00000110, B00000101, B00000011 }; // Multiple variables to synchronize behavior and capture output. volatile uint8_t out = 0, OUT = 0; volatile bool ready = false, go = false; bool capture = false; uint8_t captADC = 0; // TIMER1 interrupt handler ISR(TIMER1_COMPA_vect) { #ifdef RUN_CLOCK // Clock wave. Pulses spaced at 50us, train spaced at 50ms. ++dtC; if(dtC >= CLOCK_INTERVAL) { // Release the clock if(clk < 4) { PORTB = CLOCK[clk++]; } else if(clk == 4) { clk = 0; dtC = dtC - CLOCK_INTERVAL; PORTB = 0x00; } } #endif #ifdef RUN_SIGNAL // Data wave. Puleses spaced at 250us, train spaced at 45ms. // This train appears to be offset by 5ms from the Clock wave at start. ++dtS; if(dtS >= SIGNAL_INTERVAL) { go = true; // Release the signal ++dtS2; if(sig < 8) { if(sig == 0) { PORTB = SIGNAL[sig++]; capture = true; } else if(dtS2 % 5 == 0) { PORTB = SIGNAL[sig++]; capture = true; } else if (capture == true) { capture = false; out = (out << 1); out = ((PINC >> 3) & 0x01) | out; captADC++; } } else if(sig == 8 && dtS2 % 5 == 0) { sig = 0; dtS2 = 0; dtS = dtS - SIGNAL_INTERVAL; out = (out << 1); out = ((PINC >> 3) & 0x01) | out; PORTB = 0x00; OUT = out; out = 0; ready = true; go = false; } } #endif } void setup() { // put your setup code here, to run once: Serial.begin(115200); // 8, 9, 10 - out, A3 input (PC3) DDRB = B00000111; PORTB = 0x00; timer1_Init(); // // Reference is AVCC // ADMUX |= (1 << REFS0); // // Scale ADC clock by 128 (F_CPU == 16MHz) // ADCSRA |= (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0); // // Use channel 3 // ADMUX |= 3; // // Enable ADC // ADCSRA |= (1 << ADEN); sei(); } uint8_t captADC2 = 255, a = 0; uint16_t adc[8] = { 0 }; void loop() { #ifdef RUN_SIGNAL if(go == true && captADC2 != captADC) { //ADCSRA |= _BV(ADSC); //while(bit_is_set(ADCSRA, ADSC)); //ADCSRA |= _BV(ADSC); //while(bit_is_set(ADCSRA, ADSC)); //adc[a] = ADC; //adc[a] = analogRead(A3); adc[a] = analogRead(A3); captADC2 = captADC; a++; } else if(go == false && captADC2 == captADC) { Serial.print(adc[0]);Serial.print(", "); Serial.print(adc[1]);Serial.print(", "); Serial.print(adc[2]);Serial.print(", "); Serial.print(adc[3]);Serial.print(", "); Serial.print(adc[4]);Serial.print(", "); Serial.print(adc[5]);Serial.print(", "); Serial.print(adc[6]);Serial.print(", "); Serial.println(adc[7]); a = 0; captADC2 = 255; } #endif #ifdef RUN_CLOCK if(ready == true) { ready = false; Serial.print(OUT, BIN);Serial.print(", "); Serial.println(OUT, DEC); } #endif }
How I started (and am currently) reading the sensor.
Wrote a short program to sequentially input a constant voltage into channels 1, 2 or 3 in a sequence. Only one of the input channels holds a voltage at a time and this is read on channel 4 using the ADC. A "frame" consists of three analog readings in sequence and the voltage for each channel is stored in separate variables. Using these readings the values can be used to compute the direction and velocity of the movement on the scroll wheel by comparing them to each other. Essentially how I first decided to read the sensor was by doing the same as described above except that only taking 1 sample per input channel per "frame", as opposed to the 8 samples described above where each sample is a combination of the 3 input channels. With some tuning, this method has worked for me so far but after seeing what the original hardware was doing, seems deceptively simple and gives me the feeling that there is a pitfall in the method which I am not seeing.
The questions I have
- Is there a name for the technique used by the original hardware to drive an read this scroll wheel sensor?
- Is my understanding on interpreting the pulse train into an analog signal correct? Is this how these signals are meant to be read?
- Is there an advantage to driving the inputs with a pulse train as opposed to simply relying on a sequence of constant voltages?
- Any suggestions on better methods I could try to read this sensor?