Today I Learned how to minimise latency when sending data to a computer from an Arduino (or any other FTDI-based device.) I learned it specifically for Windows, Linux and OS X.
Well, actually I learned this a few weeks ago while developing the Hairless MIDI<->Serial Bridge. But the blog post had to wait until today.
- By default, serial latency with FTDI chips (including Arduino Duemilanove/Mega) on Windows & Linux can be quite high (>16ms) and unpredictable.
- In audio applications (like sending MIDI data), this can add enough latency to create audible artifacts.
- In Java-based applications that use it, librxtx introduces an additional 20ms of latency. I wasn’t using Java or librxtx, but you’ll want to read that if you are.
The good news is that you can reduce FTDI latency substantially with a simple tweak.
In this case, latency is the amount of time between when some data gets sent from one side (the Arduino), and received on the other side (the computer.)
In lots of cases latency doesn’t matter, or you accept higher latency in exchange for higher throughput. However, for real-time applications like MIDI controllers, you don’t want a noticeable delay between pressing a button and hearing the sound that it makes.
The consensus seems to be that for acceptable MIDI audio responses, you need to keep MIDI message latency under about 20ms.
FTDI Latency Timer
The problem stems from the Arduino’s “Serial to USB converter” chip, the FTDI FT232R. The FTDI can’t send a USB packet to the computer for every byte that comes from the Arduino’s microcontroller. Instead, it stores the serial data in an internal buffer and only sends a USB packet when the buffer is full, or after a period of time has elapsed. This period of time is determined by the FTDI Latency Timer, which is the reason why FTDI chips can give bad latency characteristics.
On Linux & Windows, the default latency timer setting is 16ms. For example, say you send a 3 byte MIDI message from your Arduino at 115200bps. As serial data, it takes 0.3ms for the MIDI message to go from the Arduino’s microcontroller to the FTDI chip. However, the FTDI holds the message in its buffer for a further 15.8ms (16ms after the first byte arrived), before the latency timer expires and it sends a USB packet to the computer.
Thankfully, the latency timer can be tweaked. The tweaking method varies between operating systems.
In proper Linux style, the kernel’s FTDI driver exposes a nice sysfs interface that lets you get and set the latency timer. For example, if your serial port is ttyUSB0:
# cat /sys/bus/usb-serial/devices/ttyUSB0/latency_timer 16 # echo 1 > /sys/bus/usb-serial/devices/ttyUSB0/latency_timer # cat /sys/bus/usb-serial/devices/ttyUSB0/latency_timer 1
… that will lower the timer from 16ms to 1ms (the minimum), to reduce latency.
In my experience, the timer value won’t change immediately on an open serial port. If an application is using it then you’ll need to close and reopen it before the new value takes effect.
If you’re writing code, there is also a Linux-specific serial flag ASYNC_LOW_LATENCY that programmatically sets the latency timer down to 1ms. This is how Hairless MIDI<->Serial Bridge does it. You can see a succinct C code example in this patch I submitted to the ttyMIDI project.
In testing, I found that ASYNC_LOW_LATENCY also only works if you subsequently close the serial port and then reopen it (annoying, because setting the flag requires you have open()ed it already.)
FTDI’s own driver for Windows has a combo box in the Port Settings dialog that lets you choose the latency timer value. This Instructable has some screen shots showing how to find the setting in the Windows Device Manager control panel.
Programmatically, setting the timer is a bit hackier on Windows but not impossible. The FTDI driver saves the current latency setting for each device in the registry, so you can use Microsoft’s Registry API to write a new value, then reopen the serial port. The registry key is
SYSTEM\CurrentControlSet\Enum\FTDIBUS\-device id-\0000\Device Parameters
OS X does things differently. The driver bundle contains a file, /System/Library/Extensions/FTDIUSBSerialDriver.kext/Contents/Info.plist. This XML plist file describes different profiles for the serial port, including different LatencyTimer values, depending on how the FTDI identifies itself on the USB bus. FTDI’s own Technical Note on the subject [PDF] explains how to edit that value to change the latency.
The good news is that on OS X the latency timer defaults to 2ms for any FTDI FT232 that uses the default vendor & device USB IDs (0403:6001). This includes Arduino & clone FTDIs, so there is no real need to change anything.
I ran a test of the tweaked latency timers. The test sketch sends a MIDI note (3 bytes), then waits to see that same note echoed back. It does this many times and calculates the average delay.
With the latency timer set to 1-2ms, the entire round trip averages 18-19ms. This includes at least 1ms spent in the MIDI framework on the computer. So, based on those results, I estimate the one-way latency to be under 10ms. Excellent!
With a 16ms latency timer, the one-way latency would have been 25ms or more.
I don’t know how much of the 10ms latency is now coming from FTDI/USB layer, or from higher layers in the host operating system. It’s good enough for MIDI use, so I stopped investigating!
Arduinos with FTDI chips include the Arduino Duemilanove & Mega, and some clones like the Seeeduino.
The newer Arduino Uno & Mega 2560 have a different AtMegaU8 chip, programmed to behave as a USB/Serial converter. According to tests I’ve seen, these have good latency characteristics.