A minimalistic (8N1 only, RTS flow control) USB-to-UART bridge for the original Digispark (ATtiny85) and limited to 9,600 bps "quasi-full duplex" (both directions at once, almost) by taking turns around USB traffic (see notes below); receiving alone works up to 19,200 bps. Which is not too shabby for a device that has no UART at all: it receives with the USI peripheral, oversampling in hardware, and transmits with bit edges timed by a timer's compare output, while DigiCDCFast runs bitbanged USB in software on the same 8-bit chip.
Is it useful? At 1200 to 4800 bps, yes: every test passes, in either
direction and both at once, with no lost or corrupted bytes. At 9600 bps it
works almost perfectly -- either direction alone is clean, and only with both
near saturation does it corrupt a few bytes per 100 kB on the way out. The
other price is throughput: the bridge and the host take turns on USB, so each
direction carries about 600 bytes/s instead of 860 transmitting and 960
receiving, and received bytes can wait up to 20 ms. That is
time sharing, which can be switched
off for full speed at the cost of more corruption (TIME_SHARING in the
sketch). That's why "quasi full duplex". It's pretty usable, but not perfect.
Above 9600 bps only receiving works.
For a 9600 serial GPS, for instance, those imperfections are inconsequential; so, yes, it is useful -- if anything, to give those aging Digisparks a better meaning to their lives than sitting unused in your drawer.
For a true serial port at any rate, a USB-serial chip costs less than a dollar, and the DigisparkProBridge has a hardware UART and is lossless to 57600 bps in both directions.
It is also a demonstration of what a fast USB serial library like DigiCDCFast makes possible on these boards, and of the techniques involved; the notes below explain them.
| Digispark | Other device |
|---|---|
| PB0 (P0) | TX |
| PB1 (P1) | RX |
| PB2 (P2) | CTS (optional, see Flow control) |
| PB5 (P5) | RTS (optional; read the warning under Flow control first) |
| GND | GND |
The Digispark's I/O is at 5 V; use a level shifter for 3.3 V devices. The LED on PB1 flickers with the data, which doesn't matter.
PB2 asks the other device to pause when the bridge's buffer fills; leave it unconnected if the device has no CTS input. There is no CTS input on the bridge's side: it has no pin left for one.
- 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. No changes are needed for the ATtiny85. - DigiCDCFast 1.2.0 or later, from the Arduino Library Manager (1.1.0 works too, except that unsupported bit rates are then accepted silently).
- A Digispark with the micronucleus bootloader. Build for its 16.5 MHz clock setting: the board definition defaults to 16 MHz, where USB doesn't work.
Plug it in, set the bit rate on the port and use it like any serial port:
stty -F /dev/ttyACM0 9600 raw -echoThe bit rates it supports are 1200, 2400, 4800 and 9600, and 19200
for receiving alone. Any other rate is refused: the bridge answers the
host's request with a STALL, as the CDC specification asks, and keeps the
rate it had. Linux doesn't pass that on, so stty still reports success and
the host then believes a rate the bridge isn't using; a program can check by
reading the settings back from the device. The format is always 8N1: the
data bits, parity and stop bits the host asks for are ignored, as are DTR
and RTS, and there are no CTS or break signals. Two bit rates are commands
instead:
- 134 bps, asked for three times within two seconds, jumps to the micronucleus bootloader, for reflashing without replugging.
- 110 bps prints diagnostic counters as hex and clears them, leaving the UART as it was (see Diagnostics).
With the Arduino IDE: open TinyBridge/TinyBridge.ino, select the board
Digispark and Tools → Clock → 16.5 MHz - For V-USB, and upload; plug the
board in when asked.
With arduino-cli:
arduino-cli compile --fqbn digistump:avr:digispark-tiny:clock=clock165 --output-dir build TinyBridge
stty -F /dev/ttyACM0 134 # only if a bridge is already running
~/.arduino15/packages/digistump/tools/micronucleus/2.6/micronucleus --run build/TinyBridge.ino.hexSettings at the top of TinyBridge.ino, and two in DigiCDCFast:
-
TIME_SHARING(1): hold data going to the host while transmitting, for reliable full duplex at 9600 bps (see Time sharing). 0 is faster and corrupts a byte now and then, and saves 284 bytes of flash. -
STATS(1): the diagnostic counters and the 110 bps command that prints them (see Diagnostics). They cost 510 bytes of flash and 28 of RAM, which this sketch can ill afford -- but turning them off is not free either. The transmitter's timing was tuned with them compiled in, and without them three runs of 50 kB in both directions corrupted two bytes, where the same code with them corrupted none. They stay on until the planner is retuned without them. WithWIDE_STATS(0) the byte counter is 32-bit, printed as two words, the low one first (nN): it wraps at 65535 otherwise, which a long run will do. -
CRC_STATS(0):candb, the bridge's own CRC and count of the bytes read from USB, to compare with what the host sent, at 94 bytes of flash and 4 of RAM. They answered one question -- whether USB itself was corrupting anything, which it was not -- so they are off. -
RTS_OUTPUT(0): PB2 as an RTS output (see Flow control), 20 bytes of flash. Harmless if you switch it on without wiring it: the bridge drives a pin nobody reads. -
CTS_INPUT(0): PB5 as a CTS input (see Flow control), 10 bytes of flash. Switch this on only with the wire, and only on a board whose reset pin has been given up: see the warnings under Flow control. With it on and nothing driving PB5, the pull-up reads "wait" and the bridge never sends a byte -- it still receives, so the link looks half dead rather than broken. -
CALL_HOOK(1): the largest single thing that reduces corruption here (see When the driver makes the edges), and it needs no changes to DigiCDCFast at all -- it fills inusbTransactionEnd(), the weak hook the library has called since 1.3.0. It does needUSB_PACKET_SIZE2, the sketch's default: the build fails otherwise, because the hook assumes no transaction outlasts a bit time. 138 bytes of flash.
With the defaults the sketch uses 6544 of the 6650 bytes available and 309 of the 512 bytes of RAM; with both flow control lines, 6574; without the hook, 6406; without the counters, 6034 and 281 -- and see what that costs, above.
V-USB handles every USB transaction with interrupts off, for up to ~200 µs,
so an interrupt that catches a start bit can be most of a bit late at
9600 bps. Instead, Timer0 clocks the USI shift register at three samples per
bit, so every sample is taken on time in hardware whatever the interrupts
are doing. USI's 4-bit counter overflows every 8 samples; the handler copies
the 8 samples and rewrites the counter so the next overflow comes exactly 8
samples after the last one, however late the handler ran. Its deadline is
8 samples, 2 2/3 bits (278 µs at 9600 bps, 139 µs at 19200). loop() finds
start bits in the sample stream and reads each bit near its middle.
One subtlety: rewriting the counter must not race with a sample shifting in. Samples shift at Timer0's compare match, but USI's counter changes slightly later than TCNT0 does, so the handler waits out the timer ticks on both sides of the match.
Timer1 keeps running freely for millis(). Each bit edge is a compare match
on OC1A (set or clear), so the hardware makes it on time; the handler only
schedules the next edge, and has one bit to do so (104 µs at 9600 bps).
USB transactions are longer than that. While data flows, the host makes one or two every millisecond, each starting at the same point of the frame. So each byte is planned: the handler's late runs show when the transactions start, and a byte starts a little later, or after an idle bit, when one would begin near an edge whose next edge changes the level. Bytes go out between the host's transactions, which is why a busy full-duplex link carries a bit less than the line rate. The handler keeps running on idle bits for a second after the last byte, so the next burst starts with current timing.
That covers the host's own traffic. The data the bridge sends to the host is traffic too, and it is up to the bridge when to send it: see Time sharing.
The sketch includes DigiCDCMedium.h instead of DigiCDCFast.h: 2-byte USB
packets instead of 8-byte ones. Each transaction then keeps interrupts off
for ~73 µs instead of ~110 µs, less than a bit at 9600 bps, which is what
makes the planning work reliably. It limits USB to about 2000 bytes/s each
way, plenty for 9600 bps.
At 9600 bps a transmitted bit lasts 104 us and a USB transaction keeps V-USB's interrupts off for ~73 us, so a transaction that starts at the wrong moment delays an edge enough to corrupt a byte. Planning bytes around the host's transactions (above) handles the host's own traffic, but the data the bridge sends to the host is extra traffic at times of the bridge's choosing. Transmitting alone is clean; adding traffic the other way is what corrupts bytes.
So, with TIME_SHARING (on by default), no data goes to the host while a
byte is being transmitted. Received bytes collect in a 32-byte buffer until
20 of them wait or the oldest is 20 ms old; then the transmitter finishes
its byte and holds the line idle while they go to the host, and resumes
when the host has taken them all. A hold ends after 100 ms regardless, in
case no program is reading the port. Below 9600 bps nothing is held.
The cost is throughput and a little latency: at 9600 bps with both
directions saturated, each carries about 600 bytes/s instead of 860, and a
received byte can wait up to 20 ms. One direction at a time is unaffected.
To trade that back for speed, set TIME_SHARING to 0 at the top of
TinyBridge.ino (284 bytes less flash), and the bridge behaves as it did
before: a few corrupted bytes per 10 kB transmitted while receiving, on some
hosts (see below).
Time sharing is on unless the table says otherwise. Against a CH340
USB-serial adapter wired to the bridge, on a PC (Intel
xHCI, Linux 6.8) with the board in a port of its own, the host sending as
fast as the bridge accepts (bridge_test.py). Bytes corrupted; none lost
unless noted; throughput per direction:
| Bit rate | Receive | Transmit | Both at once, time sharing on (default) | Both at once, time sharing off |
|---|---|---|---|---|
| 1200 (3 kB) | 0, 120 B/s | 0, 120 B/s | 0 / 0, 120 B/s | 0 / 0, 120 B/s |
| 2400 (3 kB) | 0, 240 B/s | 0, 240 B/s | 0 / 0, 240 B/s | 0 / 0, 240 B/s |
| 4800 (10 kB) | 0, 481 B/s | 0, 479 B/s | 0 / 0, 479 B/s | 0 / 0, 480 B/s |
| 9600 (20 kB) | 0, 961 B/s | 0, 859 B/s | 0 / 0, 601 B/s | receive 0; transmit 2, 858 B/s |
| 9600 (50 kB, 3 runs) | 0 / 0, 600 B/s | receive 0; transmit 8, 10, 14; 859 B/s |
Later, against an FT232R, three rounds of 50 kB in both directions at once
with the shipping defaults: nothing lost, nothing corrupted. The same rounds
with STATS at 0 corrupted two transmitted bytes, which is how we learned
that the counters are part of what the transmitter's timing was tuned
around.
Protocols do not send streams; they send frames with gaps, and the bridge
carries those less well. frame_test.py measures it: 170-byte frames 30 ms
apart, both directions at once. Over 80 runs of 20 s with a fresh boot before
each -- 748 kB transmitted, 1.6 MB received -- 2.5 bytes per 100 kB were
corrupted on the way out, and not one of the 1.6 MB coming in. Most runs are
perfectly clean; the bad ones come in episodes lasting minutes, during which a
run can corrupt hundreds of bytes, and no counter the bridge keeps says
anything is wrong. That is what a PPP link over this bridge runs into: TCP
retransmits the frames, so it works, but it is not free.
With the driver making the edges (see When the driver makes the edges) the same test gives 2.6 per 100 kB -- but the distribution is what changed. Over 20 runs with a reboot before each, fourteen were perfectly clean and the other six corrupted exactly one byte, where before a single run could corrupt eleven. Receiving stayed perfect: 0 in 492 kB.
Over real PPP links carrying two 400 kB file transfers at once, four runs totalling 3.4 MB out and about 4 MB back: 91 damaged frames, which is 2.7 corrupted bytes per 100 kB, and one single error in everything received. The four runs measured 1.3, 3.1, 4.3 and 4.4 per 100 kB, which is the spread to expect rather than noise around a fourth decimal. Before the hook the same test damaged about 34 frames per 100 kB.
Anyone measuring this should know that one run tells you nothing. The same
firmware gave 6.0 and 446.7 corrupted per 100 kB in two consecutive sets of
20 runs. Comparing two configurations means interleaving them run by run in
one binary (AB_TEST in the sketch does this for the hold thresholds): done
that way, holding at 8 bytes / 8 ms measured 4.3 per 100 kB against 0.8 for
the 20/20 the sketch ships with, over 40 runs each -- the opposite of what the
same comparison said when the two were measured one after the other. Part of
the reason is that the board boots into one of two regimes and stays there;
see Two regimes.
| 19200 (10 kB) | 0, 1921 B/s | 15, 1293 B/s | receive 846 lost, 1271 corrupted; transmit 35 | receive 465 lost, 489 corrupted; transmit 221 |
Below 9600 bps time sharing does nothing: the bits are long enough that USB traffic can't spoil them, and both directions run at the line rate. At 19200 bps transmitting is beyond reach either way, and in full duplex the receiver loses sample windows: it has 139 us to service them, less than the ~200 us V-USB can keep interrupts off when it handles a transaction in each direction back to back.
Other tests at 9600 bps, with time sharing on except where noted:
- Bursty traffic (
burst_test.py, bursts of up to 100 bytes with 50-300 ms pauses, both directions at once): clean, 40 kB each way (2 corrupted bytes in 48 kB with time sharing off). - Zmodem (lrzsz), a 62 kB file each way, time sharing off: both copies identical; 784-836 bytes/s through the bridge's transmitter, 911-930 bytes/s through its receiver.
- PPP (
pppdat both ends, MTU/MRU 296,novj,nocrtscts), a 62 kB file over TCP in each direction at once: no frame errors either way, 85 kB carried each way. The transfer into the host finishes well before the one out of it, since time sharing gives receiving priority. (Earlier runs showed 6-14 bad frames per 95 kB from the bridge, all of them bytes the planner gave up on; see When a byte fits nowhere.)
The planner is unchanged since the first release, but a bug in how the transmit handler returned is fixed (see Lessons), and with time sharing off the sketch is smaller (flash 6638 -> 6092 bytes, RAM 388 -> 281, with DigiCDCFast 1.1.0; 6442 and 302 with time sharing). Side by side with the first release on the same bench, three boots each, time sharing off: bit rate changes requested right after traffic were missed 5 times in 27 by the first release and never since; receiving and 4800 bps were clean for both; transmitting at 9600 bps in both directions corrupted 12 bytes in 120 kB with the first release and 18 since, and bursts 3 and 7 in 72 kB, which is within chance.
The bridge has 32 bytes to hold what it receives until USB takes it. If the
host stops reading its port, that fills, and the bytes that arrive next are
lost (counter r: 20 kB sent to a port nobody read lost 15956 of them).
PB2 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 22 bytes are waiting, until
the buffer is down to 8 again. Wired to an FT232R's CTS, with crtscts set
on that side, the same test lost nothing: the adapter paused, accepting
8704 bytes in 150 s while the host read nothing, and the bridge's r
stayed 0.
Not every adapter obeys CTS. A CH340 kept sending regardless (and dropped
16793 bytes), because Linux's ch341 driver accepts crtscts without
implementing it; the bridge's line was correct all the while, as the
adapter's own CTS pin showed. PB2 is left alone until RTS_OUTPUT is set to
1 in the sketch (20 bytes of flash), so that a board with nothing wired to it
behaves as it always did.
The other direction, a device asking the bridge to pause, is CTS_INPUT on
PB5 -- and it needs a board whose reset pin has been given up. PB0, PB1 and
PB2 are the UART and RTS, PB3 and PB4 are USB, and PB5 is the reset pin
unless the RSTDISBL fuse is programmed (AVR fuses read 0 when programmed,
so RSTDISBL = 0 is what you want).
Check the fuse before wiring anything to P5. On a board where RSTDISBL is not programmed, P5 is still the reset pin, and reset is active low -- which is the same level the other device's RTS uses to say "go ahead". Wire them together and the board sits in reset exactly when it is being told it may send. It will look dead, not slow. Some Digisparks are said to ship with the fuse programmed and many clones, "rev3" boards among them, not, so read it rather than assume it.
Programming it costs the reset pin for good: micronucleus becomes the only way in, and a high-voltage programmer (an ISP one cannot do it) the only way back if the bootloader is ever damaged. For a bridge that transmits at most 860 bytes/s into devices that usually have a UART buffer, it is rarely worth it.
With it wired and switched on, the bridge finishes the byte it is sending and
stops while PB5 is high; loop() starts the transmitter again when it goes
low. Told to wait by an FT232R's RTS driven by hand, the bridge sent 0 bytes,
and then all 200 of them in order and intact once let go (cts_test.py).
With CTS_INPUT on and nothing driving PB5, the pin's pull-up reads "wait"
and the bridge never transmits, while still receiving normally: switch it on
only together with the wire.
It leaves the sketch little room to spare, though. The ATtiny85 build
runs close to the edge of its RAM: the k counter, which reports the stack
bytes never touched, ranges from about 20 to 140 between runs with everything
else equal -- one reading says nothing -- and with CTS_INPUT on it has been
seen at 0, the stack reaching the first byte past the globals without yet
passing it. No test lost or corrupted anything at 9600 bps because of it.
The flags the sketch used to keep in bytes now live in the bits of GPIOR0,
which gave back 6 bytes of RAM and 62 of flash; STATS at 0 gives back
another 25 and 726, at the price of the counters that would tell you.
Nothing needs to be told to the host, and CDC has no way to tell it: the
SERIAL_STATE notification carries carrier, ring, break, framing, parity and
overrun, and no CTS bit. It is not needed either: when the bridge can't take
more, USB makes the host wait.
A byte whose level changes at many edges sometimes can't be placed safely
anywhere in the frame. The planner then sends idle bits and tries again, and
after IDLE_LIMIT of them (40, about 4 ms at 9600 bps) it gives up and
sends the byte where it is, corrupted now and then. The limit has to exist:
without it the bridge would stop sending, its buffers would fill, and
DigiCDCFast's flow control would then refuse even the host's line-coding
requests.
The limit was 10 idle bits until it turned out to be the main source of
errors under PPP: every corrupted byte in a long test was one of these
(counter z). At 40 the counter stays at 0 in the same tests, and
throughput is unchanged (601 bytes/s each way in both directions at once).
Raising it further only delays a stubborn byte more.
With time sharing off, how often transmitted bytes get corrupted depends on
where the host's USB controller places the bridge's transactions in each
frame, and that changes with the host and with a hub in between. The same
sketch and adapter at 9600 bps, both directions at once, 50 kB each, three
runs per setup (e is the bridge's count of edges made late, see
Diagnostics):
| Host | Connection | Corrupted bytes to the adapter | Edges made late |
|---|---|---|---|
| PC (Intel Meteor Lake xHCI, Linux 6.8) | directly | 8, 10, 14 | ~37,400 |
| PC (Intel Meteor Lake xHCI, Linux 6.8) | through a USB 3 hub (Genesys Logic, USB ID 05e3:0610) | 1, 0, 2 and 0, 0, 0 | ~7,000 |
| Raspberry Pi 5 (RP1, Linux 6.12) | directly | 5, 2, 4 | ~38,800 |
| Raspberry Pi 5 (RP1, Linux 6.12) | through the same hub | 7, 9, 9 | ~37,300 |
Behind a high-speed hub, the host controller schedules a low-speed device's
transactions through the hub's transaction translator. On the PC that left
the transmitter's handlers five times fewer late edges and made full duplex
practically clean (and slightly faster, 873 bytes/s); on the Raspberry Pi
the same hub changed nothing. It isn't the Digispark's clock: its frame
length (P) was the same within 0.1% in the best and worst of these
sessions. So if you turn time sharing off and full duplex matters, try the
bridge with and without a hub on your host. With time sharing on, both
setups were clean.
Things tried along the way:
- 8-byte USB packets (plain
DigiCDCFast.h): transmit 3-7 corrupted per 10-20 kB, both directions 12-19 per 10 kB. So it almost works at 9600 bps with normal packets; without planning, ~35% of transmitted bytes were corrupted. - 3-byte packets: as good as 2-byte ones at 9600 bps, and better at transmitting at 19200 bps (17-22 corrupted per 10 kB), which still isn't usable.
- Exact USB timing instead of the handler's late runs. A variant of DigiCDCFast recorded when V-USB's interrupt ended (an experiment, not published). It showed each frame's transactions at fixed places (in full duplex, an OUT transaction ending at +0 and +13 ticks, an IN one at +39 and +43), so the frame's timing can be known exactly. Planners built on it did no better than the estimate from late runs: seeding the estimate at the start of each burst corrupted 1-6 bytes per 48 kB of bursts where the estimate alone corrupted none; planning around one region per frame transmitted cleanly one way but corrupted 7-11 per 20 kB in both directions, against ~3; and learning a map of where handlers ran late made the handler too slow and too deep for the ATtiny85. Late runs measure what actually matters, whatever causes it, at almost no cost per edge.
A bit edge is a compare match on OC1A, so the hardware places it on time whatever the processor is doing. What the handler must do is load the next one, and it has one bit time to do it -- 104 µs at 9600 bps. V-USB keeps interrupts off for a whole transaction, and for a whole run of back-to-back transactions: up to about 200 µs. Planning each byte around the transactions (below) helps, but nothing the sketch does can help an edge that came due during a blackout, because the sketch is not running. Only the driver is.
So the driver makes those edges. DigiCDCFast calls usbTransactionEnd() at
the end of every transaction -- a weak, do-nothing function since 1.3.0 -- and
the sketch fills it in with about sixty instructions of assembler
(CALL_HOOK). Nothing in the library changes. At the end of every transaction
it shifts the sketch's frame along, puts the
next bit on the compare output and moves the compare one bit further --
repeatedly, for as long as the transactions keep coming, so a blackout of any
length up to the end of a byte is covered. It stops before the edge that ends
a byte, which wants planning, and counts what it did so the handler can catch
up.
The handler does not recompute where the driver got to: it reads OCR1A, the
driver's own latest edge, and adjusts the high byte for the single wrap that
is possible. Only one of them ever does the arithmetic, so the two clocks
cannot disagree. The fraction of a bit time is shared for the same reason,
in 256ths of a tick -- a tick is 64 CPU cycles, so the conversion is exact,
and the driver carries it with a single adc. An earlier version added whole
ticks only and drifted about a tick every four bits, after which the
handler's next edge read as displaced by all of it at once.
The same hook does the receiver the same favour first, because its deadline is
tighter: it takes USI's window of 8 samples into a stash and re-arms USISR.
Re-arming clears USIOIF, so the sketch's own overflow interrupt does not run
for that window at all -- which is most of what it is worth, since that
interrupt is also what delays the transmitter.
The rules for that function are DigiCDCFast's, and they are strict: naked
assembler, and only the registers the driver saved. See Borrowing the
interrupt in DigiCDCFast's README.
An earlier version of this work inlined the same instructions into the driver
instead, on the assumption that the seven cycles of rcall and ret were
unaffordable. They are not: the inlined form has to sit at the end of the
driver's assembler file and jump there and back, which costs eight. Measured
against each other the two are indistinguishable -- forced edges 12014 against
12061 per run -- so the call wins on the only ground that separates them,
which is that it leaves the library alone.
Measured per 8 kB in both directions at once, edges the handler had to force:
| transmitting only | both directions | |
|---|---|---|
| no hook | ~4950 | -- |
| edge hook, one stashed edge | ~1340 | ~5000 |
| the rest of the byte, fraction carried | 553 | 4257 |
The same firmware sometimes behaves quite differently from one run to the next. In a good run the handler forces about 4200 edges per 8 kB of duplex traffic and loses one sample window; in a bad one, about 10000 and some 400 windows. Nothing in the sketch chooses this, and it is not the build: the same hex, reflashed, gave one and then the other, and two hex files that measured twelvefold apart on this turned out to be byte-identical.
It often persists for a whole session, which is what first suggested that the
host's frame schedule sets it -- whether this device's two transactions land
back to back, one blackout of 200 µs rather than two of 110. But it has also
flipped between consecutive runs with no re-enumeration at all, which that
explanation does not allow. The other candidate is the sketch's own planner
latching a good or a bad estimate when a run starts: probeBits and the
activity estimate are reset in txStart(), and the planner learns from there.
Unresolved, and worth resolving.
It is worth knowing about because it makes measurements lie. Two consecutive
sets of 20 runs of the same firmware measured 6.0 and 446.7 corrupted bytes
per 100 kB. Compare configurations interleaved in one binary, switched at
run time, never in blocks -- frame_test.py --modes does this -- and read
g and e to see which regime a run was in.
With the edges made from the transaction hook the difference mostly goes away:
across 20 runs each preceded by a reboot, forced edges stayed within 1.3% of
each other and no run corrupted more than a single byte. It has not gone
entirely -- the twelvefold pair above was measured after that -- so read g
and e before trusting any run.
TinyBridge/: the bridge described above.TinyBridgeUsi3x/: the receiver on its own (receive only, PB0).TinyBridgeUsi1x/: the classic approach, for comparison: INT0 catches the start bit and USI takes one sample per bit. It needs the UART's TX on both PB0 and PB2. It loses 0.56% of bytes at 9600 bps with USB idle, and 3.7-12.8% at 19200 bps, as V-USB delays the start-bit interrupt.
Setting the port to 110 bps makes the sketch print a line of hex counters and clear them:
nbytes received,gsample windows lost,ocapture queue overflows,fframing errors;- TinyBridge also:
kstack bytes never used,eedges made late,aUSB transactions seen,hbytes started later,pidle bits spent looking,zbytes sent without a safe place (see When a byte fits nowhere),rreceived bytes dropped for want of room,wtimes transmitting was held for data going to the host,Htimes the handler found edges the driver had made for it,Pthe USB frame length it measures, in 1/16 Timer1 ticks (4125 with an exact 16.5 MHz clock; 0.1% is about 4), and withCRC_STATSon,candb, the CRC-16 (XMODEM) and count of the bytes read from USB, to compare with what the host sent; - TinyBridgeUsi3x also:
lglitches,rreceive buffer overflows.
Each counter is tagged by the letter at its own position in COUNTER_TAGS,
so adding one means adding a letter there too; a static_assert fails the
build if the two ever drift apart. g and e are worth reading first: they
say whether a run went well or badly, which varies from one power-up to the
next (see Two regimes).
All of them are 16-bit and wrap silently, so n reads 34464 after 100000
bytes, unless WIDE_STATS is set (see Build options). r
rises when the host stops reading the port and the other device keeps
sending without watching the bridge's RTS (see
Flow control). Ask for the counters
with the port drained, or the line comes back truncated: the host's buffer
is then full, and DigiCDCFast drops what it can't deliver after 50 ms.
bridge_test.py --stats prints them after each transfer, with the host's
own c and b for the data it sent to the bridge.
Both scripts use a reference USB-serial adapter wired to the bridge (TX to
RX, RX to TX, GND to GND), and default to /dev/ttyACM0 for the bridge and
/dev/ttyUSB0 for the adapter. They report bytes lost, extra or corrupted,
with the offsets of the first mismatches.
bridge_test.py [--bytes N] [--rx-only] [--duplex-only] [--stats] [--stats-wait SECONDS] BAUD...sends pseudo-random data one direction at a time, then both at once.--rx-onlyis for the receive-only sketches,--duplex-onlyruns only the test in both directions, and--stats-waitsets the quiet time before asking for the counters.cts_test.py [--baud N] [--bytes N]drives the adapter's RTS by hand, rather than leaving it to the kernel's flow control, and checks that the bridge stops while told to wait and loses nothing when let go.burst_test.py [--bytes N] [--max-burst MAX] [--pause MIN MAX] [--direction both|to-bridge|to-adapter] BAUDsends random bursts with random pauses.
- Forcing a compare match with FOC1A right after changing COM1A made no edge: the output kept its level. A late edge is made as a compare match 2 ticks ahead instead.
- Extending Timer1's count with the core's
millis_timer_overflow_countwas occasionally off by 256 ticks, as its non-blocking handler counts late; the transmitter extends TCNT1 itself. - A byte whose level changes at many edges may fit nowhere between the USB transactions. The planner gives up after 10 idle bits: otherwise the bridge stops sending, its buffer fills, and DigiCDCFast's flow control then refuses even the host's line-coding requests.
- A plain
ISR()for the transmit handler saved 27 registers with interrupts off (~5 µs), long enough to worry V-USB. An entry stub now masks the handler's interrupt and turns interrupts on before saving anything. - The way out matters as much as the way in. The handler and its stub
restored their registers with interrupts off: over 100 CPU cycles after
every edge. V-USB must start within about 40 cycles of a packet, and at
16.5 MHz it doesn't check CRCs, so a packet arriving then could be misread
and accepted. Because the planner keeps the handler at a fixed place in
the USB frame, some sessions lost whole 2-byte packets from the host (seen
as pairs of corrupted bytes, confirmed by the
ccounter disagreeing with the host), and requests to change the bit rate were lost about half the time right after traffic. Restoring registers with interrupts on, and unmasking the handler's interrupt in one short step at the end, fixed both. - A USB-UART bridge makes such bugs hard to see: corrupted data looks like a transmit timing error. Checksumming what the sketch received over USB told the two apart.
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.
- DigisparkBridge by Marco Carnut.