Introduction

The microsoft intellimouse pro makes a whining noise when it’s on. This blog post details my efforts in attempting to silence that whine. To Microsoft’s (and Pixart’s) credit, it’s a very quiet whine. I only notice it at night when my room is very quiet. I could have returned the mouse and bought another product but I wanted to try my hand at fixing this issue myself.

Is it just mine?

At first I thought the whining noise was just a defect so I had amazon send a replacement mouse. The replacement whined as well. I still wasn’t convinced that it was a design defect so I posted on a forum asking for people to listen to their intellimouse. Quite a few people reported an audible whine up close so I must conclude that this is a defect of the design.

Finding the source of the whine

Conventional electrical engineering wisdom will tell you that the source of the whine is probably an inductor. Changing current through the inductor produces a changing magnetic field which tugs on some magnetic part of the product causing the inductor to jiggle in place. The noise this produces is called coil whine. Most switching power supplies will operate their switching elements far past 22kHz to keep any coil whine out of the range of human hearing. It seems unlikely Microsoft forgot to do this. The other possibility is that the whine has something to do with driving the RGB LED. Current to the red, green, and blue LED is almost always pulse-width modulated and if the PWM frequency is too low that could be the source of the noise. The only way to find out is to open the mouse and probe around.

Opening the mouse

The bottom of the mouse has four rubber pads that cover four small torx screws which hold the case together. The bottom of the mouse comes off easily after taking out the torx screws but it still held on by a ribbon cable that is soldered to the PCB.

Looking at the PCB there is a RGB LED mounted at a right angle with four exposed pads at the bottom of the PCB. Probing each pad didn’t show any PWM occurring. Considering the default color setting is white, this isn’t surprising. After probing some SMD components, I couldn’t find any high frequency signals or noise on any of the pads. Pin 2 of the sensor however showed a ~4 kHz signal.

pin 2 waveform

I used an FFT app (Spectroid) on my phone to observe the whine, and when the mouse was on I observed an increase in amplitude at ~4 kHz. The gap in the noise signal on the waterfall diagram occurred when I unplugged the mouse. This confirms that the sensor is somehow responsible for the noise.

fft

The Sensor

Line Number Text
1 PAW 3389-MS
2 N92380320

The sensor–according to this Reddit post– is a custom sensor designed by Pixart for Microsoft. The datasheet doesn’t seem to be available on the internet but the datasheet for the PMW 3389 is available. Pin 2 is “reserved” which isn’t much help. Looking further into the datasheet we see a block diagram.

block_diagram

There is an LED inside the chip labelled here as “illumination source”. It emits infrared light that illuminates the surface below the mouse so the optical sensor can track movement. The package exposes the anode of the LED labelled LED_P. The board should tie this to 1.9 volts through a 13 ohm resistor according to a reference layout.

reference_layout

Probing the LED anode reveals a waveform similar to the one on the reserved pin.

led_anode

My guess is that the LED driver contains a pass element being controlled by the signal at pin 2. When the pass element is off the LED anode sits at 1.9 volts pulled up by its 13 ohm resistor. When the pass element is on, current flows through the LED and resistor, causing the anode’s voltage to dip slightly. I also noticed that the duty cycle (and the sound of the audible whine) would change if I lifted the mouse away from the surface it was sitting on. This demonstrates that the purpose of the signal is the vary the brightness of the LED.

The Problem

Putting together the information I found from probing the PCB I can say with a a high degree of certainty that the source of the whine in the microsoft intellimouse pro is the PWM’ing of the built-in IR LED.

Finding a solution

Making this whine go away won’t be very easy considering the signal is part of the normal operation of the mouse. I figured the least invasive way to get rid of the whine is to increase the frequency past the limits of human hearing. At the bottom of the sensor’s datasheet there is a description of the registers accessible over SPI. One of the registers is named PWM_Period_Cnt. I decided I would try writing to this register to see if that would increase the PWM frequency. There are a few issues with this plan, though. The datasheet that is listed publicly is for the PMW 3389, not the PAW 3389-MS. Looking through the other public datasheets not all Pixart projects have a PWM_Period_Cnt register. The PAW 3389-MS sensor in the intellimouse may not actually have this register either. For lack of better options, I decided to assume that the PAW 3389-MS had a PWM_Period_Cnt register at the same address as the PMW 3389. There is a microcontroller on the board that communicates with the sensor and the host PC. Modifying the firmware on the microcontroller to write to the PWM_Period_Cnt register could eliminate the noise problem.

Understanding the SPI interface

Before I dive into reverse engineering the firmware it would be nice to have some landmarks. I soldered bodge wires to the MOSI, MISO, SCLK, and CS pins of the sensor DIP package to view transactions on my oscilloscope. I set the scope to capture 5ms of data after 1 falling edge of CS. I did this to reject a spurious falling edge of CS at startup. This is what the first few SPI interactions looks like:

first_spi_interactions

The 1st signal from the top is SCLK. The 2nd is MOSI, the 3rd is CS, and the 4th is MISO. Zooming in on a single interaction looks like this:

single_interaction

Although it may be hard to see, for the duration of the interaction, CS is pulled low. There are usually two 8 bit transactions in a single interaction. One at the start, and one at the end. A single 8 bit transaction looks like this:

single_transaction

Looking at a few SPI interactions, I was able to divine a few facts about the interface:

  • SCLK idles high and devices sample the data lines on a falling edge
  • Bits are transmitted MSB first
  • The MSB of the first byte on MOSI determines whether the interaction is a read or a write (1 is write, 0 is read)
  • The other 7 bits of the first byte on MOSI make up the register address
  • If a read is being performed then the second byte on MISO is the value being read
  • If a write is being performed then the second byte on MOSI is the value being written

Using this information I was able to figure out the first few SPI reads/writes.

# R/W Register Value
1 W 0x3A 0x59
2 R 0x02 0x00
3 R 0x03 0x00
4 R 0x04 0x00
5 R 0x05 0x00
6 R 0x06 0x00
7 W 0x10 0x00
8 W 0x13 0x1D
9 W 0x13 0x18

Following this sequence, the microcontroller writes many bytes all in one interaction. This turns out to be the microntroller loading a SROM on the chip. More SPI writes follow the SROM loading and also when the mouse is moved but I did not decode them by hand.

Dumping the firmware

These are the lines printed on the microcontroller:

Line Number Text
1 M27P8V
2 1N71K
3 AXLXP

This is a Kinetis KL27 microcontroller designed by Freescale (bought by NXP). It’s a 32-bit little endian ARM Cortex M0+ processor. Fortunately it has a public datasheet and reference manual. Since it’s an ARM Cortex processor it has SWD (Serial Wire Debug) pins. On the bottom left of the PCB there are pads for a small 10 pin .05” pitch SMD connector that exposes SWDIO, SWDCLK, and RST. I soldered jumpers to the corresponding three pads and connected them to my ST-Link/V2. This is what the mouse looked like with the three wires for the debug probe soldered in on the bottom left and the four wires for SPI debugging soldered on the sensor package.

mouse_setup

I used OpenOCD to interface with the on-chip debugger and dump the firmware:

openocd -f interface/stlink-v2.cfg -f target/klx.cfg 

In another shell:

telnet localhost 4444
halt
reset
flash read_bank 0 /path/to/file 0 0x2000

You can learn more about OpenOCD flash commands here.

(Partially) Reverse Engineering the Firmware

I started by loading the firmware dump into Ghidra. The MMIO address for the lower byte of the KL27 SPI data register is 0x40076006. Any code that reads or writes to the SPI interface will need to read or write to that location. I searched for functions that used that memory address and started to classify them based on the decompiler’s output.

For example one of the functions Ghidra found and decompiled looks like this:

byte spi_xfer_blocking(byte param_1)

{
byte *pbVar1;

pbVar1 = DAT_00005dac;
do {
} while (-1 < (int)((uint)*DAT_00005dac << 0x1a));
DAT_00005dac[6] = param_1;
do {
} while (-1 < (char)*pbVar1);
return pbVar1[6];
}

I named it byte spi_xfer_blocking( byte param_1) because it seems to take in a byte, clock it out on the SPI interface, and then return the contents of the shift register. I repeated this process for all the other instances of a read or write to the SPI data register MMIO and found:

  • byte spi_xfer_blocking( byte param_1 ), works as described above
  • byte spi_xfer_blocking_copy( byte param_1 ), a copy of the above function
  • byte spi_rx_blocking( void ), works like spi_xfer_blocking but only transmits 0x00

Those functions are called by a couple other higher level SPI functions I also gave names:

  • void spi_write(byte reg,byte val), write val to reg on the sensor
  • byte spi_read(byte reg), reads from reg on the sensor
  • void spi_write_bytes(byte *param_1,uint param_2) writes a sequency of bytes to the sensor (used for loading the SROM)

The basic spi read/write functions are then called by some even higher level functions:

  • bool spi_write_and_config_srom(void), writes to some SROM configuration registers then writes lots of bytes to the sensor’s SROM
  • void set_resolution(uint16_t resolution), something to do with setting the resolution of the sensor
  • void initial_sensor_config(uint16_t resolution,byte angle_snap_msb,byte angle_snap), a function that ultimately calls all the functions above directly or indirectly. This function is called by main to configure the sensor.

Once I found the initial_sensor_config function I read through the decompiler output and confirmed that this is the function that makes the first few SPI writes. The first few reads/writes I decoded from the scope view are marked below. The rest of the reads/writes are inside the spi_write_and_config_srom function.

void initial_sensor_config(uint16_t resolution,byte angle_snap_msb,byte angle_snap)

{
byte bVar1;
int iVar2;

FUN_00005db0();
iVar2 = DAT_000060f4;
*(undefined4 *)(DAT_000060f4 + 4) = 0x10;
another_delay?(1);
*(undefined4 *)(iVar2 + 8) = 0x10;
spi_write(0x3a,0x5a); <-- 1
another_delay?(0x34);
spi_read(2); <-- 2
spi_read(3); <-- 3
spi_read(4); <-- 4
spi_read(5); <-- 5
spi_read(6); <-- 6
do {
    another_delay?(5);
    iVar2 = spi_write_and_config_srom();
} while (iVar2 == 0);
spi_write(0x3d,0x80);
do {
    another_delay?(1);
    bVar1 = spi_read(0x3d);
} while (bVar1 != 0xc0);
spi_write(0x3d,0);
spi_write(0x10,0);
set_resolution(resolution);
if ((angle_snap_msb < 2) && (angle_snap - 2 < 5)) {
    spi_write(0x42,angle_snap_msb << 7 | angle_snap);
}
FUN_0000aefe(*DAT_000060f8);
spi_write(0x2c,10);
spi_write(0x2b,0x10);
spi_write(99,2);
spi_write(0x17,10);
spi_write(0x50,1);
return;
}

Patching the firmware

Still going on the assumption that the PAW 3389-MS chip has a PWM_Period_Cnt register at 0x73 just like the PMW 3389, I searched for spi_write calls to address 0x73. I didn’t find any which is bad news. Either the register just doesn’t exist in the intellimouse sensor or it does and my patch isn’t going to be as simple as changing one byte in the firmware. Continuing under the assumption that the register exists at 0x73 I managed to patch the firmware.

How the firmware patch works

In the microsoft intellimouse pro firmware functions are called using the ARM Branch with Link instruction or BL. BL stores the value of the program counter (PC) into the link register (LR) then jumps to a location in flash memory provided by a literal value. The function at the memory location then stores LR somewhere (usually the stack). When the function finishes executing it jumps back to the caller by writing the stored value to PC. The last function call of initial_sensor_config uses a BL instruction to call spi_write(0x50,0x1):

000060ea 01 21           mov        r1,#0x1
000060ec 50 20           mov        r0,#0x50
000060ee 00 f0 29 f8     bl         spi_write

Using Ghidra, I modified that BL instruction to jump to spi_extra instead:

000060ea 01 21           mov        r1,#0x1
000060ec 50 20           mov        r0,#0x50
000060ee 19 f0 7d ff     bl         spi_extra

spi_extra is a function I placed at the of the end flash memory where there is quite a bit of unused space. It carries out the original write to 0x50 and an additional write to 0x73 (the PWM_Period_Cnt register):

0001ffec 70 b5           push       { r4, r5, r6, lr }
0001ffee e6 f7 a9 f8     bl         spi_write
0001fff2 73 20           mov        r0,#0x73
0001fff4 20 21           mov        r1,#0x20
0001fff6 e6 f7 a5 f8     bl         spi_write
0001fffa 70 bd           pop        { r4, r5, r6, pc }

The push at the start stores the value of LR in the stack. The pop at the end writes that value to PC to jump back to the caller.

Writing the patch to the microcontroller

Once I had the binary I had to write it to the flash memory which proved quite difficult. At first I tried a procedure similar to the steps I used to read from flash memory:

openocd -f interface/stlink-v2.cfg -f target/klx.cfg 

In another shell:

telnet localhost 4444
halt
reset
program /path/to/bin verify reset 0

This didn’t work. The verify step failed meaning the data read back after writing didn’t match what was written. It turns out the Kinetis KL27 has a flash memory protection feature that is only accessible through the ARM DAP (Debug Access Port). This feature is called the MDM-AP and can perform a number of tasks including locking out writes to the flash memory. Fortunately an MDM mass erase will unlock the memory and permit normal writing. To perform a mass erase through the MDM one must first obtain a DAP compliant debug probe. Many forums online will direct you to expensive probes but the ST-Link/V2 version V2J24 and above are DAP compliant. OpenOCD just needs to be given the correct config file to be told that it can use its kinetis driver with the ST-Link.

This is where things get tricky. The latest version of OpenOCD has DAP support for ST-Link and a stlink-dap config file but the OpenOCD package provided by my package manager is sorely out of date and doesn’t have any of this. After following the steps provided by OpenOCD for compiling and packaging the latest version I was able to install the latest version and run this:

openocd -f interface/stlink-dap.cfg -f target/klx.cfg 

In another shell:

telnet localhost 4444
halt
reset
kinetis mdm mass_erase
program /path/to/bin verify reset 0

It goes without saying that you should have an untouched version of the original firmware saved before erasing the flash memory

This successfully unlocked the flash memory and programmed the device. I first confirmed that the patch was calling my new function using hardware breakpoints. I then confirmed that the patch was writing to register 0x73 on the sensor with an oscilloscope. Now that the flash memory is unlocked even probes that aren’t DAP compliant can write to the memory unhindered.

It doesn’t work

Unfortunately this SPI write did not change the PWM frequency as observed with an oscilloscope. I tried writing a few other values to 0x73 to no avail. I thought that perhaps that register can only be written to when the sensor is in a particular state. A datasheet would be nice.

I reached out to Pixart to obtain the datasheet for the PAW 3389-MS but they said they can’t provide that because they are under an NDA with Microsoft. I decided to ask microsoft for the datasheet but they didn’t want to share it either. Fortunately, Pixart has been extremely cooperative and is willing to share the PMW 3389 datasheet with me if I sign an NDA. I will post an update if I make any further progress (while respecting any NDAs I sign, of course).

Special thanks to Shawn Anastasio for helping with the flash memory write issue and showing me Ghidra.

Update 1/13/2020

A Pixart representative told me that the PWM_Period_Cnt does not have anything to do with the IR LED PWM. I tried removing the 13 ohm resistor at the anode to see what would happen. The 4 kHz noise is gone but there is still audible noise at 8 kHz. If I hold down the sensor package it seems to get quieter. It doesn’t seem like I can investigate this any further so this will be my last update. If you have any insight into this issue feel free to contact me.