查看“Photonicat 2 SOC Calculation”的源代码
←
Photonicat 2 SOC Calculation
跳到导航
跳到搜索
因为以下原因,您没有权限编辑本页:
您请求的操作仅限属于该用户组的用户执行:
用户
您可以查看和复制此页面的源代码。
'''Photonicat 2''' does not guess the battery level from voltage. It runs a proper '''coulomb counter''' (also called a fuel gauge): think of it as a water meter on the battery, accounting for every milliamp-second in and out. This page explains how that system works — from the measuring hardware, through the firmware's integration, self-learning and temperature compensation, to the Linux kernel driver that turns it all into ordinary sysfs files. For the indicator see [[Photonicat 2 LED Indicator]]; for sysfs paths see [[Photonicat 2 sysfs]]. 中文版见 [[Photonicat 2 SOC 计算]]。 == Voltage and charge level == A lithium cell's voltage-vs-charge curve is very flat in the middle. The figures below come from the firmware's '''discharge OCV table''' (IR-compensated pack voltage): {| class="wikitable" ! Pack OCV (open-circuit voltage) !! Charge level |- | 7.58 V || 70 % |- | 7.50 V || 60 % |- | 7.44 V || 50 % |- | 7.36 V || 40 % |- | 7.28 V || 30 % |} Going from 70 % down to 30 % moves the voltage by just '''0.30 V''', about '''6 mV per percent'''. Under load the IR drop across the internal resistance reaches tens of millivolts, so voltage alone can be off by more than 10 %, and the error grows with load. The two ends are steep: in this table 8.34 V is 100 % and 6.34 V is 0 %. The firmware therefore '''anchors at the ends with voltage and advances through the middle by current integration'''. == Overall structure == charge = starting charge − ∫(discharge current)dt + ∫(charge current)dt Integration needs a starting point, and its error accumulates over time, so it is paired with a set of correction mechanisms. Photonicat 2 layers them as follows: {| class="wikitable" ! Layer !! Job |- | Measurement hardware || TPA626 samples voltage/current every 100 ms |- | Coulomb integration || Accumulates current into a µAs ledger every 200 ms |- | OCV fusion || At rest, looks up charge from voltage and gently pulls the integral back |- | Anchors || At full/empty, pins the value directly to 100 % / 0 % |- | Self-learning || Capacity, resistance, empty point and charge gain — learned per unit |- | Temperature || Cold raises resistance and shrinks usable capacity; both corrected |- | Display layer || Maps internal charge to the 1–100 % the user sees |} == 1. Measurement hardware == The measuring chip is a '''TPA626''' (an I²C current/voltage monitor, in the same family as the INA226). It sits across a milliohm-class shunt resistor in the battery path, deriving current from the voltage drop while also measuring bus voltage. * '''Sampling''': roughly every 100 ms (<code>VD1_Collect</code>) * '''Median filtering''': voltage and current each pass through a median filter that rejects occasional I²C glitches and load spikes * '''Raw values kept separately''': the median filter lags by about 3 s and would smear out the charger plug/unplug voltage step; internal-resistance learning needs that step, so unfiltered raw values are kept alongside === Current calibration === The shunt and amplifier have unit-to-unit variation, so each board is calibrated against a precision multimeter and fitted to a quadratic: I_true(mA) = C2·x² + C1·x + C0 Current coefficients (second calibration round, 2026-07-24; residuals ≤3 mA across four points): {| class="wikitable" ! Term !! Value !! Purpose |- | C2 || −4.444×10⁻⁵ || Quadratic term, corrects high-current nonlinearity |- | C1 || 1.0411 || Linear term (gain) |- | C0 || +20 mA (discharge) / 0 mA (charge) || Zero offset, per direction |} Implementation details: * '''Clamp before evaluating''': outside the calibrated range the parabola bends back on itself (vertex near +11.7 A), so the input is clamped to ±3 A and extended linearly by the endpoint slope beyond that. * '''64-bit arithmetic''': x² reaches 9×10⁶, which overflows int32 once scaled. * '''55 mA dead band''': anything smaller is treated as zero, keeping idle noise out of the accumulator. == 2. Coulomb integration == <code>coulomb_counter_update()</code> runs every '''200 ms''', multiplying current by elapsed time into a 64-bit '''µAs (microamp-second)''' accumulator. Using µAs rather than mAh avoids fixed-point rounding error accumulating over long runs. * Discharge subtracts, charge adds * Currents under '''30 mA''' in magnitude are ignored (dead band, anti-drift) * The charge direction carries its own learned '''gain''' — charging is not 100 % efficient == 3. OCV fusion == Whenever the battery is '''near rest''', the firmware looks up charge from voltage and applies a small correction to the integral. All of the following must hold: {| class="wikitable" ! Condition !! Threshold !! Notes |- | Current small enough || |I| ≤ 340 mA (~0.05C) || IR-model error is amplified at higher current |- | Held long enough || ≥ 30 s || Lets polarisation and surface charge relax |- | Voltage slew || ≤ 20 mV/s || Still in a transient, so not really at rest |- | Current slew || ≤ 2000 mA/s || Likewise |- | Plug/unplug grace || 10 s || Voltage has not settled after a charger event |} The correction is '''blended''' rather than written over the integral: * Weight capped at '''α = 0.07''' (OCV contributes at most 7 %), ramping linearly from 0 over 8 s * Any single correction is limited to '''0.20 %''' OCV therefore corrects the integral gradually, and the displayed reading does not jump. === Three OCV tables === Lithium cells show '''hysteresis''': at the same charge level, terminal voltage reads higher while charging and lower while discharging (about 30 mV per cell, roughly ±60 mV for a 2S pack). The firmware carries three tables: * '''Discharge table''' — used in discharge mode (|I| > 900 mA) * '''Charge table''' — used in charge mode * '''Rest table''' — the average of the two, used at low current (|I| ≤ 600 mA) Mode switching is '''hysteretic''' (enter at 900 mA, leave at 600 mA) to avoid chattering around the threshold. Before any lookup the reading is '''IR-compensated''': OCV = terminal voltage − I×R_pack, using the learned resistance described below. == 4. Anchors == The middle is carried by integration; the two ends are set directly by anchors: * '''Full anchor''': charger present, vbus ≥ 8 V, and current has fallen low → charge = 100 %. The charger requirement is necessary because '''resting OCV at 95 % is about 8.16 V''', so a high resting pack is easily mistaken for full; a poor or under-voltage charger cannot reach true full and is refused. * '''Empty anchor''': discharged to the cutoff point → charge = 0 %, which also triggers empty-point learning. == 5. Self-learning == Unit-to-unit variation can exceed 10 % even within one batch of cells and boards, so the firmware '''learns four quantities per unit''' and stores them in data flash, where they survive power loss. === 5.1 Full-capacity learning (ageing) === A cell rated 6800 mAh may be down to 5500 mAh after two years. With capacity hard-coded, an aged pack reads optimistically. Learning procedure: # The full anchor opens a window and zeroes the discharge accumulator # The pack discharges all the way to the low-voltage cutoff # Full capacity = accumulated discharge + '''700 mAh''' # Detecting any charging mid-window voids that attempt The '''700 mAh''' is the cutoff reserve: the device never runs the pack down to 0 V, so at the cutoff (around 6.3–6.45 V) roughly 700 mAh remains inside and must be added back. The learned value also yields '''health''': health = learned capacity / 6800 mAh Health is folded in slowly via an EMA, so a single measurement does not move it sharply. === 5.2 Internal-resistance learning (voltage-step method) === Resistance is measured from the voltage step at the '''instant a charger is plugged or unplugged''': current steps by ΔI and voltage steps by ΔV, so R = ΔV / ΔI Conditions: voltage > 7.8 V (near full, otherwise OCV itself drifts with charge and spoils the measurement), current step ≥ 300 mA, voltage step ≥ 15 mV. Results fold in by EMA at 3/10 weight, clamped to 10–200 mΩ, defaulting to 50 mΩ. This is the '''whole-loop''' resistance: cell body + wiring/connectors (20 mΩ) + protection-board MOSFET (15 mΩ). IR compensation uses that series total with nothing subtracted; the 35 mΩ of board parasitics is subtracted only when the host displays "cell internal resistance". === 5.3 Empty-point learning === Each unit's current-sense gain and true capacity differ slightly, so the moment internal SOC reaches zero and the moment the hardware actually cuts out do not line up (observed as the device cutting out while still showing 20 %). Rather than altering the current/mAh chain, the firmware applies a per-unit learned linear map at the '''display layer''': displayed = (internal SOC − empty) × 100 / (100 − empty) '''Empty''' is the internal SOC recorded the last time the battery genuinely ran out. Learning events: a low-voltage shutdown command, an unexpected host death while discharging at low voltage, or the empty anchor. It also applies '''downward self-correction''': if voltage is still healthy (> 6.8 V) during discharge but internal SOC has already fallen below the empty point, empty was learned too high and follows the reading down. A replaced battery, or an overshoot, recovers within one discharge cycle. === 5.4 Charge-gain learning === Charging is not 100 % efficient, so charge pushed in differs from charge actually stored. This gain is learned at the full anchor and written to flash when it moves by ≥1 %. == 6. Temperature compensation == An on-board NTC feeds temperature to the coulomb counter every 200 ms, driving two corrections. === 6.1 Resistance temperature factor k(T) === Internal resistance varies with temperature, which directly affects IR compensation. The factor is a piecewise-linear approximation of typical NCA temperature behaviour: {| class="wikitable" ! Temperature !! Resistance factor k(T) |- | −20 °C || ×3.00 |- | −10 °C || ×2.30 |- | 0 °C || ×1.80 |- | 10 °C || ×1.35 |- | 25 °C || ×1.00 (reference) |- | 40 °C || ×0.85 |} At −20 °C the resistance is about '''three times''' its room-temperature value. The learned resistance is normalised to 25 °C and multiplied by k(T) in use. Values outside −20…40 °C are clamped to the endpoints. === 6.2 Cold-weather capacity compensation === Usable capacity falls in the cold (the energy is not gone, it just cannot be drawn out). The firmware compensates by '''raising the display empty point''' so the reading falls earlier: {| class="wikitable" ! Temperature !! Empty point raised by |- | ≥ 15 °C || 0 (no compensation) |- | 0 °C || about +4 % |- | −10 °C || about +8 % |- | ≤ −20 °C || +12 % (capped) |} An NTC reading outside −30…70 °C is treated as a fault, and the last valid temperature is held (initially 25 °C). == 7. The display curve == The internally computed SOC is not displayed directly. One more mapping sits in between: * '''4 % → displays 1 %''' * '''96 % → displays 100 %''' * Linear in between (slope 99/92), clipped at both ends So '''when the display reads 0 %, roughly 4 % is still in the pack''' (plus the hardware margin below the empty point), leaving time to save work and shut down cleanly; 96 % shows as 100 %. Both are standard consumer-electronics practice. The reported percentage is '''clamped to a minimum of 1 %''': while the device is running it will not show 0 %. == 8. Retaining charge across power-off == There are three paths. === 8.1 SOC snapshot === While running, SOC is written to flash whenever it drifts by ≥5 %; shutdown, host power-down, standby entry and OTA reset all force a write and refresh the timestamp. On boot: * '''≤12 hours''': SOC = snapshot − 2.0 mA × Δt (standby current), and OCV re-estimation is '''blocked''' from overriding it. * '''>12 hours''': the snapshot is stale, so a cold-start OCV estimate is used. === 8.2 Deep-sleep discharge estimation === In deep sleep the TPA626 is powered down and no current can be measured. The firmware records an RTC minute stamp on entry and, on waking, subtracts the whole-device standby draw of about '''2.0 mA'''. === 8.3 Detecting a battery swap === Removing the battery cuts power to the RTC, which resets, so its time runs backwards. The firmware treats this as a battery swap, voids the snapshot and re-estimates from cold. A 10-minute tolerance avoids discarding a valid snapshot after an update reboot. == 9. How the data reaches Linux == Once the MCU has computed everything, it sends status frames to the host (RK3576) over '''UART'''. Two consumers sit on the host side. === 9.1 Kernel driver: photonicat-pm === This is a '''serdev''' driver (<code>990-photonicat-pm-add-driver.patch</code>, kernel 6.12) that attaches to the serial port, parses status frames, and registers as standard Linux devices: * '''power_supply''' class → <code>/sys/class/power_supply/battery</code> * '''RTC''' class → <code>/dev/rtc</code> Because these are '''standard subsystems''', existing Linux tools work directly without knowing the private protocol: <syntaxhighlight lang="bash"> # Charge level (%) — the final output of everything described above cat /sys/class/power_supply/battery/capacity # Voltage µV / current µA / power µW cat /sys/class/power_supply/battery/voltage_now cat /sys/class/power_supply/battery/current_now cat /sys/class/power_supply/battery/power_now # Energy µWh: energy_full is the learned capacity — compare against # energy_full_design to see ageing cat /sys/class/power_supply/battery/energy_now cat /sys/class/power_supply/battery/energy_full cat /sys/class/power_supply/battery/energy_full_design # Charger online cat /sys/class/power_supply/charger/online </syntaxhighlight> '''The MCU is the hardware RTC.''' There is no separate RTC chip on the board; the MCU is always powered, keeps time itself, and sends it up with the status frames. The driver registers it through <code>rtc_class_ops</code>, so standard tools work directly: <syntaxhighlight lang="bash"> hwclock -r # read the time from the MCU hwclock -w # write system time back to the MCU </syntaxhighlight> This is why '''the clock is still right after days unplugged and offline'''. It is also the mechanism behind the battery-swap detection in 8.3, since removing the battery resets the RTC and time runs backwards. === 9.2 Userspace daemon: pcat-manager === The kernel driver only consumes the power-related part of the status frame. '''The rest of the UART protocol is handled by pcat-manager''': modem management, watchdog, power on/off policy, button events, pushing the charge threshold down, MCU firmware updates and so on. The full protocol is documented at [[Photonicat 2 MCU 通讯协议]]. The division of labour: {| class="wikitable" ! Component !! Responsibility |- | MCU firmware || Measurement, integration, learning, compensation — all the algorithms |- | photonicat-pm (kernel) || Power/RTC data → standard sysfs interfaces |- | pcat-manager (userspace) || Everything else: modem, watchdog, power policy, OTA |} For example, the '''charge limit''': set 80 % in the web UI and pcat-manager sends it to the MCU over UART; the MCU stores it in data flash and drives the charging MOSFET directly. From firmware '''RA2E1260726005''' the setting persists on the MCU side and survives resets and OTA updates, and pcat-manager keeps its own mirror, re-sending it if it detects an MCU restart. == 10. FAQ == ;The reading occasionally moves slightly :OCV fusion or an anchor is correcting integration drift. A single correction is capped at 0.20 %, so large jumps should not occur in normal use; a large one generally means a stale snapshot led to a cold-start re-estimate. ;Charge drops faster in winter :Usable capacity falls when cold, and the firmware also raises the display empty point (about +8 % at −10 °C). It recovers as things warm up. ;96 % shows as 100 % :This is the display curve, see section 7. Likewise there is still margin when it reads 0 %. ;Does a new battery need calibration :No specific procedure is needed. One full discharge (from full to automatic shutdown) lets the firmware learn true capacity and the empty point; ordinary use gets there gradually. ;How often health updates :It needs a complete discharge window (full anchor → cutoff), and charging mid-window voids that attempt, so it updates slowly and changes gradually by EMA.
返回
Photonicat 2 SOC Calculation
。
导航菜单
个人工具
登录
命名空间
页面
讨论
大陆简体
查看
阅读
查看源代码
查看历史
更多
搜索
导航
首页
最近更改
随机页面
MediaWiki帮助
工具
链入页面
相关更改
特殊页面
页面信息