Using the DTR, RTS and CTS pins from UART for your own tricks

There are now several options for inexpensive USB-UART converter chips that allow you to organize data exchange between a device and a computer based on an ancient interface called a COM port (or RS-232).

But in this article we will not delve into the data transfer via this serial interface. I was interested to learn about the capabilities of additional pins that were once used to initialize and synchronize data transfer. These pins include digital outputs DTR and RTS and digital inputs CTS, DSR, DCD and RI.

For testing, I decided to take three of the most popular USB-UART chips: CP2102, FT232, and CH340. The first two chips can be bought on Aliexpress on boards with the pins we need. Only modules with FT232 very often have a fake chip that constantly freezes, so it's better not to buy them there at all.

Standard USB-UART modules with additional pins

Standard USB-UART modules with additional pins

But for the CH340 chip, only modules with the main RX and TX pins are common, without additional ones. Therefore, I had to solder such a module myself for testing. However, I bought a CH340 chip with the index “G” – in this version, the circuit should have a 12 MHz quartz with capacitors. But for the CH340C, quartz is not required, so in the future, it is better to take only it for your crafts.

My beautiful soldering
And it works great.

And it works great.

How to control additional UART pins

I will show code examples in C# from Visual Studio, but everything can be easily transferred to other languages. The main thing is to find the necessary functions for managing the COM port.

To access the port I used a standard C# class System.IO.Ports.SerialPortwhich provides everything you need except the function of reading the RI pin. I don't need this pin for tests, but there is probably some way to read this port pin as well. Perhaps one of the experts will write in the comments how to do this.

Code to manage additional pins in C#
//Создаём объект для дальнейшего использования
public static SerialPort MyPort = new SerialPort();

//Далее просто список часто используемых методов и свойств (не рабочий код)

//Настройка порта:
string[] SerialPort.GetPortNames(); //Выдаёт список названий доступных COM-портов
MyPort.PortName = "COM1"; //Название нужного порта
MyPort.BaudRate = 115200; //Частота обмена данными
MyPort.Parity = Parity.None; //Чётность для проверки данных
MyPort.DataBits = 8; //Количество бит для передачи
MyPort.StopBits = StopBits.One; //Количество стоп-бит
MyPort.ReadTimeout = 200; //Максимальное время ожидания данных
MyPort.WriteTimeout = 1000; // Максимальное время отправки данных

//Инициализация порта
MyPort.Open(); //Открытие указанного порта
MyPort.Close(); //Закрытие активного порта

//Работа с данными
MyPort.Write(byte[] buffer, int offset, int count); //Запись байт в порт
MyPort.Read(byte[] buffer, int offset, int count); //Чтение байт из порта
MyPort.BytesToRead; //Количество доступных байт в приёмном буфере
MyPort.DiscardInBuffer(); //Очистка входного буфера
MyPort.DiscardOutBuffer(); //Очистка выходного буфера

//Управление дополнительными выводами:
MyPort.DtrEnable = true; //Управление выходом DTR
MyPort.RtsEnable = true; //Управление выходом RTS
bool cts_read = MyPort.CtsHolding; //Считывание входа CTS
bool dsr_read = MyPort.DsrHolding; //Считывание входа DSR
bool cd_read = MyPort.CDHolding; //Считывание входа DCD

When writing the value “true”, a logical 0 (zero voltage value) is set at the digital outputs DTR or RTS. When writing “false”, a logical one is set (3.3V or 5V, depending on the power supply circuit of the microcircuit).

Similarly, when reading digital inputs CTS, DSR or DCD: if “true” is returned, then the input is logical 0. If “false” is returned, then the voltage applied to the output is above the response threshold.

When powering up all test chips, the DTR and RTS outputs are set to a high voltage level. This means that they are set to “false” by default. However, in some other USB-UART chips, initialization may be different. In addition, the initialization time may also be different, so immediately after powering up, there may be zeros on the DTR and RTS outputs for some time, which then switch to a high level.

Digital output speed

Unfortunately, the additional UART pins are very slow compared to the data transfer speed of the main TX and RX pins.

To evaluate the running time of the functions, I created a program with a button on the form and wrote the following code for the click event:

private void button1_Click(object sender, EventArgs e)
{
    //Переключение пинов
    for (int i = 0; i < 10; i++)
    {
        MyPort.DtrEnable = !MyPort.DtrEnable;
    }
}

This code inverts the value on the DTR pin 10 times. On the oscilloscope I got the following picture for the CP2102 chip:

As you can see, the duration of switching moments changes, and this happens randomly from launch to launch. For the RTS output, the picture is similar, but the average values ​​of the output function operation time vary greatly for different chips: for CP2102 it is 12-20 ms, for FTDI 6-8 ms, and for CH340G – 2.5-3.5 ms.

These are the values ​​measured with an oscilloscope. But I also tried to estimate the runtime of the write function using the computer's system timer. Below is the code measuring the runtime for 1000 calls to the write function on the DTR port (the same was for RTS). The results are written to a text file for further analysis.

Test code for output to DTR
//Считывает время системного таймера (1 Tick = 100 ns)
Int64 GetTicks()
{
	return DateTime.Now.Ticks;
}

//Обработка нажатия на кнопку запуска теста
private void button2_Click(object sender, EventArgs e)
{
	int n = 1000;//Количество циклов опроса

	string textDelta = "";//Текст для сохранения значений

	for (int i = 0; i <= n; i++)
	{
		bool b = (i % 2 == 0);//Значение для записи в порт

		Int64 start_time = GetTicks();//Запоминаем начальное время

		MyPort.DtrEnable = b;//Запись в DTR

		Int64 deltaTime = GetTicks() - start_time;//Вычисляем время работы

		//Строим текстовую таблицу c микросекундами:
		textDelta += ((double)deltaTime * 0.1).ToString("F01") + "\n";
	}

	//Сохраняем данные
	File.WriteAllText(@"z:\WriteTimes.txt", textDelta);
}

The resulting tables with the function execution times turned out to be a bit strange. So I presented the results of some measurements as histograms with 20 intervals. Below is a scale with milliseconds (intervals), and on the Y axis is the number of hits in the corresponding interval.

Histograms for 1000 measurements
For CP2102 chip

For CP2102 chip

For CH340 chip

For CH340 chip

For FT232 chip

For FT232 chip

As you can see, the time spent is mostly close to whole milliseconds, although the values ​​of the lower digits are always random. That is, for example, for CP2102 the time is mostly either about 10 ms, or 12 ms, or 14 ms, and the other intervals are rarer.

Probably, the reason for such variations is in the different operation of the drivers for these chips in the Windows system. There is probably a link to the Windows software timer, which works unstably. Perhaps, somewhere in the driver the function is used Sleep(1) while waiting for a response from the chip. This explains the fact that when the program is running, it does not load the processor.

I don't think the time will change much from computer to computer, but in other operating systems the values ​​may be completely different. Also the time may be different if you use some low-level Windows functions to work with ports.

To evaluate the read speed of the pins I also used the system timer. Below is the code for reading the CTS input (for DSR and DCD it will be similar).

Test code for reading CTS
private void button3_Click(object sender, EventArgs e)
{
	int n = 10;//Количество циклов опроса

	string textDelta = "";
	string textValue = "";

	for (int i = 0; i <= n; i++)
	{
		Int64 start_time = GetTicks();//Запоминаем начальное время

		bool pinValue = MyPort.CtsHolding;//Считываем значение на входе

		Int64 deltaTime = GetTicks() - start_time;//Вычисляем время работы

		//Строим текстовую таблицу c микросекундами:
		textDelta += ((double)deltaTime * 0.1).ToString("F01") + "\n";

		//Строим таблицу с входными значениями:
		if (pinValue)
		{
			textValue += "1\n";
		}
		else
		{
			textValue += "0\n";
		}
	}

	//Сохраняем данные
	File.WriteAllText(@"z:\ReadTimes.txt", textDelta);
	File.WriteAllText(@"z:\Values.txt", textValue);
}

Here everything is similar in terms of spread: mostly the time takes milliseconds, but sometimes there are intermediate values. However, on average, it takes less time to read. I got the following average values: for CP2102 and CH340G it is 2-3 ms, and for FTDI 0.001-1 ms (faster than all). And for other inputs these values ​​are very similar.

Of course, such speed of work is very bad. But for some tasks it can be useful.

Don't forget about safety

Below, various generalized circuits will be shown to help you understand the basic idea. But when creating a real device, you should always remember the risks of working with high voltages and currents. If you decide to add an additional power supply to your device to turn on some load, you should also add galvanic isolation to avoid accidentally burning out the computer's USB port or the entire computer. For example, you can use optocouplers, solid-state relays, or low-power mechanical relays.

Also remember that by default the USB port limits the current consumption to 0.1A and exceeding this threshold may cause the port to turn off.

And another important detail: ready-made USB-UART modules can be configured to work with both 5V and 3.3V logic. In the following examples, I used the 5V option everywhere, but the circuits should work with 3.3V logic as well.

Flashing LEDs

Low-power LEDs can be connected directly to the DTR and RTS pins, and if you need to turn on something more powerful, you can use transistors.

LED connection diagram

LED connection diagram

I think there is no need to explain how to turn the LEDs on and off. Everything should be clear from the previous examples.

Indicators in the form of LEDs can be useful for displaying the status of some long processes on the computer. In fact, it is very convenient: you can separately launch a process that monitors another process and turns on the LEDs in accordance with the operating modes. You can even do such exotic debugging of programs: evaluating the work of your program by LEDs is much more interesting than through the debug console.

And if you need to make something like a traffic light, you can add a K561ID1 binary decoder chip (analogous to CD4028B). Such a circuit will make it possible to display the status as the glow of one of 4 different LEDs (for example, blue, green, yellow, red).

Scheme with decoder

However, due to the inertia of the DTR and RTS pins, when they are switched, the wrong LEDs will sometimes blink.

The TX pin can be supplemented with a monopulse generator on the NE555 chip to blink another LED. That is, you can send a zero byte to the port to light the LED for a while. This can be used to show that some actions are being performed regularly.

Here is a function that outputs a zero byte to TX, acting as a sync pulse:

//Выводит импульс на выход TX
void WriteTxClock()
{
    MyPort.Write(new byte[1] { 0 }, 0, 1);
}
Monopulse generator circuit for 555
In my opinion, a 0.22 μF capacitor gives the optimal duration.

In my opinion, a 0.22 μF capacitor gives the optimal duration.

Reading the buttons

Since we have digital inputs CTS, DSR and DCD, we can connect buttons to them. We need to add pull-up resistors to them and capacitors to smooth out contact bounce.

Button connection diagram
Capacitors are not required

Capacitors are not required

In this circuit, the buttons short the output to ground, so in the program, when the button is pressed, the read function will return the value “true”.

To read the buttons, you can add a timer component to your C# program. System.Windows.Forms.Timerset its response time to 1-10 ms, and in processing the timer response event, you can read the values ​​of the CTS, DSR, and DCD pins. By analyzing the read values, you can perform some actions, for example, launch some programs. This is how you can make primitive quick launch buttons for programs.

In addition to regular buttons, you can use a pedal button, a laser barrier, or a thermal relay output. You can even connect digital inputs to the output of some generator to read random bits if you need a simple random number generator. There are many ideas you can come up with.

We make more exits

If digital outputs are not enough for you, you can add 74HC595 serial-parallel register chips. The DTR pin can be assigned to the data input of such a register, the RTS pin can be assigned to the data “latch” (LD input of the register), and zero bytes of data from the serial port, i.e., from the TX pin, can be used as synchronization pulses. You just need to invert this signal, for example, using a transistor.

Connection diagram of the register 74HC595
By the way, I used "digital transistor"which already has resistors inside

By the way, I used a “digital transistor” which already has resistors inside.

Moreover, it is possible to connect many 74HC595 registers in series. Each microcircuit will provide 8 digital outputs.

True, the speed of data output to these registers will be low. If you call the write function to DTR only when the bit value changes, then the fastest bytes to be output will be 0x00 and 0xFF, and the slowest will be 0x55 and 0xAA (0b01010101 and 0b10101010).

Code for writing a byte to register 74HC595
void WriteTxClock()
{
    MyPort.Write(new byte[1] { 0 }, 0, 1);
}

void WriteDtr(bool data)
{
    MyPort.DtrEnable = !data;
}

void WriteRts(bool data)
{
    MyPort.RtsEnable = !data;
}

bool[] ConvertByteToBoolArray(byte data)
{
    bool[] result = new bool[8];

    for (int i = 0; i < 8; i++) 
    {
        if ((data & (1 << i)) != 0) 
        {
            result[i] = true;
        } else
        {
            result[i] = false;
        }
    }
    return result;
}

//Вывод байта в регистр 74HC595
void WriteToSerialReg(byte data)
{
    bool[] bits = ConvertByteToBoolArray(data);

    bool data_bit = false;
    for (int i = 0; i < 8; i++) 
    {
        if ((i == 0) | (data_bit != bits[i]))
        {
            WriteDtr(bits[i]);
        }
        data_bit = bits[i];
        
        WriteTxClock();
    }

    //Load clock
    WriteRts(true);
    WriteRts(false);
}

As before, using the computer's system timer, I estimated the execution time of the functions. For the CP2102 chip, I got an average byte write time (depending on its value) from 39 ms to 130 ms, for the FT232 – from 20 ms to 69 ms, and for the CH340 chip – from 8 ms to 30 ms.

We make more entrances

There is another type of register: parallel-serial register – 74HC165 microcircuit. It has 8 inputs, the values ​​of which can be read sequentially. Such registers can be used to increase the number of inputs.

We can assign the RTS pin to the data “latch”, and also use the TX pin for synchronization (this will be faster than generating pulses via DTR). Data can be read via any input, for example, via CTS. Just keep in mind that the 74HC165 register has an inverted LD input (data “latch”): data “latching” will occur when RTS transitions from 1 to 0.

Connection diagram of the register 74HC165
Code to read a byte from 74HC165
bool ReadCts()
{
    return !MyPort.CtsHolding;
}

byte BoolArrayToByte(bool[] bits)
{
    byte result = 0;

    for (int i = 0; i < 8; i++)
    {
        if (bits[i])
        {
            result |= (byte)(1 << i);
        }
    }

    return result;
}

byte ReadSerialReg()
{
    bool[] bits = new bool[8];
    
    //Load clock
    WriteRts(false);
    WriteRts(true);

    for (int i = 0; i < 8; i++)
    {
        bits[i] = ReadCts();

        WriteTxClock();
    }

    return BoolArrayToByte(bits);
}

The speed of reading data from the register will be higher than when outputting data, because, as we found out earlier, reading CTS takes less time. For the CP2102 chip, I got an average byte read time of 45 ms, for the FT232 – 14 ms, and for the CH340 – 24 ms.

Similarly, several 74HC165 microcircuits can be connected at once. But since we have several free digital inputs, to increase the number of registers, they can be installed not in series, but in parallel, to save time on the number of synchronization pulses (although this will be a very small time saving).

More inputs and outputs

If you need to expand inputs and outputs at the same time, you can use both types of registers. For example, you can make 8 inputs and 8 outputs using both 74HC165 and 74HC595.

Wiring diagram 74HC165 and 74HC595
The second transistor is needed to invert the LD signal for 74HC165

The second transistor is needed to invert the LD signal for 74HC165

True, here, to read the inputs from 74HC165, you must first load the data into 74HC595. That is, you cannot read the inputs without updating the outputs, so in any case you will have to spend double the time.

Code for working with 74HC595 and 74HC165
byte WriteAndReadSerialReg(byte data)
{
    WriteToSerialReg(data);

    bool[] bits = new bool[8];

    for (int i = 0; i < 8; i++)
    {
        bits[i] = ReadCts();

        WriteTxClock();
    }

    return BoolArrayToByte(bits);
}

Some of the features have been shown above.

For the CP2102 I got average write and read times from 60ms to 152ms (depending on the byte being output), for the FT232 – from 20ms to 69ms (reading is very fast here), and for the CH340 – from 33ms to 59ms.

How it looked in reality

Making an I2C interface

There are quite a lot of devices that can be controlled via the I2C interface. Usually, such devices are not demanding on the data exchange rate. Therefore, having two digital outputs, you can easily organize such an interface by simply adding a transistor with pull-up resistors to the outputs. If you need to read data, you can use the CTS input.

I2C interface organization diagram

To check the operation of our super brake slow I2C, I decided to connect to it a module of a seven-segment indicator with 6 digits with the TM1637 driver.

LED indicator with TM1637 driver

LED indicator with TM1637 driver

Such displays are sold on Aliexpress, but often there are too large capacitors on the SDA and SCL lines, smoothing the signals. So if the display does not work, find and remove these capacitors. I talked about this malfunction in in your video.

To output data to this indicator, we need to create low-level I2C protocol functions, as well as a function for setting the brightness and outputting information to the display.

Code for display output with TM1637 driver
void I2C_SetClockLow()
{
    MyPort.RtsEnable = false;
}

void I2C_SetClockHigh()
{
    MyPort.RtsEnable = true;
}

void I2C_SetDataLow()
{
    MyPort.DtrEnable = false;
}

void I2C_SetDataHigh()
{
    MyPort.DtrEnable = true;
}

void I2C_Start()
{
    I2C_SetClockHigh();
    I2C_SetDataHigh();
    I2C_SetDataLow();
}

void I2C_Stop()
{
    I2C_SetClockLow();
    I2C_SetDataLow();
    I2C_SetClockHigh();
    I2C_SetDataHigh();
}

bool I2C_ReadAck()
{
    I2C_SetClockLow();
    I2C_SetDataHigh();
    bool ack = ReadCts();
    I2C_SetClockHigh();
    I2C_SetClockLow();
    return ack;
}

void I2C_WriteByte(byte data)
{
    bool[] bits = ConvertByteToBoolArray(data);

    bool data_bit = false;
    for (int i = 0; i < 8; i++)
    {
        I2C_SetClockLow();
        if ((i == 0) | (data_bit != bits[i]))
        {
            if (bits[i])
            {
                I2C_SetDataHigh();
            }
            else
            {
                I2C_SetDataLow();
            }
        }
        data_bit = bits[i];

        I2C_SetClockHigh();
    }
}

//Convert number to 7-segment code
byte NumberToSegments(int n)
{
    if (n == 0) return 0x3F;//0
    if (n == 1) return 0x06;//1
    if (n == 2) return 0x5B;//2
    if (n == 3) return 0x4F;//3
    if (n == 4) return 0x66;//4
    if (n == 5) return 0x6D;//5
    if (n == 6) return 0x7D;//6
    if (n == 7) return 0x07;//7
    if (n == 8) return 0x7F;//8
    if (n == 9) return 0x6F;//9
    if (n == 10) return 0x77;//A
    if (n == 11) return 0x7C;//B
    if (n == 12) return 0x39;//C
    if (n == 13) return 0x5E;//D
    if (n == 14) return 0x79;//E
    if (n == 15) return 0x71;//F
    if (n == 16) return 0x40;//-
    if (n == 17) return 0x77;//A
    if (n == 18) return 0x3D;//G
    if (n == 19) return 0x76;//H
    if (n == 20) return 0x3C;//J
    if (n == 21) return 0x73;//P
    if (n == 22) return 0x38;//L
    if (n == 23) return 0x6D;//S
    if (n == 24) return 0x3E;//U
    if (n == 25) return 0x6E;//Y
    return 0x00;
}

//Send segments data into display
//d0 - low, d5 - high
void DisplayUpdate(byte d0, byte d1, byte d2, byte d3, byte d4, byte d5)
{
    I2C_Start();
    I2C_WriteByte(0x40);//Memory write command
    I2C_ReadAck();
    I2C_Stop();

    I2C_Start();
    I2C_WriteByte(0xc0);//Start address
    I2C_ReadAck();

    I2C_WriteByte(d3);
    I2C_ReadAck();
    I2C_WriteByte(d4);
    I2C_ReadAck();
    I2C_WriteByte(d5);
    I2C_ReadAck();
    I2C_WriteByte(d0);
    I2C_ReadAck();
    I2C_WriteByte(d1);
    I2C_ReadAck();
    I2C_WriteByte(d2);
    I2C_ReadAck();
    I2C_Stop();
}

// Brightness values: 0 - 8
void SetBrightness(byte brightness)
{
    I2C_Start();
    brightness += 0x87;
    I2C_WriteByte(brightness);
    I2C_ReadAck();
    I2C_Stop();
}

//Send number into display
void DisplaySendNumber(int num)
{
    byte dg0, dg1, dg2, dg3, dg4, dg5;
    dg0 = NumberToSegments((byte)(num / 100000));
    num = num % 100000;
    dg1 = NumberToSegments((byte)(num / 10000));
    num = num % 10000;
    dg2 = NumberToSegments((byte)(num / 1000));
    num = num % 1000;
    dg3 = NumberToSegments((byte)(num / 100));
    num = num % 100;
    dg4 = NumberToSegments((byte)(num / 10));
    num = num % 10;
    dg5 = NumberToSegments((byte)num);
    DisplayUpdate(dg5, dg4, dg3, dg2, dg1, dg0);
}

Everything worked fine, except for the big delay in display refresh. Calling the function SetBrightness for CP2102 it takes on average 348 ms, for FT232 – 193 ms, and for CH340 – 91 ms.

Function DisplayUpdate for CP2102 it takes from 2.1 s (when all bytes are 0xFF or 0x00) to 2.6 s (when all bytes are 0xAA). For FT232 similar tasks take from 1.2 s to 1.5 s, and for CH340 – from 545 ms to 725 ms.

Let me remind you that this time is unstable and individual calls may take 20-30% more or less time than indicated. However, with other chips or drivers this time may be radically different.

Photo of the working diagram
When updating, the numbers are replaced sequentially and this is noticeable

When updating, the numbers are replaced sequentially and this is noticeable

Summary

As we can see, the additional pins of the USB-UART converter can be quite useful for some tasks, although with great limitations. I am sure that the functionality of these devices can be significantly expanded if more logic chips are added, but in the modern world it will be easier to attach some microcontroller to the port. Sometimes it will even be easier to connect directly to USB if the microcontroller has such an interface.

By the way, if you select the COM1 port, the speed of the functions is much higher. But I did not check this port with a real circuit, since it is hidden somewhere inside the computer.

All mentioned code for circuit tests can be downloaded in the archive with the Visual Studio project from my website.

Write in the comments what ideas you have come up with for using these additional USB-UART pins. Maybe we can invent some new “wheel” together?

Why did I need all this?

In fact, I had a specific goal when studying this topic: I wanted to make a simple programmer for STM32 based on a USB-UART converter, in which the DTR and RTS pins are used to automatically reset the microcontroller and activate the built-in factory program loader. I needed to understand what limitations there are when working with these outputs. And since I collected the information, I decided to write an article about it at the same time.

By the way, to some extent I have implemented such a programmer: it is called NyamFlashLoader. It programs some microcontrollers (mostly old models). At the same time, I wanted to make a universal programmer, but, as it turned out later, different microcontrollers activate the built-in bootloader differently, and I implemented only one option. Therefore, at the moment, this project is suspended.

Thank you all for your attention!

Similar Posts

Leave a Reply

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