RS-485 for BeagleBone: a quick peek at the OMAP UART

A few weeks ago, I published a blog post demonstrating the need of a real-time solution in order to support the RS-485 standard reliably on BeagleBones. Although I also wrote that the existing OMAP UART (official UART on the BeagleBone SoC) is not a solution to consider here, I decided to take a look at it anyway, out of curiosity.

Actually, I was really interested in discovering how the authors of the driver implemented support for RS-485, which was added in August 2013. Then, when trying it and looking at the source later, I noticed a minor issue affecting performance during ends of transmissions (which is a critical step when implementing RS-485). This led me to submit a patch to the Linux kernel, which was merged last week.

If you are interested in the Linux kernel and industrial protocols, you should appreciate this rather technical post.

Source code analysis

Quickly reading the new version of omap-serial.c (with added RS-485 support), I found that many properties related to RS-485 may now be added to UART OMAP nodes in the DTS. Amongst them, rs485-rts-delay caught my attention: a first look at the code revealed a few calls to mdelay, which uses the values of this property (busy-wait delays before and after UART transmissions).

I first thought those calls were responsible for ensuring a completed transmission: waiting for a fixed time interval after receiving an empty transmit FIFO interrupt. However, when actually reading the code, I found that the mdelay calls are only waiting for an extra time to be added before and after transmissions, while the actual end of transmission is known otherwise. Simply set both values of rs485-rts-delay to 0 in order to get the shortest possible turnaround delay. In fact, the values of this property are directly given to the delay_rts_before_send and delay_rts_before_send members of an instance of struct serial_rs485, a structure defined by the Linux serial subsystem itself.

The data direction signal uses a GPIO pin reserved by the device driver.

In short, here’s how the OMAP UART transmission works within its Linux driver when RS-485 support is enabled:

  • When the Linux serial subsystem asks the driver to start a transmission, the latter enables transmit FIFO interrupts (or THR interrupts, for Transmitter Holding Register). The driver also asserts the RS-485 data direction GPIO pin to take the bus.
  • The OMAP UART asserts a THR interrupt as long as its transmit FIFO contains 32 characters or less.
  • When the driver handles a THR interrupt, it adds up to 16 new characters to this FIFO, potentially increasing its level to 48.
  • If the driver, when a THR interrupt is asserted, has nothing else to provide to its UART (no more user-supplied characters), it checks if the transmission is completed (a status bit is dedicated to this condition), and:
    • if the transmission is not finished, it leaves the THR interrupt enabled;
    • if the transmission is finished, it disables the THR interrupt and deasserts the data direction GPIO pin in order to release the RS-485 bus.

Bearing in mind that the UART asserts a THR interrupt as long as its transmit FIFO level is equal or less than 32 characters, we see that the driver is essentially polling the end of transmission status bit during the whole transmission time of 33 characters (since as soon as the transmit FIFO level reaches 32 characters, another one is being transmitted). Indeed, by not disabling the interrupt, the handler is called back immediately since the transmit FIFO is in the process of emptying out. At 9600 bauds (with 1 stop bit and without parity), we’re talking about 34 ms of the ARM doing nothing else but handling UART interrupts. This is true everytime there’s an end of transmission, which obviously happen very often in applications that need RS-485, like industrial control systems.

Let’s confirm those assumptions physically.

Preparing a first test

In my DTS file, I specified no additional delay after/before transmissions. I also set the node to enable RS-485 support as soon as the driver loads. I could also have enabled RS-485 afterwards, in userspace, by calling ioctl with appropriate values. Since the first UART instance, UART0, is used for the console, I’m testing with UART1 (the AM335x has 6 OMAP UARTs):

uart1: serial@48022000 {
    pinctrl-names = "default";
    pinctrl-0 = <&uart1_pins>;

    /* No extra delay after/before transmissions */
    rs485-rts-delay = <0 0>;

    /* GPIO1 pin 17 for data direction */
    rts-gpio = <&gpio1 17 GPIO_ACTIVE_HIGH>;

    /* GPIO1 pin 16 for testing */
    test-gpio = <&gpio1 16 GPIO_ACTIVE_HIGH>;

    /* Enable RS-485 */
    linux,rs485-enabled-at-boot-time;

    status = "okay";
};

The symbol uart1_pins is a reference to the following configuration node for pin multiplexing:

uart1_pins: pinmux_uart1_pins {
    pinctrl-single,pins = <
        0x180 (PIN_INPUT_PULLUP | MUX_MODE0)    /* uart1_rxd */
        0x184 (PIN_OUTPUT_PULLDOWN | MUX_MODE0) /* uart1_txd */
        0x044 (PIN_OUTPUT_PULLDOWN | MUX_MODE7) /* gpio1_17 */
        0x040 (PIN_OUTPUT_PULLDOWN | MUX_MODE7) /* gpio1_16 */
    >;
};

Pin 17 of the GPIO1 controller is available on the P9 expansion header, next to the Tx output of UART1, on the white BeagleBone I’m using to test (pins 23 and 24 of this expansion header). In order to verify the frequency of THR interrupts, I also added another GPIO output (pin 16 of GPIO1, which is pin 15 of BeagleBone’s P9 expansion header) that will assert a very short pulse everytime transmit_chars is called (when there’s a THR interrupt).

Expansion Header P9 Pinout Table

I’m using the Saleae Logic logic analyzer to verify precisely the UART outputs and the RS-485 turnaround delay. Here’s my setup:

Test setup

The orange probe is connected to the Tx output of UART1, while the red one is probing the RS-485 data direction output. The blue probe is used for THR interrupts pulses.

Now, on the BeagleBone userspace side:

# stty -F /dev/ttyO1 115200 cs8
# while true; do
    echo -n '0123456789abcdef0123456789abcdefWXYZ0123456789abcdef0123456789abcdef' > /dev/ttyO1
  done

The time required from one iteration to the next to open, configure and close the /dev/ttyO1 port is way enough to properly delimit each little serial transmission. The WXZY part is used here to isolate the 32 last characters of a single transmission.

First test

Using the Saleae analysis software shipped with the Logic, I read 5 million samples at 12 MHz:

Salae Logic result diagram

This looks like the expected behaviour. First of all, three THR interrupts are handled successively (just before the 4 ms mark):

  • The first one adds 16 characters to the empty UART transmit FIFO. The UART continues asserting a THR interrupt since its transmit FIFO has less than 33 characters. Moreover, it immediately starts the transmission of the first character, making the transmit FIFO level 15 characters.
  • The second THR interrupt adds 16 more characters, which increases the transmit FIFO level to 31 characters. Again, the transmit FIFO has 32 characters or less, so the UART continues asserting a THR interrupt.
  • Finally, the third THR interrupt adds 16 more characters to the transmit FIFO, making the UART not hungry anymore. The transmit FIFO count is now 47.

The transmit FIFO now has to drain 15 characters before the next THR interrupt is asserted, which we observe with the transmission of characters 1 to f. As soon as f is copied from the transmit FIFO to the UART’s transmit shift register, the FIFO level becomes 32, explaining the pulse seen just before f is sent.

This cycle repeats itself until only 4 characters can be added (because of the substring WXYZ) since the driver buffer has nothing else to provide. Those characters are the last cdef to be transmitted at the end. When the next THR interrupt is asserted, just before transmitting Z, the driver buffer is now empty, so it enters in this polling loop we discussed before. This loop goes on during the transmission of 33 characters. The big white rectangles on the above screenshot are in fact an interrupt flood, more easily seen when we zoom in:

Salae Logic result diagram

Eventually, the last bit is completely transmitted. This condition being satisfied, the driver finally disables THR interrupts and deasserts the RS-485 data direction GPIO pin.

As you can see on the last screenshot, the RS-485 turnaround delay is pretty good (much shorter than the transmission time of a single character). By constantly polling the end of transmission status bit thanks to the interrupt handler being called back all the time, the driver is able to achieve a decent delay between the effective end of a transmission and the update of the RS-485 data direction signal.

Improving omap-serial.c

The behaviour of the UART driver shown above can be improved. The OMAP UART is compatible with 16C750, a well-known UART type, but it also adds a supplementary control register, SCR. The TMEMPTYCTLIT bit of SCR is especially interesting if set: THR interrupts is generated when TX FIFO and TX shift register are empty. This is exactly what is needed.

Obviously, we don’t want to be interrupted at the end of a transmission when the driver buffer is not empty: the transmission has to be continuous, without « holes ». So we only need to set TMEMPTYCTLIT when handling a THR interrupt without it (transmit FIFO reached its threshold) and when the driver has nothing else to provide to the UART, which means an end of transmission is inevitably upcoming. In this case, we must also keep THR interrupts enabled. The next asserted THR interrupt should then tell us that the transmission is finished, in which case we may now disable THR interrupts with peace of mind.

Results? See the difference:

Salae Logic result diagram

If, during the transmission of the last 32 characters, a new transmission is asked to the driver, then TMEMPTYCTLIT is unset, which causes the assertion of a THR interrupt since the transmit FIFO level is below 33 characters. This is needed to avoid holes between two consecutive independent transmissions.

Although the OMAP UART seems to support the RS-485 standard with a pretty decent turnaround delay, keep in mind that it’s still not a real-time solution. The turnaround delay is always dependent upon the driver availability and therefore not guaranteed. If many other interrupts with higher priorities have to be handled, the OMAP UART driver one will undergo a greater latency, and so will be the turnaround delay.

* The patch was submitted to Linux on October 23th, 2013 and merged on October 29th.

12 réponses à “RS-485 for BeagleBone: a quick peek at the OMAP UART”

  1. Hi, great post..

    Can the BeagleBone be used as a master to collect data from a RS-485 Modbus slave? Do I need to buy the RS-485 cape to do this?

    Your post is way beyond my basic understanding of RS-485…!

    Répondre

  2. Thank you, Chris.

    Yes, the BeagleBone can be used as an RS-485 master (or as a slave). The underlying communication protocol may be anything since it will be implemented in userspace, so Modbus RTU is not a problem. Take a look at this C library, for example: http://libmodbus.org/.

    Like I wrote in my previous post, you need some sort of transceiver to convert RS-485 electrical differential signalling to a simple TTL/CMOS UART (and vice versa). In this regard, the RS-485 cape you’re talking about is a good choice, although smaller boards could do the same job (like this one).

    The conclusion of this post is that the RS-485 functionality of the OMAP driver seems to work fine, but is not a real-time solution and could malfunction in certain specific conditions, i.e. with sudden heavy interrupt loads. I will present a real-time solution in my next post which doesn’t require an extra component (except for the aforementioned transceiver).

    Répondre

  3. Hi, I have found your post after getting some problems with the direction control.

    I have a problem while receiving the data. For testing I am just executing cat /dev/ttyO2. I get some good characters, but many trash as the flow direction pin goes high in my case. I have added some printout at start_tx and stop_tx.

    [ 1645.218505] omap_uart 48024000.serial: IN -> D – LOW at stop tx
    [ 1645.227153] omap_uart 48024000.serial: D -> OUT – HIGH at start tx
    [ 1645.246130] omap_uart 48024000.serial: IN -> D – LOW at stop tx
    [ 1645.255053] omap_uart 48024000.serial: D -> OUT – HIGH at start tx
    [ 1645.272989] omap_uart 48024000.serial: IN -> D – LOW at stop tx
    [ 1645.281686] omap_uart 48024000.serial: D -> OUT – HIGH at start tx
    [ 1645.302757] omap_uart 48024000.serial: IN -> D – LOW at stop tx
    [ 1645.311655] omap_uart 48024000.serial: D -> OUT – HIGH at start tx
    [ 1645.329590] omap_uart 48024000.serial: IN -> D – LOW at stop tx
    [ 1645.338244] omap_uart 48024000.serial: D -> OUT – HIGH at start tx
    [ 1645.356183] omap_uart 48024000.serial: IN -> D – LOW at stop tx

    The time between switching the pins is high because of the print outputs.

    I start to trace the problem. Did you have such during you testing? Perhaps it’s only a configuration error on my site?

    Répondre

  4. Thomas, indeed you should trace instead of printk()ing in this situation. Have a look at LTTng: it’s efficient and has nice viewers.

    What is your baud rate?

    Perhaps in this situation, the best would be to trace the wires with a tool like Salaea Logic. Are you sure the GPIO pin used for RS-485 data direction is properly configured in the DTS like I did?

    Répondre

  5. Hi Philippe,
    Thank you for your suggestions. I have found the culpit, after tracing a little bit. I did not disable echo with stty, as I even did not know it’s enabled by default. Currently everything works fine.

    I will retest the auto rts feature of the AM3359 cpu. The rts pin was low on sending and receiving in my earlier tests. Maybe it’s the same problem. Did you tried the auto rts port?

    Thomas

    Répondre

  6. I guess you’re talking about the OMAP UART Auto-RTS feature; I did not investigate this. However, I don’t think this is going to help for RS-485, where you typically want to control the data direction pin according to the transmit state, whereas the Auto-RTS feature is:

    RTS goes high (inactive) when the receiver FIFO HALT trigger level, TCR[3:0], is reached and goes low (active) when the receiver FIFO RESTORE transmission trigger level is reached.

    I don’t even know if the Linux OMAP serial driver supports enabling Auto-RTS.

    Répondre

  7. The kernel support is not necessary. The cpu makes everything while the pin is configured to uart_rtsn. But while sending (cat /dev/zero >/dev/ttyO2) and reading (cat /dev/ttyO2) the line goes to low. I just jumbled the rs232 stuff. A hardware support should be possible on uart0, as there exists the modem features.

    Répondre

  8. hi sir,from past few weeks i’ve been trying to pass 2 frames of 68 bytes each between the uart1 and uart2 of the beagle bone the 1st frame of 68 bytes is received completely… but the 2nd frame 68th byte is either being lost or the 68th byte of 1st frame is being copied to it…help me out please

    Répondre

  9. Hi Philippe,
    great post, thanks.
    What linux-distribution do you use?

    Répondre

  10. I cant compile the dts file, im getting the following
    error when I run:
    dtc -O dtb -o rs485test-00A0.dtbo -b 0 -@ rs485test-00A0.dts

    Error: rs485test.dts:190.30-31 syntax error
    FATAL ERROR: Unable to parse input tree

    DTS content:

    bb_uart1_pins: pinmux_bb_uart1_pins {
    pinctrl-single,pins =
    ;
    };

    fragment@1 {
    target = ;
    __overlay__ {
    pinctrl-names = « default »;
    pinctrl-0 = ;
    rs485-rts-delay = ;
    rts-gpio = ;
    linux,rs485-enabled-at-boot-time;
    status = « okay »;
    };
    };

    Répondre

  11. The code again:

    I cant compile the dts file, im getting the following
    error when I run:
    dtc -O dtb -o rs485test-00A0.dtbo -b 0 -@ rs485test-00A0.dts

    Error: rs485test.dts:190.30-31 syntax error
    FATAL ERROR: Unable to parse input tree

    DTS content:

    bb_uart1_pins: pinmux_bb_uart1_pins {
    pinctrl-single,pins =
    ;
    };
    ...
    ...
    fragment@1 {
    target = ;
    __overlay__ {
    pinctrl-names = "default";
    pinctrl-0 = ;
    rs485-rts-delay = ;
    rts-gpio = ;
    linux,rs485-enabled-at-boot-time;
    status = "okay";
    };
    };

    Répondre

  12. Hello Philippe,

    I’ve read through your blog and found it extremely useful – thank you for sharing all this information. I do have couple of questions regarding the setup:

    1. I have not yet transferred to the new device tree kernel, still running 3.2 (TI SDK 06.00.00.00) so in that case, how do I setup RS485 for a particular uart?

    2. I have a system with specific hardware connected to the AM3352 via a RS485 interface. I was hoping to write a driver for that hardware, such that the user space application only talks to my driver for data. The driver on the other hand will communicate with the hardware using RS485 kernel driver. [If that makes sense]. I’ve seen this approach for eg: on SHT21 driver where the SHT21 uses I2C to communicate with the SHT21 chip. In this case, how can my custom driver setup the RS485 interface and send and receive data from it.

    Thanks once again for the post and hopefully your reply.
    Regards

    Santhosh

    Répondre

Leave a Reply

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>