Automotive A/C Compressor Protection Code

First post, and please go easy on me since I'm a hardware guy much more than a coder. I have a 1974 Challenger with a Hellcat crate engine and a Vintage Air A/C system. For a number of reasons there is no good way to get the ECM to natively control the A/C compressor on this system and it instead cycles entirely based off of the trinary switch. That works fine, but I have zero protection against excessive RPM or a WOT lockout like you'd have in a factory application and it'll even engage the compressor with the engine off if the A/C is turned on.

My plan is to have my Arduino Uno R3 installed such that it reads "LISTEN ONLY" the 500k CANBUS for the existing RPM and APS messages that I've already sniffed and decoded. It is also installed so that the ground output from the VA controller that would normally go to the A/C relay is wired to the Arduino to serve as an A/C Request input on pin 6.

Eventually, I want it to control the factory PWM fan, send a tach output signal, and maybe broadcast an ambient air temp message on the bus if I get really crazy. I've already built the Arduino in question with a Seeed V2.0 Can Shield, 12v to 5v DC-DC power supply, and octocouplers on the hardwired inputs to protect the device from the electrical system.

I'd like for it to allow A/C operation by grounding pin 7 (which goes to the relay) only when RPM is between 600 and 4500, TPS <80%, and it has been 4 seconds since the last actuation to have some hysteresis against a steady cruise or APS right around the lockout settings.

RPM is contained in message ID 0x7E8 in the A and B bits per standard OBD2 routines.

APS is a little weird; it is in message ID 0x22f but it is in the very first two bits of the message like:
07D595800843C. I have verified this during the CAN sniffing testing that I did and it does in fact mirror APS position on a scan tool. The APS OBD2 standard PID's are NOT being broadcast, and my Dakota Digital gauges use a CAN bus polling device so I don't want to mess with conflicting request messages bringing down the bus.

Could I get some input on my code and let me know if this looks like it'll work before I go waste time testing it? It compiled ok, but I'm not sure the CAN message coding is going to read the messages and convert into the actions correctly as it is written. I won't pretend that I wrote this all myself; I modified existing example sketch and internet searched code to get to where I am at right now. The CAN sniffing and decoding went a lot easier than I expected, but this is at the limit of my ability. Appreciate the help!

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

#define CAN_2515
// #define CAN_2518FD

// Set SPI CS Pin according to your hardware

#if defined(SEEED_WIO_TERMINAL) && defined(CAN_2518FD)
// For Wio Terminal w/ MCP2518FD RPi Hat:
// Channel 0 SPI_CS Pin: BCM 8
// Channel 1 SPI_CS Pin: BCM 7
// Interupt Pin: BCM25
const int SPI_CS_PIN = BCM8;
const int CAN_INT_PIN = BCM25;
#else

// For Arduino MCP2515 Hat:
// the cs pin of the version after v1.1 is default to D9
// v0.9b and v1.0 is default D10
const int SPI_CS_PIN = 9;
const int CAN_INT_PIN = 2;
#endif


#ifdef CAN_2518FD
#include "mcp2518fd_can.h"
mcp2518fd CAN(SPI_CS_PIN);  // Set CS pin
#endif

#ifdef CAN_2515
#include "mcp2515_can.h"
mcp2515_can CAN(SPI_CS_PIN);  // Set CS pin
#endif

const int OUTPUT_PIN = 7;  // Output pin to control
const unsigned long cycleInterval = 4000; // 4 seconds

unsigned long lastCycleTime = 0;

void setup() {
  pinMode(OUTPUT_PIN, OUTPUT);
  pinMode(6, INPUT);
  SERIAL_PORT_MONITOR.begin(115200);

  while (CAN_OK != CAN.begin(CAN_500KBPS)) {  // init can bus : baudrate = 500k
    SERIAL_PORT_MONITOR.println("CAN init fail, retry...");
    delay(100);
  }
  SERIAL_PORT_MONITOR.println("CAN init ok!");
  }

void loop() {
  unsigned long currentMillis = millis();

  if (currentMillis - lastCycleTime >= cycleInterval) {
    lastCycleTime = currentMillis;

    // Read CAN message
    unsigned char len = 0;
    unsigned char buf[8];

    if (digitalRead(6) == LOW) {
      if (CAN.checkReceive() == CAN_MSGAVAIL) {
        CAN.readMsgBuf(&len, buf);

        // Check if it's the Engine RPM message (0x7E8)
        if (buf[1] == 0x08 && buf[2] == 0x01 && buf[3] == 0x0C && buf[4] == 0x00 && buf[5] == 0x00 && buf[6] == 0x00 && buf[7] == 0x00) {
          // Extract RPM value
          unsigned int rpm = ((unsigned int)buf[0] * 256 + buf[1]) / 4;

          // Check conditions for Engine RPM and TPS
          if (rpm >= 600 && rpm <= 4500) {
            // Check for TPS message (0x22F)
            if (CAN.checkReceive() == CAN_MSGAVAIL) {
              CAN.readMsgBuf(&len, buf);
              if (buf[0] == 0x22 && buf[1] == 0xF0) {
                // Extract TPS value
                unsigned int tps = (buf[2] & 0x03) * 100 / 3; // Get the first two bits

                // Check TPS condition
                if (tps < 80) {
                  digitalWrite(OUTPUT_PIN, HIGH); // Set output pin high
                } else {
                  digitalWrite(OUTPUT_PIN, LOW); // Set output pin low
                }
              }
            }
          }
        }
      }
    }
  }
}

All of us are not "into" automotive stuff, so please don't throw around ABRs but spell it out. :slightly_smiling_face:

Sorry about that... Does this help?

A/C - Air Conditioning
APS - Accelerator Position Sensor
ECM - Engine Control Module
RPM - Revolutions per Minute
VA - Vintage Air ( the air conditioning system brand; not really important to this discussion - it just sends a ground signal when air conditioning is desired)
WOT - Wide Open Throttle

I'll read your full description again carefully when my circumstances for doing are improved.

WOT - surprised I didn't recognize at least that one, WOT WOT!

So I think you said until you get more ambitious, this is a listen only connection to the CAN bus.
You've said some aspect of CAN listening has been successfully done.

I would proceed by bravely testing. Just with the UNO (and the CAN shield) and my laptop, using the serial monitor and/or LEDs as proxies for the hardware to be controlled.

I've not looked at al into your code, but if it isn't already, make it very "chatty" so it spews a crap ton of info to the serial monitor so you can verify the flow of the program in its responses to the CAN message traffic.

I do not know if there are any whizzy CAN simulators that would allow this to be done from the comfort of your big chair in the lab, or in front of a roaring fire, but if there were, I would be going out to the garage with something I was very sure would not present any software problems.

Then... start hanging the real controlled stuff on your UNO.

I think the hardware issues will outweight the software, which from a glance seem fairly stratigh ahead.

HTH

a7

TNX for that, you only have to SIO (Spell It Out) the first time, I can retain for a few days. :grin:

One minute of googling from under the umbrella, gotta go back to play now but check out

which may save time in the long run.

a7

I remember why you have a four second thing in there, but I express concern ( again, at a glance) that the entirety of your loop body only runs once every cycleOnterval (4 seconds).

I do not know enough about CAN to say it may be better to listen with more attention, that is to say continuously, and let the 4 second thing be wrapped around only what needs to be so throttled (<- see what I did there?).

And, TBH, handle the hysteresis on some basis other than time.

a7

Are you using APS and TPS interchangeably in your description?

I plan on testing the code by having the Uno hooked up to the CAN bus and the input/output functions isolated via a breadboard so I could see what it was doing and reacting to safely before having it actually control the A/C relay. I can build test circuits pretty easily to verify the operation is as expected.

I'm open to other ideas other than the 4 second delay - ultimately what I'm trying to do is prevent the A/C relay from ratcheting on and off rapidly if I happen to be hovering around 80% throttle or 4500RPM. Maybe there is a way to code in a smoothing effect so it doesn't react until it drops 100RPM or 10% throttle after tripping over one of the lock outs? I dunno... I've done a lot of vehicle calibration both professionally and recreationally, so I know the parameters and ways I want things to work, but translating that into working code is another animal entirely!

It's definitely APS only - if I typed TPS once it was out of old habits. The electronic throttle body is going to behave independent of the accelerator pedal at times, so I only want to use Accelerator Pedal Position.

@dmod1974 OK, good plan.

If you get confident about the other part separately, that is you simulate what the UNO will say to the controlled parts, then you can hook the two parts together for more testing.

Classic hysteresis is very simple code-wise, viz: (pseudocode here)

  if some value is over the high threshold
     turn on the device

  if that value is below the low threshold
     turn off the device

Or vice versa. The above is plausible for air conditioning based on temperature.

Nothing more than two if statements. It's just how your household thermostat works, assuming you heat or cool your house.

a7

Your code looks dubious to me, but I'm no CAN bus expert.

It looks like you only check for TPS after you read RPM and determine that it is between 600 and 4500. Also, there's an assumption that the TPS message will immediately follow the RPM message which I suspect is not always true.

I'd have the code read the bus looking for either value and perform your logic checks whenever you get one of them.

OK, I've been busy since the initial build. I think I have everything built minus the Tach output coding. I'm going to do testing, but what are everyone's thoughts on the fan control logic?

Basically, if the Fan Override switch is LOW it needs to command full fan speed PWM no matter what. If that is off, fan operation is limited to <80KPH and >600 Engine RPM. It then depends on whether the air conditioning is on, and what the ECT (coolant temp) duty cycle requires. Whichever duty cycle is lower wins and determines the PWM DC sent to the fan. This fan requires a 100Hz PWM signal that has min speed at max DC and vice versa.

I'm not sure if my CAN filtering and writing updating of the pertinent const int PID's is correct either, but I think I'm going to have to test and figure that out on my own.

I don't have any delays anymore since the CAN traffic should set that I'd think, and none of these functions need to be processed more than 1-2x a second. The fan has a built in controller so it determines it's own response speed (several seconds to ramp up or down), and the A/C compressor doesn't need much more speed.

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

int ECT;
int RPM;
int VSS;
int AAT;
int TPS;

long unsigned int rxId = 0x7E8;  // ECM
long unsigned int rxId2 = 0x22F; // TPS
unsigned char len = 0;
unsigned char rxBuf[8];

const int AAT_Sensor = A3;
const int Fan_PWM = 3;
const int Tach_PWM = 5;
const int AC_REQ = 6;
const int AC_Relay = 7;
const int FAN_Override = 8;

const int ECT_Low_Speed = 97;
const int ECT_High_Speed = 103;
const int acOnMinPwmPct = 54; // If the A/C is on, this is the minimum PWM duty percentage

// Adjust these for PWM for fan start and fan full on
const int slowSpeedFanDuty = 221; // Duty Cycle for 87% (Off Fan Speed, 87% of 255)
const int fanOnFullDuty = 51; // Duty Cycle for 90% (Full Fan Speed, 20% of 255)

int acOnMinPWMDuty;

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

void setup() {
  pinMode(A3, INPUT);
  InitTimersSafe();
  SetPinFrequency(Fan_PWM, 100);

  Serial.begin(115200);

  if (CAN0.begin(MCP_STDEXT, CAN_500KBPS, MCP_16MHZ) == CAN_OK)
    Serial.println("MCP2515 Init Okay!!");
  else
    Serial.println("MCP2515 Init Failed!!");

  pinMode(2, INPUT); // Setting pin 2 for /INT input

  // Set mask and filter for message ID 0x22F
  CAN0.init_Mask(0, 0, 0x1FFFFFFF); // Mask 0: Accept any extended ID
  CAN0.init_Filt(0, 0, 0x22F00000); // Filter 0: Match message ID 0x22F

  // Set mask and filter for message ID 0x7E8
  CAN0.init_Mask(1, 0, 0x1FFFFFFF); // Mask 1: Accept any extended ID
  CAN0.init_Filt(1, 0, 0x7E800000); // Filter 1: Match message ID 0x7E8

  Serial.println("MCP2515 Library Mask & Filter Example...");
  CAN0.setMode(MCP_LISTENONLY);
}

void loop() {
  if (!digitalRead(2)) // If pin 2 is low, read receive buffer
  {
    CAN0.readMsgBuf(&rxId, &len, rxBuf); // Read data: len = data length, buf = data byte(s)
    Serial.print("ID: ");
    Serial.print(rxId, HEX);
    Serial.print(" Data: ");
    for (int i = 0; i < len; i++) // Print each byte of the data
    {
      if (rxBuf[i] < 0x10) // If data byte is less than 0x10, add a leading zero
      {
        Serial.print("0");
      }
      Serial.print(rxBuf[i], HEX);
      Serial.print(" ");
    }

    Serial.println();

    if (rxId == 0x7E8) {
      if (rxBuf[1] == 0x05) {
        // Extract ECT
        ECT = rxBuf[3] - 40; // Update the global ECT variable
        Serial.print("Engine Coolant Temperature: ");
        Serial.print(ECT);
        Serial.println(" degrees Celsius");
      }
    }

    if (rxId == 0x7E8) {
      if (rxBuf[1] == 0x0C) {
        // Extract RPM
        RPM = ((rxBuf[3] * 256) + rxBuf[4]) / 4; // Update the global RPM variable
        Serial.print("Engine RPM: ");
        Serial.print(RPM);
        Serial.println(" RPM");
      }
    }

    if (rxId == 0x7E8) {
      if (rxBuf[1] == 0x0D) {
        // Extract VSS
        VSS = rxBuf[3]; // Update the global VSS variable
        Serial.print("VSS: ");
        Serial.print(VSS);
        Serial.println(" VSS");
      }
    }

    if (rxId2 == 0x22F) {
      TPS = rxBuf[0] * (100.0 / 255.0); // Update the global TPS variable
      Serial.print("TPS: ");
      Serial.print(TPS);
      Serial.println(" TPS");
    }
  }
  
  // A/C Compressor Control Logic
  if (digitalRead(AC_REQ) == LOW && RPM > 600 && RPM < 4500 && TPS < 204) {
    digitalWrite(AC_Relay, HIGH); // NEED 4 SECOND DELAY???
  }

  // Hardwired Fan control logic
  if (digitalRead(FAN_Override) == LOW) {
    pwmWrite(Fan_PWM, fanOnFullDuty);
  }

  if (digitalRead(FAN_Override) == HIGH && digitalRead(AC_REQ) == LOW && RPM > 600 && VSS < 80) { // A/C is on
    acOnMinPWMDuty = (255 * acOnMinPwmPct) / 100; // If the A/C is on, this is the minimum actual PWM duty
  } else {
    acOnMinPWMDuty = 220;
  }

  int duty = map(ECT, ECT_Low_Speed, ECT_High_Speed, slowSpeedFanDuty, fanOnFullDuty); // calc PWM Duty

  if (digitalRead(FAN_Override) == HIGH && RPM > 600 && VSS < 80 && duty > acOnMinPWMDuty) { // assign minimum duty if A/C is on.
    duty = acOnMinPWMDuty;
  }

  int constrainedDuty = constrain(duty, 0, fanOnFullDuty); // PWM duty is never allowed outside of min or max duties

  pwmWrite(Fan_PWM, 255 - constrainedDuty); // send PWM to output pin, invert for 12V transistor

  Serial.print("constrainedDuty = ");
  Serial.println(constrainedDuty);
}

here for updates and progress, I too am fiddling around with automotive canbus..... Chevy side of the world though :slight_smile:
clint

I Am surprised that it is CAN, I would expect it to be a CCD or similar bus. I was at the conference in 1983 where CAN was officially introduced, 9 years after your model year. If my memory is correct Chrysler was the first to use multiplexing system in a production vehicle starting about 1986. Is it possible a lot has been redone on the electronics of your car?

This is from wikipedia: Development of the CAN bus started in 1983 at Robert Bosch GmbH.[1] The protocol was officially released in 1986 at the Society of Automotive Engineers (SAE) conference in Detroit, Michigan. The first CAN controller chips were introduced by Intel in 1987, and shortly thereafter by Philips.[1] Released in 1991, the Mercedes-Benz W140 was the first production vehicle to feature a CAN-based multiplex wiring system.[2][3]

There is a good app note AN2689 by ST on automotive electronics. reading it will help you a lot.

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