说到时序图,相信所有从事嵌入式开发的伙伴都非常熟悉,在各种元器件手册以及处理器说明书中,但凡涉及到通信、接口、交互等内容,都会涉及到时序图。时序图可以非常详细且明确地描述硬件及软件接口中各个信号的时序关系,例如时钟信号、数据信号、控制信号等。这些对于确保设备间的正确交互至关重要。可以说能够读懂时序图是每一个嵌入式开发者必备的技能。然而,要想进一步成为资深工程师,那仅仅会看是不够的!
如何既简单又方便还优雅地绘制属于我们自己的时序图呢?如果说要我们用画图软件一条一条线画那是绝对不可能的,因此,我们需要借助一个时序图绘制工具 —— WaveDrom。
WaveDrom 是一个 JavaScript 的开源项目,其使用一种叫 WaveJSON 的格式来描述数字时序图。从其名称就很容易看出,WaveJSON 是基于 JSON 并且有一些保留关键字以及结构限制的格式。因此,如果你拥有 JSON 基础,那么 WaveJSON 会非常容易上手。
WaveDrom 官网如下:
https://wavedrom.com/
其支持下载安装到本地,也支持浏览器在线使用,还集成了一个 VSCode 插件 —— Wavefrom Render,从而支持在代码编辑器中直接绘制图形,整体使用非常灵活方便。
本文使用官方的在线编辑器来演示。在线编辑器地址如下:
https://wavedrom.com/editor.html
打开后的页面如下图所示:
整体界面非常简洁,分为上下两个部分,并且自带一个 Demo 供我们参考。上面是 WaveJSON 的描述文本,下面是基于描述文本渲染出来的时序图。而我们所要做的就是根据我们的需求写出对应的描述文本,最终由 WaveDrom 来生成我们想要的时序图。
到这里不难看出,画好时序图的重点就是写好描述文本。而要写好描述文本,自然是要完全理解并会使用 WaveJSON!庆幸的是,WaveJSON 并不难,其基于 JSON 的特性也让它的组织结构非常清晰并且易于理解。
官方提供了一个上手指南,不过是英文版的,这里我们直接搬运过来并进行翻译,帮助大家快速理解。
1. 信号 (Signal)
在 WaveJSON 中,signal 是根节点,也可以叫做主节点。它是一个由 Wavelane 组成的数组,而 Wavelane 则是用于描述单个信号波形的基本单元,每个 Wavelane 中包含两个必填字段:
-
name:用于指定信号的名称。这个名称存在于时序图信号波形旁边,帮助识别和理解该信号的作用。
-
wave:用于定义信号的状态变化。它由一系列字符组成,每个字符代表一个时间周期内的信号状态。常见的字符包括:
-
0/1:分别表示信号的低电平和高电平。
-
.:表示信号在该周期内保持前一个状态不变。
-
x:表示信号在该周期内处于任意状态或未知状态。
-
z:表示信号在该周期内处于高阻状态。
-
2-9:表示数据,每个数字带有不同的颜色
-
=:表示数据,与 2 代表的颜色相同。
-
p/n: 分别表示时钟信号的上升沿和下降沿。
-
P/N:与 p/n 相同但是带有箭头。
-
u/d:分别表示弱上拉和弱下拉。
-
|:用于分隔不同的时钟周期,以便在时序图中清晰地显示时钟周期的边界。
-
更详细的说明会在后续针对于 WaveJSON 的文章中列举。
我们用一个简单的示例来理解,以下代码将创建一个名为 Alfa 的单比特信号,并且随着时间的推移其状态会发生变化:
{ signal: [{ name: "Alfa", wave: "01.zx=ud.23.456789" }] }
在 wave 字段中,每一个字符都代表一个单一的时间周期,渲染后的时序图如下:
2. 添加时钟
数字时钟是一种特殊的信号类型,它在每个周期内变化两次,并且可以具有正负极性。它还可以在工作边缘添加可选的标记。时钟块可以与其他信号状态混合,以创建时钟门控效果。示例代码如下:
{ signal: [
{ name: "pclk", wave: 'p.......' },
{ name: "Pclk", wave: 'P.......' },
{ name: "nclk", wave: 'n.......' },
{ name: "Nclk", wave: 'N.......' },
{},
{ name: 'clk0', wave: 'phnlPHNL' },
{ name: 'clk1', wave: 'xhlhLHl.' },
{ name: 'clk2', wave: 'hpHplnLn' },
{ name: 'clk3', wave: 'nhNhplPl' },
{ name: 'clk4', wave: 'xlh.L.Hx' },
]}
渲染后的时序图如下:
3. 信号组合
典型的时序图可能会包含时钟和触发信号。需要多个比特组合的数据一般可以通过加入 data 字段来标注:
{ signal: [
{ name: "clk", wave: "P......" },
{ name: "bus", wave: "x.==.=x", data: ["head", "body", "tail", "data"] },
{ name: "wire", wave: "0.1..0." }
]}
渲染后的时序图如下:
4. 隔断
使用 | 来分隔不同的时钟周期,以便在时序图中清晰地显示时钟周期的边界:
{ signal: [
{ name: "clk", wave: "p.....|..." },
{ name: "Data", wave: "x.345x|=.x", data: ["head", "body", "tail", "data"] },
{ name: "Request", wave: "0.1..0|1.0" },
{},
{ name: "Acknowledge", wave: "1.....|01." }
]}
渲染后的时序图如下:
5. 信号分组
多个 Wavelanes 可以组合成命名组,这些组在 WaveJSON 中以数组的形式表示,类似于:['group name', {...}, {...}, ...]。数组的第一个元素是组名,后续元素是属于该组的 Wavelanes 或其他子组,因此也说明,组是可以嵌套的。以下是代码示例:
{ signal: [
{ name: 'clk', wave: 'p..Pp..P'},
['Master',
['ctrl',
{name: 'write', wave: '01.0....'},
{name: 'read', wave: '0...1..0'}
],
{ name: 'addr', wave: 'x3.x4..x', data: 'A1 A2'},
{ name: 'wdata', wave: 'x3.x....', data: 'D1' },
],
{},
['Slave',
['ctrl',
{name: 'ack', wave: 'x01x0.1x'},
],
{ name: 'rdata', wave: 'x.....4x', data: 'Q2'},
]
]}
渲染效果如下:
6. 周期和相位
“period” 和 “phase” 这两个参数可以用来调整每个 Wavelane 的显示特性。
-
period(周期):这个参数用于设置波形通道的时间周期长度。例如,如果一个信号的周期被设置为2,那么它的每个时间单位将占据两个默认时间单位的长度。
-
phase(相位):这个参数用于调整波形通道的起始相位。它允许你将信号的起始点提前或延后,具体为:如果一个信号的相位偏移为正,这意味着信号波形会在时间轴上向左移动,从而在时间上提前出现。相反,如果相位偏移为负,则信号波形会在时间轴上向右移动,从而在时间上滞后出现。
下面以 DDR 读时序为例,描述代码为:
{ signal: [
{ name: "CK", wave: "P.......", period: 2 },
{ name: "CMD", wave: "x.3x=x4x=x=x=x=x", data: "RAS NOP CAS NOP NOP NOP NOP", phase: 0.5 },
{ name: "ADDR", wave: "x.=x..=x........", data: "ROW COL", phase: 0.5 },
{ name: "DQS", wave: "z.......0.1010z." },
{ name: "DQ", wave: "z.........5555z.", data: "D0 D1 D2 D3" }
]}
渲染效果:
7. 配置属性
配置属性 config:{...} 用于控制渲染的不同特征。
hscale
config:{hscale:#} 用于控制时序图的水平缩放。用户可以设置任何大于零的整数值:
{ signal: [
{ name: "clk", wave: "p...." },
{ name: "Data", wave: "x345x", data: ["head", "body", "tail"] },
{ name: "Request", wave: "01..0" }
],
config: { hscale: 1 }
}
hscale = 1 (默认)
hscale = 2
hscale = 3
skin
config:{skin:'...'} 属性可以用来选择 WaveDrom 的皮肤样式。这个属性只在页面上的第一个时序图中有效。WaveDrom 编辑器包括两种标准的皮肤样式:'default' 和 'narrow'。
head/foot
head:{...} 属性定义了时序图的头部区域。可以在这个区域中添加标题、时间标记等信息。
foot:{...}属性定义了时序图的底部区域。可以在这个区域中添加图例、说明等信息。
tick
添加与垂直标记对齐的时间线标签。
tock
在垂直标记之间添加时间线标签。
title
添加标题或说明文字。
every
在时间轴上,只在每 N 个周期中渲染一次 tick 和 tock 标记。
以下是上述几个属性的示例代码:
{signal: [
{name:'clk', wave: 'p....' },
{name:'Data', wave: 'x345x', data: 'a b c' },
{name:'Request', wave: '01..0' }
],
head:{
text:'WaveDrom example',
tick:0,
every:2
},
foot:{
text:'Figure 100',
tock:9
},
}
渲染效果如下:
注意,head/foot 文本具有 SVG 文本的所有属性。可以使用标准的 SVG tspan 属性来修改文本的默认属性。JsonML 标记语言用于表示 SVG 文本内容。可以使用并混合多种预定义样式:
-
h1、h2、h3、h4、h5、h6 —— 预定义的字体大小。
-
muted、warning、error、info、success —— 字体颜色样式。
其他 SVG tspan 属性可以像下面的示例那样自由使用:
{signal: [
{name:'clk', wave: 'p.....PPPPp....' },
{name:'dat', wave: 'x....2345x.....', data: 'a b c d' },
{name:'req', wave: '0....1...0.....' }
],
head: {text:
['tspan',
['tspan', {class:'error h1'}, 'error '],
['tspan', {class:'warning h2'}, 'warning '],
['tspan', {class:'info h3'}, 'info '],
['tspan', {class:'success h4'}, 'success '],
['tspan', {class:'muted h5'}, 'muted '],
['tspan', {class:'h6'}, 'h6 '],
'default ',
['tspan', {fill:'pink', 'font-weight':'bold', 'font-style':'italic'}, 'pink-bold-italic']
]
},
foot: {text:
['tspan', 'E=mc',
['tspan', {dy:'-5'}, '2'],
['tspan', {dy: '5'}, '. '],
['tspan', {'font-size':'25'}, 'B '],
['tspan', {'text-decoration':'overline'},'over '],
['tspan', {'text-decoration':'underline'},'under '],
['tspan', {'baseline-shift':'sub'}, 'sub '],
['tspan', {'baseline-shift':'super'}, 'super ']
],tock:-5
}
}
渲染效果如下:
8. 箭头
平滑曲线
~ -~
<~> <-~>
~> -~> ~->
具体使用形式及渲染图如下:
{ signal: [
{ name: 'A', wave: '01........0....', node: '.a........j' },
{ name: 'B', wave: '0.1.......0.1..', node: '..b.......i' },
{ name: 'C', wave: '0..1....0...1..', node: '...c....h..' },
{ name: 'D', wave: '0...1..0.....1.', node: '....d..g...' },
{ name: 'E', wave: '0....10.......1', node: '.....ef....' }
],
edge: [
'a~b t1', 'c-~a t2', 'c-~>d time 3', 'd~-e',
'e~>f', 'f->g', 'g-~>h', 'h~>i some text', 'h~->j'
]
}
折线
- -| -|-
<-> <-|> <-|->
-> -|> -|-> |->
+
具体使用形式及渲染图如下:
{ signal: [
{ name: 'A', wave: '01..0..', node: '.a..e..' },
{ name: 'B', wave: '0.1..0.', node: '..b..d.', phase:0.5 },
{ name: 'C', wave: '0..1..0', node: '...c..f' },
{ node: '...g..h' },
{ node: '...I..J', phase:0.5 },
{ name: 'D', wave: '0..1..0', phase:0.5 }
],
edge: [
'b-|a t1', 'a-|c t2', 'b-|-c t3', 'c-|->e t4', 'e-|>f more text',
'e|->d t6', 'c-g', 'f-h', 'g<->h 3 ms', 'I+J 5 ms'
]
}
9. 代码绘制
除了直接提供 WaveJSON 描述文本,还可以直接写代码,这在某些情况下能提供更高的灵活性和便捷性:
function (bits, ticks) {
var i, t, gray, state, data = [], arr = [];
for (i = 0; i < bits; i++) {
arr.push({name: i + '', wave: ''});
state = 1;
for (t = 0; t < ticks; t++) {
data.push(t + '');
gray = (((t >> 1) ^ t) >> i) & 1;
arr[i].wave += (gray === state) ? '.' : gray + '';
state = gray;
}
}
arr.unshift('gray');
return {signal: [
{name: 'bin', wave: '='.repeat(ticks), data: data}, arr
]};
})(5, 16)
渲染效果如下:
最后,我们可以通过在线编辑器的右下角导出时序图:
官方目前提供两种格式,分别为 SVG 和 PNG。其中 SVG 是基于矢量的图形格式,可以在不损失分辨率的情况下放大缩小。导出后就能得到自己的时序图啦。