Flying ADC 16 channel BMS 96S test

Topics concerning OEM and open source BMSes
User avatar
johu
Site Admin
Posts: 6679
Joined: Thu Nov 08, 2018 10:52 pm
Location: Kassel/Germany
Has thanked: 361 times
Been thanked: 1519 times
Contact:

Re: Flying ADC 16 channel BMS 96S test

Post by johu »

Thanks for checking on that.

I carried out an experiment of my own with 64 LFP cells all charged to the maximum of 3.4V or roughly 220V total.

With the 4 blocks not interconnected I observed nothing strange. So I connected them all with 500 Ohm in between and measured across one of them with the scope to be able to see any unwanted current spikes. Nothing.

It's a mix of 2.1 and 2.3 boards.

Became brave and connected them straight through without the resistors to see if anything happened. Again, nothing.

Will build more battery bricks (have about 120 cells)
Attachments
1748436731999.jpg
Support R/D and forum on Patreon: https://patreon.com/openinverter - Subscribe on odysee: https://odysee.com/@openinverter:9
User avatar
Proton
Posts: 261
Joined: Sat May 06, 2023 2:23 am
Location: Georgia/US
Has thanked: 173 times
Been thanked: 27 times

Re: Flying ADC 16 channel BMS 96S test

Post by Proton »

Do you have any 3.7v batteries to go over 4v? Even some small 26500 batteries would work.
User avatar
johu
Site Admin
Posts: 6679
Joined: Thu Nov 08, 2018 10:52 pm
Location: Kassel/Germany
Has thanked: 361 times
Been thanked: 1519 times
Contact:

Re: Flying ADC 16 channel BMS 96S test

Post by johu »

In fact I developed the BMS with a 16S NMC battery that I charged to 4.1V. Like said, I've never seen it blow up when only one module is used.
Support R/D and forum on Patreon: https://patreon.com/openinverter - Subscribe on odysee: https://odysee.com/@openinverter:9
User avatar
uhi22
Posts: 1106
Joined: Mon Mar 14, 2022 3:20 pm
Location: Ingolstadt/Germany
Has thanked: 200 times
Been thanked: 607 times

Re: Flying ADC 16 channel BMS 96S test

Post by uhi22 »

johu wrote: Wed May 28, 2025 12:58 pm With the 4 blocks not interconnected I observed nothing strange. So I connected them all with 500 Ohm in between and measured across one of them with the scope to be able to see any unwanted current spikes. Nothing.
That's a good starting point. In theory, nothing should break even if we inject voltage between the separate battery modules, like 10V DC, 100V DC, 400V rectangle, etc, right? Because the we assume "perfect isolation". But thinking of this:
2025-06-01_C_iso_C_CE.jpg
2025-06-01_C_iso_C_CE.jpg (23.98 KiB) Viewed 107 times
1. There is capacitance across the opto. If we apply a rectangle between the battery and the chassis ground (which basically a isolation monitoring device could do), we can open the FETs, if the voltage divider, formed by C_iso and C_GS provides sufficient voltage on the gate.

2. Also a weak resistive connection (1 MOhm) over C_iso, together with higher voltage, has the same effect.

3. Also the C_CE needs to be checked, because it also forms a voltage divider with C_GS, in case the OUTP is jumping.
User avatar
johu
Site Admin
Posts: 6679
Joined: Thu Nov 08, 2018 10:52 pm
Location: Kassel/Germany
Has thanked: 361 times
Been thanked: 1519 times
Contact:

Re: Flying ADC 16 channel BMS 96S test

Post by johu »

Thanks!
So adding 22n in parallel to R6 should have a positive effect, right? Now the combined C_GS >> C_iso and C_CE which should pretty much rule out false triggers?
The dead time now needs to be increased to around 2 ms until R6 has sufficiently discharged C_22. With the given time constants (one switch every 25 ms) that should be no problem
Support R/D and forum on Patreon: https://patreon.com/openinverter - Subscribe on odysee: https://odysee.com/@openinverter:9
User avatar
uhi22
Posts: 1106
Joined: Mon Mar 14, 2022 3:20 pm
Location: Ingolstadt/Germany
Has thanked: 200 times
Been thanked: 607 times

Re: Flying ADC 16 channel BMS 96S test

Post by uhi22 »

Hm, I'm not really convinced of my assumption anymore.
The C_iso is around 1pF (https://www.mouser.com/datasheet/2/143/ ... M5gYtOZNbV) and the C_gs specified as "max 600pF" (http://www.stansontech.com/download/Tre ... SRG_V1.pdf), so if we assume 300pF we would have a divider 1/300. So for reaching the treshold voltage (above 1V), we would need 300V. Of course, the layout may increase the C_iso, but I would not treat this as 100% proof to the the root cause.

Maybe multiple effects come together to reach the worst case, e.g. the above combined with some resistive leakage due to humidity. 1V threshold voltage together with 10k needs only 100µA. How much could the 10k be lowered?
User avatar
johu
Site Admin
Posts: 6679
Joined: Thu Nov 08, 2018 10:52 pm
Location: Kassel/Germany
Has thanked: 361 times
Been thanked: 1519 times
Contact:

Re: Flying ADC 16 channel BMS 96S test

Post by johu »

uhi22 wrote: Sun Jun 01, 2025 10:22 am How much could the 10k be lowered?
Probably a lot. Cell drain reduction was priority but maybe I overshot the target there?
Support R/D and forum on Patreon: https://patreon.com/openinverter - Subscribe on odysee: https://odysee.com/@openinverter:9
User avatar
uhi22
Posts: 1106
Joined: Mon Mar 14, 2022 3:20 pm
Location: Ingolstadt/Germany
Has thanked: 200 times
Been thanked: 607 times

Re: Flying ADC 16 channel BMS 96S test

Post by uhi22 »

Maybe go with 2k2 || 10nF, and look what the statistics will say.
And reduce the cell drain by reducing the sampling frequency. Maybe introducing two modes with A: fast sampling, higher drain and B: slow sampling, low drain, so depending on the use case everybody can optimize just by setting a parameter.
User avatar
uhi22
Posts: 1106
Joined: Mon Mar 14, 2022 3:20 pm
Location: Ingolstadt/Germany
Has thanked: 200 times
Been thanked: 607 times

Re: Flying ADC 16 channel BMS 96S test

Post by uhi22 »

Another direction of thinking: At which voltage the flying ADC is flying, before any of the mux switches are activated? By which path is this voltage determined?
- into direction of chassis ground, it is isolated via the 5V/5V DCDC and the data isolators. Fine.
- into direction of battery, the voltage can rise or fall until a PFET will reach the avalanche voltage (>100V according to http://www.stansontech.com/download/Tre ... SRG_V1.pdf). Not much current can flow, it is only like static electricity.

Using this situation as starting point, could this lead to an overload of U_GS or an unintended switching of other channels at the moment when the first MUX channel turns on?

Edit: One possible damage scenario is caused by the Miller capacity. It turns the FET on, even if the gate is grounded by resistor. We have 20pF Miller, and C_GS<600pF. Let's assume 400pF. This forms a voltage divider 20pF/400pF, which is factor 20. The threshold voltage of 1V is easily reached by applying a voltage jump on the drain of 20V. Having the ADC sitting around at +100V or -100V at the beginning, it is easy to get a jump of 100V U_DS, resulting in a gate voltage of 5V. Multiple FETS will start to draw current in this situation, until the 10k gets the gates discharged.

Also for this scenario, lowering the 10k and adding a parallel gate capacitance would help.

Edit 2: An other scenario is, that in such a situation the maximum allow U_GS of +/-20V is violated. Because the source can get a quickly rising voltage via the body diode of the other FET, and the gate is still sitting and hold by the C_CE of the optocoupler. If the C_CE is in the same order of magnitude as the C_GS (20pF) or even higher, the gate voltage is only following the source with delay.
Also for this case, an additional gate capacitance would help.
User avatar
johu
Site Admin
Posts: 6679
Joined: Thu Nov 08, 2018 10:52 pm
Location: Kassel/Germany
Has thanked: 361 times
Been thanked: 1519 times
Contact:

Re: Flying ADC 16 channel BMS 96S test

Post by johu »

In favour of this theory FETs always failed at power-up. Once the system is up and running I have never seen or heard of failures.

At power-up all FETs are open, this should be ensured by the pull-down resistors. So the ADC floats *somewhere*

I have witnessed the Miller effect on a simple H-bridge. The low-side FETs V_GS can be clamped to 0 with a strong driver and about 1 Ohm gate resistor. Still when you turn on the upper FET you get a spurious turn-on of the lower FET as well. It is only very short and doesn't do much damage because of the strong pull-down. But with 10k this is a different story (despite the lower Miller cap of the smaller FET)
Support R/D and forum on Patreon: https://patreon.com/openinverter - Subscribe on odysee: https://odysee.com/@openinverter:9
skr
Posts: 42
Joined: Wed Jun 01, 2022 7:11 pm
Has thanked: 8 times
Been thanked: 14 times

Re: Flying ADC 16 channel BMS 96S test

Post by skr »

so my 36S MEB modules have been in use for <1 week, so far no issues. Balancing is slow, I am helping the weakest cells with PSU CV set below before it starts tripping the hell out of ADC measurements on the adjacent cells and OCP <300mA.
The gate resistor has 8200pF 100V cap across it, extra 3R on cell taps through the polyfuse. Same i2cdelay=5 as other 2.3HW. Pretty thick coat of conformal coat. I have it completely shutting off with ignition at the moment. Cell taps go in pretty close proximity to the phase cables, which see <300A phase current.

Current bmsio code looks like this:

Code: Select all

oid BmsIO::ReadCellVoltages()
{
   const int totalBalanceCycles = 30;
   static uint16_t balanceCycles[18] = {0};
   static uint8_t chan = 0;
   static float sum = 0, min, max, avg;
   int balMode = Param::GetInt(Param::balmode);
   bool balance = Param::GetInt(Param::opmode) == BmsFsm::IDLE && Param::GetFloat(Param::uavg) > Param::GetFloat(Param::ubalance) && BAL_OFF != balMode;
   FlyingAdcBms::BalanceStatus bstt;

   if (balance)
   {
      if (balanceCycles[chan] == 0)
      {
         balanceCycles[chan] = totalBalanceCycles; //this leads to switching to next channel below
      }
      else
      {
         balanceCycles[chan]--;
      }

      if (balanceCycles[chan] > 0 && balanceCycles[chan] < (totalBalanceCycles - 1))
      {
         float udc = Param::GetFloat((Param::PARAM_NUM)(Param::u0 + chan));
         float balanceTarget = 0;

         switch (balMode)
         {
         case BAL_ADD: //maximum cell voltage is target when only adding
            balanceTarget = CorrectVoltage(Param::GetFloat(Param::umax));
            break;
         case BAL_DIS: //minimum cell voltage is target when only dissipating
            balanceTarget = CorrectVoltage(Param::GetFloat(Param::umin));
            break;
         case BAL_BOTH: //average cell voltage is target when dissipating and adding
            balanceTarget = CorrectVoltage(Param::GetFloat(Param::uavg));
            break;
         default: //not balancing
            break;
         }

         // Calculate adaptive thresholds based on voltage delta and minimum values
         float deltaV = CorrectVoltage(Param::GetFloat(Param::umax)) - CorrectVoltage(Param::GetFloat(Param::umin));
         float chargeThreshold = MAX(3.0f, deltaV * 0.02f);  // At least 3mV or 2% of delta
         float dischargeThreshold = MAX(1.0f, deltaV * 0.01f);  // At least 1mV or 1% of delta
         
         // Static variables to cache global values for slave nodes
         static float cachedUmin = 0.0f;
         static float cachedUmax = 0.0f;
         static float cachedUavg = 0.0f;
         static uint32_t lastCacheUpdateTime = 0;
         static uint32_t cacheCounter = 0;
         cacheCounter++;
         
         // Cache values with a timeout mechanism for slave nodes
         if (!bmsFsm->IsFirst()) {
             // Current cached values
             float currentUmin = CorrectVoltage(Param::GetFloat(Param::umin));
             float currentUmax = CorrectVoltage(Param::GetFloat(Param::umax));
             float currentUavg = CorrectVoltage(Param::GetFloat(Param::uavg));
             
             // Check if values changed significantly (received update from master)
             bool valuesChanged = (ABS(currentUmin - cachedUmin) > 1.0f) || 
                                 (ABS(currentUmax - cachedUmax) > 1.0f) || 
                                 (ABS(currentUavg - cachedUavg) > 1.0f);
             
             if (valuesChanged) {
                 // Update cached values
                 cachedUmin = currentUmin;
                 cachedUmax = currentUmax;
                 cachedUavg = currentUavg;
                 lastCacheUpdateTime = cacheCounter;
             }
             
             // Make balancing decisions more sensitive on slave nodes
             chargeThreshold = MAX(1.5f, deltaV * 0.01f);    // More aggressive (1.5mV minimum)
             dischargeThreshold = MAX(0.5f, deltaV * 0.005f); // More aggressive (0.5mV minimum)
             
             // If cache is stale (no updates for 200 cycles ~5 seconds), use even more aggressive thresholds
             if ((cacheCounter - lastCacheUpdateTime) > 200) {
                 // Use more aggressive thresholds if not receiving regular updates
                 chargeThreshold = MAX(1.0f, deltaV * 0.005f);     // Very aggressive
                 dischargeThreshold = MAX(0.3f, deltaV * 0.0025f); // Very aggressive
             }
             
             // Special case for significant outliers
             float significantDeviation = 50.0f; // mV
             if (ABS(udc - balanceTarget) > significantDeviation) {
                 // For big deviations, use even more aggressive thresholds
                 if (udc < balanceTarget)
                     chargeThreshold = 1.0f; // Extremely aggressive charge threshold for low cells
                 else
                     dischargeThreshold = 0.3f; // Extremely aggressive discharge threshold for high cells
             }
         }
                  
         if (udc < (balanceTarget - chargeThreshold) && (balMode & BAL_ADD))
         {
            bstt = FlyingAdcBms::SetBalancing(FlyingAdcBms::BAL_CHARGE);
         }
         else if (udc > (balanceTarget + dischargeThreshold) && (balMode & BAL_DIS))
         {
            bstt = FlyingAdcBms::SetBalancing(FlyingAdcBms::BAL_DISCHARGE);
         }
         else
         {
            bstt = FlyingAdcBms::SetBalancing(FlyingAdcBms::BAL_OFF);
            balanceCycles[chan] = 0;
         }
         Param::SetInt((Param::PARAM_NUM)(Param::u0cmd + chan), bstt);
      }
      else
      {
         FlyingAdcBms::SetBalancing(FlyingAdcBms::BAL_OFF);
      }
   }
   else
   {
      balanceCycles[chan] = totalBalanceCycles;
      bstt = FlyingAdcBms::SetBalancing(FlyingAdcBms::BAL_OFF);
      Param::SetInt((Param::PARAM_NUM)(Param::u0cmd + chan), bstt);
   }

   //Read cell voltage when balancing is turned off
   if (balanceCycles[chan] >= totalBalanceCycles)
   {
      float gain = Param::GetFloat(Param::gain);
      int numChan = Param::GetInt(Param::numchan);
      bool even = (chan & 1) == 0;

      if (chan == 0)
         gain *= 1 + Param::GetFloat(Param::correction0) / 1000000.0f;
      else if (chan == 1)
         gain *= 1 + Param::GetFloat(Param::correction1) / 1000000.0f;
      else if (chan == 15)
         gain *= 1 + Param::GetFloat(Param::correction15) / 1000000.0f;

      //Read ADC result before mux change
      float udc = FlyingAdcBms::GetResult() * (gain / 1000.0f);

      Param::SetFloat((Param::PARAM_NUM)(Param::u0 + chan), udc);

      min = MIN(min, udc);
      max = MAX(max, udc);
      sum += udc;

      //First we sweep across all even channels: 0, 2, 4,...
      if (even && (chan + 2) < numChan)
         chan += 2;
      //After reaching the furthest even channel (say 12) we either change over to a higher odd channel
      else if (even && (chan + 1) < numChan)
         chan++;
      //or lower odd channel
      else if (even)
         chan--;
      //Now we sweep across all odd channels until we reach 1
      else if (chan > 1)
         chan -= 2;
      //We have no reached chan 1. Accumulate values and restart at chan 0
      else
      {
         chan = 0;
         avg = sum / Param::GetInt(Param::numchan);
         Accumulate(sum, min, max, avg);

         min = 8000;
         max = 0;
         sum = 0;
      }

      FlyingAdcBms::SelectChannel(chan);
      FlyingAdcBms::StartAdc();
   }
}
Post Reply