基于无迹卡尔曼滤波的锂电池SOC估计Matlab实现详解

📅 发布时间:2026/10/14 10:31:36
基于无迹卡尔曼滤波的锂电池SOC估计Matlab实现详解
锂电池的剩余电量SOC不是一个能直接测出来的物理量。你拿着电压表看开路电压负载一上去电压就被极化拉偏了你做过安时积分电流传感器的零点漂移用久了简直能让人怀疑人生。这也是为什么我在多个电池管理系统相关项目里最终都绕回到卡尔曼滤波这一类状态估计方法上。而在这条技术路线里无迹卡尔曼滤波UKF是我自己最推荐优先落地的版本。它不是最炫的算法也不是计算量最小的算法但它对非线性系统的适应能力、对初值的容忍度以及实现时的可调试性组合起来非常顺手。这篇文章就把我写的一个Matlab版UKF电池SOC估计脚本掰开揉碎讲清楚从电池建模到滤波器方程再到代码每一段的作用一次性讲完。1. 项目概述为什么选UKF做SOC估计1.1 SOC估计的方法谱系聊SOC估计之前先看一圈市面上的方法。最简单粗暴的是开路电压法把电池静置一两个小时然后查OCV-SOC曲线。这个方法离线很准但电动车或便携设备不可能天天静置动态工况下基本失效。再往上一步是安时积分法把电流对时间积分乘上库伦效率再从初始SOC里减去。这个方法轻量、在线但致命弱点是初始SOC一旦有误差这个误差永远不会消失而且电流传感器偏置会随时间累积漂移。很多人以为安时积分法“累积误差”是电流积分造成的其实真正麻烦的是那个永远不知道在哪里的初始值。为了把初始值和噪声一块儿解决就要上状态观测器。最早被引入电池SOC估计的是扩展卡尔曼滤波EKF核心思路是在每个采样点对非线性方程做一阶泰勒展开用雅可比矩阵近似线性化。EKF实现简单、计算量小但它有个先天毛病当OCV-SOC曲线在中低SOC区间弯曲比较厉害时一阶线性化截断误差明显增大滤波器容易表现出“看着收敛了但协方差已经失真”的假象。随后出现的无迹卡尔曼滤波UKF则绕过线性化直接用一组确定的Sigma点穿过非线性函数再统计出均值和协方差。它不需要算雅可比也不展开到高阶精度通常能到二阶以上实现难度又不比EKF高多少。1.2 无迹卡尔曼滤波在电池场景的核心优势我选择UKF而不是EKF原因其实很朴素。电池模型的非线性主要来自OCV-SOC映射曲线这条曲线在SOC接近0和接近1时非常陡中段又相对平缓简单线性化很难兼顾全区间。而无迹变换的本质是用一组加权点去拟合状态分布经过非线性变换后的真实均值和协方差相当于用“样本统计”代替“函数展开”在曲线拐弯剧烈的地方也不容易跑偏。另一个实际原因是EKF需要你手推雅可比矩阵模型稍微改一下就得重新算一遍导数。比如把一阶RC模型扩展成二阶RC或者加入温度修正EKF的雅可比推导成本成倍增长而UKF只需要改一下状态转移函数和观测函数滤波器主体代码完全不用动。对做工程落地的人来说这个维护成本差异非常实在。况且现在MCU算力早就不是瓶颈UKF只多了几个Sigma点传播计算量完全可以接受。1.3 这篇脚本帮你解决什么问题我这次整理的Matlab脚本目标非常明确用一阶RC等效电路模型做被控对象用UKF在线估计SOC代码带全量注释让你能直接改参数、换模型、接自己的电池数据。看完之后你能得到一个可以直接跑通的SOC估计框架并且理解每个矩阵、每个参数是怎么来的。你不需要先把扩展卡尔曼滤波完全搞懂才能看这篇只要会基本的矩阵运算和Matlab语法跟着把代码搭起来就能看到SOC估计曲线从初始误差快速收敛到真值的过程。2. 电池建模先给滤波器一个可靠的“心脏”2.1 一阶RC等效电路的数学描述滤波器的本质是基于模型做状态估计模型建不好后面算法再花哨也白搭。工程上最常用的折中是戴维南Thevenin一阶RC模型把电池表达成一个理想电压源表示开路电压OCV(SOC)串联一个欧姆内阻R0表示瞬态压降再并联一个R1-C1网络描述极化效应。模型只有三个参数但足以捕捉电池在中低频段的动态响应。状态向量我取x [SOC; Vp]其中Vp是极化电压。负载电流I作为输入u端电压V_t作为观测输出z。离散化后的状态方程可以写成SOC(k1) SOC(k) - (η·Ts/Q)·I(k)Vp(k1) exp(-Ts/τ)·Vp(k) R1·(1 - exp(-Ts/τ))·I(k)这里的Q是电池总电量换算成库仑当量就是额定容量Ah×3600η是库伦效率Ts是采样周期τR1·C1是极化时间常数。观测方程则是V_t(k) OCV(SOC(k)) - R0·I(k) - Vp(k)这组方程写起来简单但背后的物理含义很清楚SOC是慢变量Vp是快变量两者通过电流输入耦合在一起。UKF要做的就是在一堆带噪声的电压电流测量里把这两个状态估计出来。2.2 OCV-SOC曲线的工程处理SOC和OCV之间的映射是电池模型里最核心的非线性环节。理想做法是用HPPC混合脉冲功率特性实验测出一系列不同SOC下的开路电压然后离线拟合。工程上我见过三种处理方式多项式拟合、查表插值和分段函数。多项式拟合连续性好、求导方便但在SOC两端容易出现过冲出现“电压不随SOC单调变化”这种荒谬结果查表插值则非常稳健数据点足够密时精度也够所以我更推荐在代码里用插值表。我脚本里给的OCV-SOC查找表是这个样子soc_pts 0:0.1:1; ocv_pts [2.80, 3.20, 3.50, 3.65, 3.72, 3.78, 3.84, 3.90, 3.99, 4.10, 4.20]; OCV (s) interp1(soc_pts, ocv_pts, min(max(s, 0), 1), pchip);这里用了pchip插值避免线性插值在节点处转折过于突兀。你拿到自己电池的实测数据后只需要把这两行替换成你的SOC点阵和对应的OCV数组UKF主体代码完全不用动。这个解耦设计是我刻意保留的因为实际项目中换电池型号太常见了。2.3 模型参数辨识与噪声假设R0、R1、C1这三个参数严格说应该通过HPPC实验辨识。我提供一组典型三元锂电芯的参数R0约28毫欧、R1约42毫欧、C1约2000法左右。需要提醒的是电池内阻受温度和SOC双重影响热态和冷态可以差出百分之几十。如果你的项目覆盖宽温度范围一定要在模型里加入温度修正项或者至少在不同温度下分别辨识参数组运行时查表切换。滤波器设计里还要交代清楚过程噪声和测量噪声。过程噪声Q描述的是模型不确定性包括参数偏差、未建模动态、离散化误差等测量噪声R描述的是传感器噪声。很多新手把Q和R当玄学参数其实它们的相对大小决定了滤波器对测量和模型的信任程度。Q越大滤波器越相信测量R越大滤波器越相信模型。具体数值我先给一组合理的初值后面调试时会详细讲怎么调。3. 无迹卡尔曼滤波核心原理别被“无迹”吓到3.1 无迹变换的直觉理解无迹变换的核心思想是“用确定的采样点去逼近概率分布”。假设状态x服从均值为x̄、协方差为P的高斯分布我们要计算它经过非线性函数g(x)后的分布。EKF的做法是把g展开成线性函数而UKF则选出一组Sigma点让这些点的样本均值和协方差恰好等于x̄和P然后把每个点单独通过g传播再统计传播后点的均值和协方差。这就像估算一堆人走完迷宫后的平均位置。EKF是只看入口和出口附近的地图假设路径是直线UKF则是派出一小队代表让他们真正走一遍迷宫再统计他们的位置。后者显然对强非线性更稳健。Sigma点的生成公式如下设状态维度为nλ α²·(n κ) - n第0个Sigma点就是状态均值x̄。其余2n个点沿协方差矩阵的Cholesky分解方向展开χ_i x̄ (√((nλ)P))ii1,...,n χ{in} x̄ - (√((nλ)P))_ii1,...,n其中(√((nλ)P))_i表示矩阵平方根的列向量。这组点加权求和后能精确还原x̄和P的一阶、二阶矩。3.2 Sigma点的权值计算权值分均值权Wm和协方差权Wc两类公式如下Wm_0 λ/(nλ) Wc_0 λ/(nλ) (1 - α² β) Wm_i Wc_i 1/[2(nλ)]i1,...,2n这里的α是决定Sigma点散布范围的比例因子通常取1e-3到1之间的较小值β用于融入状态分布先验信息高斯分布下取2是最佳κ是备用尺度因子一般取0或3-n。这些参数看起来有点唬人但实际调参时范围很窄取α1e-3、β2、κ0基本能覆盖大多数场景。3.3 UKF滤波流程拆解UKF的一步滤波循环可以分成六个动作和普通卡尔曼滤波的预测-更新结构一脉相承。第一步基于上一时刻的状态均值x_{k-1}和协方差P_{k-1}生成2n1个Sigma点。第二步把每个Sigma点代入状态转移函数f得到传播后的Sigma点加权求和得到预测状态均值x_pred再算出预测协方差P_pred并加上过程噪声Q。第三步把预测Sigma点代入观测函数h得到预测测量值z_pred同时计算测量协方差S和状态与测量的交叉协方差Pxz测量噪声R加在S上。第四步计算卡尔曼增益K Pxz·S^(-1)。第五步用实际测量值z_meas与预测测量值z_pred的差值修正状态得到x_post。第六步更新协方差P_post P_pred - K·S·K。整个流程没有求导、没有线性化近似所有的非线性都交给Sigma点传播去消化。这也是为什么我说UKF是“只要会矩阵乘法就能实现”的滤波算法。4. Matlab脚本实现与核心代码详解4.1 脚本的总体结构设计为了让代码清晰易读我把整个脚本拆成四个模块参数初始化、UKF核心函数、仿真数据生成、主循环与绘图。参数初始化负责设置电池模型参数、噪声协方差和OCV查找表UKF核心函数做成独立子函数输入上一时刻状态与协方差、当前输入与量测、模型函数句柄以及滤波参数输出更新后的状态与协方差。这样设计的好处是你想把UKF用到其他领域只要把函数抽走改两个函数句柄就能复用。函数句柄的使用是Matlab里很容易被忽略但非常优雅的功能。f_func和h_func把模型方程和滤波器剥离开主循环里不用关心模型细节换模型只换句柄指向的函数。这一点在项目迭代中能省大量时间。4.2 模型参数与状态方程初始化主脚本的参数部分我写得很直白% 电池额定参数 Cn 2.85; % 额定容量Ah Ts 1; % 采样周期s Qc Cn * 3600; % 库仑当量As % 一阶RC模型参数HPPC离线辨识得到 R0 0.028; % 欧姆内阻Ohm R1 0.042; % 极化电阻Ohm C1 1980; % 极化电容F tau1 R1 * C1; alpha1 exp(-Ts / tau1); % 状态转移矩阵和输入矩阵 A_state [1, 0; 0, alpha1]; B_input [-Ts / Qc; R1 * (1 - alpha1)]; f_func (x, u) A_state * x B_input * u; h_func (x, u) OCV(x(1)) - R0 * u - x(2);有人会问为什么状态转移矩阵第一行是[1, 0]而不是别的形式因为SOC的离散化方程是SOC(k1)SOC(k)-η·Ts/Q·I(k)它只与自身和输入有关与Vp无关所以第一行第二列是0。Vp那行则受Vp和I共同影响所以a22是alpha1b2是R1(1-alpha1)。这些矩阵元素不是随手写的每个都对应离散化公式。4.3 UKF核心函数Sigma点生成与权值计算这是整个脚本的灵魂。我把它写成独立的function文件ukf_step.mfunction [x_upd, P_upd] ukf_step(x_prev, P_prev, u_meas, z_meas, ... Q_proc, R_meas, f_func, h_func, ... alpha, beta, kappa) % ukf_step - 单步无迹卡尔曼滤波 % 输入: x_prev 上一时刻状态向量[n,1] % P_prev 上一时刻协方差矩阵[n,n] % u_meas 当前输入(电流) % z_meas 当前量测(端电压) % Q_proc 过程噪声协方差[n,n] % R_meas 量测噪声协方差[1,1] % f_func 状态转移函数句柄 (x,u) - x_next % h_func 观测函数句柄 (x,u) - z % alpha,beta,kappa UKF尺度参数 % 输出: x_upd 更新后状态均值 % P_upd 更新后协方差矩阵 n numel(x_prev); lambda alpha^2 * (n kappa) - n; % ---- Sigma点生成 ---- P_sqrt chol((n lambda) * P_prev, lower); X zeros(n, 2*n 1); X(:,1) x_prev; for i 1:n X(:, 1i) x_prev P_sqrt(:,i); X(:, 1ni) x_prev - P_sqrt(:,i); end % ---- 权值计算 ---- Wm zeros(1, 2*n 1); Wc zeros(1, 2*n 1); Wm(1) lambda / (n lambda); Wc(1) lambda / (n lambda) (1 - alpha^2 beta); for i 2:2*n 1 Wm(i) 1 / (2*(n lambda)); Wc(i) Wm(i); end % ---- 时间更新 ---- X_pred zeros(n, 2*n 1); for i 1:2*n 1 X_pred(:,i) f_func(X(:,i), u_meas); end x_pred X_pred * Wm; dX X_pred - x_pred; P_pred dX * diag(Wc) * dX Q_proc; % ---- 量测更新 ---- Z_pred zeros(1, 2*n 1); for i 1:2*n 1 Z_pred(i) h_func(X_pred(:,i), u_meas); end z_pred Z_pred * Wm; dZ Z_pred - z_pred; S dZ * diag(Wc) * dZ R_meas; Pxz dX * diag(Wc) * dZ; % ---- 卡尔曼增益与状态修正 ---- K Pxz / S; x_upd x_pred K * (z_meas - z_pred); P_upd P_pred - K * S * K; end有几个细节值得讲。第一代码用了chol函数做Cholesky分解要求P_prev是正定矩阵。很多人第一次跑就挂在P_non-positive definite的报错上这往往是因为初始P没有加上足够的正则项或者数值发散后P失去了对称正定性。第二diag(Wc)在这里会生成一个(2n1)×(2n1)的对角矩阵对于n2的场景没问题如果你状态维度上了一二十建议改成循环累加避免大矩阵乘法的内存开销。第三观测方差S是标量时K Pxz / S用的是Matlab矩阵右除结果等价于Pxz乘以S的倒数。4.4 仿真数据生成与主循环为了让UKF有一个可以对照的“真值”我先生成一个理想的电池电流工况然后用模型递推得到真实SOC和真实端电压。电流工况我设计了一个脉冲序列覆盖恒流放电、静置、大电流脉冲和回充几种常见场景代码如下N 3600; % 仿真1小时 pattern [zeros(1,50), 2*ones(1,100), ... zeros(1,50), 1*ones(1,200), ... zeros(1,80), 3*ones(1,60), ... zeros(1,60), -1*ones(1,100)]; % 负值为充电 I_seq repmat(pattern, 1, ceil(N/numel(pattern))); I_seq I_seq(1:N);这里把放电定义为正电流充电为负电流和前面状态方程的B_input符号是匹配的。然后递推真实状态并加测量噪声x_true zeros(2, N); x_true(:,1) [1; 0]; % 初始SOC 100% for k 1:N-1 x_true(:, k1) f_func(x_true(:, k), I_seq(k)); end V_t zeros(1, N); for k 1:N V_t(k) h_func(x_true(:, k), I_seq(k)); end V_meas V_t 0.01 * randn(1, N); % 模拟实测电压噪声主循环调用UKF初值故意设得不准比如真实SOC是100%滤波器初始值给0.9看看能不能拉回来x_ukf zeros(2, N); x_ukf(:,1) [0.9; 0]; P diag([0.1, 1]); Q_proc diag([1e-6, 1e-5]); R_meas 1e-3; for k 1:N-1 [x_ukf(:, k1), P] ukf_step(x_ukf(:, k), P, ... I_seq(k), V_meas(k1), ... Q_proc, R_meas, ... f_func, h_func, ... 1e-3, 2, 0); end注意一个坑观测方程里用的电压点是k1时刻的V_meas输入用的电流是k时刻的I_seq(k)因为状态方程离散化是在k时刻用该时刻电流驱动到k1时刻而量测则在k1时刻到达。这个时序关系一旦错位滤波器性能会显著下降而且很难靠调噪声参数救回来。4.5 结果可视化的对比代码最后画三张图真实SOC与估计SOC的对比、SOC估计误差、以及端电压拟合情况。figure; subplot(3,1,1); plot(1:N, x_true(1,:)*100, k-, LineWidth, 1.5); hold on; plot(1:N, x_ukf(1,:)*100, r--, LineWidth, 1.5); ylabel(SOC (%)); legend(真值, UKF估计); grid on; subplot(3,1,2); plot(1:N, (x_ukf(1,:) - x_true(1,:)) * 100, b-); ylabel(SOC误差(%)); grid on; subplot(3,1,3); plot(1:N, V_meas, g., MarkerSize, 2); hold on; plot(1:N, V_t, k-, LineWidth, 1.5); ylabel(端电压(V)); legend(带噪测量, 理想电压); grid on;从误差曲线里你能直观看到滤波器最初几秒的大误差如何被快速压制以及稳态后误差是否还能维持在合理区间。我建议你把这段代码跑通后再故意改大Q_proc或R_meas观察误差曲线如何变化这对建立调参直觉非常有效。5. 常见问题与调试经验实录5.1 协方差矩阵非正定的噩梦用UKF大概率会遇到的第一个报错是chol函数提示矩阵不是正定。这个问题的来源通常有两种一是初始协方差P给得太小或太单薄比如直接用zeros(n)导致Sigma点退化成同一个点二是长时间运行后由于状态约束或数值截断P_pred悄悄失去对称性。解决起来有几个习惯很好用初始P对角线要比状态不确定度预估稍大一些比如SOC不确定30%就给0.1极化电压不确定几毫伏就给1e-3到1e-2每次计算完P_upd后主动做一次P_upd (P_upd P_upd)/2强制对称如果cholesky仍然失败可以退而用sqrtm或者给P加上一个很小的单位阵正则项。我在实际项目里还见过一种很有意思的非正定来源状态转移矩阵A_state里有NaN或者Inf。这往往来自模型参数为0导致除零或时间常数算错。所以遇到非正定报错先查模型参数再查P不要一头扎进滤波器代码。5.2 噪声协方差Q和R的调参思路很多初学者拿到Q和R会很迷茫感觉是全靠猜。我的经验是分三步走先把R定死因为传感器的标准差比较好标定比如电压噪声标准差约10mV那R(0.01)^21e-4再把Q理解为“你允许模型漂移的程度”先给一个很小的值比如对角元1e-6然后逐步增大直到估计结果出现轻微抖动。判断标准有个笨但有效的办法观察新息序列innovation也就是实际测量值减预测测量值。如果新息均值不为零且有很大正偏负偏说明Q偏小、模型跟不上测量如果新息噪声很大且SOC估计曲线毛刺多说明R偏小、测量被过度信任。理想状态是估计曲线平滑新息序列在零附近均匀分布。调参是个动手活我建议你写一个for循环批量跑几组Q和R组合把SOC误差的均值、标准差打出来用数据选参数比自己盯着曲线调靠谱得多。5.3 初值不准时的收敛行为SOC估计在车辆启动或系统上电时经常面临初值未知的问题因为电池可能已经静置很久也可能刚充满。UKF对初值的容忍度整体比EKF好但P初值的选择决定了收敛速度快慢。如果你把SOC初值设为0.5但P_0的对角元给得极小比如1e-8滤波器会以为自己对初值非常有信心结果就是几百秒都拉不回真值。反过来P_0给得大会让前期估计波动很大但收敛很快。上面代码里我把SOC初值设为0.9真实值1.0P_0对应SOC那维取0.1意味着标准差约31.6%这是合理的保守尺度。运行后你会看到估计值在几十秒内快速接近真值这正是“信任测量、怀疑模型”的初始状态设计所导致的良性发散阶段。还有一个容易踩的坑如果把SOC状态值飞出[0,1]区间插值函数里的min(max(s,0),1)会强制截断导致观测函数在边界处梯度突变滤波器可能因此卡住。较稳妥的做法是在每次时间更新后对状态做软约束映射比如soc超过1就拉回0.98并适当增大P对角元表示“我不确定它到底是多少”。这样既保留滤波器的收敛能力又照顾到电池物理边界。5.4 电压异常值与数据同步问题端电压测量偶尔会出现瞬间跳变比如大功率继电器吸合瞬间的毛刺。UKF对单个异常值并不算敏感但如果连续出现几个异常值滤波器的SOC估计会出现可见的暂时偏移。一个低成本防护是在进入滤波主循环前对电压新息做简单的阈值检查超过预设范围就跳过这次量测更新只执行时间更新。注意跳过量测更新的那个周期P会因为过程噪声逐步增大下一拍有效测量进来后会自动体现“我变不确定了”的信息。数据同步是另一个隐蔽问题。电流和电压采样来自不同传感器如果采集时间戳对不齐相当于观测方程里的u和z不是同一时刻导致状态估计出现固定偏差。这类问题在Matlab仿真里不明显但在嵌入式实时系统里非常常见。建议在代码里加一个时间对齐的注释说明强调这个坑。6. 从仿真到实测结果与性能分析6.1 仿真工况设计与结果解读我跑的仿真是3600秒的复合工况SOC从100%逐渐下降到大约70%左右过程中包含2A、1A、3A的放电脉冲段、充电回填段和静置段。UKF从初始SOC90%开始收敛到大尺寸SOC误差低于2%用了不到60秒。进入稳态后SOC估计误差基本在±1%之内。对应端电压的拟合残差标准差大约在10mV附近和设置的测量噪声水平匹配。这个结果说明只要电池模型参数比较准确UKF的SOC估计在仿真层面完全能打得过传统安时积分。另外我也跑过把真实模型故意调偏10%的测试比如模型R0比实际小10%UKF依然能保持SOC误差在3%以内但极化电压估计会出现明显偏差。这提醒我们模型参数失配对SOC的影响有限但对电压拟合和能力估算的影响更大后者如果用于SOH诊断就要格外小心。6.2 与EKF和安时积分的对比分析我从实践经验出发用表格把三种方法放在一起对比对比维度安时积分EKFUKF对初值的敏感度极大初值误差不可消除中等依赖P0设置较低Sigma点分布更自然非线性处理不处理一阶泰勒展开无迹变换统计近似实现难度很低需要推导并实现雅可比矩阵只需实现状态与观测函数计算开销极低低中等但满足实时要求对模型参数失配的鲁棒性无一般较好靠Q调节有冗余这张表不是要证明UKF全面碾压其他方法而是说明在不同约束条件下选择是不同的。对嵌入式单片机资源紧张的老平台EKF仍然是不错的选择但如果你的控制器算力足够UKF的开发和维护成本往往是更低的因为它省掉了雅可比推导这个最容易出错的环节。6.3 代码后续扩展方向这个脚本只做了单温度、单模型参数的估计。实际工程中你可以在这个基础上做几个有价值的扩展。第一把一阶RC模型升级成二阶RC状态向量变成[SOC; Vp1; Vp2]只需要改f_func和h_func再把n改成3UKF主体函数完全复用。第二加入温度修正把内阻和时间常数写成温度的函数再扩展一个温度状态或者直接查表。第三把UKF脚本用Matlab Coder生成C代码部署到嵌入式控制器的典型流程参考某个公司的BMS开发方案注意生成代码前要把动态内存分配关掉把数组维度定上限。我还尝试过把UKF的状态估计结果与安时积分做信息融合用UKF输出的协方差去加权修正安时积分的漂移这个方案在电流传感器噪声较大时能进一步压低误差。但本质上这就是在UKF框架之上再叠了一层融合逻辑收益有但不是线性的建议你先跑通基础版再考虑要不要加。最后再分享一点经验写这段代码我最大的体会是UKF的难点从来不在算法理解而在于把模型参数、噪声矩阵、初始状态这三样东西真实地对齐到你的电池上去。很多人上来就追求高精度结果把Q和R调得特别极端看似SOC曲线贴得漂亮一换工况马上崩。我自己的习惯是先粗调Q和R让滤波器在全工况下不崩溃然后再用一组独立验证数据做精度评估。参数设计上我也会把初始SOC偏移故意设大检验滤波器的鲁棒性如果连这个保护性测试都过不了离上车验证还有很大距离。最后再提一个小技巧跑UKF前可以用开路电压法估计一个粗略SOC作为滤波器初值再用滤波结果去校正这个“粗对齐精确追踪”的两步流程比单纯依赖滤波器初值收敛要稳健得多。真到调实时系统的时候你会感谢这个习惯。