系统综合设计 智能电池小车 Final Report

作者:赖海斌 覃仁杰 陈沛安

摘要

在本次SDIM101课程中,我们小组制作了一辆碳纤维锌电池寻轨小车。小车整体结构采用拱形构造,设计上分为多层,上层将压力点化为面,中层电池采用三并两串的接线为小车提供动力,下层主板驱动小车巡线。小组采用预浸料+真空袋的方式制作小车车身,底板上装配电池组,控制上采用PID算法进行巡线。在制作中,我们采用仿真+实验的方式改善小车在巡线与三点弯测试中的效果,并最终在巡线测试中10分钟内完成27圈,在三点弯测试中失效压力约为1700N,挠度约为7.2mm,取得了良好的巡线性能与抗压效果。本次设计让我们对电池制作、材料设计、结构制作与系统设计有了更全面的了解。

关键词:系统设计;三点弯测试;巡线测试

小车结构

设计思路

小车整体为类龟壳结构,采用拱形构造,目标为承受更大压力,并将上部压力点化为面,分担至全车身。

在设计上,下方放置小车控制主板,巡线传感器,电机及驱动轮。中间层放置结构电池,使用复合材料封装。上层设计为类龟壳结构,主要负责承压及分担压力。

小车整体参数

长:280mm。宽:200mm。高:116mm。质量:900g。

小车主要结构

主要结构包括车壳,底板,车架以及六块软包电池。

车壳:龟壳造型,与底板用螺丝连接。

底板(碳纤板):六块软包电池(三并两串),底板与车架之间用螺丝连接。

车架(3D打印):电机(车架的四个角),传感器(车架前方),电路板(车架中间)。

小车需求分析

功能需求:小车可进行简单的循线;小车可承受三点弯的测试,有一定的抗压能力。

设计考量

力学性能方面,通过乌龟壳的拱形结构实现更大的承重,通过3D打印的抗压配件,将点压力化为面压力,防止刺穿。

电学性能方面,对于电池,进行了电学性能测试。就电池的放电曲线,电池组的系统仿真进行了探究,决定实行三并两串的组合。代码方面:针对节能和控制两个核心思想进行代码优化,尝试使用贝叶斯优化来改进代码中的超参数,从而使小车在相同的电量内走更长的距离。

小车测试

巡线测试

对装有电动机的小车进行路径追踪能力测试。在标记有特殊路径的赛道上进行,小车通过其内置的自动驾驶系统来跟随这些轨迹。同时要求运行十分钟,对于电池与电机的稳定输出能力提出要求。

三点弯测试

将小车底座两端车轴固定在测试机器下方两点处,上方液压机对准PLA的加强片。设定仪器压深为10mm,随后开始测试。获得车身的载荷-位移曲线。

车身制作

制作流程

在车身的制作工艺上,我们采用预浸料+真空袋的方式制作小车的车身。其具体流程是:

第一步,调配树脂。使用创园一栋402室提供的树脂,以树脂:固化剂为100:30的比例(质量比)混合,搅拌均匀。

第二步,树脂脱泡。将树脂置于真空环境中15分钟,去除树脂内的气泡,取出备用。

第三步,制作预浸料。裁切合适大小的碳纤维布料四份,均匀涂抹树脂,确保树脂完全浸润碳纤维布。

第四步,铺脱模布。将脱模布铺设在模具内部。

第五步,铺设碳纤维。将碳纤维布料铺在脱模布上,尽可能让碳纤维与模具内部贴合。

第六步,铺设脱模布和填充物。在碳纤维上再覆盖一层脱模布,放入大量柔软的填充物,确保下一步抽真空时模具内部的碳纤维受压均匀。

第七步,抽真空。将模具放入真空袋内密封,用气泵抽真空,确认真空袋未漏气,压力维持在某个定值,静置。

第八步,脱模。等待4-6小时树脂大致固化后,将车身从模具中脱出。若树脂未固化,则放入烤箱内保持80℃,直至树脂完全固化。

车身制作流程图

工艺示意图

抽真空

具体实践

在实际制作中,我们按照流程对车身进行制作。

模具难点

因为只使用一个模具,车身的表面并未能做到光滑,而且表面的树脂因为承受的压力分布不均匀和脱模布的褶皱而厚度不均,最严重的区域碳纤直接裸露在外,未被树脂保护。同时在车身的侧壁,碳纤维因填充物在抽真空过程中的压缩形变而产生褶皱。褶皱不仅让车身的美观性受到破坏,同时也让车身在承受水平载荷时应力分布不沿竖直中轴面对称。

正面效果图,红色圈中碳纤维裸露

车身侧壁褶皱

电池板方案

电池板我们则采用了成品碳纤维板+玻璃纤维布的方案,使用雕刻机切割了一块长280mm,宽200mm,厚1mm的碳纤维板,在碳纤维板表面覆盖一层用以绝缘的玻璃纤维,然后放置电池组,再铺设玻璃纤维布,制成一块含有电池的平板。

两块电池板

三点弯测试准备

使用solidworks的simulation插件对小车车身进行模拟,查看其承压和变形情况。

我们发现。在小车直接受压的情况下,小车上方中心的形变相比其他地方变化较大,且形变的范围小。说明我们的压力并没有较好地分担到小车的全身各结构上,在三点弯实验中,这有很大概率发生局部穿刺。

为了防止车身在三点弯实验中发生局部穿刺,我们在车身的顶部平面区域加装了一块PLA的加强片,使用AB胶将加强片与车身连接。该加强片可以较好地分担承受压力,将压力分担到更广的车身上,使得车身的受力面积增加,车身所受的压强减少,进而降低车身的形变程度。在仿真中我们可以看到,小车在加强片的作用下得以承受更大的压力与形变。

安装加强片

在最终的测试中,我们对小车进行了三点弯测试。在测试中,小车的失效压力约为1700N,形变幅度为7.2mm,小车的材料制作优良,结构设计良好,使得最后的抗压能力表现较好。

电池测试

在制作小车电池组的同时,我们小组想明确电池的大致性能,从而对车辆的行驶、运行状态有一个更好的评估。对此,小组进行了充放电测试与数据分析和建模。电池测试的全部代码放在github:

HaibinLai/Zn_Battery_SOC_Analysis (github.com)

电池充放电测试

我们对电池进行了充放电测试,将数据采集后,尝试对电池进行数学建模。

电池的充放电测试包含恒流-恒压充电、恒流放电模式,我们在测试中记录该过程的测试时间、电压和电流等数据,通过分析该过程中数据的变化,查看电池的的容量、充放电平台以及电池内部参数变化等电化学性能参数。

将提供的电池接入电池充放电测试系统。测试系统采用新威neware CT/CTE-4000电芯测试设备,上位机控制软件采用BTS 8.0。接入系统后,打开上位机软件,设置对应的测试格。充放电恒定电流值设置为50mA/cm-2 ,随后从充电开始,进行测试,得到下图所示的电池电压-比容量曲线。

测试数据导入

在进行了3-4次测试后,将数据导入至计算机进行处理,具体流程如下。

1.使用Python等程序将ndax数据转化为csv格式。

import NewareNDA

df = NewareNDA.read('Data.ndax')
print(df)

# save df into csv:
import numpy as np

# Open the file in append mode
with open("output.csv", "a") as csv_file:
    # Create a NumPy array with the new data
    new_data = np.array(df)
    # Append the data to the file
    np.savetxt(csv_file, new_data, delimiter=",", fmt="%s")

2.将csv数据汇入MATLAB数据分析软件。

% 读取表格数据
tableData = readtable('postgres_public_battsingle.csv');

% 将第一列数据转换为向量
voltage = tableData(:, 1).Variables;

% 将第二列数据转换为向量
charge = tableData(:, 2).Variables;
voltage_flipped_manual = voltage(end:-1:1);

% 生成一个8530行的向量,每一行的数值都是0.01
in_res = ones(8439, 1) * 0.01;

n = 8439
v_T = linspace(276, 293, n);
v_T = v_T'

A = linspace(1,8439,n);
A = A';

R_up = linspace(0.01,2,n);
R_up = R_up'

3.使用MATLAB Data Cleaner软件包检查导入的数据是否有残缺或断片。随后输出电池数据,即可为测试数据进行分析。

单块电池放电数学建模与SOC估计

给定一个输入电压,我们能知道目前电池所在的具体状态吗?

在得到一块电池放电数据后,我们随后试图对电池的放电情况进行建模。由于我们在测试过程中电流保持恒定,我们主要针对电池输出电压(即开路电压OCV)对电池的健康状态(SOH)、电池荷电状态(SOP)进行建模与估计。对单块电池的建模有助于我们后续对电池放电情况有更好的理解,在小车行驶及用电控制上有更好的系统设计及优化。

基于最小二乘法的多项式拟合建模

我们首先尝试检验电池的放电曲线是否具有一定的规律,从而可以针对电压来做电池电压预测。

使用MATLAB Curve Fitter 工具箱,将电池电压数据导入,使用多项式拟合选项进行拟合,并在Robust选项上选择off(即采用传统的最小二乘方法进行拟合)。

上图为拟合曲线与原始曲线,下方为残差曲线

对应的拟合曲线的评估参数如下表所示。可以看到,多项式拟合的效果表现较好,虽然对数据整体没有很强的解释性,但是可以根据该曲线在小范围内对电池的充放电进行分析。

模型评估方法 解释 数值大小
SSE 预测值与原始值对应点的误差的平方和 0.4299
R-square 相关性 0.9967
RMSE 平方根误差,反应了预测值与真值的误差 0.0071

有趣的是,我们注意到拟合后的残差图Residual plot,可以发现,拟合曲线与原曲线的残差是一个上下波动的类正弦函数。也就是说,这个拟合曲线的残差展现了一定的规律性。然而,随机性(randomness)和不可预测性(unpredictability)是任何回归模型的关键组成部分,在误差(error)中不应该含有任何可解释、可预测的信息。因此,电池的放电曲线应具有一定的规律。

使用组合优化方法拟合基于理论模型的电池放电曲线

如果我们通过物理公式,能够知道电池曲线的物理建模,那么,我们不就能对着求出的曲线,预测我们电池的状态了吗?

针对放电曲线的规律,查阅相关文献,发现电池的放电模型可以使用基尔霍夫定律描绘。在文献[1]中,经定律求解后的电路状态方程见下图。

我们可以发现,模型的SOC变化率为:

模型的极化电压(即在放电时电池两端的电压)变化率为:

在电池管理系统控制电池输出电流不变的情况下,电流i保持不变,则极化电压Vp的变化率将与自身有关。在文献[2]中,其认为极化电压应该以常微分方程(ODE)的形式呈现。但是,极化电压仍与极化电阻、极化电容有关,那么,他们在电池反应过程中的改变量是否会影响到整个方程?我们在这里进行进一步探究。

我们尝试构造一个类似的一阶常微分方程来拟合我们的电池数据曲线。由于在构造方法中没有直接可以拟合常微分方程的方法,我们使用组合优化的思路对方程进行拟合,下图为拟合流程。

针对特定的方程dv/dt = C - kv,我们给定初始值C、k,随后使用计算机描绘出该初始模拟曲线。随后进行特征点采样,将对应的模拟数据点与电池实验所得的数据点做差,并求和,得到拟合的残差函数。如果该残差函数数值越小,所拟合的微分方程便与原数据点越接近,拟合的效果就越好。因此,我们在这之中使用非线性优化器进行优化,直到输出一个拟合的比较完善的方程。

在实践中,我们参考文献[3]进行程序设计。不断优化得到最接近的ode微分方程曲线。

voltage_data = voltage
voltage_data = voltage(500:5500);

k1 = 8.169934640522876e-5

% 微分方程模型
ode_model = @(t, v, C ) C - k1 * v;

% 时间序列
tdata = 1:length(voltage_data);

% 残差函数
residuals = @(params)(abs( voltage_data - ode45(@(t, v) ode_model(t, v, params(1) ), tdata, voltage_data(1)).y));

% 初始猜测值
initial_guess = [0];  % 假设的初始值
lb = [0];  % 下界
ub = [0.011];  % 上界

% 使用 lsqnonlin 进行优化
options = optimoptions('lsqnonlin', 'Display', 'iter');
opt_params = lsqnonlin(residuals, initial_guess, lb, ub, options);

% 输出优化结果
A_opt = opt_params(1);
%k_opt = opt_params(2);
disp(['优化后的参数 A_opt = ', num2str(A_opt)]);
%disp(['优化后的参数 k_opt = ', num2str(k_opt)]);

% 获得了优化后的参数
k_opt = k1

% 定义微分方程模型和时间序列
ode_model = @(t, v, C) C - k1 * v;
tdata = 1:length(voltage_data);

% 使用最优参数求解微分方程模型,得到预测的电压数据
[t, predicted_voltage] = ode45(@(t, v) ode_model(t, v, A_opt), tdata, voltage_data(1));

随后,我们进行数据可视化。

% 绘制原始数据、优化模型预测和它们的对比图像
figure;
plot(tdata, voltage_data, 'b-', 'LineWidth', 2);  % 原始数据
hold on;
plot(t, predicted_voltage, 'r--', 'LineWidth', 2);  % 预测数据
hold off;
xlabel('Step');
ylabel('OCV');
title('原始数据与优化模型预测');
legend('原始数据', 'ode预测');
grid on;

然而我们发现,在不断的拟合下,预测的方程仍与原始数据有较大差距。这可能说明在电池的放电变化过程中,极化电容及内阻发生了一定的改变,使得其放电电压不再符合常微分方程的形式。

基于机器学习决策树回归算法进行电池数据学习与预测

在使用模型预测时,在给定一个输入电压情况下,我们对电池所在的具体状态能了解多少,这和机器学习的方法很近似。因此,在这里,我们尝试使用机器学习的方法进行学习和预测是否会比直接用模型的方法预测效果更优。结合文献[4],我们使用MATLAB回归分析库进行电池数据的学习与预测,具体流程如下。

  1. 将1段电池的放电曲线的数据导入到库中,准备好数据。其中,这段函数的输入为电池电压,输出为我们的Record number,即记录时的index数。

  1. 使用Fine Tree 模型进行训练。在训练开始前,使用PCA主成分分析选项,对我们的数据进行降维。
  2. 开始训练,并得到最后的训练效果图。下图中黄色部分即为训练的模型对电池电压曲线变化进行的预测。可以看到模型预测的效果较好,RMSE仅为0.0012241。

查看其残差图,可以发现其样点分布没有明显的曲线趋势,所有的预测样本点残差都落在正负40的区间内。在实际测试中,单周期我们测试了8000秒,也就是说,凭借机器学习的方法,我们可以将电池的SOC预测控制在两分钟左右的误差范围内。

不过,在预测的过程中,我们只是希望查看大致的电池数据范围,因此这样的误差相对而言也还算不错。但是,如果是在实际的小车放电情况下,电流的数值会比目前的50mA大,有可能此时的情况会和预测的不一致。如果是前面理论模型的方法,其可以很好的解决,但是基于数据的机器学习难以应对。这是我们这一方法的缺点

本文方法与文献[4]的不同

文献[4]针对一系列NASA公开电池数据集,根据电池的电压、温度等数据,来估测电池的SOC状态。在机器学习方法上,由于数据量较多,他们使用了高斯过程回归GPR,支持向量回归SVR,神经网络NN及决策树算法。

从实验数据上,作者发现高斯过程回归的效果是最好的,RMSE取到了最小值。不过我们也进行了对应的尝试,发现GPR效果确实比决策树做的好,但是训练的时间远长于决策树回归。

文献[4]中使用高斯回归、神经网络、线性回归、支持向量回归、决策树算法对电池数据进行学习

上图中的三份图表分别对应电池放电曲线,拟合曲线,及残差图

基于决策树、支持向量机、高斯回归的数据训练测试

我们发现,在实验室中测试得到的电池数据使用高斯过程回归进行拟合,在残差图上的效果表现和文中的不一致。文中的效果基本呈现随机分布,但是我们的训练效果显示,数据在7000左右残差增大。我们推测这可能是由于电池电压开始出现拐点,开始迅速下降导致的。而使用决策树算法,虽然其误差更大,但是其残差分布更均匀。推测造成这一现象的主要原因是我们的数据量较小,没有更多的数据,这使得决策树在小规模数据上表现更好,而高斯回归出现了类似过拟合的现象。

决策树简介

决策树(Decision Tree),它是一种以树形数据结构来展示决策规则和分类结果的模型,作为一种归纳学习算法,其重点是将看似无序、杂乱的已知数据,通过某种技术手段将它们转化成可以预测未知数据的树状模型,每一条从根结点(对最终分类结果贡献最大的属性)到叶子结点(最终分类结果)的路径都代表一条决策的规则

三种电池放电曲线的建模方法总结

结合上述三种方法,我们最后简单对他们进行总结。

建模方法 拟合/预测效果 RMSE 可解释性
多项式拟合 较好 62.3906 从残差中可看到变化趋势
基于微分方程理论模型 一般 - 可以反应极化内阻及电容的信息
基于机器学习决策树 很好 19.641 难以从决策过程中挖掘信息

难以从决策过程中挖掘信息

基于机器学习高斯回归 1.4175

多项式拟合可以比较好的描绘电压曲线,拟合效果较好。同时其残差可以看出该曲线的变化趋势,具有一定的可解释性。基于理论模型的建模方法利用基尔霍夫定律,将电压曲线的变化规律用各物理参数进行描绘,虽然其最终拟合效果一般,但是我们可以从拟合的过程中感受到极化内阻、电容对电池电压的影响,其含有的信息量较多。而基于机器学习的方法其决策过程由学习原理决定,其决策树决策的过程为黑盒过程,因而我们难以从过程中捕捉更多的信息。

基于电池放电曲线的电池组建模

我们针对单块电池建模完成后,针对我们的小车电池,我们进行电池组的建模与仿真。

我们将电池数据在MATLAB中建模完后,使用simscape搭建电池组,随后在MATLAB simlink中进行电池系统测试。

电池测试仿真系统简介

将单组电池导入后,我们按照在车身上装配6块电池的目标,如下图制作了电池组的仿真模型。

图中,6块电池按照3并2串的拓扑结构结合,右侧连接电压表与SOC观测表,用于电池的实验观测。整体电路接入一个电路元器件(图左侧),其“-3”表示其恒定消耗3A电流。因此,整个电池组输出为3A,我们可以观测其输出的持续时间,电压大小等性能。

电池系统建模与测试:电流-持续时间测试

给测试元器件输入不同的电流值,我们可以查看我们电池的具体持续时间。可以看到,在恒定电流3A及以下时,电池组可持续输出10分钟以上。在实际的测试中,我们发现电机的输出电流基本在1-2A之间,在10分钟限定的测试时间内,6块基本充满的电池可以满足巡线需要。

恒定电流(A) 持续时间(s)
1 1836.37
2 918.42
3 612.80
3.5 524.70
4 459.04
4.5 408.01

我们也可以观察仿真得出的电压下降曲线,如下图电流为3A时,电压在前400秒内基本保持稳定,在之后进入下降状态,直到612秒时电池电荷量降为0。这一特性可以帮助我们对电机的输出有更好的把握。

电池组网络结构拓扑探究

在选定6块电池作为我们最终小车上搭载的电池数后,我们发现电池组有两种接线方式:先串后并(A型),先并后串(B型)。那么,这两种接线方式对我们的电量会有什么影响吗?我们对此进行测试。

在第一组实验中,我们将所有电池设置电荷量为170mAh满电状态,随后查看他们的持续时间。结果显示,所有的电池持续时间是一样的。

但是我们设计了第二个实验,在这之中,我们将6块电池中的一块的电荷量改为120mAh,即不满电状态,随后比较两者的持续时间。此时,先并后串的电池连接方式相比先串后并持续了更长的时间。

从原理上分析,这一现象应该是在先并后串中,较少电量的电池在没电后,剩下的电池仍然可以持续放电,对没电的电池进行补偿,从而延续了电池的放电时间。

从实验中,我们最后得出结论,与先串后并相比,先并后串的电池接线方法具有一定的补偿效应与鲁棒性。

同时在后面的实际接线中,我们也发现,先并后串的方法复杂度更简单,电池充放电更方便。因此,电池组应采用先并后串更佳。

小车程序设计

在小车巡线的设计上,我们使用arduino nano MCU作为控制芯片,操控小车的4台电机进行移动。小车电机同侧同速,通过改变两侧电机的差速,实现小车的转弯、直走。小车接入5路巡线传感器电路,通过传感器对轨道的探测,可以实现小车的寻轨操作。

小车控制的源代码可以在github上找到:

HaibinLai/PID-for-Arduino-Car: PID Line Following Algorithm for Arduino-based Car (github.com)

整体设计思路

在设计时,考虑到电池在高电压时,给电机输出的功率更大,小车的速度更高,我们尽可能地尝试将芯片的耗能降低。同时,我们根据之前的电池测试所得出的电压变化特点,将电机的速度改为随电压变化。最后,针对巡线控制,我们对针对传感器,使用PID控制算法,并进行相应优化。

高性能程序设计

查阅arduino官方提供的arduino nano Data sheet A000005-datasheet.pdf (arduino.cc),可以发现芯片最高耗电可达800mA,为4.7C,节能的重要性不言而喻。

在CPU内,我们设计了这么几条节能指令:

  1. CPU降频

原有的arduino芯片时钟频率为16MHz,其速度过高,超过我们的需求标准,因此我们选择降频至8MHz。一份测试表明,单卡内降频为原速的一半可节约大约三分之一的电流。

单卡内降频测试(仅独立芯片)

  1. 减少不必要的存储:

  2. int->short

MCU使用的芯片为ATMEGA328P,作为一个8位微控制器,其操作均围绕8位进行。因此,我们将部分的存储变量从int类型改为short类型,使得芯片在运算时不用花费更长的指令判断或使用auipc指令进行移位计算。

  1. SRAM -> EEPROM

EEPROM(Electrically Erasable Programmable read-only Memory), 是一种电可擦除可编程只读存储器,并且其内容在掉电的时候也不会丢失。

EEPROM允许按字节进行读写操作,可以灵活地修改和读取单个字节的数据,而不需要对整个块进行擦除和重写。虽然其写入成本较高,但是在写入完毕后,其访问的能耗基本较低,在维持数据上所用功耗也低。

与静态ROM存储芯片相比,SRAM存储芯片中的每一位均存储在四个晶体管当中,这四个晶体管组成了两个交叉耦合反向器。这个存储单元具有两个稳定状态,通常表示为0和1。另外还需要两个访问晶体管用于控制读或写操作过程中存储单元的访问。因此,一个存储位通常需要六个MOSFET。这使得SRAM芯片在访问上的功耗比EEPROM高很多。

因此,我们将一些写入后就不变的常量改为const static,其编译器将会将其写入至ROM中。

经编译后,动态变量仅使用54字节,大部分程序及常量存储至程序存储空间。

  1. 减少不必要的耗能指令,如乘除法

查阅ATMEGA328P芯片设计手册中的指令集部分,发现芯片没有提供乘法以及除法,其仅有算数加减及逻辑运算。说明在计算乘法时,编译器是使用程序解决乘除法问题,同时该计算需要频繁访问SRAM内存,这对于功耗消耗是非常大的。

因此在程序中,我们在传感器读入、电机速度处理部分尽可能地减少了该类高耗能程序。

在主板方面,我们主要关闭了未使用的引脚,主板电压检测等耗能设备:

  1. 关闭数模转换、I2C等高耗能不必要引脚

  1. 关闭电量不足检测(该检测仅判断输入电压是否高于2.2V,在本次测试中没有必要)

基于电池电压变化特点的小车速度控制

在电池组仿真时,我们注意到随着时间推移,电池组电压会有一段迅速下降期。为了确保此时电机仍有足够动力,同时电机速度仍可驱动小车转弯,我们使用一个计时器函数控制小车速度,使得小车在电压下降的情况下依旧可以向前保持速度行驶。

我们在原本提供的小车模型上进行了对应测试。小车在加入该段程序后可以继续稳定运行。

基于PID与卡尔曼滤波的巡线程序设计

小车的控制系统使用PID控制算法,针对小车的速度进行控制(速度环)。

小车速度控制逻辑

小车首先接受传感器参数,并就情况赋予其状态值。如果小车在中间,其状态值为0。小车在轨迹偏左处,状态值为负数,在右侧,状态值为正数。偏离程度越大,状态值的绝对值越大。

随后,根据PID算法计算出要调控的数值。

最后,更新小车的速度。其中,使用PID计算速度偏移量。巡线速度为设置常数,跟小车动力相关。

在对模型小车进行测试时,我们发现,模型小车质量较轻,在转弯时非常容易出现PID调节值过大,使得小车在短时间回正时,小车向反方向回正幅度过大,小车很容易因此出轨。

我们认为这是PID调节中比例参数使得调节曲线波动较大导致的。为了改进,我们增加了卡尔曼滤波,使得我们的调节曲线更加平滑。下图为PID调节演示图。

下图为PID与卡尔曼滤波结合后的调节演示图。

我们在计算PID_value时加入滤波

随后我们发现,模型小车在走直线上更加的平滑了。

然而,当我们接下来测试结构车身小车时,我们发现,在结构车身的加持下,小车的质量大幅增加,这使得小车的惯性增加。此时,我们需要更强的控制,而在多次测试中我们发现,速度环PID及卡尔曼滤波在低配重,高灵敏度的系统下表现较好。但是在增加配重后,PID控制的电机速度变化不再非常明显。主要原因可能在于,由于小车的质量增加,惯性变大,导致小车在转弯时改变方向较为困难。

因此我们得出结论,卡尔曼滤波在对我们实际的车身控制效果反而不如单纯的PID算法。我们在最终方案里选择不再使用卡尔曼滤波。不过,基于新车身,我们的PID控制参数仍需要优化,我们要快速找到新的参数。

基于贝叶斯优化的PID参数调优

在PID设计好后,我们发现,PID算法对比例、积分、微分的调节强度有较高的需求,如果参数调节偏差较大,最终的算法将很难指引小车实现巡线。对此,我们尝试使用机器学习中针对超参数调优的贝叶斯优化方法对P、I、D三者的比例系数进行调优。

贝叶斯优化简介

贝叶斯优化 (Bayesian Optimization)由 J Snoek et.在 NIPS 2012 [1206.2944] Practical Bayesian Optimization of Machine Learning Algorithms (arxiv.org) 中提出,并随后多次进行改进 。它要求已经存在几个样本点,并且通过高斯过程回归(假设超参数间符合联合高斯分布)计算前面 n 个点的后验概率分布,得到每一个超参数在每一个取值点的期望均值和方差,其中均值代表这个点最终的期望效果,均值越大表示模型最终指标越大,方差表示这个点的效果不确定性,方差越大表示这个点不确定是否可能取得最大值非常值得去探索。

如果随机过程的有限维分布均为正态分布,则称此随机过程为高斯过程或正态过程。在贝叶斯优化算法中,我们通常假设 假设我们需要估计的模型f(x)服从高斯过程。贝叶斯优化在不知道目标函数(黑箱函数)长什么样子的情况下,通过猜测黑箱函数长什么样,来求一个可接受的最大值。

在高斯过程下,优化器会根据已知的几个测试点,使用高斯过程(GP)尝试估计最大值(EI)出现的地方。随后,优化器将选择该位置作为下一个测试点,看看数值是否较大。接着,以该点和之前的测试点结合,使用贝叶斯公式,计算出下一个最大值出现位置的后验概率。在逐步迭代后,优化器便可以逐渐找到这一函数的变化趋势或最大值。

贝叶斯优化PID程序设计

贝叶斯优化适用于低维度(小于20维)的参数优化,正好符合我们PID调参的需求。我们参考文献[5],进而设计出我们的调参程序路径图(下图左侧流程图):

在实际程序设计中,我们使用了两张WiFi芯片将电脑与arduino芯片连接。在小车每跑完一圈后,小车记录外侧传感器检测到轨道的计数,代表小车偏移的程度。如果计数越小,说明小车的轨道偏移程度越小,小车的控制效果越好。小车将其发送到电脑端,电脑端程序运行贝叶斯优化,将新的PID参数发送回小车。小车在更新后,继续运行新的一轮更新,再跑一圈,记录参数,更新。

下方为电脑端贝叶斯优化程序代码:

import asyncio
import threading
import serial
from bayes_opt import BayesianOptimization
from bayes_opt.util import UtilityFunction

try:
    import json
    import tornado.ioloop
    import tornado.httpserver
    from tornado.web import RequestHandler
    import requests
except ImportError:
    raise ImportError(
        "In order to run this example you must have the libraries: " +
        "`tornado` and `requests` installed."
    )

# hyper parameter
Number_of_iter = 30

# for bayesian
Kappa = 3
Xi = 1

serial_port = 'COM4'

ser = serial.Serial(serial_port, 9600, timeout=1)  # 根据实际串口号和波特率设置

def black_box_function(P, I, D):
    """Function with unknown internals we wish to maximize.

    This is just serving as an example, however, for all intents and
    purposes think of the internals of this function, i.e.: the process
    which generates its outputs values, as unknown.
    """

    I = I/100
    P = P/20

    pid_bytes = bytes([P, I, D])
    try:
        # Send PID bytes to serial port
        ser.write(pid_bytes)
        print(f"Sent PID values: P={P}, I={I}, D={D} to serial port {serial_port}")
    except serial.SerialException as e:
        print(f"Error writing to serial port: {e}")

    answer = 0

    try:
        while True:
            if ser.in_waiting > 0:
                data = ser.read(ser.in_waiting)
                print("接收到的数据:", data.decode('utf-8'))  # 解码为字符串并打印
                answer = data.decode('utf-8')
    except serial.SerialException as e:
        print("串口读取错误:", e)

    return answer

class BayesianOptimizationHandler(RequestHandler):
    """Basic functionality for NLP handlers."""
    PID_para = {"D": (5,8), "I": (0, 25), "P": (0, 20)}
    _bo = BayesianOptimization(
        f=black_box_function,
        pbounds=PID_para
    )
    _uf = UtilityFunction(kind="ucb", kappa=Kappa, xi=Xi)

    def post(self):
        """Deal with incoming requests."""
        body = tornado.escape.json_decode(self.request.body)

        try:
            self._bo.register(
                params=body["params"],
                target=body["target"],
            )
            print("BO has registered: {} points.".format(len(self._bo.space)), end="\n")
        except KeyError:
            pass
        finally:
            suggested_params = self._bo.suggest(self._uf)

        self.write(json.dumps(suggested_params))

def run_optimization_app():
    asyncio.set_event_loop(asyncio.new_event_loop())
    handlers = [
        (r"/bayesian_optimization", BayesianOptimizationHandler),
    ]
    server = tornado.httpserver.HTTPServer(
        tornado.web.Application(handlers)
    )
    server.listen(9009)
    tornado.ioloop.IOLoop.instance().start()

def run_optimizer():
    name = "PID Optimizer"
    # colour = Fore.GREEN

    register_data = {}
    max_target = None
    for _ in range(Number_of_iter):
        status = name + " wants to register: {}.\n".format(register_data)

        resp = requests.post(
            url="http://localhost:9009/bayesian_optimization",
            json=register_data,
        ).json()
        target = black_box_function(**resp)

        register_data = {
            "params": resp,
            "target": target,
        }

        if max_target is None or target > max_target:
            max_target = target

        status += name + " got {} as target.\n".format(target)
        status += name + " will to register next: {}.\n".format(register_data)
        print(status, end="\n")

    global results
    results.append((name, max_target))
    print(name + " is done!", end="\n\n")

if __name__ == "__main__":

    print("welcome to bayesian_optimization on PID Control")

    ser.write(b'1')  # 发送字符 '1',需要转换为字节类型

    try:
        while True:
            if ser.in_waiting > 0:
                data = ser.read(ser.in_waiting)
                print("小车匹配成功!")  # 解码为字符串并打印
                answer = data.decode('utf-8')
    except serial.SerialException as e:
        print("串口读取错误:", e)

    ioloop = tornado.ioloop.IOLoop.instance()
    optimizers_config = [
        {"name": "PID Optimizer"},
    ]

    app_thread = threading.Thread(target=run_optimization_app)
    app_thread.daemon = True
    app_thread.start()

    targets = (
        run_optimizer,
    )
    optimizer_threads = []
    for target in targets:
        optimizer_threads.append(threading.Thread(target=target))
        optimizer_threads[-1].daemon = True
        optimizer_threads[-1].start()

    results = []
    for optimizer_thread in optimizer_threads:
        optimizer_thread.join()

    for result in results:
        print(result[0], "found a maximum value of: {}".format(result[1]))

    ioloop.stop()

    ser.close()

按照之前的经验,我们将KP的搜索空间定义在5到40,KI的搜索空间定义在0-0.5,KP搜索空间定义在0-2。

最后我们看到贝叶斯优化的路径图:

Kp Ki Kd
28.21348409 0.5815 1.439
28.94666667 0.35 0.572
27.94333333 0.589916667 1.381
32.79333333 0.305 1.44
31.65 0.2335 1.3628
29.58666667 0.182166667 0.4925
33.14 0.300916667 1.334
32.62 0.315741667 1.442
12.57333333 0.314333333 1.51
32.24666667 0.347666667 1.54
33.33333333 0.380666667 1.52
32.04666667 0.358541667 1.026
33.33333333 0.342483333 1.53
31.33 0.342 1.50
32.32333333 0.331166667 1.50

可以看到,优化系统不断迭代寻找最佳参数。我们在现场也进行肉眼观察,检测小车的效果。

在现场观察小车时,我们发现,贝叶斯优化在几轮优化内可以迅速找到能巡线的p,i,d参数。但是,对于精细化的参数,贝叶斯优化仍旧不够灵敏。相比之下,贝叶斯优化更适合在面对全新的车身时,快速找到可以巡线的参数,之后再由人手动调节参数进行测试,或在小范围内使用grid search。因此,在参数调优方面,手动搜寻与经验仍不可缺少。

在最终实践中,我们的PID参数选择如下:

KP KI KD
33 0.30 1.2

速度参数选择如下:

巡线速度 最低速度 最高速度
130 240 20

本方法与文献5的不同

文献5仅使用高斯过程对PID进行了调参。我们在这以基础上使用贝叶斯优化,提高调参的准确性与连续性。

巡线测试

在实际巡线中,我们使用了基于贝叶斯优化后的PID参数。

小车整体行驶稳定,运行总圈数为27圈。在运行中,没有出现脱轨、打滑、动力不足等现象。

在运行过程中,小车的整体输出功率可以看到在缓慢减少,而基于此设计的调速函数使得小车全程保持稳定。

同时在转弯过程中,我们也看到由于小车整体的功率限制,小车出现了在一侧轮子全速后,另一侧轮子由于输出功率不足而动力不足的情况,但是我们的降速函数很好地起到了调节作用,使得小车最中得以平稳地通过曲线。

在转弯过程中,5路传感器的传感基本保持在内路控制,外侧的传感器很少响应。在贝叶斯优化的效果下,小车的运行状态稳定,达到了预想的效果。

不过,小车由于整体质量在900g左右,这使得小车的驱动力需求大,小车的整体运行速度较低。另外,由于小车的速度上限受转向上的影响,小车在直线上的行驶速度没有达到最大值,在后续,该程序可以进一步的提高。

三点弯曲测试

我们对小车进行三点弯测试。将小车底座两端车轴固定在测试机器下方两点处,上方液压机对准PLA的加强片。设定仪器压深为10mm,随后开始测试。

在传感器传回的数据图上,我们可以看到我们车身的载荷-位移曲线,其在开始接触并产生形变的阶段基本为一条直线,此时小车车身正发生弹性形变。随着压缩深度增加,小车结构内发生不可逆转的形变,小车无法进一步承受载荷,其曲线斜率迅速下降。(由于我们增加了塑料板,小车没有发生明显的刺穿情况)

读取曲线发生拐点的数值,其为我们结构材料所能承受的最大的弹性载荷。

可以从上图中看到,在测试中,小车的失效压力约为1700N,挠度约为7.2mm,承压为0.1684 MPa。小车的平均刚度为236.11N/mm。

我们使用MATLAB对实验所得的形变图像进行求导,使用数值微分的方法来求出数据图像的导数,即切线模量(在弹性极限内,应力与应变的比值):

% 示例数据
x = Untitled.VarName1; % 形变数据
y = Untitled.VarName2; % 压力数据

% 计算导数
dy_dx = gradient(y, x);  % 使用 gradient 函数计算导数

% 过滤导数数据,将大于 5000 的值设置为 NaN
threshold = 5000;
dy_dx(dy_dx > threshold) = NaN;
dy_dx(dy_dx < -threshold) = NaN;

% 绘制原始数据
figure;
subplot(2, 1, 1);        % 创建 2x1 网格的第一个子图
plot(x, y, '-b', 'LineWidth', 1.5);
title('Original Data');
xlabel('x');
ylabel('y');
grid on;

% 绘制导数
subplot(2, 1, 2);        % 创建 2x1 网格的第二个子图
plot(x, dy_dx, '-r', 'LineWidth', 1.5);
title('Derivative of Data');
xlabel('x');
ylabel('dy/dx');
grid on;

得到我们形变曲线的瞬时变化率随形变深度的图像,即小车的形变弹性模量随形变深度的变化曲线(下图红色线)。

可以看的,在测试机器压入约x=0.9mm后,液压机开始接触小车结构,弹性模量快速上升,在x=1.37mm左右达到峰值359N/mm,随后弹性模量逐步稳定下降,在x=7.8mm处下降到107N/mm,对应承压约为1620N,随后弹性模量出现断裂式下滑,而承压曲线来到拐点,随后曲线以另一个速度向上增长至1681N,深度来到x=8.98mm,随后开始下滑。因此我们认为,小车在x=7.8mm处达到了其结构的弹性限度。

不过,在测试完毕后,小车上方开始回弹。说明小车对压力的抵抗有一定的回复性。

综上,小车的失效压力约为1620N,最大挠度约为6.9mm。小车的材料制作优良,结构设计良好,使得最后的抗压能力表现较好。

附:液压机检测的各项数据:

Ef s0.2 sfY efY
MPa MPa MPa %
0.168420637 0.03759403
sfM efM h b A0
MPa % mm mm mm^2
35.03386688 10.09050679 12 40 480

感想 & 团队分工

覃仁杰(CAD建模,碳纤制作):101课程能与不同专业的同学合作,让我能更清晰地向他人传递自己的想法,同时对项目的风险管控的重要性有了深刻的认识(小车的质量控制,尺寸控制,项目的进度管理,工艺技术成本)。在此之外,学习了碳纤维产品的生产工艺,模具设计和手工制作。

赖海斌(电池测试,程序设计):101课程让我在电池设计及制作上开阔了眼界,提高了动手能力。第一次了解电池制作,电池研究,我发现了这里边很多的乐趣与挑战。在团队合作与制作中,我学习到了在自己专业外的动手能力,接触到了全新的实验设备。我对项目的系统设计,深入探究有了更全面的认识,让我对系统设计有了更好的概念。

陈沛安(碳纤制作,车身组装):101课程让我了解到了设计和制作的相关内容,学会了电池(软包电池,结构电池,纽扣电池)的制作,复合材料的制作,solidworks的使用,3D打印,SOP的书写以及团队的协作。这是一次难得且收获巨大的体验,转行到工科无疑拓宽了我的视野,加深了对系统设计的理解,锻炼了我的动手能力,结识到了不同专业的朋友,体会到了不一样的精彩。对于复合材料的制作,电池的制备,一些工程软件的使用也都有了初步的了解。

参考文献

[1]施宝昌,沈爱弟.并联锂离子电池组的模型化与电流分配 计算机测量与控制[J].,2017,25(10):189-193.

[2]R.Drummond, L. D. Couto and D. Zhang, "Resolving Kirchhoff’s Laws for Parallel Li-Ion Battery Pack State-Estimators," in IEEE Transactions on Control Systems Technology, vol. 30, no. 5, pp. 2220-2227, Sept. 2022, doi: 10.1109/TCST.2021.3134451.

[3] 使用优化变量拟合ODE参数 MATLAB & Simulink - MathWorks 中国

[4]S.Gupta and P. K. Mishra, "Machine Learning based SoC Estimation for Li-Ion Battery," 2023 5th International Conference on Energy, Power and Environment: Towards Flexible Green Energy Technologies (ICEPE), Shillong, India, 2023, pp. 1-6, doi: 10.1109/ICEPE57949.2023.10201546.

[5]Lester Lik Teck Chan, Tao Chen, Junghui Chen, PID based nonlinear processes control model uncertainty improvement by using Gaussian process model, Journal of Process Control,Volume 42,2016,Pages 77-89,ISSN 0959-1524, https://doi.org/10.1016/j.jprocont.2016.03.006.

EOF