摄像头实现记录
apps/iot 的 GC2145 采集链路与 ST7789 面板接线,在真实硬件上摸出来的判断。AI 产出,未经人类审阅,见上级说明。
每条判断标注依据:[测试] 有对应单元测试,[实测] 有硬件测量并附复现方式,[据称] 未复现、仅留痕。
复现方式统一为:从 apps/iot/ 烧录 camera-probe,看串口里 [PROBE] 与 [CAM] 开头的行。探针每 2 秒打印 sensor exposure、blanking h/v(page 0 的 0x05–0x08 实测值)与该窗口的 KB/s —— 帧率相关的判断都建立在这三行上。
这块板上的传感器是 GC2145,立创 wiki 说的 GC0308 是错的
[实测] 这是本文件记录的第一条硬件事实,也是整条摄像头链路第一次在实物上跑起来。
厂商文档(立创·实战派 ESP32-S3 摄像头章节)写明该板出厂摄像头是 GC0308;仓库驱动的是 GC2145。两者不只是型号名不同:
| GC2145(代码驱动) | GC0308(wiki 声称) | |
|---|---|---|
| 芯片 ID 寄存器 | page 2 的 0xf0/0xf1 | page 0 的 0x00,值 0x9b |
| 阵列 | 1600×1200 | 640×480 |
| 页选择机制 | 不同 | 不同 |
所以「代码错了」与「wiki 错了」不会表现得同样:若板上真是 GC0308,仓库那个读 page 2 的 read_id() 只会读回垃圾,而不会读出 0x0308 —— 而且 lckfb_szpi_esp32s3/camera.rs 对这个 ID 只打日志、不校验,属于本仓库反复出现的那一类静默失败。
实测(moon run iot:probe-camera-s3,烧到 S3):
[00:00:00.850] [CAM] sensor id 0x2145
[00:00:00.851] [CAM] sensor reports 320x240 out, read 1616x1208 at 0,0 ratio 5:5, format 0x06
[00:00:02.892] [PROBE] sensor exposure 486 lines
[00:00:02.896] [PROBE] painted 10, 9 new in 2000 ms = 691 KB/s of new sensor data
ID 是 0x2145,读窗口 1616×1208、5:5 抽取 —— GC2145 的形状,不是 GC0308 的 640×480。而且 691 KB/s 的新数据说明它在真出帧,不是在回读寄存器。代码是对的,wiki 是错的。
这条同时说明:一个从未在硬件上跑过的驱动和一个厂商文档之间的分歧,只有实测能裁决,而这次代码赢了。它也说明 read_id() 只打日志不校验是个真实的缺口 —— 见 aec.md 里同一类问题的另一次。
视野是算术,不是寄存器
[实测] 视野 = 输出像素宽 ÷ 阵列宽,与任何寄存器的取值无关。 一个 320 像素宽的输出,最多只能覆盖 1600 宽阵列的 20%。改 ratio、改 bins、改标量、改边距,都不改变这个数。这是"输出多少像素 = 能看多宽"的恒等式。
[实测] ratio 5 作用在整个 1600×1200 阵列上,而 1600÷5 和 1200÷5 都是整除,所以 320×240 就是抽取后的完整图像,out_win 一个像素都不裁 —— 这是满视野。
[实测] 由此推翻了"换个更大的输出能拓宽视野":800×600 在 ratio 2 下同样整除、同样是满视野,只是多 6.25 倍像素需要缩小。它被实现过(流式捕获 + 5:2 box 降采样),视野没有变宽,代价是引入三个新故障。驱动里因此没有 800 宽的路径。
[测试] 整除性写成了编译期断言(ARRAY_WIDTH / EXTRACT_RATIO == POWER_ON_OUT_WIDTH),ratio 一旦不再整除就编译不过。
bins 决定视野,代价是帧率
[实测] 0x9b–0xa2(Sub_row_N1…Sub_col_N4)指定每个抽取 bin 内保留哪几行哪几列。填 0x00 是对整个 bin 求平均;填 0x01/0x23 是每个 bin 只取一个固定偏移。
实测两者差别极大:
| bins | 视野 | 帧率 | DMA |
|---|---|---|---|
0x01/0x23 | 约 1/5,明显放大 | 10 fps | 1558 KB/s |
0x00 | 整个阵列 | 4.5 fps | 691 KB/s |
满视野和帧率在这颗传感器上是同一个开关的两面。 esp32-camera 自己的表格注释早就写着:A smaller ratio brings a larger view, but it reduces the frame rate。本仓库写 0x00。
[据称] 为什么平均一个 5×5 的 bin 比取一个点贵一倍,无从解释 —— 缺少传感器 DSP 的资料。
读出窗口的边距从哪里来
[实测] 0x09–0x10 是读出窗口的起点与尺寸,器件把边距计入自己的行时序,所以程序里写的尺寸要加上它。取值是 纵向 +8、横向 +16,来自 esp32-camera 的 set_framesize:
write_reg(sensor->slv_addr, 0x0d, H8(win_h + 8));
write_reg(sensor->slv_addr, 0x0f, L8(win_w + 16));
[实测] 曾用 +16,理由是数据手册给这两个寄存器的复位值是 1216 而非 1208(即 1200 + 16)。那个复位值属于器件坐在自己默认窗口上的状态,不是被程序设成 320×240 的状态;官方驱动是从它刚选定的窗口算出边距的。改回 +8 后视野正常。
[实测] 横向用 16 纵向用 8 并不对称,这不是笔误 —— 它就是官方驱动写进 QVGA 的两个值。
各 ratio 的实测吞吐与细节
[实测] 0x99 的抽取比写入后,其效果只能从 DMA 收到多少字节反推:比值生效则字节率大致按比例下降,不生效则不动。逐档实测:
| ratio | 读出窗口 | DMA | 每行不同取值数 |
|---|---|---|---|
| 5 | 1600×1200 | 1558 KB/s | 186 |
| 3 | 976×736 | 3444 KB/s | — |
| 2 | 656×496 | 3474 KB/s | — |
| 1 | 336×256 | 4641 KB/s | 0(无帧) |
[实测] ratio 1 完全不出帧;ratio 3 和 4 能出帧但每行只有 ratio 5 约一半的不同取值 —— 帧是对的,像素是欠采样的。ratio 5 是唯一读满整个阵列的档。
[据称] 为什么 ratio 1 不出帧、而 3 和 4 欠采样,寄存器层面无从解释。
标量模式:帧率翻倍,视野变窄
[实测] 0xfd(Scalar mode,bit 0 行标量、bit 1 列标量)打开后帧率从 5 fps 涨到 10 fps,其他寄存器不变。它把"逐行跳过再对 bin 求平均"换成了一次重采样。
[实测] 但画面比它替代的抽取路径更窄,不是更宽。 0x95–0x98 指认的是读出窗口上的一个矩形,无论器件怎么走到那里;一个 320 宽的矩形压在 1616 宽的读出窗口上,两种模式都是阵列的 1/5。
[实测] 所有参考驱动都把它和 out_win 配对使用,仿佛它能让输出尺寸独立于读出窗口(sunxi 的 sensor_svga_regs、rockchip 的 sensor_preview_data、Linux mainline 的 gc2145_mode_640_480_regs,都写 0xfd = 0x01,而唯一的全分辨率模式 1600x1200 写 0x00)。
因此本驱动不写 0xfd,让它留在上电表的 0x00,但读回(WindowGeometry::scalar)以便确认它确实是关的。
0x9a:两个值无可测差异
[实测] 0x06(数据手册复位值,Linux mainline 与 ST 640×480 模式都写它)与 0x0e(esp32-camera 上电表的值,set_framesize 从不碰它)在视野、字节率(2 秒窗口内 691/768/839 KB/s,两者相同)、帧率上全部一致。唯一差别是 bit 3(Y 插值)。
本驱动取数据手册的 0x06。
0x95–0x98 的单位仍未确认
[据称] 数据手册只说这四个寄存器是 Out window height/width,不说它们是传感器像素还是输出像素,而三份参考驱动给出相反答案:Zephyr 和 ArduCAM 写输出尺寸,传感器自己的上电表与 Linux 上唯一被验证可用的 640x480 模式则与读出尺寸一致。
[实测] 两者做过一次区分实验,结果无法区分:out_win 写 320×240 与写 1600×1200,DMA 字节率完全相同(都落在 out_win 读回值上,速率不变)。所以这个参数被从 API 里移除了,只保留"写输出尺寸"这一种行为,并在代码里注明该单位未确认。
[据称] 理论上的区分办法是 EofMode::VsyncSignal:让边界由器件给出而不是由字节数给出。但它每帧产生一个 EOF,而这里的描述符是一帧的四十分之一,会要求首个描述符装下整帧并溢出。
环形描述符链此芯片不走
[实测] 描述符链做成环形(最后一个指回第一个)可以彻底消除 re-arm,从而消除 re-arm 空窗里丢字节的问题。但实测 28 KB/s,而线性链是 645 KB/s —— 外设不走完第一圈就停了,ring 只能填满一次,此后全靠 stall 路径重新 arm 才有进展。已撤回,保留线性链。
[实测] 线性链的空窗确实在丢字节:同一传感器配置下,ring 61 KB 时 DMA 只有 645 KB/s,ring 恢复到一帧(153 KB)时是 1558 KB/s。丢一个字节,它之后每一行都错开一行,画面表现为水平条带("百叶窗")在移动。 所以 ring 必须是整帧大小;当前 320×240 整帧是 153,600 B,camera-probe 每 2 s 报 10–11 次 re-arm(约 5.2 fps)。底带曾用 76,800 B 一帧,实测 9.5–10 fps、每 2 s 19–20 次 re-arm;那是已撤回的方案。
内部 DRAM 不是内存不够,是 DMA 打不进去
[实测] ring 必须在内部 DRAM。ESP32-S3 有 8 MB PSRAM,放这块 150 KB 的 arena 绰绰有余,但:
- Espressif 自己的
ll_cam.c把接收描述符与数据 burst 位放在//internal SRAM only之下,两者对 PSRAM 都保持清零; - esp-hal 无法表达这个区分:
set_descr_burst_mode在每次 receive 时都以字面量true调用,BurstConfig::is_burst_enabled在 PSRAM 路径上恒为真。
所以 PSRAM ring 永远停在 Espressif 明确拒绝使用的配置上,器件往里写的数据是乱的。ES32-S3 相机 issue tracker 对同一字段的描述是:the artifacts you see are exactly missing bytes in the frame。没有任何设置能到达官方那个状态。
[实测] 描述符列表也不能指向 PSRAM。
[实测] 内部 DRAM 的对齐约束是 32 字节 cache line(DMA_ALIGNMENT_BYTES),不是外部 burst 块。
环的 150 KB 曾挂在 feature 组成上,现在不挂了
[实测] 产品里这 150 KB 现在是面板帧缓冲本身。FRAME_ARENA(原 FRAME_RING)无条件在 .bss 里,camera 已并入板 feature,--gc-sections 不再有机会把它丢掉 —— 于是 150 KB 从"取决于一次链接期 GC"变成链接期确定事实。cargo check 不链接、check-s3 看不见,仍然只有 moon run iot:image / build-s3 那条路能证伪它。
[实测] 省下的是每帧 150 KB 的 memcpy(320×240 时代实测帧率 5.5 fps,约 4.5 ms/次),而不是 DRAM:面板原来那份 Vec 变成这个静态 arena,两者尺寸相同,.bss 净增 14,336 B。DRAM 上"合一"与"两份并存"等价,差别在 CPU 时间与所有权耦合。
相机占满整屏,读数反色叠在画面上
[测试](编译期断言)Camera 页的相机画面就是面板整帧:dvp::FRAME_WIDTH/FRAME_HEIGHT 与面板同为 320×240,dvp::RING_BYTES 等于 FRAME_ARENA 的 153,600 B。板模块断言宽、高、环字节数都与面板一致,所以画面铺满玻璃;页面不再保留顶部黑底,也不存在「读数行与相机行不相交」这条约束。FRAME_ARENA 仍是面板帧缓冲本身,相机环不额外占内存。
[测试](编译期断言)传感器按 set_window(320, 240, 5) 配置:读窗 1600×1200、5:5 抽取,寄存器读回 1616×1208(含边距),输出 320×240。描述符按整帧切成 40 段,每段 3,840 B;每段都是 32 B 对齐的整数倍。
[实测] 整屏帧率:camera-probe 的 [PROBE] painted 每 2 s 报 10–11 个新帧、691–844 KB/s,即 5.0–5.5 fps;[PROBE] capture 同期报 10–11 次 re-arm,链有 40 个描述符;曝光仍为 486 lines。产品 IOT_BOOT_PAGE=camera 直入 Camera 页实测 11 repaints in 2119 ms(约 5.2 fps),stack peak 43224 B of 66360 B。复现:IOT_PORT=… moon run iot:flash-probe-camera-s3,读 [PROBE] painted … KB/s;产品读 [DISPLAY] Camera … repaints。
[实测] 画面已由人眼确认:整屏 320×240 传感器画面,顶部读数以反色叠在画面上(CAMERA/WIN/OUT/RD 与 FRM/RM/MOD/FP),没有黑色页眉。FPS 徽标回到与其他页相同的右下角 FPS_Y = 232,不再占用第 4 行。
[测试] 持有者规则没有变:Camera 页在时面板拒绝 CPU 填充(FrameOwner::Camera,五条单测覆盖)。相机路径在 advance 报帧后、下一次 re-arm 前把读数写进同一帧并整帧下发;此时 DMA 空闲,CPU 与相机不会同时写这些字节。指纹在写任何字形之前对整帧折叠,所以读数本身不会让指纹每帧变化。
底带方案:实现过、测过、已撤回
[实测] 曾把相机缩到面板底部 120 行(320×120,76,800 B,20 个描述符),顶部 120 行留给黑底和读数。传感器按 set_window(320, 120, 5) 读 1600×600,垂直居中;这不是旧画面的裁剪,而是阵列上更宽的一条。
[实测] 底带帧率:camera-probe 的 [PROBE] capture 每 2 s 增加 19–20 帧(19 → 39 → 59 → 78),即 9.5–10 fps;同窗 [PROBE] painted 报 18–20 个新帧、691–768 KB/s。产品直入 Camera 页的 [DISPLAY] 稳定后同量级(20 repaints / 2020–2040 ms,约 9.9 fps;首个窗口 17 / 2077 ms 含启动)。字节率与整屏(691–844 KB/s)几乎相同,帧率翻倍来自每帧从 153,600 B 降到 76,800 B;进页空窗也从约 192 ms 降到约 100–105 ms(由实测帧率推得,未单独计时)。
[据称] 底带画面质量未及人眼确认就撤回了:只测了帧率与几何,不把「画面正常」写成实测。
[实测] 撤回原因是产品要整屏画面:底带只剩 8:3 的一条,且把 FPS 挤到第 4 行。恢复整屏后画面回到完整 4:3,读数叠在画面上,FPS 回到右下角。底带不是当前配置,上面的数字只作为「帧大小决定帧率」的对照。
描述符的 TX 列表吃掉过主栈,整屏因此起不来
[实测] 整屏把每帧从 76,800 B 增到 153,600 B 后,产品在扬声器 bring-up 处挂死。根因不是 DRAM 或 DMA,而是 dma_descriptors_chunk_size! 的第二个参数:它是 TX 总大小,传 RING_BYTES 会额外静态分配一份用不到的 TX 描述符列表(484 B)。相机只收不发,这份静态量白占主栈区;整屏增长后主栈区从 66,360 B 掉到 65,848 B,扬声器初始化时主栈不够,启动挂死。
[实测] 改成三参数 dma_descriptors_chunk_size!(RING_BYTES, 0, CHUNK_BYTES) 后 TX 列表为 0,主栈区恢复 66,360 B,产品正常启动到 Camera 页;日志 stack peak 43224 B of 66360 B。这条和 RING_BYTES 一样是链接期事实:check-s3 只 type-check、不链接,看不见它,只有 build-s3 / iot:image 能证伪。
[测试] CHUNK_BYTES 保持 3,840:改成 7,680 会触发 esp-hal 的 chunk size must be <= 4095(描述符 size 字段 12 位),所以整帧只能切成 40 段,不能靠减少描述符来省静态量。
像素时钟不能改;帧率上限来自数据量,不来自曝光
[实测] PIXEL_CLOCK_HZ 固定 24 MHz,不是自由参数:改成 20 MHz 时传感器 PLL 不锁,此后持续输出帧结构完全正确但内容全是噪声的画面 —— 寄存器读回一切正常,肉眼看见的是坏点。任何寄存器读回都不会报告这件事。
[实测] 帧率不由 AEC 曝光锁死:camera-probe 测到 sensor exposure 486 lines,整帧读出约 1200 行,曝光低于窗口,没有锁住任何东西。rockchip 那组帧率档(0x27–0x2e)上电表写了,在 page 1 且写了两次(0x27 先 0x03 后 0x01,0x28–0x2c 先 0x96 后 0xe6,0x25 先 0x01 后 0x00,0x26 先 0x32 后 0xa2)。读曝光走 page 0 的 0x03/0x04,上电表值 0x04/0x62 = 1122 行。
[实测] 帧率由数据量定死:当前 320×240 整屏实测 691–844 KB/s,即 5.0–5.2 fps,而 153,600 B/帧正是这个数;已撤回的底带 76,800 B/帧实测 691–768 KB/s、9.5–10 fps —— 字节率没有变,帧率翻倍只是因为每帧小了一半。底带的帧率不是当前产品值。
[实测] 手册(CSP DataSheet V1.0,7.1.1)的公式 Ft = VB + Vt + 8 推出 11.9 fps(blanking v=46 + win_height=1208 = 1262 行,24 MHz / 1600 像素),与实测的 5.2 fps 差 2.29 倍——等效行时钟 ~10.5 MHz。消隐值与上电表一致,所以差距不在寄存器里:要么公式不适用这一版,要么 0xf7/0xf8(PLL 模式,上电表 0x1d/0x84,中途写过 0x83 又写回)使实际行时钟与 PIXEL_CLOCK_HZ 不一致。这一步未定位。
结论:想更快只能换更小的读出窗口(与视野同一开关)或换芯片。不存在与视野正交的寄存器旋钮——手册把垂直消隐指为帧率控制,而实测消隐与上电表一致、帧率仍低 2.29 倍,说明改它不会换来更快。
帧缓冲的持有者是一个值,不是两个标志
[测试] 面板与摄像头共用一块帧缓冲(153,600 B),谁在写它必须只有一个持有者。这件事一度被记成两个标志:「摄像头是否在跑」+「CPU 有没有待画颜色」,于是有两种布尔组合、其中一种在领域里不成立 —— 摄像头在跑且没有颜色待画。
[测试] 那一组合出不来,是因为解除它的那条路被自己守着:consume() 只在「有颜色待画」时重绘,而释放摄像头的是 paint();进 Camera 页时颜色被清成「没有」,于是离开页面时唯一能释放摄像头的调用被自己的守卫挡住——标志永远为真,每次填充都被拒,屏幕停在最后一帧摄像画面上,没有任何一列 tick 能走出来。
[测试] 修复是 FrameOwner(iot-core::drivers::camera):NeedsPaint / Painted { fill, color } / Camera 三个变体各对应一种合法状态,第四种组合不再可表示。5 条单测覆盖转换,其中 a_released_camera_lets_the_next_fill_through 走的正是当年出不去的那条序列;host-smoke 的 HostLight 用同一个类型,断言离开 Camera 页后必须有填充到达面板。
[测试] 配套的是把「持有者」这份事实收回一处:LightRenderer 曾同时存 camera_page: bool 与 page: Option<DisplayPage> 记同一件事,删掉前者,step() 现算。面板侧的 owner 字段不按 camera feature 门控 —— 颜色的那一半也是没有摄像头的面板判断一次填充值不值得下发的依据,门控它等于把这个决定的一半落在持有它的值之外。
反复进出 Camera 页会把 DMA 链反复重建,而且建起来就堵
[实测] 复现:烧默认固件,连续快速点屏幕走完一圈(CYCLE),重复若干分钟,读串口里的 [CAM] no descriptor completed、[DISPLAY] Camera … repaints … held the executor 与 [INPUT] held … worst single hold。
启动直接落在 Camera 页(IOT_BOOT_PAGE=camera)与用触摸反复进出,是两个不同的成本:
| 启动直入 | 反复进出(183 次页切换) | |
|---|---|---|
| 重绘 / 窗口 | 11 / 2119 ms(5.2 fps) | 25–29 / 2100 ms |
| render 侧占用 | 188 ms = 8% | 626–754 ms = 29–36% |
| 链重装 | 0 | 140 次 |
表内「启动直入」就是当前整屏(11 / 2119 ms,约 5.2 fps);「反复进出」的 25–29 / 2100 ms 是 320×240 整屏时代测的,重绘占比与重装次数未重测。已撤回的底带方案直入曾测到 20 repaints / 2020–2040 ms(约 9.9 fps)。
那个 fps 数字不可直接采信:140 次重建期间每次都可能交出一帧被记成「新的」,所以「重绘变多」可能是重装churn 而不是真的出图更多。但 render 侧 8% → 29–36% 是实打实的。
[实测] 机制上说得通:离开 Camera 页时 demand 里的 camera 变 Off,Screen::consume 调 pause() 并交还缓冲;回来时 paint_camera 发现缓冲不属于相机,调 resume()——而 resume 是重建整条链(camera.receive(ring)),不是恢复。本轮 36 次进入 Camera 页、140 次重装,约每次进入后立刻停滞数次:进一次相机页,链建起来就堵。
[实测] 顺带一次单次持有越界:00:00:06 出现 worst single hold 31 ms(界 MAX_SINGLE_HOLD_MS = 30),而 Camera 页在 00:00:04.68 进入——进入相机页可以突破单次持有界,与重装churn 很可能是同一件事。
[据称] 「建起来就堵」的成因未定位。候选是 resume 重建后 ring 尚未被填满就被 check_stall 判停(STALL_MS 420 ms),或重建本身耗掉的窗口让第一次 take_latest 落空。未复现确认,仅留痕。
进 Camera 页有约 192 ms 看不见画面的空窗
[实测] 单缓冲下第一帧必须等 DMA 填满整帧才可能显示,所以从三击到看见画面之间有一段固定空窗。当前 320×240 整屏实测 11 repaints / 2.12 s(约 5.2 fps)⇒ 空窗约 192 ms;已撤回的 76,800 B 底带实测 9.5–10 fps ⇒ 空窗约 100–105 ms(由实测帧率推得,未单独计时),与上面「帧率死限」一节同源。
[实测] 现在的处置是过渡帧:进页面那一拍画 CAMERA WAIT / ----(stamp_camera 的 starting 分支),不留上一页的颜色。原实现留的是上一页那一帧冻在屏上,看起来像卡死;过渡帧把它读成"正在启动"。这是缓解,不是消除 —— 空窗的时长由传感器出一帧要多久决定,不由代码决定。
[实测] 过渡帧顺带跳过指纹计算:那一刻缓冲里是填充色而不是画面,frame_fingerprint 会报一个不存在的画面的指纹。
[实测] 空窗不能再靠帧率档缩短:原本打算用 page 1 的帧率档,实测证伪了那个前提(见「像素时钟不能改」)。帧率是这颗传感器在当前读出窗口下的物理上限。底带曾把空窗从约 192 ms 降到约 100 ms(帧大小减半),但该方案已撤回;当前整屏 Camera 页仍是约 5.2 fps 的预览,不是待优化的中间状态。
传感器能读回,面板不能:两条验证路线不是一条
[实测] GC2145 走 I²C,read_window_geometry() 读的是器件实际持有的寄存器,不是写下去的字节。所以「写没落地」在传感器这一侧立刻可见,不会变成"形状对、内容错"的画面晚点出现。
被否的验证方式:只读 0x95–0x98。那报的是被要求的尺寸,对读出窗口和抽取比一个字都不说——抽取比写错时画面被拉伸,却会通过"尺寸正确"这一关。
面板侧相反:没有 MISO,任何寄存器读回都是 0x00(详见下一节)。所以面板的 MADCTL 验证无法走读回路线,只能靠已知图案目视确认。
这条分界决定了代码里哪些地方必须写"read-back not write-echo"、哪些地方写不出来——gc2145.rs / lckfb_szpi_esp32s3/camera.rs / camera-probe.rs 三处说的都是同一件事,因此只在 read_window_geometry 的文档里写一次。
面板没有 MISO,寄存器读回全是 0
[实测] 这块板的面板 SPI 只有 SCK 和 MOSI,没有 MISO。任何寄存器读回都返回 0x00 —— 那正是一个悬空输入的样子。曾试图用读回 COLMOD 验证 MADCTL,读到的 0 就在刚写下的 0x55 旁边,于是这条验证路线被排除。窗口几何只能靠已知图案目视确认。
[实测] 面板是 320×240 的,MADCTL 用 MADCTL_LANDSCAPE(0x60),320×240 的缓冲原样送出、不做转置。控制器自身尺寸是面板的转置(240×320),两者不可混淆。
[实测] 弄错的症状是截断而不是旋转:地址窗口随 MADCTL 一起转置,于是 CASET 写 0..319 越过了 240 宽的 RAM,画面被切掉。那些给出 (0x60, 320, 240, 0, 0) 的方向表是给 240×320 面板的。
GC2145 没有官方广角支持
[据称] Espressif FAQ 在"支持广角吗"一节推荐 BF3005 和 OV2640,GC2145 不在其中。该传感器的低分辨率裁切在 espressif/esp32-camera issue #845 有人提过(场景正是广角镜头下的低分辨率),无官方回应。
结合上文"视野是算术"一条:视野窄若换传感器配置无效,剩下的就是镜头与传感器本身。