Solving the DL180 G6 Fan Controller Problem

Some time ago I acquired a DL180-G6 rack mount server for very little money as these go for next to nothing on eBay in most configurations and one version has 12×3.5″ drive bays. Unfortunately there seems to be a bit of a problem with these systems where if you change any of the hardware (such as an unsupported raid card) or a whole variety of other things the on board fan controller defaults to high speed and it will sound like a jet engine. The fans are high flow high speed fans and so while they do work well they really scream at high speeds. Disconnecting the fans won’t help because if you do that the system will refuse to boot entirely.

The Fans

I decided that I needed to come up with a solution to this problem because well…how hard can it be?! Fundamentally these are standard 4 pin PWM type fans which have the following connections:

1 – GND
2 – +12V
3 – Tach
4 – PWM

Unfortunately the ones in the DL180G6 are a special case in that they have 6 pin connectors which follow a slightly different layout:

1 – +12V
2 – GND
3 – Blank
4 – Tach
5 – PWM
6 – Blank

The blank pins on the fan connector may seem a bit pointless at first but they’re like that because the motherboard support redundant pairs of fans in some configurations (I saw this on a Storageworks x1600g2 which shares the same main components). Due to this the motherboard needs a second tach (RPM) signal to monitor the second fan in the redundant pair and presumably due to the high power consumption of these fans a second +12V supply. This means you can either connect a single fan direct to the motherboard or a pair via the splitter which comes in the storage works (HP p/n  534358-001). The motherboard header looks like this:

1 – +12V-1
2 – GND
3 – +12V-2
4 – Tach – 1 (Inlet)
5 – PWM
6 – Tach – 2 (Outlet)

The good news is this means you only need one PWM per pair of fans in this configuration.

Arduino 25kHz PWM

At this point I decided I needed to build something I could fully control and so I decided to try to control it with an Arduino using the on board hardware PWM. This isn’t as easy as it sounds because PWM fans operate on a PWM control frequency of 25kHz which isn’t a standard configurable frequency on an Arduino IDE because it isn’t a direct division of the system clock. Thankfully there is a way round this. Timer 1 on the Arduino nano is a 16 bit timer and by directly manipulating the timer registers we can make the timer reset after a number of cycles so we can basically create a timer of any frequency we need. In this case  rather than being limited to prescalers (divide by 1,8,64,256,1024) from the main clock (16MHz) and 0-255 (0% – 100% duty) by using the 16 bit timer we can set a clock reset every 320 cycles which gives 25kHz at this clock frequency. I found some code which nicely did this on stack exchange and so just went with it.

https://arduino.stackexchange.com/questions/25609/set-pwm-frequency-to-25-khz

The clever bits are shown here:

    // Configure Timer 1 for PWM @ 25 kHz.
    TCCR1A = 0;           // undo the configuration done by...
    TCCR1B = 0;           // ...the Arduino core library
    TCNT1  = 0;           // reset timer
    TCCR1A = _BV(COM1A1)  // non-inverted PWM on ch. A
           | _BV(COM1B1)  // same on ch; B
           | _BV(WGM11);  // mode 10: ph. correct PWM, TOP = ICR1
    TCCR1B = _BV(WGM13)   // ditto
           | _BV(CS10);   // prescaler = 1
    ICR1   = 320;         // TOP = 320
void analogWrite25k(int pin, int value)
{
    switch (pin) {
        case 9:
            OCR1A = value;
            break;
        case 10:
            OCR1B = value;
            break;
        default:
            // no other pin will work
            break;
    }
}

The first bit sets up the timers to run at 25kHz as we need. The second bit is a special analogue write function to make sure we set the time right. We will use this function to update the timer value. We now have two PWM channels on pins 9 and 10 of our Arduino Nano.

Temperature Control

Next up I decided to add temperature control to these fans using a couple old DS18B20 temperature sensors I had lying about. These are good because multiple sensors can be daisy chained to a single data pin and the value doesn’t need scaling or anything unlike an analogue input pin and a thermistor. They cost a little more but for convenience they’re hard to argue with. They usually use +5V, Gnd and one signal pin. The signal pin needs a pullup resistor to work – this is normally specified as 4.7k. Technically these are a “1wire” device which can be run on parasitic power so the +5V isn’t actually required at the device end but if used in parasitic power mode you short it’s Vdd pin to ground. More information can be found here:

https://datasheets.maximintegrated.com/en/ds/DS18B20.pdf

There are specific Arduino libraries for using these devices so they’re really easy to use but they have one issue in this application. To do what I intended and have two separate fan zones we need to know which sensor is which. Thankfully included with the Onewire library is an example function called “DS18x20_Temperature” which will scan for devices on a specified pin. In the example this is called “ds” and it pin 10 by default but for my purposes this won’t work because we specifically need pin 9 and pin 10 for the two PWM outputs so this needs to be relocated somewhere else.

#include <OneWire.h>

// OneWire DS18S20, DS18B20, DS1822 Temperature Example
//
// http://www.pjrc.com/teensy/td_libs_OneWire.html
//
// The DallasTemperature library can do all this work for you!
// http://milesburton.com/Dallas_Temperature_Control_Library

OneWire ds(10); // on pin 10 (a 4.7K resistor is necessary)

void setup(void) {
Serial.begin(9600);
}

Changing the pin to whatever you choose and running the example with serial monitor open you’ll get an output of the devices it finds. If you do this with one device connected at a time you can note down the device address for each. I marked them as #1 and #2 before hand to keep track of these later.

The Circuit

The next bit is bringing it all together electronically. We already looked at the Onewire sensors so these are really easy to connect up, I used two normal 3 pin PC fan connectors to allow these to be disconnectable for easy installation. The next bit is the fans. The PWM on fans are controlled using a pulse signal as described earlier but this isn’t a direct voltage control input. The fans need the PWM pin pulled to ground to control the signal but the output pins on the Arduino aren’t capable of absorbing the current required for direct connection so we need to add a transistor to do this for us. I went for a 2N7000 Mofset for each PWM channel because..well basically I had a bag full of them to hand and they’ll work at the output voltage of the Arduino. These will connect the fan PWM connection to ground when the Arduino output is driven high which unfortunately will reverse our PWM direction but that doesn’t matter because we can flip it back the right way in code. To correctly drive a a mosfet from an arduino you will need a gate drive resistor (to limit inrush to the Mosfet gate capacitance as this could damage the Arduino) and a gate pulldown resistor to make sure the mosfet fully turns off when the Arduino is no driving it. People will suggest all sorts of value for this but I went for a 220Ω for the gate drive and a 10kΩ for the pull down. The capacity of this Mosfet means we can use a single one for a large number of fans.

For simplicity I decided to use another of the eBay 6 pin fan connectors to connect up the cables for the fans. I intend to pass the power and tach signals directly to the motherboard so I’ll come up with a breakout cable to just isolate the PWM signal from the motherboard and allow us to feed in our own. Due to doing this we only actually need 4 pins (+12V, GND, PWM1 and PWM2) running up to the Arduino to control the fan but since I had them anyway why not!

Arduino Nano PWM Fan Breadboard

So this is the result the two connections for the DS18B20’s are on the right and the 6 pins for the fan/motherboard loom in the middle.

The Complete Code

This is where we start to bring everything together:

#include <DallasTemperature.h>
#include <OneWire.h>

// PWM output @ 25 kHz, only on pins 9 and 10.
// Output value should be between 0 and 320, inclusive.

int fanspeed1 = 25;
int fanspeed2 = 25;

float temp1;
float temp2;

#define onewirepin 3
OneWire oneWire(onewirepin);
DallasTemperature sensors(&oneWire);

DeviceAddress TempAdd1 = {0x28, 0x83, 0xC8, 0xE6, 0x3, 0x0, 0x0, 0x15};
DeviceAddress TempAdd2 = {0x28, 0x4A, 0xDE, 0xE6, 0x3, 0x0, 0x0, 0xFC};

void analogWrite25k(int pin, int value)
{
    switch (pin) {
        case 9:
            OCR1A = value;
            break;
        case 10:
            OCR1B = value;
            break;
        default:
            // no other pin will work
            break;
    }
}

void setup()
{

    Serial.begin(9600);    //Start diagnostic serial port
    
    // Configure Timer 1 for PWM @ 25 kHz.
    TCCR1A = 0;           // undo the configuration done by...
    TCCR1B = 0;           // ...the Arduino core library
    TCNT1  = 0;           // reset timer
    TCCR1A = _BV(COM1A1)  // non-inverted PWM on ch. A
           | _BV(COM1B1)  // same on ch; B
           | _BV(WGM11);  // mode 10: ph. correct PWM, TOP = ICR1
    TCCR1B = _BV(WGM13)   // ditto
           | _BV(CS10);   // prescaler = 1
    ICR1   = 320;         // TOP = 320

    // Set the PWM pins as output.
    pinMode( 9, OUTPUT);
    pinMode(10, OUTPUT);

    sensors.begin (); // Initialize the sensor and set resolution level
    sensors.setResolution(TempAdd1, 10);
    delay(1000);
    sensors.setResolution(TempAdd2, 10);
    delay(1000); 
}

void loop()
{

    sensors.requestTemperatures(); // Command all devices on bus to read temperature
    delay(1000);
    temp1 = sensors.getTempC(TempAdd1);             //Read Temp probe 1
    fanspeed1 = constrain(map((int)temp1,20,60,15,100),15,100);   
//Map fan speed for channel 1 to 25% at 30C to 100% at 60C and constrain the values to 25-100% range 
//(prevents fan from either going below 25% or trying to be set to above 100%)
    Serial.print ("Temp 1 is : ");
    Serial.print (temp1);
    Serial.print ("  Speed 1 is : ");
    Serial.println (fanspeed1);
    delay(1000);
    temp2 = sensors.getTempC(TempAdd2);            //Read Temp probe 2
    fanspeed2 = constrain(map((int)temp2,20,75,15,100),10,100);   
//Map fan speed to 25% at 30C to 100% at 75C and constrain the values to 25-100% range
    //(prevents fan from either going below 25% or trying to be set to above 100%)
    Serial.print ("Temp 2 is : ");
    Serial.print (temp2);
    Serial.print ("  Speed 2 is : ");
    Serial.println (fanspeed2);
    delay(1000);
  
    // Update Fan Speeds:
    analogWrite25k( 9, map(fanspeed1,0,100,320,0));         // Scale % fan speed from sensor one to actual PWM value for 25kHz output      
    analogWrite25k(10, map(fanspeed2,0,100,320,0));         // Scale % fan speed from sensor two to actual PWM value for 25kHz output   
    
   
}

Ok, it may not be perfect but it works.  The main bits to note are as follows:

1. The two “DeviceAddress” fields, these are the ID’s for our two DS18B20’s that we identified earlier.

2.  This line – fanspeed1 = constrain(map((int)temp1,20,60,15,100),15,100); which maps a temperature (converted to a float) between 20°C and 60°C to a fan speed of between 15% and 100% which is then constrained to this range so a temperature of 10°C doesn’t cause the fan to be given a 0% demand. You may want to change these constraints to minimise fan noise but I was being cautious and wanted to be sure the fans didn’t even slow down too far that anything overheated or triggered a BIOS warning. You can also tweak the temp vs speed mapping to optimise for a specific volume or temperature.

Map and ConstrainThere is another line like this for fanspeed2 so we can set different scaling ranges for the two sets of fans. You might only need a lower fan speed to maintain a temperature through the raid card area for example.

3. This line : analogWrite25k( 9, map(fanspeed1,0,100,320,0));
It maps our 0-100% fan speed to a value between 320-0 respectively so that we correct the signal inversion caused by using the N channel Mosfet.

The Wiring Loom

This bit is actually really simple but takes ages, buy or borrow a proper crimp tool for fan connectors a soldering iron and a good selection of heat shrink to cover everything up.

With this loom you can actually wire it up to a standard PWM fan controller off eBay or whatever if you just want a quick solution without all the soldering for the breadboard bit since it gives you power, ground and the PWM signals. Just be aware ONLY PWM fan controllers (the 4 pin ones). By doubling up the tach lines you could also take out several fans if that’s what you want so run the tach line from one fan to multiple motherboard headers if thats what you want. Here’s just how to intercept the right signal from the fan headers the way I did it:

DL180G6 Fan Wiring

Here’s what we end up with:

DL180g6 Fan Breakout

 

The Result

Finished Arduino DL180G6 Fan Controller
As mentioned earlier this will also work with the redundant fan systems because it passes through the tach signal of the second fan in the pair and a single PWM controls both. If doing this be careful, different servers seem to have different fan inputs configured depending on exactly what they are and you need to make sure you connect up to the right ones or the server will error on fan failure. This will probably work OK with other similar servers but can’t guarantee anything so  your mileage may vary. The other good news is that if PWM fans don’t get a PWM signal they default to 100%.

With the settings I have the fans will spin up to 100% briefly as the Arduino powers up but then spin down a few seconds later- this is normal behaviour for these fans. When the system is idling the fans seem to sit at the 15% most of the time occasionally going a little faster and now the power supply is by far the loudest part of the server so I probably wouldn’t bother setting a speed below 15% but the system is drastically quieter overall. You probably still won’t want it in your bedroom but you won’t be able to hear it everywhere in your building any more!

Other developments

When I get a bit more time I’m hoping to wire the Arduino USB connection up to an internal USB header so I can monitor the temperatures and even reprogram it when the server is running to fine tune the settings in operation. Technically its also possible to write a program to monitor CPU load and ramp the fan accordingly but this might just be too much effort.

Hopefully this will help those of you out there who have been having problems with these servers. Good luck

RX8 Project – Part 13, Canbus #3, Labview!

If you just want the CANbus ID for the RX8 PS light and not all the background work just skip to the end!

So it’s been quite a long time since I had chance to have another go at getting the CANbus on the cluster working and while previously I manged to get everything apart from the power steering warning light working I decided I really should find out why not. This is a simple lesson in why getting sufficient sleep is really important!

I was contacted a while ago buy a guy doing a similar conversion to mine who happened to have a second complete RX8 and a variety of diagnostic equipment that can talk CANbus who had send me a block of CANbus log data that I’ve done nothing like enough work on since I’ve had it! Anyway the key here is I knew that the car some of the logs had come from had the factory PCM (ECU) working as intended and as such the power steering light doesn’t illuminate. This meant that somewhere in that log file was the data that turned off the light, I just had to work out where it was!

Now first off I took the approach of taking the two sets of data logs I had, one from a car with a functioning PCM and one from a car that doesn’t. Then list out all the ID’s that occurred in each set of data. I’m going to assume for my purposes that any that occur in both sets are not required. The logic being that the data that turns off the light must be present in one set and not the other. I admit that this might not be the case if there’s something more complex going on like if with the PCM removed the power steering computer doesn’t get the required data and sends a different value on the same ID. But for now it’s a starting point!

The ID’s that remain are as follows:

201, 203, 215, 231, 240, 250, 420, 620, 630, 650

A couple of these we’ve already seen elsewhere, specifically 201, which is the RPM and speed to the dash, and 420 which control the cluster warning lights. So after setting up the arduino and canbus data to strobe all these remaining address and nothing happening I gave up!

Many weeks went by and I it was nagging at me why I couldn’t find the right code. Eventually I decided to try a different tack so I ordered a USBtin which is a really neat little USB to CAN adapter which appears as a virtual COM device in windows and can be controlled using a series of commands known as the LAWCIEL protocol (details of which can be found here). The kit version is really quite cheap and would probably be a good option for the budget conscious but on this occasion I just decided to be lazy and buy the prebuilt one.

Clearly I’ve decided to PC control it at this point! Next up I needed a way to stop it when the warning light went off. I ordered a very cheap optical detector off ebay which can be wired into an Arduino or something similar. These are the ones that vary around £1-2  so difficult to argue with. They offer a potentiometer to adjust the switching brightness so I can tune it to what I need and a digital output so I don’t need to mess about doing analog reads or calibrating things on a microcontroller. Yes i know it’s not the neatest or most efficient way of doing it but for my purposes it’s so cheap and easy it really doesn’t matter! So I need to make that talk to a PC in a way I can use and that’s where this whole thing starts to get more interesting.

I’d pondered ways of interfacing a microcontroller to a PC easily and while it’s not terribly hard to make it shout over serial when an input goes high I came across something much more interesting. There’s a company called National Instruments who make lots of very expensive software and equipment for recording data from experiments but fairly recently they started supporting hobbyists by producing the Labview Makerhub, and more specifically a package called LINX. LINX includes a series of drivers and firmwares to allow things like Arduinos, Beaglebones and even Raspberry Pi’s to be used as IO devices (the Raspberry Pi can actually have programs loaded to them as a full remote node). This is quite a major step because it suddenly allows hobbyists to use really good professional software without having the problem of only being able to use NI’s extremely expensive hardware! This gave me and idea – use labview as the core software then I can use the supplied LINX firmware to set up an arduino as IO. To make this deal even sweeter you can also download Labview for free from NI for home use. Take a look here

So after a quick bit of following the instructions we have a basic labview program that will read the arduino IO via serial:

Labview Lynx1

Basically what this does  is it starts by opening a serial connection via the LINX toolkit, this returns a device name to an on screen string display and passes a reference which identifies the connection to the read stage. The next bit the larger grey rectangle is how Labview handles a while loop so it’ll keep performing the enclosed functions constantly from left to right until the conditional term in the bottom right goes high – in this case it’s a stop button. So basically the loop just calls a LINX channel read of channel 2 where I connected the light sensor to the Arduino. The inner rectangle only executes when the read value is false (i.e. when the light goes off) and while there’s a lot of information recorded here from elsewhere in the program basically if it sees the light go off it records the current ID being tested, the time that has elapsed since the test started. This means we know when it’s right!

Labview is designed to capture data from lab instruments and so there’s a really handy thing called the VISA toolkit that allows blocks of data read and write via the serial port and basically you can just open a port with specified settings then make read and write requests and do things like crop the incoming data at a predefined character. In this case that character needed to be CR (Carriage Return) this is ASCIIcharacter 13 because LAWCIEL terminates everything with one.

Labview Lynx2

For the USBtin we open the correct COM port at 115200,8 Data bits, No parity, 1 stop bit and no flow control. The other thing to note is at the top right, this sets the termination character to the numeric value of CR, the benefit here is you can perform a read of any length and it will automatically break up the data in the buffer so a single read can vary in length but the start will always synchronise  with the read call. Opening the connection in a terminal program for the first time and you’ll see nothing actually happens as such, an OK is signified by a CR so all you see is the cursor move. At this stage we are only connected to the USBtin, not the CANbus. So next, configure the CAN connection, send a value of “S6\r” . The code is S6, this will set the USBtin to 500kbit correct for the dash, the \r is how you indicate a CR in a Labview string. Next I chose to send “v\r” which requests the version from the USBtin, we don’t need this but it gives a solid confirmation it’s talking to us. Next up Z1\r tells the USBtin to timestamp the incoming data, I thought this might be useful but never actually implemented it.

Labview Lynx3

With the setup complete we can start reading data by opening the CAN connection by sending O\r. On a terminal program (assuming you have the CANbus wired up) doing this would result in packets of can data from the cluster appearing. The initial read of length 1 byte reads just the confirmation from the USBtin that it has received the open request. Next is the main read, it’s worth noting the read length is set at 50 bytes but this will be cut short by the termination character set earlier so we can accept varying length CAN data. C\r closes the CAN connection and again another read 1 byte clears the acknowledge. Tacked on the end is a section to read the controller status looking for error states etc. The keen eyed amongst you will notice the majority of this code is conditional, this is because the code needs to insert send requests among the stream of reads. This is because if the data is not read from the USBtin constantly a buffer somewhere fills (I imagine on the USBtin itself but can’t confirm this) and the port crashes. I spent a lot of time finding this out the hard way!

Labview Lynx4This is the write data code, again very similar but it just opens the port, writes whatever string is in the buffer and closes the connection. Once the connection is confirmed closed it resets the Boolean that causes the ‘write’ condition so on the next loop it goes back to reading again to keep the buffer clear. The read loop runs at the maximum possible speed but it is slowed down because it waits for either a termination character or 50 characters to be received before it completes and loops again.

Beyond that the only other bits of code just generate the data for the write buffer using an increment counter for the ID field and toggling between either 8 bytes of FFFF or 0000 every 100ms for 20 cycles and setting the write flag high to send the data..

So after letting this run for a fair while it started spitting out values, specifically the ID 300 for the power steering light. Wait a minute that wasn’t in the list earlier. Yes I know that, that’s where getting enough sleep comes in. Originally I split the data based on whether or not the PCM was fitted and ignored the ones that occurred in both sets, the obvious mistake here is that of course the power steering light isn’t controlled by the PCM, quite logically it’s controlled by the power steering controller!

So there we go, ID 300, the first byte (leading) controls the light, values below 80 turn the light off. Unplugging the PCM causes the controller to send 80 on loop hence the the warning light.

Get data from ID: 4B1
0	0	0	0	0	0	0	0	
-----------------------------
Get data from ID: 81
43	6F	1	4B	
-----------------------------
Get data from ID: 300
80	
-----------------------------
Get data from ID: 231
F	0	FF	FF	0	
-----------------------------
Get data from ID: 430
95	5F	83	0	0	0	60	
-----------------------------
Get data from ID: 81
25	6F	1	4B	
-----------------------------
Get data from ID: 81
16	6F	1	4B	
-----------------------------
Get data from ID: 630
8	0	0	0	0	0	6A	6A

Looking at the log data again we see that ID 300 is getting a value of 80 – this is during the self test before the engine is started. I previously tried sending this data on the original Arduino CAN setup and go no result so what did I do wrong. Again this is based on another assumption, I though the logger was ignoring high order bytes with zero value (ie this value if could be 00-00-00-00-00-00-00-80) well it turns out that was totally wrong, it actually ignores trailing zeros, the value of the can data here should be 80-00-00-00-00-00-00-00.

So while all these hours of work told me one thing I should have already known it’s actually worked out Ok because it highlighted this other problem (read ‘incorrect assumption’ !). This means The odds of me working out all the other data from the logs (that I’d previously written off as not usable) is actually much higher!

 

 

RX8 Project – Part 6, Canbus #2

So now we have a powered up RX8 cluster and some Canbus hardware that we can talk to we can start doing a few interesting things with our new setup.

Fire up the Arduino IDE and we can start writing our basic controller code. Canbus devices have a device address and a memory location for all the required data so we need to work all of these out. This is a lot easier for digital IO than the more complicated analog values because digital IO are just either set bit on or off whereas analog values can have funny scaling and things going on. I initially used a canbus demo program which worked because I basically cloned the Arduino Canbus shield. Plenty of starter guides and information can be found here : https://learn.sparkfun.com/tutorials/can-bus-shield-hookup-guide

My initial approach involved setting the target address for the Canbus transmission and sending patterns of bits high and low and see what happens. Google told me that the cluster Canbus interface operates on 500 kHz clock so with that information we should get a connection.

#include <Arduino.h>
#include <mcp_can.h>
#include <mcp_can_dfs.h>

#define CANint 2
#define LED2 8
#define LED3 7

MCP_CAN CAN0(10); // Set CS to pin 10

void setup() {
 // put your setup code here, to run once:
 Serial.begin(115200);
 Serial.println("Init…");

Serial.println("Setup pins");
 pinMode(LED2, OUTPUT);
 pinMode(LED3, OUTPUT);
 pinMode(CANint, INPUT);

Serial.println("Enable pullups");
 digitalWrite(LED2, LOW);
 Serial.println("CAN init:");
 
 if (CAN0.begin(CAN_500KBPS) == CAN_OK) 
 {
 Serial.println("OK!");
 } 
 else 
 {
 Serial.println("fail :-(");
 while (1) 
 {
 Serial.print("Zzz… ");
 delay(1000);
 }
 }

Serial.println("Good to go!");
}

unsigned char offarray[8] = {0, 0, 0, 0, 0, 0, 0, 0}; // Always Off Array
unsigned char onarray[8] = {255,255,255,255,255,255,255,255}; // Always On Array

void loop() {
 // put your main code here, to run repeatedly:

for (int i=0; i <= 512; i++){ 
  for (int l=0; l <= 10; l++){  
    for (int j=0; j <= 100; j++){
       CAN0.sendMsgBuf(i, 0, 8,offarray);
       delay(10);
     }

     for (int k=0; k <= 100; k++){
     CAN0.sendMsgBuf(i, 0, 8,onarray);
     delay(10);
     }
   }
  }
}

So this loop will iterate through 512 addresses strobing all 8 bytes high and low on 1 second intervals. The trick here is that canbus needs regular packets to stay active, so we can’t just send a value once. In this case each value (high or low) is send 100 times at 10ms intervals so the cluster should stay active long enough to see if anything is happening. Each address will get 10 cycles of high and low before moving onto the next address. In concept this would work and provided ‘i’ is set to increment high enough you would cover all the addresses. Bear in mind at this rate you’d have to watch it for about 90 mins…

Thankfully around the time I started running though this trying to find any useful addresses I came across this :

https://www.cantanko.com/rx-8/reverse-engineering-the-rx-8s-instrument-cluster-part-one/

This nicely tied in with the hardware I was working on and so the code and information is all exceedingly useful and saved me a lot of time. Specifically it gives the address locations for most of the indicators on the cluster which is what we really need.

After lots of trial and error I managed to come up with a block of code that can control the cluster but easily allow me to set any warning light I need on the dash, or more accurately also to turn off all the warning lights. Something that will be essential come MOT time as a warning light that stays on is an MOT fail and since most of the systems that control them will no longer be in the car (primarily the original ECU).

#include <Arduino.h>
#include <mcp_can.h>
#include <mcp_can_dfs.h>


#define COMMAND 0xFE
#define CLEAR 0x01
#define LINE0 0x80
#define LINE1 0xC0

#define CANint 2
#define LED2 8
#define LED3 7

#define NOP __asm__ ("nop\n\t")

// Variables for StatusMIL
bool checkEngineMIL;
bool checkEngineBL;
byte engTemp;
byte odo;
bool oilPressure;
bool lowWaterMIL;
bool batChargeMIL;
bool oilPressureMIL;

// Variables for PCM
byte engRPM;
byte vehicleSpeed;

// Variables for DSC
bool dscOff;
bool absMIL;
bool brakeFailMIL;
bool etcActiveBL;
bool etcDisabled;


MCP_CAN CAN0(10); // Set CS to pin 10

void setup() 
{
    
    //Serial.begin(115200);
    //Serial.println("Init…");
    //Serial.println("Setup pins");
    
    pinMode(LED2, OUTPUT);
    pinMode(LED3, OUTPUT);
    pinMode(CANint, INPUT);

    //Serial.println("Enable pullups");
    digitalWrite(LED2, LOW);
    //Serial.println("CAN init:");
    
    if (CAN0.begin(CAN_500KBPS) == CAN_OK) 
    {
        //Serial.println("OK!");
    } 
    else 
    {
        //Serial.println("fail :-(");
        while (1) 
        {
            //Serial.print("Zzz… ");
            delay(1000);
        }
     }

Serial.println("Good to go!");
}

unsigned char stmp[8]       = {0, 0, 0, 0, 0, 0, 0, 0};                         // Always Off Array
unsigned char otmp[8]       = {255,255,255,255,255,255,255,255};                // Always On Array

unsigned char statusPCM[8]  = {125,0,0,0,156,0,0,0};                            // Write to 201
unsigned char statusMIL[8]  = {140,0,0,0,0,0,0,0};                              // Write to 420
unsigned char statusDSC[8]  = {0,0,0,0,0,0,0,0};                                // Write to 212

unsigned char statusEPS1[8] = {0x00,0x00,0xFF,0xFF,0x00,0x32,0x06,0x81};        // Write to 200 0x00 00 FF FF 00 32 06 81
unsigned char statusEPS2[8] = {0x89,0x89,0x89,0x19,0x34,0x1F,0xC8,0xFF};        // Write to 202 0x89 89 89 19 34 1F C8 FF

unsigned char statusECU1[8] = {0x02,0x2D,0x02,0x2D,0x02,0x2A,0x06,0x81};        // Write to 215 - Unknown
unsigned char statusECU2[8] = {0x0F,0x00,0xFF,0xFF,0x02,0x2D,0x06,0x81};        // Write to 231 - Unknown
unsigned char statusECU3[8] = {0x04,0x00,0x28,0x00,0x02,0x37,0x06,0x81};        // Write to 240 - Unknown
unsigned char statusECU4[8] = {0x00,0x00,0xCF,0x87,0x7F,0x83,0x00,0x00};        // Write to 250 - Unknown


/*

215 02 2D 02 2D 02 2A 06 81 // Some ECU status

231 0F 00 FF FF 02 2D 06 81 // Some ECU status

240 04 00 28 00 02 37 06 81 // Some ECU status

250 00 00 CF 87 7F 83 00 00 // Some ECU status

*/


void updateMIL()
{
    statusMIL[0] = engTemp;
    statusMIL[1] = odo;
    statusMIL[4] = oilPressure;
    
  if (checkEngineMIL == 1)
  {
    statusMIL[5] = statusMIL[5] | 0b01000000;
  }
  else
  {
    statusMIL[5] = statusMIL[5] & 0b10111111;
  }
   
    if (checkEngineBL == 1)
  {
    statusMIL[5] = statusMIL[5] | 0b10000000;
  }
  else
  {
    statusMIL[5] = statusMIL[5] & 0b01111111;
  }

   if (lowWaterMIL == 1)
  {
    statusMIL[6] = statusMIL[6] | 0b00000010;
  }
  else
  {
    statusMIL[6] = statusMIL[6] & 0b11111101;
  }

     if (batChargeMIL == 1)
  {
    statusMIL[6] = statusMIL[6] | 0b01000000;
  }
  else
  {
    statusMIL[6] = statusMIL[6] & 0b10111111;
  }

       if (oilPressureMIL == 1)
  {
    statusMIL[6] = statusMIL[6] | 0b10000000;
  }
  else
  {
    statusMIL[6] = statusMIL[6] & 0b01111111;
  }
}

void updatePCM()
{
    statusPCM[0] = engRPM;
    statusPCM[4] = vehicleSpeed;
}

void updateDSC()
{       
  if (dscOff == 1)
  {
    statusDSC[3] = statusDSC[3] | 0b00000100;
  }
  else
  {
    statusDSC[3] = statusDSC[3] & 0b01111011;
  }

    if (absMIL == 1)
  {
    statusDSC[4] = statusDSC[4] | 0b00001000;
  }
  else
  {
    statusDSC[4] = statusDSC[4] & 0b11110111;
  }

      if (brakeFailMIL == 1)
  {
    statusDSC[4] = statusDSC[4] | 0b01000000;
  }
  else
  {
    statusDSC[4] = statusDSC[4] & 0b10111111;
  }

     if (etcActiveBL == 1)
  {
     statusDSC[5] = statusDSC[5] | 0b00100000;
  }
  else
  {
    statusDSC[5] = statusDSC[5] & 0b11011111;
  }

    if (etcDisabled == 1)
  {
    statusDSC[5] = statusDSC[5] | 0b00010000;
  }
  else
  {
    statusDSC[5] = statusDSC[5] & 0b11101111;
  }
 

}

void loop() 
{
    // StatusMIL
    engTemp         = 145;
    odo             = 0;
    oilPressure     = 1;    // Either 0 (fault) or >=1 (Ok)
    checkEngineMIL  = 0;
    checkEngineBL   = 0;
    lowWaterMIL     = 0;
    batChargeMIL    = 0;
    oilPressureMIL  = 0;

    updateMIL();
    CAN0.sendMsgBuf(0x420, 0, 8, statusMIL);
    delay(10);


    // StatusPCM
    engRPM          = 60;    // RPM  Value*67 gives 8500 RPM Reading Redline is 127
    vehicleSpeed    = 93;    // Speed  Value=0.63*(Speed)+38.5

    updatePCM();
    CAN0.sendMsgBuf(0x201, 0, 8, statusPCM);          //CAN0.sendMsgBuf(CAN_ID, Data Type (normally 0), length of data, Data
    delay(10);

    // StatusDSC
    dscOff          = 0;
    absMIL          = 0;
    brakeFailMIL    = 0;
    etcActiveBL     = 0;    // Only works with dscOff set
    etcDisabled     = 0;    // Only works with dscOff set

    updateDSC();
    CAN0.sendMsgBuf(0x212, 0, 8, statusDSC);
    delay(10);

/*
    CAN0.sendMsgBuf(0x200, 0, 8, statusEPS1);
    delay(10);            

    CAN0.sendMsgBuf(0x202, 0, 8, statusEPS2);
    delay(10);    
           
    
    CAN0.sendMsgBuf(0x215, 0, 8, statusECU1);
    delay(10);  

    CAN0.sendMsgBuf(0x231, 0, 8, statusECU2);
    delay(10);  

    CAN0.sendMsgBuf(0x240, 0, 8, statusECU3);
    delay(10);  
    CAN0.sendMsgBuf(0x250, 0, 8, statusECU4);
    delay(10);  

*/
            
 }

This is the current latest version of the code I have, the serial comms section at the start is handy for checking you have a working connection but if you don’t have the serial monitor open it will cause the Arduino to hang at the transmit instruction and not get as far as updating the canbus so I recommend enabling these initially but once you get a working connection comment them back out as above. There are also sections above taken directly from the Cantanko page to deal with the power steering controller and some other bits, I haven’t tried these yes as the cluster isn’t in a car but I plan do so at some stage.

This code has some limitations in this form, basically the warning lights etc are set in the code as static values so this code will just set everything normal and all the lights I have addresses for to off. One particular curiosity is the way the cluster treats the oil pressure value. The data space actually has a whole byte for a value here but the cluster only only has two values despite having an actual gauge, a value of 0 drops the needle to the bottom, a value of 1 or more sets the gauge to normal. My feeling on this is the car was originally intended to have a proper oil pressure transmitter but someone decided it was too expensive and a normal pressure switch was used instead and rather than redesign the cluster they just changed the scaling in the on board microcontroller to behave as described. long term I’d love to mod the cluster to add the analog functionality back in but without the code for the controller this would probably involve disconnecting the original drive to that gauge and adding another Arduino inside the cluster just for driving that gauge. It seems like a lot of work to know oil pressure on a scale of low to high!

The code also sets a speed and RPM however the ODO and trip meters will not increment as it stands – I purposefully left this bit of code out because it’s only really applicable if you’re on an actual car otherwise your cluster will just keep clocking up. The ODO and trips seemed to be the one bit no-one else had managed to do or identify but after quite a few hours of testing  random blocks will various combinations of high and low as well as sending incrementing / decrementing values I noticed my trip meter had actually moved up by 0.1 miles which meant something was working. I eventually managed to trace it to the address listed above. The trick to actually making it work is that the value in the byte is irrelevant to what the cluster displays, so no this doesn’t let you clock it back! What it appears to do is counts the changes to the value so I made it work by setting up a loop of code that keeps changing this value up to a certain number to give a specific distance. Turns out the cluster needs to see 4140 changes per mile. Eventually I plan to tie this in to the speedometer value so only one value needs to be set, by knowing the pulses per mile and the speed we can work out the time delay between each change. As an example at 100mph we need to send 115 changes per second, this is one every 8.7ms which is obviously faster than the loops above and so for speed and accuracy would have to be hardware timed with an interrupt to do the update at the right speed. I’ll probably do an updated version at a later date.

There’s a couple other things I need to sort out, the airbag warning light being the prime one because this is an outright MOT fail. Now in theory when it’s back on the car it should work just fine as the airbag wiring is all there but I figured it was worth working out anyway. So after working through all of the RX8 cluster wiring diagrams (1)(2)(3)(4) I pieced this together :RX8 Cluster Pinout

This covers the majority of everything someone might need to use the cluster on a simulator apart from the fuel gauge. The RX8 fuel gauge is a bit of an oddity because the car uses a saddle shaped fuel tank to allow it to clear the driveshaft and so it has two senders denoted as ‘main’ and ‘sub’.

RX8 Fuel System Senders

Each of these apparently has a resistance value of between about 325Ω at the bottom and 10Ω at the top. There seems to be some questions about how this system works out what to display based on the two sender values and whether it expects to see one remain full while the other empties or something similar. Unfortunately I’ve not played with it much because I’ve don’t actually need to make this work off the car (since the car will have the senders but I can say putting 100Ω a resistor between both 2R-2P and 2R-2T gives a value of about 1/3 of a tank and 9Ω (because I just added a 10Ω on along side the existing 100Ω) across these connections gives a full tank reading  (actually a little above full) so as long as you move both together it works fine, this is ok if all you want to do is get the fuel warning light off for a driving simulator but not that helpful for a single fuel sender conversion which is why most people are interested. If I get chance I’ll investigate further. Also for some reason it takes an absolute age to move up the scale which is a bit unfortunate but I suspect is as intended. At least it should be stable!

All of that leaved us with just one light I can’t get to turn of, the warning light for the electronic power steering. This is controlled via canbus by the look of the circuit diagram but as yet I’ve not found the address for it. If anyone knows the address please leave a comment! Else I will have to go back to either trial and error or the really dodgy backup plan which involves cutting the tracks in the cluster and doubling up on another warning light which isn’t used much! Maybe I should just add another controller in the cluster!

So hopefully that gives you (almost) everything you need to know about making the cluster work! We might get back to mechanical things in the next update…

RX8 Project – Part 5, Canbus

Following the decision to go for the complete engine swap I had the sudden realisation that nothing was going to work on the car because I wouldn’t have the factory engine and so also wouldn’t have the factory ECU and so no engine data and nothing on the instrument cluster. So I needed to come up with a better plan!

Initially I intended to open up the stock instrument cluster and replace the normal controller circuits inside the cluster with a microcontroller (probably an Arduino) with suitable supporting electronics such as motor driver IC’s to control the gauges and small MOSFET’s to switch the warning lights and anything else I needed to get it working. Done in that way I could interface almost anything to the Arduino and make the cluster behave however I wanted.

I started looking into what wiring the cluster had normally to see if I could hack into anything directly and not have to fake signals, say a nice direct tachometer connection I could use, but found that virtually everything on the cluster was controlled via a canbus communications connection and this gave me other ideas.RX8 ClusterFor anyone who doesn’t know canbus is a differential communications interface used in the majority of common cars to connect all the separate devices together. It is what’s called multi drop and as such all the devices are basically just daisychained on a single pair of wires. Complexity is the data is sent to a specific target device based on a device ID which we don’t know and the format the device needs the data in is to give a particular result is entirely up the manufacturer. So for example even if you found the memory location that controls RPM it might be using a single byte (256 possible values) to represent RPM in the range of 0-7500 rpm. This means that each increment would be a step of approximately 30 rpm which would likely not be noticeable. Or they might use two bytes at different addresses which combine to give the value, or something more complicated! Figure it all out and you can directly control the cluster!

The Arduino can be made to communicate with a canbus system with appropriate components added on. These are available as an off the shelf canbus shield and if you’ve not build circuits like this before I recommend that approach but since I have an electronics background I decided to DIY my own and save the money and so I started down the route of designing a canbus interface. I then had another interesting thought, if I had an engine ECU for the new engine it would be transmitting all sorts of data about what it was doing which would normally go to the cluster of the car it was from. I could work out what address it was sending that data too I could read it all. So suddenly one canbus interface grew to a pair of them so I could connect one to the engine ECU and one to the cluster and eventually use the pair of interfaces to bridge the two canbus connections and convert and reformat all the engine data in real time to control the cluster. Sounds simple right!

Based on information found via Google the chips to use seemed to be the MCP2515 transceiver and MCP2551 canbus line driver. Part of the reason behind this was there are well documented libraries available for the Arduino to work with these, plus they are available in DIP packages to make prototyping on stripboard easier!

Sadly I can’t seem to find the original circuit diagram I drew for this but it was closely based on this one (click to enlarge):Arduino MCP2515 Interface

I tend to use Arduino nanos in my projects, they are very small but have a good clock speed and all the handy features you’re likely to need for fairly small IO count projects. Plus you can buy the knock off version on eBay for very little, my last ones were 5 for £12 delivered next day but can be had for less if bought from a Chinese seller but obviously with a much longer delivery. The down side of these is they make them cheaper by not using the proper FTDI branded USB to serial converter chip, instead they use the CH340G which means the driver isn’t included with the normal Arduino IDE and must be downloaded and installed manually. The appropriate driver can be found from a number of sources via Google.

After quite a bit of work I ended up with something that looked a bit like this (ok, exactly this!)

Arduino Canbus adapter top Arduino Canbus adapter bottom

The red and black coloured solder lines are to distinguish the 5V power from the Arduino from everything else to make sure I hadn’t accidentally shorted  it out. Needless to say there’s quite a bit going on on the bottom of this board! The converter board has options for either screw terminal connectors or the 9 way DSUB used for certain applications. I’ve also included options for connecting power into the DSUB if required. On particularly important feature is the termination resistor, this is situated next to the blue jumper, moving the jumper turns this on or off. Canbus must have this termination resistor connected between the two bus wires (called CANL(-) and CANH(+)) at each end of the bus, so if this is an end node on the bus it needs enabling. If it’s being added to the middle of an existing bus it should be disabled. Other keen people will notice there’s only one Arduino slot occupied, this is because by the time I took the photo it had been commandeered for another project but since I was still working with identifying signals in a single bus at a time it didn’t really matter. I did initial test with two Arduinos connected via canbus  with one transmitting and one receiving just to prove the hardware out.

Next up we need to connect the new hardware up to the RX8 cluster. This assumed it is disconnected from the car, if it isn’t you won’t need to tie in power to the cluster but I have no idea how the but will behave in this situation. If you experimenting as I am it’s advisable to disconnect the canbus pins to avoid problems or just disconnect the whole cluster. to make this work we need to use the smaller connector on the back only. In the diagram above you can see the pinout for this, the simple version is :

Upper Row going left to right
Spare
Spare
B/Y – Ignition Switched +12V
B – 0V (PSU Minus)
L/R – Permanent +12V
Spare

Lower Row
G/B – CANL
L/W – CANH
Spare
G/Y – Unknown
BR/B – Unknown
R/Y – Unknown

For testing purposes both +12V can be connected together and any normal 12VDC PSU (cheapo wall wart or anything else you have) can be used. Congratulations, you now have a powered up cluster you can work with!

Things will get technical in the next section Canbus #2