基于 Arduino 的伪 NTSC 视频信号生成与发射的尝试

发布于 28 天前  259 次阅读


去年闲的没事干在煤炉上收了一台松下的手持式模拟电视,就想着在日本模拟视频信号全面终止放送的今天能不能让它再次接收到模拟视频信号。(本来以为它有AV接口但是没有)

但是我手上没有发射机(当然),毕竟在这种公用频率上胡乱个人发射违反日本电波法,个人也买不到这种东西。所以想了想,反正模拟信号说罢了就是时间控制下电压的变化,单片机既可以控制电平又可以控制输出时间那不正好可以用单片机来输出模拟视频信号。

然后我查了一下日本的模拟电视广播标准,用的是NTSC-J标准,那不是用单片机模拟一个NTSC标准的信号就行了,本来我也是这么想的,然后查了下资料,发现NTSC是三电平视频信号,分别是同步电平(用于锁定扫描) 黑电平(亮度基准) 白电平(最大亮度),单片机就只能开关开关,也就是只能提供二电平信号。所以这样的话就只能强行构造一个时间结构跟NTSC信号差不多的信号来骗接收机,让它以为是NTSC信号来接收。同时单片机主频太低了只有16兆赫,单个时钟周期只有62.5ns,NTSC的行同步脉冲只有4~5 μs,反正简单说就是时钟精度不够,这里问题太复杂了我看不懂(哭

总之就是最后输出的不是规范的NTSC标准所以说伪NTSC。

还有就是发射,我直接一根导线接在D6上,毕竟是个高频信号直接就以电磁波形式辐射出去了,单片机没有RF电路?频率只有几兆赫达不到UHF/VHF?无所谓,靠谐波干扰直接就窜到接收机相应接收频率范围去了。就跟以前模拟电视莫名其妙收到隔壁家放DVD一个道理。

思路有了,通过Chat GPT帮我写下代码,让他帮我生成一个显示字母“A”的伪NTSC信号

但是按照NTSC-J的参数会失同步,可能还是时钟精度的问题,在我不断调整参数下终于成功同步了。(调的跟ntsc快没关系了)可以看到一个歪歪扭扭的“A”。虽然我觉得这看起来也是失同步,但是能分辨出来已经很不容易了,就算他同步了。(*^_^*)。最后我在想这算不算一种SDR(笑

如果想自己尝试可以用一下代码自己调整行时序
// ========================================================
//   Arduino 伪NTSC-J
// ========================================================

const uint8_t RF_PIN = 6;

// 行时序
const uint8_t HSYNC_US      = 5;
const uint8_t BACKPORCH_US  = 5;
const uint8_t ACTIVE_US     = 50;
const uint8_t FRONT_US      = 3;

const uint16_t LINES_PER_FRAME = 262;

//  VSync
const uint8_t VSYNC_LONG_LINES  = 6;  // 6 行长同步
const uint8_t VSYNC_SHORT_LINES = 6;  // 6 行短同步

const uint8_t TOP_BORDER_LINES  = 20;
const uint8_t PAT_HEIGHT_LINES  = 160;  // 中间用于画图的总高度

// 在 PAT_HEIGHT_LINES 里面再放一个高度为 100 行的 A
const uint8_t CHAR_HEIGHT_LINES = 100;
const uint8_t CHAR_TOP_OFFSET   = (PAT_HEIGHT_LINES - CHAR_HEIGHT_LINES) / 2; // 上下留边

// 载波基频参数
#define CARRIER_OCR0A  1   // 4 MHz 基频(可改 2/3/4 扫频)

inline void carrierOn()  { TCCR0A |= _BV(COM0A0); }
inline void carrierOff() { TCCR0A &= ~_BV(COM0A0); PORTD &= ~_BV(PD6); }

void setupCarrier() {
  pinMode(RF_PIN, OUTPUT);
  TCCR0A = _BV(WGM01);    // CTC
  TCCR0B = _BV(CS00);     // no prescale
  OCR0A  = CARRIER_OCR0A;
  carrierOff();
}

// =====================
// 判断在 (row, segmentIndex) 是否属于 "A" 的黑色部分
// =====================
bool isAPixel(uint8_t row, uint8_t segmentIndex) {
  // 水平分 16 段,用中间 3~12 段画字,左右留空
  if (segmentIndex < 3 || segmentIndex > 12) return false;

 
  uint8_t x = segmentIndex;             // 3..12
  uint8_t midRow = CHAR_HEIGHT_LINES / 2;  // 50

  // 顶部小横条(A 的顶部)
  if (row < 10) {
    // 顶部在 5~10 这一段变黑
    if (x >= 5 && x <= 10) return true;
    return false;
  }

  // 左右竖边(整段高度)
  if (x == 4 || x == 5 || x == 10 || x == 11) {
    return true;
  }

  // 中间横杠(行数在中间附近)
  if (row >= midRow - 4 && row <= midRow + 4) {
    if (x >= 5 && x <= 10) return true;
  }

  return false;
}

// =====================
// 发送一行 RF 视频
// =====================
void sendLineRF(uint16_t line) {

  // HSync
  carrierOn();
  delayMicroseconds(HSYNC_US);

  // Back Porch
  carrierOff();
  delayMicroseconds(BACKPORCH_US);

  // 视频区
  if (line < TOP_BORDER_LINES ||
      line >= TOP_BORDER_LINES + PAT_HEIGHT_LINES) {
    // 上下边框:全白(无载波)
    carrierOff();
    delayMicroseconds(ACTIVE_US + FRONT_US);
    return;
  }

  // 计算这个行在 A 字中的纵向位置
  int16_t yInPat  = (int16_t)line - TOP_BORDER_LINES;           // 0..PAT_HEIGHT_LINES-1
  int16_t yInChar = yInPat - CHAR_TOP_OFFSET;                   // 可能为负/超出

  const uint8_t SEG = 16;
  uint8_t perSeg    = ACTIVE_US / SEG;  // 50/16≈3us,略短一些没关系

  for (uint8_t s = 0; s < SEG; s++) {
    bool dark = false;  // 默认背景(白)→ 无载波

    if (yInChar >= 0 && yInChar < CHAR_HEIGHT_LINES) {
      dark = isAPixel((uint8_t)yInChar, s);
    }

    if (dark) carrierOn();   // A 的部分 → 黑 → 有载波
    else      carrierOff();  // 背景 → 白 → 无载波

    delayMicroseconds(perSeg);
  }

  // 前肩
  carrierOff();
  delayMicroseconds(FRONT_US);
}

void setup() {
  setupCarrier();
}

void loop() {

  uint16_t line = 0;

  // ====== 垂直同步 ======

  // 6 行长同步(全行载波 = 宽脉冲)
  for (uint8_t i = 0; i < VSYNC_LONG_LINES; i++, line++) {
    carrierOn();
    delayMicroseconds(63);   // 整行 = 宽同步
  }

  // 6 行短同步(只在行头载波 = 窄脉冲)
  for (uint8_t i = 0; i < VSYNC_SHORT_LINES; i++, line++) {
    carrierOn();
    delayMicroseconds(3);    // 短同步脉冲
    carrierOff();
    delayMicroseconds(60);   // 行剩余
  }

  // ====== 正常扫描 ======
  for (; line < LINES_PER_FRAME; line++) {
    sendLineRF(line);
  }
}


mua~