Fixing the Adafruit Ultimate GPS with USB-C Linux Driver
2023 Sep 30 - Brian Kloppenborg
In my previous post on
the Adafruit Ultimate GPS with USB-C, I explored an issue related to the
PPS signal being connected to the Ring Indicator (RI) pin, but undetectable by
gpsd. I’m happy to report, that I’ve figured out the problem
and implemented a solution to the problem.
I’ve submitted the relevant changes to the Linux kernel, but if you need a working driver earlier, visit https://github.com/bkloppenborg/cp210x.
What was the problem?
The Adafruit Ultimate GPS uses a Silicon Labs CP2012N USB to serial (UART) adapter to convert the RS-232 signals from the GPS module to USB. Unfortunately, neither the driver from the manufacturer nor the driver in the Linux kernel implemented the relevant functions to detect changes in the modem status lines.
What specifically was missing?
According to the
the CP2102N can, in fact, read signals from the modem status lines; however,
this feature is not enabled by default. Instead, it must be explicitly activated
by the driver or interfacing application.
After much digging, I found that these signals are provided by
as described in
AN571: CP210x Virtual COM Port Interface.
Once enabled, these events are presented as two to four characters that are
interleaved with the rest of the data stream. Each event begins with a
user-selected escape character and is followed by pre-defined values
(see Section 5.28 of AN571 for further information). Unfortunately, this way
of presenting the data means that every character coming from the device must
be checked. Once a modem status change is detected, it needs to signal the kernel using
mechanisms to be usable by either
Having known nothing about how the Linux kernel handles USB or serial devices, this was a bit complicated to figure out. I ended up spending quite a lot of time reading through source code from the GPSD, Linux Kernel, and Fortian Inc repositories. I paid particular attention to these files:
- Fortian’s cp210x.c
Normally a PPS signal takes this path:
- PPS signal from the GPS receiver is sent to the DCD pin.
- The USB to serial adapter detects this change and sends it to the host, when polled, in the form of a USB Request Block (URB).
- Most USB device drivers I’ve inspected override the default
usb_serial_generic_process_read_urbfunction by replacing the function pointed to by
usb_serial_driver.process_read_urb. This new function processes the packets and looks for state changes on the DCD pin. When a change is detected, the driver calls
- The primary job of
usb_serial_handle_dcd_changeis to get the TTY line discipline for the port and call the
tty_ldisc_ops.dcd_changefunction which registers the PPS event with the kernel.
Interestingly, the ONLY file in the Linux kernel source that populates
$ cd linux $ grep -r "\.dcd_change" * drivers/pps/clients/pps-ldisc.c: pps_ldisc_ops.dcd_change = pps_tty_dcd_change;
This means that you can safely call
usb_serial_handle_dcd_change for any PPS
signal, even if it doesn’t appear on the DCD pin.
What changes were required to the stock cp210x driver?
In the end, the stock cp210x driver had most of what was needed. The only modifications were as follows:
- Ensure that the
EMBED_EVENTSare always turned on. This required commenting out some logic in
cp210x_set_termiosthat only enabled events when parity checking was requested.
cp210x_process_charto process the modem status events.
- Add a call to
usb_serial_handle_dcd_changefor the RI event.
One quick note about
While testing my driver modifications, I found an interesting difference
gpsmon /dev/ttyUSB0 and
sudo gpsmon /dev/ttyUSB0. The former attempts to
get time using only
gpsmon --debug 10 --logfile gpsmon_normal.log /dev/ttyUSB0
and searching the log
grep "PPS" gpsmon_normal.log gpsmon:PROG: KPPS:/dev/ttyUSB0 checking /sys/devices/virtual/pps/pps0/path, /dev/ttyUSB0 gpsmon:INFO: KPPS:/dev/ttyUSB0 running as 1000/1000, cannot open /dev/pps0: Permission denied(13) gpsmon:WARN: KPPS:/dev/ttyUSB0 kernel PPS unavailable, PPS accuracy will suffer gpsmon:PROG: PPS:/dev/ttyUSB0 thread launched gpsmon:PROG: TPPS:/dev/ttyUSB0 ioctl(TIOCMIWAIT) succeeded, time: 1696189330.000203832, state: 0 gpsmon:PROG: TPPS:/dev/ttyUSB0 Clear, cycle: 1696189330000203, duration: 1696189330000203 @ 1696189330.000203832 gpsmon:PROG: PPS:/dev/ttyUSB0 Clear cycle: 1696189330000203, duration: 1696189330000203 @ 1696189330.000203832
whereas launching it will
sudo will cause
gpsmon to use
KPPS + PPS_CANWAIT or
KPPS + TIOCMIWAIT.
sudo gpsmon --debug 10 --logfile gpsmon_sudo.log /dev/ttyUSB0
grep "PPS" gpsmon_sudo.log gpsmon:PROG: KPPS:/dev/ttyUSB0 checking /sys/devices/virtual/pps/pps0/path, /dev/ttyUSB0 gpsmon:INFO: KPPS:/dev/ttyUSB0 RFC2783 path:/dev/pps0, fd 5 gpsmon:INFO: KPPS:/dev/ttyUSB0 pps_caps 0x1133 gpsmon:INFO: KPPS:/dev/ttyUSB0 have PPS_CANWAIT gpsmon:INFO: KPPS:/dev/ttyUSB0 kernel PPS will be used gpsmon:PROG: PPS:/dev/ttyUSB0 thread launched gpsmon:INFO: KPPS:/dev/ttyUSB0 kernel PPS timeout Connection timed out(110)
Because the Adafruit module puts PPS on the RI line and my original
implementation of the driver only accounted for
sudo gpsmon and the
systemd instance of
gpsmon couldn’t read the PPS
signal! The solution was quite simple: add a call to
There is more discussion on this topic on the
Wrapping it up
As a person who has never dove this deep into the Linux kernel device drivers, this took a lot longer to figure out than I anticipated. Nevertheless, I got it working!
As stated above, the patches have been submitted to the Kernel, so it should eventually make it into your distribution. If you need support earlier, you can get a copy of the driver along with build and installation instructions here: https://github.com/bkloppenborg/cp210x.