Understanding DTE & DCE: SystemVerilog's Perspective

Available in 中文

Intro#

RS-232 is a common electrical interface standard for communications, often physically presented as a DE-9 connector, and people today consistently run UART over it. But with a closer look at the standard TIA/EIA-232-F, readers will find some weird concepts like DTE and DCE. They often appear in many specifications published by TIA.

Searching DTE & DCE on the internet, people tend to present PCs and MODEMs connected to PSTN and assert that

Computers are DTE while MODEMs are DCE.

Dial-up Internet Access

This seems a bit far from the era, so are they abandoned? Not really.

Even if modern embedded software developers only encounter software problems about UART in TTL voltage levels, where there are few pins with no RS-232 voltage levels at all, they will still be exposed to these traditional terms unexpectedly in online documentation.

In this post, I will discuss the motivation of differentiating DTE & DCE and how people today weaken them, then manifest the potential benefits of applying them in digital design, using SystemVerilog.

Motivation#

The motivation behind DTE & DCE is simply that we are defining an interface between two devices. That’s it!

Interface is Bundled Wires with Directions#

This is not generally true. For example, I2C features a bidirectional bus. But for any given instant, the direction should be definitive since signal timing is well defined in the specification.

Let’s have a look at the common SPI interface definition.

SPI Interface Name Meaning Direction
CS Chip Select Main → Sub
SCLK Serial Clock Main → Sub
MOSI Main Out, Sub In Main → Sub
MISO Main In, Sub Out Sub → Main

That is the key point here. To clarify the signal direction, the standard has to classify devices and define the flow between them.

The same applies to RS-232. Here is a common definition for DTE and DCE.

DTEs produce data autonomously while DCEs only do data switching and signal conversion.

RS-232 Interface Name (DE-9) Meaning Direction
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

So it’s always expected to connect some DTE with another DCE together in RS-232, using a straight through cable.

Confusion and Weakening#

Here comes a dilemma, which makes the whole thing much confusing.

The most common use cases for RS-232 are peer-to-peer UART. For example, connecting a PC to an FPGA board with DE-9 connector makes a duplex channel between two devices. The FPGA board is collecting the sensor data and sending it through UART. The question is, which device is acting as a DCE?

In fact, they’re all DTE.

If you want to connect two DTEs together, you need a special crossover cable called null MODEM. This is not even a cable, it’s a fully qualified DCE.

Null MODEM

The differentiation between DTE & DCE claims that the TXD pin on a UART-capable device does not necessarily mean “transmitted data FROM this device”.

But connections between UART DTEs are dominant now, as a result, DTE & DCE are considered so old-school and confusing that modern equipments generally weaken them. So by the natural sense, a pin marked with TXD usually means “transmitted data FROM this device” today, since most of them are DTE in essence.

Ethernet (off-topic)#

Ethernet devices / ports are also classified into DTE & DCE, with straight through or crossover cables, but they are overwhelmingly weakened today in a different fashion.

Unlike RS-232 UART connections, you cannot claim which kind of device is the majority in Ethernet. To reduce the burden on terminal users of using correct cables, auto MDI-X is implemented to negotiate the link (especially for 10BASE-T / 100BASE-TX), no matter what kind of cables they’re using. Furthermore, PMA in 1000BASE-T natively defines that the cable pairs are bidirectional, and can identify many cases of polarity inversion.

For convenience, most Ethernet RJ-45 cables are straight through now, since the orders of cable pairs can be resolved and recognized by Ethernet transceivers. Thus there isn’t sensible distinction as per the connections between routers, switches, hubs and PCs. Same connections leads to same ports, same ports lead to no differences at all for end users.

Applications in Digital Design#

The concepts are not practical in real life because most of the devices are DTE now. But we can have internal DCEs when designing a complex logic on FPGA.

Here we demonstrate some examples in SystemVerilog.

Unified Interface#

This is a UART interface with standard MODEM signals.

 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

To make DTE and DCE come alive, we can use modport keyword to clarify the port direction for DTE and DCE variants.

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

Null MODEM#

This is a construction of a null MODEM. The variant uart_io.dce is used because null MODEM itself is a DCE, and the other end of each port is some DTE.

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

Furthermore we can even remove the variant for the port, the synthesizer will not work if there is a violation on signal directions. (e.g. connect a DTE and a DCE together using null MODEM)

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

Mux#

Mux plays an important role in the next example. It allows the signal select_i to choose which device is connected to the main one. The sample code looks ugly because SystemVerilog does not natively support the interface mux, so developers should manually mux the wires.

 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

A (not quite) surprising result is that null MODEM can be redefined by a mux with static selection.

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

Simple Mux-based Switch#

This is a simple mux-based switch. There is a PC and two extra DTEs (A, B) available. Now you can press the key to switch which DTE is connected to the PC.

I really love this example because it explains why the network nerds use “switch” as a term. This is a basic UART switch, with the same mechanisms behind L2 switch and L3 switch in complex computer networks.

 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

Complete Example#

Check alphabet.sv to see a complete example. The top module is shown as follows:

 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

Having 2 alphabet generators, one is uppercase while the other is lowercase, users can switch the generator after press the key. Remember to change the clock frequency based on your own boards.

 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
);
...

Result

Conclusion#

The concepts of DTE & DCE are invented to clarify the wires’ directions in communications interface. But DTEs are dominant today and TXD RXD on DTEs convey a more natural meaning, so people care less about these concepts, and always use some kind of null MODEM.

Properly making use of these concepts in digital design makes unification possible and produces more elegant HDL code.