作者: Jet L

  • 【摄影】尼康F90X/N90S 卷片控制——半格/方格/N格改造

    一、改造目标与思路

    Nikon F90X / N90S 采用一套复杂的机制,来进行过片控制。

    6. 胶片进片(过片)

    1. 当参考开关(也就是我们监测的ref)断开四次(相当于四个画幅)时,自动装片操作停止。
    2. 参考开关始终保持在导通状态。该开关在胶片每进给约 19 mm(半幅)时断开一次。
    3. 当参考开关断开后,监测胶片进片光电遮断器(intp)的脉冲输出,进片停止定时控制开始。
    4. 胶片进片光电遮断器在进给一个画幅期间输出 114 个脉冲。
    5. 在单张拍摄(S)、低速连拍(CL)或高速连拍(CH)模式下,拍摄后序列电机和进片电机同时转动,以进给胶片并为快门上弦及反光镜复位。但如果因电池电量不足或低温导致反光镜动作和胶片进给时间超出规定值,则序列电机在快门帘完全走完后自动转动,随后进片电机再转动(此操作称为“方波模式”)。

    当电池电量恢复后,自动恢复正常操作。


    7. 胶卷末尾(卷片结束)

    如果胶片进片操作无法在规定时间内完成,进片电机停止,取景器和 LCD 面板上会显示警告指示,并伴有蜂鸣声。

    此时按下快门释放按钮,仅进片电机转动,并重复上述操作。

    但当计数器显示数字超过 37 时,进片电机不工作。当计数器显示数字低于 37 时,若胶片正常进给,警告指示消失。从下一画幅起可正常拍摄。


    8. 胶片倒片

    1. 即使未装入胶片时激活倒片操作,倒片电机也会转动 2 秒。
    2. 若启动倒片时因电池电量不足导致进片电机停止,则快门释放操作被锁定。此时,请尝试再次激活倒片操作。当胶片完全倒回暗盒后,快门释放锁定解除。

    在一般状态下,正常全画幅过片约 114 个 INTP (光遮断传感器,其与卷片齿轮耦合,INTK点亮,监测传感器通过齿孔旋转,在不断的遮断/打开之间获得脉冲,来计算过片量)脉冲。

    方画幅 / 半格改造需要在胶片走到约 5/8 位置(实测约 71 脉冲,可配置为 50 等)时强行停住,让机身认为本帧结束,同时不改动机身固件。

    可行路径:

    路径结论
    外刹电机 + 串口写 RAM 恢复状态实用主路径,本项目采用

    PS:单纯靠附件口把 FE7E 改成 71、或伪造 REF(P5.5 ~#57)不能稳定实现方画幅;

    真正可靠的是:INTP 计数到目标值 → 拉停 IN1/IN2 → 用串口清 RAM 残留态。

    通过监测真实的INTP,并电控拉停IN1/IN2,我们可以实现比较精确的过片量控制,从而任意实现半格/方格乃至N格的控制。


    二、硬件连接

    因为机身请求5V IO控制,所以本次试验我们采用了Arduino Uno进行控制。

    信号Arduino Uno机身
    INTP 脉冲D2(中断输入)顺序光耦 TP-INTP
    电机刹停 IN1D6(开漏拉低)TP-IN1
    电机刹停 IN2D7(开漏拉低)TP-IN2
    串口 RXD10机身 TX
    串口 TXD11机身 RX
    GND共地
    调试USB 115200

    刹停采用开漏拉低(BRAKE_OPEN_DRAIN=1),松刹时引脚改回输入,由机身上拉释放电机。

    附件口协议与 Nikon 官方附件一致:1200 bps 唤醒 → S1000 识别 → 9600 bps → 0x80 读 RAM / 0x81 写 RAM。


    三、ROM 里卷片链在干什么(为什么要外刹 + 写 RAM)

    3.1 关键 RAM 变量

    地址含义
    FE7DINTP 脉冲计数(过片中递增)
    FE7E停止阈值(由 EEPROM 演算,不是固定 114)
    FEE1连拍空闲计时;无脉冲 ≥ 0xC8(~200ms) → FE4E=0x23(End)
    FEE4堵转计时(16 bit)
    FE4E错误 / 模式码(0=正常,02=装卷,23/33=End 等)
    FED4.1过片活跃
    FED4.6电机运行(来自 FE20.7
    FEBBH过片阶段标志
    FE50 / FE51LCD 字符(52/5A = End 显示)
    FEC5.5置位后 FE51=0x5A
    FD21帧计数相关(影响串口 busy 门)

    3.2 外刹 @71 时机身处于什么状态

    外刹停住物理胶片后,机身 MCU 仍认为过片进行中:

    • FED4.1 常为 1
    • FE7D 停在 ~47(71 脉冲附近)
    • FEE1 继续向 200ms 超时计数
    • 若不干预 → FE4E=0x23,LCD 显示 End

    方画幅要的是:停在半帧位置,但 LCD 不过 End、下一张能正常拍。

    3.3 串口写 RAM:什么时候能写、什么时候不能

    附件 0x81 写 RAM 经 4767 → 47A2 拷贝,拷贝本身不查 FED4,但之前有 busy 门 @34FC:

    • FED4.1 且 FD21 ≥ 0x26(约第 37 张后)→ 常返回 busy
    • FEB2.2FEB0.1FED5≠0 等也会挡写
    • 读 RAM(0x80) 通常比写容易

    因此:

    • 过片中途 fast 抢写 FEE1 几乎总失败(严格 ACK + busy),与等待 25ms 还是 80ms 无关
    • full 恢复用「发写 + 读回验证」,不依赖每次 ACK,所以能稳定成功

    3.4 正常过片收尾 @24BE(恢复要模拟什么)

    ROM 正常过完一片后 @24BE 会:

    • 清 FEB2.2
    • FEBC / FEBD := 0
    • FE9F := 0x04
    • 调用过片链 4584H

    固件 slim 恢复路径 resumeAdvanceStateMinimal() 即对此的精简模拟;手动 x / w 用更全的 ClearLcdEndState + ResumeAdvanceState


    四、固件架构(v17.2)

    4.1 状态机

    BOOT → IDLE ⇄ LATCH

    ↓ 半按快门 5 脉冲(LATCH 后 3 脉冲)

    ARM → RUN → HOLD →(verify ok)→ IDLE

    ↓ partial

    LATCH(轮询恢复)

    • ISR(D2):微秒级计数 INTP,BRAKE_AT 到点拉刹
    • 主循环:SoftwareSerial 阻塞做 RAM 恢复(瓶颈在这里)
    • 装卷保护:FE4E ∈ {01,02,04,21} 时禁止 ARM

    4.2 一帧成功路径(当前推荐配置)

    PREBOOT ok ← 空闲时自动握手,保持 FEC1.4 会话

    RUN

    BRAKE@50/71 ← BRAKE_AT 可配置

    BRAKE full ~3.5–4s ← v17.2 优化后

    BRAKE verify ok

    OK brake=… hold_ms=…

    BRAKE auto brake off

    v17.2 相对早期版本的主要优化:

    变更效果
    去掉 fast 路径省 ~850ms 无效 ACK 等待
    slim 恢复掩码改直写省 7 次读 RAM(~1s)
    去掉二次 FEE4 写再省 ~150ms
    verify 后自动松刹进 IDLE修复「verify ok 但刹不松」
    slim resume 补 FEBC/FEBD对齐 ROM @24BE

    实测优化前 hold_ms ≈ 5.8s,优化后目标 <5s,约 3.8–4.2s。

    4.3 两档恢复策略

    场景函数特点
    自动 BRAKEf90xCamBrakeFullRecoverslim 写 + 读回 verify,~20 次写 + 7 次读
    手动救机 x / wClearLcdEndState + ResumeAdvanceState全量掩码写,更稳

    附Uno代码:

    /*
     * F90X 卷片控制 — v17.0
     *
     * D2=INTP  D6/D7=IN1/IN2  机身TX→D10 RX←D11  GND
     * USB 115200:r/p  x  w  c  b  e  l  h
     */
    
    #include "f90x_cam_serial.h"
    
    #define FW_VERSION         "v17.2"
    
    #ifndef AUTO_RELEASE_BRAKE_ON_X
    #define AUTO_RELEASE_BRAKE_ON_X  1
    #endif
    
    #define PIN_INTP      2
    #define PIN_IN1       6
    #define PIN_IN2       7
    
    #define BRAKE_OPEN_DRAIN  1
    
    const uint8_t  BRAKE_AT          = 71;
    const uint16_t BRAKE_CLR_SETTLE_MS = 100;
    const uint16_t POST_CLR_GAP_MS   = 40;
    
    const uint16_t LATCH_END_POLL_MS = 2000;
    const uint16_t LATCH_END_FAIL_MS = 8000;
    const uint16_t LATCH_SERIAL_DEFER_MS = 500;
    
    const uint8_t  ARM_PULSES        = 5;
    const uint8_t  ARM_PULSES_LATCH  = 3;
    const uint16_t ARM_WINDOW_MS     = 120;
    const uint16_t CONFIRM_MS        = 80;
    const uint16_t COOLDOWN_MS       = 300;
    const uint16_t BOOT_MS           = 1000;
    const uint16_t PREBOOT_RETRY_MS  = 15000;
    const uint16_t PREBOOT_FAIL_MS   = 3000;
    const uint16_t LOAD_BLOCK_POLL_MS = 1000;
    const uint8_t  LATCH_STUCK_FAIL_N = 3;
    
    enum State { ST_BOOT, ST_IDLE, ST_ARM, ST_RUN, ST_HOLD, ST_LATCH };
    
    volatile State    gSt     = ST_BOOT;
    volatile uint16_t gCnt    = 0;
    volatile uint32_t gArmUs  = 0;
    volatile bool     gCamBusy = false;
    
    uint32_t gBootMs            = 0;
    uint32_t gLastIntpMs        = 0;
    uint32_t gCoolUntilMs       = 0;
    uint32_t gHoldStartMs       = 0;
    uint32_t gBrakeClrDoneMs    = 0;
    uint32_t gLastEndPollMs     = 0;
    uint32_t gLatchQuietUntilMs = 0;
    uint8_t  gArmN              = 0;
    uint8_t  gCntAtBrake        = 0;
    
    bool gBrakeClrPending       = false;
    bool gBrakeClrDone          = false;
    bool gBrakeClrVerified      = false;
    
    static volatile bool gFromLatch = false;
    static volatile bool gBlockArmForLoad = false;
    
    uint32_t gLastPrebootMs = 0;
    uint32_t gLastLoadPollMs = 0;
    bool     gEndProbeDone    = false;
    uint8_t  gLatchClrFailN   = 0;
    
    void onIntp();
    
    static void printHexByte(uint8_t v) {
      if (v < 0x10) Serial.print('0');
      Serial.print(v, HEX);
    }
    
    static void printHexBuf(const uint8_t *buf, uint8_t len) {
      for (uint8_t i = 0; i < len; i++) {
        if (i) Serial.print(' ');
        printHexByte(buf[i]);
      }
    }
    
    static bool printWindErrorDiag() {
      uint8_t b = 0;
      uint16_t w = 0;
      bool ok = true;
    
      Serial.print(F("WIND "));
      if (f90xCamReadByte(0xFE4E, &b)) {
        Serial.print(F(" FE4E=0x"));
        printHexByte(b);
      } else ok = false;
      delay(25);
    
      if (f90xCamReadByte(0xFEE1, &b)) {
        Serial.print(F(" FEE1=0x"));
        printHexByte(b);
      } else ok = false;
      delay(25);
    
      if (f90xCamReadByte(0xFE7D, &b)) {
        Serial.print(F(" FE7D=0x"));
        printHexByte(b);
      } else ok = false;
      delay(25);
    
      if (f90xCamReadByte(0xFE7E, &b)) {
        Serial.print(F(" FE7E=0x"));
        printHexByte(b);
      } else ok = false;
      delay(25);
    
      if (f90xCamReadWord(0xFEE4, &w)) {
        Serial.print(F(" FEE4=0x"));
        printHexByte((uint8_t)(w & 0xFF));
        printHexByte((uint8_t)(w >> 8));
      } else ok = false;
    
      if (!ok) {
        Serial.print(F(" (partial)"));
        f90xCamPrintLastRx();
      }
      Serial.println();
      return ok;
    }
    
    static void printAdvanceChainExtra() {
      uint8_t feb0 = 0, feb2 = 0, fe40 = 0, fed5 = 0, fed7 = 0, feb5 = 0, fe47 = 0;
      uint8_t fe9f = 0, fe5e = 0;
      uint16_t fe70 = 0, fee4w = 0;
    
      if (f90xCamReadByte(0xFEB0, &feb0)) {
        Serial.print(F("  FEB0=0x"));
        printHexByte(feb0);
        if (feb0 & 0x04) Serial.print(F(" [.2=待过片!]"));
        Serial.println();
      }
      delay(20);
      if (f90xCamReadByte(0xFEB2, &feb2)) {
        Serial.print(F("  FEB2=0x"));
        printHexByte(feb2);
        if (feb2 & 0x04) Serial.print(F(" [.2=过片处理中!]"));
        Serial.println();
      }
      delay(20);
      if (f90xCamReadByte(0xFE40, &fe40)) {
        Serial.print(F("  FE40=0x"));
        printHexByte(fe40);
        if (fe40 == 0x0C) Serial.print(F(" (过片镜像态)"));
        else if (fe40 == 0x0D) Serial.print(F(" (FEBBH忙)"));
        else if (fe40 == 0x0E) Serial.print(F(" (错误/等待态)"));
        Serial.println();
      }
      delay(20);
      if (f90xCamReadByte(0xFED5, &fed5)) {
        Serial.print(F("  FED5=0x"));
        printHexByte(fed5);
        if (fed5 & 0x04) Serial.print(F(" [.2=过片链]"));
        Serial.println();
      }
      delay(20);
      if (f90xCamReadByte(0xFED7, &fed7)) {
        Serial.print(F("  FED7=0x"));
        printHexByte(fed7);
        Serial.println();
      }
      delay(20);
      if (f90xCamReadWord(0xFE70, &fe70)) {
        Serial.print(F("  FE70=0x"));
        printHexByte((uint8_t)(fe70 & 0xFF));
        printHexByte((uint8_t)(fe70 >> 8));
        Serial.println(F(" (延迟函数指针)"));
      }
      delay(20);
      if (f90xCamReadByte(0xFEB5, &feb5)) {
        Serial.print(F("  FEB5=0x"));
        printHexByte(feb5);
        if (feb5) Serial.print(F(" (异步忙)"));
        Serial.println();
      }
      delay(20);
      if (f90xCamReadByte(0xFE47, &fe47)) {
        Serial.print(F("  FE47=0x"));
        printHexByte(fe47);
        if (fe47 & 0x20) Serial.print(F(" [.5→FE40=0E]"));
        Serial.println();
      }
      delay(20);
      if (f90xCamReadByte(0xFE9F, &fe9f)) {
        Serial.print(F("  FE9F=0x"));
        printHexByte(fe9f);
        Serial.println();
      }
      delay(20);
      if (f90xCamReadByte(0xFE5E, &fe5e)) {
        Serial.print(F("  FE5E=0x"));
        printHexByte(fe5e);
        if (fe5e == 0) Serial.print(F(" (倒计时归零→0E)"));
        Serial.println();
      }
      delay(20);
      if (f90xCamReadWord(0xFEE4, &fee4w)) {
        Serial.print(F("  FEE4=0x"));
        printHexByte((uint8_t)(fee4w & 0xFF));
        printHexByte((uint8_t)(fee4w >> 8));
        Serial.println();
      }
    }
    
    static bool printFilmEndDiag(const __FlashStringHelper *tag) {
      uint8_t fd14[18];
      uint8_t fed[7];
      uint8_t fe57 = 0, fe24 = 0, fe50 = 0, fe51 = 0;
      uint8_t fed4 = 0, fec5 = 0, fe46 = 0;
      uint8_t febb = 0, fec4 = 0, fe7d = 0;
      uint8_t fd2d = 0;
      uint8_t fd39[10];
      uint8_t n = 0;
      uint8_t fedLen = 0;
      uint8_t fd39Len = 0;
      bool ok = true;
    
      Serial.print(tag);
      Serial.println(F("FILM/LCD"));
    
      if (f90xCamReadRam(0xFD14, 18, fd14, sizeof(fd14), &n) && n >= 18) {
        Serial.print(F("  FD14="));
        printHexBuf(fd14, 2);
        Serial.print(F(" FD16(eep186)="));
        printHexBuf(fd14 + 2, 8);
        Serial.print(F(" FD21="));
        printHexByte(fd14[13]);
        Serial.print(F(" FD22="));
        printHexBuf(fd14 + 14, 2);
        Serial.print(F(" FD25=shutter "));
        printHexByte(fd14[17]);
        Serial.println();
      } else {
        ok = false;
        Serial.println(F("  FD14..FD25 read fail"));
        f90xCamPrintLastRx();
      }
      delay(40);
    
      if (f90xCamReadByte(0xFD2D, &fd2d)) {
        Serial.print(F("  FD2D=0x"));
        printHexByte(fd2d);
        Serial.println(F(" (帧计数相关)"));
      } else ok = false;
      delay(25);
    
      if (f90xCamReadRam(0xFD39, 10, fd39, sizeof(fd39), &fd39Len) && fd39Len >= 1) {
        Serial.print(F("  FD39="));
        printHexBuf(fd39, fd39Len > 4 ? 4 : fd39Len);
        Serial.println();
      } else ok = false;
      delay(25);
    
      if (f90xCamReadRam(0xFED0, 7, fed, sizeof(fed), &fedLen) && fedLen >= 7) {
        Serial.print(F("  FED0=0x"));
        printHexByte(fed[0]);
        Serial.print(F(" FED6=0x"));
        printHexByte(fed[6]);
        if (fed[6] & 0x02) Serial.print(F(" [FED6.1]"));
        Serial.println();
      } else {
        ok = false;
        Serial.println(F("  FED0..FED6 read fail"));
      }
      delay(25);
    
      if (f90xCamReadByte(0xFE57, &fe57)) {
        Serial.print(F("  FE57=0x"));
        printHexByte(fe57);
        Serial.println();
      } else ok = false;
      delay(25);
    
      if (f90xCamReadByte(0xFE24, &fe24)) {
        Serial.print(F("  FE24=0x"));
        printHexByte(fe24);
        if (fe24 == 0x72) Serial.print(F(" (=114脉冲?)"));
        Serial.println();
      } else ok = false;
      delay(25);
    
      if (f90xCamReadByte(0xFED4, &fed4)) {
        Serial.print(F("  FED4=0x"));
        printHexByte(fed4);
        if (fed4 & 0x02) Serial.print(F(" [.1=LCD52]"));
        if (fed4 & 0x40) Serial.print(F(" [.6=过片中!]"));
        Serial.println();
      } else ok = false;
      delay(25);
    
      if (f90xCamReadByte(0xFE7D, &fe7d)) {
        Serial.print(F("  FE7D=0x"));
        printHexByte(fe7d);
        if (fe7d != 0) Serial.print(F(" (半帧计数残留)"));
        Serial.println();
      } else ok = false;
      delay(25);
    
      if (f90xCamReadByte(0xFEBB, &febb)) {
        Serial.print(F("  FEBBH=0x"));
        printHexByte(febb);
        if (febb & 0x01) Serial.print(F(" [.0=过片忙]"));
        Serial.println();
      } else ok = false;
      delay(25);
    
      printAdvanceChainExtra();
    
      if (f90xCamReadByte(0xFEC4, &fec4)) {
        Serial.print(F("  FEC4=0x"));
        printHexByte(fec4);
        Serial.println();
      } else ok = false;
      delay(25);
    
      if (f90xCamReadByte(0xFEC5, &fec5)) {
        Serial.print(F("  FEC5=0x"));
        printHexByte(fec5);
        if (fec5 & 0x20) Serial.print(F(" [FEC5.5→FE51=5A]"));
        Serial.println();
      } else ok = false;
      delay(25);
    
      if (f90xCamReadByte(0xFE46, &fe46)) {
        Serial.print(F("  FE46=0x"));
        printHexByte(fe46);
        Serial.println();
      } else ok = false;
      delay(25);
    
      if (f90xCamReadByte(0xFE50, &fe50) && f90xCamReadByte(0xFE51, &fe51)) {
        Serial.print(F("  LCD FE50=0x"));
        printHexByte(fe50);
        Serial.print(F(" FE51=0x"));
        printHexByte(fe51);
        bool lcdEnd = (fe51 == 0x5A) && (fe50 == 0x52 || fe50 == 0x4D || fe50 == 0x54);
        if (lcdEnd) Serial.print(F(" [LCD End]"));
        Serial.println();
      } else ok = false;
    
      if (ok && n >= 18) {
        uint8_t fd21 = fd14[13];
        if (fe51 == 0x5A && fe50 == 0x52) {
          Serial.println(F("  => LCD End @第5张: 外刹残留态(FE4E已清)"));
          Serial.println(F("     FE50=52←FED4.1  FE51=5A←FEC5.5  非片尾"));
        } else if (fe51 == 0x5A) {
          Serial.println(F("  => LCD End态 (ROM @1C45/@3CFD)"));
        } else if (fed4 & 0x40) {
          Serial.println(F("  => FED4.6 过片链卡住 — 按 w 或 x"));
        } else if (fe7d != 0) {
          Serial.println(F("  => FE7D 半帧残留 — 按 w 恢复"));
        } else if (febb & 0x07) {
          Serial.println(F("  => FEBBH 过片忙 — 按 w"));
        } else {
          Serial.println(F("  => 无 LCD End;若仍不过片看 FEB2/FE40"));
        }
        if (fd21 >= 0x01 && fd21 <= 0x28) {
          Serial.print(F("  => FD21=第"));
          Serial.print((int)fd21 - 1);
          Serial.println(F(" 张附近"));
        }
      }
    
      return ok;
    }
    
    static void printRamDiag(const __FlashStringHelper *tag) {
      gCamBusy = true;
    
      if (!f90xCamEnsureSession(true)) {
        Serial.println(F("RAM bootstrap fail — press b"));
        gCamBusy = false;
        return;
      }
      delay(100);
    
      Serial.print(tag);
      printWindErrorDiag();
      printFilmEndDiag(F(""));
    
      gCamBusy = false;
    }
    
    static void printUsbHelp() {
      Serial.println(F("--- USB commands (115200, Uno USB) ---"));
      Serial.println(F("  r  read wind-error + film-end RAM"));
      Serial.println(F("  p  read film/LCD (FD21 FE50/FE51 FD2D)"));
      Serial.println(F("  c  clear wind-error (FE4E/FEE1/FEE4)"));
      Serial.println(F("  x  clear LCD End + resume (LATCH 成功自动松刹)"));
      Serial.println(F("  w  resume advance (FEB0/FEB2/FE40 chain)"));
      Serial.println(F("  b  bootstrap camera serial"));
      Serial.println(F("  e  end serial session"));
      Serial.println(F("  l  force IDLE + release brake"));
      Serial.println(F("  h  this help"));
    }
    
    static bool lcdEndLooksCleared(uint8_t fe50, uint8_t fe51) {
      return fe51 != 0x5A && fe50 != 0x52;
    }
    
    static bool fe4eIsLoadMode(uint8_t fe4e) {
      return fe4e == 0x01 || fe4e == 0x02 || fe4e == 0x04 || fe4e == 0x21;
    }
    
    static void updateLoadArmBlock() {
      if (gCamBusy || !f90xCamIsReady()) return;
      if (gSt != ST_IDLE && gSt != ST_LATCH) return;
    
      uint32_t now = millis();
      if (gLastLoadPollMs != 0 && now - gLastLoadPollMs < LOAD_BLOCK_POLL_MS) return;
    
      gLastLoadPollMs = now;
      gCamBusy = true;
      uint8_t fe4e = 0;
      bool wasBlocked = gBlockArmForLoad;
      if (f90xCamReadByte(0xFE4E, &fe4e)) {
        gBlockArmForLoad = fe4eIsLoadMode(fe4e);
        if (gBlockArmForLoad && !wasBlocked) {
          Serial.print(F("ARM block load FE4E=0x"));
          printHexByte(fe4e);
          Serial.println();
        } else if (!gBlockArmForLoad && wasBlocked) {
          Serial.println(F("ARM load block off"));
        }
      }
      gCamBusy = false;
    }
    
    static void releaseBrakeToIdle(const __FlashStringHelper *tag) {
      brakeOff();
      gFromLatch = false;
      gSt = ST_IDLE;
      gArmN = 0;
      gCnt = 0;
      gBrakeClrPending = false;
      gBrakeClrDone = false;
      gBrakeClrVerified = false;
      gLatchClrFailN = 0;
      gLatchQuietUntilMs = 0;
      gLastEndPollMs = millis() + LATCH_END_FAIL_MS;
      Serial.print(tag);
      Serial.println(F(" auto brake off"));
    }
    
    static void releaseBrakeIfLatched(const __FlashStringHelper *tag) {
      if (gSt != ST_LATCH) return;
      releaseBrakeToIdle(tag);
    }
    
    static bool clearEndAggressive(const __FlashStringHelper *tag) {
      for (uint8_t n = 0; n < 3; n++) {
        if (f90xCamClearEndStateFast()) {
          Serial.print(tag);
          Serial.println(F(" fast ok"));
          return true;
        }
        if (f90xCamClearEndStateBrake()) {
          Serial.print(tag);
          Serial.println(F(" med ok"));
          return true;
        }
        if (f90xCamClearEndState()) {
          Serial.print(tag);
          Serial.println(F(" full ok"));
          return true;
        }
        delay(40);
      }
      return false;
    }
    
    // 手动清 End:握手 → 读 FE4E → 三档重试写零 → 读回验证
    static bool manualClearEnd() {
      gCamBusy = true;
    
      if (!f90xCamEnsureSession(true)) {
        Serial.println(F("MANUAL bootstrap fail — try b"));
        gCamBusy = false;
        return false;
      }
      delay(100);
    
      uint8_t fe4e = 0xFF;
      if (f90xCamReadByte(0xFE4E, &fe4e)) {
        Serial.print(F("MANUAL before FE4E=0x"));
        printHexByte(fe4e);
        Serial.println();
      } else {
        Serial.println(F("MANUAL read fail — blind CLR"));
        f90xCamPrintLastRx();
      }
    
      bool ok = clearEndAggressive(F("MANUAL CLR"));
      if (!ok) ok = f90xCamClearEndStateRetry(3);
    
      if (f90xCamReadByte(0xFE4E, &fe4e)) {
        Serial.print(F("MANUAL after FE4E=0x"));
        printHexByte(fe4e);
        if (fe4e == 0) {
          Serial.println(F(" OK"));
          f90xCamEndSession(false);
          gEndProbeDone = false;
        } else {
          Serial.println(F(" still error"));
        }
      } else if (ok) {
        Serial.println(F("MANUAL CLR sent (unverified)"));
      } else {
        Serial.println(F("MANUAL CLR FAIL — retry c or b then c"));
        f90xCamPrintLastRx();
      }
    
      gCamBusy = false;
      return ok;
    }
    
    // 仅恢复卷片链(LCD 已清但电机不动时用)
    static bool manualResumeAdvance() {
      gCamBusy = true;
    
      if (!f90xCamEnsureSession(true)) {
        Serial.println(F("RESUME bootstrap fail — try b"));
        gCamBusy = false;
        return false;
      }
      delay(100);
    
      uint8_t fed4 = 0, fe7d = 0, feb2 = 0, fe40 = 0;
      if (f90xCamReadByte(0xFED4, &fed4) && f90xCamReadByte(0xFE7D, &fe7d)) {
        Serial.print(F("RESUME before FED4=0x"));
        printHexByte(fed4);
        Serial.print(F(" FE7D=0x"));
        printHexByte(fe7d);
        Serial.println();
      }
    
      bool ok = false;
      for (uint8_t n = 0; n < 3 && !ok; n++) {
        ok = f90xCamResumeAdvanceState();
        delay(60);
      }
    
      bool gotAfter = f90xCamReadByte(0xFED4, &fed4) && f90xCamReadByte(0xFE7D, &fe7d);
      (void)f90xCamReadByte(0xFEB2, &feb2);
      (void)f90xCamReadByte(0xFE40, &fe40);
    
      if (gotAfter) {
        Serial.print(F("RESUME after  FED4=0x"));
        printHexByte(fed4);
        Serial.print(F(" FE7D=0x"));
        printHexByte(fe7d);
        Serial.print(F(" FEB2=0x"));
        printHexByte(feb2);
        Serial.print(F(" FE40=0x"));
        printHexByte(fe40);
        Serial.println();
        if (ok) {
          Serial.println(F(" OK —按 l 松刹后试快门过片"));
          f90xCamEndSession(false);
          gEndProbeDone = false;
        } else {
          Serial.println(F(" partial — 按 r 看 FEB2/FE40/FEBBH,再试 w 或关机"));
          if (!ok) f90xCamPrintLastRx();
        }
      } else if (ok) {
        Serial.println(F("RESUME sent (unverified)"));
      } else {
        Serial.println(F("RESUME FAIL — 按 r 或 b 后重试"));
        f90xCamPrintLastRx();
      }
    
      gCamBusy = false;
      return ok;
    }
    
    // 清 LCD End(FE4E=0 仍显示 End):FEC5.5/FED4.1/FE50/FE51
    static bool manualClearLcdEnd() {
      gCamBusy = true;
    
      if (!f90xCamEnsureSession(true)) {
        Serial.println(F("LCD CLR bootstrap fail — try b"));
        gCamBusy = false;
        return false;
      }
      delay(100);
    
      uint8_t fe50 = 0, fe51 = 0;
      if (f90xCamReadByte(0xFE50, &fe50) && f90xCamReadByte(0xFE51, &fe51)) {
        Serial.print(F("LCD before FE50=0x"));
        printHexByte(fe50);
        Serial.print(F(" FE51=0x"));
        printHexByte(fe51);
        Serial.println();
      }
    
      bool ok = false;
      bool lcdOk = false;
      bool chainOk = false;
      for (uint8_t n = 0; n < 3 && !ok; n++) {
        ok = f90xCamClearLcdEndState() && f90xCamResumeAdvanceState();
        delay(60);
      }
    
      if (f90xCamReadByte(0xFE50, &fe50) && f90xCamReadByte(0xFE51, &fe51)) {
        lcdOk = lcdEndLooksCleared(fe50, fe51);
        chainOk = f90xCamAdvanceChainLooksIdle();
        Serial.print(F("LCD after  FE50=0x"));
        printHexByte(fe50);
        Serial.print(F(" FE51=0x"));
        printHexByte(fe51);
        if (lcdOk && chainOk) {
          Serial.println(F(" OK"));
          gEndProbeDone = false;
          gLatchClrFailN = 0;
    #if AUTO_RELEASE_BRAKE_ON_X
          releaseBrakeIfLatched(F("LCD CLR"));
    #endif
        } else if (lcdOk) {
          Serial.println(F(" LCD ok chain busy —试 w 或再 x"));
          gEndProbeDone = false;
          pollRecoverAutoRelease(true);
        } else {
          Serial.println(F(" still End — retry x"));
        }
      } else if (ok) {
        Serial.println(F("LCD CLR sent (unverified)"));
      } else {
        Serial.println(F("LCD CLR FAIL"));
        f90xCamPrintLastRx();
      }
    
      gCamBusy = false;
      return ok;
    }
    
    static void probeCamEndOnce() {
      if (gEndProbeDone || gCamBusy || !f90xCamIsReady()) return;
      if (gSt != ST_IDLE && gSt != ST_LATCH) return;
    
      gCamBusy = true;
      uint8_t fe4e = 0, fe50 = 0, fe51 = 0;
      bool gotFe4e = f90xCamReadByte(0xFE4E, &fe4e);
      delay(20);
      bool gotLcd = f90xCamReadByte(0xFE50, &fe50) && f90xCamReadByte(0xFE51, &fe51);
      delay(20);
    
      if (gotFe4e && fe4e != 0) {
        Serial.print(F("CAM wind-error FE4E=0x"));
        printHexByte(fe4e);
        Serial.println(F(" — press c"));
      } else if (gotLcd && fe51 == 0x5A &&
                 (fe50 == 0x52 || fe50 == 0x4D || fe50 == 0x54)) {
        Serial.print(F("CAM LCD-End FE50=0x"));
        printHexByte(fe50);
        Serial.print(F(" FE51=0x"));
        printHexByte(fe51);
        Serial.println(F(" — press x (c无效)"));
      }
      gEndProbeDone = true;
      gCamBusy = false;
    }
    
    static bool tryBrakeFullClear() {
      uint32_t t0 = millis();
      bool verified = false;
      if (!f90xCamBrakeFullRecover(&verified)) {
        Serial.print(F("BRAKE full fail "));
        Serial.print(millis() - t0);
        Serial.println(F("ms"));
        return false;
      }
      Serial.print(F("BRAKE full "));
      Serial.print(millis() - t0);
      Serial.println(F("ms"));
      if (verified) {
        Serial.println(F("BRAKE verify ok"));
        gBrakeClrVerified = true;
        gLatchClrFailN = 0;
        gEndProbeDone = false;
      } else {
        Serial.println(F("BRAKE partial — x/w if no wind"));
      }
      return true;
    }
    
    static void pollRecoverAutoRelease(bool verified) {
    #if AUTO_RELEASE_BRAKE_ON_X
      if (verified) releaseBrakeIfLatched(F("LATCH CLR"));
    #else
      (void)verified;
    #endif
    }
    
    static void tryIdlePreboot() {
      if (gCamBusy || f90xCamIsReady()) return;
      if (gSt != ST_IDLE && gSt != ST_LATCH) return;
      if (gSt == ST_LATCH && millis() < gLatchQuietUntilMs) return;
    
      uint32_t now = millis();
      if (gLastPrebootMs != 0 && now - gLastPrebootMs < PREBOOT_RETRY_MS) return;
    
      gLastPrebootMs = now;
      gCamBusy = true;
      if (f90xCamBootstrap(false)) {
        Serial.println(F("PREBOOT ok"));
        gEndProbeDone = false;
      } else {
        Serial.println(F("PREBOOT fail — press b"));
        gLastPrebootMs = now - PREBOOT_RETRY_MS + PREBOOT_FAIL_MS;
      }
      gCamBusy = false;
    }
    
    static void pollEndAndClear() {
      if (gSt != ST_LATCH) return;
    
      gCamBusy = true;
      uint32_t nextPollMs = LATCH_END_POLL_MS;
    
      if (gBrakeClrVerified) {
        if (f90xCamIsReady()) {
          uint8_t fe51 = 0, fe50 = 0;
          if (f90xCamReadByte(0xFE51, &fe51) &&
              f90xCamReadByte(0xFE50, &fe50) &&
              lcdEndLooksCleared(fe50, fe51) &&
              f90xCamAdvanceChainLooksIdle()) {
            gLatchClrFailN = 0;
            nextPollMs = 30000;
    #if AUTO_RELEASE_BRAKE_ON_X
            releaseBrakeToIdle(F("LATCH ok"));
    #endif
            goto done;
          }
          gBrakeClrVerified = false;
          Serial.println(F("LATCH re-check: need CLR again"));
        }
      }
    
      if (!f90xCamIsReady()) {
        if (!f90xCamBootstrap(false)) {
          Serial.println(F("END poll bootstrap fail"));
          nextPollMs = LATCH_END_FAIL_MS;
          goto done;
        }
      }
      delay(50);
    
      {
        uint8_t fe4e = 0;
        bool readOk = f90xCamReadByte(0xFE4E, &fe4e);
        bool needRecover = false;
    
        if (readOk && fe4e == 0) {
          uint8_t fe51 = 0, fe50 = 0;
          bool gotLcd = f90xCamReadByte(0xFE51, &fe51) &&
                        f90xCamReadByte(0xFE50, &fe50);
          if (gotLcd && fe51 == 0x5A &&
              (fe50 == 0x52 || fe50 == 0x4D || fe50 == 0x54)) {
            needRecover = true;
          } else if (!f90xCamAdvanceChainLooksIdle()) {
            needRecover = true;
          } else {
            gLatchClrFailN = 0;
            nextPollMs = 10000;
            goto done;
          }
        } else if (readOk) {
          Serial.print(F("END det FE4E=0x"));
          printHexByte(fe4e);
          Serial.println();
          needRecover = true;
        } else {
          Serial.println(F("END read fail — blind CLR"));
          needRecover = true;
        }
    
        if (needRecover) {
          bool verified = false;
          if (f90xCamBrakeFullRecover(&verified)) {
            gLatchClrFailN = 0;
            Serial.println(verified ? F("LATCH CLR verify ok") : F("LATCH CLR partial"));
            if (verified) {
              gBrakeClrVerified = true;
              pollRecoverAutoRelease(true);
            } else {
              uint8_t fe51 = 0, fe50 = 0;
              if (f90xCamReadByte(0xFE50, &fe50) &&
                  f90xCamReadByte(0xFE51, &fe51) &&
                  lcdEndLooksCleared(fe50, fe51)) {
                Serial.println(F("LATCH LCD ok — auto brake off, try w if stuck"));
                pollRecoverAutoRelease(true);
              }
            }
            gEndProbeDone = false;
          } else {
            gLatchClrFailN++;
            Serial.print(F("LATCH CLR FAIL n="));
            Serial.println(gLatchClrFailN);
            nextPollMs = LATCH_END_FAIL_MS;
            if (gLatchClrFailN >= LATCH_STUCK_FAIL_N) {
              Serial.println(F("STUCK: force brake off → IDLE (try w/x)"));
              releaseBrakeIfLatched(F("STUCK"));
              gLatchClrFailN = 0;
            }
          }
        }
      }
    
    done:
      gLastEndPollMs = millis() + nextPollMs;
      gCamBusy = false;
    }
    
    static void handleUsbCommand(char c) {
      switch (c) {
        case 'c':
        case 'C':
          manualClearEnd();
          return;
        case 'x':
        case 'X':
          manualClearLcdEnd();
          return;
        case 'w':
        case 'W':
          manualResumeAdvance();
          return;
      }
    
      if (gCamBusy) return;
    
      switch (c) {
        case 'r':
        case 'R':
          printRamDiag(F("MANUAL "));
          break;
        case 'p':
        case 'P':
          gCamBusy = true;
          if (!f90xCamEnsureSession(true)) {
            Serial.println(F("FILM bootstrap fail — press b"));
          } else {
            delay(100);
            printFilmEndDiag(F("MANUAL "));
          }
          gCamBusy = false;
          break;
        case 'h':
        case 'H':
        case '?':
          printUsbHelp();
          break;
        case 'b':
        case 'B':
          gCamBusy = true;
          gLastPrebootMs = millis();
          if (f90xCamBootstrap(true))
            Serial.println(F("bootstrap OK"));
          else
            Serial.println(F("bootstrap FAIL"));
          gCamBusy = false;
          break;
        case 'e':
        case 'E':
          gCamBusy = true;
          f90xCamEndSession(true);
          gLastPrebootMs = 0;
          gEndProbeDone = false;
          Serial.println(F("session end"));
          gCamBusy = false;
          break;
        case 'l':
        case 'L':
          releaseBrakeToIdle(F("force"));
          break;
        default:
          break;
      }
    }
    
    static void brakeOn() {
      pinMode(PIN_IN1, OUTPUT);
      pinMode(PIN_IN2, OUTPUT);
    #if BRAKE_OPEN_DRAIN
      digitalWrite(PIN_IN1, LOW);
      digitalWrite(PIN_IN2, LOW);
    #else
      digitalWrite(PIN_IN1, HIGH);
      digitalWrite(PIN_IN2, HIGH);
    #endif
    }
    
    static void brakeOff() {
    #if !BRAKE_OPEN_DRAIN
      digitalWrite(PIN_IN1, LOW);
      digitalWrite(PIN_IN2, LOW);
    #endif
      pinMode(PIN_IN1, INPUT);
      pinMode(PIN_IN2, INPUT);
    }
    
    static void intpListenOn() {
      pinMode(PIN_INTP, INPUT);
      attachInterrupt(digitalPinToInterrupt(PIN_INTP), onIntp, RISING);
    }
    
    static void startCooldown() {
      gCoolUntilMs = millis() + COOLDOWN_MS;
    }
    
    static bool canArm() {
      return millis() >= gCoolUntilMs;
    }
    
    static void enterLatch() {
      gSt = ST_LATCH;
      gLatchQuietUntilMs = millis() + LATCH_SERIAL_DEFER_MS;
      gLastEndPollMs = millis() + LATCH_SERIAL_DEFER_MS;
    }
    
    static void endFrame() {
      Serial.print(F("OK brake="));
      Serial.print(gCntAtBrake);
      Serial.print(F(" hold_ms="));
      Serial.println(millis() - gHoldStartMs);
      gCnt = 0;
      gArmN = 0;
      startCooldown();
    #if AUTO_RELEASE_BRAKE_ON_X
      if (gBrakeClrVerified) {
        releaseBrakeToIdle(F("BRAKE"));
        return;
      }
    #endif
      Serial.println(F("LATCH"));
      enterLatch();
    }
    
    static void discardBurst() {
      if (gFromLatch) {
        gFromLatch = false;
        gArmN = 0;
        brakeOn();
        gSt = ST_LATCH;
        startCooldown();
        return;
      }
      brakeOff();
      gSt = ST_IDLE;
      gCnt = 0;
      gArmN = 0;
      startCooldown();
    }
    
    void onIntp() {
      uint32_t t = micros();
      gLastIntpMs = millis();
    
      if (gSt == ST_IDLE || gSt == ST_LATCH) {
        if (!canArm()) return;
        if (gBlockArmForLoad) return;
        gFromLatch = (gSt == ST_LATCH);
        gSt = ST_ARM;
        gArmN = 1;
        gArmUs = t;
        return;
      }
    
      if (gSt == ST_ARM) {
        gArmN++;
        uint8_t needArm = gFromLatch ? ARM_PULSES_LATCH : ARM_PULSES;
        if (gArmN >= needArm &&
            (t - gArmUs) < (uint32_t)ARM_WINDOW_MS * 1000UL) {
          brakeOff();
          gFromLatch = false;
          gSt = ST_RUN;
          gCnt = gArmN;
          gBrakeClrPending = false;
          gBrakeClrDone = false;
          gBrakeClrVerified = false;
          Serial.println(F("RUN"));
        }
        return;
      }
    
      if (gSt == ST_RUN) {
        gCnt++;
        if (gCnt >= BRAKE_AT) {
          gSt = ST_HOLD;
          gHoldStartMs = millis();
          gCntAtBrake = gCnt;
          brakeOn();
          gBrakeClrPending = true;
          gBrakeClrDone = false;
          gBrakeClrVerified = false;
          Serial.print(F("BRAKE@"));
          Serial.println(gCnt);
        }
        return;
      }
    
      if (gSt == ST_HOLD) {
        gCnt++;
      }
    }
    
    void setup() {
      pinMode(PIN_INTP, INPUT);
      brakeOff();
      intpListenOn();
      Serial.begin(115200);
      gBootMs = millis();
      f90xCamForceReset();
      Serial.print(F("F90X "));
      Serial.println(FW_VERSION);
      Serial.print(F("BRAKE_AT="));
      Serial.println(BRAKE_AT);
      Serial.println(F("USB: r/p  x  w  c  b  e  l  h"));
    }
    
    void loop() {
      uint32_t now = millis();
    
      while (Serial.available()) {
        handleUsbCommand((char)Serial.read());
      }
    
      if (gSt == ST_IDLE || gSt == ST_LATCH) {
        tryIdlePreboot();
        updateLoadArmBlock();
        probeCamEndOnce();
      }
    
      if (gSt == ST_BOOT) {
        brakeOff();
        if (now - gBootMs >= BOOT_MS) {
          gSt = ST_IDLE;
          Serial.println(F("READY"));
        }
        return;
      }
    
      if (gSt == ST_ARM && now - gLastIntpMs >= CONFIRM_MS) {
        discardBurst();
        return;
      }
    
      if (gSt == ST_HOLD) {
        if (gBrakeClrPending && !gBrakeClrDone && !gCamBusy &&
            now - gHoldStartMs >= BRAKE_CLR_SETTLE_MS) {
          gCamBusy = true;
          (void)tryBrakeFullClear();
          gBrakeClrDone = true;
          gBrakeClrPending = false;
          gBrakeClrDoneMs = millis();
          gCamBusy = false;
        }
    
        if (!gBrakeClrDone) return;
        if (now - gBrakeClrDoneMs < POST_CLR_GAP_MS) return;
        endFrame();
        return;
      }
    
      if (gSt == ST_LATCH) {
        if (!gCamBusy && now >= gLatchQuietUntilMs && now >= gLastEndPollMs) {
          pollEndAndClear();
        }
      }
    }
    
  • 【摄影】F90X回卷留片头实现!F90X/N90S Film Leader Customizer——尼康F90X/N90S回卷留片头软件堂堂发布!

    仓库链接:kuixiaoran/f90x-film-leader-customizer

    发布视频:

    写在前面:

    尼康F时代胶片相机,从F90/N90开始使用10pin接口,这个接口作为尼康机身附件和电子通讯接口,一直延续到Z时代。

    在胶片机时代,由于机身电子化程度有限,用户没有办法在机身侧设置全部自定义选项。

    但是有10pin接口,我们可以通过电脑自定义设置机身选项和读取机身存储的拍摄信息。

    尼康也为F90/N90系列、F5、F100各自开发了对应软件进行自定义设置和管理。

    F6则没有官方软件,因为F6时代机身电子化程度已经极高,日常选项均可以通过机身自定义。

    但是可惜的是,尼康的F系列电子胶片机,回卷留片头功能上除了F6,都需要回到尼康售后服务站进行设置。

    时光荏苒,你现在拿这些胶片机去尼康,售后都不一定认得全。

    因此留片头功能似乎成了遥远的传说。

    翻看尼康售后服务手册,不难发现这类功能均是通过机内eeprom进行管理,因此我们只需要找到读写eeprom的方式即可。

    拆下来eeprom进行读写是可行的操作,但是拆机和焊接对于这些几十岁的柔性fpc电路板来说,风险着实不小。

    既然官方售后软件通过10pin接口进行通讯,那么我们是不是也能找到路径?

    在F90X/N90S上,我们取得了一些突破,通过逆向MCU的启动逻辑,我们知道了如何通过10pin,操作寄存器对eeprom进行读写。

    得益于如今不断发展的AI coding,我们可以很快将其软件化,虽然这真的很小众,也没有商业价值,但整个过程真是一种又老又新的奇妙体验。

    关于软件:

    1. 读取 EEPROM 镜像(Dump)

    连接相机串口后,可一次性读取 512 字节 EEPROM 数据,并在本地保存为镜像文件。
    建议每次修改前先 Dump 并备份 .bin 文件——这是日后恢复或对比的「保险单」。

    2. 调整留片头长度(MODIFY)

    软件针对 地址 0x169 的留片头参数进行写入。数值为十进制 6~31,与物理留片头大致对应关系如下:

    数值(DEC)留片头(约)
    655 mm(较长)
    315 mm(较短)

    数值越小,留片头越长;越大则越短。
    界面上可通过 拖动胶片 的方式直观设定,软件会自动限制在合法范围内,避免写入越界数值。

    写入时软件会按协议完成数据写入,并 更新校验和(0x017F),降低因校验不匹配导致相机异常的风险。

    PS:经过测试,31(DEC)会比5mm略长一些,不过为了稳定性考虑,如果您自己重新编译该软件,不建议写入数值大于33(DEC)。

    3. 高级功能

    面向有经验的用户,「高级」面板提供:

    • HxD 风格十六进制镜像:查看完整 EEPROM 布局,0x017F 校验和位置会高亮显示
    • 校验和状态:当前值、期望值、是否一致
    • 再次 Dump:只刷新内存镜像,不弹出保存对话框
    • 仅写 0x017F:在 MODIFY 部分失败、但数据已正确写入时,可单独补写校验和

    4. 多语言日志

    日志与后端错误提示支持 中文 / English / 日本語 切换,便于不同用户阅读;不影响串口协议与写入逻辑。

    5. 界面与体验

    软件采用 Wails 桌面框架 + React 界面,配合 Motion 动画,还原「胶片、后盖、串口连接」等操作场景,让流程更清晰:
    开场连接 → 工作区 Dump / 修改 → 底部日志与高级面板。

    注意事项与免责声明(请务必阅读)

    操作风险

    1. EEPROM 写入具有不可逆性
      错误写入可能导致相机行为异常。务必在修改前 Dump 并保存备份。
    2. 仅修改受支持的参数
      常规 MODIFY 流程 只允许写入 0x169(留片头,DEC 6~31)。请勿随意使用高级功能修改其他地址,除非你完全理解后果。
    3. 串口连接要稳定
      写入过程中请勿拔线、休眠或运行可能占用串口的其他软件。若出现 FEB 超时,请按日志提示处理:可尝试重新连接、重新 Dump,或在确认数据已正确写入后 仅写校验和。
    4. 校验和很重要
      留片头数据写入后,应确保 0x017F 校验和 与算法一致。软件会在成功 MODIFY 流程中自动处理;若中断,请使用高级面板的「仅写 0x017F」或按日志指引恢复。
    5. 非官方工具
      本软件 与相机制造商无关,属于爱好者工具。使用即表示你理解并自行承担风险。建议在非关键任务上先小规模验证,再用于日常拍摄配置。

    使用建议

    • 第一次使用前:通读日志输出,确认 Dump 成功、镜像中 0x169 与预期一致
    • 每次修改前:保存一份带日期的 .bin 备份
    • 修改后:可在相机上实际装片、过片,确认留片头是否符合预期
    • 遇到异常:保留完整日志,便于排查是连接问题、协议超时还是校验问题

    写在最后

    F90X 的留片头调节,本质上是在 几字节 EEPROM 数据 与 实际装片

    如果你也在折腾 F90X/N90S的胶片回卷与留片头,欢迎交流使用反馈与改进建议。
    使用前请再次确认:已备份、已理解风险、已在安全环境下测试。

  • 【摄影】Nikon F90X/N90s F90/N90系列胶片相机串口通信协议逆向项目

    基于尼康官方软件AC-PW,提取其核心 Pcf9032.dll逆向
    由 DeepSeek 4.0 Pro 辅助完成。


    第一章 概述

    1.1 适用范围

    本报告完整描述 F90X/N90s F90/N90 胶片单反相机通过串行接口与计算机通信的底层协议、命令集、寄存器映射、功能编号及所有操作序列。所有内容均来自对官方控制库 Pcf9032.dll 及配套软件 PhotoSecretary的逆向分析,并通过资源提取进行验证。

    1.2 通信架构

    • 物理层:RS-232C 串行通信,5V 电平
    • 波特率:初始 1200 bps,握手后切换至 9600 bps
    • 数据格式:8 数据位,无校验,1 停止位 (8N1)
    • 端口选择:COM1~COM4,由功能 ID 0 设定
    • DLL 文件:Pcf9032.dll(Nikon 相机通信库),由主程序动态加载

    1.3 相机型号识别

    型号标识相机返回字符串地址字节对应型号
    1F90N900x10F90系列/N90(北美发售型号)系列
    2F90xN90s0x20F90X系列/N90S(北美发售型号)系列

    第二章 通信初始化

    2.1 唤醒与型号识别

    1. 打开串口 COM%d,配置为 1200,8N1。
    2. 发送唤醒字节:00(1 字节),等待 200ms,清空接收缓冲区。
    3. 发送型号查询:53 31 30 30 30 05S1000 + ENQ,共 6 字节)。
    4. 接收相机返回的标识字符串(例如 31 30 32 30 46 39 30 58 2F 4E 39 30 53 00 03 06 表示 1020F90X/N90S)。
    5. 根据字符串确定型号:
      • 若包含 F90N90 → 型号 1,地址字节 0x10。
      • 若包含 F90xN90s → 型号 2,地址字节 0x20。

    2.2 波特率升级至 9600

    1. 发送 9 字节命令(型号 1 用 0x10,型号 2 用 0x20):
      01 10 87 05 00 00 00 00 03 或 01 20 87 05 00 00 00 00 03
    2. 等待相机应答 06 00(ACK)。
    3. 将本地串口切换为 9600 bps,等待 200ms 稳定。

    2.3 结束 9600 会话(返回 1200)

    • 发送 04 04(EOT 两次),相机返回 04 04 确认,然后串口恢复到 1200。
    • 若相机自动休眠,波特率也会自动降回 1200。

    第三章 通信帧格式

    校验和仅计算纯数据部分,即从 STX (0x02) 之后的第一个字节开始,到 ETX (0x03) 之前的所有字节,不包括帧头(SOH)、地址、命令码、长度字段等任何头部字节。

    波特率切换等特殊握手指令会使用 0x87 作为操作码,此类特殊指令属于硬编码控制帧,不完全遵循常规寄存器读写帧的载荷结构。

    3.1 标准命令帧(命令码 ≤ 100)

    读请求(无数据)

    偏移 大小 值       说明
    0 1 0x01 帧头 (SOH)
    1 1 ADDR 设备地址 (0x10 或 0x20)
    2 1 0x80 读操作
    3 1 0x00 保留
    4 1 v6 寄存器地址高字节
    5 1 v7 寄存器地址低字节
    6 1 0x00 保留
    7 1 len 期望接收的数据长度
    8 1 0x03 帧尾 (ETX)

    总长 9 字节,无校验。

    写请求(有数据)

    偏移 大小 值       说明
    0    1    0x01     帧头
    1    1    ADDR     设备地址
    2    1    0x81     写操作
    3    1    0x00     保留
    4    1    v6       寄存器地址高字节
    5    1    v7       寄存器地址低字节
    6    1    0x00     保留
    7    1    len      数据长度(实际发送字节数)
    8    1    0x02     数据起始标识 (STX)
    9~   len   DATA    数据体
    9+len  1   CHK     校验和(所有 DATA 字节累加和的低 8 位)
    10+len 1   0x03     帧尾 (ETX)
    11+len 1   0x00     填充

    总长 12 + len 字节。

    3.2 扩展命令帧(命令码 > 100)

    读请求

    偏移 大小 值       说明
    0    1    0x01     帧头
    1    1    ADDR     设备地址
    2    1    0x1B     命令组标识
    3    1    0x90     命令组标识
    4    1    0x82     读操作
    5    1    v6       参数(由命令码决定)
    6    1    len      期望接收长度
    7    1    0x00     保留
    8    1    0x03     帧尾

    总长 9 字节。

    写请求

    偏移 大小 值       说明
    0    1    0x01     帧头
    1    1    ADDR     设备地址
    2    1    0x1B     命令组标识
    3    1    0x90     命令组标识
    4    1    0x81     写操作
    5    1    v6       参数
    6    1    len      数据长度
    7~   len   DATA    数据体
    7+len 1    CHK     校验和
    8+len 1    0x03     帧尾
    9+len 1    0x00     填充

    总长 10 + len 字节。

    3.3 应答帧

    • 短应答(ACK)06 00 表示成功。
    • 特殊同步应答04 04 用于同步。
    • 长应答(数据包)
      • 字节 0:状态码(通常忽略,或为 0x01)
      • 字节 1~(n-2):数据
      • 字节 (n-1):校验和(字节 1 到字节 n-2 的累加和低 8 位)
      • 字节 n:0x03 (ETX)

    3.4 重试机制

    发送命令后,最多重试 4 次(外层循环)× 4 次(内层发送尝试)。每次发送失败等待 200ms,若连续 4 次内层失败则退出内层;内层结束后等待 150ms 再进入下一次外循环。


    第四章 命令码与寄存器地址映射表

    4.1 标准命令码表(≤100)

    从 DLL 函数 sub_1000DB73 的 switch 分支提取。v6v7 以十六进制补码表示(负值转换为无符号字节,例 -3 = 0xFD)。

    命令码v6 (hex)v7 (hex)寄存器地址常用数据长度方向关联功能ID
    0FD19FD191读/写
    1FD17FD172(读)/6(写)读/写
    2FD1AFD1A1读/写
    3FD1CFD1C1读/写
    4FD1BFD1B1读/写
    5FD16FD161读/写
    6FD28FD28113
    7FD26FD26114,15,16
    8FD27FD27114,15,16
    9FD29FD29118
    10FD2BFD2B120
    11FD2AFD2A119
    12FD2DFD2D121
    13FD2EFD2E126
    14FD21FD211
    15FD2CFD2C122,23
    16FE44FE442读/写
    17FEC9/C1FE?1型号相关
    18FD20FD201读/写0x64,0x66
    19FD39FD391读/写
    20FD3AFD3A1读/写0x64,0x66
    21FD3BFD3B1读/写0x64
    22FD3CFD3C2读/写0x64
    23FED0FED01用户设置
    24FDF2FDF22用户设置
    25FDF3FDF31读/写
    26FE94FE942用户设置
    27FE95FE951读/写
    28FE9EFE9E1用户设置
    29FD25FD25117
    30FD36FD363读/写90-95
    31FEC8FEC81用户设置
    32FD33FD333(型号1)读/写0x46-0x52
    33FD34FD341状态检测
    34FD40FD401读/写131
    35FD30FD306(型号2)读/写0x46-0x52
    36FD41FD413读/写
    37FD24FD241状态查询
    38FE50FE501状态检测
    39FE20FE201读/写
    40FEDB/D4FE?1型号相关
    41FD93/8EFD?1型号相关
    42FD9D/90FD?1型号相关
    43FE06FE061
    44FD1FFD1F10x64

    4.2 扩展命令码表(>100)

    从 sub_1000DED3 的 switch 提取,命令码对应 ASCII 字符,v6 为帧内参数。

    命令码ASCIIv6 (hex)常用数据长度方向
    101‘e’003
    102‘f’065
    103‘g’101
    118‘v’701读/写
    119‘w’722读/写
    122‘z’801

    第五章 功能 ID 完整映射

    5.1 设定/读取函数导出索引

    导出序号函数名用途
    1FeatureExists检查功能ID是否有效
    2GetIndexString获取功能选项文本
    3GetMaxIndex获取选项最大索引
    8GetValueBool读取布尔型参数
    9GetValueFloat未实现
    10GetValueIndex读取索引型参数
    11GetValueInteger读取整型参数
    12GetValueString读取字符串参数
    13GetValueStruct读取结构体参数
    14InvalidateReads清除读取缓存
    15SetValueBool设置布尔型参数
    16SetValueFloat未实现
    17SetValueIndex设置索引型参数
    18SetValueInteger设置整型参数
    19SetValueString设置字符串参数
    20SetValueStruct设置结构体参数
    21StartProcess执行动作(拍摄、对焦、删除等)
    23WriteDataToCamera将修改的参数写入相机

    5.2 功能 ID → 命令/寄存器映射

    以下列出全部功能 ID(0~147)的精确技术映射,包括类型、设置/读取函数、所属 WriteDataToCamera 的 case、具体的命令码、寄存器地址(v6, v7)或扩展命令标识,以及在配置块中的位操作说明。

    功能 ID类型设置函数读取函数Case命令码 / 寄存器操作描述
    0IntegerSetValueInteger直接设置全局变量 word_10015218通信端口号 (1~4)
    1未使用
    2StringGetValueString直接返回相机型号字符串指针相机型号字符串 (“F90N90” 或 “F90xN90s”)
    3BoolSetValueBool直接设置 word_100122D4相机型号选择 (0=型号1, 1=型号2)
    4IndexGetValueIndex特殊:读取 word_10012170 数组电子手帐数据读取状态/进度
    5IntegerGetValueInteger直接读取 dword_10012000内部状态值(只读)
    6BoolGetValueBool5通过 sub_10008905 读取 word_100122BC未知布尔状态(只读)
    7~12未使用
    13IndexSetValueIndexGetValueIndex1命令 6 → FD28测光方式 (0=矩阵, 1=中央重点, 2=点)
    14IndexSetValueIndexGetValueIndex1命令 7/8 → FD26, FD27;位操作曝光模式 (0=P,1=S,2=A,3=M,4=CP,5-11=变程序)
    15IndexSetValueIndexGetValueIndex1命令 7/8 → FD26, FD27;位操作曝光模式(与14共用寄存器,不同位)
    16BoolSetValueBoolGetValueBool1命令 7/8 → FD26, FD27;位操作曝光模式扩展布尔
    17IndexSetValueIndexGetValueIndex1命令 29 → FD25快门速度 (B门/定时相关)
    18IndexSetValueIndexGetValueIndex1命令 9 → FD29卷片模式 (0=单张, 1=低速连拍, 2=高速连拍)
    19IndexSetValueIndexGetValueIndex1命令 11 → FD2A闪光同步模式 (0=前帘,1=慢速,2=后帘,3=防红眼)
    20IndexSetValueIndexGetValueIndex1命令 10 → FD2B对焦区域 (0=宽区, 1=点)
    21IndexSetValueIndexGetValueIndex1命令 12 → FD2D曝光补偿范围 (-5.0~+5.0 EV)
    22BoolSetValueBoolGetValueBool1命令 15 → FD2C;位操作DX/手动 ISO 选择
    23IndexSetValueIndexGetValueIndex1命令 15 → FD2CISO 感光度 (0=ISO 6, 1=ISO 8 … 30=ISO 6400)
    24IndexGetValueIndex1命令 15 读回 FD2CISO 扩展读取
    26IndexSetValueIndexGetValueIndex1命令 13 → FD2E闪光补偿/特殊设定
    27~28未使用
    29BoolGetValueBool3word_100123F2(通过 sub_100058C7 读取)MF-26 状态(只读)
    30BoolGetValueBool3word_100123F4(通过 sub_100058C7 读取)MF-26 状态(只读)
    31IndexSetValueIndexGetValueIndex3命令 18,20 等,位操作 FD20, FD3A 等MF-26 曝光模式
    32IndexSetValueIndexGetValueIndex3同上MF-26 曝光模式
    33IndexSetValueIndexGetValueIndex3命令 6 → FD28测光方式 (MF-26)
    34IndexSetValueIndexGetValueIndex3命令 9 → FD29卷片模式 (MF-26)
    35IndexSetValueIndexGetValueIndex3命令 11 → FD2A闪光同步模式 (MF-26)
    36IndexSetValueIndexGetValueIndex3命令 10 → FD2B对焦区域 (MF-26)
    37~39未使用
    40IndexSetValueIndexGetValueIndex1/8命令 1 → FD17用户设置库名称
    41IndexGetValueIndex1/8命令 1/7 读取用户设置库当前选择
    42IndexGetValueIndex8命令 43 → FE06镜头光圈值 (F1~F90)
    43IndexSetValueIndexGetValueIndex8命令 43 → FE06 等镜头光圈值(设定)
    44IndexSetValueIndexGetValueIndex1命令 9 → FD29卷片模式(另一入口)
    45IndexSetValueIndexGetValueIndex8命令 45闪光同步(另一设定)
    46BoolSetValueBoolGetValueBool8命令 15 → FD2C位操作对焦模式 (0=手动, 1=AF 等)
    47IndexGetValueIndex8命令 47ISO 显示(备用)
    48IndexSetValueIndexGetValueIndex8命令 10 → FD2B对焦区域(另一入口)
    49IndexSetValueIndexGetValueIndex8命令 49AF 方式 (0=释放优先, 1=对焦优先, 2=手动)
    50IndexGetValueIndex8命令 50对焦状态选项
    51IndexSetValueIndexGetValueIndex8命令 51对焦状态 (0=前焦,1=后焦,2=合焦,3=无法合焦)
    52IndexSetValueIndexGetValueIndex8命令 52ON/OFF 开关
    53IndexSetValueIndexGetValueIndex8命令 53曝光补偿值 (-20.0~+20.0)
    54IndexSetValueIndexGetValueIndex8命令 54曝光补偿值(另一组)
    55IndexSetValueIndexGetValueIndex8命令 55胶片计数器 (E, 0~99)
    56IndexSetValueIndexGetValueIndex8命令 56电池状态 (0=满电, 1=低电量)
    57IndexGetValueIndex8命令 57特定索引
    58IndexGetValueIndex8命令 58曝光补偿范围 (-5.0~+5.0 EV)
    59IndexSetValueIndexGetValueIndex8命令 59镜头焦距 (5mm~7600mm)
    60IndexSetValueIndexGetValueIndex8命令 60镜头焦距(另一组)
    61IndexSetValueIndexGetValueIndex8命令 61镜头焦距(另一组)
    62IndexGetValueIndex8命令 62光圈值(另一组)
    63IndexGetValueIndex8命令 63特定索引
    64IndexGetValueIndex8命令 64光圈值(另一组)
    65IndexGetValueIndex1/8命令 1 读取用户库读取
    66IndexSetValueIndexGetValueIndex1/8命令 6 → FD28测光方式(另一入口)
    67IndexSetValueIndexGetValueIndex8命令 67闪光同步(另一组)
    68IndexSetValueIndexGetValueIndex8命令 68镜头焦距(另一组)
    69BoolSetValueBoolGetValueBool4配置块位操作 (命令32/35)蜂鸣音开关(恢复默认用)
    70 (0x46)BoolSetValueBoolGetValueBool4配置块位操作 (命令32/35)合焦音开关
    71 (0x47)BoolSetValueBoolGetValueBool4配置块位操作 (命令32/35)DX 优先设定
    72 (0x48)BoolSetValueBoolGetValueBool4配置块位操作 (命令32/35)AE/AF 同时锁定
    73 (0x49)BoolSetValueBoolGetValueBool4配置块位操作 (命令32/35)AF-C 对焦优先
    74 (0x4A)BoolSetValueBoolGetValueBool4配置块位操作 (命令32/35)AF-S 释放优先
    75 (0x4B)BoolSetValueBoolGetValueBool4配置块位操作 (命令32/35)AF-S 帧间 AF 驱动
    76 (0x4C)BoolSetValueBoolGetValueBool4配置块位操作 (命令32/35)0 帧数据印记
    77 (0x4D)BoolSetValueBoolGetValueBool4配置块位操作 (命令32/35)测光偏差显示
    78 (0x4E)BoolSetValueBoolGetValueBool4配置块位操作 (命令32/35)A 模式简易曝光补偿
    79 (0x4F)BoolSetValueBoolGetValueBool4配置块位操作 (命令32/35)(保留)
    80 (0x50)BoolSetValueBoolGetValueBool4配置块位操作 (命令32/35)闪光灯相关设定
    81 (0x51)BoolSetValueBoolGetValueBool4配置块位操作 (命令32/35)闪光灯相关设定
    82 (0x52)BoolSetValueBoolGetValueBool4配置块位操作 (命令32/35)闪光灯相关设定
    83IndexSetValueIndexGetValueIndex4配置块 (命令32/35)闪光灯模式
    84IndexSetValueIndexGetValueIndex4配置块 (命令32/35)长时间曝光时间 (4秒~60秒/B门)
    85IndexSetValueIndexGetValueIndex4配置块 (命令32/35)同步释放模式
    86IndexSetValueIndexGetValueIndex4配置块 (命令32/35)(预留)
    87IndexSetValueIndexGetValueIndex4配置块 (命令32/35)自拍/间隔时间
    88StartProcess(0x58) 调用恢复默认设置(操作码)
    89StartProcess(0x89) 调用触发拍摄(操作码)
    90IndexSetValueIndexGetValueIndex7命令 30 → FD36闪光灯/自定义程序点 (快门)
    91IndexSetValueIndexGetValueIndex7命令 30 → FD36闪光灯/自定义程序点 (光圈)
    92IndexSetValueIndexGetValueIndex7命令 30 → FD36同上
    93IndexSetValueIndexGetValueIndex7命令 30 → FD36同上
    94IndexSetValueIndexGetValueIndex7命令 30 → FD36同上
    95IndexSetValueIndexGetValueIndex7命令 30 → FD36同上
    96StartProcess(0x60) 调用闪光灯操作(操作码)
    97IndexGetValueIndex8命令 97微调值
    98IndexSetValueIndexGetValueIndex8/11命令 98微调值 (0.3~2.0)
    99IndexSetValueIndexGetValueIndex8/11命令 99曝光补偿步长 (-3.0~+1.0)
    100Bool8(保留)
    101IntegerGetValueInteger8读取多个配置寄存器配置块值(只读)
    102~105未使用
    106IndexSetValueIndexGetValueIndex9扩展命令 103, 122 等时区城市 (1~24)
    107未使用
    108Index9扩展命令时区城市(备用)
    109IndexSetValueIndexGetValueIndex10扩展命令时差数值 (-10~+12, -11)
    110IndexSetValueIndexGetValueIndex10扩展命令时差数值 (-11)
    111~114未使用
    115IndexGetValueIndex11扩展命令模式选择
    116IndexSetValueIndexGetValueIndex11扩展命令微调值 (另一入口)
    117未使用
    118IndexSetValueIndexGetValueIndex11扩展命令曝光补偿步长 (同 99)
    119IndexSetValueIndexGetValueIndex11扩展命令特殊设定(自拍/闪光相关)
    120~123未使用
    124StructGetValueStruct11扩展命令读取结构体 (2 字段)
    125StructGetValueStruct11扩展命令读取结构体 (3 字段)
    126未使用
    127~12912扩展命令保留
    130StructGetValueStruct11扩展命令读取结构体 (3 字段)
    131IndexSetValueIndexGetValueIndex5命令 34 → FD40数据存储模式 (0/69/78/95)
    132IndexSetValueIndexGetValueIndex5命令 5 → FD34 bit7数据印记 / 特殊设定位
    133命令 133(内部全消去)内部使用
    134IntegerGetMinMaxInteger只读,范围 0-9999胶片编号
    135未使用
    136StartProcess(0x88) 调用指针复位/状态初始化
    137StartProcess(0x89) 调用触发拍摄(同 89)
    138未使用
    139IndexGetIndexString6字符串资源自定义功能名称/选项
    140~145未使用
    146StructGetValueStruct6扩展命令相机信息(型号、序列号等)
    147StructGetValueStruct6/5扩展命令对焦数据读取

    注释

    • 配置块位操作:功能 ID 69~82 的具体位定义已在第七章详细列出。每个布尔值对应配置块(命令32/35)的某一位,通过读-修改-写流程改变。
    • 扩展命令:功能 ID 106~130 使用命令码 >100 的扩展命令(格式 1B 90 cmd v6 len),具体 v6 和 len 见第四章扩展命令表。
    • 操作码:88, 89, 96, 136, 137 等不直接对应设置/读取函数,而是由 StartProcess 分发执行,内部命令序列已在前文各章详述。
    • 表中粗体为软件中直接面向用户的主要功能。
    • 未使用的功能 ID 在 DLL 中返回错误 2(不支持)。
    • 对于131:
    • 这些值直接写入 FD40 寄存器,对应相机的“数据存储模式”0 (0x00) → 不存储数据(Disabled)
    • 69 (0x45) → 存储最小数据量(Minimum)
    • 78 (0x4E) → 存储中间数据量(Intermediate)
    • 95 (0x5F) → 存储最大数据量(Maximum)
      该映射与我们之前整理的 FD40 寄存器功能一致,且与 DLL 资源中对应的字符串(“記憶しない”、“最小情報量記憶”等)吻合。

    第六章 特殊操作流程

    6.1 拍摄(快门释放)

    • 操作码:StartProcess(0x89)
    • DLL 函数:sub_10007BB0 → 型号1调用 sub_10007C4B,型号2调用 sub_10007CE0
    • 命令序列(以型号1为例):
      1. 读 FD20FD24FD40 获取状态。
      2. 构造写命令 01 10 81 00 FD 41 00 01 02 <data> <cs> 03 00,数据为 0x42(’B’)及校验。
      3. 等待 ACK,再读 FD20 和 FD24 完成清理。

    6.2 自动对焦

    • DLL 中未作为独立导出命令,但 EXE 的“AFの実行”菜单调用 StartProcess,其内部可触发对焦操作,对应的相机命令为 01 20 86 00 00 00 00 00 03(也可被直接发送)。

    6.3 用户设置库(保存/调用)

    • 保存到库:StartProcess(0x25) → sub_10005B79
    • 从库读取:StartProcess(0x26) → sub_10005A2EStartProcess(0x27) → sub_10005D47
    • 这些函数内部发送命令 18、20、31、119 以及 sub_1000E07D/sub_1000E183 等位操作,并读写 FD17、FD26、FD27 等寄存器。

    6.4 恢复默认设置

    • StartProcess(0x58)
    • 先将内部变量设为一组出厂默认值(详见代码中的连续赋值),然后调用 sub_100065A7,发送命令 119 或 18,最后写入配置块(命令 32 或 35)和公共配置(命令 20)。

    6.5 胶卷数据读取

    • 入口:sub_10006F09 → 型号1调用 sub_10007042,型号2调用 sub_1000749C
    • 步骤:
      1. 读 FD20FD24FD40 验证状态和存储模式。
      2. 读 FD41 获取帧数 v19 和相关信息。
      3. 根据 v19 计算动态地址:addr = 2 * v19(可能进行回绕修正)。
      4. 调用 sub_10007A14 发送动态读命令,接收数据包。
      5. 数据以 0xFF 作为卷结束标记,ISO 索引在最后。
      6. 解析帧数据(每帧 3/4/6 字节,取决于存储模式),字段包括快门速度、光圈、曝光模式、测光方式、焦距、曝光补偿等。
      7. 发送确认命令 0x42(’B’)完成本次读取。

    6.6 删除胶卷数据

    • 全消去StartProcess(0x1C) → sub_1000A9CD
      • 检查相机内是否有数据,若无则发送命令 133,相机清空整个环形缓冲区。
    • 部分删除(移动读取指针)StartProcess(0x1B) → sub_1000A7E1
      • 发送命令 134,并操作命令 19(FD39)的位 3,使相机内部的待读指针前移,等效于删除已读取的卷。
    • 指针/状态复位StartProcess(0x88) → sub_10008403
      • 型号1:写 FD41 特定值,清除 FED6 的 bit0,清零 FD40。
      • 型号2:读 FD00,写命令 ‘B’,清除 FECE 的 bit0,清零 FD40。

    第七章 配置块位定义

    从 sub_1000694E 提取(以型号1配置块命令32为例):

    • v34 低字节 = a3[10]
    • v34 高字节:
      • bit0: a3[13] 控制
      • bit1: a3[2] 控制
      • bit2: a3[11] 控制
      • bit3: a3[3] 控制
      • bit5: a3[9] 控制
      • bit6: a3[8] 控制
    • v35 低字节:
      • bit2: a3[12] (1或3置1)
      • bit3: a3[12] (2或3置1)
      • bit4: a3[5] 控制
      • bit5: a3[7] 控制
      • bit7: a3[6] 控制

    型号2配置块(命令35)有类似的位定义,详见原始反编译代码。


    第八章 错误码表

    错误码 (hex)含义
    0x19 (25)参数无效
    0x1D (29)数据块读取校验失败
    0x25 (37)对焦扫描初始化失败
    0x28 (40)功能不可用/状态错误
    0x29 (41)功能不支持
    0x32 (50)通用“不支持此操作”
    0x33 (51)参数冲突
    0x34 (52)参数范围错误
    0x35 (53)参数范围错误
    0x37 (55)模式不匹配
    0x45 (69)初始化失败
    -18校验和不匹配
    -27未收到 ACK (0x06)
    -30接收超时
    -35相机状态错误

    所有错误均通过全局变量 dword_10015220 返回。


    第九章 数值编码与资源字符串

    相机参数值到显示文本的映射完全来自 DLL/EXE 资源,无需第三方数据。例如:

    • 快门速度:EXE 资源 17408 开始 "30"",连续列出 Bulb、30″、25″、… 1/8000。
    • 光圈:资源 17664 起 F1、F1.1、… F90。
    • ISO:资源 19968 起 ISO 6、ISO 8、… ISO 6400。
    • 曝光补偿:资源 19712 起 0.0、-0.2、… +20.0。
    • 测光方式:资源 22190 起 “多分区测光”、”中央重点测光”、”点测光”。
    • 曝光模式:资源 17920 起 “P:多程序自动”、”S:快门优先” 等。

    所有字符串均已在 EXE 和 DLL 的资源提取中完整列出,并翻译为中文。


    第十章 与第三方文档的主要差异说明(主要指f90x-serial-documentation/f90x-serial-documentation.md at trunk · antarktikali/f90x-serial-documentation · GitHubgIcon source 项目等)

    • 地址字节:第三方固定使用 0x20,我们根据 DLL 逻辑动态使用 0x10 或 0x20
    • 扩展命令格式:第三方可能省略 0x90,我们严格采用 DLL 的 1B 90 cmd 格式。
    • 环形缓冲区指针:第三方直接操作 FD44/FD46,DLL 通过动态地址计算和 FED6/FECE 等寄存器间接实现,功能等效。
    • 部分只读状态寄存器(如 FE20、FE22)在 DLL 中未出现立即数,但其信息已通过功能 ID 提供,不影响使用。
    • 所有命令码和寄存器映射完全基于 DLL 的 sub_1000DB73sub_1000DED3,比第三方仅列举的片段更为完整和准确。

    附录 A 数据块格式(胶卷记录)

    卷头:2 字节 0x5A58(“ZX”)
         2 字节 元数据
         2 字节 0x0000
         2 字节 卷号(BCD)
    帧数据:N × (3/4/6 字节) 取决于存储模式
         1 字节 0xFF(结束标记)
         1 字节 ISO 索引

    每帧数据字段(6 字节最大模式):快门速度、光圈、曝光模式、测光模式、闪光同步、焦距、曝光补偿、闪光补偿。具体编码与命令寄存器返回的原始值一致,可参照资源字符串表解析。

    补充:

    • 在 sub_10007042 中,读取胶卷数据时使用的动态地址 v28 = 2 * v19,并且有如下回绕处理:
    v15 = 总字节数 + v28;
    if ( v15 >= 0x200u )
        v15 -= 444;   // 回绕修正,444 = 0x1BC
    • 这说明环形缓冲区的大小为 512 (0x200) 字节,当读取指针超过 0x1FF 时,会回绕到 0x200 - 444 = 0x44(实际基址由相机固定)。DLL 内部自动处理了回绕,开发者只需按此逻辑实现即可。
  • 【摄影】穷鬼如何进入毒德大学?

    Leica FOTOS,徕卡公司推出的一款徕卡相机管理软件,其内置了多款LUT供高贵的SL3及Q3使用,让我等穷鬼十分眼馋。那么穷鬼如何迈入毒德大学呢?

    方法一:HALD LUT,受限于HALD 色块图的分辨率,提取的LUT总让我等穷鬼觉得不够德味。

    方法二:反编译Leica FOTOS,我们将会得到纯正的徕卡LUT,但是万恶的资本主义徕卡,在两年前就发现了这个BUG,为了阻止穷鬼不购买SL3就获得富哥们LUT,将软件中的LUT加密,文件也变为了.CUBE.enc后缀。

    但是穷鬼们没有徕卡,有电脑哇↓

    1、分析加密结构
    将 Leica FOTOS 的 APK 解包,寻找资源文件夹,assets\looks\cube中可看到一批 .CUBE.enc 文件。直接查看其编码,开头 Base64 字符串 +] 分隔符 +密文,反编译寻找lut相关的类,基本确认采用AES加密,开头字符串为初始化向量。

    2、寻找密钥
    不管徕卡的程序员是不是外包的,让人遗憾的是他们都没有把Key明文存储。穷鬼们还是要继续努力,进一步发现Key通过JNI 放在底层 libnative-lib.so 动态库中,并通过一个名为 NativeKeyProvider 的类进行调用。

    3、拦截getkey
    安卓人一般用安卓机,碰巧安卓机还是ROOT的,因此穷鬼们可以使用 Frida 对程序进行动态 Hook,直接拦截 getKey() 方法。
    成功 Hook 后,程序会返回实际使用的密钥:
    bGVp■■■■■■■■■■■■■HRfa2V5
    Base64 解码后内容为:
    leica_camera_encrypt_key(好直白)

    4、解密 LUT 文件
    拿到 Key + IV 后,可使用 Python 的 cryptography 库,实现 AES-GCM 解密即可,将 .CUBE.enc 批量还原为标准 .cube 文件。

    最终,我们迈进了毒德大学。

    就是你这个徕卡LUT怎么还是个17x17x17的,嗯,感觉又不够德味了。

  • 【ESP32】ESP32S3通过USB+WIFI透传串口GPS信息并自动切换波特率

    基于微雪ESP32-S3-Touch-LCD-1.47,基于ESP-IDF 5.5.x,该代码目的适配ATGM和UBLOX部分GPS模块,可以热切换115200、38400波特率,实测115200波特率下可以实现5Hz下的NMEA语句持续平稳透传,10Hz下不太稳定。

    #include <stdio.h>
    #include <string.h>
    #include <stdbool.h>
    
    #include "freertos/FreeRTOS.h"
    #include "freertos/task.h"
    #include "freertos/semphr.h"
    #include "freertos/ringbuf.h"
    
    #include "driver/uart.h"
    #include "driver/usb_serial_jtag.h"
    
    #include "esp_log.h"
    #include "esp_wifi.h"
    #include "esp_event.h"
    #include "nvs_flash.h"
    #include "lwip/sockets.h"
    #include "esp_timer.h"
    
    /* ================== 配置常量 ================== */
    #define TAG                "GPS_BRIDGE"
    
    // 物理串口
    #define GPS_UART_NUM       UART_NUM_1
    #define GPS_TX_PIN         43
    #define GPS_RX_PIN         44
    
    // 缓存大小
    #define UART_BUF_SIZE      2048
    #define RINGBUF_SIZE       (16 * 1024)
    
    // 网络配置
    #define TCP_PORT           8080
    #define WIFI_SSID          "ESP32S3_GPS"
    #define WIFI_PASS          "12345678"
    
    // GPS参数
    static const int baud_list[] = {115200, 38400};
    #define BAUD_NUM            (sizeof(baud_list) / sizeof(baud_list[0]))
    #define DETECT_TIMEOUT_MS   1500  // 每个波特率停留尝试时间
    #define LOSS_TIMEOUT_SEC    5     // 5秒无NMEA数据则判定丢失
    
    /* ================== 全局句柄 ================== */
    static RingbufHandle_t uart_rb;
    static SemaphoreHandle_t sock_mutex;
    static SemaphoreHandle_t uart_mutex;
    
    static int  client_sock = -1;
    static int  current_baud_idx = 0;
    static bool baud_locked = false;
    
    /* ================== 工具函数 ================== */
    
    // 安全地重新初始化 UART
    static void uart_reinit(int baudrate)
    {
        xSemaphoreTake(uart_mutex, portMAX_DELAY);
        
        if (uart_is_driver_installed(GPS_UART_NUM)) {
            uart_driver_delete(GPS_UART_NUM);
        }
    
        uart_config_t cfg = {
            .baud_rate = baudrate,
            .data_bits = UART_DATA_8_BITS,
            .parity    = UART_PARITY_DISABLE,
            .stop_bits = UART_STOP_BITS_1,
            .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
            .source_clk = UART_SCLK_DEFAULT,
        };
    
        // 安装驱动:缓冲区设为 UART_BUF_SIZE 的 2 倍
        uart_driver_install(GPS_UART_NUM, UART_BUF_SIZE * 2, 0, 0, NULL, 0);
        uart_param_config(GPS_UART_NUM, &cfg);
        uart_set_pin(GPS_UART_NUM, GPS_TX_PIN, GPS_RX_PIN, -1, -1);
    
        ESP_LOGW(TAG, "UART re-init: %d baud, scanning for GPS...", baudrate);
        
        xSemaphoreGive(uart_mutex);
    }
    
    // 协议特征检测
    static bool is_valid_nmea(const uint8_t *buf, int len)
    {
        if (len < 6) return false;
        // NMEA 标准报文必须以 $ 开头
        if (buf[0] != '$') return false;
    
        // 检查常见的 NMEA 类型标识符
        if (memmem(buf, len, "GGA", 3) || 
            memmem(buf, len, "RMC", 3) || 
            memmem(buf, len, "GSA", 3) ||
            memmem(buf, len, "GSV", 3)) {
            return true;
        }
        return false;
    }
    
    /* ================== 核心任务 ================== */
    
    /**
     * 任务1: UART 接收与波特率自动匹配
     * 优先级: 高 (10)
     */
    static void gps_rx_task(void *arg)
    {
        static uint8_t rx_buf[UART_BUF_SIZE];
        int64_t last_baud_switch = 0;
        int64_t last_valid_data  = 0;
    
        uart_reinit(baud_list[current_baud_idx]);
        last_baud_switch = esp_timer_get_time();
    
        while (1) {
            // 使用锁保护 UART 读取过程
            xSemaphoreTake(uart_mutex, portMAX_DELAY);
            int len = uart_read_bytes(GPS_UART_NUM, rx_buf, sizeof(rx_buf), pdMS_TO_TICKS(20));
            xSemaphoreGive(uart_mutex);
    
            int64_t now = esp_timer_get_time();
    
            if (len > 0) {
                if (is_valid_nmea(rx_buf, len)) {
                    last_valid_data = now;
                    if (!baud_locked) {
                        baud_locked = true;
                        ESP_LOGI(TAG, ">>> GPS Locked @ %d baud <<<", baud_list[current_baud_idx]);
                    }
                }
                
                // 只有锁定了正确的波特率才转发数据,过滤扫描时的乱码
                if (baud_locked) {
                    xRingbufferSend(uart_rb, rx_buf, len, 0);
                }
            }
    
            /* 状态机切换逻辑 */
            if (!baud_locked) {
                // 未锁定时:超时轮换波特率
                if ((now - last_baud_switch) / 1000 > DETECT_TIMEOUT_MS) {
                    current_baud_idx = (current_baud_idx + 1) % BAUD_NUM;
                    uart_reinit(baud_list[current_baud_idx]);
                    last_baud_switch = now;
                }
            } else {
                // 已锁定时:长时间无有效 NMEA 信号判定为丢失
                if ((now - last_valid_data) / 1000000 > LOSS_TIMEOUT_SEC) {
                    ESP_LOGE(TAG, "GPS signal lost, restarting scan...");
                    baud_locked = false;
                    last_baud_switch = now;
                }
            }
        }
    }
    
    /**
     * 任务2: 数据多路分发 (UART -> USB & WiFi)
     * 优先级: 中 (6)
     */
    static void tx_dispatcher_task(void *arg)
    {
        size_t len;
        uint8_t *data;
    
        while (1) {
            // 从环形缓冲区提取数据,最大等待
            data = (uint8_t *)xRingbufferReceive(uart_rb, &len, portMAX_DELAY);
            if (!data) continue;
    
            // 1. 发送到物理 USB (CDC)
            if (usb_serial_jtag_is_connected()) {
                usb_serial_jtag_write_bytes(data, len, 0);
            }
    
            // 2. 发送到 TCP 客户端
            if (xSemaphoreTake(sock_mutex, 0) == pdTRUE) {
                if (client_sock != -1) {
                    // 使用非阻塞发送,防止 WiFi 拥塞卡住串口读写
                    int sent = send(client_sock, data, len, MSG_DONTWAIT);
                    if (sent < 0 && (errno != EAGAIN && errno != EWOULDBLOCK)) {
                        ESP_LOGW(TAG, "TCP send error, client might be dead");
                    }
                }
                xSemaphoreGive(sock_mutex);
            }
    
            vRingbufferReturnItem(uart_rb, data);
        }
    }
    
    /**
     * 任务3: 反向路径 USB -> UART
     */
    static void usb_rx_task(void *arg)
    {
        static uint8_t buf[512];
        while (1) {
            int len = usb_serial_jtag_read_bytes(buf, sizeof(buf), pdMS_TO_TICKS(10));
            if (len > 0 && baud_locked) {
                xSemaphoreTake(uart_mutex, portMAX_DELAY);
                uart_write_bytes(GPS_UART_NUM, (char *)buf, len);
                xSemaphoreGive(uart_mutex);
            }
        }
    }
    
    /**
     * 任务4: TCP 服务端 (WiFi -> UART)
     */
    static void tcp_server_task(void *arg)
    {
        struct sockaddr_in addr = {
            .sin_family = AF_INET,
            .sin_port = htons(TCP_PORT),
            .sin_addr.s_addr = htonl(INADDR_ANY)
        };
    
        int listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
        int opt = 1;
        setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
        bind(listen_sock, (struct sockaddr *)&addr, sizeof(addr));
        listen(listen_sock, 1);
    
        static uint8_t buf[512];
        while (1) {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int sock = accept(listen_sock, (struct sockaddr *)&client, &len);
            if (sock < 0) continue;
    
            xSemaphoreTake(sock_mutex, portMAX_DELAY);
            if (client_sock != -1) close(client_sock);
            client_sock = sock;
            xSemaphoreGive(sock_mutex);
    
            ESP_LOGI(TAG, "New TCP client connected");
    
            while (1) {
                int r = recv(sock, buf, sizeof(buf), 0);
                if (r <= 0) break;
    
                if (baud_locked) {
                    xSemaphoreTake(uart_mutex, portMAX_DELAY);
                    uart_write_bytes(GPS_UART_NUM, (char *)buf, r);
                    xSemaphoreGive(uart_mutex);
                }
            }
    
            ESP_LOGI(TAG, "TCP client disconnected");
            xSemaphoreTake(sock_mutex, portMAX_DELAY);
            if (client_sock == sock) client_sock = -1;
            close(sock);
            xSemaphoreGive(sock_mutex);
        }
    }
    
    /* ================== 系统初始化 ================== */
    
    static void wifi_init_softap(void)
    {
        ESP_ERROR_CHECK(esp_netif_init());
        ESP_ERROR_CHECK(esp_event_loop_create_default());
        esp_netif_create_default_wifi_ap();
    
        wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
        ESP_ERROR_CHECK(esp_wifi_init(&cfg));
    
        wifi_config_t ap_cfg = {
            .ap = {
                .ssid = WIFI_SSID,
                .ssid_len = strlen(WIFI_SSID),
                .password = WIFI_PASS,
                .channel = 1,
                .max_connection = 4,
                .authmode = WIFI_AUTH_WPA2_PSK
            }
        };
    
        ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
        ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &ap_cfg));
        ESP_ERROR_CHECK(esp_wifi_start());
    }
    
    void app_main(void)
    {
        // 1. 初始化存储
        esp_err_t ret = nvs_flash_init();
        if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
            ESP_ERROR_CHECK(nvs_flash_erase());
            ret = nvs_flash_init();
        }
        ESP_ERROR_CHECK(ret);
    
        // 2. 初始化同步组件与缓冲区
        uart_rb    = xRingbufferCreate(RINGBUF_SIZE, RINGBUF_TYPE_BYTEBUF);
        sock_mutex = xSemaphoreCreateMutex();
        uart_mutex = xSemaphoreCreateMutex();
    
        // 3. 初始化 USB 驱动
        usb_serial_jtag_driver_config_t usb_cfg = {
            .rx_buffer_size = 2048,
            .tx_buffer_size = 2048
        };
        usb_serial_jtag_driver_install(&usb_cfg);
    
        // 4. 初始化 WiFi
        wifi_init_softap();
    
        // 5. 创建任务集群
        // 核心 0 处理实时性最强的串口 IO
        xTaskCreatePinnedToCore(gps_rx_task,        "gps_rx",  4096, NULL, 10, NULL, 0);
        xTaskCreatePinnedToCore(usb_rx_task,        "usb_rx",  4096, NULL, 5,  NULL, 0);
    
        // 核心 1 处理数据分发和网络服务
        xTaskCreatePinnedToCore(tx_dispatcher_task, "tx_dis",  4096, NULL, 6,  NULL, 1);
        xTaskCreatePinnedToCore(tcp_server_task,    "tcp_srv", 4096, NULL, 4,  NULL, 1);
    
        ESP_LOGI(TAG, "Bridge System Started. Use TCP 192.168.4.1:8080 or USB JTAG Serial.");
    }

  • 【HIFI】弗西音频FOSI AUDIO SK02拆解及 I²C 上电数据

    1、拆解及部分细节

    如图所示,板子还是比较简洁规整的:

    • DAC采用单颗ESS 的 ES9038Q2M
    • 运放部分四颗SGM 8262,测量±V引脚,工作电压差为15.8V左右(板子上标却是±6V),工作发热量巨大,为运放供电的是一颗 SGM6623 4.4A 微型升压转换器。
    • 运放之后还有两颗模拟开关,在B站关于该厂ZD3的视频下看到有人讨论了同款芯片作用,也是来自SGM的芯片——SGM3710(不过官网已经找不到这颗芯片的PDF,只有第三方来源)。
    • USB音频控制器采用SAVITECH的SA9312,可以安装BRAVO-HD驱动,实现ASIO输出。
    • 单片机使用STC的8H1K28
    • 音量旋钮旁边还有一颗TI的LM358放大器。
    • 主板看起来是四层PCB,似乎做了数字、模拟部分的隔离设计,主板与外壳接地的弹性触点焊接歪的离谱,是有什么神秘设计么XD

    2、监听I²C得到上电逻辑

    利用逻辑分析仪,分析上电后的通讯:

    3、SK02对ES9038Q2M的寄存器操作:

    通过Gemini分析,供参考:

    寄存器操作数据 (hex)说明
    64读取0x72Chip ID=0x1C,automute_status=1(自动静音触发),lock_status=0(DPLL未锁定)
    8写入0x99GPIO 配置:gpio2_cfg=9 (Analog Input Shutdown),gpio1_cfg=9 (Analog Input Shutdown)
    12写入0xFFASRC/DPLL 带宽:dpll_bw_serial=15,高带宽;dpll_bw_dsd=15,高带宽
    14写入0x8ASoft Start 配置:soft_start=1(正常启动),soft_start_on_lock=0(解锁时不自动升压),soft_start_time=10
    22写入0x30THD Compensation C2 高字节=0x30(16-bit signed coefficient 高位)
    23写入0x00THD Compensation C2 低字节=0x00(16-bit signed coefficient 低位)
    24写入0x80THD Compensation C3 高字节=0x80(16-bit signed coefficient 高位)
    25写入0x01THD Compensation C3 低字节=0x01(16-bit signed coefficient 低位)
    64读取0x72Chip ID=0x1C,automute_status=1,lock_status=0(重复读取确认)
    8写入0x99GPIO 配置再次设置:gpio2_cfg=9,gpio1_cfg=9(保持 Analog Input Shutdown)
    12写入0xFFASRC/DPLL 带宽再次设置:dpll_bw_serial=15,高带宽;dpll_bw_dsd=15,高带宽
    14写入0x8ASoft Start 配置再次设置:soft_start=1,soft_start_on_lock=0,soft_start_time=10
    22写入0x30THD Compensation C2 高字节=0x30
    23写入0x00THD Compensation C2 低字节=0x00
    24写入0x80THD Compensation C3 高字节=0x80
    25写入0x01THD Compensation C3 低字节=0x01
    1写入0x84Input Selection:serial_length=32-bit,serial_mode=I2S,auto_select=DSD/Serial自动选择,input_select=serial
    8写入0x99GPIO 配置:gpio2_cfg=9 (Input Select),gpio1_cfg=9 (Input Select)
    15写入0x5CVolume Control 1=0x5C (-46dB)
    16写入0x5CVolume Control 2=0x5C (-46dB)
    7读取0x80读取寄存器 7:确认当前滤波器配置为 apodizing fast roll-off,未静音
    7写入0x80Filter & Mute:filter_shape=apodizing fast roll-off,bypass_osf=0(内部滤波器),mute=0

    上电后的完整原始数据:

    6653126-6653410 I2C: Address/data: Address write: 48
    6653073-6653073 I2C: Address/data: Start
    6653410-6653451 I2C: Address/data: Write
    6653451-6653492 I2C: Address/data: ACK
    6653493-6653818 I2C: Address/data: Data write: 40
    6653818-6653859 I2C: Address/data: ACK
    6653856-6653856 I2C: Address/data: Start repeat
    6653909-6654193 I2C: Address/data: Address read: 48
    6654193-6654233 I2C: Address/data: Read
    6654234-6654274 I2C: Address/data: ACK
    6654262-6654477 I2C: Address/data: Data read: 72
    6654491-6654518 I2C: Address/data: NACK
    6654527-6654527 I2C: Address/data: Stop
    6654564-6654564 I2C: Address/data: Start
    6654617-6654901 I2C: Address/data: Address write: 48
    6654901-6654941 I2C: Address/data: Write
    6654942-6654982 I2C: Address/data: ACK
    6654984-6655308 I2C: Address/data: Data write: 08
    6655309-6655349 I2C: Address/data: ACK
    6655358-6655682 I2C: Address/data: Data write: 99
    6655683-6655723 I2C: Address/data: ACK
    6655723-6655723 I2C: Address/data: Stop
    6656014-6656014 I2C: Address/data: Start
    6656068-6656352 I2C: Address/data: Address write: 48
    6656352-6656393 I2C: Address/data: Write
    6656393-6656434 I2C: Address/data: ACK
    6656435-6656760 I2C: Address/data: Data write: 0C
    6656760-6656801 I2C: Address/data: ACK
    6656809-6657134 I2C: Address/data: Data write: FF
    6657134-6657175 I2C: Address/data: ACK
    6657174-6657174 I2C: Address/data: Stop
    6657411-6657411 I2C: Address/data: Start
    6657465-6657749 I2C: Address/data: Address write: 48
    6657749-6657790 I2C: Address/data: Write
    6657790-6657831 I2C: Address/data: ACK
    6657831-6658157 I2C: Address/data: Data write: 0E
    6658156-6658197 I2C: Address/data: ACK
    6658205-6658531 I2C: Address/data: Data write: 8A
    6658530-6658571 I2C: Address/data: ACK
    6658571-6658571 I2C: Address/data: Stop
    6658808-6658808 I2C: Address/data: Start
    6658862-6659146 I2C: Address/data: Address write: 48
    6659146-6659187 I2C: Address/data: Write
    6659186-6659227 I2C: Address/data: ACK
    6659228-6659554 I2C: Address/data: Data write: 16
    6659553-6659594 I2C: Address/data: ACK
    6659602-6660036 I2C: Address/data: Data write: 30
    6659981-6660076 I2C: Address/data: ACK
    6660022-6660022 I2C: Address/data: Stop
    6660259-6660259 I2C: Address/data: Start
    6660312-6660597 I2C: Address/data: Address write: 48
    6660597-6660638 I2C: Address/data: Write
    6660637-6660678 I2C: Address/data: ACK
    6660679-6661005 I2C: Address/data: Data write: 17
    6661004-6661045 I2C: Address/data: ACK
    6661053-6661379 I2C: Address/data: Data write: 00
    6661378-6661419 I2C: Address/data: ACK
    6661419-6661419 I2C: Address/data: Stop
    6661656-6661656 I2C: Address/data: Start
    6661709-6661994 I2C: Address/data: Address write: 48
    6661994-6662035 I2C: Address/data: Write
    6662034-6662075 I2C: Address/data: ACK
    6662076-6662402 I2C: Address/data: Data write: 18
    6662401-6662442 I2C: Address/data: ACK
    6662450-6662776 I2C: Address/data: Data write: 80
    6662775-6662816 I2C: Address/data: ACK
    6662816-6662816 I2C: Address/data: Stop
    6663053-6663053 I2C: Address/data: Start
    6663106-6663391 I2C: Address/data: Address write: 48
    6663391-6663432 I2C: Address/data: Write
    6663431-6663472 I2C: Address/data: ACK
    6663473-6663799 I2C: Address/data: Data write: 19
    6663798-6663839 I2C: Address/data: ACK
    6663903-6664229 I2C: Address/data: Data write: 01
    6664228-6664269 I2C: Address/data: ACK
    6664269-6664269 I2C: Address/data: Stop
    6665462-6665462 I2C: Address/data: Start
    6665515-6665799 I2C: Address/data: Address write: 48
    6665799-6665839 I2C: Address/data: Write
    6665840-6665880 I2C: Address/data: ACK
    6665882-6666207 I2C: Address/data: Data write: 40
    6666207-6666248 I2C: Address/data: ACK
    6666245-6666245 I2C: Address/data: Start repeat
    6666298-6666582 I2C: Address/data: Address read: 48
    6666582-6666622 I2C: Address/data: Read
    6666623-6666663 I2C: Address/data: ACK
    6666651-6666866 I2C: Address/data: Data read: 72
    6666880-6666907 I2C: Address/data: NACK
    6666916-6666916 I2C: Address/data: Stop
    6666953-6666953 I2C: Address/data: Start
    6667006-6667291 I2C: Address/data: Address write: 48
    6667291-6667332 I2C: Address/data: Write
    6667331-6667372 I2C: Address/data: ACK
    6667373-6667697 I2C: Address/data: Data write: 08
    6667698-6667738 I2C: Address/data: ACK
    6667747-6668125 I2C: Address/data: Data write: 99
    6668126-6668166 I2C: Address/data: ACK
    6668166-6668166 I2C: Address/data: Stop
    6668404-6668404 I2C: Address/data: Start
    6668457-6668742 I2C: Address/data: Address write: 48
    6668742-6668783 I2C: Address/data: Write
    6668782-6668823 I2C: Address/data: ACK
    6668824-6669150 I2C: Address/data: Data write: 0C
    6669149-6669190 I2C: Address/data: ACK
    6669198-6669524 I2C: Address/data: Data write: FF
    6669523-6669564 I2C: Address/data: ACK
    6669564-6669564 I2C: Address/data: Stop
    6669801-6669801 I2C: Address/data: Start
    6669855-6670139 I2C: Address/data: Address write: 48
    6670139-6670180 I2C: Address/data: Write
    6670180-6670221 I2C: Address/data: ACK
    6670221-6670547 I2C: Address/data: Data write: 0E
    6670547-6670588 I2C: Address/data: ACK
    6670596-6670921 I2C: Address/data: Data write: 8A
    6670921-6670962 I2C: Address/data: ACK
    6670961-6670961 I2C: Address/data: Stop
    6671198-6671198 I2C: Address/data: Start
    6671252-6671536 I2C: Address/data: Address write: 48
    6671536-6671577 I2C: Address/data: Write
    6671577-6671618 I2C: Address/data: ACK
    6671619-6671998 I2C: Address/data: Data write: 16
    6671998-6672039 I2C: Address/data: ACK
    6672047-6672372 I2C: Address/data: Data write: 30
    6672372-6672413 I2C: Address/data: ACK
    6672412-6672412 I2C: Address/data: Stop
    6672649-6672649 I2C: Address/data: Start
    6672703-6672987 I2C: Address/data: Address write: 48
    6672987-6673027 I2C: Address/data: Write
    6673028-6673068 I2C: Address/data: ACK
    6673070-6673394 I2C: Address/data: Data write: 17
    6673395-6673435 I2C: Address/data: ACK
    6673444-6673768 I2C: Address/data: Data write: 00
    6673769-6673809 I2C: Address/data: ACK
    6673810-6673810 I2C: Address/data: Stop
    6674047-6674047 I2C: Address/data: Start
    6674100-6674385 I2C: Address/data: Address write: 48
    6674385-6674426 I2C: Address/data: Write
    6674426-6674467 I2C: Address/data: ACK
    6674467-6674793 I2C: Address/data: Data write: 18
    6674792-6674833 I2C: Address/data: ACK
    6674842-6675167 I2C: Address/data: Data write: 80
    6675167-6675208 I2C: Address/data: ACK
    6675207-6675207 I2C: Address/data: Stop
    6675444-6675444 I2C: Address/data: Start
    6675498-6675836 I2C: Address/data: Address write: 48
    6675836-6675931 I2C: Address/data: Write
    6675877-6675972 I2C: Address/data: ACK
    6675919-6676244 I2C: Address/data: Data write: 19
    6676244-6676285 I2C: Address/data: ACK
    6676293-6676618 I2C: Address/data: Data write: 01
    6676618-6676659 I2C: Address/data: ACK
    6676658-6676658 I2C: Address/data: Stop
    6677101-6677101 I2C: Address/data: Start
    6677154-6677439 I2C: Address/data: Address write: 48
    6677439-6677480 I2C: Address/data: Write
    6677479-6677520 I2C: Address/data: ACK
    6677521-6677847 I2C: Address/data: Data write: 01
    6677846-6677887 I2C: Address/data: ACK
    6677895-6678221 I2C: Address/data: Data write: 84
    6678220-6678261 I2C: Address/data: ACK
    6678261-6678261 I2C: Address/data: Stop
    6678291-6678291 I2C: Address/data: Start
    6678345-6678629 I2C: Address/data: Address write: 48
    6678629-6678670 I2C: Address/data: Write
    6678670-6678711 I2C: Address/data: ACK
    6678712-6679037 I2C: Address/data: Data write: 08
    6679037-6679078 I2C: Address/data: ACK
    6679086-6679411 I2C: Address/data: Data write: 88
    6679411-6679452 I2C: Address/data: ACK
    6679451-6679451 I2C: Address/data: Stop
    6679686-6679686 I2C: Address/data: Start
    6679739-6680078 I2C: Address/data: Address write: 48
    6680078-6680119 I2C: Address/data: Write
    6680118-6680159 I2C: Address/data: ACK
    6680160-6680486 I2C: Address/data: Data write: 0F
    6680485-6680526 I2C: Address/data: ACK
    6680534-6680860 I2C: Address/data: Data write: 5C
    6680859-6680900 I2C: Address/data: ACK
    6680900-6680900 I2C: Address/data: Stop
    6680930-6680930 I2C: Address/data: Start
    6680983-6681268 I2C: Address/data: Address write: 48
    6681268-6681309 I2C: Address/data: Write
    6681308-6681349 I2C: Address/data: ACK
    6681350-6681674 I2C: Address/data: Data write: 10
    6681675-6681715 I2C: Address/data: ACK
    6681724-6682050 I2C: Address/data: Data write: 5C
    6682049-6682090 I2C: Address/data: ACK
    6682090-6682090 I2C: Address/data: Stop
    12626945-12626945 I2C: Address/data: Start
    12626998-12627283 I2C: Address/data: Address write: 48
    12627283-12627324 I2C: Address/data: Write
    12627323-12627364 I2C: Address/data: ACK
    12627365-12627689 I2C: Address/data: Data write: 07
    12627690-12627730 I2C: Address/data: ACK
    12627728-12627728 I2C: Address/data: Start repeat
    12627781-12628066 I2C: Address/data: Address read: 48
    12628066-12628107 I2C: Address/data: Read
    12628106-12628147 I2C: Address/data: ACK
    12628134-12628350 I2C: Address/data: Data read: 80
    12628364-12628391 I2C: Address/data: NACK
    12628400-12628400 I2C: Address/data: Stop
    12628434-12628434 I2C: Address/data: Start
    12628487-12628771 I2C: Address/data: Address write: 48
    12628771-12628811 I2C: Address/data: Write
    12628812-12628852 I2C: Address/data: ACK
    12628854-12629178 I2C: Address/data: Data write: 07
    12629179-12629219 I2C: Address/data: ACK
    12629228-12629552 I2C: Address/data: Data write: 80
    12629553-12629593 I2C: Address/data: ACK
    12629594-12629594 I2C: Address/data: Stop

    4、单片机检测选项

    单片机型号: STC8H1K28
    软件协议, V3.0: 7.3.13U

    当前芯片的硬件选项为:
    . 系统ISP工作频率: 23.938MHz
    . 内部IRC振荡器的频率: 11.054MHz
    . 掉电唤醒定时器的频率: 34.825KHz
    . 振荡器放大增益使能
    . 用户EEPROM大小被设置为 0.5 K
    . P3.2和P3.3与下次下载无关
    . 上电复位时增加额外的复位延时
    . 复位引脚用作普通I/O口
    . 检测到低压时复位
    . 低压检测门槛电压 : 2.00 V
    . 上电复位时,硬件不启动内部看门狗
    . 上电自动启动内部看门狗时的预分频数为 : 256
    . 空闲状态时看门狗定时器停止计数
    . 下次下载用户程序时,将用户EEPROM区一并擦除
    . 下次下载用户程序时,没有相关的端口控制485
    . 下次下载时不需要校验下载口令
    . 内部参考电压: 1184 mV (参考范围: 1100~1300mV)

  • 【音频】TASCAM DR-701D如何正确回放立体声

    TASCAM DR-701D录音机,作为一款多通道线性录音机,其设计上并不适合作为一款回放设备使用,但是如果你真的,真的很想用只支持WAV,还不能完整显示文件名字符的它来放音乐,真的很想用它内部的古老的罗姆BA4580RF+TPA4411运放来回放音乐,那也不是不能放,虽然和DR100MK3一样底噪感人。

    那么我们会遇到第一个问题,其默认输出的音频是被MIX过的,没有任何分离度可言,这对于音频回放过于灾难了,那么如何解决这个问题呢?

    方法很简单——进入菜单第二页,将混音器的CH1/CH2的MS设置为录制,即可得到正确的立体声回放。

    此刻回放音乐你甚至还能额外获得两颗0电平指示灯,非常的复古,非常的美味。

  • 【音频】神牛VDS-M3枪式麦克风拆解

    神牛VDS-M3指向性麦克风,关于此麦克风的资料很少,官方初始定价似乎是千元,现在普遍二百左右,不确定用料是否有过改动,该拆解只对应目前200左右的产品。

    主要的能找到参数的芯片已经标记在图中,核心采用 一颗 14mm 咪头,电池为一颗 18490,运放为ADI LT6234(似乎是主板上最贵的芯片),DFN 8封装, 周边还留了个额外焊盘。高低切、增益切换开关应该是通过三颗SN74LVC1G3157DBVR。

    该麦克风的功能和造型类似罗德的NTG4 +,不过可惜网上基本没有NTG 4+的拆解资料,唯一一篇拆解在eeworld 论坛,不过拆的那款怎么看都是个山寨款。

    但是,有意思的就在这里,那款寨版NTG 4+使用的运放和模拟开关方案和神牛这款一毛一样,隔离变压器都长得一样,区别在供电和微处理器部分,可能用的类似方案🤣,不过神牛这个主板做工看起来好一些。

    另外,拆解此类麦克风注意不要硬拽底部xlr座,拆卸所有螺丝后要从上往下把主体推出来,否则可能会把xlr连接线拽断,还得重新焊接(就是我)。

    以上仅供参考。

  • 【音频】TASCAM DR100MK3 的音频架构及改造

    早些年我购入了一个DR100MK3录音笔,看中其又能录音又能回放,相当于大号的录音机+MP3。

    不过回放功能在用几次后感觉并不如人意,其底噪较大,声音较为拥挤,所以后续基本没有再使用。

    后来看了一些“发烧”贴,想当然想去换一下机内的运放,想能不能提升一些效果,当然结果可想而知,没有考虑配套电路+压根没有练习过的焊功,想当然的操作自然是差点把主板搞废了。

    几年过去了,机子还是那个机子,知识越学越多,越学越感觉自己无知,在多次学习电烙铁的使用后,最终还是成功在录音机的主板上修补了七个SOP8的焊盘,让几年前搞坏的录音机重获新生,在此期间,寻找了一些手册,发现了一些有趣的事情。

    1、DR100MK3的运放是OPA1652吗?

    OPA1652是用于麦克风的前置放大,并不是用于音频输出【经过了拆机验证】。

    2、DR100MK3的音频回放架构是?

    两颗AK4558同时作为DAC,其中一颗AK4558+NJM4580C用于Line Out输出,一颗AK4558+NJM4580C经过音量调节,通过一颗BD5461GUL放大输出到机内喇叭,耳机输出则是在音量调节后,经过一颗TPA4411再输出到耳机口。【经过了拆机验证】

    在这个架构下,经过简单的RMAA测试,音频回放在15kHz后就快速滚降,只更换运放可以拓展频率响应的范围,比如我将NJM4580C更换为了OPA1612,其他电路不做调整,测得频响在20kHz后滚降,不过响应曲线没有之前那么平直,在5kHz至15kHz之间有略微波动,噪音及动态范围的总体指标均略有上升,幅度不大。

    NJM4580C下的FR图:

    NJM4580C下的RMAA测试结果(EXT-LINE IN 2 LINE OUT):

    Frequency response (from 40 Hz to 15 kHz), dB+0.07, -0.57Good
    Noise level, dB (A)-97.4Excellent
    Dynamic range, dB (A)98.0Excellent
    THD, %0.00081Excellent
    THD + Noise, dB (A)-88.3Good
    IMD + Noise, %0.00618Excellent
    Stereo crosstalk, dB-67.9Good
    IMD at 10 kHz, %0.00533Excellent
    General performanceVery good

    OPA1612下的FR图:

    更换OPA1612后的RMAA测试结果(EXT-LINE IN 2 LINE OUT):

    Frequency response (from 40 Hz to 15 kHz), dB+0.07, -0.09Very good
    Noise level, dB (A)-100.7Excellent
    Dynamic range, dB (A)102.3Excellent
    THD, %0.00070Excellent
    THD + Noise, dB (A)-89.9Good
    IMD + Noise, %0.00527Excellent
    Stereo crosstalk, dB-74.0Good
    IMD at 10 kHz, %0.00442Excellent
    General performanceVery good

    综合测试结果,硬素质还是距离专业播放器有不少差距。由于缺乏测试设备,其回放音乐的效果无法准确量化,不再推测。

    3、DR100MK3的麦克风大小和型号是?

    DR100MK3内置四颗麦克风【从架构图及BOM表中获得】,均为Primo的产品,其中两颗指向性麦克风——EM164(φ10.0mm x 4.5mm)【也可能是EM182,这里网上能获取到的BOM表和架构图有冲突。另外EM164的规格表显示最大声压级是120,EM182则是132】,两颗为全向麦克风——EM281(φ5.8mm x 2mm)。【这颗全向麦克风规格表最大声压级128,DR100MK3的宣传页显示125,鉴于广告宣传的一般原理,如果DR100MK3选择了EM182,那么他们一定会宣传其高达132的声压级,但是没有,因此量产机的指向性麦克风大概率为EM164】

  • 【网站】Nginx[warn]错误: an upstream response is buffered to a temporary file xxx while reading upstream的解决方法

    如果采用的Nginx+Apache,请求会通过proxy到Apache。

    例如:

        location / {
            proxy_pass http://127.0.0.1:8080;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto https;
            proxy_redirect off;
        }

    这就存在一个问题,也是导致服务器几次宕机的问题,在遇到突发的大频率请求下,尤其是对一些大图、视频之类的体积较大的文件进行请求,Nginx会写入、读取大量的缓存,出现类似的错误,导致硬盘IO暴涨,进而让低配置服务器卡死。

    在Nginx的错误日志中就表现为大量的an upstream response is buffered to a temporary file XXX while reading upstream之类的日志,观察发现基本都是较大的视频、图片、JS文件等。

    解决方法:

    1、可以在Nginx.conf配置适当的缓存,启用proxy buffer(不写on似乎也是默认启用的),然后观察是否依旧存在类似的问题。

    2、如果有大文件例如视频之类静态文件,可以将其进行静态处理,直接由Nginx进行处理,避免频繁的IO读写。

    官方文档:https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffers

    proxy_buffering on;
    proxy_buffer_size 16k;
    proxy_buffers 16 32k;
    proxy_busy_buffers_size 256k;
    proxy_temp_file_write_size 256k;

    在站点的Nginx配置中对静态文件进行单独处理,要指定网站的目录位置。

    参考WordPress相关设置:https://developer.wordpress.org/advanced-administration/server/web-server/nginx/

    root /var/html/wp; #定义网站根目录
        location ~ /\. {
        deny all;#拒绝访问隐藏文件
        }
        location ~* /(?:uploads|files)/.*\.php$ {
        deny all;#拒绝uploads、filrs文件夹的php执行
        }
        location = /favicon.ico {
        log_not_found off;#网站图标文件不记录日志
        access_log off;
        }
        location = /robots.txt {
        allow all;#允许robots文件访问
        log_not_found off;
        access_log off;
        }
        location ~* \.(?:jpg|jpeg|png|gif|webp|ico|css|js|woff2?|ttf|svg|eot|mp4|webm|mov|mp3)$ {
            access_log off;#对静态资源做nginx处理
            expires 30d;
            add_header Cache-Control "public";
            try_files $uri =404;
        }
       location / {#其他请求转发到apache
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Host $server_name;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_redirect off;
        }