月度归档: 2026 年 6 月

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

    一、改造目标与思路

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

    6. 胶片进片(过片)

    1. 当参考开关(也就是我们监测的ref)断开四次(相当于四个画幅)时,自动装片操作停止。
    2. 参考开关始终保持在导通状态。该开关在胶片每进给约 19 mm(半幅)时断开一次。
    3. 当参考开关断开后,监测胶片进片光电遮断器(intp)的脉冲输出,进片停止定时控制开始。
    4. 胶片进片光电遮断器在进给一个画幅期间输出 114 个脉冲。

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

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

    经过测试,单纯修改RAM地址 FE7E 为 71、或伪造 REF不能稳定实现方画幅;

    一个可行的路径是:检测INTP 计数到目标值 → 提前拉停 IN1/IN2 → 进入END状态→用串口清 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 门)

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

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

    因此我们可以通过串口对特定地址的RAM进行复位。


    四、UNO卷片控制固件架构

    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回卷留片头实现!Nikon F90X/N90S Film Leader Customizer——尼康F90X/N90S回卷留片头软件堂堂发布!

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

    发布视频:

    写在前面:

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

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

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

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

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

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

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

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

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

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

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

    在Nikon 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 备份
    • 修改后:可在相机上实际装片、过片,确认留片头是否符合预期
    • 遇到异常:保留完整日志,便于排查是连接问题、协议超时还是校验问题

    写在最后

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

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