A USB-to-UART bridge for the Digispark Pro (ATtiny167): plug it into a Linux
computer and it appears as a serial port (/dev/ttyACM0) connected to the
board's hardware UART. The UART follows the bit rate you set on the port, and
from 9600 up to 57600 bps -- the fastest standard rate -- it was tested to
lose nothing, in both directions at once. 76800 bps works too, and is the
real ceiling, but it is a nonstandard rate that most programs cannot ask for.
It uses the ATtiny167's LIN/UART peripheral in UART mode for the serial side and DigiCDCFast for the USB side.
- Bit rate from the host:
stty -F /dev/ttyACM0 38400, or the rate in any terminal program, sets the UART's rate. The LIN/UART can use 8 to 63 samples per bit, and the bridge picks the combination with the smallest error: 0.44% at 57600 bps, where the core'sSerial(always 16 samples per bit) is 3.5% off. - Lossless up to 57600 bps in both directions at once, at the full 8000 bytes/s low-speed USB allows (details below), and up to 76800 bps if you can get your programs to ask for a nonstandard rate: that is the last one whose line rate fits in the 8000 bytes/s. Faster rates can still be set and used -- two terminals at 115200 bps talk to each other perfectly well -- as long as nothing sends more than 8000 bytes/s for long.
- Hardware flow control both ways, if you want it: an RTS output on PA2, so a device that obeys CTS is stopped before the bridge runs out of room, and a CTS input on PA3, so a device that needs a moment can stop the bridge. Both are off until switched on in the sketch, each on its own. With RTS, 115200 bps carries a file losslessly one direction at a time, at the 8000 bytes/s USB allows.
- Reflashing without replugging: setting the port to 134 bps
(
stty -F /dev/ttyACM0 134) jumps to the micronucleus bootloader. - 8N1 only; flow control is the RTS output on PA2 and the CTS input on PA3, both off until switched on.
| Digispark Pro | Other device |
|---|---|
| PA0 (LIN/UART RX) | TX |
| PA1 (LIN/UART TX) | RX |
| PA2 (RTS out) | CTS (optional, and RTS_OUTPUT must be set) |
| PA3 (CTS in) | RTS (optional, and CTS_INPUT must be set) |
| GND | GND |
The Pro's I/O is at 5 V. Use a level shifter for 3.3 V devices, and an RS-232 transceiver (such as a MAX232) for RS-232 ports.
-
Linux host. Windows refuses low-speed USB serial devices; see DigiCDCFast's README.
-
Digistump AVR core 1.7.5, from the board manager URL
https://raw.githubusercontent.com/ArminJo/DigistumpArduino/master/package_digistump_index.json. -
A one-line fix to that core. Its
millis()timer interrupt on the Pro is blocking,SIGNAL(TIMER0_OVF_vect)incores/pro/wiring.c. Timer0 overflows at 976.56 Hz, beating against the host's 1000 Hz USB frames, so every 42.7 ms the handler delays V-USB just as the host polls, and data sent to the host is lost. Change the line to:ISR(TIMER0_OVF_vect, ISR_NOBLOCK)
The file is in
~/.arduino15/packages/digistump/hardware/avr/1.7.5/cores/pro/on Linux. (The ATtiny85 core already does this.) -
DigiCDCFast 1.0.0 or later, from the Arduino Library Manager.
With the Arduino IDE: open ProBridge/ProBridge.ino, select the board
Digispark Pro (16 MHz) and upload; plug the board in when asked.
With arduino-cli:
arduino-cli compile --fqbn digistump:avr:digispark-pro --output-dir build ProBridge
stty -F /dev/ttyACM0 134 # only if the bridge is already running
~/.arduino15/packages/digistump/tools/micronucleus/2.6/micronucleus --run build/ProBridge.ino.hexAgainst two USB-serial adapters wired to the UART, a Linux PC with an xHCI
host controller, each direction measured separately and both at once
(bridge_test.py):
| Bit rate | Bytes per test | Adapter | One direction | Both directions at once |
|---|---|---|---|---|
| 19200 | 50000 | FT232R | intact | intact |
| 19200 | 50000 | CH340 | intact | intact |
| 38400 | 100000 | FT232R | intact | intact |
| 38400 | 100000 | CH340 | intact | intact |
| 57600 | 100000 | FT232R | intact | intact (3 runs) |
| 57600 | 200000 | FT232R | intact | intact |
| 76800 | 100000 | FT232R | intact | intact |
Nothing is lost at any of these rates in either direction, with the full 8-byte USB packets; see Collecting the byte in time for how. Earlier versions lost 0.33% of the bytes travelling to the host at 57600 bps when both directions ran at once, and could not do 76800 bps at all.
Sending runs a little under the line rate, in both directions at once or alone:
| Bit rate | Line rate | The bridge sends at |
|---|---|---|
| 9600 | 960 | 867 bytes/s |
| 19200 | 1921 | 1714 bytes/s |
| 38400 | 3841 | 3332 bytes/s |
| 57600 | 5757 | 4998 bytes/s |
| 76800 | 7680 | 6494 bytes/s |
Nothing is lost by it, it only takes longer, and it is not the bridge running out of data to send: the counters say the transmitter found its buffer empty once per transfer, at the end. Each byte takes about one bit time more than its ten, which is the LIN/UART's own spacing between bytes, plus some 10 µs of waiting to be handed the next one.
PPP between two hosts over the bridge (pppd at both ends, MTU/MRU 296,
novj), with a 410 kB file going each way over TCP at the same time: at
57600 bps, 858 kB and 899 kB carried, no frame errors at either end, and
two ping floods thrown in at the end for good measure. Ping flooding drops
packets, as any link does when its queues fill, but without errors. At
38400 bps the same test is likewise clean. Before the bridge collected bytes
from inside the USB interrupt, 57600 bps lost 83 frames per 103 kB on the way
to the host, which TCP retransmitted.
The LIN/UART holds one received byte besides the one arriving, so a byte must be collected within one byte time: 174 µs at 57600 bps. V-USB handles a USB transaction with interrupts off, and when the host runs transactions back to back it handles the whole run without ever letting go, which reaches 200 µs. No handler of ours can get in: the processor serves the pending USB interrupt before any interrupt with a higher vector number, so returning between transactions would not help either (it was tried, and the latency distribution did not change at all).
So the byte is collected from inside the USB interrupt. DigiCDCFast calls
usbTransactionEnd() at the end of every transaction, and the bridge's is
twenty instructions of assembler that read LINDAT into the receive ring,
using only the registers the driver has already saved. The ring's head and
tail live in two general-purpose I/O registers, which one instruction
reaches. The handler still collects most bytes -- the hook only catches the
ones that would not have waited.
The same hook hands the transmitter its next byte, for the same reason in reverse: it holds one byte, and nobody refills it while the USB interrupt has the processor. That only shows above 38400 bps, where a byte takes less time than a run of transactions: it is worth 11% at 38400 bps and 8% at 76800, and nothing at all at 57600.
It is called on every transaction and not only when the next packet is already arriving. Calling it only in that case seems the thriftier choice and is far worse than no hook at all: the bridge's own handler then stays pending through the whole run and fires the moment the driver returns, which is exactly when the next packet arrives, and an ordinary handler's prologue is long enough to make the driver miss it.
The bridge has 64 bytes to hold what it receives until USB takes it, and USB takes at most 8000 bytes/s. Anything sent faster for long fills that, and the bytes arriving next are lost: flooding it at 115200 bps with its USB port closed lost 118649 of 118784 bytes.
PA2 prevents that, with RTS_OUTPUT set to 1 and the other device's CTS
wired to it. The bridge holds it low while it can take data and raises it once 44 bytes are waiting, until the
buffer is down to 16 again. In the same test, wired to an FT232R's CTS with
crtscts set on that side, nothing was lost: the adapter paused after its
own 4096 bytes were gone, and the bridge's r counter stayed 0.
That makes rates above 76800 bps usable, within the 8000 bytes/s:
| At 115200 bps, 60 kB | Result |
|---|---|
| adapter to bridge | intact, 7935 bytes/s |
| bridge to adapter | intact, 7937 bytes/s |
| both at once | 932 bytes lost and 100 corrupted on the way to the host |
One direction at a time runs at the USB ceiling itself. Both at once is past what the board can keep up with -- and the losses are not the UART's, whose overrun and ring counters stay 0, but USB transactions going wrong while the bus is saturated in both directions and the UART interrupts 11520 times a second. 76800 bps is the fastest rate that works in both directions at once.
Not every adapter obeys CTS: a CH340 ignored the equivalent line on the
ATtiny85 bridge, because Linux's ch341 driver accepts crtscts without
implementing it. PA2 is left alone until RTS_OUTPUT is set to 1 at the top
of the sketch (26 bytes of flash).
A device that needs the bridge to pause can say so on PA3, once CTS_INPUT
is set to 1 at the top of the sketch (22 bytes of flash). While it is high
the bridge finishes the byte it is sending and stops; loop() starts the
transmitter again when it goes low. Driving an FT232R's RTS by hand, the
bridge sent 0 bytes while told to wait and then all 20000 of them, in
order and intact, once let go (cts_test.py).
Nothing needs to be told to the host, and there would be no way to tell it:
CDC's SERIAL_STATE notification carries carrier, ring, break, framing,
parity and overrun, and has no CTS bit at all. It is not needed, because USB
provides the same back-pressure by itself: the bridge stops feeding its
transmitter, its buffer fills, it stops taking bytes from USB, and
DigiCDCFast makes the host's transactions wait. In that same test the host
got 2560 bytes into the bridge and was then simply blocked until the bridge
was let go.
Both halves are off by default and switch on independently, since either may be wired without the other. Switching on RTS without the wire is harmless -- the bridge drives a pin nobody reads -- but CTS without the wire stops the bridge from transmitting: the pin is pulled up, an unconnected input reads "wait", and it never sends a byte, while still receiving normally. The link then looks half dead rather than broken. Turn each on only along with its wire.
USB_PACKET_SIZE, at the top of ProBridge.ino (or -DUSB_PACKET_SIZE=6),
sets how many bytes a USB packet carries, 1 to 8. Each transaction keeps
V-USB's interrupts off for roughly 6 µs per byte, so smaller packets
interrupt the sketch for less time, but they also carry less: one packet per
millisecond each way, so 8 bytes gives 8000 bytes/s and 6 gives 6000, against
the 5760 that 57600 bps needs. Leave it at 8 unless something else on the
board needs the processor sooner: nothing is lost at 8 at any rate the bridge
supports.
With the defaults the sketch uses 4506 of the 14970 bytes of flash and 391 of the 512 bytes of RAM; with both flow control lines, 4554; without the counters, 4182 and 381.
- Sending runs at about 87% of the line rate (see the table under Results). Nothing is lost by it; each byte just takes about one bit time longer than its ten.
- 8000 bytes/s is the most low-speed USB carries per direction, and that, rather than any bit rate, is the real ceiling. 76800 bps is the fastest rate whose line rate fits under it, and it was tested to lose nothing both ways. Above that the bit rate is still yours to set and use -- 115200 bps carries a file losslessly one direction at a time -- but only if the sender can be stopped, which is what the RTS line on PA2 is for (see Flow control). A sender that ignores it overruns the bridge's buffer.
- 76800 bps needs a nonstandard rate. Linux has no
B76800constant, sosttyand Python'stermiosrefuse it;bridge_test.pysets it throughTCSETS2, and a program that wants it has to do the same. A received byte must also be collected within 130 µs there, less than a run of USB transactions takes -- which is what the hook is for. - Interrupt handlers on the board must never keep interrupts off for long, or V-USB misses USB transactions: the bridge's own LIN/UART handler masks itself and re-enables interrupts, for the same reason as the core fix. A missed packet is not the worst of it -- a packet caught slightly late is received corrupted, and silently, since at 16 MHz V-USB has no time to check a CRC either.
- Service the UART from
loop()and stop interrupting altogether. What still fails is 115200 bps in both directions at once, and the counters say it is not the UART: overruns and ring drops stay 0, so the losses are USB transactions going wrong. The suspect is the receive handler's prologue, which runs 11520 times a second at that rate and needs only to start just before a packet does to make the driver miss it -- the same mechanism that made callingusbTransactionEnd()conditionally worse than not calling it at all. With no handler there would be no prologue to collide with. It would not replace the hook: while the USB interrupt has the processorloop()does not run either, so the hook stays the only thing that can reach the UART during a transaction. And it would need every path throughloop()to be shorter than a byte time, 87 µs at 115200 bps, whichusbPoll()and the loop that hands 64 bytes to USB would have to be measured against. - Reporting the flow control lines to the host. There is nowhere to put
CTS: CDC's
SERIAL_STATEnotification carries carrier, ring, break, framing, parity and overrun, and no CTS bit. The bridge acts on the pin itself instead, which is what the host would have done with it anyway.
Setting the port to 110 bps prints a line of counters and clears them, so a test can ask the bridge what it thinks happened:
$ stty -F /dev/ttyACM0 110 && timeout 2 cat /dev/ttyACM0
S n0079c o0000 f0000 r0000
n |
bytes the handler took from the UART (the hook's are not counted, so that it costs no cycles) |
o |
bytes the UART lost before anyone came for them: receive overruns |
f |
framing errors |
r |
bytes dropped because the receive ring was full |
s |
times the transmitter stopped because there was nothing left to send |
All are 16-bit and wrap silently. bridge_test.py --stats prints them after
each transfer. Set STATS to 0 at the top of ProBridge.ino to leave the
counters out -- but measure afterwards rather than assuming it costs nothing:
on the ATtiny85 bridge, whose transmitter times itself around its own
handler, turning them off was enough to corrupt a byte in every 75 kB.
bridge_test.py [--bridge PORT] [--adapter PORT] [--bytes N] [--stats] BAUD...sends pseudo-random data through the bridge against a reference USB-serial adapter wired to the UART (TX to RX, RX to TX, GND to GND): each direction, then both at once. It reports throughput and bytes lost, extra or corrupted, with the offsets of the first mismatches, and with--statsthe bridge's own counters as well. Defaults:/dev/ttyACM0for the bridge,/dev/ttyUSB0for the adapter, 100000 bytes.cts_test.py [--baud N] [--bytes N]drives the adapter's RTS by hand -- rather than leaving it to the kernel's flow control, so that it tests the bridge and not the two drivers -- and checks that the bridge stops while told to wait and loses nothing when let go.rts_test.py [--baud N] [--seconds N] [--no-crtscts]floods the bridge's UART while its USB port stays closed, so nothing drains what it receives, and reports whether the adapter was stopped: it reads the bridge's RTS line back through the adapter's CTS input rather than taking it on trust, and then asks the bridge how many bytes it dropped.LinDuplex/withlin_duplex_test.py [BAUD] [SECONDS]checks the LIN/UART on its own, without USB carrying the data: the board transmits a counting sequence while checking one it receives. It showed the LIN/UART itself is fine in full duplex (57600 bps both ways, no errors).
GPL version 2 or version 3, at your choice; see LICENSE.
- DigiCDCFast, and through it V-USB by OBJECTIVE DEVELOPMENT Software GmbH.
- The Digistump AVR core, by Digistump LLC, as maintained by ArminJo.
- DigisparkProBridge by Marco Carnut.
See also DigisparkBridge, the experimental bridge for the original Digispark (ATtiny85).