分类: 摄影器材

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

  • 【器材】松下欣然发布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万像素传感器到底还要用多久。