A note on the Adafruit Ultimate GPS with USB-C and PPS

2023 Sep 09 - Brian Kloppenborg

While exploring methods to receive PPS signal over USB, I purchased the Adafruit Ultimate GPS with USB-C thinking that it offered everything I needed to receive the PPS signal over USB and route it to GPSD. Unfortunately, it doesn’t work as expected due to some not-so-easily discovered technical limitations. While researching things for this blog post, I also uncovered some information that provides some hope that this device may eventually work as expected.

Note: I’ve also linked to this post from the Adafruit forums hoping that someone there might pick up where I left off or perhaps that Adafruit will revise what is otherwise a very nice product.

What does the hardware provide?

Adafruit Ultimate GPS USB-C Edition

The Adafruit Ultimate GPS with USB-C (2023 April 26 edition) has the following capabilities:

As advertised, the PPS signal is indeed connected to the Ring Indiactor pin. This can be verified using the Python provided on the Adafruit Ultimate GPS FAQ:

brian@epislon:~$ sudo python3 pps-test.py

---------- Pulse low ----------

---------- Pulse high ----------

So what’s the issue?

On my Ubuntu 22.04 system with the 5.15.0-83-generic kernel there are two definitive issues and one speculative issue:

  1. Applications that attempt to set a TIOCMIWAIT IOCTL on any of the handshake lines (e.g. Data Set Ready (DSR), Data Carrier Detect (DCD), Ring Indicator (RI), or Clear to Send (CTS)) will error out as follows:
sudo ppscheck /dev/ttyUSB0 
# Seconds  nanoSecs   Signals
PPS ioctl(TIOCMIWAIT) failed: 25 Inappropriate ioctl for device

This is because the underlying cp210x driver in the Linux kernel does not implement a usb_serial_driver.tiocmiwait function.

  1. The driver does not have a method to poll the handshake lines that registers an IOCTL event. In other drivers, like ftdi_sio, this is provided by a usb_serial_driver.process_read_urb function.

  2. There are claims (discussed below) that the hardware itself does not support the relevant interfaces; however, I find that somewhat unlikely because the datasheet claims that all relevant modem handshake signals are supported.

Prior works

I’m not the first person to spot this issue. While doing some research for this blog post, I found a thread started by Adafruit Forum user bjmcculloch who noted an identical issue with the previous version of the GPS module that used a CP2104 chip. Likewise, Silicon Labs forum user D444540130 wrote that events on the DCD pin are delayed on the cp210x family of converters until the GPS receiver sends additional data. There are also two patches to the cp210x Linux driver that provide some additional information about the problem. In the notes for a patch that added support for line-status events it is claimed that the device’s event-insertion mode doesn’t work as expected on the CP2102. Likewise, a subsequent patch that added support for TIOCGICOUNT claims that modem status events are buffered and can’t be used to implement TIOCMIWAIT commands.

What is the underlying problem?

A review of the usb_serial_driver structure

The Linux Kernel defines the usb_serial_driver structure in include/linux/usb/serial.h. This structure permits device drivers to provide specific functionality. The usb-serial.c provides several default implementations of various functions which can be used by driver developers as needed.

To figure out what functions might need to be implemented, I compared the ftdi_sio.c (which provides a PPS signal), with that of cp210x.c. This becomes important in later portions of this post.

Investigating the usb_serial_driver.tiocmiwait function

First, lets address the issue with TIOCMIWAIT IOCTL. The error emanating from ppscheck comes from either here or here depending on your upstream repository. In both cases, the code is attempting to set an IOCTL on the device and the error indicates that the function fails. This is because the driver does not implement the usb_serial_driver.tiocmiwait. In the case of the ftdi_sio.c driver, they simply point to the default usb_serial_generic_tiocmiwait function, so that is likely adequate in our situation. Updating cp210x.c to have this function pointer as follows:

static struct usb_serial_driver cp210x_device
    .tiocmiwait =		usb_serial_generic_tiocmiwait,

does indeed resolve the issue; however, a PPS signal never arrives.

We also need the usb_serial_driver.process_read_urb function

While skimming the ftdi_sio driver, I also noticed that their ftdi_process_packet has code that checks the status of the handshake lines. This code is called from one location, the ftdi_process_read_urb function which is pointed to by the usb_serial_driver.process_read_urb function pointer. Reading through the ftdi_sio.h file, it appear that the handshake lines are stored in registers in the device. Figuring this out for the CP2012N will require more knowledge of the device than what I’ve found in the corresponding data sheets. So I can’t add this functionality myself.

A newfound hope

After getting to this point, I thought it was a done deal; however, I found a custom cp210x driver by Fortian Inc. Said driver claims to provide a PPS signals from a Jackson Labs “Firefly” GPS Disciplined Oscillator over an unspecified cp210x device.

Looking at that code, they are able to detect and register a DCD change by checking the value of specific device registers (see here). Unfortunately, there aren’t any breadcrumbs there for me to follow to identify where they discovered the register values.

Picking up from here

In case anyone is interested in picking up this project, here are a few things that might be of help.

Where to start?

The custom cp210x driver from Fortian Inc. requires a number of modifications to get working under Linux 5.13. Instead, I would suggest that you start with the cp210x driver from Silicon Labs. At the time of this writing, there are only two functions whose return type changed from int to void in the kernel so it is trivial to get working. Also, it makes a standalone .ko that is easy to load into the kernel.

Using your own custom drivers

Like most kernel drivers, the cp210x driver is trivial to disable. All you need to do is add one line to the blacklist.conf file and reboot:

brian@epislon:~$ tail /etc/modprobe.d/blacklist.conf 

blacklist cp210x

Update the cp210x driver to comply with current interfaces:

Although I’d much rather start from the cp210x driver found in the Linux kernel, working with that code is a bit cumbersome unless you’ve done kernel development before. Therefore, I recommend starting with the Silicon Labs cp210x driver. At the time of this writing, the v4 driver (dated January 29, 2021 in the release notes) requires only one minor modification to get working. It also builds a standalone .ko that is trivial to load into the kernel.

The one change required is to change the return type of cp210x_port_remove from int to void as follows:

static void cp210x_port_remove(struct usb_serial_port *);


static void cp210x_port_remove(struct usb_serial_port *port)
	struct cp210x_port_private *port_priv;

	port_priv = usb_get_serial_port_data(port);

then you can build the driver and load it as follows:

cd Linux_3.x.x_4.x.x_VCP_Driver_Source
sudo modprobe usbserial
sudo sudo insmod ./cp210x.ko

Note that when you run ppscheck it will still fail because we haven’t fixed the usb_serial_driver.tiocmiwait issue noted above:

$ sudo ppscheck /dev/ttyUSB0 
# Seconds  nanoSecs   Signals
PPS ioctl(TIOCMIWAIT) failed: 25 Inappropriate ioctl for device

To fix that issue, simply modify the usb_serial_generic_tiocmiwait function to point to the generic usb_serial_generic_tiocmiwait function as follows:

static struct usb_serial_driver cp210x_device = {
	.tiocmiwait     = usb_serial_generic_tiocmiwait,

then you can reload the driver (I also unplug and re-plug the device):

sudo rmmod cp210x.ko
sudo insmod ./cp210x.ko

Now when you run ppscheck the code no longer errors out, but the PPS signal will never arrive due to the usb_serial_driver.process_read_urb noted above:

brian@epislon:~/workspace/Linux_3.x.x_4.x.x_VCP_Driver_Source$ sudo ppscheck /dev/ttyUSB0 
# Seconds  nanoSecs   Signals

I’ll continue to dig into this further, but I’m not terribly confident I will find a solution anytime soon.