深入理解 DTE & DCE:SystemVerilog 的视角

有 1 种语言版本可以阅读 English

Intro#

RS-232 是一个通信中常见的电气接口标准,现在常以 DE-9 的物理端子相互连接,在其之上几乎总是运行 UART. 但当我们细看其标准 TIA/EIA-232-F 时, 就会发现一些诸如 DTEDCE 奇怪的概念. 这些概念一般就出现在 TIA 发布的规范中.

网上搜一下 DTE 与 DCE,总是能看到有人将 PCMODEM 通过 PSTN 拨号上网的时代搬出来,并断言

PC 是 DTE 而 MODEM 是 DCE.

Dial-up Internet Access

然而那个时代离现在似乎有点远,就好像在说这些概念是被历史抛弃了吗?是也不是.

即使当今的嵌入式开发者在使用 TTL 电平的 UART 时碰到一些纯软件问题,他们可能还是会在一些设备文档中不经意之间跟这些术语打交道.

本文将探讨区分 DTE 与 DCE 背后的动机,以及这些概念在当今是怎么被弱化的,并展示在 SystemVerilog 数字设计时合理运用这些概念可能带来的好处.

动机#

因为我们要在设备之间定义一种接口,所以才创造出了 DTE 与 DCE 的概念.

接口就是一捆有方向的线#

这么说可能会有瑕疵. 以 I2C 为例,其中就有双向的数据总线,但考虑任一时刻,数据线的方向总是确定的,因为规范中对于信号时序是良定义的.

我们可以看一下 SPI 的接口定义.

SPI 接口 含义 方向
CS 片选 主设备 → 从设备
SCLK 串行时钟 主设备 → 从设备
MOSI 主设备的输出,从设备的输入 主设备 → 从设备
MISO 主设备的输入,从设备的输出 从设备 → 主设备

这里的关键点就是,为了区分信号的方向,标准必须将设备进行某种分类,然后才能定义它们之间的数据流向.

RS-232 也是这样的,下面是比较常见的一种对于 DTE 与 DCE 的定义.

DTE 自发产生数据而 DCE 负责数据交换与信号转换.

RS-232 接口 含义 方向
DCD Data Carrier Detect DCE → DTE
RXD Received Data DCE → DTE
TXD Transmitted Data DTE → DCE
DTR Data Terminal Ready DTE → DCE
GND Ground
DSR Data Set Ready DCE → DTE
RTS Request To Send DTE → DCE
CTS Clear To Send DCE → DTE
RI Ring Indicator DCE → DTE

为了连接 DTE 与另外一个 DCE,必须要使用直通线.

困境与弱化#

实际场景下迎来了两难,让这些概念变得更加困惑.

RS-232 最常见的应用场景就是点对点的 UART. 例如 PC 连接到 FPGA 上的 DE-9 端子后,就建立了一个全双工的信道,FPGA 收集传感器数据并通过 UART 发送. 问题来了,在这种场景下哪个设备是 DCE 呢?

事实上,它们都是 DTE.

要连接两个 DTE 就必须使用交叉线,也叫零 MODEM. 它甚至不只是一根线,根据定义,它完全有资格作为一个独立的 DCE.

Null MODEM

特意区分 DTE 与 DCE 的话,在运行 UART 设备上标注着 TXD 的引脚就不一定表示 “从该设备发出的数据” 了.

然而运行 UART 的 DTE 设备之间的连接占大头,就使得 DTE 与 DCE 的概念显得格格不入,逐渐被现代设备弱化. 所以现在如果有标注着 TXD 的引脚一般都表示 “从该设备发出的数据”,因为它们本质上一般都是 DTE.

Ethernet (略微偏题,可跳过)#

Ethernet 设备 / 端口同样被分类为了 DTE 与 DCE,也就有直通线交叉线,但它们以另外一种方式被弱化了.

不像 RS-232 之间的 UART 连接,你没法说清楚在 Ethernet 中哪种类型的设备是主体. 为了减轻用户区分不同线缆的心智压力,自动 MDI-X 可以协商链路类型(主要针对 10BASE-T / 100BASE-TX), 使得用户不用关心线缆类型. 更进一步地,PMA 在 1000BASE-T 中直接支持双向信号对,并能识别可能存在的极性翻转.

由于线序可以被 Ethernet 收发器自动解析和识别,现在大多数 Ethernet 的 RJ-45 线缆都是直通线,所以在路由器、交换机、集线器与 PC 之间的连接也就没有什么差别了. 相同的连接也就有相同的端口,相同的端口也就造就了实际用户感受不到差别.

数字设计中的应用#

因为实际生活中的设备以 DTE 居多,这些概念在现实生活中并不实用. 然而在数字设计中,我们可以在内部使用 DCE 来实现复杂的逻辑. 下面我们用 SystemVerilog 演示一些例子.

统一接口#

这是一个含有标准 MODEM 信号集的 UART 接口.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
interface uart_io;
    logic dcd;
    logic rxd;
    logic txd;
    logic dtr;
    logic dsr;
    logic rts;
    logic cts;
    logic ri;
endinterface: uart_io

要让 DTE 与 DCE 有用武之地,我们可以使用 modport 关键字来定义 DTE 与 DCE 变体的信号方向.

Modports

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
interface uart_io;
    logic dcd;
    logic rxd;
    logic txd;
    logic dtr;
    logic dsr;
    logic rts;
    logic cts;
    logic ri;

    modport dte (
        input dcd,
        input rxd,
        output txd,
        output dtr,
        input dsr,
        output rts,
        input cts,
        input ri
    );

    modport dce (
        output dcd,
        output rxd,
        input txd,
        input dtr,
        output dsr,
        input rts,
        output cts,
        output ri
    );
endinterface: uart_io

零 MODEM#

这是一个简单的零 MODEM的构造,这里使用了 uart_io.dce 变体是因为零 MODEM本身是一个 DCE,并且对端分别连接的是 DTE.

1
2
3
4
5
6
module null_modem (uart_io.dce a, uart_io.dce b);
    alias a.rxd = b.txd, b.rxd = a.txd;
    alias a.cts = b.rts, b.cts = a.rts;
    alias a.dsr = b.dtr, b.dsr = a.dtr;
    alias a.dcd = a.dtr, b.dcd = b.dtr;
endmodule: null_modem

更进一步,我们可以直接移除变体的使用,如果有信号方向违例的话(比如把一个 DTE 用零 MODEM连接到了 DTE),综合器是会发出错误消息的.

1
2
3
4
5
6
module null_modem (uart_io a, uart_io b);
    alias a.rxd = b.txd, b.rxd = a.txd;
    alias a.cts = b.rts, b.cts = a.rts;
    alias a.dsr = b.dtr, b.dsr = a.dtr;
    alias a.dcd = a.dtr, b.dcd = b.dtr;
endmodule: null_modem

Mux#

Mux 在下面的一个例子中比较重要,它通过 select_i 信号来选择具体是哪一个设备连接到主设备上. 下面的样例代码看上去比较迂腐,主要是因为 SystemVerilog 不支持基于 interface 的 mux,于是开发者必须手动展开并复用信号线.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
interface uart_io;
    logic dcd;
    logic rxd;
    logic txd;
    logic dtr;
    logic dsr;
    logic rts;
    logic cts;
    logic ri;

    modport dte (
        input dcd,
        input rxd,
        output txd,
        output dtr,
        input dsr,
        output rts,
        input cts,
        input ri
    );

    modport dce (
        output dcd,
        output rxd,
        input txd,
        input dtr,
        output dsr,
        input rts,
        output cts,
        output ri,
        import terminate
    );

    function terminate;
        dcd = 1'b0;
        rxd = 1'b0;
        dsr = 1'b0;
        cts = 1'b0;
    endfunction: terminate
endinterface: uart_io

module uart_mux (
    uart_io.dce x,
    uart_io.dce a,
    uart_io.dce b,
    input logic select_i
);
    always_comb begin
        x.rxd = (select_i == 1'b0) ? a.txd : b.txd;
        x.cts = (select_i == 1'b0) ? a.rts : b.rts;
        x.dsr = (select_i == 1'b0) ? a.dtr : b.dtr;
        x.dcd = (select_i == 1'b0) ? a.dtr : b.dtr;
        if (select_i == 1'b0) begin
            a.rxd = x.txd;
            a.cts = x.rts;
            a.dsr = x.dtr;
            a.dcd = x.dtr;
            b.terminate();
        end
        else begin
            b.rxd = x.txd;
            b.cts = x.rts;
            b.dsr = x.dtr;
            b.dcd = x.dtr;
            a.terminate();
        end
    end
endmodule: uart_mux

一个(并不)令人惊讶的事实,先前的 null MODEM 可由 mux 反向定义,如下所示.

1
2
3
4
module null_modem (uart_io a, uart_io b);
    uart_io dangling ();
    uart_mux mux (a, b, dangling, 1'b0);
endmodule: null_modem

简单的基于 mux 的交换机#

这里是一个简单的基于 mux 的交换机. 有一台 PC 以及两个额外的 DTE,通过按键可以选择 PC 连接到哪个 DTE 上.

这是我最喜欢的例子,因为这很好的解释了为什么网络极客使用「switch」作为交换机的术语. 这个 UART 交换机虽然非常简单,但它与复杂计算机网络中的 L2 交换机、L3 交换机背后的原理是一样的.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
module uart_switch (
    input logic clock_i,
    input logic reset_ni,
    input logic key_i,
    uart_io.dce pc,
    uart_io.dce a,
    uart_io.dce b
);
    logic index = 1'b0;

    logic last_key = 1'b0;
    always @ (posedge clock_i, negedge reset_ni) begin
        if (!reset_ni) begin
            index <= 1'b0;
        end
        else begin
            if (key_i && !last_key) begin
                index <= !index;
            end
            last_key <= key_i;
        end
    end

    uart_mux mux (pc, a, b, index);
endmodule: uart_switch

完整示例#

可以查看完整示例 alphabet.sv,顶层模块如下所示.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
module top (
    input logic clock_i,
    input logic reset_ni,
    input logic key_i,
    uart_io.dce uart
);
    uart_io uart_uppercase ();
    alphabet_generator #(.BASE("A")) uppercase (
        .clock_i(clock_i),
        .reset_ni(reset_ni),
        .uart(uart_uppercase)
    );
    uart_io uart_lowercase ();
    alphabet_generator #(.BASE("a")) lowercase (
        .clock_i(clock_i),
        .reset_ni(reset_ni),
        .uart(uart_lowercase)
    );
    uart_switch switch (
        .clock_i(clock_i),
        .reset_ni(reset_ni),
        .key_i(key_i),
        .pc(uart),
        .a(uart_uppercase),
        .b(uart_lowercase)
    );
endmodule: top

有两个字母生成器,一个大写一个小写,用户可以通过轻触按键切换生成器. 记得把下面的时钟频率给改成合适的值.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
module alphabet_generator #(
    parameter byte BASE = "A",
    parameter longint unsigned CLOCK = 12_000_000,
    parameter longint unsigned DIVISOR = (CLOCK / 9_600)
) (
    input logic clock_i,
    input logic reset_ni,
    uart_io.dte uart
);
...

结果

总结#

为了明确通信接口之间的信号方向,才会发明 DTE 与 DCE 的概念. 然而 DTE 占多数的今天,TXD RXD 这些标注只有在 DTE 才有更自然的含义,所以人们不管不顾 DTE 与 DCE,总是使用某种零 MODEM来连接设备.

在数字设计中恰当地使用这些概念,能让声明更加统一,并产出更优雅的 HDL 代码.