
關注、星標公眾號,直達精彩內容
線性插值法(linear interpolation),是指使用連接兩個已知量的直線來確定在這兩個已知量之間的一個未知量的值的方法。
有好幾種插值方法,本文僅僅介紹一維線性插值和雙線性插值在BMS開發中的應用。
如下圖:

已知坐標 (x0, y0) 與 (x1, y1),要得到 [x0, x1] 區間內某一位置 x 在直線上的值。
從數學上來看,3點處于1條直線,斜率是相等的,于是有:

由于 x 值已知,所以可以從公式得到 y 的值:

公式太長不好記,可以進行簡化方便記憶,方然推導也沒問題....
由
,
令α = (y-y0)/(x-x0),同樣有,α = (y1 - y0)/(x1 - x0),上面的方程式就可以簡化為:
y = y0 + α(y1 ? y0)
這樣已知x的值,就可以輕松計算出y的值,同樣的,已知y的值,可以輕松求出x的值。
在數學上,雙線性插值是有兩個變量的插值函數的線性插值擴展,其核心思想是在兩個方向分別進行一次線性插值。
以下理論搬自網絡。
紅色的數據點與待插值得到的綠色點假如我們想得到未知函數 f 在點 P = (x, y) 的值,假設我們已知函數 f 在 Q11 = (x1, y1)、Q12 = (x1, y2), Q21 = (x2, y1) 以及 Q22 = (x2, y2) 四個點的值。
首先在 x 方向進行線性插值,得到:


然后在 y 方向進行線性插值,得到:

這樣就得到所要的結果 f(x, y):


電芯SOC和開路電壓是有一定關系的,也就是我們常聽說的OCV,OCV是Open circuit voltage(開路電壓),指的是電池不放電開路時,兩極之間的電位差.
但是因為電池的極化效應,想要測量準確的OCV得靜止2小時,假設我們通過設置放電電流來控制電池的SOC從100%-0%變化,間隔為1%,那么整個實驗做完至少需要200小時。
來看一組電池數據,一般電芯廠家提供的都是5%步進的SOC對應的電壓值,在兩個電壓點之間的SOC可以近似直線,當然這樣也是有誤差的。


那么如何利用一維線性差值計算不同電壓下對應的SOC值呢?
例如:計算紅框中的某一電壓對應的SOC值
根據一維線性差值的公式編寫代碼如下:
#include?
#include?
#define?SOC_FULL?(100)
#define?SOC_OCV_STEP?(SOC_FULL?/?20)
#define?CELLVOL_LEN?(21)
const?uint16_t?voltage[CELLVOL_LEN]?=?{2966,?3140,?3244,?3343,?3427,?3491,?3525,
???????????????????????????????????????3576,?3633,?3687,?3730,?3772,?3813,?3858,
???????????????????????????????????????3914,?3955,?4007,?4054,?4077,?4099,?4180};
/**
?*根據ocv曲線計算SOC
?*/
uint8_t?get_soc_by_cellocv(const?uint16_t?*cell_table,?uint16_t?cellvol)
{
??/**?y?=?y0?+?(x-x0)*(y1???y0)/(x1-x0)*/
??/**計算3343和3427電壓直接的電壓對應的SOC值,取3400電壓下的SOC*/
??uint16_t?soc;
??uint8_t?i;
??if?(cellvol?<=?cell_table[0])?//?0%
??{
????return?0;
??}
??else?if?(cellvol?>=?cell_table[SOC_FULL?/?SOC_OCV_STEP])?//?100%
??{
????return?SOC_FULL;
??}
??for?(i?=?0;?i???{
????if?(cellvol?>?cell_table[i])
????{
??????i++;
????}
????else
????{
??????break;
????}
??}
??/**y0?=?(i?-?1)?*?SOC_OCV_STEP*/
??/**(x-x0)?=?(cellvol?-?cell_table[i?-?1])?*/
??/**(y1???y0)?=?SOC_OCV_STEP,這里由于SOC是5%均勻步進,所以直接取了步進值作為y1-y0*/
??soc?=?(i?-?1)?*?SOC_OCV_STEP?+?SOC_OCV_STEP?*?(cellvol?-?cell_table[i?-?1])?/
?????????????????????????????????????(cell_table[i]?-?cell_table[i?-?1]);
??return?soc;
}
int?main(void)
{
??uint16_t?soc_ocv?=?0;
??soc_ocv?=?get_soc_by_cellocv(voltage,?3400);
??printf("\n\n----soc_ocv?=?%d---\n\n",?soc_ocv);
??return?0;
}
vscode環境下編譯看看結果,我們要計算的是3400mV時候對應的SOC為18%,計算結果是OK的,要注意限幅處理,0%和100%對應的點。


要計算在負載情況下的SOC,需要對電壓和電流做建模,獲得比較準確的SOC,當然這個SOC也只是盡可能準確一些,相比較OCV,電池工作過程中是不能直接使用OCV計算SOC的。
包括電池的充放電MAP,都是需要進行二維插值計算的,例如:

看一組數據,橫軸是電流,縱軸是電壓,中間數據為SOC值,接下來看看如何利用雙線性插值計算SOC,這里取得都是1%精度,沒有用浮點類型數據。

還是要回歸到第一章節介紹的公式,雙線性插值實際上是進行3次單線性插值,x軸進行2次插值計算,y軸進行1次插值計算。
就不再針對公式一一分析了,直接上代碼:
#include?
#include?
#include?
#include?
#define?SOC_FULL?(100)
#define?SOC_OCV_STEP?(SOC_FULL?/?20)
#define?CELLVOL_LEN?(21)
#define?CURRENT_LEN?7
#define?VOLTAGE_LEN?6
const?uint16_t?voltage[CELLVOL_LEN]?=?{2966,?3140,?3244,?3343,?3427,?3491,?3525,
???????????????????????????????????????3576,?3633,?3687,?3730,?3772,?3813,?3858,
???????????????????????????????????????3914,?3955,?4007,?4054,?4077,?4099,?4180};
static?const?int32_t?current_map[CURRENT_LEN]?=?{0,?50,?200,?500,
?????????????????????????????????????????????????1200,?2500,?2501};?//?橫軸
static?const?uint16_t?voltage_map[VOLTAGE_LEN]?=?{0,?2500,?3200,
??????????????????????????????????????????????????3380,?3500,?3501};?//?縱軸
static?const?int16_t?load_soc_map[CURRENT_LEN][VOLTAGE_LEN]?=?{
????{100,?100,?100,?100,?100,?100},
????{0,?0,?4,?15,?35,?100},
????{-2,?-2,?0,?8,?22,?100},
????{-4,?-4,?-1,?4,?15,?100},
????{-6,?-6,?-3,?2,?10,?100},
????{-6,?-6,?-3,?2,?10,?100},
????{-6,?-6,?-3,?2,?10,?100}};
/**
?*根據ocv曲線計算SOC
?*/
uint8_t?get_soc_by_cellocv(const?uint16_t?*cell_table,?uint16_t?cellvol)
{
??/**?y?=?y0?+?(x-x0)*(y1???y0)/(x1-x0)*/
??/**計算3343和3427電壓直接的電壓對應的SOC值,取3400電壓下的SOC*/
??uint16_t?soc;
??uint8_t?i;
??if?(cellvol?<=?cell_table[0])?//?0%
??{
????return?0;
??}
??else?if?(cellvol?>=?cell_table[SOC_FULL?/?SOC_OCV_STEP])?//?100%
??{
????return?SOC_FULL;
??}
??for?(i?=?0;?i???{
????if?(cellvol?>?cell_table[i])
????{
??????i++;
????}
????else
????{
??????break;
????}
??}
??/**y0?=?(i?-?1)?*?SOC_OCV_STEP*/
??/**(x-x0)?=?(cellvol?-?cell_table[i?-?1])?*/
??/**(y1???y0)?=?SOC_OCV_STEP,這里由于SOC是5%均勻步進,所以直接取了步進值作為y1-y0*/
??soc?=?(i?-?1)?*?SOC_OCV_STEP?+?SOC_OCV_STEP?*?(cellvol?-?cell_table[i?-?1])?/
?????????????????????????????????????(cell_table[i]?-?cell_table[i?-?1]);
??return?soc;
}
/*
?*負載SOC查表
?*/
static?int16_t?get_soc_by_load_map(const?int16_t?*table,?uint16_t?voltage,?int32_t?current)
{
??int16_t?result?=?0;
??int16_t?map_voltage_low?=?0,?map_voltage_high?=?0,?map_current_low?=?0,
??????????map_current_high?=?0;
??int16_t?map_high?=?0,?map_low?=?0;
??uint8_t?i?=?0;
??int16_t?vol_step?=?0;
??int16_t?vol_diff?=?0;
??int32_t?current_step?=?0;
??int32_t?current_diff?=?0;
??int16_t?soc_diff?=?0;
??int16_t?soc_step?=?0;
??for?(i?=?0;?i?//?循環電壓
??{
????if?(voltage?????{
??????if?(i?!=?0)
??????{
????????map_voltage_low?=?i?-?1;
????????map_voltage_high?=?i;
??????}
??????else
??????{
????????map_voltage_low?=?i;
????????map_voltage_high?=?i;
??????}
??????break;
????}
??}
??for?(i?=?0;?i?//?循環電流
??{
????if?(abs(current)?????{
??????if?(i?!=?0)
??????{
????????map_current_low?=?i?-?1;
????????map_current_high?=?i;
??????}
??????else
??????{
????????map_current_low?=?i;
????????map_current_high?=?i;
??????}
??????break;
????}
??}
??if?(((map_voltage_high?==?map_voltage_low)?||
???????(abs(current)?>?current_map[CURRENT_LEN?-?1])?||
???????(voltage?>?voltage_map[VOLTAGE_LEN?-?1])))
??{
????return?100;
??}
??vol_diff?=?voltage?-?voltage_map[map_voltage_low];
??vol_step?=?voltage_map[map_voltage_high]?-
?????????????voltage_map[map_voltage_low];
??soc_step?=?table[(CURRENT_LEN?-?1?-?map_current_low)?*?VOLTAGE_LEN?+
???????????????????map_voltage_high]?-
?????????????table[(CURRENT_LEN?-?1?-?map_current_low)?*?VOLTAGE_LEN?+
???????????????????map_voltage_low];
??map_low?=?(int16_t)(soc_step?*?vol_diff?/?vol_step?+
??????????????????????table[(CURRENT_LEN?-?1?-?map_current_low)?*?VOLTAGE_LEN?+?map_voltage_low]);
??vol_diff?=?voltage?-?voltage_map[map_voltage_low];
??vol_step?=?(voltage_map[map_voltage_high]?-
??????????????voltage_map[map_voltage_low]);
??soc_step?=?(table[(CURRENT_LEN?-?1?-?map_current_high)?*?VOLTAGE_LEN?+
????????????????????map_voltage_high]?-
??????????????table[(CURRENT_LEN?-?1?-?map_current_high)?*?VOLTAGE_LEN?+
????????????????????map_voltage_low]);
??map_high?=?(int16_t)(vol_diff?*?soc_step?/?vol_step?+
???????????????????????table[(CURRENT_LEN?-?1?-?map_current_high)?*?VOLTAGE_LEN?+
?????????????????????????????map_voltage_low]);
??result?=
??????(int16_t)((abs(current)?-?current_map[map_current_low])?*
????????????????????(map_high?-?map_low)?/?(current_map[map_current_high]?-?current_map[map_current_low])?+
????????????????map_low);
??return?result;
}
int?main(void)
{
??int16_t?soc_ocv?=?0;
??uint16_t?cell_vol?=?3200;
??int32_t?current?=?0;
??while?(1)
??{
????current?+=?100;
????soc_ocv?=?get_soc_by_cellocv(voltage,?3400);
????printf("----soc_ocv?=?%d----\n",?soc_ocv);
????soc_ocv?=?get_soc_by_load_map((const?int16_t?*)load_soc_map,?cell_vol,?current);
????printf("!!!!current?=?%d,cell_vol?=?%d,soc_load?=?%d!!!!\n\n",?current,?cell_vol,?soc_ocv);
????if?(current?>?2600)
??????return?0;
????Sleep(1000);
??}
??return?0;
}
看下運行結果,驗證也是OK的,這個代碼寫的略微shi,大家可以自己優化優化,可以把一維線性函數抽出來封裝,這樣單線性和雙線性可以復用函數,代碼更簡潔一些。

歡迎關注公眾號“小飛哥玩嵌入式”,交流更多嵌入式相關的開發知識,目前從事BMS行業,也可以交流BMS開發相關的內容哈