资讯动态

BISHI18多项式输出实现:从最小二乘拟合到定点优化

发布时间:2026/9/10 3:00:11 来源:尧图企业网站定制
搞嵌入式或者仪表开发的朋友对多项式输出这几个字应该不陌生。年初我们团队拿到一台BISHI18的设备核心需求一句话给它一路模拟输入它得按预设的多项式关系在输出端给出对应的控制量。听起来就是输入进、输出出的线性映射对吧真要动起手来里头涉及的拟合算法选型、数值稳定性处理、实时性保障还有各种边界情况着实让人掉了几把头发。这篇就把我们基于BISHI18做多项式输出功能的完整过程写出来包括硬件接口的取舍、最小二乘拟合的落地细节、实测中遇到的精度漂移问题以及最后在MCU上跑通的定点优化方案给正准备做类似功能的同行一个参考。机器本身不带多项式运算模块厂家给的SDK里只有基础的模拟量读写函数等于说从拟合系数到实时求值全套逻辑都得自己写。而且BISHI18的采样通道和输出通道之间没有硬件隔离模拟地上稍微有点干扰曲线就会抖。这个项目最适合的读者是那些手头正在做传感器线性化、流量计曲线补偿、或者任何非线性映射需要高精度还原需求的朋友只要你有类似输入一个值、按函数关系输出另外一个值的场景这篇里的经验基本都能平移过去。1. BISHI18项目为什么非要多项式输出需求来源与方案对比1.1 需求其实是一张标定曲线表最初拿到需求文档现场工程师给的描述是要一条平滑的S型曲线。他们要做的是一个液位监测系统液位传感器的输出在0到3.3伏之间但传感器本身响应是非线性的尤其在低液位和高液位两端电压变化特别慢中间段又特别敏感。如果直接用ADC值换算液位误差能到正负5%这在实际工程里是不可接受的。厂家随货提供了一张标定表每隔0.1V给一个标准液位值总共33个点。需求方希望BISHI18能做到输入任意电压值比如2.347V输出端能给出对应的液位数值而且误差要控制在正负0.5%以内。这就摆明了是要在33个离散点之间做连续映射本质上是曲线拟合问题。1.2 备选方案绕了一圈查表插值、分段线性、多项式拟合说句实在话最开始我并没有直接奔着多项式去。那时候图省事想用查表加线性插值结果给自己挖了个大坑。BISHI18的输入量程是0到3.3V如果要做成0.001V分辨率那就需要3300个标定点标定表里只有33个点意味着中间的绝大部分值都靠插值算出来。线性插值的精度取决于标定点之间的曲线弯折程度S型曲线在拐点附近曲率大线性插值出来的误差正好卡在需求允许误差的临界点附近非常不保险。分段三次样条插值也考虑过。理论上它能保证过所有数据点曲线足够平滑。但问题在于计算量每个输出都要先找到所在区间再计算三次多项式而且样条系数计算涉及求解三对角方程组在BISHI18那颗主频不算高的MCU上跑实时性有点悬。更麻烦的是样条插值的结果是过拟合的标定表本身的测量误差会被完整保留在曲线里相当于把传感器的原始误差原封不动地搬到了输出端。果断转投多项式拟合本质原因就是它只求一条尽量靠近所有数据点的曲线而不强求穿过每一个点天然有低通滤波的效果能把标定表里的随机测量误差抹掉一部分。对于一个标定表只有33个点、MCU资源又有限的场景多项式拟合换来的是全局平滑性和极低的计算开销。这条思路定下来之后后面所有问题其实都在围绕怎么在BISHI18上评出足够稳定的多项式系数来展开。1.3 阶数选择的第一直觉陷阱定方案的时候团队里最激烈的争论是多项式阶数取多少。有人一看S型曲线直接拍板说这至少得7阶、8阶。这个直觉很危险因为阶数越高多项式越容易在端点处剧烈震荡也就是常说的龙格现象。尤其是输入范围两端数据点稀疏的时候高次多项式为了穿过所有点曲线会大幅摆动输出值可能飞到完全离谱的程度。我当时定了个原则先用3到6阶分别做拟合看均方根误差和最大绝对误差再结合BISHI18的ADC实际噪声水平来决定。如果3阶已经能把最大误差压到需求的一半以内那就没必要上5阶。后面实测数据也确实印证了这个原则4阶多项式在33个标定点上的最大误差已经做到0.2%6阶看上去误差更小但在端点附近出现了明显的过冲输出值已经在物理意义上不合理了。所以最后选了4阶这个后面会详细展开。2. 多项式拟合的工程落地从最小二乘到正规方程的取舍2.1 最小二乘拟合的数学本质和两种实现路径多项式拟合说白了就是给定n个数据点找一条m阶多项式去逼近这些点让每个点的残差平方和最小。这个最小是可以用矩阵运算直接解的不需要迭代。具体来说假设多项式形式是 y a0 a1·x a2·x² ... am·x^m把每一组(x_i, y_i)代入会得到一个超定方程组。用矩阵表达就是 X·a y其中X是n行(m1)列的范德蒙德矩阵第i行是[1, x_i, x_i², ..., x_i^m]。方程数目n远大于未知数数目(m1)所以这个方程组通常没有精确解只能求最小二乘意义下的解。解这个超定方程组有两条主流路径。第一条是解正规方程两边同时左乘X的转置得到 XᵀX·a Xᵀy这是一个(m1)阶的对称正定方程组用高斯消元或者Cholesky分解就能解。这条路实现最简单代码量最少但缺点也很明显当阶数较高、输入数据范围较大时XᵀX的条件数会变得非常大导致解出来的系数对数据的小扰动非常敏感数值上可能很不稳定。第二条路径是直接对X做QR分解再求a数值稳定性比正规方程好得多代价是代码复杂度上了一个台阶。2.2 BISHI18上为什么先用正规方程、后来又补了QR分解做这个项目的时候我先在PC上用Python的numpy试了一遍一次性算出系数跑到MCU上直接用整个过程完全绕过了在MCU上实时解方程的问题。既然是离线算系数那数值稳定性问题理论上只在PC端出现PC上有双精度浮点正规方程的条件数问题几乎不会暴露出来。不过我自个儿留了个心眼因为BISHI18做的是液位监测不同量程的液位传感器标定表的x值范围差异很大有的可能是0到0.1V的小信号有的可能直接到0到10V。哪怕在PC上算当输入范围很大、阶数上到6阶以上时正规方程照样可能在PC上出现数值问题。所以我在PC端直接就把路径选成了QR分解。但这里有一个关键操作多项式拟合之前必须先把x归一化到[-1, 1]区间否则范德蒙德矩阵的列向量数量级差异巨大x和x⁴差了四个数量级哪怕QR分解也救不回来。归一化的方法很简单取数据中x的最小值x_min和最大值x_max把每个x映射为 x_norm (2·x - x_min - x_max) / (x_max - x_min)这样归一化后所有输入值都落在[-1,1]区间内。后面在BISHI18的MCU上实时求值时也是先把ADC值做同样的归一化再代入多项式算出的结果再反归一化回实际液位值。这个先归一化、后拟合、再对输入做同步归一化的做法是整个项目里性价比最高的一个设计直接规避了高次多项式所有能想到的数值麻烦。2.3 用Python先把系数算出来完整流程在PC端我用Python完成系数求解的完整流程大概是这样import numpy as np # 标定点数据示例电压/V液位/m cal_points np.array([ [0.0, 0.0], [0.1, 0.18], [0.2, 0.42], [0.5, 0.98], [1.0, 1.8], [1.5, 2.5], [2.0, 3.1], [2.5, 3.45], [3.0, 3.55], [3.3, 3.6], ]) x cal_points[:, 0] y cal_points[:, 1] # 归一化 x_min, x_max x.min(), x.max() x_norm (2.0 * x - x_min - x_max) / (x_max - x_min) # 用4阶多项式拟合 coeffs np.polyfit(x_norm, y, 4) print(归一化后的系数从高阶到低阶:, coeffs)numpy的polyfit默认用的是最小二乘底层做的是SVD分解数值稳定性比我自己手写QR还要好一些所以PC端这一步完全不需要我操心稳定性问题。关键在于算出来的系数是归一化输入下的系数不能直接用于原始输入移植到BISHI18的时候必须把归一化系数与x_min、x_max一起写进设备的参数表。2.4 到这一步还没完别忘了检查残差很多新手算完系数就急着往设备里写我建议你先停下来看两组指标一是拟合均方根误差RMSE反映整体偏离程度二是最大绝对误差max_error这个直接对标用户需求的±0.5%。这两个指标可以用下面的代码一次性算出来# 拟合误差评估 y_fit np.polyval(coeffs, x_norm) rmse np.sqrt(np.mean((y_fit - y) ** 2)) max_error np.max(np.abs(y_fit - y)) print(fRMSE {rmse:.5f}) print(fMax error {max_error:.5f})对于液位传感器那组标定数据4阶多项式的RMSE大约在0.05%最大误差在0.2%都低于需求上限的一半。到这里PC端的准备工作就全部结束了接下来要把这套算法搬进BISHI18的开发环境在这里才是真正磨人的地方。3. BISHI18上的实时求值实现从双精度浮点到定点优化3.1 BISHI18主控对浮点运算的不友好程度拿到BISHI18的SDK我去查了一下它主控的信息主频不算高而且是颗不带硬件FPU的单片机。这里就冒出一个问题直接用float类型跑浮点运算CPU的负载会有多高我做了个简单测试在BISHI18上跑100万次4阶多项式计算用霍纳法则就是秦九韶算法实测耗时float类型大约是80毫秒如果改成double类型耗时会急剧膨胀到接近280毫秒。别看单次计算看起来不慢但BISHI18在这个项目里还要同时跑采样任务、显示刷新和通讯任务控制周期也不能拉长CPU占用率一旦飙上去其他任务就会开始抖。这个测试让我下决心做定点化。所谓定点化就是不用浮点数而是把小数乘一个固定的倍数变成整数来算。比如我把多项式系数和输入x都放大到Q15格式就是放大32768倍用16位有符号整数表示计算过程全程用整数乘加只在最后输出结果时才把放大倍数除回来。这样既保证了精度又把CPU负载降了一个数量级。3.2 霍纳法则与定点融合的细节多项式求值最经典、最高效的算法就是霍纳法则。4阶多项式 y a0 a1·x a2·x² a3·x³ a4·x⁴可以写成嵌套乘加的形式 y ((a4·x a3)·x a2)·x a1)·x a0这样只需要4次乘法和4次加法而且不需要显式计算x的幂次省去了大量中间运算。在BISHI18上我用C语言实现了这个过程先做了浮点版本float poly_eval_float(float x, const float *coeff, int order) { float result coeff[order]; for (int i order - 1; i 0; i--) { result result * x coeff[i]; } return result; }如果只是原型验证这个版本就够了。但真要量产还得换成定点版本。定点版本的坑在于溢出的控制。我选的Q15格式输入x归一化后范围是[-1,1]对应的Q15整数表示是[-32768, 32767]。但系数a并不是都在[-1,1]区间内有些系数可能会到几十甚至上百。这就意味着如果直接把系数也按Q15放大乘法运算中很容易溢出16位范围结果直接变成负数。我的解决办法是先对所有系数做一次整体缩放分析找出绝对值最大的系数计算一个缩放因子让所有系数乘这个因子之后都落在Q15的表示范围内然后再把输入x的Q15倍率做相应调整。简化版的做法是归一化后的输入x我用Q15表示但系数在MCU上以int32存储计算时直接用(int64)做乘加最后统一移位还原。BISHI18这颗主控的乘法指令是32位乘32位得64位结果硬件上不会溢出这样最省心。代码如下#include bishi18.h // 全局归一化参数 static float x_min, x_max; static int32_t coeff_q[5]; static int s_shift; // 初始化传入PC端计算好的浮点系数 void poly_init(const float *coeff_f, float xmin, float xmax) { x_min xmin; x_max xmax; // 把浮点系数转成Q15并放大一定倍数最终用shift统一还原 // 这里Q15即乘32768额外再乘2^8最终移位还原 for (int i 0; i 4; i) { coeff_q[i] (int32_t)(coeff_f[i] * 32768.0f * 256.0f); } s_shift 8 15; // 8是额外放大倍数15是Q15的倍率 } // 输入原始电压x_raw输出液位值 float poly_eval_fixed(float x_raw) { // 归一化 float x_norm (2.0f * x_raw - x_min - x_max) / (x_max - x_min); // x转Q15用int32保存避免溢出 int32_t xq (int32_t)(x_norm * 32768.0f); int64_t result coeff_q[4]; result (result * xq 15) coeff_q[3]; result (result * xq 15) coeff_q[2]; result (result * xq 15) coeff_q[1]; result (result * xq 15) coeff_q[0]; // 还原到实际液位值 float y (float)(result s_shift); return y; }这里的移位时机要格外注意。我在每次乘完之后立刻右移15位是为了防止中间结果无限膨胀。举个例子如果result和xq都是32767的量级乘积大约是1.07×10^9完全在int64范围内但立即右移15位后结果就回到32767量级后续乘法再次乘积也不会撑爆。这个乘完就移、移完再乘的操作手法是定点多项式求值不出错的核心。3.3 实测精度对比定点与浮点的差距为了确认定点化没有牺牲太多精度我在BISHI18上对同一组输入做了浮点和定点两种实现的对比测试。输入从0到3.3V每0.01V取一个点运行100次统计最大偏差。结果让我放心了两个版本的输出差值最大只有0.003V对应液位误差不超过0.01%。这个精度损失完全在可接受范围内因为BISHI18的ADC本身的有效位数才12位在3.3V量程下1个LSB就相当于0.8mV定点化引入的误差比ADC的量化噪声还要小一个数量级。不过这里还有一个参数必须实测校准BISHI18的ADC参考电压。SDK文档写着参考电压是3.3V但我用高精度万用表实测实际只有3.285V。如果按照3.3V去换算整个量程就会产生0.45%的系统误差正好把需求允许的误差吃掉一大半。所以我把参考电压作为参数在代码里显式定义实测后填了3.285V。4. 实测中的精度漂移与边界问题踩坑记录和排查链路4.1 奇怪的零点漂移输出在低端歪了代码写完后我们做了一轮72小时连续通电的老化测试。前24小时一切正常到36小时左右现场反馈说低液位段输出偏了而且是一点点往外爬。这个现象很诡异因为多项式系数是常量输入没变输出理论上是完全确定的它怎么可能会自己漂我第一时间怀疑的是参考电压温漂。BISHI18的ADC参考电压用的是内部的LDO输出温度升高后电压值确实会缓慢漂移。但这个假设有个漏洞如果是参考电压整体漂移那高液位段应该也同样受影响了可现场数据显示高液位段误差几乎为零。我再仔细看了一眼数据发现低液位段并不是整体平移而是呈一种非线性的扭曲。这时候我意识到漂移可能根本不在输出侧而在输入侧。也就是说ADC采样到的原始值本身在变多项式只是忠实地把漂移后的输入值映射成了输出而已。BISHI18的模拟输入通道在PCB布局上比较紧凑低液位对应的是传感器输出信号比较小的时候此时信号线上的微弱干扰相对占比变大了如果线缆屏蔽层没有在设备端单点接地共模干扰就会顺着信号线灌进ADC。排查的突破口是一个简单的对比实验把输入端直接用短接线短接到地看ADC读数是不是稳定在零附近。结果发现短接后ADC读数稳定没有任何漂移。那就说明问题出在外部传感器信号链路上而不是BISHI18内部。最后通过把屏蔽层在设备端改接模拟地同时把采样均值滤波窗口从4次提到16次漂移现象消失了。这个坑让我学到一条硬经验BISHI18这类集成度高的采集设备线性度、多项式精度都建立在输入信号干净的基础上多项式拟合能修正传感器非线性但修不了外部干扰。排查的时候不要一上来就怀疑公式要先确认输入数据本身是不是稳的。4.2 端点过冲高次多项式在边界处的翻车前面提到6阶多项式在端点出现过冲这块我展开说一下。在PC端用6阶多项式拟合时残差确实比4阶更好看RMSE降到了0.02%。但把6阶系数烧进BISHI18后在输入接近量程上限3.3V时输出液位值开始出现了一个明显的凸起超过最大标定液位一大截然后才回落。这个现象在数学上就是典型的龙格现象在端点处的表现。那问题来了既然残差更小为什么实际输出却更离谱原因在于拟合误差指标只看标定点上的误差而龙格现象是发生在标定点之间的区间内尤其是在端点以外的小段延伸区域。BISHI18的输入不可能永远精确落在标定点上一旦输入值超过了标定表的最大值高次多项式的延伸趋势会急剧上扬输出直接爆掉。解决方案有两个层面。第一层是把阶数降回4阶牺牲一点点标定点拟合误差换来端点外延区域的收敛性。第二层是即使在4阶下也实现了输出限幅在poly_eval_fixed的最后如果输出超出物理液位范围0到满量程就把它截断到边界值。这两条叠加后端点行为完全可控。4.3 标定点分布不均导致的隐形误差还有个隐蔽的问题值得单独拎出来说。标定表是厂家给的33个点在低液位段比较密集高液位段比较稀疏。这本来符合传感器特性因为低液位段非线性强、需要更多点来描述。但对多项式拟合来说数据点分布不均会导致拟合结果偏袒数据密集的区域也就是低液位段拟合得很好高液位段可能稍微偏离。这个现象在残差图上看得很清楚低液位段的残差在±0.05%以内波动高液位段残差却系统性偏到-0.15%左右。解决办法是在最小二乘拟合中引入权重。我按每个区间的长度倒数作为权重让稀疏区域的点获得更高的权重从而迫使拟合曲线更均衡地照顾各个区域。# 基于区间长度的倒数构造权重 x_sorted np.sort(x) weights np.ones_like(x) # 简化示例按点到最近邻点的平均距离构造权重 for i in range(len(x)): if i 0: weights[i] 2.0 / (x[1] - x[0]) elif i len(x) - 1: weights[i] 2.0 / (x[-1] - x[-2]) else: weights[i] 2.0 / (x[i1] - x[i-1]) # numpy polyfit支持权重参数w coeffs_w np.polyfit(x_norm, y, 4, wweights)加权重之后最大绝对误差重新回落到0.1%以内而且分布更加均匀不再是一端好、一端差的局面。这个细节如果用得好相当于在不提升阶数的情况下白赚了一截精度我后续做类似拟合都会保留这个加权步骤。4.4 数据采集的均值滤波与阶数选择的联动还有一个容易忽略的联动关系输入数据的噪声水平会直接影响多项式阶数的最优选择。BISHI18的ADC在默认配置下单次采样噪声大约有3到4个LSB。如果噪声大你就需要一个更高的阶数去拟合出数据的大趋势但实际上是在拟合噪声的随机形态。如果噪声小其实4阶就足够。我实测过一组对比把均值滤波窗口从4提到16后ADC噪声降到1个LSB以内重新用4阶多项式拟合最大误差比滤波前降低了近一半。这个过程的启发是多项式拟合的精度上限不取决于拟合算法本身而取决于输入数据的信噪比。先把数据洗干净的收益远大于在拟合算法上抠那零点几个百分点的残差。5. 多项式输出模块的架构拆分参数表、诊断与应用扩展5.1 把系数做成可配置参数表工程化和实验室Demo最大的区别就是你不希望每次改一个系数都要重新编译烧录固件。我把多项式系数5个、归一化参数x_min、x_max、输出限幅范围、均值滤波深度全部做进了BISHI18的Flash参数区通过modbus寄存器读写。这样现场替换不同量程的传感器时只要往参数区写入一组新的标定系数不需要动固件。参数表的结构大概是这样寄存器地址参数名类型说明0x100Coeff_A0float32多项式常数项0x102Coeff_A1float32一次项系数0x104Coeff_A2float32二次项系数0x106Coeff_A3float32三次项系数0x108Coeff_A4float32四次项系数0x10AX_Minfloat32归一化最小值0x10CX_Maxfloat32归一化最大值0x10EOut_Limitfloat32输出限幅值实际部署时有个细节很关键参数写入后不能只是改几个全局变量必须重新初始化定点系数coeff_q和移位值s_shift。我专门做了一个poly_reconfigure()函数参数区有任何变化就调用一次确保浮点参数和定点缓存始终同步避免出现参数改了但输出没变化这种让人抓狂的问题。5.2 状态自检多项式输出模块的实时健康监测BISHI18用在现场不可能每时每刻盯着电脑看曲线。我加了一个简化的自检函数每秒运行一次把ADC原始值、归一化输入、多项式输出、限幅标记这四个变量上报给上位机同时设置一个输出异常标志。这个自检主要关注三类异常输入失效ADC输入恒为0或恒满量程、输出越限连续10次输出值被限幅器截断、系数失配校验参数区的校验和后与固件内置默认值不一致。这三类异常对应着传感器故障、量程设置错误、参数区被误写三种典型现场问题。有了这个自检机制现场排查效率大幅提升很多时候不用再带万用表去量信号直接看设备自诊断码就能定位方向。5.3 横向扩展同样的框架能用来做什么BISHI18的多项式输出模块完成后我又用同一套框架做了两个不同场景的应用验证了它的可移植性。第一个场景是热电偶的冷端补偿。热电偶的输出电压和温度本身就是非线性关系标准分度表也是离散点只是点数比液位传感器多得多。直接把标定表换成热电偶分度表用6阶加权多项式拟合在0到400摄氏度范围内误差做到0.1摄氏度以内。第二个场景是流量计的流道非线性修正。流量计在低流速段的压差和流速关系近似线性但高流速段会明显偏离用5阶多项式修正后全量程精度提升了一个数量级。这些扩展都建立在多项式输出这个通用框架之上参数表、定点求值、自检模块完全复用只更换标定点数据和阶数就完成了适配。6. 关于BISHI18多项式输出项目的最终复盘从最初的一纸标定表到最后在BISHI18上稳定跑通的定点多项式输出模块这个项目前前后后花了三周。回过头看最核心的技术判断有三个一是选定了多项式拟合而非查表插值换来的是全局平滑和低计算量二是坚持了先归一化再拟合、MCU端同步归一化的策略从根上规避了数值稳定性问题三是把浮点算法迁移到定点场景时用int64中间量和乘完就移的方法在BISHI18这颗没有FPU的主控上跑出了足够低的CPU占用率。如果让我再做一个类似的项目我会在一开始就把输入信号质量和标定点分布合理性这两件事的验证排到前面。多项式拟合解决的是数学映射层面的问题但整个系统的精度上限始终被输入信号的信噪比和标定数据的质量锁死。算法层面的优化空间其实很有限真正决定项目成败的往往是那些在现场才会暴露出来的接地、屏蔽、滤波、参数管理这些看似无关紧要的细节。最后分享一个小技巧在PC端把拟合做完、系数烧进BISHI18之后别急着收工务必用一段线性度特别好的标准信号源走一遍全量程对比实际输出和理论输出画一条误差曲线。这一步能帮你一次性发现标定点疏密不均、ADC参考电压偏差、端点过冲等一揽子问题比出问题后现场绞尽脑汁排查高效得多。BISHI18的多项式输出功能本身不复杂但要把把输入按函数映射到输出这件事做到既准又稳功夫全在这些看起来不起眼的细节里。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价