Skip to content

Commit

Permalink
BMS Coulomb Counting SOC (#988)
Browse files Browse the repository at this point in the history
### Summary
<!-- Quick summary of changes, optional -->
Track state-of-charge using coulomb counting on BMS. No current limiting
implemented, solely statistical tracking.

SOC is saved to EEPROM on 1s intervals, utilizing a basic wear-leveling
algorithm
- Address of up-to-date soc stored in page 0 of EEPROM, this is only
written once per power cycle, no need to wear-level
- SOC value stored in page 1-127, written once per second. Active SOC
page written to will increment once per power cycle.
- SOC value read is checked for corruption:
   - When SOC value saved to EEPROM, 4 copies saved to page
- When SOC value read, 4 values are checked to see if they match and are
within range
- If not, read SOC from Ocv to SOC lookup table (inaccurate but better
than corrupted value)

SOC can be reset in one of two ways, both controlled by debug CAN
messages
- Update from Ocv-Soc LUT
- Update with manually specified SOC value

### Changelist 
<!-- Give a list of the changes covered in this PR. This will help both
you and the reviewer keep this PR within scope. -->
- Added SOC tracking logic
- Added EEPROM reading/writing
- Added simple test for SOC

### Testing Done
<!-- Outline the testing that was done to demonstrate the changes are
solid. This could be unit tests, integration tests, testing on the car,
etc. Include relevant code snippets, screenshots, etc as needed. -->
- Test checking for accuracy added to TestStateMachine
- LUT lookup tested in TestStateMachine (will be moved during refactor)
- Tested on car:
- SOC can be reset using both custom value and via OCV lookup from CAN
debug message
- Test value saved and recovered between power cycles from EEPROM
- Checked that SOC tracks linearly with current draw (tested with full
charge cycle)

### Resolved Issues
<!-- Link any issues that this PR resolved like so: `Resolves #1, #2,
and #5` (Note: Using this format, Github will automatically close the
issue(s) when this PR is merged in). -->

### Checklist
*Please change `[ ]` to `[x]` when you are ready.*
- [x] I have read and followed the code conventions detailed in
[README.md](../README.md) (*This will save time for both you and the
reviewer!*).
- [x] If this pull request is longer then **500** lines, I have provided
*explicit* justification in the summary above explaining why I *cannot*
break this up into multiple pull requests (*Small PR's are faster and
less painful for everyone involved!*).

---------

Co-authored-by: will-chaba <[email protected]>
  • Loading branch information
will-chaba and will-chaba authored Nov 17, 2023
1 parent b377f77 commit aa40d1f
Show file tree
Hide file tree
Showing 19 changed files with 632 additions and 295 deletions.
3 changes: 2 additions & 1 deletion can_bus/json/BMS/BMS_rx.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"DIM_Vitals",
"Debug_Charging",
"Debug_CanMode",
"Debug_CellBalancing"
"Debug_CellBalancing",
"Debug_ResetSoc"
]
}
9 changes: 9 additions & 0 deletions can_bus/json/BMS/BMS_tx.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@
},
"State": {
"enum": "BmsState"
},
"Soc": {
"resolution": 0.1,
"min": 0,
"max": 100,
"unit": "%"
},
"SocCorrupt": {
"bits": 1
}
}
},
Expand Down
19 changes: 19 additions & 0 deletions can_bus/json/Debug/Debug_tx.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,24 @@
"unit": "%"
}
}
},
"ResetSoc": {
"msg_id": 704,
"cycle_time": 100,
"description": "Value set by user to set SOC based on min cell voltage.",
"signals": {
"ResetSoc_MinCellV": {
"bits": 1
},
"ResetSoc_CustomEnable": {
"bits": 1
},
"ResetSoc_CustomVal": {
"resolution": 0.1,
"min": 0,
"max": 100,
"unit": "%"
}
}
}
}
5 changes: 3 additions & 2 deletions firmware/shared/src/app/App_SharedProcessing.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
* @param f_x1 The current value of f(x) used to approximate the value of the current trapezoid
* @param d_x The uniform delta-x of each trapezoid
*/
static inline void App_SharedProcessing_TrapezoidalRule(float *integral, float *f_prev, float f_curr, float d_x)
static inline void App_SharedProcessing_TrapezoidalRule(double *integral, float *f_prev, float f_curr, float d_x)
{
// Calculate the trapezoid and add to the previous integral
*integral = *integral + d_x * (*f_prev + f_curr) * 0.5f;
// Double is used here as the extra precision is needed when adding a small delta to a large integral amount
*integral = *integral + (double)(d_x * (*f_prev + f_curr) * 0.5f);
*f_prev = f_curr;
}

Expand Down
4 changes: 2 additions & 2 deletions firmware/shared/test/Test_TrapezoidalRule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ extern "C"

TEST(VoltageSenseTest, tractive_system_voltage_calculation)
{
float integral = 0.0f;
float prev_fx = 0.0f;
double integral = 0.0f;
float prev_fx = 0.0f;

// A vector between 0 and 2pi, in pi/4 increments
std::vector<float> x_vec{ 0.0, M_PI * 0.25, M_PI * 0.5, M_PI * 0.75, M_PI,
Expand Down
9 changes: 9 additions & 0 deletions firmware/thruna/BMS/Inc/App/App_BmsWorld.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include "App_TractiveSystem.h"
#include "App_SharedClock.h"
#include "App_Eeprom.h"
#include "App_Soc.h"

struct BmsWorld;

Expand All @@ -35,6 +36,7 @@ struct BmsWorld *App_BmsWorld_Create(
struct OkStatus * imd_ok,
struct OkStatus * bspd_ok,
struct Accumulator * accumulator,
struct SocStats *const soc_stats,
struct Airs * airs,
struct PrechargeRelay * precharge_relay,
struct TractiveSystem * tractive_system,
Expand Down Expand Up @@ -103,6 +105,13 @@ struct OkStatus *App_BmsWorld_GetBspdOkStatus(const struct BmsWorld *world);
*/
struct Accumulator *App_BmsWorld_GetAccumulator(const struct BmsWorld *world);

/**
* Get the soc_stats for the given world
* @param world The world to get the accumulator for
* @return The accumulator for the given world
*/
struct SocStats *App_BmsWorld_GetSocStats(const struct BmsWorld *const world);

/**
* Get the AIRs for the given world
* @param world The world to get the AIRs for
Expand Down
37 changes: 27 additions & 10 deletions firmware/thruna/BMS/Inc/App/App_Eeprom.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
#include <stdbool.h>
#include <stdlib.h>
#include <assert.h>
#include "App_SharedExitCode.h"
#include "App_EepromExitCode.h"

#define PAGE_SIZE ((uint16_t)16U) // in Bytes
#define DEFAULT_SOC_ADDR 1U

struct Eeprom;

Expand Down Expand Up @@ -63,21 +65,36 @@ EEPROM_StatusTypeDef
EEPROM_StatusTypeDef App_Eeprom_PageErase(struct Eeprom *eeprom, uint16_t page);

/**
* Write an address 4 times to EEPROM
* Update address and write 4 times to EEPROM
* @note SHOULD ONLY BE CALLED ONCE EVERY 5ms, DOING SO MORE QUICKLY WILL VIOLATE EEPROM SPECS
* @param eeprom Eeprom to write address to
* @param page the number of the page to write to
* @param address the address to store on the EEPROM
* @param address pointer to the the address to store on the EEPROM
* @return EEPROM_StatusTypeDef returns success status for debug
*/
EEPROM_StatusTypeDef App_Eeprom_Write4CopiesOfAddress(struct Eeprom *eeprom, uint16_t page, uint16_t address);
EEPROM_StatusTypeDef App_Eeprom_UpdateSavedSocAddress(struct Eeprom *eeprom, uint16_t *address);

/**
* Read the stored 4 copies of adress from EEPROM
* Read stored copies of last SOC address and perform error checking
* @param eeprom Eeprom to erase a page from
* @param page the number of the page to read from
* @param addresses array of 4 addresses to be chosen from. Should all contain same value but may not due to
* data-corruption in EEPROM
* @return EEPROM_StatusTypeDef returns success status for debug
* @param address address where stored SOC values can be found
* @return ExitCode returns EXIT_CODE_ERROR in case of data corruption or read error
*/
ExitCode App_Eeprom_ReadSocAddress(struct Eeprom *eeprom, uint16_t *address);

/**
* Write SOC value of cell with lowest voltage to EEPROM (writes 4 identical copies to protect against data corruption)
* @param eeprom EEPROM to write SOC to
* @param min_soc value of current min_soc to write
* @param address page on EEPROM to store soc
* @return EEPROM_StatusTypeDef returns status of write operation
*/
EEPROM_StatusTypeDef App_Eeprom_WriteMinSoc(struct Eeprom *eeprom, float min_soc, uint16_t address);

/**
* Read previous SOC value of cell with lowest voltage to EEPROM and perform error checking
* @param eeprom
* @param address
* @param min_soc
* @return ExitCode returns EXIT_CODE_ERROR in case of data corruption or read error
*/
EEPROM_StatusTypeDef App_Eeprom_Read4CopiesOfAddress(struct Eeprom *eeprom, uint16_t page, uint16_t *addresses);
ExitCode App_Eeprom_ReadMinSoc(struct Eeprom *eeprom, uint16_t address, float *min_soc);
139 changes: 105 additions & 34 deletions firmware/thruna/BMS/Inc/App/App_Soc.h
Original file line number Diff line number Diff line change
@@ -1,39 +1,110 @@
#pragma once

#include "App_SharedExitCode.h"
#include "App_Accumulator.h"
#include "App_Timer.h"
#include "App_Eeprom.h"

#define STATE_OF_HEALTH (0.94f)
#define SERIES_ELEMENT_FULL_CHARGE_C (5.9f * 3600.0f * 3.0f * STATE_OF_HEALTH)

struct SocStats
{
// Address in EEPROM where SOC is being stored (address changes for wear levelling reasons, address always stored in
// address 0 of EEPROM)
uint16_t soc_address;

// charge in cell in coulombs
double charge_c;

// Charge loss at time t-1
float prev_current_A;

// Indicates if SOC from EEPROM was corrupt at startup
bool is_corrupt;

TimerChannel soc_timer;
};

/**
* Function to retrieve soc closest to given open circuit voltage
* @param voltage open circuit voltage
* @return soc related to open circuit voltage
*/
float App_Soc_GetSocFromOcv(float voltage);

/**
* Function to retrieve open circuit voltage closest to given soc
* @param soc_percent state of charge in percent
* @return open circuit voltage closest to given state of charge
*/
float App_Soc_GetOcvFromSoc(float soc_percent);

/**
* Create the SocStats object
* @param eeprom Eeprom to read stored SOC value from
* @param accumulator Accumulator to get min cell voltage from
* @return struct SocStats pointer to object
*/
struct SocStats *App_SocStats_Create(struct Eeprom *eeprom, struct Accumulator *accumulator);

/**
* Destroy the SocStats object
*/
void App_SocStats_Destroy(struct SocStats *soc_stats);

/**
* Return the active address on the EEPROM where SOC values are being stored
* @param soc_stats soc_stats object to retrieve address from
* @return page being used to store SOC values
*/
uint16_t App_SocStats_GetSocAddress(struct SocStats *soc_stats);

/**
* Return if the SOC value was corrupt on startup
* @param soc_stats soc_stats object to retrieve corrupt status from
* @return corrupt status
*/
bool App_SocStats_GetCorrupt(struct SocStats *soc_stats);

/**
* Update the state of charge of all series elements using coulomb counting.
* @param soc_stats The charge stats of the pack
* @param current The current from the cell to be updated (- is out of the cell, + is into the cell)
*/
void App_SocStats_UpdateSocStats(struct SocStats *soc_stats, float current);

/**
* return Coulombs of the SE with the lowest SOC
* @param soc_stats The charge stats of the pack
* @return cloulombs of lowest SOC SE
*/
float App_SocStats_GetMinSocCoulombs(struct SocStats *soc_stats);

/**
* Given three state-of-charge (SoCs), check whether the absolute difference
* between any two SoCs is less than or equal to the specified maximum absolute
* difference. If any two SoCs pass the check, return the average value of those
* two SoCs.
* @param max_abs_difference The maximum allowable absolute difference between
* any two given SoCs, must be between 0 and 100
* inclusive
* @param soc_1 The first SoC, must be between 0 and 100 inclusive
* @param soc_2 The second SoC, must be between 0 and 100 inclusive
* @param soc_3 The third SoC, must be between 0 and 100 inclusive
* @param result This will be set to the average value of the two SoCs whose
* difference is less than or equal to max_difference. Multiple
* SoC pairs may have their difference less than or equal to
* max_difference. In this scenario, we prioritize as follows:
*
* Highest Priority: soc_1 / soc_2
* soc_2 / soc_3
* Lowest Priority: soc_1 / soc_3
*
* If no two suitable SoCs can be found, this will be set to NaN.
* @note An example of a tiebreaker:
*
* max_difference = 5
* soc_1 = 0
* soc_2 = 50
* soc_3 = 100
*
* In this scenario, the difference between soc_1 and soc_2 is the same as
* the difference between soc_2 and soc_3, but we prioritize soc_1 / soc_2
* over soc_2 / soc_3. Thus, result is set to (soc_1 + soc_2) / 2 = 25.
* @return EXIT_CODE_INVALID_ARGS if max_difference, soc_1, soc_2, or soc_3 is
* not between 0 and 100 inclusive
*/
ExitCode App_Soc_Vote(float max_abs_difference, float soc_1, float soc_2, float soc_3, float *result);
* return percent SOC of the SE with the lowest SOC
* @param soc_stats The charge stats of the pack
* @return soc % of lowest SOC SE
*/
float App_SocStats_GetMinSocPercent(struct SocStats *soc_stats);

/**
* Get the minimum series element open circuit voltage approximation given current pack SOC status
* @param soc_stats current SOC of each series element in pack
* @return float minimum series element open circuit voltage approximation
*/
float App_Soc_GetMinOcvFromSoc(struct SocStats *soc_stats);

/**
* Reset SOC value based on current minimum cell voltage
* @param soc_stats object to write SOC stats to
* @param accumulator accumulator cell information
*/
void App_Soc_ResetSocFromVoltage(struct SocStats *soc_stats, struct Accumulator *accumulator);

/**
* Reset SOC value with custom input
* @param soc_stats object to write SOC stats to
* @param soc_percent desired SOC in percent
*/
void App_Soc_ResetSocCustomValue(struct SocStats *soc_stats, float soc_percent);
Loading

0 comments on commit aa40d1f

Please sign in to comment.