STIL语言入门:STIL语言结构、时序定义、Pattern定义、宏定义

各位同学,今天我们来聊聊STIL语言。说实话,我刚接触STIL那会儿,觉得它就是个“怪胎”——既不像C语言那样直观,又不像Verilog那样有硬件对应关系。但干久了才发现,STIL其实是ATE测试的“通用语言”,你想想看,没有它,不同厂商的测试机怎么交换测试向量?

我个人习惯把STIL比作“测试界的PDF”。它不关心你芯片内部怎么设计,只关心你给什么激励、期望什么响应。嗯,这个比喻可能不太准确,但意思到了就行。

STIL语言结构:骨架要搭好

STIL文件的结构其实很清晰,就像盖房子,先搭骨架再填砖。一个完整的STIL文件,通常包含以下几个部分:

  • Header:文件头,声明版本、日期、作者等信息
  • Signals:信号声明,定义所有用到的端口
  • Timing:时序定义,描述波形和时序关系
  • PatternBurst:Pattern组,组织多个Pattern
  • Pattern:具体的测试向量
  • MacroDefs:宏定义,复用常用操作

我给大家画个结构图,这样更直观:

STIL文件结构 Header(文件头) Signals(信号声明) Timing(时序定义) PatternBurst(Pattern组) Pattern(测试向量) MacroDefs(宏定义) 自上而下,层层嵌套

看到没?这就是STIL的骨架。每个部分都有自己明确的职责,不能乱。我记得刚带新人时,有个小伙子把Timing写到了Signals前面,结果解析器直接报错。嗯,顺序很重要。

时序定义:测试的“节拍器”

时序定义是STIL里最核心的部分之一。说白了,就是告诉测试机:什么时候拉高、什么时候拉低、什么时候采样。

一个典型的时序定义长这样:

Timing {
  WaveformTable "default" {
    Period '100ns';
    Waveforms {
      CLK  { 0 { '0ns' D; } 1 { '50ns' D; } }
      DATA { 0 { '0ns' D; } 1 { '0ns' D; } Z { '0ns' Z; } }
      OEN  { 0 { '0ns' D; } 1 { '0ns' D; } }
    }
  }
}

这里有几个关键点:

  • Period:定义时钟周期,比如100ns
  • Waveforms:定义每个信号的波形形状
  • D:表示驱动(Drive),测试机主动输出
  • Z:表示高阻,常用于三态总线
小技巧:我个人习惯把时序定义单独放在一个文件里,用Include语句引用。这样多个Pattern可以共用同一套时序,改起来也方便。

为什么会这样设计?因为ATE测试机执行Pattern时,是按周期来跑的。每个周期里,所有信号的行为都严格按照WaveformTable来执行。你想想看,如果没有这个统一的“节拍器”,不同Pattern之间的时序怎么对齐?

Pattern定义:测试向量的“剧本”

Pattern就是测试向量的具体内容。它告诉测试机:每个周期,每个信号应该是什么状态。

来看个简单的例子:

Pattern "write_test" {
  V { '100ns' 'default'; }
  W {
    // 写操作
    CLK = 0; DATA = 1; OEN = 1;
    CLK = 1; DATA = 1; OEN = 1;
    CLK = 0; DATA = 0; OEN = 1;
    CLK = 1; DATA = 0; OEN = 1;
  }
}

这里:

  • V:设置电压和时序
  • W:具体的波形向量
  • 每一行代表一个周期
注意:我曾经遇到过一个问题——Pattern里忘了设置OEN信号,结果测试机一直输出,和DUT的输出怼上了。嗯,那一次烧了好几个芯片。所以,三态信号一定要管好。

Pattern定义里还有个重要的概念叫“Compare”。测试机不光要驱动信号,还要在特定时刻采样DUT的输出,和期望值做比较。比如:

Pattern "read_test" {
  V { '100ns' 'default'; }
  W {
    CLK = 0; DATA = Z; OEN = 0; // 让DUT输出
    CLK = 1; DATA = Z; OEN = 0;
    CLK = 0; DATA = C; OEN = 0; // C表示Compare,采样数据
  }
}

看到那个C了吗?它告诉测试机:在这个周期,我要采样DATA引脚,和期望值做比较。如果对不上,就报Fail。

宏定义:让Pattern“瘦身”

宏定义是STIL里最实用的功能之一。说白了,就是把重复出现的操作封装起来,用的时候直接调用。

我给大家看个例子:

MacroDefs {
  "write_cycle" {
    W {
      CLK = 0; DATA = 1; OEN = 1;
      CLK = 1; DATA = 1; OEN = 1;
    }
  }
  
  "read_cycle" {
    W {
      CLK = 0; DATA = Z; OEN = 0;
      CLK = 1; DATA = C; OEN = 0;
    }
  }
}

Pattern "burst_test" {
  V { '100ns' 'default'; }
  Call "write_cycle" { DATA = 1; }
  Call "write_cycle" { DATA = 0; }
  Call "read_cycle" {};
  Call "read_cycle" {};
}

你看,原来要写8行,现在4行就搞定了。而且宏定义可以带参数,比如上面的DATA = 1就是传参。

核心思想:宏定义的本质是“模板”。它把固定的操作模式抽象出来,让Pattern更简洁、更易维护。我见过有些工程师一个Pattern写几千行,全是重复的。嗯,那维护起来简直是噩梦。

宏定义还有一个妙用——做“测试序列”。比如你要做一组连续的读写操作,用宏定义可以轻松实现:

MacroDefs {
  "write_read_check" {
    W {
      // 写数据
      CLK = 0; DATA = 1; OEN = 1;
      CLK = 1; DATA = 1; OEN = 1;
      // 读数据
      CLK = 0; DATA = Z; OEN = 0;
      CLK = 1; DATA = C; OEN = 0;
      // 检查结果
      CLK = 0; DATA = Z; OEN = 0;
      CLK = 1; DATA = C; OEN = 0;
    }
  }
}

这样,一个“写-读-检查”的完整操作,只需要一行Call "write_read_check" {}就搞定了。

好了,STIL的四个核心部分就讲到这里。说实话,STIL入门不难,难的是写出高效、可维护的Pattern。我建议大家多动手写,多看看别人的Pattern是怎么组织的。嗯,实践出真知。

个人经验:我刚开始写STIL时,总想把所有东西塞到一个文件里。后来发现,拆分成多个文件,用Include组织起来,才是正道。一个文件管时序,一个文件管宏定义,一个文件管Pattern。这样,改时序不用动Pattern,改Pattern不用动时序。

记住,STIL不是编程语言,它是描述语言。它的目标不是“计算”,而是“描述”——描述测试机该怎么动、DUT该怎么响应。想明白这一点,你就入门了。