标签: 摄影

  • 【摄影】尼康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的胶片回卷与留片头,欢迎交流使用反馈与改进建议。
    使用前请再次确认:已备份、已理解风险、已在安全环境下测试。

  • 【摄影】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的,嗯,感觉又不够德味了。

  • 【摄影番外】M43难兄难弟(1)——奥之心官方维修体验

    前言:

    最近坏了一颗12-40 F2.8二代港版,使用过程中发送碰撞,镜头卡口与镜身固定螺柱发生断裂,只能送修换部件。考虑到奥之心在国内可怜的占有率,因此选择官方售后进行维修,体验了一次奥之心在国内的送修服务,整体流程倒是非常顺畅,在此做简单分享。

    奥之心官方售后网络:

    奥之心官网维修网络

    奥之心如今的官方维修网络分为三个层级:客户服务中心微型单电维修中心以及微型单电受理中心。其中客户服务中心为官方设立,微型单电维修中心为授权,应该可以简单理解为有资质的第三方,受理中心则会将受理的设备转送到客户服务中心或者维修中心进行修复。

    本次维修选择的是上海客户服务中心,由于其只提供寄修服务,因此在寄送之前需要做好沟通工作。

    维修过程:

    1、咨询客户服务中心,所需维修的镜头是否有备件等条件:

    直接拨打021-61767030转分机8016,咨询工程师。

    2、寄送需要准备的说明文字:

    寄送时候,工程师需要你在其中放一个纸条,写上一些基本信息,主要是联系方式及所要修理的问题,例如——

    联系方式:

    姓名:XX

    电话:XXXXX

    回寄地址:XXXXXXX

    存在的问题:(以下是我当时写的问题)

    1. 拍摄时候发现成像一侧不清晰;
    2. 初步检查发现底部卡口与镜身连接有旷量;
    3. 自行拆解后进一步检查发现两个螺丝柱断裂;
    4. 自行拆解后,螺丝垫片掉落,因此目前顺序与数量不确定是否出错;
    5. 安装回时扭矩过大,镜头卡口两颗螺丝有滑丝情况。

    3、等待售后工程师检测与说明情况:

    寄送到上海客户服务中心,邮费需要自理。

    然后中间可能需要等待3-5个工作日的时间,等待工程师联系你,我当时是等了大概5天,期间可以打上面的021开头电话询问工程师进度,不过一般工程师检测后都会联系你。

    4、确认检测及报价并付款:

    工程师检测后告知镜头存在的问题及需要的维修方式,更换的部件等,然后会给出大致价格。

    而后,奥之心会给你发送一个短信,告知具体的维修价格并附上汇款账户,而后付款。

    5、维修并寄回:

    本次维修时间较快,付款维修后当天傍晚就顺丰发回,注意同样是到付,奥之心不会承担邮费。

    本次维修中寄回的内容包括修复完成的镜头、损坏的部件、奥之心维修清单,其中清单记载物料费用439,人工费用500。

    6、发票问题:

    如果要开票,可以联系工程师,对方会短信发送邮箱,你可以把开票信息发送到邮箱,等待月底后拿到发票。

    总结:

    奥之心虽然作为Other中的Other,但是维修网络依旧在正常运转,而且体验尚可,维修手艺也是不错,作为M43难兄难弟之一,在中国市场的蜜汁定价下还有可靠的售后也属实不易,奥之心真的可以重新考虑以下自己在中国市场的定位和定价,或许还能在Other里绽放一些光芒。

  • 【器材】松下欣然发布M43“新机”——Lumix G97

    原始链接:LUMIX G97: New Compact Hybrid Micro Four Thirds Camera

    The new LUMIX G97 camera balances high performance and simplicity, giving creators the tools to elevate their photography and video skills.

    Superb picture quality

    The 20.3MP CMOS sensor, (估计还是老汤底)combined with the high-performance Image processor, delivers superb image quality with vibrant colors and sharp details. 

    Equipped with LUMIX Photo Style feature, (不是Lut功能)users can fine-tune their images with a variety of color effect options, ensuring every shot matches your creative vision.

    The Live View Composite feature is also included, providing the ability to combines multiple exposures in real time to create stunning light trails, star trails, or illuminated scenes without overexposing the background.

    Smooth performance

    The LUMIX G97’s 5-stop 5-axis Dual I.S.2*1 (防抖没有升级)system ensures unrivalled stability, so your photos and videos remain sharp even in challenging conditions. 

    With 4K PHOTO capabilities, you can capture bursts of high-resolution photos at 30fps, ensuring you never miss a fleeting moment. The perfect shot from burst footage makes this feature ideal for fast-moving subjects or spontaneous scenes.

    Versatile video features

    Record in crisp 4K at 30p with no time limitations*2,(不限时录制好评,但你还是DFD呀) experiment with slow-motion (max.4x) or quick-motion (max.8x) in FHD, and create cinematic content with 12-stops of V-Log L.(有Vlog,但是L) Dedicated headphone and microphone jacks ensure total audio control while recording.

    Intuitive operation and reliable design

    With a 1,840k-dot free-angle LCD and 2,360k-dot OLED Live View Finder*3,(换屏幕了,好评!) the LUMIX G97 makes it easy to frame and focus your shots accurately, even in bright conditions.

    The durable dust/splash-resistant*4 construction is ideal for everyday creators looking for a camera that can reliably handle a variety of situations.

    Built-in Bluetooth® v5.0 and Wi-Fi make sharing and remote control effortless, while USB Type-C charging adds convenience.(先进的蓝牙5.0与Type-c确保这是21世纪20年代的产品)

    Price and availability

    The new LUMIX G97 will be available in late February 2024 for $849.99 for a 12-60mm lens kit (DC-G97MK) at valued channel partners.

    *1 Based on the CIPA standard [Yaw/Pitch direction: focusing distance f=140mm (35mm camera equivalent f=280mm), when H-FSA14140 is used.]

    *2 When the ambient temperature is high, the camera may stop the recording. Wait until the camera cools down.

    *3 35mm camera equivalent

    *4 Dust and Splash Resistant does not guarantee that damage will not occur if this camera is subjected to direct contact with dust and water

    翻译一下:

    松下欣然发布了一款名为G97的“新机”,那么这款与G95长得一毛一样的新机,对比G95有哪些“重大”改进呢?

    1、换装了Type-C!好耶,避免了在欧洲市场卖不下的尴尬。同时Type-C接口支持充电,增强了充电的便利性,这个倒是好评,跟上了时代的步伐。

    2、换装了蓝牙5.0!好耶,新的总比旧的好,对吧?!

    3、拥有了预装的Vlog-L!好耶,Vlog-L也是Vlog!

    4、不限时的视频录制!好耶!但是会过热吗?

    5、换装了184万像素显示屏,应该是与S5M2、G9M2等同款的3英寸屏幕。这个喷不了,屏幕是好屏幕!

    综上,G97基本上就是G95(D)的小修小补强化版本,核心画质目测没有本质区别,发售日期国外定于2025年2月,价格定为1260(非徕卡)套机$849.99,对比一下现在的G95-1260套机美国官网促销售价$699.99,因此G97应该会作为G95的替代产品,保持相同的定位与价格。

    (更新:国行单机身售价4998,和国行目前降了N次价的G9同价,当然松下指导价只能指导着玩,目测还是要跳水)

    所以,松下你这块M43画幅——先进DFD技术——祖传2030万像素传感器到底还要用多久。

  • 【全景图】大疆球形全景合成2:1全景图(等距柱状投影)

    前言:

    全景平台普遍要求提供2:1的等距柱状投影全景图,而大家使用一些老款无人机拍摄的全景图可能没有机内合成功能,比如mini2之类的,因此我们需要一些手段将其进行转换与合成。(目前一些新款无人机都可以直接机内合成了,不过也可以用这个方法,把一些合成有问题的重新做一下编辑。)

    网络上的教程普遍基于PTGui,该软件好用易用且可以很方便的补天。不过该软件是付费软件,虽然可以进行破解,但有没有开源的软件可以进行制作?

    当然有,我们可以使用开源的全景合成软件—— Hugin 对全景图进行合成。

    用到的软件:Hugin(合成)、Photoshop(补天)。

    点击快速跳转步骤:

    步骤:

    1、导入大疆全景图片

    Hugin有三个用户界面,后两个涉及一些复杂操作,我推荐使用第一个简单界面来导入与修正图片。

    直接点击加载图片,选择大疆全景图文件夹,将全景图片全选导入。

    你会看到载入的图片犬牙交错,导入成功后我们可以进入下一步骤。

    2、分析图片,创建2:1全景图,调整轴心

    点击排列,软件会启动脚本,分析各个图片的关联,创建相应的控制点。

    该过程需要耗费一定的时间。

    等待脚本运行完毕,我们可以在预览界面看到排列完成的图片。

    其中的天空有一大片空缺,这里先不用管,因为无人机拍摄时候确实没法向上拍摄,这里的补天我们可以通过PS中的一个简单步骤实现。

    另外,这里需要注意的是我们需要提前指定全景图片的三轴,可以在移动工具中,拖动左侧的全景球来指定相应轴心位置。

    3、导出全景图

    接下来我们导出2:1的全景图。如果直接点击创建全景图像,你会发现分辨率是不对的,因此我们切换界面,使用高级功能导出。

    点击高级界面。

    可以看到全景图的控制点等已经创建完毕,其他的不用动,我们点击该界面的缝合器。

    投影确认是等矩形。然后我们点击计算优化尺寸。

    这时尺寸会重新计算,得到我们需要的足够清晰的输出尺寸。

    点击缝合按钮,脚本开始运行,这时软件会创建一个PTO项目文件,以及多个TIF文件,最终会输出一张合并完成的TIF全景图片文件。

    而后我们对缺失的天空部分进行填充。

    4、补天

    将图片导入Photoshop,使用选区工具框选缺失部分及周围部分天空。

    使用填充工具,对空缺部分进行填充。

    在此过程中我们还可以对图片进行一些色彩调整,完成后导出,我们就得到了一个很棒的2:1全景图,而后可以将其导入到其他全景平台进行内容制作。

    5、效果展示

    此处若出现跨域问题,则需要修改Nginx的配置文件,对类型图片进行请求放行。

    这个全景效果使用了pannellum.js,可以很方便置入网站来进行展示。

    <script src="https://cdn.jsdelivr.net/npm/pannellum/build/pannellum.js"></script>
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pannellum/build/pannellum.css">
        <style>
            #panorama {
                width: 100%;
                height: 500px;
            }
        </style>
    </head>
    <body>
        <div id="panorama"></div>
        <script>
            pannellum.viewer('panorama', {
                "type": "equirectangular",
                "panorama": "xxx.jpg", // 图片地址
                "autoLoad": true,
                "compass": true // 显示指南针
            });
        </script>
    </body>

  • 【摄影特辑】青岛2023

    【摄影特辑】青岛2023

    镜头:Panasonic 30mm Macro

    相机:Panasonic G9M2

    后期:Snapseed

    很荣幸,第二张图片还被松下影像翻了牌子

  • 【摄影特辑】粉黛乱子草

    【摄影特辑】粉黛乱子草

    What is Muhlenbergia capillaris?

    粉黛乱子草(Muhlenbergia capillaris)是禾本目、禾本科、乱子草属植物。多年生暖季型草本,株高可达30-90厘米,宽可达60-90厘米。顶端呈拱形,绿色叶片纤细。顶生云雾状粉色花絮,花期9-11月,成片种植可呈现出粉色云雾海洋的壮观景色,景观可由9月份一直持续至11月中旬,观赏效果极佳。