9

I have an Arduino Uno attached over USB, using the cdc_acm driver. It is available at /dev/ttyACM0.

The convention for the Arduino's serial interface is for the DTR signal to be used for a reset signal—when using the integrated serial-to-USB adapter, the DTR/RTS/DSR/CTS signal; or, when using an RS-232 cable, pins 4 or 5 (and possibly 6 or 8) are wired to the RESET pin.

This reset avenue has the important advantage of being, if not truly out-of-band, at least very near-failsafe (due to being implemented via the always-out-of-band serial controller in conjunction with the not-normally-user-controllable watchdog circuit), and while it can be physically disabled (via wiring either a capacitor or a resistor, depending on the model, to the RESET pin), to do so completely ruins this important killswitch and all associated utility.

Unfortunately, it seems that, currently, Linux absolutely always sends this signal when any program attaches to an ACM device for any reason, and (unlike Windows,) provides no even-vaguely-known-reliable way to prevent this.

(Currently both -hupcl, "send a hangup signal when the last process closes the tty" and -clocal, "disable modem control signals" do not prevent this signal from being sent every time the device is opened.)


tl;dr: What do I need to do to access /dev/ttyACM0 without sending it a DTR/RTS/DSR/CTS signal (short of blocking the signal on the hardware level)?

JamesTheAwesomeDude
  • 825
  • 3
  • 14
  • 28
  • and I guess this question applies to ALL serial drivers, not just `cdc_acm`. But first things first, and the once-in-a-blue-moon that onboard RS-232 chips are used, even then probably nobody cares about being unable to control this. But with Arduinos, there's a clear and pressing concern. (In BOTH CASES, though, it's highly concerning that Windows has us so handily beat; I'd have expected this situation to be the other way around) – JamesTheAwesomeDude Sep 25 '18 at 22:12
  • I hope that I'm mistaken, but you'll probably have to modify the driver; I had to do the same with with the pl2303 driver in order to prevent it from pulling dtr/rts high on reset so I could use those (otherwise useless) signals separately from the serial port via the `TIOCMSET` ioctl. –  Sep 25 '18 at 22:37
  • 1
    If you can recompile the `cdc-acm.ko` kernel module, you can try commenting out this [line](https://github.com/torvalds/linux/blob/7876320f88802b22d4e2daf7eb027dd14175a0f8/drivers/usb/class/cdc-acm.c#L1076) from `drivers/usb/class/cdc-acm.c`. –  Sep 25 '18 at 23:50
  • Removing that line would entirely break _control over_ that feature, right? (Which would make it so you **couldn't program** an Arduino, for instance) – JamesTheAwesomeDude Sep 28 '18 at 19:18
  • No. Have you read my comment? I'm setting dtr/rts fine with `ioctl(TIOCMSET)`. Removing it will only prevent the kernel from raising dtr/rts on opening the tty and lowering them on closing it. –  Sep 28 '18 at 19:29
  • so, have you tried my hack? it won't burn your house down ;-) in the worst case, it will simply not work. if you have trouble building the kernel module, please tell what distribution are you using. –  Oct 19 '18 at 14:00
  • (It's been a while since I had a "hacker-friendly" setup...Gentoo was fun, but I would be in a bit of a pickle if I accidentally my current battlestation as-is. I don't have any spare HDDs or flash drives at the moment, and my optical drive is currently MIA.) Though all that not to say that "just recompile a driver" is a solution, since that's not sustainable (DKMS is only not flaky in my experience when it's integrated into package manager); merely an interesting step for the kernel developers on the road to fixing this bug – JamesTheAwesomeDude Oct 20 '18 at 18:34
  • 1
    @mosvy I did try your hack. It does indeed prevent DTR from being trigger on open/close. Triggering it manually is still possible too. – 1N4001 Jun 04 '19 at 23:43
  • 1
    @mosvy This is the only software solution I have found for Linux - can you post your hack as an answer? – Mtl Dev Sep 24 '19 at 19:58
  • 1
    @MtlDev I want to submit a patch for a sysctl which allow to disable this globally, but I did not come yet round to it; so in the meanwhile, I'll make the hack more visible. –  Sep 24 '19 at 20:31

2 Answers2

6

When a userland process is opening a serial device like /dev/ttyS0 or /dev/ttyACM0, linux will raise the DTR/RTS lines by default, and will drop them when closing it.

It does that by calling a dtr_rts callback defined by the driver.

Unfortunately, there isn't yet any sysctl or similar which allows to disable this annoying behavior (of very little use nowadays), so the only thing that works is to remove that callback from the driver's tty_port_operations structure, and recompile the driver module.

You can do that for the cdc-acm driver by commenting out this line:

--- drivers/usb/class/cdc-acm.c~
+++ drivers/usb/class/cdc-acm.c
@@ -1063,7 +1063,7 @@
 }

 static const struct tty_port_operations acm_port_ops = {
-       .dtr_rts = acm_port_dtr_rts,
+       /* .dtr_rts = acm_port_dtr_rts, */
        .shutdown = acm_port_shutdown,
        .activate = acm_port_activate,
        .destruct = acm_port_destruct,

This will not prevent you from using the DTR/RTS lines via serial ioctls like TIOCMSET, TIOCMBIC, TIOCMBIS, which will be handled by the acm_tty_tiocmset(), etc callbacks from the acm_ops structure, as usual.

Similar hacks could be used with other drivers; I personally have used this with the PL2303 usb -> serial driver.

[The diff is informative; it will not apply directly because this site mangles tabs and whitespaces]

  • so, to be clear: this is a straight-up *bug* in [the upstream mainline Linux driver code](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/drivers/usb/class/cdc-acm.c?h=v5.3.6#n1065), which will require compiling a custom kernel with patches to even test the solution to, and won't be functional OOTB until we've done all of: a) found a way to ensure this solution doesn't break anything, b) convinced the upstream maintainer it doesn't break anything, and then c) waited for the changes to get rolled into an official kernel release and picked up by the distros? – JamesTheAwesomeDude Oct 14 '19 at 18:09
  • __1.__ it's not a bug, it's very much intended behavior, leftover from an era where serial ports were mainly used with modems. __2.__ there's no need to compile a custom kernel -- just a custom module (and even that is not necessary -- you could just clear 8 or 4 bytes in the `cdc-acm.ko` binary, if you give the details about your system, I may be able to tell you where ;-)) __3.__ the changes needed to implement the sysctl I was talking about are trivial, there's no way they could break anything. Convincing the maintainer that this is useful may not be that simple, though. –  Oct 14 '19 at 22:10
  • 1
    **1.** the "bug" being that `stty -F $i -clocal && cat $i` still sends a _modem control signal_, despite such being ostensibly disabled; **2.** I'm currently limping along a dying Ubuntu 16.04 installation lol; I'll add this to the to-do list if I ever spin up a Gentoo though; **3.** I think the key will be selling this as a bugfix ("`-clocal` is BROKEN wrt `open(2)` due to the current state of the code") rather than as a feature request ("pander to my peripherals by changing current behavior that other code may be built on by now") – JamesTheAwesomeDude Oct 18 '19 at 19:07
  • I'm accepting this answer based on @1N4001's and @MtlDev's testimonies that it works. I'd _love_ it if you could edit in some exposition emphasizing the fact that requires **running a freaking custom kernel** (all distros tested contained `cdc_acm` as a builtin module), so it's not reliable/viable for most users not willing to make such deep, "warranty-breaking" modifications to their OS that may or may not require intervention to preserve thru future OS+kernel updates; but, it's apparently the best "solution" that exists currently. Thanks for your investigation and testing. – JamesTheAwesomeDude May 06 '20 at 18:06
  • Does anyone closer to The Internet Elders than I know how to get the ball rolling on getting this **bugfix** — see my previous comment @2019-08-18 — merged into upstream? While this patch _is nice_ for the Sufficiently Determined (who would have just either killed the RST pin by now or made their AVR code expect hard-resets on connection, if they had any actual/external engineering requirements to comply with,) I think it's telling that the answer has fewer _upvotes_ than the question has _stars_ — this code, **as a syadmin-level patch**, is just a workaround on par with other workarounds. – JamesTheAwesomeDude May 06 '20 at 18:13
  • @JamesTheAwesomeDude Hi, I just modified the cdc-acm driver for my Linux **distro** kernal (NOT the *Generic Linux Kernal*), it totally works, although the above given technique is the easiest, there could be other possible alternatives for changing the code to achieve this. If someone needs detailed instruction on how to do this on ubuntu distro please leave a comment here mentioning me. – Ubdus Samad Oct 02 '20 at 23:59
  • As for submitting it as a patch to the Kernal, it'll be very difficult, plus even if by some miracle we could come up with a way to fix this without breaking anything ( as we'll likely have to modify the termios struct to accomadate this feature), and we manage to persuade the elders to merge that patch. It'll still take years before it'll be rolled intro a mainstream distro. – Ubdus Samad Oct 03 '20 at 00:01
0

I think there is a nice workaround that solves the problem. Instead of reading data from the device /dev/ttyUSB0 or /dev/ttyACM0, it could be read from the named pipe, e.g. /tmp/arduino and the simple program (below) would be copying data from the device to pipe and holding the device open (thus avoiding setting DTR high). This also avoids dealing with all the difficulties of reading from the device. With the named pipe, tools like cat, less -f can be used or just any program with standard open + read without any need to issue ioctl commands to control tty. The program for copying the device to pipe would be running e.g. as upstart service and copying data from the device to the pipe (and perhaps producing some logs). The program has to take care of the SIGPIPE signal, to avoid being closed on closing any pipe reader process. The load on server would be negligible because of intentionally setting blocking on the input via fcntl. I have tested it and it seems to be working well. I am acutually trying to solve the same problem with interfacing Arduino. The nice side effect is that restart of Arduino can be done simply by restarting the upstart service whenever needed as it sets DTR high.

#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <fcntl.h>

/* for simplicity, most error handling is ommited, make sure you add it before using in production code */
void ignore_signal(int sig)
{
        static struct sigaction _sigact;
        memset(&_sigact, 0, sizeof(_sigact));
        _sigact.sa_handler = SIG_IGN;
        sigaction(sig, &_sigact, NULL);
}

int main()
{
        ignore_signal(SIGPIPE);

        int flags;
        flags = fcntl(0, F_GETFL, 0) & ~O_NONBLOCK;
        fcntl(0, F_SETFL, flags);


        char c;
        int n;
        while(1)
        {
                n = read(0,&c,1);
                if(n!=1)
                {
                        sleep(1);
                }
                else
                {
                        write(1,&c,1); /* ignoring the case that return code = -1 and errno = EPIPE means that data from Arduino are lost whenever pipe is not read */
                        write(2,&c,1);
                }
        }
        return 0;
}

The shell script to start it would be (needs to be re-implemented as upstart service):

#!/bin/bash

DEV=/dev/ttyUSB0
PIPE=/tmp/arduino
LOG=/var/log/arduino.log

if test ! -p $PIPE
then
    rm -f $PIPE
    mkfifo $PIPE
fi

./my_dd <$DEV >$PIPE 2>>$LOG &
dd if=$PIPE of=/dev/null count=0 bs=1

I think logrotate file needs to use copytruncate, because the file is still open (I had no chance to test this):

/var/log/arduino.log {
  rotate 5
  daily
  compress
  missingok
  notifempty
  create 640 root root
  copytruncate
}

Additional note : Meanwhile I have realized that to avoid the broken pipe, it could probably be achieved with use of cat or dd in combination with trap command with parameter PIPE, that would be filtering the SIGPIPE signal too. The solution above works for me, so I was not experimenting with the trap command to achieve comparable results.

ludvik02
  • 86
  • 5