[Driving] Freelander EV With Gen1 Leaf electrics and Original EVBMW VCU

Tell us about the project you do with the open inverter
Alibro
Posts: 949
Joined: Sun Feb 23, 2020 9:24 am
Location: Northern Ireland
Has thanked: 346 times
Been thanked: 208 times
Contact:

Re: [Driving] Freelander EV With Gen1 Leaf electrics and EVBMW VCU

Post by Alibro »

Used the car again for the sort of jobs I built it for.
Yesterday I drove to the airport to pick up my Mrs and daughter, and today I borrowed an old horse trailer and used it to bring home two large round bails of hay.
IMG_20240619_170524897_HDR.jpg
IMG_20240619_170507642_HDR.jpg
The trailer is stupidly heavy and the wifes Ford Kuga really struggles to tow it as it's clutch is on it's last legs and the engine hasn't much torque.
Normally in the Freelander I don't use first gear as it isn't necessary but when towing this trailer I use first to get moving and cross roads quickly. Pulling through slow traffic was a breeze however I don't like pulling up long fast hills with the constant high current load. Maybe I'm being silly but I was glad to get it back to it's owner.
Alibro
Posts: 949
Joined: Sun Feb 23, 2020 9:24 am
Location: Northern Ireland
Has thanked: 346 times
Been thanked: 208 times
Contact:

Re: [Driving] Freelander EV With Gen1 Leaf electrics and EVBMW VCU

Post by Alibro »

New - Old Vid
I recorded this a few months ago but only got around to editing and releasing it now.

Alibro
Posts: 949
Joined: Sun Feb 23, 2020 9:24 am
Location: Northern Ireland
Has thanked: 346 times
Been thanked: 208 times
Contact:

Re: [Driving] Freelander EV With Gen1 Leaf electrics and EVBMW VCU

Post by Alibro »

Last night I towed brother in law's Golf to the mechanic with it.
Funny thing was his Golf clutch pedal was stuck to the floor just like mine. His didn't go as well like that tho. 🤣

Who said EV's can't tow! 😋

The best bit about towing the Golf was the route to the mechanic has a fairly long and very steep hill which I was concerned about. I just dropped from 2nd (which is good for up to 45mph) to 1st and the car sailed up the hill like it wasn't there doing around 20mph.
Alibro
Posts: 949
Joined: Sun Feb 23, 2020 9:24 am
Location: Northern Ireland
Has thanked: 346 times
Been thanked: 208 times
Contact:

Re: [Driving] Freelander EV With Gen1 Leaf electrics and EVBMW VCU

Post by Alibro »

So this week I decided it was time to secure my power steering pump properly instead of just jamming it in with foam packing.

I never got a photo of it installed like this but this is the foam I was using. It worked surprisingly well for a couple thousand miles. 😋
IMG_20241104_111016628.jpg
So I started by cutting a hydraulic pump shaped hole in a bit of 1.5mm steel then welded a lip onto it.
IMG_20241102_122512155.jpg
IMG_20241102_122517857.jpg
Before grinding my Fugly welds.
IMG_20241102_131012022.jpg
Then I welded on some angle supports.
IMG_20241102_145451436.jpg
Before final grind and paint
IMG_20241102_145440348.jpg
IMG_20241102_145445665.jpg
IMG_20241102_163156789.jpg
I then drilled and tapped holes to secure the pump and welded it into position. It is welded to my battery bracket, not to the car so no issue with DVLA
IMG_20241103_115007389.jpg
Nice and solid now.
IMG_20241103_122918067.jpg
I'm really pleased with how it turned out but I currently don't have any rubber mounts holding it. I just used M8 bolts and washers so I find it much louder then it was before. Hopefully with rubber bushings it will be a bit quieter but I need to figure out a way to make them as they didn't come with the pump.
Alibro
Posts: 949
Joined: Sun Feb 23, 2020 9:24 am
Location: Northern Ireland
Has thanked: 346 times
Been thanked: 208 times
Contact:

Re: [Driving] Freelander EV With Gen1 Leaf electrics and EVBMW VCU

Post by Alibro »

I happened across this video I took back in May and had a second look. Turns out my car is putting out a little more power than I expected when I started this build.
I'm using the inverter and motor from a 2011 Gen1 Nissan Leaf which had a claimed power of 80kW. In this video I'm using a high quality shunt made by Isabellenhütte so I think the numbers are accurate.
I watched carefully and saw a peak power of 89kW but a constant power for several seconds of 85/86kW.
TBH this is pretty much how it feels but with the instant torque of electric. It's not a power monster but I'm very happy with how it goes.

Alibro
Posts: 949
Joined: Sun Feb 23, 2020 9:24 am
Location: Northern Ireland
Has thanked: 346 times
Been thanked: 208 times
Contact:

Re: [Driving] Freelander EV With Gen1 Leaf electrics and EVBMW VCU

Post by Alibro »

The PAS pump is much quieter now with these, I think they're called bobbins but they make a big difference.

https://ebay.us/IgouJS
IMG_20241109_112235374_HDR.jpg
Not quite as quiet as the foam but not as bad as bolting in directly.
Alibro
Posts: 949
Joined: Sun Feb 23, 2020 9:24 am
Location: Northern Ireland
Has thanked: 346 times
Been thanked: 208 times
Contact:

Re: [Driving] Freelander EV With Gen1 Leaf electrics and EVBMW VCU

Post by Alibro »

Last week I used my Freelander EV to tow a mates motorbike (on a trailer) to a repair shop. Only problem was the shop is 30 miles away and my car has a range of up to 100 miles on a good day. In the winter at around 3 Deg C with a large trailer and a motorbike it could not be called "a good day". I wasn't sure what the range would be but decided 60 miles should be OK if I took it easy. If I had figured out how to implement fast charging I would have been able to stop and recharge enroute but I haven't, not yet anyway. In fact my ac charging is pretty slow too at 3kW so I can only add around 9 miles per hour to the range, probably less while towing but at least it was an option.

Anyway we set off and apart from one of his securing straps breaking resulting in the bike almost falling off the trailer we made it there and I made a quick calculation that we should be able to make it home too.
The only issue we had on the whole trip was the cold. The heater uses 2kW which is around 5% of the battery capacity per hour. On a journey that lasted 1.5 hours that would be approx 7.5% reduction in the range or 7.5 miles so went without.
In the end we probably could have had the heater on at a low temp and still made it back but I didn't feel confident to do that, however I was delighted that my dodgy home built Frankenstein EV conversion was able to do the job at all.
Sadly I forgot to take photo's but it was a fun trip and confirmed the car is still a practical workhorse.
User avatar
marcexec
Posts: 157
Joined: Tue May 14, 2019 12:52 pm
Location: Dublin, Ireland
Has thanked: 706 times
Been thanked: 60 times

Re: [Driving] Freelander EV With Gen1 Leaf electrics and EVBMW VCU

Post by marcexec »

Probably not much point, but are there any aero mods you could do on the underbody to increase range? E.g. cover the sections that are now open and thus messy where the exhaust went?
A motorcyclist is never late, Frodo Baggins. Nor is he early. He arrives precisely when he means to.
Getting started guide for Celeron55's iPDM56
My Suzuki RF400 build @ES
Honda IMA & Lebowski howto
Image
arber333
Posts: 3507
Joined: Mon Dec 24, 2018 1:37 pm
Location: Slovenia
Has thanked: 123 times
Been thanked: 319 times
Contact:

Re: [Driving] Freelander EV With Gen1 Leaf electrics and EVBMW VCU

Post by arber333 »

marcexec wrote: Tue Dec 17, 2024 10:59 am Probably not much point, but are there any aero mods you could do on the underbody to increase range? E.g. cover the sections that are now open and thus messy where the exhaust went?
I think one inconsiderate press of the throttle would annul everything you save by painstaking aeromodding :). Better use low rolling resistance tires and drive slower...
Alibro
Posts: 949
Joined: Sun Feb 23, 2020 9:24 am
Location: Northern Ireland
Has thanked: 346 times
Been thanked: 208 times
Contact:

Re: [Driving] Freelander EV With Gen1 Leaf electrics and EVBMW VCU

Post by Alibro »

marcexec wrote: Tue Dec 17, 2024 10:59 am Probably not much point, but are there any aero mods you could do on the underbody to increase range? E.g. cover the sections that are now open and thus messy where the exhaust went?
I had considered this but the Freelander is not too cluttered underneath and on the day I suspect the slab sides of the trailer were the major problem.
Someday I'll upgrade the old Leaf Gen1 VCU to Zombieverter and use the regen to claw back a few miles, I know it won't make a huge difference but every little helps.
arber333 wrote: Tue Dec 17, 2024 12:37 pm

I think one inconsiderate press of the throttle would annul everything you save by painstaking aeromodding :). Better use low rolling resistance tires and drive slower...
I drove the whole time treating the throttle like it had eggs under the pedal. :lol:
The tyres are new but finding EV tyres for a Freelander is not easy, 215-65-R16. :shock:
Alibro
Posts: 949
Joined: Sun Feb 23, 2020 9:24 am
Location: Northern Ireland
Has thanked: 346 times
Been thanked: 208 times
Contact:

Re: [Driving] Freelander EV With Gen1 Leaf electrics and EVBMW VCU

Post by Alibro »

Last few days I've been playing with the VCU firmware trying to figure out if it was possible to have regen.
It should hopefully help improve the range a bit but even more important it should take some pressure off the brakes.
At the moment the brakes are great but probably over worked without any engine braking so a little regen should help.
The reason I done this before is when it comes to Arduino coding I'm thick! My Mrs would say I'm thick in many ways but that's another story.
Anyway I recently watched a video where a Youtuber was using AI to generate Arduino code so figured it was worth a try.



So finally after several days playing with it I've finally got regen working.
There are two ways regen works in EV's, first when you lift off the throttle and second when you press the brake pedal. In my case I can't easily have lift off regen because I need to lift off to change gear. In theory I could rig up a switch to stop the regen as I change gear but that's a lot of complexity. Maybe some day.
In the meantime I use the brake light switch to trigger the regen by just touching the brake pedal, it stars low and ramps up over a set time which is currently 850 milliseconds, provides fairly strong retardation and I've seen up to 100A going back into the batteries.
It probably needs a bit more tweaking but for the moment I'm happy it's working so I'll get back to setting up a timer to start charging at 2am for the cheaper leccy.
User avatar
tom91
Posts: 2169
Joined: Fri Mar 01, 2019 9:15 pm
Location: Bristol
Has thanked: 190 times
Been thanked: 491 times

Re: [Driving] Freelander EV With Gen1 Leaf electrics and EVBMW VCU

Post by tom91 »

Alibro wrote: Tue Jan 21, 2025 10:52 pm when it comes to Arduino coding I'm thick!
Alibro wrote: Tue Jan 21, 2025 10:52 pm AI to generate Arduino code so figured it was worth a try.
These two lines clearly should never be said together along with
Alibro wrote: Tue Jan 21, 2025 10:52 pm So finally after several days playing with it I've finally got regen working.
The level of checks of parameters required when implementing regen are not minimal. The idea that AI will code for you and you do not have the knowledge to even verify what it does. This just sounds like a recipe for distaster. There are just way too many failure options here and risks associated with it.
Founder Volt Influx https://www.voltinflux.com/
Webstore: https://citini.com/
Alibro
Posts: 949
Joined: Sun Feb 23, 2020 9:24 am
Location: Northern Ireland
Has thanked: 346 times
Been thanked: 208 times
Contact:

Re: [Driving] Freelander EV With Gen1 Leaf electrics and EVBMW VCU

Post by Alibro »

tom91 wrote: Tue Jan 21, 2025 11:34 pm These two lines clearly should never be said together along with



The level of checks of parameters required when implementing regen are not minimal. The idea that AI will code for you and you do not have the knowledge to even verify what it does. This just sounds like a recipe for distaster. There are just way too many failure options here and risks associated with it.
Thanks Tom I was hoping you would see this and appreciate your experience and advice.
I live at the end of a 300m lane so all initial testing was done on private ground and I only took it on the road when it worked without any issues.
I have only done very brief testing and wanted to post here for someone like yourself to have a glance at it before deciding it is OK to keep it.

This is the original section that controls throttle from Damiens code.

Code: Select all

void readPedals()
{
ThrotVal = analogRead(Throttle1); //read throttle channel 1 directly
ThrotVal = constrain(ThrotVal, 145, 620);
ThrotVal = map(ThrotVal, 145, 620, 0, MaxTrq); //will need to work here for cal.
if(ThrotVal<0) ThrotVal=0;  //no negative numbers for now.
if(digitalRead(Brake)) ThrotVal=0;  //if brake is pressed we zero the throttle value.
//Serial.println(ThrotVal);   //print for calibration. 
}



void SendTorqueRequest()
{

final_torque_request = ThrotVal;  //send mapped throttle signal to inverter.  
  
}
And this is what AI and I have come up with

Code: Select all

// Define constants
const int THROTTLE_MIN = 145;
const int THROTTLE_MAX = 620;
const int REGEN_TORQUE_MIN = -30;
const int REGEN_TORQUE_MAX = -75;
const int REGEN_RAMP_TIME = 850;
const int BRAKE_PEDAL_THRESHOLD = 400;
const int PACK_VOLTAGE_THRESHOLD = 396;

// Define variables
unsigned long regenStartTime = 0;
bool regenActive = false;
int regenTorque = 0;

void readPedals()
{
    // Read throttle value and constrain it to valid range
    ThrotVal = analogRead(Throttle1);
    ThrotVal = constrain(ThrotVal, THROTTLE_MIN, THROTTLE_MAX);
    ThrotVal = map(ThrotVal, THROTTLE_MIN, THROTTLE_MAX, 0, MaxTrq);

    // Apply regen braking if conditions are met
    if (digitalRead(Brake) && inverter_status.speed > BRAKE_PEDAL_THRESHOLD && inv_volts_local < PACK_VOLTAGE_THRESHOLD)
    {
        applyRegenBraking();
    }
    else
    {
        regenActive = false;
    }
}

void applyRegenBraking()
{
    // Start regen braking if not already active
    if (!regenActive)
    {
        regenStartTime = millis();
        regenActive = true;
    }

    // Calculate elapsed time since regen started
    unsigned long elapsedTime = millis() - regenStartTime;

    // Ramp regen torque from REGEN_TORQUE_MIN to REGEN_TORQUE_MAX over REGEN_RAMP_TIME
    regenTorque = map(elapsedTime, 0, REGEN_RAMP_TIME, REGEN_TORQUE_MIN, REGEN_TORQUE_MAX);

    // Reduce regen torque based on speed
    float speedRatio = inverter_status.speed / 1500.0;
    regenTorque = int(regenTorque * speedRatio);
}

void SendTorqueRequest()
{
    if (regenActive)
    {
        final_torque_request = regenTorque;
    }
    else
    {
        final_torque_request = ThrotVal;
    }
}
Note, I'm using Damiens Gen1 Leaf VCU here

What I'm trying to implement with this code is
1. Use the brake light switch connection to the VCU to confirm is the brake pedal being pressed.
2, Confirm is the car stationary by checking "inverter_status.speed" (rpm of motor) is less than 400
It could be coasting in neutral at this point but if that were the case regen wouldn't work anyway.
3. Check is the pack voltage below a certain level to avoid over charging. I live on flat ground and have to go up hill before any significant regen would kick in so I set it at 396V but will adjust down if necessary. Any thoughts on this appreciated.
So if all above are yes then it should apply regen at a low level, initially Throtval = -30 but ramps up to -75 over time which I set to 850ms.

Like I said I've only very briefly tested it on a quiet country road and it seems to work with me seeing up to 95A regen current.
It also seems to bring the car to a complete stop which is handy.
I don't think it is reducing regen as I slow down but that may just need the float speed ratio tweaked.
Initially the regen was a bit severe with max regen set to Throtval = -150
I reduced the max regen from -150 to -75 and although it still doesn't reduce as I slow down it felt a lot nicer.
User avatar
tom91
Posts: 2169
Joined: Fri Mar 01, 2019 9:15 pm
Location: Bristol
Has thanked: 190 times
Been thanked: 491 times

Re: [Driving] Freelander EV With Gen1 Leaf electrics and EVBMW VCU

Post by tom91 »

Alibro wrote: Wed Jan 22, 2025 10:30 am glance at it before deciding it is OK to keep it.
Sorry, no I will not review code snippets, you need to understand the full VCU code to work out how the data interacts and data quality is checked.
Founder Volt Influx https://www.voltinflux.com/
Webstore: https://citini.com/
Alibro
Posts: 949
Joined: Sun Feb 23, 2020 9:24 am
Location: Northern Ireland
Has thanked: 346 times
Been thanked: 208 times
Contact:

Re: [Driving] Freelander EV With Gen1 Leaf electrics and EVBMW VCU

Post by Alibro »

This is the full code.
I didn't want to presume you would check it all.

Code: Select all

/*
Leaf Gen1 Inverter driver. Alpha software for testing.
Runs on the Arduino Due SAM3X8E MCU. V1 Leaf open source vcu.
Enter torque request on serial window.
As of now only responds to negative torque requests. e.g. -10
Positive torque requests trigger the inverter pwm but do not rotate the motor.

V5 incorporates ISA can shunt on CAN0. Let's hope the leaf inverter doesnt mind the isa messages and vice versa:)
WiFi on Serial2.
Precharge control : out1 = precharge , out2= main contactor


Copyright 2019 
Perttu Ahola (all the hard work!)
http://productions.8dromeda.net/c55-leaf-inverter-protocol.html

Damien Maguire (copy and paste).
OpenSource VCU hardware design available on Github :
https://github.com/damienmaguire/Nissan-Leaf-Inverter-Controller

2011 Nisan Leaf Gen 1 EV CAN logs on Github:
https://github.com/damienmaguire/LeafLogs


    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.



*/
#include <Metro.h>
#include <due_can.h>  
#include <due_wire.h> 
#include <DueTimer.h>  
#include <Wire_EEPROM.h> 
#include <ISA.h>  //isa can shunt library



#define Serial SerialUSB
template<class T> inline Print &operator <<(Print &obj, T arg) { obj.print(arg); return obj; }


CAN_FRAME outFrame;  //A structured variable according to due_can library for transmitting CAN data.
CAN_FRAME inFrame;    //structure to keep inbound inFrames

//////timers//////////////////////////////
Metro timer_Frames10 = Metro(10);
Metro timer_Frames100 = Metro(100);
Metro timer_wifi = Metro(1100);
Metro timer_hv = Metro(1000);


int inv_volts_local;
int16_t final_torque_request = 0;
#define INVERTER_BITS_PER_VOLT 2
#define INVERTER_BITS_PER_RPM 2


////////////////////Pin Map////////////////////////////////////////////////////
int led = 13;         //onboard led for diagnosis
#define Throttle1 A0  //Analog throttle channel 1
#define Throttle2 A1  //Analog throttle channel 2
#define Brake 61      //Brake pedal switch. High = brake on
#define IN1 6         //General purpose digital input 1. High =12v
#define IN2 7         //General purpose digital input 2. High =12v
#define OUT1  48      //Low side switched general purpose digital output 1. high = on.
#define OUT2  49      //Low side switched general purpose digital output 2. high = on.
#define OUT3  50      //Low side switched general purpose digital output 3. high = on.
/////////////////////////////////////////////////////////////////////////////////

#define HVPreset 320 //voltage at which to enable main contactor // Alibro changed from 340

uint16_t ThrotVal =0; //analog value of throttle position.
uint16_t outRPM;      //calculated value of rpm for e46 ike
static int16_t MaxTrq=2000;  //max torque request
struct InverterStatus {
  uint16_t voltage = 0;
  int16_t speed = 0;
  int8_t inverter_temperature = 0;
  int8_t motor_temperature = 0;
  bool error_state = false;
} inverter_status;

String readString;
byte ABSMsg=0x11;
uint8_t tempValue; //value to send to e46 temp gauge.

bool T15Status; //flag to keep status of Terminal 15 from In1
bool can_status;  //flag for turning off and on can sending.
bool Pch_Flag;    //precharge status flag
bool HV_Flag;     //hv on flag

ISA Sensor;  //Instantiate ISA Module Sensor object to measure current and voltage 

void setup() 
  {
  Can0.begin(CAN_BPS_500K);   // Inverter CAN
  Can1.begin(CAN_BPS_500K);   // Vehicle CAN
  //Can0.watchFor(0x1da); //set message filter for inverter can. Note sure if I can use two seperate values here. it might just pick 1!
  Can0.watchFor();
  Can1.watchFor(0x1ff); //just a blank message to block receive from e46 messages.
    
    Serial.begin(115200);  //Initialize our USB port which will always be redefined as SerialUSB to use the Native USB port tied directly to the SAM3X processor.
    Serial2.begin(19200);  //Serial comms with ESP32 WiFi module on serial 2
   // Timer3.attachInterrupt(Msgs10ms).start(10000); // 10ms CAN Message Timer
   // Timer4.attachInterrupt(Msgs100ms).start(100000); //100ms CAN Message Timer
    
  pinMode(led, OUTPUT);
  pinMode(Brake, INPUT);
  pinMode(IN1, INPUT);  //T15 input from ign on switch
  pinMode(IN2, INPUT);
  pinMode(OUT1, OUTPUT);
  pinMode(OUT2, OUTPUT);
  pinMode(OUT3, OUTPUT);
  
  //digitalWrite(led, HIGH);
  digitalWrite(OUT1, LOW);  //precharge
  digitalWrite(OUT2, LOW);  //main contactor
  digitalWrite(OUT3, LOW);  //inverter power
  }
  
void loop()
{ 
Check_T15();  //is the ignition on?  
if (timer_hv.check()) HV_Con(); //control hv system
Msgs100ms();  //fire the 100ms can messages
Msgs10ms();   //fire the 10ms can messages
readPedals(); //read throttle and brake pedal status.
SendTorqueRequest();  //send torque request to inverter.
ProcessRPM(); //send rpm and temp to e46 instrument cluster
CheckCAN(); //check for incoming can
handle_wifi();  //send wifi data

}

void Check_T15()
{
if (digitalRead(IN1))
{
T15Status=true;
can_status=true;
}
else
{
T15Status=false;
can_status=false;
Pch_Flag=false;
HV_Flag=false;
inv_volts_local==0;
}

}

void HV_Con()
{
  inv_volts_local=(inverter_status.voltage / INVERTER_BITS_PER_VOLT);

if (T15Status && !Pch_Flag)  //if terminal 15 is on and precharge not enabled
{
  digitalWrite(OUT3, HIGH);  //inverter power on
  if(inv_volts_local<330)    //Alibro changed from 200 to speed up restart
  {
  digitalWrite(OUT1, HIGH);  //precharge on
  Pch_Flag=true;
  }
}
if (T15Status && !HV_Flag && Pch_Flag)  //using inverter measured hv for initial tests. Will use ISA derived voltage in final version.
{
  if (inv_volts_local>300)     // Alibro changed to 300 for low battery condition
  {
  digitalWrite(OUT2, HIGH);  //main contactor on
  HV_Flag=true;  //hv on flag
  delay(300);                 //Alibro added 19/11/24 delay 300ms before turning off precharge, to be sure to be sure!
  digitalWrite(OUT1, LOW);    //Alibro added 19/11/24 Precharge off after main contactor turned on
  Pch_Flag=false;             //Alibro added flag precharge as off
  }
}

if (!T15Status)
{
  digitalWrite(OUT1, LOW);  //precharge off
  digitalWrite(OUT2, LOW);  //main contactor off
  digitalWrite(OUT3, LOW);  //inverter power off
  
}

}

void handle_wifi(){
  if (timer_wifi.check())
  {
/*
 * 
 * Routine to send data to wifi on serial 2
The information will be provided over serial to the esp8266 at 19200 baud 8n1 in the form :
vxxx,ixxx,pxxx,mxxxx,oxxx,rxxx* where :

v=pack voltage (0-700Volts)
i=current (0-1000Amps)
p=power (0-300kw)
m=motor rpm (0-10000rpm)
o=motor temp (-20 to 120C)
r=inverter temp (-20 to 120C)
*=end of string
xxx=three digit integer for each parameter eg p100 = 100kw.
updates will be every 1100ms approx.

v100,i200,p35,m3000,o20,r100*
*/
  
//Serial2.print("v100,i200,p35,m3000,o20,r100*"); //test string

digitalWrite(13,!digitalRead(13));//blink led every time we fire this interrrupt.
      Serial.print(inv_volts_local);
      Serial.print(F(" Volts"));
      Serial.println();
      Serial.println(HV_Flag);

Serial2.print("v");//dc bus voltage
Serial2.print(inv_volts_local);//voltage derived from Inverter  //Alibro changed from Sensor.Voltage
Serial2.print(",i");//dc current
Serial2.print(Sensor.Amperes);//current derived from ISA shunt
Serial2.print(",p");//total motor power
Serial2.print(Sensor.KW);//Power value derived from ISA Shunt
Serial2.print(",m");//motor rpm
Serial2.print(inverter_status.speed);
Serial2.print(",o");//motor temp
Serial2.print(inverter_status.motor_temperature);
Serial2.print(",r");//inverter temp
Serial2.print(inverter_status.inverter_temperature);
Serial2.print("*");// end of data indicator
  }
}

void Msgs10ms()                       //10ms messages here
{
if(timer_Frames10.check())
{
  if(can_status)
  {
    
  
  static uint8_t counter_11a_d6 = 0;
  static uint8_t counter_1d4 = 0;
  static uint8_t counter_1db = 0;
   static uint8_t counter_329 = 0;
    static uint8_t counter_100ms = 0;

  // Send VCM gear selection signal (gets rid of P3197)
  

        outFrame.id = 0x11a;            // Set our transmission address ID
        outFrame.length = 8;            // Data payload 3 bytes
        outFrame.extended = 0;          // Extended addresses - 0=11-bit 1=29bit
        outFrame.rtr=1;                 //No request
    
 //   CAN_FRAME inFrame;
  //  inFrame.id = 0x11a;
  //  inFrame.length = 8;

    // Data taken from a gen1 inFrame where the car is starting to
    // move at about 10% throttle: 4E400055 0000017D

    // All possible gen1 values: 00 01 0D 11 1D 2D 2E 3D 3E 4D 4E
    // MSB nibble: Selected gear (gen1/LeafLogs)
    //   0: some kind of non-gear before driving
    //   1: some kind of non-gear after driving
    //   2: R
    //   3: N
    //   4: D
    // LSB nibble: ? (LeafLogs)
    //   0: sometimes at startup, not always; never when the
    //      inverted is powered on (0.06%)
    //   1: this is the usual value (55% of the time in LeafLogs)
    //   D: seems to occur for ~90ms when changing gears (0.2%)
    //   E: this also is a usual value, but never occurs with the
    //      non-gears 0 and 1 (44% of the time in LeafLogs)


    
    outFrame.data.bytes[0] = 0x4E;
    //outFrame.data.bytes[0] = 0x01;

    // 0x40 when car is ON, 0x80 when OFF, 0x50 when ECO
    outFrame.data.bytes[1] = 0x40;

    // Usually 0x00, sometimes 0x80 (LeafLogs), 0x04 seen by canmsgs
    outFrame.data.bytes[2] = 0x00;

    // Weird value at D3:4 that goes along with the counter
    // NOTE: Not actually needed, you can just send constant AA C0
    const static uint8_t weird_d34_values[4][2] = {
      {0xaa, 0xc0},
      {0x55, 0x00},
      {0x55, 0x40},
      {0xaa, 0x80},
    };
    outFrame.data.bytes[3] = weird_d34_values[counter_11a_d6][0];
    outFrame.data.bytes[4] = weird_d34_values[counter_11a_d6][1];

    // Always 0x00 (LeafLogs, canmsgs)
    outFrame.data.bytes[5] = 0x00;

    // A 2-bit counter
    outFrame.data.bytes[6] = counter_11a_d6;

    counter_11a_d6++;
    if(counter_11a_d6 >= 4)
      counter_11a_d6 = 0;

    // Extra CRC
    nissan_crc(outFrame.data.bytes, 0x85);

    /*Serial.print(F("Sending "));
    print_fancy_inFrame(inFrame);
    Serial.println();*/

 Can0.sendFrame(outFrame);
 
    
  
  
  // Send target motor torque signal
  

        outFrame.id = 0x1d4;            // Set our transmission address ID
        outFrame.length = 8;            // Data payload 3 bytes
        outFrame.extended = 0;          // Extended addresses - 0=11-bit 1=29bit
        outFrame.rtr=1;                 //No request

    // Data taken from a gen1 inFrame where the car is starting to
    // move at about 10% throttle: F70700E0C74430D4

    // Usually F7, but can have values between 9A...F7 (gen1)
    outFrame.data.bytes[0] = 0xF7;
    // 2016: 6E
   // outFrame.data.bytes[0] = 0x6E;

    // Usually 07, but can have values between 07...70 (gen1)
    outFrame.data.bytes[1] = 0x07;
    // 2016: 6E
    //outFrame.data.bytes[1] = 0x6E;

    // Requested torque (signed 12-bit value + always 0x0 in low nibble)
    static int16_t last_logged_final_torque_request = 0;
    if(final_torque_request != last_logged_final_torque_request){
      last_logged_final_torque_request = final_torque_request;
      //log_print_timestamp();
      //Serial.print(F("Sending torque request "));
      //Serial.print(final_torque_request);
      //Serial.print(F(" (speed: "));
      //Serial.print(inverter_status.speed / INVERTER_BITS_PER_RPM);
      //Serial.print(F(" rpm)"));
      //Serial.print(inverter_status.voltage / INVERTER_BITS_PER_VOLT);
      //Serial.print(F(" Volts)"));
      //Serial.println();
    }
    if(final_torque_request >= -2048 && final_torque_request <= 2047){
      outFrame.data.bytes[2] = ((final_torque_request < 0) ? 0x80 : 0) |
          ((final_torque_request >> 4) & 0x7f);
      outFrame.data.bytes[3] = (final_torque_request << 4) & 0xf0;
    } else {
      outFrame.data.bytes[2] = 0x00;
      outFrame.data.bytes[3] = 0x00;
    }

    // MSB nibble: Runs through the sequence 0, 4, 8, C
    // LSB nibble: Precharge report (precedes actual precharge
    //             control)
    //   0: Discharging (5%)
    //   2: Precharge not started (1.4%)
    //   3: Precharging (0.4%)
    //   5: Starting discharge (3x10ms) (2.0%)
    //   7: Precharged (93%)
    outFrame.data.bytes[4] = 0x07 | (counter_1d4 << 6);
    //outFrame.data.bytes[4] = 0x02 | (counter_1d4 << 6);

    counter_1d4++;
    if(counter_1d4 >= 4)
      counter_1d4 = 0;

    // MSB nibble:
    //   0: 35-40ms at startup when gear is 0, then at shutdown 40ms
    //      after the car has been shut off (6% total)
    //   4: Otherwise (94%)
    // LSB nibble:
    //   0: ~100ms when changing gear, along with 11A D0 b3:0 value
    //      D (0.3%)
    //   2: Reverse gear related (13%)
    //   4: Forward gear related (21%)
    //   6: Occurs always when gear 11A D0 is 01 or 11 (66%)
    //outFrame.data.bytes[5] = 0x44;
    //outFrame.data.bytes[5] = 0x46;

    // 2016 drive cycle: 06, 46, precharge, 44, drive, 46, discharge, 06
    // 0x46 requires ~25 torque to start
    //outFrame.data.bytes[5] = 0x46;
    // 0x44 requires ~8 torque to start
    outFrame.data.bytes[5] = 0x44;

    // MSB nibble:
    //   In a drive cycle, this slowly changes between values (gen1):
    //     leaf_on_off.txt:
    //       5 7 3 2 0 1 3 7
    //     leaf_on_rev_off.txt:
    //       5 7 3 2 0 6
    //     leaf_on_Dx3.txt:
    //       5 7 3 2 0 2 3 2 0 2 3 2 0 2 3 7
    //     leaf_on_stat_DRDRDR.txt:
    //       0 1 3 7
    //     leaf_on_Driveincircle_off.txt:
    //       5 3 2 0 8 B 3 2 0 8 A B 3 2 0 8 A B A 8 0 2 3 7 
    //     leaf_on_wotind_off.txt:
    //       3 2 0 8 A B 3 7
    //     leaf_on_wotinr_off.txt:
    //       5 7 3 2 0 8 A B 3 7
    //     leaf_ac_charge.txt:
    //       4 6 E 6
    //   Possibly some kind of control flags, try to figure out
    //   using:
    //     grep 000001D4 leaf_on_wotind_off.txt | cut -d' ' -f10 | uniq | ~/projects/leaf_tools/util/hex_to_ascii_binary.py
    //   2016:
    //     Has different values!
    // LSB nibble:
    //   0: Always (gen1)
    //   1:  (2016)

    // 2016 drive cycle:
    //   E0: to 0.15s
    //   E1: 2 messages
    //   61: to 2.06s (inverter is powered up and precharge
    //                 starts and completes during this)
    //   21: to 13.9s
    //   01: to 17.9s
    //   81: to 19.5s
    //   A1: to 26.8s
    //   21: to 31.0s
    //   01: to 33.9s
    //   81: to 48.8s
    //   A1: to 53.0s
    //   21: to 55.5s
    //   61: 2 messages
    //   60: to 55.9s
    //   E0: to end of capture (discharge starts during this)

    // This value has been chosen at the end of the hardest
    // acceleration in the wide-open-throttle pull, with full-ish
    // torque still being requested, in
    //   LeafLogs/leaf_on_wotind_off.txt
    //outFrame.data.bytes[6] = 0x00;

    // This value has been chosen for being seen most of the time
    // when, and before, applying throttle in the wide-open-throttle
    // pull, in
    //   LeafLogs/leaf_on_wotind_off.txt
    outFrame.data.bytes[6] = 0x30;    //brake applied heavilly.

    // Value chosen from a 2016 log
    //outFrame.data.bytes[6] = 0x61;

    // Value chosen from a 2016 log
    // 2016-24kWh-ev-on-drive-park-off.pcap #12101 / 15.63s
   // outFrame.data.bytes[6] = 0x01;
    //byte 6 brake signal

    // Extra CRC
    nissan_crc(outFrame.data.bytes, 0x85);

    /*Serial.print(F("Sending "));
    print_fancy_inFrame(inFrame);
    Serial.println();*/

 Can0.sendFrame(outFrame);

//We need to send 0x1db here with voltage measured by inverter

        outFrame.id = 0x1db;            // Set our transmission address ID
        outFrame.length = 8;            // Data payload 3 bytes
        outFrame.extended = 0;          // Extended addresses - 0=11-bit 1=29bit
        outFrame.rtr=1;                 //No request
        outFrame.data.bytes[0]=0x00;  
        outFrame.data.bytes[1]=0x00;
        outFrame.data.bytes[2]=0x00;
        outFrame.data.bytes[3]=0x00;
        outFrame.data.bytes[4]=0x00;
        outFrame.data.bytes[5]=0x00;
        outFrame.data.bytes[6]=counter_1db;
        outFrame.data.bytes[7]=0x00;


    counter_1db++;
    if(counter_1db >= 4)
      counter_1db = 0;

        

        Can0.sendFrame(outFrame);

///////////////Originally sent as 100ms messages.///////////////////////////////////////////

  

        outFrame.id = 0x50b;            // Set our transmission address ID
        outFrame.length = 7;            // Data payload 8 bytes
        outFrame.extended = 0;          // Extended addresses - 0=11-bit 1=29bit
        outFrame.rtr=1;                 //No request
    // Statistics from 2016 capture:
    //     10 00000000000000
    //     21 000002c0000000
    //    122 000000c0000000
    //    513 000006c0000000

    // Let's just send the most common one all the time
    // FIXME: This is a very sloppy implementation
  //  hex_to_data(outFrame.data.bytes, "00,00,06,c0,00,00,00");
        outFrame.data.bytes[0]=0x00;
        outFrame.data.bytes[1]=0x00;  
        outFrame.data.bytes[2]=0x06;
        outFrame.data.bytes[3]=0xc0;
        outFrame.data.bytes[4]=0x00;
        outFrame.data.bytes[5]=0x00;
        outFrame.data.bytes[6]=0x00;

    /*CONSOLE.print(F("Sending "));
    print_fancy_inFrame(inFrame);
    CONSOLE.println();*/
        Can0.sendFrame(outFrame); 

/////////////////////////////////////////////////////////////////////////////////////////////////////


//these messages go out on vehicle can and are specific to driving the E46 instrument cluster etc.

//////////////////////DME Messages //////////////////////////////////////////////////////////

        outFrame.id = 0x316;            // Set our transmission address ID
        outFrame.length = 8;            // Data payload 8 bytes
        outFrame.extended = 0;          // Extended addresses - 0=11-bit 1=29bit
        outFrame.rtr=1;                 //No request
        outFrame.data.bytes[0]=0x05;
        outFrame.data.bytes[1]=0x00;
        outFrame.data.bytes[2]=lowByte(outRPM);  //RPM LSB
        outFrame.data.bytes[3]=highByte(outRPM);  //RPM MSB [RPM=(hex2dec("byte3"&"byte2"))/6.4]  0x12c0 should be 750rpm on tach
       // outFrame.data.bytes[2]=0xc0;  //RPM LSB
       // outFrame.data.bytes[3]=0x12;  //RPM MSB [RPM=(hex2dec("byte3"&"byte2"))/6.4]  0x12c0 should be 750rpm on tach
 //       outFrame.data.bytes[2]=0xff;  //RPM LSB
 //       outFrame.data.bytes[3]=0x4f;  //RPM MSB [RPM=(hex2dec("byte3"&"byte2"))/6.4]  0x4fff gives 3200rpm on tach
        outFrame.data.bytes[4]=0x00;
        outFrame.data.bytes[5]=0x00;
        outFrame.data.bytes[6]=0x00;
        outFrame.data.bytes[7]=0x00;
        Can1.sendFrame(outFrame);


/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////        


  //********************temp sense  *******************************
 // tempValue=analogRead(tempIN); //read Analog pin voltage
//  The sensor and gauge are not linear.  So if the sensed 
//  Voltage is less than the mid point the Map function
//  is used to map the input values to output values For the gauge
//  output values (in decimal):
//  86 = First visible movment of needle starts
//  93 = Begining of Blue section
//  128 = End of Blue Section
//  169 = Begin Straight up
//  193 = Midpoint of needle straight up values
//  219 = Needle begins to move from center
//  230 = Beginning of Red section
//  254 = needle pegged to the right
// MAP program statement: map(value, fromLow, fromHigh, toLow, toHigh)

 // if(tempValue < 964){  //if pin voltage < mid point value 
tempValue= inverter_status.inverter_temperature;  //read temp from leaf inverter can.
tempValue= map(tempValue,15,80,133,254); //Map to e46 temp gauge    //Alibro Changed 88 to 133
 //   }
//  else {
 //   tempValue= map(tempValue,964,1014,219,254); //Map upper half of range
 // }

//Can bus data packet values to be sent
        outFrame.id = 0x329;            // Set our transmission address ID
        outFrame.length = 8;            // Data payload 8 bytes
        outFrame.extended = 0;          // Extended addresses - 0=11-bit 1=29bit
        outFrame.rtr=1;                 //No request
        outFrame.data.bytes[0]=ABSMsg;  //needs to cycle 11,86,d9
        outFrame.data.bytes[1]=tempValue; //temp bit tdata 
        outFrame.data.bytes[2]=0xc5;
        outFrame.data.bytes[3]=0x00;
        outFrame.data.bytes[4]=0x00;
        outFrame.data.bytes[5]=0x00; //Throttle position currently just fixed value
        outFrame.data.bytes[6]=0x00;
        outFrame.data.bytes[7]=0x00;
        Can1.sendFrame(outFrame);


    counter_329++;
    if(counter_329 >= 22) counter_329 = 0;
    if(counter_329==0) ABSMsg=0x11;
    if(counter_329==8) ABSMsg=0x86;
    if(counter_329==15) ABSMsg=0xd9;
   
    



//From ECU, MPG, MIL, overheat light, Cruise
// ErrorState variable controls:
//Check engine(binary 10), Cruise (1000), EML (10000)
//Temp light Variable controls temp light.  Hex 08 is on zero off


  //**************** set Error Lights & cruise light ******
 // hex 08 = Overheat light on
//  // hex 08 = Overheat light on
 //Set check engine. Binary 0010 (hex 02)
// No error light (zero)


 // int z = 0x60; // + y;  higher value lower MPG


//Can bus data packet values to be sent
        outFrame.id = 0x545;            // Set our transmission address ID
        outFrame.length = 8;            // Data payload 8 bytes
        outFrame.extended = 0;          // Extended addresses - 0=11-bit 1=29bit
        outFrame.rtr=1;                 //No request
        outFrame.data.bytes[0]=0x00;  //2=check ewwngine on , 0=check engine off
        outFrame.data.bytes[1]=0x00; //LSB fuel comp
        outFrame.data.bytes[2]=0x00;  //MSB fuel comp
        outFrame.data.bytes[3]=0x00;   // hex 08 = Overheat light on
        outFrame.data.bytes[4]=0x7E;
        outFrame.data.bytes[5]=0x10; 
        outFrame.data.bytes[6]=0x00;
        outFrame.data.bytes[7]=0x18;
        Can1.sendFrame(outFrame);



}
}
 
    }
    



void Msgs100ms()                      ////100ms messages here
{
if(timer_Frames100.check())
{
  if(can_status)
   {

//  digitalWrite(led, !digitalRead(led)); //toggle led everytime we fire the 100ms messages.

        outFrame.id = 0x50b;            // Set our transmission address ID
        outFrame.length = 7;            // Data payload 8 bytes
        outFrame.extended = 0;          // Extended addresses - 0=11-bit 1=29bit
        outFrame.rtr=1;                 //No request
    // Statistics from 2016 capture:
    //     10 00000000000000
    //     21 000002c0000000
    //    122 000000c0000000
    //    513 000006c0000000

    // Let's just send the most common one all the time
    // FIXME: This is a very sloppy implementation
  //  hex_to_data(outFrame.data.bytes, "00,00,06,c0,00,00,00");
        outFrame.data.bytes[0]=0x00;
        outFrame.data.bytes[1]=0x00;  
        outFrame.data.bytes[2]=0x06;
        outFrame.data.bytes[3]=0xc0;
        outFrame.data.bytes[4]=0x00;
        outFrame.data.bytes[5]=0x00;
        outFrame.data.bytes[6]=0x00;

    /*CONSOLE.print(F("Sending "));
    print_fancy_inFrame(inFrame);
    CONSOLE.println();*/
        Can0.sendFrame(outFrame); 
}
}      
}

// Define constants
const int THROTTLE_MIN = 145;
const int THROTTLE_MAX = 620;
const int REGEN_TORQUE_MIN = -30;
const int REGEN_TORQUE_MAX = -75;
const int REGEN_RAMP_TIME = 850;
const int BRAKE_PEDAL_THRESHOLD = 400;
const int PACK_VOLTAGE_THRESHOLD = 396;

// Define variables
unsigned long regenStartTime = 0;
bool regenActive = false;
int regenTorque = 0;

void readPedals()
{
    // Read throttle value and constrain it to valid range
    ThrotVal = analogRead(Throttle1);
    ThrotVal = constrain(ThrotVal, THROTTLE_MIN, THROTTLE_MAX);
    ThrotVal = map(ThrotVal, THROTTLE_MIN, THROTTLE_MAX, 0, MaxTrq);

    // Apply regen braking if conditions are met
    if (digitalRead(Brake) && inverter_status.speed > BRAKE_PEDAL_THRESHOLD && inv_volts_local < PACK_VOLTAGE_THRESHOLD)
    {
        applyRegenBraking();
    }
    else
    {
        regenActive = false;
    }
}

void applyRegenBraking()
{
    // Start regen braking if not already active
    if (!regenActive)
    {
        regenStartTime = millis();
        regenActive = true;
    }

    // Calculate elapsed time since regen started
    unsigned long elapsedTime = millis() - regenStartTime;

    // Ramp regen torque from REGEN_TORQUE_MIN to REGEN_TORQUE_MAX over REGEN_RAMP_TIME
    regenTorque = map(elapsedTime, 0, REGEN_RAMP_TIME, REGEN_TORQUE_MIN, REGEN_TORQUE_MAX);

    // Reduce regen torque based on speed
    float speedRatio = inverter_status.speed / 1500.0;
    regenTorque = int(regenTorque * speedRatio);
}

void SendTorqueRequest()
{
    if (regenActive)
    {
        final_torque_request = regenTorque;
    }
    else
    {
        final_torque_request = ThrotVal;
    }
}


void ProcessRPM() //here we convert motor rpm values received from the leaf inverter into BMW E46 RPM can message.
{
outRPM = (inverter_status.speed)*4.1;
//if(outRPM<4800) outRPM=4800;  //set lowest rpm to 750 displayed on tach to keep car alive thinking engine is running.
//if(outRPM>44800) outRPM=44800;  //DONT READ MORE THAN 7000RPM!
//Serial.println(outRPM);  
//Serial.println(inverter_status.speed);  
}




static int8_t fahrenheit_to_celsius(uint16_t fahrenheit)
{
  int16_t result = ((int16_t)fahrenheit - 32) * 5 / 9;
  if(result < -128)
    return -128;
  if(result > 127)
    return 127;
  return result;
}

void CheckCAN()
{

///////////////////////////////////////////////////////////////////////////////////////////////////////////
//read incomming data from inverter//////////////////
//////////////////////////////////////////////////////
  if(Can0.available())
  {
    Can0.read(inFrame);
    //Serial.println(inFrame.id, HEX);
  

  if(inFrame.id == 0x1da && inFrame.length == 8){
  //  last_received_from_inverter_timestamp = millis();

    inverter_status.voltage = ((uint16_t)inFrame.data.bytes[0] << 2) |
        (inFrame.data.bytes[1] >> 6);

    int16_t parsed_speed = ((uint16_t)inFrame.data.bytes[4] << 8) |
        (uint16_t)inFrame.data.bytes[5];
    inverter_status.speed = (parsed_speed == 0x7fff ? 0 : parsed_speed);

    inverter_status.error_state = (inFrame.data.bytes[6] & 0xb0) != 0x00;
  }

  if(inFrame.id == 0x55a && inFrame.length == 8){
   // last_received_from_inverter_timestamp = millis();

    inverter_status.inverter_temperature = fahrenheit_to_celsius(inFrame.data.bytes[2]);
    inverter_status.motor_temperature = fahrenheit_to_celsius(inFrame.data.bytes[1]);
    //Serial.println(inverter_status.inverter_temperature);
  }

  }

////////////////////////////////////////////////////////////////////////////////////////////////

  
}

static void nissan_crc(uint8_t *data, uint8_t polynomial)
{
  // We want to process 8 bytes with the 8th byte being zero
  data[7] = 0;
  uint8_t crc = 0;
  for(int b=0; b<8; b++)
  {
    for(int i=7; i>=0; i--)
    {
      uint8_t bit = ((data[b] &(1 << i)) > 0) ? 1 : 0;
      if(crc >= 0x80) 
        crc = (byte)(((crc << 1) + bit) ^ polynomial);
      else 
        crc = (byte)((crc << 1) + bit);
    }
  }
  data[7] = crc;
}
Alibro
Posts: 949
Joined: Sun Feb 23, 2020 9:24 am
Location: Northern Ireland
Has thanked: 346 times
Been thanked: 208 times
Contact:

Re: [Driving] Freelander EV With Gen1 Leaf electrics and EVBMW VCU

Post by Alibro »

Several hundred miles later, after making a few changes to the code above I'm happy to report I have regen working reliably. It's not perfect and is dialled back to help keep things smooth but is working well enough for me to move on to something else.

And that is charging.
Up to now I have been plugging the car in during the day, turning on the ignition and setting a timer for the approximate time charging should finish. This is partly to check the charging had stopped as it should but mainly to turn the ignition off again. It has worked for over a year but is a tad suboptimal.
So for the last couple of weeks I've been working on automating the charging process and today I finally got the last part of the puzzle sorted that will allow me to install it.
The first thing I did was build a circuit to detect when the charging plug was shoved in.

This is from a post in another thread.
viewtopic.php?p=79533#p79533
Alibro wrote: Mon Feb 03, 2025 2:39 pm So after a bit of messing about I came up with a circuit that works well enough to turn on the relays when the charger plug is inserted and turn off the relays when the catch is pressed on the J1772 handle so that I can't pull the handle out with the current still flowing.

PP_Sense.png

In my case the J1772 socket already has a 2.7k resistor going to ground built into it so I took that out of the circuit.
I also found that with 3.3k resistor in the voltage divider circuit from 12V to pin 2 the relays were on all the time even without the cable plugged in. I tried changing the 2.7k resistor but couldn't get the circuit to trigger when the catch was pressed so changed the 3.3k resistor instead. I used a POT fo figure out what the ideal resistance was and it ended up being 255R. I didn't have a 255R resistor have so used a 240R and 13R in series which was close enough.

The relay module I'm using is a 12V 8 way block that can be set to trigger on a neg or pos signal.
https://amzn.to/4hroiOX

This is it on some strip board and so far it's working so now I need to work out how to implement it.

IMG_20250203_145120687.jpg
Next step was to ensure I had a backup or failsafe to prevent over charging so I made some changes to the VCU code. I never used O/P 3 to power up the inverter as it was originally intended so it's now providing GND to the charger. If the pack exceeds a preset voltage which is 2 volts higher than it the default max it will power off the charger which basically crashes out of the over charge situation, in fairness I should have done this earlier and my only excuse is it was less of an issue as I always set an alarm.
Now the charger will turn off when the Charge Control Arduino Due sees the pack reach a preset voltage from the shunt.
And if it fails to stop charging for any reason the VCU uses the Inverter voltage to turn off the charger at a preset voltage

So now I have a circuit that detects when the charger plug is in I needed to figure out how to implement it. I wrote a list of everything required for charging and spent a few nights wiring in a 6 way relay module I intend to use.
To get my head around what was required I wrote it out.

Current Setup
Plug in the cable
Turn on ignition to power up all devices.
The VCU sends GND to the charger to power it up
An Arduino Due checks the voltage from an Isabellenhutte shunt using CAN. If the voltage is within a preset range the Due closes the CP line using a relay (CP Relay) to allow charging to begin. This Arduino Due also sends Voltage, current and power info to the Android head unit over WiFi.
When the Due sees the voltage above a certain level it turns off the relay which disconnects CP and stops charging.
A backup safety is provided by the VCU which will power off the charger if it sees an over volt situation.

New Setup
Scenario 1 - Charging starts and completes
Plug in the charger cable.
LM393 detects EVSE present from PP and sends GND to a relay (ChargeEnable) and PIN8 on the Nano.
The ChargeEnable relay sends GND to power the Nano and at the same time removes GND from Throttle pedal to prevent driving off while plugged in.
The Nano powers the Due using a MOSFET or single relay module then pauses for 2 seconds. This happens only once when the Nano is turned on.
If the voltage is within a preset range the Due closes the CP relay to enable charging to begin.
The CP relay as well as connecting the CP to the charger sends GND to PIN10 of the Nano using 2nd set of contacts.
We push the button to start charging (momentary GND to PIN9) or it starts automagically at 2am.
The Nano checks for PIN8 and PIN10 to GND and if so activates the relays with the last relay sending a second (latching) ground to power the Nano. This is to prevent the Nano crashing should the plug be pulled.
Charging completes and the Arduino Due releases the CP relay to stop the charger.
The relay also disconnects GND from PIN10 so the Nano turns off the Module relays and removes power from the Due.
The Nano will still receive power from the LM393 as the EVSE is still plugged in and the Charge Enable Relay is still closed but this should be OK as even with the relay and LM393 they should only draw around 100 to 150mA with the module relays turned off.


Scenario 2 - Plug pulled while charging
Everything as above.
If the Plug is pulled while the car is charging the LM393 removes GND from the ChargeEnable relay and PIN8 of the Nano
The Nano still has GND through the last relay on the module so remains powered on.
Because the Nano has lost GND on PIN8 it turns off the relays.
When the last relay turns off it removes GND from the Nano which powers it off.

The critical thing both now and when this is implemented is that the battery should never be allowed to charge above 400V.
The Due I use to control this gets it's voltage from the shunt and the VCU which is a fail safe gets it's voltage from the Inverter so hopefully I've mitigated as best I can.
Alibro
Posts: 949
Joined: Sun Feb 23, 2020 9:24 am
Location: Northern Ireland
Has thanked: 346 times
Been thanked: 208 times
Contact:

Re: [Driving] Freelander EV With Gen1 Leaf electrics and EVBMW VCU

Post by Alibro »

The last part of the puzzle was to write the code for an Arduino Nano to control the charging process.

After many not fun hours fighting with ChatGPT and Deepseek, I finally have a working software solution, if anybody is mad enough to have a look the code is below.
Basically I gave up on using a MOSFET module as neither ChatGPT nor Deepseek could figure out the logic required to turn it on when the Nano boots, and keep it on until the relays are all turned off, so I switched to using a two way relay module instead. I figured we were already using a relay module enabled by the Arduino pins going low so this would simplify things.
ChatGPT still couldn't make it work correctly and often messed up the bits of code that were working so I gave up on it and tried Deepseek which was much better. It seemed more able to make the changes I wanted without screwing up the rest of the code and eventually gave me a working program.

I only understand the basics of Arduino code but essentially this is what happens.
When I plug in the charger cable the Arduino Nano is turned on by the LM393.
The Nano turns on relays 7 and 8 when it boots turning on the Arduino Due.
It then sits waiting for grounds to come from PP and CP,
If both are present and I don't push the charge now button it will do nothing until 2am, then turn on relays 1 to 6 starting the charging.
If the pack voltage reaches a preset level the Arduino Due releases CP and the Nano turns off all relays stopping charging.
If the car is still charging at 8am all relays are turned off stopping charging.
If I had pushed the charge now button the Nano would have turned on relays 1 to 6 starting charging and should continue until complete or 8am.

I don't expect anyone to check this for me and I will be keeping an eye on it for a while but if it's helpful to anyone else out there then that's great.
If anyone has any suggestions I'll be happy to hear them.

Code: Select all

#include <Wire.h>
#include <RTClib.h>
#include <EEPROM.h>

constexpr int NUM_RELAYS = 6;
constexpr int TOTAL_RELAYS = 8; // Now includes relays 7 and 8
constexpr unsigned long STATUS_UPDATE_INTERVAL = 5000;
constexpr int SERIAL_BAUD_RATE = 9600;
constexpr int RELAY_ACTIVE_STATE = LOW;
constexpr int RELAY_INACTIVE_STATE = HIGH;

struct TimeSettings {
    int8_t hour;
    int8_t minute;
};

struct EEPROMConfig {
    static constexpr int START_HOUR = 0;
    static constexpr int START_MINUTE = 1;
    static constexpr int END_HOUR = 2;
    static constexpr int END_MINUTE = 3;
};

class RelayController {
private:
    RTC_DS3231 rtc;
    const int relayPins[TOTAL_RELAYS] = {2, 3, 4, 5, 6, 7, 8, 9}; // 7 and 8 are special
    const int evsePresentPin = 10;
    const int cpPresentPin = 11;
    const int overridePin = 12;
    TimeSettings startTime = {2, 0};
    TimeSettings endTime = {8, 0};
    bool evseActive = false;
    bool cpActive = false;
    bool overrideTriggered = false;
    unsigned long lastStatusUpdate = 0;
    bool relaysAreOn = false;

    bool isValidTime(int hour, int minute) {
        return hour >= 0 && hour < 24 && minute >= 0 && minute < 60;
    }

    void loadFromEEPROM() {
        EEPROM.get(EEPROMConfig::START_HOUR, startTime);
        EEPROM.get(EEPROMConfig::END_HOUR, endTime);
        if (!isValidTime(startTime.hour, startTime.minute)) startTime = {2, 0};
        if (!isValidTime(endTime.hour, endTime.minute)) endTime = {8, 0};
    }

    void saveToEEPROM() {
        EEPROM.put(EEPROMConfig::START_HOUR, startTime);
        EEPROM.put(EEPROMConfig::END_HOUR, endTime);
    }

    void setRelayStates(bool active) {
        if (active) {
            for (int i = 0; i < NUM_RELAYS; i++) {
                digitalWrite(relayPins[i], RELAY_ACTIVE_STATE);
                delay(100);
            }
            relaysAreOn = true;
        } else {
            for (int i = NUM_RELAYS - 1; i >= 0; i--) {
                digitalWrite(relayPins[i], RELAY_INACTIVE_STATE);
                delay(100);
            }
            relaysAreOn = false;
        }

        // Ensure relays 7 and 8 remain ON until everything turns OFF
        digitalWrite(relayPins[6], relaysAreOn ? RELAY_ACTIVE_STATE : RELAY_INACTIVE_STATE);
        digitalWrite(relayPins[7], relaysAreOn ? RELAY_ACTIVE_STATE : RELAY_INACTIVE_STATE);
    }

    bool isTimeInRange(const DateTime& now) {
        int currentMinutes = now.hour() * 60 + now.minute();
        int startMinutes = startTime.hour * 60 + startTime.minute;
        int endMinutes = endTime.hour * 60 + endTime.minute;
        return (startMinutes <= endMinutes) ? (currentMinutes >= startMinutes && currentMinutes < endMinutes)
                                            : (currentMinutes >= startMinutes || currentMinutes < endMinutes);
    }

public:
    void begin() {
        Serial.begin(SERIAL_BAUD_RATE);

        if (!rtc.begin()) {
            Serial.println(F("RTC initialization failed!"));
            while (1) delay(1000);
        }
        if (rtc.lostPower()) {
            Serial.println(F("RTC lost power, resetting to compile time."));
            rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
        }

        for (int i = 0; i < TOTAL_RELAYS; i++) {
            pinMode(relayPins[i], OUTPUT);
            digitalWrite(relayPins[i], (i < 6) ? RELAY_INACTIVE_STATE : RELAY_ACTIVE_STATE);
        }

        pinMode(evsePresentPin, INPUT_PULLUP);
        pinMode(cpPresentPin, INPUT_PULLUP);
        pinMode(overridePin, INPUT_PULLUP);

        loadFromEEPROM();
    }

    void update() {
        DateTime now = rtc.now();
        evseActive = digitalRead(evsePresentPin) == LOW;
        cpActive = digitalRead(cpPresentPin) == LOW;
        bool overridePressed = digitalRead(overridePin) == LOW;

        if (!evseActive || !cpActive) {
            overrideTriggered = false;
            setRelayStates(false);
        } else {
            if (overridePressed) {
                overrideTriggered = true;
            }
            bool shouldActivate = overrideTriggered || isTimeInRange(now);
            setRelayStates(shouldActivate);
        }

        if (Serial.available()) {
            processCommand(Serial.readStringUntil('\n'));
        }

        if (millis() - lastStatusUpdate >= STATUS_UPDATE_INTERVAL) {
            printStatus(now);
            lastStatusUpdate = millis();
        }
    }

    void processCommand(String command) {
        Serial.println("Processing command: " + command);
    }

    void printStatus(const DateTime& now) {
        Serial.println(F("\n=== System Status ==="));
        Serial.print(F("Current Time: "));
        Serial.print(now.year(), DEC);
        Serial.print(F("/"));
        Serial.print(now.month(), DEC);
        Serial.print(F("/"));
        Serial.print(now.day(), DEC);
        Serial.print(F(" "));
        Serial.print(now.hour(), DEC);
        Serial.print(F(":"));
        Serial.println(now.minute(), DEC);
        Serial.print(F("Start Time: "));
        Serial.print(startTime.hour);
        Serial.print(F(":"));
        Serial.println(startTime.minute);
        Serial.print(F("End Time: "));
        Serial.print(endTime.hour);
        Serial.print(F(":"));
        Serial.println(endTime.minute);
        Serial.print(F("EVSE Present? "));
        Serial.println(evseActive ? F("Yes") : F("No"));
        Serial.print(F("CP Present? "));
        Serial.println(cpActive ? F("Yes") : F("No"));
        Serial.print(F("Override Active? "));
        Serial.println(overrideTriggered ? F("Yes") : F("No"));
        Serial.print(F("Temperature: "));
        Serial.print(rtc.getTemperature());
        Serial.println(F("°C"));
    }
};

RelayController controller;

void setup() {
    controller.begin();
}

void loop() {
    controller.update();
}
User avatar
marcexec
Posts: 157
Joined: Tue May 14, 2019 12:52 pm
Location: Dublin, Ireland
Has thanked: 706 times
Been thanked: 60 times

Re: [Driving] Freelander EV With Gen1 Leaf electrics and Original EVBMW VCU

Post by marcexec »

How many Arduinos do you have by now? I totally get the hammer and nail situation as I'm way behind in those skills but it seems a bit much in terms of complexity & failure points, even if only on the 12V side.
A motorcyclist is never late, Frodo Baggins. Nor is he early. He arrives precisely when he means to.
Getting started guide for Celeron55's iPDM56
My Suzuki RF400 build @ES
Honda IMA & Lebowski howto
Image
Alibro
Posts: 949
Joined: Sun Feb 23, 2020 9:24 am
Location: Northern Ireland
Has thanked: 346 times
Been thanked: 208 times
Contact:

Re: [Driving] Freelander EV With Gen1 Leaf electrics and Original EVBMW VCU

Post by Alibro »

marcexec wrote: Fri Feb 28, 2025 11:19 am How many Arduinos do you have by now? I totally get the hammer and nail situation as I'm way behind in those skills but it seems a bit much in terms of complexity & failure points, even if only on the 12V side.
Far too many.
I intend to buy a Zombieverter at some stage which will enable me to trim a lot of the silliness but for now I'm still adding Arduinos. :?

When the Zombieverter comes a total rewire of the HV and LV boxes will be in order to tidy up the mess I thought I was tidying up last year when I bought a couple of fuse blocks. This is not something I look forward to so there will be a lot of procrastination before then.
jrbe
Posts: 531
Joined: Mon Jul 03, 2023 3:17 pm
Location: CT, central shoreline, USA
Has thanked: 183 times
Been thanked: 156 times

Re: [Driving] Freelander EV With Gen1 Leaf electrics and EVBMW VCU

Post by jrbe »

Alibro wrote: Thu Feb 27, 2025 8:20 pm After many not fun hours fighting with ChatGPT and Deepseek, I finally have a working software solution, if anybody is mad enough to have a look the code is below.
Basically I gave up on using a MOSFET module as neither ChatGPT nor Deepseek could figure out the logic required to turn it on when the Nano boots, and keep it on until the relays are all turned off, so I switched to using a two way relay module instead. I figured we were already using a relay module enabled by the Arduino pins going low so this would simplify things.
ChatGPT still couldn't make it work correctly and often messed up the bits of code that were working so I gave up on it and tried Deepseek which was much better. It seemed more able to make the changes I wanted without screwing up the rest of the code and eventually gave me a working program.
My ChatGPT interactions are 90% of a typical arrignorant "brilliant" jerk for me. Doesn't really do what you ask it to do, is wrong often, and ignores tons of inputs. About 10% of the time it was worth spending the time to get something useful out.

You should try www.claude.ai It is seriously impressive at almost everything, especially with the recent 3.7 update.
If you use VS code there is the Cline extension to connect to different AIs through their API. This setup can do some seriously impressive coding with Claude 3.7. You need to keep the guardrails up so it goes in the direction you want. If you feed in datasheets / other info it needs, Claude will give you good code 99% of the time.
Alibro
Posts: 949
Joined: Sun Feb 23, 2020 9:24 am
Location: Northern Ireland
Has thanked: 346 times
Been thanked: 208 times
Contact:

Re: [Driving] Freelander EV With Gen1 Leaf electrics and EVBMW VCU

Post by Alibro »

jrbe wrote: Fri Feb 28, 2025 2:48 pm My ChatGPT interactions are 90% of a typical arrignorant "brilliant" jerk for me. Doesn't really do what you ask it to do, is wrong often, and ignores tons of inputs. About 10% of the time it was worth spending the time to get something useful out.

You should try www.claude.ai It is seriously impressive at almost everything, especially with the recent 3.7 update.
If you use VS code there is the Cline extension to connect to different AIs through their API. This setup can do some seriously impressive coding with Claude 3.7. You need to keep the guardrails up so it goes in the direction you want. If you feed in datasheets / other info it needs, Claude will give you good code 99% of the time.
I tried Claude but it wasn't all that much better than ChatGPT for what I was doing. Then it started asking me for money so I didn't go back to it.
Your description of ChatGPT is pretty spot on except perhaps not strongly enough worded. I was a bit surprised that DeepSeek worked so well but now I fear my PC will be riddled with little Asian viruses. :lol:
Post Reply