<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="zh-Hans-CN">
	<id>https://wiki.photonicat.cn/index.php?action=history&amp;feed=atom&amp;title=Photonicat_2_SOC_Calculation</id>
	<title>Photonicat 2 SOC Calculation - 版本历史</title>
	<link rel="self" type="application/atom+xml" href="https://wiki.photonicat.cn/index.php?action=history&amp;feed=atom&amp;title=Photonicat_2_SOC_Calculation"/>
	<link rel="alternate" type="text/html" href="https://wiki.photonicat.cn/index.php?title=Photonicat_2_SOC_Calculation&amp;action=history"/>
	<updated>2026-08-31T05:07:41Z</updated>
	<subtitle>本wiki上该页面的版本历史</subtitle>
	<generator>MediaWiki 1.39.1</generator>
	<entry>
		<id>https://wiki.photonicat.cn/index.php?title=Photonicat_2_SOC_Calculation&amp;diff=260&amp;oldid=prev</id>
		<title>Guangyingmao：​创建页面，内容为“'''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 in…”</title>
		<link rel="alternate" type="text/html" href="https://wiki.photonicat.cn/index.php?title=Photonicat_2_SOC_Calculation&amp;diff=260&amp;oldid=prev"/>
		<updated>2026-08-30T06:35:12Z</updated>

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