Electronic Instrument Cluster Schematics and Source - My 1976 Mazda RX-5 Cosmo Restoration


Home > My 1976 Mazda RX-5 Cosmo Restoration > Electronic Instrument Cluster Schematics and Source Buy Me A Coffee

Parts 47 - 51 of my 1976 Mazda RX-5 Cosmo restoration concentrate on converting and modernizing the instrument cluster. This includes converting the stock gauges from mechanical to stepper motor driven, lighting to LED, controls to electronic and replacing mechanical readouts with display screens. The new electronic cluster will receive CAN bus data generated by the MS3-Pro as well as other CAN devices in the vehicle.

Part 47: Electronic Instrument Cluster Conversion

View In Popup

View On YouTube

Part 48 - Electronic Instrument Cluster Conversion - Part 2

View In Popup

View On YouTube

Part 49 - Electronic Instrument Cluster Conversion - Part 3

View In Popup

View On YouTube

Part 50 - Electronic Instrument Cluster Conversion - Part 4

View In Popup

View On YouTube

Part 51 - Electronic Instrument Cluster Conversion - Part 5

View In Popup

View On YouTube

It's not awesome presenting schematics and code in video, and very time consuming. That stuff is documented here and this page will be updated as the cluster electronics and software is developed.

MS3-Pro CANbus Test Interfacing Circuit and Code

This entire adventure depends on being able to receive CANbus data from the MS3-Pro, which will carry almost all the information the gauge cluster needs to display. For example, engine RPM, coolant temp, vehicle speed and air/fuel ratio. The CANbus data available from the MS3-Pro is basically everything the ECU is receiving, calculating and sending so a load of useful stuff is available which can be displayed in a variety of ways. For example, a dash will receive RPM updates over the bus allowing it to display the engine speed via an analog tach dial, digital display, etc. Data can also be used internally. If the CAN data indicates a check engine condition exists, the appropriate data can be displayed to the driver.

Before going any further with the instrument cluster, I wanted to make absolutely sure I could receive and process the CAN data from the MS3-Pro. Of course I'm not the first one to read CAN data from a MegaSquirt, this just happens to be the first time I have ever done it.

To test the CANbus interface, I used the simple circuit below consisting entirely of an Ardunio Uno and MCP2515 CANbus module. The MCP2515 module is very common, available for a few dollars and easy to use. It consists of an MCP2515 CAN controller, TJA1050 CAN transceiver and support components. The MCP2515 communicates with the processor via SPI.

Schematic

MCP2515, Arduino Uno, MS3-Pro test circuit schematic

As the purpose of this circuit is to simply send a few CAN values received from the MS3-Pro to a computer via the serial port, power was supplied via the Arduino USB connection.

MS3-Pro CANbus Interfacing Test Source Code

In trying to read CAN values from the MS3-Pro via an Arduino Uno and MCP2515 CANbus module, I discovered that many of the MCP2515 modules are made with an 8Mhz crystal. If one uses the Arduino included CAN libraries, they assume a 16Mhz crystal. So while the library will report that communication with the CAN module has been achieved, no CANbus data will ever be read/written. So the solution is to either swap the crystal with a 16Mhz unit (pain in the butt), or use a library that is aware of the different clock frequency. I ended using the MCP_CAN library as it can run with the 8Mhz crystal.

I apologize for not commenting the code below. It was in all honesty just a copy and paste from a random example I found somewhere on the MS Extra Forums as I was searching for CAN libraries, and the example included with MCP_CAN. I'll explain a bit below.


// CAN bus libs
#include <SPI.h>
#include <mcp_can.h>

const int SPI_CS_PIN = 10;

MCP_CAN CAN(SPI_CS_PIN);                                    // Set CS pin

// init CAN bus vars
unsigned char Flag_Recv = 0;
byte len = 1;
byte buf[8];
char str[20];

unsigned int caseCan;

// def array size for input smoothing
const int sizeArray = 8;

// def struct for displaying fields
struct fieldDisp {
	boolean isFloat;
	boolean hasField;
	byte lenField;
	byte fieldX;
	byte fieldY;
	boolean roughBar;
	boolean smoothBar;
	byte lenBar;
	byte barX;
	byte barY;
	unsigned int barMax;
	byte index;
	unsigned int input;
	unsigned long total;
	unsigned int average[2];
	unsigned int raw[sizeArray];
};

// def disp vars
fieldDisp rpm = { false, true,  4,  0, 1, false, true, 20, 0, 0, 8500 };
fieldDisp tempH2O = { false, true,  3,  4, 3, false, false };
fieldDisp vel = { false, true,  3, 13, 1, false, false };
fieldDisp lambda = { true,  true,  3,  4, 2, false, false };
fieldDisp levelFuel = { false, false, 4, 16, 2, false, true,  4, 16, 2, 255 };

void setup()
{
	// connect serial at 115200
	Serial.begin(9600);

		// start can bus
	// baudrate = 500k
	if (CAN_OK == CAN.begin(CAN_500KBPS, MCP_8MHz))
	{
		   Serial.println("CAN BUS Shield init ok!");
	}
	else
	{
		  Serial.println("CAN BUS Shield init fail");
	}

	
	
}

void loop()
{
	// read and parse data from CAN Bus
	CAN.readMsgBuf(&len, buf);
	caseCan = CAN.getCanId();
	//Serial.println(caseCan);
	switch (caseCan) {
	case 1520:
		// RPMs are the 7th and 8th byte from CAN ID 1520
		rpm.input = 256 * buf[6] + buf[7];
		break;
	case 1522:
		// temperature is the 7th and 8th byte from CAN ID 1522
		// it is stored in F and must be divided by 10
		// convert from f to c because f is stupid
		tempH2O.input = ((256 * buf[6] + buf[7]) / 10 - 32) * 5 / 9;
		break;
	case 1551:
		//convert from AFR to lambda but keep as an int. Will convert to fload before displaying
		lambda.input = buf[0] / 0.147;
		break;
	}

	
	// print fields to display
	Serial.print("RPM: ");
	Serial.println(rpm.input);
	Serial.print("CLT: ");
	Serial.println(tempH2O.input);
	Serial.print("Lambda: ");
	Serial.println(lambda.input);


}


				

Beginning at the top, after the libraries are included, a constant is set up for the SPI CS pin as pin 10. Then the library is initialized with that pin. Some variables are set up to hold the length of the CAN message and buffer for the CAN message (str was never used). A structure is set up to hold the CAN message for easy housekeeping. Then several variables are created with that structure (RPM, tempH20, etc.) to hold CAN messages.

In setup, the CAN bus is started with CAN.begin(), passing the bus speed and the all important crystal speed MCP_8MHz. Then some serial info is printed out letting us know if the init was successful.

Down in the loop, over and over again CAN.readMsgBuf is called to check for available CAN data and populate the buffer if there is something to receive. A decision is based on the caseCan variable which was assigned the CAN ID a few lines earlier. If the CAN ID is 1520, that's the RPM broadcast. Here I actually comment as some math needs to be done to convert the byte values to a decimal RPM. That is done and assigned to rpm.input. Then the same thing is done for temperature at CAN ID 1522, and AFR at CAN ID 1551. This is the simplest, most dirty way of listening for CAN messages. We are instructing the MCP2515 to receive everything then snagging the data we want in code. A much better way is to use a mask filter (this is covered in the MCP_CAN examples) to instruct the MCP2515 to only listen for the specific values we want.

Once the can data has been received and processed a little, everything gets written out to the serial port via Serial.print(). The loop then recycles.

The end result is a continuous stream of RPM, Temp and AFR values out to the serial port, thus proving it is relatively simple to obtain data via the CANbus from an MS3-Pro for use in the gauge cluster.

Stepper Motor Test Circuit and Code

To test the Switec x27.168 and other micro steppers used to drive the gauge needles, I whipped up this simple stepper motor test based around an Arduino Uno, potentiometer, and A4988 stepper motor driver module. The idea being that this circuit and the associated code was a quick way to set the position of the stepper motor to the position of the potentiometer, allowing me to find out the best step timings and speed for each motor, plus have the satisfaction of seeing the gauge work. It's a pretty useful circuit to test any sort of bipolar stepper motor.

Schematic

Schematic of Arduino Uno A4988 micro stepper motor test circuit

The circuit itself is very simple and about the minimal configuration with an A4988 stepper driver module one can use. An Arduino Uno is powered by 12V through Vin, making use of the on board 5V regulator to supply 5V to the processor and the A4988 driver chip. The A4988 uses a separate supply for the motor, allowing you to drive motors with different voltage requirements than the chip itself. So that 12V gets split and fed to the A4988 VMOT pin, with a 100uF capacitor to filter as the current draw from the motor coils will tend to want to pull the supply down at the beginning of each step. A 50K pot connects to analog pin A0 of the Uno which will serve as a control to adjust the motor position.

Using the A4988 module is very easy. The reset (RST) and sleep (SLP) pins are tied together to keep them both low, enabling the chip and disabling sleep mode. And the stepper motor is simply connected to the outputs, matching up the 1A, 1B, 2A and 2B pins with the corresponding connection to the motor coils. With every pulse of the STEP pin sent from Arduino pin 6, the A4988 moves the motor by one step. The direction is controlled via the DIR pin, connected to pin 7 on the Uno. With pin 7 held low, the motor moves clockwise. Holding the pin high causes the motor to move counter-clockwise.

Stepper Motor Test Source Code

The very simple test program below runs on, well, basically any processor supported in the Arduino environment with the appropriate I/O pins. I used an Arduino Uno onto which I snapped a prototype shield with a small section of breadboard to assemble the simple circuit above.


//Cosmo Cluster Micro Stepper Test Program
//Aaron Cake, Copyright © 2019
//http://www.aaroncake.net/
//http://www.aaroncake.net/76cosmo/cluster

//Rights to modify, distribute, re-use are granted as long as credit to original author remains


// defines pins numbers
const int StepPin = 6;
const int DirectionPin = 7;

const int PotPin = A0;

//motor steps
const int MaxSteps = 680;                // X27 stepper (tach) max steps to send the motor from zero to full clockwise
//const int MaxSteps = 575;                //qx56 2a42 stepper (speedo) max steps to send the motor from zero to full clockwise
int MotorCurrentStepPosition = 0;           //current position of the motor
int MotorDirection = LOW;               //motor direction. HIGH = CCW, LOW = CW

const int MotorDirectionCCW = HIGH;           //friendly names for directions
const int MotorDirectionCW = LOW;

bool MotorPulsed = false;               //track whether motor has been pulsed in loop

//speed variables
const int StepSpeedFast = 375;              // x27 delay in microseconds
//const int StepSpeedFast = 840;              // qx56 delay in microseconds

int PotPosition = 0;
int PrevPotPosition = 0;
int long MappedPot = 0;

//timing variables

unsigned long PreviousMillis = 0;           //the last time the pot check ran



void setup() {

  pinMode(StepPin, OUTPUT);
  pinMode(DirectionPin, OUTPUT);


  //move the motor fully to position zero

  digitalWrite(DirectionPin, MotorDirectionCCW);        // set direction to CCW
                
  for (int x = 0; x < MaxSteps; x++) {            //now just pulse the step pin until MaxSteps reached
    digitalWrite(StepPin, HIGH);              //step pin on
    delayMicroseconds(StepSpeedFast);           //wait
    digitalWrite(StepPin, LOW);               //off
    delayMicroseconds(StepSpeedFast);           //wait
    
  }

  delay(500);                          //wait 500 ms. Need a slight wait

  //move the motor fully clockwise

  digitalWrite(DirectionPin, MotorDirectionCW);             //set direction to CW

  for (int x = 0; x < MaxSteps; x++) {                  //same as above
    digitalWrite(StepPin, HIGH);
    delayMicroseconds(StepSpeedFast);
    digitalWrite(StepPin, LOW);
    delayMicroseconds(StepSpeedFast);
    
  }

  delay(500);

  //and zero again

  digitalWrite(DirectionPin, MotorDirectionCCW);        //set direction to CCW

  for (int x = 0; x < MaxSteps; x++) {
    digitalWrite(StepPin, HIGH);
    delayMicroseconds(StepSpeedFast);
    digitalWrite(StepPin, LOW);
    delayMicroseconds(StepSpeedFast);
    
  }

  MotorCurrentStepPosition = 0;               //track the motor current position. Set to zero

  delay(2000);



}


void loop() {

  unsigned long CurrentMillis = millis();                //get the current time count

  if (CurrentMillis - PreviousMillis >= 300) {           //check the pot position every 1000 ms
    PreviousMillis = CurrentMillis;                  //update the time the check last ran
    PotPosition = analogRead(PotPin);                //  Read Potentiometer current value
    if ((PotPosition > PrevPotPosition + 3) || (PotPosition < PrevPotPosition - 3)) {     // Hysterisys of 3 to avoid jitter
      MappedPot = map(PotPosition, 0, 1023, 0, MaxSteps);        // map pot value to 0 - MaxSteps (full rotation)
      PrevPotPosition = PotPosition;                   // make this the previous pot value
    }
  }

  if (MappedPot > MotorCurrentStepPosition) {               //if pot position greater than motor position
    MotorDirection = MotorDirectionCW;                  //we need to move motor clockwise     
  
    digitalWrite(DirectionPin, MotorDirection);             //set the motor direction
    digitalWrite(StepPin, HIGH);                    //then just go through procedure to step it 1 movement
    delayMicroseconds(StepSpeedFast);
    digitalWrite(StepPin, LOW);
    delayMicroseconds(StepSpeedFast);
    MotorCurrentStepPosition++;                     //add 1 to MotorCurrentStepPosition to track motor position

    }
    
  if (MappedPot < MotorCurrentStepPosition) {               //if pot position less than motor, move CCW
    MotorDirection = HIGH;                        //set direction CCW

    digitalWrite(DirectionPin, MotorDirection);             //and same as above
    digitalWrite(StepPin, HIGH);
    delayMicroseconds(StepSpeedFast);
    digitalWrite(StepPin, LOW);
    delayMicroseconds(StepSpeedFast);

    MotorCurrentStepPosition--;                     //subtract 1 from motor current position to track stepper position

  }
 }

				

I try to keep my code fairly self explanatory via comments so I think the basic function is already well explained. As this is just test code, I didn't care particularly about using delay() and delayMicroseconds() to create waits in the program. However because these are blocking calls, which quite literally just loop the processor for the supplied value of milli or microseconds, in the real gauge control code this will need to be done via a state machine. Nothing else can happen on the processor while a delay is running which obviously would be an issue for a processor needing to receive inputs from the CAN bus, control multiple motors, possibly take user input, etc.

The StepSpeedFast variable contains the microsecond delay between step pulses sent to the motor, essentially the speed at which the processor commands the motor to move. The x27.168 steppers are quite fast with a specification of 600 degrees of movement per second according to the datasheet. The other steppers I used, the "qx56", are about half as slow. So the step speed needs to be set for each motor. They also have a different number of total steps in one rotation, as set by the MaxSteps variable.

Note that these little micro steppers are designed to be used in gauges where it is expected they will be zeroed by simply banging them into the end stops. The x27.168 has a hard stop inside the motor. I haven't pulled apart a "qx56" however they seem to have a clutch inside.

PCB Patterns For Cluster Components

One-off PCBs were created for the cluster components. The one-off PCBs were all created in the excellent Robot Room Copper Connection software. Unfortunately, ExpressPCB bought Copper Connection and has renamed it to ExpressPCB Plus, at the same time removing the ability to easily print PCB patterns for toner transfer. This sort of behavior is rather disgusting. Thankfully the old version still works. So if you somehow found a zipped copy of it, it can be manually extracted into the Program Files directory and run without violating the license of the free version.

The backlights for the tach and speedometer gauges use two roundish boards mounting a series of through hole WS2812B addressable LED modules, breaking them out to a 4 pin connector. The shape as created by scanning the gauge face, converting it to a black and white image, then pasting an inverted slightly scaled down version of itself onto itself...thus creating an outline.

Tach Backlight PCB Speedometer Backlight PCB
Image of printed circuit board pattern of the tach backlight board Image of speedometer backlight printed circuit board pattern
Download (.ZIP, 7K) Download (.ZIP, 7K)

The fuel and temperature gauges are combined into one unit, with little space to house all the components below the gauge face. So a single PCB was designed to mount the two stepper motors required, and all of the WS2812B LEDs used as the gauge backlight. Additionally the gauge face contains a large red "idiot" light in the middle so another LED was placed behind that area to mimic that function. Small PCBs also had to be made for the rotary encoder, as well as the tach and speedometer steppers to provide mounting for the wiring connector.

Fuel & Temp Stepper & Backlight PCB Speedometer Backlight PCB
Image of fuel and temp gauge backlight and PCB pattern Image of tach and speedo stepper motor PCBs, rotary encoder PCB
Download (.ZIP, 7K) Download (.ZIP, 6K)

I very nearly forgot turn signal indicators! The originals were mounted to the big factory PCB on the rear of the cluster. With that gone, no indicators. So I built this quick board housing four WS2812B LEDs (two per signal indicator) and the necessary capacitors.

Each of the boards containing WS2812B LEDs has a 4 pin connector which supplies power and ground on two pins 1 and 2, the LED data input on pin 3, the LED data output on pin 4. In this way the LED connection can be daisy changed between boards.

Turn Signal Indicator PCB
Image of turn signal indicators PCB pattern
Download (.ZIP, 5K)

Instrument Cluster Controller

To control the instrument cluster I designed a custom board based on the Atmega 1284 microcontroller, MCP2515 CAN controller (with TJA1051T line driver), A4988 stepper drivers and LM2674M-5.0 switching 5V voltage regulator. This board is able to read the CAN bus, perform the necessary processing, then drive the outputs as necessary to run the motors, screens and LEDs on the cluster.

Image of cluster controller PCB board, 3D render

At first the plan was to use an off the shelf development board but after I became experienced with SMD soldering for other projects I realized I could make something from scratch far more integrated. Top on the list of considered boards was the STM32F103C8T6 based "Blue Pill". Despite having plenty of memory, a fast processor and integrated CAN controller it was disqualified because there are no Arduino framework CAN libraries and I was not willing to learn a completely new programming framework. ESP8266 and ESP32 were considered as they are fast and have plenty of RAM. What they lack are all the I/O pins necessary and the WiFi functionality is not necessary. I immediately discounted the Arduino Uno and Nano as the Atmega 328 doesn't have enough I/O nor memory for the application. The Arduino Mega with its Atmega 2560 has plenty of I/O, lots of RAM, but is just way too huge physically.

I ended up choosing the Atmega1284 as it provides all the necessary I/O pins (32), in a reasonable physical size (44TQFP), with 16K of SRAM, 128K of program space, 2 UARTs, SPI and I2C. Plus as it is an AVR, it has excellent Arduino library support so I could be sure that any peripherals (such as screens, CAN controller) would be easy to use.

The peripherals which this board controls are:

Peripheral Chip Bus / Pins
CAN Controller MCP2515 SPI Bus, 1 GPIO (CS)
1.8" TFT (Odometer) ST7735 SPI Bus, 3 GPIO
1.8" TFT (Trip) ST7735 SPI Bus, 3 GPIO
Stepper Motor 1 A4988 3 GPIO
Stepper Motor 2 A4988 3 GPIO
Nextion HMI Serial 1 UART
Rotary Encoder None, Quadrature 3 GPIO
IGN 12V Sense 4N35 Optoisolator 1 GPIO
I2C Breakout none I2C Bus

Schematic

Image of schematic of cluster controller circuit
Click/tap to enlarge

Breaking the circuit down, it is fairly simple as most of it simply follows the datasheets of the components used with some minor tweaks.

On the left, the Atmega1284p is set up with the standard support components likely familiar to anyone who has designed with these processors before or looked at the schematic of their favourite Arduino board. This includes the 16MHz crystal with 22pF load capacitors to run the clock, the 6 pin ICSP programming header tied to the SPI lines, power lines and reset pin 4. The ADC needs a bypass capacitor on AREF to assure a stable analog to digital conversion even though I'm not using it (nicer than tying it to ground). And all the 0.1uF capacitors bypassing all the power supply pins. Notice also the LED on pin PB4 because it is very handy to have a LED to blink, and I had a few spare I/O pins (which I should have tied to ground via 10K resistor instead of floating them).

Down at the bottom left, there is a 12V to 5V switching power regulator based on the LM2674M-5.0. The circuit starting at C8 and moving right is straight from the datasheet using the recommended values. Input and output capacitors C8 and C12 are tantalum. The catch diode D3 is a Schottky type due to the fast switching and low forward voltage drop (specifically requested in the application notes). Inductor L1 came right from the application notes chart, chosen for the 5V output with maximum 1A output current. At the input of the power supply circuit I've placed a MOV (ERZ-VF2M220) to shunt any spikes to ground, a reverse bias diode D1 to send any low voltage reverse spikes to ground and to blow the fuse if the circuit is connected polarity backwards. D2 prevents any damage to the circuit if connected backwards. Then at the output of D2, I take a 12V (actually about 0.7V lower than battery voltage) as required by the stepper motor drivers.

To the right of the power supply, the CAN interfacing is performed by the MCP2515 SPI CAN controller which then uses the TJA1051T transceiver to connect to the CANbus. This circuit is straight from the common Arduino CAN modules because there honestly isn't much needed to make these chips work. The MCP2515 just requires a crystal to run the clock with appropriate load capacitors, a bypass capacitor (C13, 0.1uF) on the power supply line, and the reset line (pin 17) held high. I should have tired the reset line to a spare pin on the Atmega 1284 so I could perform a hardware reset of the MCP2515 if necessary (the chip does allow a software reset via SPI bus). The TJA1051T just connects to the MCP1515 RX/TX lines, and provides CANH and CANL outputs to drive the CAN bus. CANH/CANL connect to the vehicle harness connector which also provides power (12V) to the circuit. The MCP2515 connects to the processor via the SPI bus with the CS (chip-select) line held high via 10K pullup and on pin PB4 (Arduino pin 10).

Left of the harness connector, a 4N35 opto-isolator is used to provide an "IGN on" signal to the processor. The LED side of the opto connects through a 1K series resistor to the harness connector. The open-collector output on the transistor side is held high via a 4.7K resistor and connects to processor pin PD7 (Arduino pin 31).

Back to the top of the schematic, two A4988 microstepping bipolar stepper drivers run the gauge steppers. These are very easy to use because they do the dirty work of figuring out how to drive the motor coils, requiring only a few inputs from the controlling processor. RESET and SLEEP are tied together as per the datasheet to keep the driver away. The MS1 - MS3 lines, which select the type of motor stepping to perform, are left floating so the internal pull downs keep them all low. This configures the driver for full step mode. These tiny steppers are highly geared so setting any of the microstep modes would just slow them down without providing more positioning accuracy. The driver requires two different power supplies: VDD to provide 5V to the logic, and VMOT to provide motor drive voltage which in this case is 12V. The outputs of the driver are routed directly to a connector into which the motor cable will plug. The ENABLE, STEP and DIR lines are how the processor controls the motion of the motor. The ENABLE line enables the stepper driver outputs when low, and disables them when held high. Useful because the driver keeps the coils energized between movements to hold the motor in position. Not necessarily desirable behaviour if the instrument cluster is going to have a "sleep" mode. Pulsing the STEP line moves the motor one step. Holding DIR low moves the motor one direction, bringing it HIGH moves it the opposite direction. So 3 pins per stepper driver are needed on the processor PA0 - PA5 (Arduino pins A7 - A2). The fact these pins are capable of taking an analog input is incidental; no analog inputs are needed on this controller.

Beside the stepper controllers on left, J11 is an output to the WS2812B RGB LEDs used as backlights and turn signal indicators. Just connector with 5V and ground, bypassed by a filter capacitor, and the data line to the LEDs driven by processor pin PB0 (4). All it takes is one data line to drive an unlimited number of LEDs as each LED receives data, takes its instructions, then passes the rest of the data onto the next.

At the far right, there are two TFT connectors to drive the odometer and trip displays. The displays have built in Sitronix ST7735 TFT controllers. This is an SPI bus based controller, so it is easy to use and supported by multiple Arduino libraries. The display is an output only device, so I break the MISO and SCLK SPI lines out to the display connector. Note each goes through a 1K resistor to drop the voltage down to the 3.3V level the ST7735 requires. Each display also requires a D/C (data/command select) connection, and CE (chip enable/chip select) line. These connect to PC3 and PC4 (Arduino pins 25,26) for the first display, and PC6/PC7 (Arduino pins 28/29) through the necessary 1K dropping resistors. CE (when brought low) is used to enable the chip to receive commands via the bus. The D/C line is used to tell the chip whether it is being sent a command (ie. change colour) or the data a command is to use (ie. colour). Finally, each display has a backlight which turns on when supplied with power. On the display modules I use, this connects directly to the backlight LEDs. So I use a P-channel MOSFET on each display as a high side switch to power the backlight via a 100 Ohm series resistor. The MOSFET gates connect to processor pin PC5 (Arduino pin 27) so the backlights can be switched on and off via software.

Nestled between the TFT connectors I also broke out the I2C bus to J8 with the necessary pullup resistor on each line. And added some filter capacitors to the to the power lines. While I don't have any I2C devices in the cluster I figured I may as well make the bus accessible either as I2C, or two I/O pins. Never know what one might need in the future and I wasn't using the two processor pins PC0 and PC1 (22, 23) for anything else.

Below the display connectors, J10 connects to the rotary encoder. As the rotary encoder includes 3 switches (clockwise, counterclockwise, shaft pushbutton) it needs a minimum of 4 connections. 5V to supply to one side of each switch, then a line from each switch to an I/O pin on the processor. I chose to send two 5V lines to the rotary encoder so that the harness between the encoder and board is just a straight through cable matching pin to pin, with no jumpers between pins needed at the rotary encoder side. Each input to the processor has a 0.01uF capacitor bypassing it to ground for noise filtering and a 10K resistor to keep it pulled low. Note that the two outputs from the encoder shaft, the quadrature, must connect to interrupt capable pins on the processor. This is so the processor will always respond to a user turning the encoder because the signal generates an interrupt, allowing the processor to register the turn immediately. As opposed to polling where the processor must continually check the pin state to see if it has changed and could thus miss inputs if, for example, it is processing a CAN message at the time. Therefore the two quadrature output pins connect to interrupt capable processor pins PD2 (INT0, Arduino pin 2) and PB2 (INT2, pin 6). The button switch doesn't need an interrupt because humans are slow so any button press is going to be at least 75mS, plenty of time for the processor to poll it and register a press.

And finally, near the bottom between the Atmega1284 and MCP2515 is J5. J5 brings out the UART0 serial port (PD0, PD1) to the Nextion HMI used as an "everything" display. It is an intelligent HMI (Human-Machine Interface) meaning it contains its own embedded processor to take care of running the display and taking user input. The host processor simply sends it commands over a serial port. So all that is needed to drive it is power and the serial lines. J5 includes the standard filter capacitors bypassing the power supply, and then the TX/RX serial lines. Note the lines need to cross: TX from the Atmega1284 connects to RX on the Nextion.

J9 brings out the serial TX pin of UART1 which is handy for debugging output because of course, a lot of software development is going to be needed to make this thing work.

As the board was assembled, each section was tested.

Cluster Controller CAN Controller Test Code

To test each section of the controller I mainly used slightly modified examples from the Arduino libraries for each peripheral. As such, I'm not going to explain the code here line by line as the examples are (mostly) well documented following the links to the corresponding library website. I'll just note any changes I made along the way.

The MCP2525 CAN controller was first tested in loopback mode using a slightly modified version of the example from the MCP_CAN Arduino library. Two modifications were made: the serial port was changed from Serial to Serial1, thus using UART1 on the Atmega1284. And the MCP_CAN library was initialized with the MCP_8MHZ parameter because I used an 8MHz crystal.



/* CAN Loopback Example
 * This example sends a message once a second and receives that message
 *   no CAN bus is required.  This example will test the functionality 
 *   of the protocol controller, and connections to it.
 *   
 *   Written By: Cory J. Fowler - October 5th 2016
 */

#include <mcp_can.h>
#include <SPI.h>

// CAN TX Variables
unsigned long prevTX = 0;                                        // Variable to store last execution time
const unsigned int invlTX = 1000;                                // One second interval constant
byte data[] = {0xAA, 0x55, 0x01, 0x10, 0xFF, 0x12, 0x34, 0x56};  // Generic CAN data to send

// CAN RX Variables
long unsigned int rxId;
unsigned char len;
unsigned char rxBuf[8];

// Serial1 Output String Buffer
char msgString[128];

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

 
void setup()
{
  Serial1.begin(115200);  // CAN is running at 500,000BPS; 115,200BPS is SLOW, not FAST, thus 9600 is crippling.
  
  // Initialize MCP2515 running at 8MHz with a baudrate of 500kb/s and the masks and filters disabled.
  if(CAN0.begin(MCP_ANY, CAN_500KBPS, MCP_8MHZ) == CAN_OK)
    Serial1.println("MCP2515 Initialized Successfully!");
  else
    Serial1.println("Error Initializing MCP2515...");
  
  // Since we do not set NORMAL mode, we are in loopback mode by default.
  //CAN0.setMode(MCP_NORMAL);

  
  
  Serial1.println("MCP2515 Library Loopback Example...");
}

void loop()
{
    CAN0.readMsgBuf(&rxId, &len, rxBuf);              // Read data: len = data length, buf = data byte(s)
    
    if((rxId & 0x80000000) == 0x80000000)             // Determine if ID is standard (11 bits) or extended (29 bits)
      sprintf(msgString, "Extended ID: 0x%.8lX  DLC: %1d  Data:", (rxId & 0x1FFFFFFF), len);
    else
      sprintf(msgString, "Standard ID: 0x%.3lX       DLC: %1d  Data:", rxId, len);
  
    Serial1.print(msgString);
  
    if((rxId & 0x40000000) == 0x40000000){            // Determine if message is a remote request frame.
      sprintf(msgString, " REMOTE REQUEST FRAME");
      Serial1.print(msgString);
    } else {
      for(byte i = 0; i<len; i++){
        sprintf(msgString, " 0x%.2X", rxBuf[i]);
        Serial1.print(msgString);
      }
    }
        
    Serial1.println();
  
  
  if(millis() - prevTX >= invlTX){                    // Send this at a one second interval. 
    prevTX = millis();
    byte sndStat = CAN0.sendMsgBuf(0x100, 8, data);
    
    if(sndStat == CAN_OK)
      Serial1.println("Message Sent Successfully!");
    else
      Serial1.println("Error Sending Message...");

  }
}

/*********************************************************************************************************
  END FILE
*********************************************************************************************************/


				

All this code does is enable the MCP2515 CAN controller, set it to loopback mode, then "send" a CAN packet every second and read the result. That will, of course, be the same packet. This proves the MCP2515 is up and running, communicating with the Atmega1284 via the SPI bus.

Once proved to work in loopback mode, it was time to connect the board to a live CANbus to test not only the MCP2515, but also the TJA1051T to prove full connectivity to the CANbus. I connected the board to the MS3-Pro CANbus and enabled "dash broadcasting" to send data (configurable values one would use for a dash display) out on the bus at a regular interval. At this point I don't care what the values are, I just want to confirm that the board can read from the live CANbus and send that data out on the serial port. The code is just slightly modified from the MCP_CAN library CAN receive example. The only changes I needed to make were to initialize the library using the MCP_8MHZ parameter, and change Serial to Serial1 as my debug info is going out on URAT1 instead of UART0.



// CAN Receive Example
//

#include <mcp_can.h>
#include <SPI.h>

long unsigned int rxId;
unsigned char len = 0;
unsigned char rxBuf[8];
char msgString[128];                        // Array to store Serial1 string

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


void setup()
{
  Serial1.begin(115200);
  
  // Initialize MCP2515 running at 16MHz with a baudrate of 500kb/s and the masks and filters disabled.
  if(CAN0.begin(MCP_ANY, CAN_500KBPS, MCP_8MHZ) == CAN_OK)
    Serial1.println("MCP2515 Initialized Successfully!");
  else
    Serial1.println("Error Initializing MCP2515...");
  
  CAN0.setMode(MCP_NORMAL);                     // Set operation mode to normal so the MCP2515 sends acks to received data.

    
  Serial1.println("MCP2515 Library Receive Example...");
}

void loop()
{
  
    CAN0.readMsgBuf(&rxId, &len, rxBuf);      // Read data: len = data length, buf = data byte(s)
    
    if((rxId & 0x80000000) == 0x80000000)     // Determine if ID is standard (11 bits) or extended (29 bits)
      sprintf(msgString, "Extended ID: 0x%.8lX  DLC: %1d  Data:", (rxId & 0x1FFFFFFF), len);
    else
      sprintf(msgString, "Standard ID: 0x%.3lX       DLC: %1d  Data:", rxId, len);
  
    Serial1.print(msgString);
  
    if((rxId & 0x40000000) == 0x40000000){    // Determine if message is a remote request frame.
      sprintf(msgString, " REMOTE REQUEST FRAME");
      Serial1.print(msgString);
    } else {
      for(byte i = 0; i<len; i++){
        sprintf(msgString, " 0x%.2X", rxBuf[i]);
        Serial1.print(msgString);
      }
    }
        
    Serial1.println();
  
}

/*********************************************************************************************************
  END FILE
*********************************************************************************************************/




				

All this does is read data from the CANbus, print out some info on that data (standard or extended frame, length, etc.) then send that data out on the serial port. The result was a continuous scrolling dump of the CANbus into the terminal (Tera Term) as fast as loop() would run. As it was now proved that the board could read live CANbus data (kind of the most important requirement) the rest of the assembly proceeded in stages with testing at each.

Cluster Controller TFT Screens Test Code

Next the TFT screen interfaces were added to the board. Testing the TFT screen was once again done with the example code. The Adafruit ST7735 library example was modified only to change the pin numbers defined to those required for my Atmega 1284 based board and add the digitalWrite (27, HIGH) statement to bring 27 high, enabling the backlight.


/**************************************************************************
  This is a library for several Adafruit displays based on ST77* drivers.

  Works with the Adafruit 1.8" TFT Breakout w/SD card
    ----> http://www.adafruit.com/products/358
  The 1.8" TFT shield
    ----> https://www.adafruit.com/product/802
  The 1.44" TFT breakout
    ----> https://www.adafruit.com/product/2088
  The 1.14" TFT breakout
  ----> https://www.adafruit.com/product/4383
  The 1.3" TFT breakout
  ----> https://www.adafruit.com/product/4313
  The 1.54" TFT breakout
    ----> https://www.adafruit.com/product/3787
  The 2.0" TFT breakout
    ----> https://www.adafruit.com/product/4311
  as well as Adafruit raw 1.8" TFT display
    ----> http://www.adafruit.com/products/618

  Check out the links above for our tutorials and wiring diagrams.
  These displays use SPI to communicate, 4 or 5 pins are required to
  interface (RST is optional).

  Adafruit invests time and resources providing this open source code,
  please support Adafruit and open-source hardware by purchasing
  products from Adafruit!

  Written by Limor Fried/Ladyada for Adafruit Industries.
  MIT license, all text above must be included in any redistribution
 **************************************************************************/

#include <Adafruit_GFX.h>    // Core graphics library
#include <Adafruit_ST7735.h> // Hardware-specific library for ST7735
#include <Adafruit_ST7789.h> // Hardware-specific library for ST7789
#include <SPI.h>
 #define TFT_CS        26
  #define TFT_RST        24 // Or set to -1 and connect to Arduino RESET pin
  #define TFT_DC         25
  // For the breakout board, you can use any 2 or 3 pins.
  // These pins will also work for the 1.8" TFT shield.
 

// OPTION 1 (recommended) is to use the HARDWARE SPI pins, which are unique
// to each board and not reassignable. For Arduino Uno: MOSI = pin 11 and
// SCLK = pin 13. This is the fastest mode of operation and is required if
// using the breakout board's microSD card.

// For 1.44" and 1.8" TFT with ST7735 use:
Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST);

// For 1.14", 1.3", 1.54", and 2.0" TFT with ST7789:
//Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_RST);


// OPTION 2 lets you interface the display using ANY TWO or THREE PINS,
// tradeoff being that performance is not as fast as hardware SPI above.
//#define TFT_MOSI 11  // Data out
//#define TFT_SCLK 13  // Clock out

// For ST7735-based displays, we will use this call
//Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_MOSI, TFT_SCLK, TFT_RST);

// OR for the ST7789-based displays, we will use this call
//Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_MOSI, TFT_SCLK, TFT_RST);


float p = 3.1415926;

void setup(void) {
  Serial1.begin(9600);
  Serial1.print(F("Hello! ST77xx TFT Test"));

  pinMode(27,OUTPUT);                     //enable the backlight by bringing pin 27 high, triggering the MOSFET
  digitalWrite(27,HIGH);

  // Use this initializer if using a 1.8" TFT screen:
  tft.initR(INITR_BLACKTAB);      // Init ST7735S chip, black tab

  // OR use this initializer if using a 1.8" TFT screen with offset such as WaveShare:
  // tft.initR(INITR_GREENTAB);      // Init ST7735S chip, green tab

  // OR use this initializer (uncomment) if using a 1.44" TFT:
  //tft.initR(INITR_144GREENTAB); // Init ST7735R chip, green tab

  // OR use this initializer (uncomment) if using a 0.96" 160x80 TFT:
  //tft.initR(INITR_MINI160x80);  // Init ST7735S mini display

  // OR use this initializer (uncomment) if using a 1.3" or 1.54" 240x240 TFT:
  //tft.init(240, 240);           // Init ST7789 240x240

  // OR use this initializer (uncomment) if using a 2.0" 320x240 TFT:
  //tft.init(240, 320);           // Init ST7789 320x240

  // OR use this initializer (uncomment) if using a 1.14" 240x135 TFT:
  //tft.init(135, 240);           // Init ST7789 240x135
  
  // SPI speed defaults to SPI_DEFAULT_FREQ defined in the library, you can override it here
  // Note that speed allowable depends on chip and quality of wiring, if you go too fast, you
  // may end up with a black screen some times, or all the time.
  //tft.setSPISpeed(40000000);

  Serial1.println(F("Initialized"));

  uint16_t time = millis();
  tft.fillScreen(ST77XX_BLACK);
  time = millis() - time;

  Serial1.println(time, DEC);
  delay(500);

  // large block of text
  tft.fillScreen(ST77XX_BLACK);
  testdrawtext("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur adipiscing ante sed
nibh tincidunt feugiat. Maecenas enim massa, fringilla sed malesuada et, malesuada
 sit amet turpis. Sed porttitor neque ut ante pretium vitae malesuada nunc bibendum.
 Nullam aliquet ultrices massa eu hendrerit. Ut sed nisi lorem. In vestibulum purus a
tortor imperdiet posuere. ", ST77XX_WHITE);
  delay(1000);

  // tft print function!
  tftPrintTest();
  delay(4000);

  // a single pixel
  tft.drawPixel(tft.width()/2, tft.height()/2, ST77XX_GREEN);
  delay(500);

  // line draw test
  testlines(ST77XX_YELLOW);
  delay(500);

  // optimized lines
  testfastlines(ST77XX_RED, ST77XX_BLUE);
  delay(500);

  testdrawrects(ST77XX_GREEN);
  delay(500);

  testfillrects(ST77XX_YELLOW, ST77XX_MAGENTA);
  delay(500);

  tft.fillScreen(ST77XX_BLACK);
  testfillcircles(10, ST77XX_BLUE);
  testdrawcircles(10, ST77XX_WHITE);
  delay(500);

  testroundrects();
  delay(500);

  testtriangles();
  delay(500);

  mediabuttons();
  delay(500);

  Serial1.println("done");
  delay(1000);
  
}

void loop() {
  tft.invertDisplay(true);
  delay(500);
  tft.invertDisplay(false);
  delay(500);
}

void testlines(uint16_t color) {
  tft.fillScreen(ST77XX_BLACK);
  for (int16_t x=0; x < tft.width(); x+=6) {
    tft.drawLine(0, 0, x, tft.height()-1, color);
    delay(0);
  }
  for (int16_t y=0; y < tft.height(); y+=6) {
    tft.drawLine(0, 0, tft.width()-1, y, color);
    delay(0);
  }

  tft.fillScreen(ST77XX_BLACK);
  for (int16_t x=0; x < tft.width(); x+=6) {
    tft.drawLine(tft.width()-1, 0, x, tft.height()-1, color);
    delay(0);
  }
  for (int16_t y=0; y < tft.height(); y+=6) {
    tft.drawLine(tft.width()-1, 0, 0, y, color);
    delay(0);
  }

  tft.fillScreen(ST77XX_BLACK);
  for (int16_t x=0; x < tft.width(); x+=6) {
    tft.drawLine(0, tft.height()-1, x, 0, color);
    delay(0);
  }
  for (int16_t y=0; y < tft.height(); y+=6) {
    tft.drawLine(0, tft.height()-1, tft.width()-1, y, color);
    delay(0);
  }

  tft.fillScreen(ST77XX_BLACK);
  for (int16_t x=0; x < tft.width(); x+=6) {
    tft.drawLine(tft.width()-1, tft.height()-1, x, 0, color);
    delay(0);
  }
  for (int16_t y=0; y < tft.height(); y+=6) {
    tft.drawLine(tft.width()-1, tft.height()-1, 0, y, color);
    delay(0);
  }
}

void testdrawtext(char *text, uint16_t color) {
  tft.setCursor(0, 0);
  tft.setTextColor(color);
  tft.setTextWrap(true);
  tft.print(text);
}

void testfastlines(uint16_t color1, uint16_t color2) {
  tft.fillScreen(ST77XX_BLACK);
  for (int16_t y=0; y < tft.height(); y+=5) {
    tft.drawFastHLine(0, y, tft.width(), color1);
  }
  for (int16_t x=0; x < tft.width(); x+=5) {
    tft.drawFastVLine(x, 0, tft.height(), color2);
  }
}

void testdrawrects(uint16_t color) {
  tft.fillScreen(ST77XX_BLACK);
  for (int16_t x=0; x < tft.width(); x+=6) {
    tft.drawRect(tft.width()/2 -x/2, tft.height()/2 -x/2 , x, x, color);
  }
}

void testfillrects(uint16_t color1, uint16_t color2) {
  tft.fillScreen(ST77XX_BLACK);
  for (int16_t x=tft.width()-1; x > 6; x-=6) {
    tft.fillRect(tft.width()/2 -x/2, tft.height()/2 -x/2 , x, x, color1);
    tft.drawRect(tft.width()/2 -x/2, tft.height()/2 -x/2 , x, x, color2);
  }
}

void testfillcircles(uint8_t radius, uint16_t color) {
  for (int16_t x=radius; x < tft.width(); x+=radius*2) {
    for (int16_t y=radius; y < tft.height(); y+=radius*2) {
      tft.fillCircle(x, y, radius, color);
    }
  }
}

void testdrawcircles(uint8_t radius, uint16_t color) {
  for (int16_t x=0; x < tft.width()+radius; x+=radius*2) {
    for (int16_t y=0; y < tft.height()+radius; y+=radius*2) {
      tft.drawCircle(x, y, radius, color);
    }
  }
}

void testtriangles() {
  tft.fillScreen(ST77XX_BLACK);
  uint16_t color = 0xF800;
  int t;
  int w = tft.width()/2;
  int x = tft.height()-1;
  int y = 0;
  int z = tft.width();
  for(t = 0 ; t <= 15; t++) {
    tft.drawTriangle(w, y, y, x, z, x, color);
    x-=4;
    y+=4;
    z-=4;
    color+=100;
  }
}

void testroundrects() {
  tft.fillScreen(ST77XX_BLACK);
  uint16_t color = 100;
  int i;
  int t;
  for(t = 0 ; t <= 4; t+=1) {
    int x = 0;
    int y = 0;
    int w = tft.width()-2;
    int h = tft.height()-2;
    for(i = 0 ; i <= 16; i+=1) {
      tft.drawRoundRect(x, y, w, h, 5, color);
      x+=2;
      y+=3;
      w-=4;
      h-=6;
      color+=1100;
    }
    color+=100;
  }
}

void tftPrintTest() {
  tft.setTextWrap(false);
  tft.fillScreen(ST77XX_BLACK);
  tft.setCursor(0, 30);
  tft.setTextColor(ST77XX_RED);
  tft.setTextSize(1);
  tft.println("Hello World!");
  tft.setTextColor(ST77XX_YELLOW);
  tft.setTextSize(2);
  tft.println("Hello World!");
  tft.setTextColor(ST77XX_GREEN);
  tft.setTextSize(3);
  tft.println("Hello World!");
  tft.setTextColor(ST77XX_BLUE);
  tft.setTextSize(4);
  tft.print(1234.567);
  delay(1500);
  tft.setCursor(0, 0);
  tft.fillScreen(ST77XX_BLACK);
  tft.setTextColor(ST77XX_WHITE);
  tft.setTextSize(0);
  tft.println("Hello World!");
  tft.setTextSize(1);
  tft.setTextColor(ST77XX_GREEN);
  tft.print(p, 6);
  tft.println(" Want pi?");
  tft.println(" ");
  tft.print(8675309, HEX); // print 8,675,309 out in HEX!
  tft.println(" Print HEX!");
  tft.println(" ");
  tft.setTextColor(ST77XX_WHITE);
  tft.println("Sketch has been");
  tft.println("running for: ");
  tft.setTextColor(ST77XX_MAGENTA);
  tft.print(millis() / 1000);
  tft.setTextColor(ST77XX_WHITE);
  tft.print(" seconds.");
}

void mediabuttons() {
  // play
  tft.fillScreen(ST77XX_BLACK);
  tft.fillRoundRect(25, 10, 78, 60, 8, ST77XX_WHITE);
  tft.fillTriangle(42, 20, 42, 60, 90, 40, ST77XX_RED);
  delay(500);
  // pause
  tft.fillRoundRect(25, 90, 78, 60, 8, ST77XX_WHITE);
  tft.fillRoundRect(39, 98, 20, 45, 5, ST77XX_GREEN);
  tft.fillRoundRect(69, 98, 20, 45, 5, ST77XX_GREEN);
  delay(500);
  // play color
  tft.fillTriangle(42, 20, 42, 60, 90, 40, ST77XX_BLUE);
  delay(50);
  // pause color
  tft.fillRoundRect(39, 98, 20, 45, 5, ST77XX_RED);
  tft.fillRoundRect(69, 98, 20, 45, 5, ST77XX_RED);
  // play color
  tft.fillTriangle(42, 20, 42, 60, 90, 40, ST77XX_GREEN);
}


				

As there are two screens, I just ran the demo twice, changing the pin numbers between tests to select the appropriate screen.

Cluster Controller Stepper Motor Driver Test Code

Next up were two A4988 stepper motor controllers. They don't require any fancy control, just 3 GPIO ports that are used to enable/disable, set direction, and tell the motor to step. So the code was just a copy-paste modified version of my original stepper test, set up to run two motors back and forth. I simply added a second set of variables to represent the 2nd motor.


// defines pins numbers
//first stepper J3
const int StepPin1 = 20;
const int DirectionPin1 = 19;
const int EnablePin1 = 21;

//2nd stepper J4
const int StepPin2 = 17;
const int DirectionPin2 = 16;
const int EnablePin2 = 18;


//motor steps
//const int MaxSteps = 680;                // X27 stepper (tach) max steps to send the motor from zero to full clockwise
const int MaxSteps = 575;                //qx56 2a42 stepper (speedo) max steps to send the motor from zero to full clockwise
int MotorCurrentStepPosition1 = 0;           //current position of the motor
int MotorDirection1 = LOW;               //motor direction. HIGH = CCW, LOW = CW

int MotorCurrentStepPosition2 = 0;           //current position of the motor
int MotorDirection2 = LOW;               //motor direction. HIGH = CCW, LOW = CW


const int MotorDirectionCCW = HIGH;           //friendly names for directions
const int MotorDirectionCW = LOW;

//speed variables
//const int StepSpeedFast = 375;              // x27 delay in microseconds
const int StepSpeedFast = 840;              // qx56 delay in microseconds


//timing variables



void setup() {

  pinMode(5,OUTPUT);
  digitalWrite(5,HIGH);

  
  pinMode(StepPin1, OUTPUT);
  pinMode(DirectionPin1, OUTPUT);

  pinMode(EnablePin1, OUTPUT);
  digitalWrite(EnablePin1, LOW);               //enable the driver by pulling pin low...HIGH = DISABLED


  pinMode(StepPin2, OUTPUT);
  pinMode(DirectionPin2, OUTPUT);

  pinMode(EnablePin2, OUTPUT);
  digitalWrite(EnablePin2, LOW);               //enable the driver by pulling pin low...HIGH = DISABLED


}


void loop() {

  digitalWrite(DirectionPin1, MotorDirectionCCW);        // set direction to CCW
  digitalWrite(DirectionPin2, MotorDirectionCCW);        // set direction to CCW
                
  for (int x = 0; x < MaxSteps; x++) {            //now just pulse the step pin until MaxSteps reached
    digitalWrite(StepPin1, HIGH);              //step pin on
    digitalWrite(StepPin2, HIGH);              //step pin on
    delayMicroseconds(StepSpeedFast);           //wait
    digitalWrite(StepPin1, LOW);               //off
    digitalWrite(StepPin2, LOW);               //off
    delayMicroseconds(StepSpeedFast);           //wait
    
  }

  delay(1500);                          

  //move the motor fully clockwise

  digitalWrite(DirectionPin1, MotorDirectionCW);             //set direction to CW
  digitalWrite(DirectionPin2, MotorDirectionCW);             //set direction to CW

  for (int x = 0; x < MaxSteps; x++) {                  //same as above
    digitalWrite(StepPin1, HIGH);
    digitalWrite(StepPin2, HIGH);
    delayMicroseconds(StepSpeedFast);
    digitalWrite(StepPin1, LOW);
    digitalWrite(StepPin2, LOW);
    delayMicroseconds(StepSpeedFast);
    
  }

  delay(1500);

}

				

And the result was two motors waggling back and forth slowly. This is blocking code due to all the delay() and delayMicroseconds() commands used so it isn't a viable method to control stepper motors when the code needs to do other things. As a demo or test however, it does the job. It is highly likely that on the final code that actually runs in the car I'll be using the excellent AccelStepper library to run my motors.

Cluster Controller Rotary Encoder Test Code

The final bit of test code is to verify the functionality of the rotary encoder. To read the encoder I turned to the aptly named Encoder Library and the example code provided. Note that I did have to make a modification to the library to define the Atmega 1284 interrupt pins as there was no definition included for that processor.


/* Encoder Library - Basic Example
 * http://www.pjrc.com/teensy/td_libs_Encoder.html
 *
 * This example code is in the public domain.
 * 
 * interruptpins.h modified to add:
 *ATMEL 1284 Bobduino (Aaron, Sept. 2020)
 *#elif defined(__AVR_ATmega1284__) || defined(__AVR_ATmega1284P__) 
 *#define CORE_NUM_INTERRUPT 3
 *#define CORE_INT0_PIN   2
 *#define CORE_INT1_PIN   3
 *#define CORE_INT2_PIN   6
 * 
 * Because library is not Atmega 1284 aware, so needed interrupt capable pins added
 * 
 */

#include <Encoder.h>

// Change these two numbers to the pins connected to your encoder.
//   Best Performance: both pins have interrupt capability

Encoder myEnc(2, 6);
//   avoid using pins with LEDs attached

void setup() {
  Serial1.begin(9600);
  Serial1.println("Basic Encoder Test:");

  //turn on the LED
  pinMode(5,OUTPUT);
  digitalWrite(5,HIGH);

  pinMode(30, INPUT);
  
}

long oldPosition  = -999;

void loop() {
  long newPosition = myEnc.read();
  if (newPosition != oldPosition) {
    oldPosition = newPosition;
    Serial1.println(newPosition);
  }
  if (digitalRead(30) == HIGH) {
    Serial1.println(F("BUTTON PRESSED"));
  }
}


				

There's not much to say here. The example code just reads the position of the encoder then prints it out via the serial port. All of the hard work is done in the library. It attaches to the interrupt pins so it doesn't need to constantly poll for pin state changes in loop().

My only change to the example was swapping the UART to Serial1 of course, and adding a statement to print out "BUTTON PRESSED" when pin 30 is high to indicate that the encoder button is currently pressed. It worked as intended, printing out the encoder position when it was turned, and "BUTTON PRESSED" when appropriate. With no button debouncing in this code the "BUTTON PRESSED" messages were a screen filling flood.

The two remaining outputs really require no software testing as they are just breakouts directly to the processor pins. J5 simply breaks out the connection to UART0 for the Nextion display and J11 breaks out I/O pin PB0 through a 330 Ohm resistor for the WS2812B LEDs.

Once all the tests were complete I was happy to see that the circuit worked as intended. Of course with the physical assembly, only about 75% of the work is done. I still need to write all the software which will make use of this hardware.

Software Development Test Rig

To facilitate software development without having the car present, I built this test rig.

Image of test right for software development with MS3-Pro connected to Stimulator, power switches, connection breakout terminals

On a piece of plywood I mounted my old school MegaSquirt Stimulator. The rather humorously named board simply provides simulated signals for the most important sensors (O2, TPS, CLT, IAT, RPM pulse) to allow running a MegaSquirt on the bench. An adapter harness was made to convert the DB37 to the two AmpSeal connectors on the MS3-Pro. Finally it was wired up to a terminal strip on the right side which allows connection of 12V and access to the CANbus. A DB9 breakout board connects to the MS3-Pro serial (RS232 level) interface, and there are two power switches: a master power and another one to simulate the key switch being set to "IGN". The space at the top of the rig is to allow the cluster to be secured.

With this rig I can develop the cluster firmware at my leisure on a desk, rather than in the shop on a bench or sitting in the car.

Back To Cosmo Page | Mail Me | Search | Buy Me A Coffee