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
ppscheck
or 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
data sheet
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 EMBED_EVENTS
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 IOCTL
mechanisms to be usable by either ppscheck
or gpsd
.
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:
- usb/serial.h
- usb/serial/generic.c
- usb/serial/ftdi_sio.c
- include/linux/tty_ldisc.h
- drivers/tty/tty_ldisc.c
- drivers/pps/clients/pps-ldisc.c
- Fortian’s cp210x.c
- clients/ppscheck.c
- gpsmon/gpsmon.c
- gpsd/ppsthread.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_urb
function by replacing the function pointed to byusb_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 callsusb_serial_handle_dcd_change
found ingeneric.c
. - The primary job of
usb_serial_handle_dcd_change
is to get the TTY line discipline for the port and call thetty_ldisc_ops.dcd_change
function which registers the PPS event with the kernel.
Interestingly, the ONLY file in the Linux kernel source that populates
tty_ldisc_ops.dcd_change
is pps-ldisc.c
:
$ 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_EVENTS
are always turned on. This required commenting out some logic incp210x_set_termios
that only enabled events when parity checking was requested. - Modify
cp210x_process_char
to process the modem status events. - Add a call to
usb_serial_handle_dcd_change
for the RI event.
One quick note about gpsd
, ppscheck
, and gpsmon
While testing my driver modifications, I found an interesting difference
between gpsmon /dev/ttyUSB0
and sudo gpsmon /dev/ttyUSB0
. The former attempts to
get time using only TIOCMIWAIT
. Running
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 TIOCMIWAIT
, neither
sudo gpsmon
and the systemd
instance of gpsmon
couldn’t read the PPS
signal! The solution was quite simple: add a call to usb_serial_handle_dcd_change
.
There is more discussion on this topic on the
gpsd GitLab.
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.