PLL

PLL脚本框架

Posted by MaZhaoxin on July 24, 2022

在PLL设计过程中需要评估环路的小信号特性,如带宽、相位裕度、增益等,并基于小信号计算最终输出的噪声。通常会用MATLAB或者Python做这件事,当然也有人直接在Virtuoso里用spectre仿真,不过我总觉得不如脚本灵活、快速。对于可量产的芯片来说,这项评估要覆盖各种工艺角和参数偏差,并收集各种计算结果,单纯的用一个脚本很难清晰明了的完成这项任务。此外,有些复杂的级联、嵌套更增加了工作的复杂度,因此我想应该有一种脚本框架可以用来简化工作,提高脚本代码的重用率。

以前

早些年,我的想法是把脚本分成两部分,其中一个用来存储参数、计算、展示结果,对于同一结构的PLL来说代码可以完全复用;另一个则是用来赋值和调用前面的脚本。基于这种想法,在一个项目里我会写两个.m文件,前者叫XXX_PLL.m,后者叫sim_XXX_PLL.m

XXX_PLL.m是一个函数:

  • 若调用时不提供参数,则返回一个预定义好的结构体,其中包括了该PLL的所有参数。例如m = XXX_PLL();
  • 若调用时提供了结构体参数,则认为该结构体包含了需要评估的PLL参数,基于这些参数计算后返回计算结果,计算结构同样以结构体的形式储存。例如r = XXX_PLL(m);
  • 若调用时提供了结构体参数并说明需要展示,则认为该结构体包含了计算结果,基于这组结果展示图表。例如XXX_PLL(r, 'show');

计算和展示分两步做是为了把计算结果存到工作区,以备后续查看和处理。在计算时会自动展开参数,如设置F_in = [20e6, 25e6]; F_out = [2000e6, 2500e6];则会分别评估组合后的4种情况。

sim_XXX_PLL.m是一个脚本,我会在里面定义各种模式下PLL所需的参数,如输入输出频率、各模块噪声和增益等,然后把这些参数赋值给通过XXX_PLL.m得到的结构体,再按上面所示的那样调用两次,得到评估结果。

此处解释一下,MATLAB中.m文件有3种,分别叫做script, function, class。

  • script(脚本):顺序执行的代码,没有复杂的结构(2016b之后支持局部函数),其操作的变量在命令窗口的工作区(workspace)。换句话说,它与直接在命令窗口敲命令是等价的。
  • function(函数):包括与文件名同名的主函数和若干局部函数,局部函数仅可在该文件内调用。这是面向过程的编码方式,其本身不能存储任何数据,只有计算、处理的能力。
  • class(类):包括与文件名同名的类,其下有属性和方法,属于面向对象的编码方式。

MATLAB中常用的内置复杂数据类型有struct, cell, table等。

  • struct(结构体):以键-值对(key-value pair)存储数据,可以通过键名(MATLAB中称之为fieldname)操作所需的数据,键可以随时增减。多个结构体可以构成结构体数组,它们的键保持一致,因此有些时候与表格很像。
  • cell(元胞数组):常规的数组(矩阵)要求数组内的元素类型和格式必须一致,而元胞数组则没有这种限制,它的每个元素都可以是任意的类型、任意的格式。如结构体的键名便是用元胞数组存储,因为它们虽然都是字符数组,但长度可能不同。需要特别注意的是,与数组用圆括号()取值不同,元胞数组需要用花括号{}取值,并且它也支持用圆括号取值,只不过取到的值仍为元胞数组
  • table(表格):常规数组、结构体、元胞数组在某些条件下都可以转换为表格,而表格的优势在于显示和导出。

问题

这种脚本组织方式一直用得很好,既兼顾了代码复用,也不失灵活性。但随着项目的发展,开始暴露出几个问题:

  1. 对于新的PLL结构需要改动那个复杂的函数文件;
  2. 参数组合时没有考虑分组情况,有时候会把两种不可能同时出现的参数组合在一起,导致over-design;
  3. 组合时没有单独划分typical case,不方便与测试结果对照;
  4. 不方便处理两颗PLL级联的情况;
  5. 因为参数与计算结果分别存储,不方便显示报表。

为了解决前面的问题,我觉得一个好用的PLL脚本框架应该支持这么几项特性:

  • 较高的代码复用率,对于一种PLL只需要编写部分相应的代码,而这部分代码是单独放置的,不与共用代码搅在一起;
  • 支持参数组合,并支持分组,且在组合时划分typical case;
  • 方便级联,甚至与CDR级联;
  • 可以轻松展示报表,并导出为类似Excel的文件;
  • 可以把参数映射为寄存器值,方便调试实际芯片。

目前

基于此,我构想了一种PLL代码框架,目前实践下来感觉还不错。它包括几个公共类(是的,换成了面向对象编码方式):

  • PhaseNoise类:用来存储和计算相位噪声,通过afterDIVafterCDR等方法产生衍生的相位噪声,以方便级联;
  • PLL类:抽象类,完成最基本的PLL参数计算,包括calc_H_olcalc_PNsimshow_results等抽象方法,项目中的实际PLL需要继承该类,然后添加自己的属性,并完成对应的方法实现;
  • Corners类:初始化时存储需要扫描的变量,通过add_corneradd_corner_group等方法产生参数表,再在run方法中逐一把参数传递给PLL的sim方法,并保存返回的数据,最后把数据汇总形成简报或通过PLL的show_results显示。
  • Specs类:静态类,用来存储常用Spec的评估方式和指标。
  • TransferFunction类:静态类,用来完成传递函数的生成和计算。

在使用时主要需要维护两个文件,一个是描述实际PLL的类文件,依然命名为XXX_PLL.m,它继承了PLL类,并如前面所述完成了对应方法的实现;另一个是脚本sim_XXX_PLL.m,类似之前的做法,依然把实际参数存在这里,并作为仿真计算的起点。

这个框架的基本思路是所有与项目相关的代码都编写在前一个文件中,毕竟只有这个类自己才知道这个PLL有哪些参数,如何计算传递函数和噪声,以及如何显示计算结果。如果有不同类型的PLL,可以由任意一个PLL的sim方法完成顶层连接关系的描述,也可以单独写一个类。而那些公共类完全不需要修改,甚至可以以加密的方式传给他人使用。

总结

“工欲善其事,必先利其器”。通过一些小工具提高效率可以更好地完成工作、享受生活。