1. 项目概述与核心思路做嵌入式开发久了总会遇到一些需要自己动手“攒”设备的时候。最近手头一个项目里需要给一批不同型号的18650电池做循环测试市面上的成品充电器要么精度不够要么协议固定无法自定义参数于是决定自己动手设计一个高精度的锂离子电池充电器。核心目标很明确实现高精度的恒流CC和恒压CV两阶段充电控制并且要足够灵活能适配从几百毫安时到几千毫安时不同容量的电池。为什么非得自己搞锂离子电池的充电是个精细活。过充会严重损害电池寿命甚至引发安全问题充不满又会影响设备续航。标准的CC-CV充电曲线要求先以一个恒定电流充电至电池达到截止电压通常是4.2V然后保持电压恒定让电流逐渐衰减至接近0。这个过程里的电流和电压控制精度直接决定了电池的最终性能和寿命。用现成的充电IC当然方便但对于需要精确控制充电参数、集成到更大系统、或者进行电池特性研究的场景自己用微控制器搭建一个数字控制的充电器就非常有价值了。我选择用Arduino Uno作为主控一方面是其生态丰富开发速度快另一方面其性能足以处理这种中等复杂度的控制算法。整个系统的灵魂在于有限状态机的编程思想。把充电过程抽象成几个明确的状态如关闭、恒流、恒压、停止每个状态有明确的任务和跳出条件状态之间的转换由清晰的逻辑条件触发。这样写出来的代码结构清晰逻辑严谨后期调试和维护也方便得多。硬件上为了实现高精度设定和测量我选用了MCP4725 DAC来输出精确的控制电压以及ADS1115 ADC来读取高精度的电池电压和电流信号。下面我就把这个从硬件选型、电路设计、到软件实现尤其是状态机构建和精度校准的全过程详细拆解一遍。2. 硬件系统设计与核心器件选型一套可靠的硬件是精确控制的基础。这个充电器的硬件架构可以看作一个典型的闭环控制系统大脑是Arduino它发出指令执行机构是DAC和模拟电路产生所需的电压或电流反馈传感器是ADC和采样电路测量真实值大脑再根据反馈调整指令。2.1 主控与通信接口Arduino Uno与I2C总线选择Arduino Uno是因为它足够简单和稳定。对于这个项目我们不需要复杂的操作系统或极高的主频Uno的ATmega328P处理器和丰富的库支持正合适。整个系统的数据交换核心是I2C总线。MCP4725DAC和ADS1115ADC都是I2C器件这意味着只需要两根信号线SDA, SCL加上电源和地就可以串联起多个高精度外设极大简化了布线。注意I2C总线上需要接上拉电阻通常值在4.7kΩ到10kΩ之间。如果通信不稳定首先检查上拉电阻是否已正确连接。Arduino Uno的A4、A5引脚分别对应SDA和SCL。2.2 高精度数模转换MCP4725 DAC详解控制精度首先来源于设定值的精度。我们选用MCP4725这是一款12位的I2C接口DAC。12位分辨率意味着它可以将0到VCC假设5V的电压分成4096个阶梯每个阶梯约1.22mV。这个精度对于设定充电电压mV级别调整和充电电流通过后续电路转换是足够的。在系统中我们使用了两个MCP4725。一个专门用于控制高侧电流源电路其输出电压值决定了恒流阶段的充电电流大小。另一个则用于控制电压跟随器电路其输出电压在恒压阶段用于设定施加在电池两端的精确电压。之所以用两个是为了实现物理通道的隔离避免状态切换时的相互干扰也让程序逻辑更清晰——恒流阶段只操作DAC_A恒压阶段只操作DAC_B。2.3 高精度模数转换ADS1115 ADC详解测量精度是闭环控制的另一只眼睛。Arduino Uno板载的ADC只有10位分辨率且基准电压容易受干扰。ADS1115是一款16位、4通道的I2C ADC其分辨率高达0.1875mV在±4.096V量程下精度和稳定性远超板载ADC。在电路中ADS1115用于测量两个关键电压电池端电压通过一个分压网络直接测量。电流采样电阻两端电压这是测量充电电流的关键。我们在充电回路中串联了一个毫欧级别的精密采样电阻如RS3。通过测量这个电阻两端的压降利用欧姆定律I V / R即可计算出实时电流。实操心得ADS1115的输入不能超过其电源电压通常5V或设定的量程。直接测量可能超过5V的电池电压或电流采样点电压是危险的因此必须使用电压分压器。分压器不仅起保护作用还能将测量信号调整到ADC的最佳量程内提高测量信噪比。2.4 功率与控制电路电流源与电压跟随器这是将DAC的数字设定值转换为实际充电功率的核心模拟电路。高侧电流源用于恒流CC阶段。其核心是一个运算放大器加MOSFET的架构。DAC_A输出的设定电压代表目标电流输入到运放的同相端。运放驱动MOSFET使得采样电阻RS3上的压降即V_RS3跟随这个设定电压。由于V_RS3 I_charge * RS3因此I_charge V_set / RS3。通过精确控制V_set就实现了精确的恒流控制。选择“高侧”放置在电源正极和电池正极之间可以方便地测量单端电流。电压跟随器用于恒压CV阶段。这是一个由运放构成的单位增益缓冲器。DAC_B输出的设定电压直接作为运放的输入运放的输出低阻抗直接驱动电池正极。电压跟随器的作用是“隔离”和“驱动”它确保DAC设定的电压能不受负载电池影响地、强有力地施加到电池两端。一个关键的设计难点在恒压阶段电池本身是一个电压源电压跟随器的输出是另一个电压源它们通过一个限流电阻或电流源的内阻并联。实际上施加在电池上的净电压是电压跟随器输出电压与电池电动势之差。因此要维持电池端电压恒定在4.2V电压跟随器的输出电压必须是一个“浮动”的设定点它需要根据实时测量到的电池电压进行动态调整V_follower_set V_batt_measured 4.2V。这与恒流阶段设定点固定不变有本质区别需要在软件控制算法中体现。2.5 整体电路布局与供电考虑我强烈建议在完成原理图验证后将核心模拟电路部分制作成一块独立的PCB或使用万用板焊接。对于涉及微小电压信号mV级和功率部分的电路面包板引入的接触电阻、寄生电容和噪声是不可接受的会严重破坏控制精度和稳定性。使用排针将自制电路板与Arduino可靠连接是最佳实践。供电方面需要为Arduino、ADC/DAC数字部分提供稳定的5V电源。为电流源和电压跟随器运放提供正负电源如±12V以获得足够的输出摆幅。同时主充电功率需要由一个能提供足够电流和电压的直流电源模块提供其电压应至少高于电池充满电压4.2V1-2V以保证电流源电路有足够的压差工作。3. 软件架构有限状态机的实现软件是整个系统的智慧所在。采用有限状态机模型来组织充电流程能让复杂的、有时序要求的控制逻辑变得一目了然极大地增强了代码的可读性和可维护性。3.1 状态定义与转换逻辑首先我们定义充电过程的四个核心状态OFF关闭系统初始状态所有输出关闭等待启动命令。CC恒流恒定电流充电阶段。在此状态下系统核心任务是调节电流源DAC的输出使测量到的充电电流稳定在预设值如0.5C。CV恒压恒定电压充电阶段。当电池电压达到截止电压如4.2V时状态切换。此阶段核心任务是调节电压跟随器DAC的输出使测量到的电池电压稳定在截止电压。STOP停止充电终止状态。当CV阶段电流衰减至预设的截止电流如0.05C或0.03C时进入此状态关闭所有输出充电完成。状态之间的转换由明确的布尔条件触发形成一个闭环OFF - CC当收到启动信号如按键或串口命令时。CC - CV当实时测量的电池电压V_batt V_cutoff例如4180mV留一点余量以避免突变时。CV - STOP当实时测量的充电电流I_charge I_termination例如30mA对于一颗3000mAh的电池时。任何状态下如果检测到故障如电池温度过高、电压异常都应立即跳转到OFF或一个独立的FAULT状态。3.2 主程序框架与状态调度在Arduino的loop()函数中我们使用一个switch-case语句来构建状态机的骨架。这是最直观的实现方式。// 定义状态枚举 enum ChargerState { STATE_OFF, STATE_CC, STATE_CV, STATE_STOP, STATE_FAULT }; ChargerState currentState STATE_OFF; // 全局变量存储设定值和测量值 float targetCurrent; // 目标电流单位mA float targetVoltage; // 目标电压单位mV float measuredCurrent; // 测量电流单位mA float measuredVoltage; // 测量电压单位mV float terminationCurrent; // 终止电流单位mA void loop() { // 1. 读取传感器数据电压、电流、温度 readSensors(measuredVoltage, measuredCurrent); // 2. 状态机核心 switch (currentState) { case STATE_OFF: // 关闭所有输出 disableChargerOutput(); // 检查启动条件 if (startButtonPressed()) { initializeChargeParameters(); // 设置targetCurrent, targetVoltage等 currentState STATE_CC; } break; case STATE_CC: // 执行恒流控制算法 ccControlAlgorithm(targetCurrent, measuredCurrent); // 检查转换到CV的条件 if (measuredVoltage (targetVoltage - 20)) { // 接近截止电压时切换 currentState STATE_CV; enterCVMode(); // 初始化CV控制参数 } // 检查故障条件 if (checkFault()) { currentState STATE_FAULT; } break; case STATE_CV: // 执行恒压控制算法 cvControlAlgorithm(targetVoltage, measuredVoltage, measuredCurrent); // 检查转换到STOP的条件 if (measuredCurrent terminationCurrent) { currentState STATE_STOP; } // 检查故障条件 if (checkFault()) { currentState STATE_FAULT; } break; case STATE_STOP: // 充电完成关闭输出可能点亮LED提示 disableChargerOutput(); ledIndicator(COMPLETE); // 可以在此等待系统复位或新的指令 break; case STATE_FAULT: // 立即关闭输出锁定状态并显示错误代码 emergencyShutdown(); ledIndicator(FAULT); // 需要手动复位或清除故障才能退出 break; } // 3. 更新显示或通信可选 updateDisplay(); delay(10); // 主循环控制周期例如10ms }这种结构的优势在于每个状态该做什么、什么时候跳转逻辑非常清晰。添加新的状态如预充、涓流充或新的故障检测也非常容易。3.3 模块化程序文件组织为了保持代码整洁我将不同功能的代码模块化到不同的.ino文件中。Arduino IDE会自动将这些文件链接在一起。BatteryChargerMainProgram.ino包含setup()和loop()函数以及状态机的主框架和全局变量定义。CC.ino包含ccControlAlgorithm()函数及其所有辅助函数专门处理恒流控制逻辑。CV.ino包含cvControlAlgorithm()函数及其所有辅助函数专门处理恒压控制逻辑。parametersCalculation.ino包含readSensors()函数和电流电压计算函数。这里集中了从ADS1115读取原始值、应用分压比计算真实电压、根据采样电阻计算电流的所有数学处理。RxNumber.ino预留的用户界面UI模块。目前可能包含通过串口设置电池容量setAh()函数的代码未来可以扩展为设置充电参数、显示状态的接口。4. 核心算法闭环控制与传递函数校准有了状态框架每个状态内的控制算法是精度的关键。我们采用比例P控制因为对于这个系统我们已经通过实验获得了被控对象电流源/电压跟随器相对精确的传递函数。4.1 传递函数的获取与意义所谓传递函数在这里简单理解就是“输入数字量DAC代码和输出物理量电流或电压之间的数学关系”。由于模拟电路并非完全理想这个关系不是简单的线性比例。为了获得高精度控制我们必须通过实验来标定它。校准步骤实录搭建测试环境将待充电的电池替换为一个精密的、功率合适的负载电阻例如10Ω/5W。编写扫描程序在CC.ino中写一个循环让控制电流源的DAC输出代码Output变量从最小值如20逐步增加到最大值如4000因DAC为12位。对于每一个Output值等待电路稳定后用高精度万用表测量负载电阻两端的电压计算出实际电流Iout单位mA。这样就得到了一系列(Output, Iout)数据对。数据拟合将数据对导入Excel、PythonNumPy或专业工具如CurveExpert。进行多项式回归分析。我发现对于我的电流源电路一个四阶多项式能非常好地拟合数据Output a1 b1*Iout c1*Iout^2 d1*Iout^3 e1*Iout^4通过拟合我得到了我电路中具体的系数a1, b1, c1, d1, e1。应用拟合公式在真正的控制程序ccControlAlgorithm()中当我们需要输出一个目标电流I_target时就反用这个公式。即将I_target代入上述多项式计算出所需的DAC代码Output然后通过I2C发送给MCP4725。这样就实现了“想要多少电流就输出对应精确代码”的开环前馈控制这是高精度的基础。同理对电压跟随器电路也进行同样的校准得到电压控制的传递函数Output1 a2 b2*Vbatt c2*Vbatt^2 d2*Vbatt^3 e2*Vbatt^4其中Vbatt是期望在电池两端得到的电压单位mVOutput1是控制电压跟随器的DAC代码。4.2 恒流CC控制算法实现在CC阶段设定点I_target是固定的。控制流程如下调用readSensors()获取当前实测电流I_measured。计算误差error I_target - I_measured。应用比例控制delta_Output Kp * error。Kp是比例系数需要调试。太小响应慢太大易振荡。计算新的DAC设定值new_Output previous_Output delta_Output。这里的previous_Output是上一控制周期的输出值。但更重要的是我们需要用校准的传递函数来约束和修正这个输出。更稳健的做法是 a. 根据I_target和传递函数计算一个基础DAC代码base_Output。 b. 将比例控制输出的delta_Output作为对这个基础值的微调final_Output base_Output delta_Output。 c. 对final_Output进行限幅防止超出DAC范围或电路安全范围。将final_Output写入控制电流源的DAC。void ccControlAlgorithm(float target_I, float measured_I) { static float integral 0; static float lastOutput 0; float error target_I - measured_I; // 比例项 float Kp 0.5; // 需根据实际系统调试 float delta Kp * error; // 基于传递函数计算基础输出前馈补偿 // 假设有函数 calcDacFromCurrent(float current) 根据拟合多项式计算 float baseOutput calcDacFromCurrent(target_I); // 结合前馈和反馈 float newOutput baseOutput delta; // 输出限幅 (0-4095 for 12-bit DAC) if (newOutput 4095) newOutput 4095; if (newOutput 0) newOutput 0; // 写入DAC dac_current.setVoltage((uint16_t)newOutput, false); lastOutput newOutput; }4.3 恒压CV控制算法实现CV阶段是难点因为设定点V_target电池端电压是固定的但DAC的实际输出设定点V_follower_set是浮动的。控制流程如下调用readSensors()获取当前实测电池电压V_batt_measured。计算电池电压误差error_v V_target - V_batt_measured。应用比例控制计算出对电压跟随器输出电压的调整量delta_V_follower。关键步骤计算电压跟随器DAC的目标输出电压V_follower_target V_batt_measured V_target delta_V_follower。可以这样理解V_batt_measured V_target是一个粗略的前馈让跟随器输出比当前电池电压高出一个V_target的差值delta_V_follower是反馈微调用于消除误差。使用电压跟随器电路的传递函数将V_follower_target转换为对应的DAC代码Output1。将Output1写入控制电压跟随器的DAC。void cvControlAlgorithm(float target_V, float measured_V, float measured_I) { float error_v target_V - measured_V; float Kp_v 0.8; // CV环的比例系数通常与CC环不同 // 计算对跟随器输出电压的调整 float delta_v_follower Kp_v * error_v; // 计算跟随器DAC需要设定的电压值mV float follower_target_voltage measured_V target_V delta_v_follower; // 使用传递函数计算DAC代码 // 假设有函数 calcDacFromVoltage(float voltage) 根据拟合多项式计算 uint16_t dac_code calcDacFromVoltage(follower_target_voltage); // 限幅 if (dac_code 4095) dac_code 4095; // 注意这里也需要一个下限防止电压过低 // 写入电压跟随器DAC dac_voltage.setVoltage(dac_code, false); }5. 关键实操步骤与调试心得纸上得来终觉浅绝知此事要躬行。从电路焊接、软件烧写到系统联调每一步都有需要注意的细节。5.1 硬件焊接与布局注意事项地线布局是命脉模拟地AGND和数字地DGND建议在一点相连通常选择在ADC的GND引脚附近。功率电流的路径要粗而短信号测量路径要远离功率部分避免噪声耦合。采样电阻的选择RS3的选择需要权衡。阻值大测量压降大精度高但功耗也大PI²R。阻值小功耗低但测量到的mV信号小对ADC要求高。对于最大2A的充电电流选择0.05Ω50mΩ的精密采样电阻是个不错的折中满量程压降为100mV易于测量。分压电阻精度用于测量电池电压和采样电阻两端电压的分压电阻应选用1%甚至0.1%精度的金属膜电阻并且分压比要计算准确。分压后的电压必须在ADS1115的输入量程内例如±4.096V。运放与MOSFET选型电流源运放需要选择输入失调电压低、带宽足够的型号如OPA2180。MOSFET需要根据最大充电电流和压差选择确保在安全工作区内并安装足够的散热片。5.2 软件烧写与初始参数设置库文件安装确保已安装Adafruit_MCP4725和Adafruit_ADS1X15库这两个库极大简化了I2C设备的操作。I2C地址扫描首先编写一个简单的I2C扫描程序确认两个MCP4725和ADS1115的地址是否正确识别MCP4725默认0x62或0x63ADS1115默认0x48。地址冲突是常见问题。参数初始化在setup()函数或initializeChargeParameters()函数中需要根据电池规格设置关键参数float batteryCapacity_mAh 3000.0; // 例如3000mAh的18650电池 float chargeRate_C 0.5; // 0.5C充电 targetCurrent batteryCapacity_mAh * chargeRate_C; // 1500 mA targetVoltage 4200; // 4.2V 4200mV terminationCurrent batteryCapacity_mAh * 0.03; // 终止电流为0.03C即90mA5.3 系统联调与PID参数整定调试应分步进行务必接**假负载电阻**而非真电池以防意外。开环测试注释掉闭环控制部分手动给DAC一个固定代码用万用表测量输出电流或电压验证硬件电路和传递函数计算是否正确。这是校准的基础。CC环单独调试将状态机锁定在CC状态接上假负载。先设置一个较小的Kp如0.1观察电流是否能稳定在设定值附近。如果响应太慢缓慢增大Kp如果出现振荡或超调则减小Kp。可以用串口绘图工具观察电流的响应曲线。CV环单独调试这一步较难因为需要模拟一个变化的“电池电压”。可以用一个可编程电源串联二极管来模拟电池。先让系统进入CV状态观察其能否将“电池”电压稳定在设定值。状态转换测试模拟电压上升测试CC到CV的自动切换是否平滑有无电流或电压的尖峰。完整流程测试最后接上真实电池建议先用旧电池或保护板完好的电池进行小电流如0.1C完整充电测试全程监控电压电流曲线并与电池规格书对比。6. 常见问题排查与优化技巧在实际制作和调试过程中我遇到了不少坑这里总结一下希望能帮你节省时间。6.1 测量读数跳动大或不准确问题ADS1115读到的电压值不稳定最后几位数字不停跳动。排查电源噪声检查模拟部分的电源是否干净。可以在运放和ADC的电源引脚就近加装10uF钽电容和0.1uF陶瓷电容去耦。信号干扰确保测量信号线分压器到ADC输入尽量短并远离功率走线。可以使用双绞线或屏蔽线。I2C上拉电阻确认SDA和SCL线上有合适的上拉电阻4.7kΩ到10kΩ且通信速率不要设置过高ADS1115库默认速率即可。ADS1115配置检查是否设置了合适的量程setGain(GAIN_ONE)对应±4.096V和采样速率setDataRate(RATE_ADS1115_860SPS)。更高的速率噪声可能更大可以尝试降低速率。技巧在软件中加入数字滤波。最简单的是一阶低通滤波指数加权移动平均float filteredValue 0.95 * filteredValue 0.05 * newRawValue;用滤波后的值参与控制计算能有效抑制噪声。6.2 恒流控制振荡或响应慢问题电流在设定值上下波动或者变化很慢跟不上。排查控制周期检查loop()中控制算法的执行周期是否稳定且合适。太快可能硬件响应不过来太慢则控制迟缓。一个10-50ms的周期是常见的起点。可以用millis()函数实现定时控制而不是简单的delay()。比例系数Kp这是最主要的原因。Kp太大导致超调和振荡太小导致响应慢。需要耐心调试。传递函数不准重新检查校准数据和多形式拟合的准确性。特别是在工作区间的两端小电流和大电流误差可能较大。电路响应速度电流源电路的运放和MOSFET可能带宽不足无法快速响应DAC的变化。检查运放的压摆率和带宽。优化可以考虑引入积分I项来消除静差构成PI控制器。但积分项要加抗饱和并且调试更复杂。6.3 状态转换时出现电压/电流尖峰问题从CC切换到CV的瞬间电池电压有一个过冲或者电流突变。原因状态切换时控制对象从电流源DAC突然切换到电压跟随器DAC两个DAC的输出电压可能不匹配导致功率电路瞬间的驱动电压突变。解决方案实现“无扰切换”。在CC阶段末期提前计算好CV阶段初期电压跟随器DAC应有的输出值基于当前电池电压和目标电压。在切换状态的瞬间先让电压跟随器DAC输出这个计算好的值然后再关闭电流源DAC或使其输出归零。这样两个控制源在切换瞬间是“无缝衔接”的。同样在从OFF到CC的启动瞬间可以采用“软启动”策略让DAC输出值从0缓慢斜坡上升到目标值避免对电池和电路的冲击。6.4 发热严重问题功率MOSFET或采样电阻发热严重。排查压差计算充电时电流源电路上的压降V_drop V_supply - V_batt。功耗P_loss I_charge * V_drop。如果输入电源电压过高或电池电压很低深放电后这个压差会很大导致MOSFET功耗剧增。必须确保MOSFET的散热能承受这个最大功耗。采样电阻功率P_rs3 I_charge² * RS3。确保采样电阻的额定功率远大于此计算值。优化使用开关模式Buck电路作为前级将输入电压降至比电池电压稍高一点的水平可以极大降低线性电流源的功耗和发热。但这会大大增加电路复杂性。这个基于Arduino和有限状态机的锂离子电池充电器项目从概念到实现涵盖了嵌入式系统开发的多个核心方面硬件选型、模拟电路设计、传感器接口、闭环控制算法以及状态机软件架构。它不仅仅是一个充电器更是一个学习精密测量与控制的绝佳平台。你可以在此基础上扩展功能比如增加温度监控、电池健康度SOH估算、串口图形化上位机甚至支持更多电池化学类型如LiFePO4。动手做一遍你会对“控制”二字有更深的理解。