显示屏与桌面作品 / 刷新与电量实验发布 内容更新

墨水屏刷新预算实验:记录残影、耗时和整板电量

固定GDEY0213B74与DESPI-C02,比较驱动全窗口快刷和局刷;提供脱离USB的实体操作、空实测表、明确单位的电量积分与残影记录方法。

读完能做什么
得到可编译的全窗口快刷/局刷对照器、脱USB实体操作、原始CSV转换与窗口积分工具,再按同条件照片决定自己的刷新预算。
开始之前
先按D04完成指定面板的独立接线核查,并按L02理解整板单入口测流、USB旁路与仪器量程。
所需器材
S3-DevKitC-1 v1.1 N8R8;GDEY0213B74黑白面板+DESPI-C02指定2.2Ω修订、GPIO17/18各一SPST按钮及10k上拉;BUSY外10k上拉;短线与面包板、PPK2 Source Meter5.0V、外部温度计、固定机位相机;无本站仪器读数
版本组合
平台:CLI1.5.1 / Arduino-ESP323.3.12 / S3N8R8完整FQBN见versions.lock显示库:GxEPD21.6.9 / GFX1.12.4 / BusIO1.17.4 / SPI4MHz mode0固定面板:GDEY0213B74/SSD1680Z;控制器RAM128×250、可见122×250;rotation1可见250×122
适用范围

仅ESP32-S3-DevKitC-1 v1.1 N8R8,Arduino-ESP32 3.3.12/CLI1.5.1;GxEPD2 1.6.9指定GDEY0213B74/SSD1680Z与DESPI-C02(P3=2.2Ω);3V3屏幕逻辑。所有残影、调用时间、电流和电量实测为空。

这篇验证到了哪一步

资料核对已通过
docs/esp32-evidence/display-measurements.md#d09;固定版本与实际源码核对。验证日期:
构建 / 配置已通过
docs/esp32-evidence/display-measurements-builds.json;实际编译源/配置/日志/固件SHA。对象:S3-DevKitC1v1.1 N8R8版本:CLI1.5.1 / core3.3.12验证日期:
仿真不适用
未做芯片/外设/仪器仿真;同源主机及合成CSV工具检查另列display-measurements-host.json,不冒充板端性能或能量。
本站实板待验证
没有本站烧录、触摸/光学/残影/电流或长运行结果;observations.csv实测表为空。

构建通过只说明指定环境中的代码或配置通过检查;实物功能、功耗和稳定性以对应的实板记录为准。

01 / 刷新与电量实验

一张条件表,保留三类不同的证据

本实验比较同一个黑白测试图中40×40方块与VALUE0/1变化:f走固定驱动全窗口快刷,p只更新局部窗口。把软件调用耗时、固定机位照片中的残影、整板输入电量放入同一条件表;它们由不同仪器/运行组取得,不能互相代替。

原D09要求同内容比较残影/耗时/耗电;本站没有实板与PPK2,所以提供完整可运行例程、实体操作、空原始表与计算工具,不填毫秒、mJ或“几次局刷无残影”的亲测结论。面板供应商规格和库内等待参数只作各自来源的参考。

参考:[1] GxEPD2 1.6.9具体面板声明:partial能力与只读快刷常量[2] GxEPD2具体驱动:全刷、局刷、powerOff与hibernate[4] Good Display该具体面板规格:温度条件、三种厂商标称刷新方式

02 / 刷新与电量实验

选定这一块面板与驱动,再定义全刷

面板为Good Display GDEY0213B74/SSD1680Z,黑白显示,控制器RAM128×250、可见122×250;rotation1得到250×122。配DESPI-C02有P3标记2.2/0.47Ω的已核修订,选择2.2Ω,供电与逻辑只用3V3。不同后缀、彩色屏、另一适配板或不明升压模块不能按该结果外推。

厂商页面在25°C条件列全刷3s、快刷1.5s、局刷0.3s,工作温度0–50°C;这是厂商条件值,不是本例库调用的保证或本站测量。这里只做B/W,不因器件支持灰阶就声称本代码实现四灰。

固定GxEPD2 1.6.9中的useFastFullUpdate是只读常量true;最终f明确是该驱动的全窗口快刷流程,而不是可切换普通全刷。首次赋值尝试真实编译失败已留历史,最终没有修改上游常量或SDK来假装三种模式都已可用。

参考:[1] GxEPD2 1.6.9具体面板声明:partial能力与只读快刷常量[2] GxEPD2具体驱动:全刷、局刷、powerOff与hibernate[3] GxEPD2_BW:旋转窗口、字节对齐与旧图像RAM同步[4] Good Display该具体面板规格:温度条件、三种厂商标称刷新方式[5] DESPI-C02厂家产品:与本例已核P3=2.2Ω修订分别确认

03 / 刷新与电量实验

断电接线,增加脱离USB后的实体操作

复用D04的4MHz mode0连线:先断所有电源再安装FPC,依指定连接板说明核对接点朝向、锁扣和P3,不能带电翻插排线。屏幕由开发板3V3供电,FPC上的高压波形由适配板管理,不外加裸面板电压。

Full按钮GPIO17与Partial按钮GPIO18各用10k上拉到同一3V3,SPST按下接公共GND;不接5V。操作时按住至少100ms再松开;代码只要求50ms稳定状态去抖,不把100ms当硬性资格。只有去抖后识别出的双键或交叠状态才取消;短于门限的第二键可能当成抖动被忽略。库调用期间不采样按钮,等操作返回后再按。

两按钮使PPK2测流时不必插回开发板USB;GPIO17执行f,GPIO18执行p。USB功能组也可在串口发f/p,h只在串口功能组执行休眠实验。没有自动循环刷屏。

信号DevKitC1v1.1排针接到指定外设
屏幕VCC/GND3V3 J1-1 / GND J1-22DESPI-C02的3.3V电源/GND
CLK / DINGPIO12 J1-18 / GPIO11 J1-17CLK / DIN
CS / DCGPIO5 J1-5 / GPIO4 J1-4CS / DC
RST / BUSYGPIO6 J1-6 / GPIO10 J1-16RST / BUSY;BUSY另10k上拉3V3
Full按钮GPIO17 J1-10外10k到3V3,SPST到GND
Partial按钮GPIO18 J1-11外10k到3V3,SPST到GND
做到这里,你应该看到
器件后缀、P3、排线、电源和每个GPIO逐项记录后才上电。

参考:[2] GxEPD2具体驱动:全刷、局刷、powerOff与hibernate[5] DESPI-C02厂家产品:与本例已核P3=2.2Ω修订分别确认[6] DevKitC1 v1.1固定官方提交:供电与J1排针

04 / 刷新与电量实验

固定库与完整N8R8目标构建

CLI1.5.1、core3.3.12,GxEPD2 1.6.9、GFX1.12.4、BusIO1.17.4;IDE对应S3 Dev Module、Flash8MB、OPI PSRAM、QIO、8M with spiffs、CDC Disabled、Hardware CDC/JTAG。用板载USB-to-UART接口上传和115200串口。

在解压工程根目录先安装精确依赖再构建;sketch.yaml记录可选profile而不设置default_profile,避免无意下载另一套工具。版本、库锁及许可见工程文件,传感器或网络不属于此例。

代码 / 配置
arduino-cli core install esp32:esp32@3.3.12
arduino-cli lib install --no-deps 'Adafruit BusIO@1.17.4' 'Adafruit GFX Library@1.12.4' 'GxEPD2@1.6.9'
arduino-cli compile --fqbn 'esp32:esp32:esp32s3:FlashSize=8M,PSRAM=opi,FlashMode=qio,PartitionScheme=default_8MB,CDCOnBoot=default,USBMode=hwcdc' .
# 上传端口先按F06插拔确认,使用同一完整FQBN;串口115200
python3 tests/energy_test.py

参考:[1] GxEPD2 1.6.9具体面板声明:partial能力与只读快刷常量[2] GxEPD2具体驱动:全刷、局刷、powerOff与hibernate[6] DevKitC1 v1.1固定官方提交:供电与J1排针

05 / 刷新与电量实验

先建立基线,最多五次局刷后重做全窗口

首次启动只接受f;成功后p才开放。两次刷新开始至少间隔180秒,这是本教程保守实验节奏,不是厂商承诺的最小间隔。满五次局刷后再要求f;五次上限是实验控制,不表示前五次必无残影,任何一次不可接受都提前停止并重建基线。

局部窗口在rotation1坐标为x8/y32/w112/h64,包含所有VALUE与方块变化;其他图案不变。旋转后控制器原生x方向按8像素对齐;该窗口y/h已为8的倍数。不要把两个相近型号的窗口、LUT和缓冲规则混用。

本例使用4000B一页RAM缓冲,GxEPD2_BW nextPage在快局刷后调用writeImageAgain同步旧图像RAM。刷新后powerOff,不重置控制器,软件只在该次连续供电会话保留基线。h会hibernate并作废基线,下一次只允许f;RESET、掉电后同样重建,旧画面仍在不代表控制器有可用差分基线。

代码 / 配置
// ---- RefreshPlan.h ----
// SPDX-License-Identifier: MIT
#pragma once
#include <stdint.h>
struct RefreshPlan {
 enum Decision{ALLOW,STOPPED,WAIT_INTERVAL,NEED_FULL};
 static constexpr uint64_t intervalMs=180000;
 bool baseline=false,attempted=false,stopped=false;unsigned partials=0;uint64_t last_attempt=0;
 Decision check(uint64_t now,bool partial)const{
  if(stopped)return STOPPED;
  if(partial&&(!baseline||partials>=5))return NEED_FULL;
  if(attempted&&(now<last_attempt||now-last_attempt<intervalMs))return WAIT_INTERVAL;
  return ALLOW;
 }
 void begin(uint64_t now){last_attempt=now;attempted=true;}
 void success(bool partial){if(partial)++partials;else{baseline=true;partials=0;}}
 void hibernated(){baseline=false;partials=0;}
 void fault(){stopped=true;baseline=false;}
};
struct CallBudget {
 uint64_t started=0;bool faulted=false;
 void begin(uint64_t now){started=now;faulted=false;}
 void observe(uint64_t now,bool busy){if(busy&&now>=started&&now-started>=8000000)faulted=true;}
 bool finish(uint64_t now,bool busy){observe(now,busy);if(now<started||now-started>=8000000||busy)faulted=true;return !faulted;}
};

// ---- ButtonCommand.h ----
// SPDX-License-Identifier: MIT
#pragma once
#include <stdint.h>
struct ButtonCommand {
 uint8_t candidate=0,stable=0,armed=0;uint64_t changed=0;
 char step(uint64_t now,uint8_t mask){
  mask&=3;
  if(mask!=candidate){candidate=mask;changed=now;}
  if(now<changed||now-changed<50||candidate==stable)return 0;
  uint8_t previous=stable;stable=candidate;
  if(stable==0){uint8_t action=armed;armed=0;return action==1?'f':action==2?'p':0;}
  if(previous==0&&(stable==1||stable==2))armed=stable;
  else armed=0; // Both buttons or an overlapping change cancels; never guess.
  return 0;
 }
};

参考:[1] GxEPD2 1.6.9具体面板声明:partial能力与只读快刷常量[2] GxEPD2具体驱动:全刷、局刷、powerOff与hibernate[3] GxEPD2_BW:旋转窗口、字节对齐与旧图像RAM同步[9] ESP-IDF5.5.5完整启动微秒接口

06 / 刷新与电量实验

完整入口与调用预算分别保留

RefreshPlan和ButtonCommand直接进入真实固件;完整入口下方给出。所有长期时间从esp_timer_get_time完整微秒值转换,不把已经截断的millis扩成64位。软件序号与手工局刷计数分别保存;任何错误停止后断电查线,不继续轰炸面板。

开始前BUSY高最多等2秒,仍高就停止且不发显示命令。库自己的busyCallback仅观察,不能取消正在执行的库调用。8秒预算是调用返回后的故障判定,观察到超限/BUSY仍高就停止;它不是硬件超时中断或“最多8秒返回”的保证。

日志refresh_call_us包住初始化(如有)、绘图/页面循环和库等待,through_poweroff_us再包含powerOff返回;两者都不是光学稳定时间。每行保留mode/value/序号,第一轮有初始化,应单列初次和已初始化组,不能偷偷删除较慢数据。

代码 / 配置
// ---- esp32-epaper-refresh-budget.ino ----
// SPDX-License-Identifier: MIT
#include <Arduino.h>
#include <SPI.h>
#include <GxEPD2_BW.h>
#include <gdey/GxEPD2_213_GDEY0213B74.h>
#include "esp_timer.h"
#include "RefreshPlan.h"
#include "ButtonCommand.h"
constexpr int PIN_SCK=12,PIN_MOSI=11,PIN_CS=5,PIN_DC=4,PIN_RST=6,PIN_BUSY=10;
SPIClass epdSPI(FSPI);
GxEPD2_BW<GxEPD2_213_GDEY0213B74,GxEPD2_213_GDEY0213B74::HEIGHT>
 display(GxEPD2_213_GDEY0213B74(PIN_CS,PIN_DC,PIN_RST,PIN_BUSY));
RefreshPlan plan;CallBudget budget;ButtonCommand buttons;
constexpr int FULL_BUTTON=17,PARTIAL_BUTTON=18;
bool initialized=false,value=false;uint32_t sequence=0;
static uint64_t now_us(){return static_cast<uint64_t>(esp_timer_get_time());}
static bool is_busy(){return digitalRead(PIN_BUSY)==HIGH;}
void observe_busy(const void*){budget.observe(now_us(),is_busy());delay(1);}
bool preflight(){
 uint64_t start=now_us();while(is_busy()&&now_us()-start<2000000)delay(1);
 if(is_busy()){plan.fault();Serial.println("STOP BUSY HIGH before command; no display call started");return false;}
 return true;
}
void draw_test(){
 display.fillScreen(GxEPD_WHITE);display.setTextColor(GxEPD_BLACK);display.setTextSize(1);
 display.drawRect(0,0,display.width(),display.height(),GxEPD_BLACK);
 display.setCursor(8,16);display.print("GDEY0213B74 B/W");
 display.drawRect(8,32,112,64,GxEPD_BLACK);
 if(value)display.fillRect(16,40,40,40,GxEPD_BLACK);
 else display.drawRect(16,40,40,40,GxEPD_BLACK);
 display.setCursor(65,56);display.print(value?"VALUE 1":"VALUE 0");
 display.setCursor(132,48);display.print("STATIC");
 display.setCursor(132,65);display.print("BORDER");
 display.setCursor(8,113);display.print("Optics/current: NOT VERIFIED");
}
void refresh_test(char command){
 bool partial=command=='p';uint64_t requested=now_us();
 auto decision=plan.check(requested/1000,partial);
 if(decision!=RefreshPlan::ALLOW){Serial.printf("REJECT decision=%u: 1=stopped 2=wait180s 3=full baseline required\n",unsigned(decision));return;}
 if(!preflight())return;
 plan.begin(now_us()/1000);++sequence;value=!value;
 uint64_t started=now_us();budget.begin(started);
 /* Re-init only once after boot. Do not reset between full baseline and partials. */
 if(!initialized){display.init(115200,true,10,false);display.setRotation(1);initialized=true;}
 // This fixed driver uses compile-time fast-full waveform; never mutate it.
 if(partial)display.setPartialWindow(8,32,112,64);else display.setFullWindow();
 display.firstPage();do{draw_test();}while(display.nextPage());
 uint64_t refresh_return=now_us();
 if(!budget.finish(refresh_return,is_busy())){
  plan.fault();Serial.println("STOP call returned with BUSY/budget fault; cannot cancel inside library; power off and inspect");return;
 }
 /* powerOff retains our host baseline assumption only in this uninterrupted
    powered session. hibernate/reset/power loss always invalidates it below. */
 budget.begin(now_us());display.powerOff();uint64_t end=now_us();
 if(!budget.finish(end,is_busy())){plan.fault();Serial.println("STOP powerOff returned with BUSY/budget fault");return;}
 plan.success(partial);
 Serial.printf("D09,%lu,%c,%u,%llu,%llu,%llu,%u,%u\n",(unsigned long)sequence,command,unsigned(value),
  (unsigned long long)started,(unsigned long long)(refresh_return-started),
  (unsigned long long)(end-started),unsigned(is_busy()),plan.partials);
 Serial.println("CALL_RETURNED_BUSY_LOW only; record external photo/temperature/current. MCU remains awake.");
}
void hibernate_test(){
 if(plan.stopped||!initialized||!preflight())return;
 budget.begin(now_us());display.hibernate();
 if(!budget.finish(now_us(),is_busy())){plan.fault();Serial.println("STOP hibernate returned fault");return;}
 plan.hibernated();initialized=false;
 Serial.println("HIBERNATED: baseline invalid; next image must use f, not p. Board remains powered.");
}
void setup(){
 Serial.begin(115200);delay(300);pinMode(PIN_BUSY,INPUT);
 pinMode(FULL_BUTTON,INPUT);pinMode(PARTIAL_BUTTON,INPUT); // each external10k to3V3, SPST to GND
 digitalWrite(PIN_CS,HIGH);pinMode(PIN_CS,OUTPUT);
 if(!epdSPI.begin(PIN_SCK,-1,PIN_MOSI,PIN_CS)){plan.fault();Serial.println("STOP SPI begin failed");return;}
 display.epd2.selectSPI(epdSPI,SPISettings(4000000,MSBFIRST,SPI_MODE0));display.epd2.setBusyCallback(observe_busy);
 Serial.println("D09 confirmed GDEY0213B74 + DESPI-C02 P3=2.2ohm only; no automatic refresh");
 Serial.println("f=full-window fast waveform, p=partial(max5), h=hibernate+invalidate baseline; wait180s each refresh");
 Serial.println("Physical17=full / 18=partial, release after >=100ms; external10k pullups, both USB unplugged for metering");
 Serial.println("kind,seq,mode,value,start_us,refresh_call_us,through_poweroff_us,busy_after,partials_since_full");
}
void loop(){
 if(Serial.available()){char c=Serial.read();if(c=='f'||c=='p')refresh_test(c);else if(c=='h')hibernate_test();}
 uint8_t mask=(digitalRead(FULL_BUTTON)==LOW?1:0)|(digitalRead(PARTIAL_BUTTON)==LOW?2:0);
 char command=buttons.step(now_us()/1000,mask);if(command)refresh_test(command);
 delay(1);
}

参考:[2] GxEPD2具体驱动:全刷、局刷、powerOff与hibernate[3] GxEPD2_BW:旋转窗口、字节对齐与旧图像RAM同步[9] ESP-IDF5.5.5完整启动微秒接口

07 / 刷新与电量实验

USB功能组先记录调用,再固定照片条件

仅USB-to-UART供电,PPK2输出与所有外部电源断开。先f确认黑白边框与静态文字,拍一张全窗口基线;等待180秒后按p,在同一机位/曝光/光线拍改变区和未改变区。连续做到最多五次p,再f,至少重复三轮。温度用独立温度计记录,建议先在接近25°C且稳定的环境比较;不是替器件扩大工作范围。

每次填写面板批次、室温、固件SHA、上一次模式/间隔、软件调用时间与照片路径。可把残影主观分为0不可辨、1放大可辨、2正常观看可辨、3影响阅读;这是自定义有序等级,不能相加当物理灰度量。用同一参考全刷照片和盲顺序复看,保留原图,不用后期提对比的照片伪装原结果。

若需要光学刷新耗时,用包含按钮动作与屏幕的定帧录像,从已定义的触发动作到最后一次可辨画面变化逐帧记录,并报告帧率与量化误差。软件调用返回和人眼稳定可能不同,两列分开。

运行组操作结果
USB功能f→等待→p×最多5→f,保留原日志/照片pending
PPK2独立供电同一固件与图案,改用实体按钮,保留电流轨迹/照片pending
休眠恢复串口h后p被拒绝,等待后f重建逻辑检查通过;实体pending

参考:[2] GxEPD2具体驱动:全刷、局刷、powerOff与hibernate[4] Good Display该具体面板规格:温度条件、三种厂商标称刷新方式

08 / 刷新与电量实验

测整板电量时,拔掉两条开发板USB

本组是另一条明确记录条件的测量运行,不能把USB功能组的软件耗时悄悄写成同一时刻的PPK2数据。全部断电,拔开发板USB-to-UART和原生USB,不接供电串口、调试器、另一屏幕电源或电池。PPK2自身DATA/POWER接电脑允许;它不是目标板的USB旁路。

按L02先验极性/量程:PPK2 Source Meter、5.0V,VOUT到开发板J1-21(5V),GND到J1-22,Source Meter不用VIN。面板仍接板端3V3,不能把5V移到屏幕VCC。Source模式DUT上限600mA;另一1A是Ampere连续模式,不能混用。DUT可能超过400mA时按厂家给PPK2 POWER ONLY额外至少1A输入供电,仍不能越过600mA输出限制。

先核板端5V/3V3及仪器削顶/掉压/复位;超限样本作无效,不用它估峰值。用GPIO17/18按钮做同样f/p图案,手工记录刷新历史与照片;本组无开发板USB实时串口,不为补日志重新插线。软件时间若不可取得就留空,光学时间用该组录像记录。电流轨迹选含操作前后各至少1秒静态平台的完整活动窗口,保证把刷新与powerOff尾部都纳入;另报告静态平台功耗,不把不同长度窗口的总电量直接当单次更新比较。

记录PPK2固件/应用实际版本、模式、采样率、量程、电压、线材与窗口判定方式,保持两种刷新相同窗口规则、图案、温度和重复次数。本例得到的是整板输入,包含LDO/常亮灯/USB桥接/MCU/屏幕;不等同裸面板标称功率,也不直接推电池续航。

做到这里,你应该看到
串口功能组与脱USB测流组标记清楚,无并行供电路径。

参考:[4] Good Display该具体面板规格:温度条件、三种厂商标称刷新方式[6] DevKitC1 v1.1固定官方提交:供电与J1排针[7] PPK2:Source600mA与Ampere1A两个模式不同[8] PPK2:0.8–5.0V与可能大于400mA时的额外输入电源

09 / 刷新与电量实验

显式转换列名与单位,再对完整窗口积分

current-trace-template.csv只有t_s/voltage_V/current_mA表头,不声称它是PPK2的原生导出格式。先保存仪器原工程与原始CSV,查看实际列名/单位,再用normalize_trace.py明确选择时间与电流单位;电压可取逐样实测列,或明确使用板端实测常量近似,不能把设定5.0V冒充每个采样点电压。

integrate_energy.py流式读取,要求严格递增时间、有限非负电流、正电压和完整覆盖窗口,不外推缺失开始/结束。窗口内相邻样本间隔超过声明max-gap就拒绝;不能放大上限掩盖丢掉的峰。小负值先查零偏,不偷偷截0。边界在两个样本之间时,对采样电流及采样功率分别线性插值再梯形积分。

Q[mC]=∫I[mA]dt[s],E[mJ]=∫V·I[mA]dt[s],mWh=E[mJ]/3600。不要把mJ误当J,也不要从输入5V的电量直接除3.7V电池容量。九组合成算术/无效输入检查通过,只证明计算工具,不提供本站电量。

代码 / 配置
# 下面列名、单位、电压/窗口都换成自己真实原始记录
python3 tools/normalize_trace.py raw.csv trace.csv --time-column REPLACE_TIME_COLUMN --time-unit us --current-column REPLACE_CURRENT_COLUMN --current-unit uA --voltage-column REPLACE_MEASURED_VOLTAGE_COLUMN
python3 tools/integrate_energy.py trace.csv --start REPLACE_START_S --end REPLACE_END_S --max-gap REPLACE_MAX_GAP_S
python3 tests/energy_test.py

参考:[7] PPK2:Source600mA与Ampere1A两个模式不同[8] PPK2:0.8–5.0V与可能大于400mA时的额外输入电源

10 / 刷新与电量实验

积分工具保留单位和缺样判定

完整转换器与积分器都在下载包,原始CSV输入不被覆盖;读取失败返回非零,不将部分输出当有效测量。下面给出实际积分源,合成fixture与真实观测模板分别命名。

代码 / 配置
# ---- tools/integrate_energy.py ----
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""Integrate nonnegative sampled current and power over one covered time window.
Input CSV: t_s,voltage_V,current_mA. Linear interpolation of sampled P and I.
Synthetic tests prove arithmetic only, never an ESP32 measurement.
"""
import argparse,csv,json,math

def integrate(path,start,end,max_gap):
 if not all(math.isfinite(x) for x in (start,end,max_gap)) or start>=end or max_gap<=0:
  raise ValueError('finite start<end and positive max_gap required')
 previous=None;charge=energy=covered=0.0;rows=0
 with open(path,newline='') as stream:
  reader=csv.DictReader(stream)
  if not reader.fieldnames or not {'t_s','voltage_V','current_mA'}<=set(reader.fieldnames):raise ValueError('missing explicit units/columns')
  for row in reader:
   t,v,i=(float(row[k]) for k in ('t_s','voltage_V','current_mA'))
   if not all(math.isfinite(x) for x in (t,v,i)) or v<=0 or i<0:raise ValueError('nonfinite/negative current or nonpositive voltage; inspect zero offset')
   rows+=1
   if previous is not None:
    a,av,ai=previous;dt=t-a
    if dt<=0:raise ValueError('timestamps must strictly increase')
    left=max(a,start);right=min(t,end)
    if right>left:
     if dt>max_gap+1e-12:raise ValueError('sample gap exceeds declared limit in window')
     f0=(left-a)/dt;f1=(right-a)/dt
     i0=ai+(i-ai)*f0;i1=ai+(i-ai)*f1
     p0=av*ai+(v*i-av*ai)*f0;p1=av*ai+(v*i-av*ai)*f1
     charge+=(i0+i1)*0.5*(right-left);energy+=(p0+p1)*0.5*(right-left);covered+=right-left
   previous=(t,v,i)
 if rows<2 or not math.isclose(covered,end-start,rel_tol=1e-8,abs_tol=1e-9):raise ValueError('window not fully bracketed by samples; no extrapolation')
 return {'start_s':start,'end_s':end,'duration_s':end-start,'input_rows':rows,'max_gap_s':max_gap,'charge_mC':charge,'energy_mJ':energy,'energy_mWh':energy/3600,'mean_current_mA':charge/(end-start),'method':'trapezoid of sampled nonnegative I and V*I; interpolate cut endpoints; includes whole measured input'}

if __name__=='__main__':
 parser=argparse.ArgumentParser();parser.add_argument('csv');parser.add_argument('--start',required=True,type=float);parser.add_argument('--end',required=True,type=float);parser.add_argument('--max-gap',required=True,type=float);args=parser.parse_args()
 print(json.dumps(integrate(args.csv,args.start,args.end,args.max_gap),indent=2,allow_nan=False))

参考:[7] PPK2:Source600mA与Ampere1A两个模式不同[8] PPK2:0.8–5.0V与可能大于400mA时的额外输入电源

11 / 刷新与电量实验

用自己的结果决定更新预算

同时比较重复实验的残影等级、调用/光学时间、完整活动窗口电量和静态平台;先满足可读性再看电量。若局刷不可接受、局部内容越界或供电不稳,即使电量较低也不采用。本文没有一组结果,所以没有“每N分钟最佳”的本站结论。

第六次p无变化先看是否五次上限;刚启动/休眠后p被拒绝先做f;180秒内无动作先看间隔门,别反复连按。局刷错位先回查旋转和8像素寻址;BUSY故障先断电查FPC/P3/电源/短线。库超时并不证明面板损坏,保留原错误与原图。

恢复只用已保存的固件与正常BOOT/RESET刷写;不修改面板波形、不提高VCC、不关掉欠压保护。GxEPD2为GPL-3.0,源码应用/文档的MIT不覆盖库或链接固件分发义务,第三方许可单列。全部实板、残影、PPK2与调用时间结果仍pending。

参考:[1] GxEPD2 1.6.9具体面板声明:partial能力与只读快刷常量[2] GxEPD2具体驱动:全刷、局刷、powerOff与hibernate[3] GxEPD2_BW:旋转窗口、字节对齐与旧图像RAM同步[4] Good Display该具体面板规格:温度条件、三种厂商标称刷新方式[5] DESPI-C02厂家产品:与本例已核P3=2.2Ω修订分别确认[6] DevKitC1 v1.1固定官方提交:供电与J1排针

BACK TO THE SOURCE

资料来源与版本说明

保留D09 ID/slug,原刷新预算目标落实为可执行实验方法;首次把只读驱动常量当运行参数的失败源码/日志保留,最终准确比较该驱动固定全窗口快刷与局刷,不声称改变厂商波形或获得本站测量。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。

  1. GxEPD2 1.6.9具体面板声明:partial能力与只读快刷常量
  2. GxEPD2具体驱动:全刷、局刷、powerOff与hibernate
  3. GxEPD2_BW:旋转窗口、字节对齐与旧图像RAM同步
  4. Good Display该具体面板规格:温度条件、三种厂商标称刷新方式
  5. DESPI-C02厂家产品:与本例已核P3=2.2Ω修订分别确认
  6. DevKitC1 v1.1固定官方提交:供电与J1排针
  7. PPK2:Source600mA与Ampere1A两个模式不同
  8. PPK2:0.8–5.0V与可能大于400mA时的额外输入电源
  9. ESP-IDF5.5.5完整启动微秒接口