Quentin Santos

Obsessed with computers since 2002

Linux always toggles DTR & RTS

In my previous article, I explained how Arduino makes the life of its users easier by automatically resetting the board when the UART pin DTR (or RTS) transitions from electrically high to low. This exploits the fact that this transition happens automatically when someone or something opens the serial device on the host. That is, opening a file whose name looks like:

  • /dev/ttyUSB0 on Linux
  • /dev/cu.usbserial-1410 on macOS
  • COM1 on Windows

On Windows, you can avoid the transition of DTR and RTS to electrically low with the proper system calls:

HANDLE h = CreateFileA("COM1", GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_EXISTING, 0, 0);

DCB dcb = {0};
dcb.DCBlength = sizeof dcb;
GetCommState(h, &dcb);
dcb.fDtrControl = DTR_CONTROL_DISABLE;
dcb.fRtsControl = RTS_CONTROL_DISABLE;
SetCommState(h, &dcb);

On Linux, however, this is unavoidable, unless you patch your kernel. The user or program can return them to electrically high after opening the serial device, but they will first transition to low.

Let’s see this in practice.

Observing DTR & RTS when opening

First, let’s see the case of the FT232RL-based board I used in the Arduino article.

To test the behavior, connect the device with a USB cable and open the serial device. This can be achieved by running your usual serial client, such as tio or PuTTY. The effect can be observed with a basic multimeter which you can get for 5€.

When the board is plugged in, the red LED in the bottom left corner lights up. As long as the serial device is not being opened on the computer, the DTR pin is at 5V.
As soon as I open the serial device, the voltage of DTR drops to 0V.

The decision to transition DTR and RTS to electrically low is made by the host computer, not the FT232RL chip. You can see this by looking at the debug logs of the driver, here ftdi_sio.

When we open /dev/ttyUSB0, the driver sets DTR and RTS to logically high. Somewhat confusingly, the convention is that DTR and RTS are considered to be “asserted”, or logically high, when they are electrically low. This is sometimes indicated in schematics by labels that can be any of:

  • nDTR / nRTS
  • DTR / RTS
  • DTR# / RTS#

These notations indicate that the logical meaning is inverted relative to the electrical meaning. Note that nDTR is not “the opposite of DTR”; it just indicates that, when this line is electrically high, the signal is considered to be logically low.

In any case, we can see that the ftdi_sio driver transitions DTR and RTS as expected.

Toggling DTR and RTS

We can also check the state of DTR and RTS in tio using Ctrl+t L:

Here, the “line states” are referring to the electrical level, not the logical one. CTS, DSR, DCD and RI are yet other UART pins, that we are not using currently. DTR and RTS are set by the host, while CTS, DSR, DCD and RI are only read, so they all stay electrically high.

We can manually toggle them between electrically high and low using Ctrl+t g:

Unavoidable toggling of DTR & RTS on Linux

Let’s see in detail what happens when we open the serial device on Linux. I am currently running Debian 13 (trixie) with Linux 6.12.21-amd64, so I will look at the v6.12 git tag of the Linux source tree.

You can skip to the discussion on tty_port_block_til_ready() but, following the steps in logical order, from the handler of the open() systemcall:

Now, this is where things start to become interesting:

int tty_port_block_til_ready(struct tty_port *port,
        struct tty_struct *tty, struct file *filp)
{
    // ...

        /* Indicate we are open */
        if (C_BAUD(tty) && tty_port_initialized(port))
            tty_port_raise_dtr_rts(port);

    // ...
}

Note that there is no user control on whether that tty_port_raise_dtr_rts() is called or not. We can continue following the call tree downwards to confirm this, and better understand what UART drivers really do in practice.

The FT232RL driver

In that last function, update_mctrl(), we can see how the USB message is formatted in a FTDI-specific way:

    value = 0;
    if (clear & TIOCM_DTR)
        value |= FTDI_SIO_SET_DTR_LOW;
    if (clear & TIOCM_RTS)
        value |= FTDI_SIO_SET_RTS_LOW;
    if (set & TIOCM_DTR)
        value |= FTDI_SIO_SET_DTR_HIGH;
    if (set & TIOCM_RTS)
        value |= FTDI_SIO_SET_RTS_HIGH;
        rv = usb_control_msg(port->serial->dev,
                usb_sndctrlpipe(port->serial->dev, 0),
                FTDI_SIO_SET_MODEM_CTRL_REQUEST,
                FTDI_SIO_SET_MODEM_CTRL_REQUEST_TYPE,
                value, priv->channel,
                NULL, 0, WDR_TIMEOUT);

The various FTDI-specific messages are explained in the header file. Although all the UART-to-USB adapter chips provide virtually the same features, each manufacturer has implemented these messages in a slightly different way, requiring different drivers. USB CDC-ACM is a standardization of the USB messages that should be used when communicating with an UART adapter over USB.

The CH340 driver

We can proceed similarly for the UART adapter I used in the “real” UART article:

static void ch341_dtr_rts(struct usb_serial_port *port, int on)
{
    // ...
    if (on)
        priv->mcr |= CH341_BIT_RTS | CH341_BIT_DTR;
    else
        priv->mcr &= ~(CH341_BIT_RTS | CH341_BIT_DTR);
    // ...
    ch341_set_handshake(port->serial->dev, priv->mcr);
}


static int ch341_set_handshake(struct usb_device *dev, u8 control)
{
    return ch341_control_out(dev, CH341_REQ_MODEM_CTRL, ~control, 0);
}

static int ch341_control_out(struct usb_device *dev, u8 request,
			     u16 value, u16 index)
{
    // ...
    r = usb_control_msg(dev, usb_sndctrlpipe(dev, 0), request,
            USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_DIR_OUT,
            value, index, NULL, 0, DEFAULT_TIMEOUT);
    // ...
}

This time, the details, such as the codes being passed are defined at the top of the source file itself. By comparing these two drivers, you can see how they have to send different USB messages to achieve similar features.

Comments

2 responses to “Linux always toggles DTR & RTS”

  1. Paul Duivestein Avatar
    Paul Duivestein

    (Interesting subject: cryptography. I toyed with a cypher that I invented, based on a rather elaborate system in which primes play an important role, but quite different than the way they are used nowadays. I think it is a beautiful subject and I love to think out my own systems, not to be very practical, but more as a proof of concept. I studied and teached math before I turned to multimedia design and programming for educational purposes, now retired.)

    But … that is not why I react. It is obvious that you have a deep insight in the code underlying the signalling back and forth between the system (Linux/Lubuntu in my case) and the device (CH340 in my case).

    Therefore I want to ask you how I could take care of setting the RTS on a CH340 module from a bash at my Linux laptop. I want to use it to steer a Solid State Relay into a low or a high state which should persist after setting it until the script that did set it changes it again.

    I do not care if the RTS is undefined for a short period, as long as it remains in the wished state after that.

    It would help me a great deal in my little project.

    1. Quentin Santos Avatar

      Hey Paul,

      If you use the tio command-line tool, you can manually toggle the UART lines, such as RTS using “Ctrl+t” followed by “g”, followed by “1”. To list the other available commands, press “Ctrl+t” followed by “?”.

Leave a Reply

Your email address will not be published. Required fields are marked *

About me

I have always been fascinated with computers. Nowadays, I mostly use Rust, but I started out with a QuickBASIC book from the local library when I was in elementary school. I also got a Master in computer science from ENSL and a PhD in cryptography from ENS.

qsantos@qsantos.fr

All articles