Two or three USB serial ports from one Digispark
(ATtiny85) or Digispark Pro (ATtiny167): SerialUSB, SerialUSB1 and
SerialUSB2, which Linux shows as /dev/ttyACM0 and up. One board can then
talk to two or three host programs at once -- a data stream on one port and a
console on another, say -- with no extra hardware.
It is built on V-USB and is a fork of
DigiCDCFast 1.4.0, itself a
fixed and 40x faster DigisparkCDC, and carries everything that release has:
the same API on every port, usbTransactionEnd(), and the vendor-request
hook. Linux only: Windows refuses low-speed USB serial devices, and macOS is
untested.
#include <DigiCDCDual.h> // or DigiCDCTriple.h for three
void setup() {
SerialUSB.begin(); // connects every port
}
void loop() {
if (SerialUSB.available()) SerialUSB.write(SerialUSB.read());
if (SerialUSB1.available()) SerialUSB1.write(SerialUSB1.read());
}| Ports | Each way | Notes | |
|---|---|---|---|
DigiCDCDual.h |
2 | 8000 bytes/s, shared | 8-byte packets |
DigiCDCTriple.h |
3 | 4000 bytes/s, shared | 4-byte packets: see Three ports |
One port can have the whole rate while the others are quiet, so even a third of it is 25 times what DigisparkCDC managed (161 bytes/s on its one port).
These are the things that behave differently from an ordinary serial port, or from DigiCDCFast. Most of them come from low-speed USB and from V-USB, which does USB in software.
- Call a port's function at least every ~10 ms. USB is serviced only
inside
SerialUSB/SerialUSB1calls (available(),read(),write(),print(),peek(),flush(),refresh(),delay()); either port's call services both. UseSerialUSB.delay(ms)instead ofdelay(ms), and callSerialUSB.refresh()inside anything longer. Otherwise the host resets or drops the device. - Reading one port is reading both. The host is paused for the whole device, not per port: while either port's receive buffer is full, the host can send to neither, nor make control requests (so opening or setting up the other port blocks too). A sketch that stops reading one port, even one nobody writes to much, eventually stops the other. Drain both.
- Writing to a port nobody reads is cheap, then lossy.
write()waits for room, but once the host has taken nothing for 50 ms (no program has the port open) it returns 0 at once until there is room again. What doesn't fit in the transmit buffer is lost; what does fit arrives when a program opens the port. - No carrier or serial state. The ports never send the CDC serial-state
notification, so DCD, DSR and the like are never reported (see
How it works). Linux opens the ports anyway, because
cdc_acmports default toCLOCAL; a program that clearsCLOCALmay wait forever for carrier. - Use the stable names.
ttyACM0/ttyACM1are only those numbers if no other ACM device got there first./dev/serial/by-id/has names that always point at the right port:usb-digistump.com_Digispark_Serial-if00isSerialUSB,...-if02isSerialUSB1. With two boards, tell them apart by/dev/serial/by-path/(the USB port they're plugged into), as they have no serial number. - Open the ports raw. A tty echoes by default. For binary data, or against
a sketch that echoes (an echo bouncing between host and board never stops),
set raw mode:
stty -F /dev/ttyACM0 raw -echo, ortty.setraw()in Python. - ModemManager. The ports declare themselves as modems (CDC ACM, AT
commands), and ModemManager, where it runs, may probe them with AT commands
when they appear. If it does, tell it to leave the board alone with a udev
rule:
ATTRS{idVendor}=="16d0", ATTRS{idProduct}=="087e", ENV{ID_MM_DEVICE_IGNORE}="1". - The ports share one low-speed device, and its 8000 bytes/s each way (see Performance): one port can have it all, or the two can split it however their traffic does. V-USB runs every transaction with interrupts off, and two ports mean more transactions: budget for it in timing-sensitive code (see Interrupt latency).
- RAM is tight on the ATtiny85. It has 512 bytes. The
DualEchoexample uses 336 for globals, leaving 176 for the stack. Shrink the buffers if you need room (see Configuration). - Include the port header in one file of the sketch only. It defines the
further ports, their buffers and the USB descriptor. Other files that need
the ports include
DigiCDCFast.h, which only declares them. - Same USB IDs as DigisparkCDC (16d0:087e, "Digispark Serial"). Programs that look for a Digispark by ID find this too.
- Windows doesn't work at all, and macOS is untested (see Hosts).
SerialUSB and SerialUSB1 are both DigiCDCDevice, a Stream, so
print(), println(), readBytes() and the rest work as usual.
| Function | |
|---|---|
begin(), begin(baud) |
Connects to the host, both ports at once, and waits 500 ms for enumeration. Only the first call does anything; the baud rate is ignored. |
end() |
Disconnects from the host: both ports. |
available(), read(), peek() |
As usual; read() and peek() return -1 when there is nothing to read. |
write(), print()... |
Queue the data, waiting for room while the host is reading (see above). |
availableForWrite() |
Free space in the transmit buffer, to write without waiting. |
flush() |
Waits until the host has taken everything written (50 ms timeout). |
baud() |
The bit rate the host last set for this port (stty, tcsetattr()). Each port has its own. |
refresh(), task() |
Services USB. |
delay(ms) |
Waits while servicing USB. |
setDtrPin(pin) |
Drives a pin of port B from this port's DTR line (untested). |
Define these before including the port header:
| Macro | Default | |
|---|---|---|
DIGICDC_PORTS |
2 | How many ports, 2 to 4; DigiCDCDual.h and DigiCDCTriple.h set it. |
DIGICDC_PACKET_SIZE |
8 (4 in DigiCDCTriple.h) |
USB packet size of every port, 1 to 8 bytes. |
DIGICDC_TX0_SIZE, DIGICDC_RX0_SIZE |
64, 32 | SerialUSB's buffers, 1 to 255 bytes; the receive one must hold a packet. |
DIGICDC_TX1_SIZE, DIGICDC_RX1_SIZE |
32, 32 | SerialUSB1's, and the default for SerialUSB2's and SerialUSB3's. |
DIGICDC_TX2_SIZE, DIGICDC_RX2_SIZE, DIGICDC_TX3_SIZE, DIGICDC_RX3_SIZE |
as port 1 | The third and fourth ports'. |
Smaller buffers lose nothing (the host is paused, and write() waits), but
they cost throughput -- see How small the buffers can
be.
Don't combine with DigiCDCMedium.h (DigiCDCFast's 2-byte packets for one
port): its descriptor is ignored here, and only its 8-byte buffers would
apply, to SerialUSB. Use DIGICDC_PACKET_SIZE instead.
A sketch that can only provide some settings, such as a USB-UART bridge, can
define digiCdcAcceptLineCoding() (with C linkage); returning 0 makes the
library answer SET_LINE_CODING with a STALL and keep the settings it had:
extern "C" uint8_t digiCdcAcceptLineCoding(const uint8_t *coding)
{
/* coding: bit rate (4 bytes, little endian), stop bits, parity, data bits */
uint32_t baud = (uint32_t)coding[0] | (uint32_t)coding[1] << 8
| (uint32_t)coding[2] << 16 | (uint32_t)coding[3] << 24;
return baud == 9600 || baud == 4800;
}It is asked about both ports, and isn't told which. Linux doesn't pass the
refusal on: tcsetattr() and stty still succeed.
DigiCDCTriple.h gives three, with the 4-byte packets they need. The
library must be compiled with V-USB's third transmit buffer, which the
Arduino IDE cannot take from a sketch: set USB_CFG_HAVE_INTRIN_ENDPOINT5 to
1 in src/usbconfig.h, or build with
arduino-cli compile --fqbn digistump:avr:digispark-tiny:clock=clock165 \
--build-property "build.extra_flags=-DUSB_CFG_HAVE_INTRIN_ENDPOINT5=1" MySketchMeasured on a Digispark against Linux 6.8 on an xHCI root port, echoing
through every port (extras/test/multiecho.py; bytes per second each way,
"alone" is one port working while the others idle):
| Ports | Packet size | Alone | Shared | |
|---|---|---|---|---|
| 2 | 8 | 7990 | 7996 | |
| 3 | 8 | refused: "Not enough bandwidth for new device state" | ||
| 3 | 6 | refused | ||
| 3 | 4 | 3990 | 3989 | what DigiCDCTriple.h builds |
| 3 | 2 | 1996 | 1999 | |
| 4 | 2, and 1 | refused |
A third port costs half the throughput. The host polls every data endpoint in every 1 ms frame, and six of them only fit a low-speed frame at 4 bytes each. The device's total is the packet size times the frame rate, so three ports share 4000 bytes/s where two share 8000.
A fourth is refused whatever the packet size, so what runs out is the
number of periodic endpoints, not their size: four ports declare twelve.
Dropping the notification endpoints, which this library never sends on, gets
the configuration accepted -- and then Linux's cdc_acm refuses every
interface with -22, because a communication interface must have one. So
four CDC ports are out of reach here, below the library's own ceiling of four
(V-USB's two extra buffers, and a 241-byte descriptor against the 254 V-USB
can return).
Untested: any other host. A board behind a USB 2 hub is scheduled by the hub's transaction translator rather than the root port and may answer differently.
Each port costs about 62 bytes of RAM with one-packet buffers: its object (~32), its two buffers, and V-USB's 12-byte transmit buffer.
| Configuration | RAM | Alone | Shared |
|---|---|---|---|
| 2 ports, 64/32 and 32/32 (the defaults) | 336 | 7990 | 7996 |
| 2 ports, 16/16 both | 243 | 7973 | 7373 |
| 2 ports, 8/8 both | 213 | 4694 | 4201 |
3 ports, 8/4 (TripleEcho) |
258 | 3988 | 3986 |
| 3 ports, 1-byte transmit buffers | 253 | 3982 | 3980 |
- A receive buffer must hold one packet. The library pauses the host when another would not fit, so anything smaller pauses it for good.
- One packet is not enough to keep 8-byte packets flowing: at 8/8 the host is stopped after every packet until the sketch drains it, and the rate falls by nearly half. Two packets (16/16) get it back for 93 bytes less RAM than the defaults. With 4-byte packets the sketch keeps up, and one packet costs nothing measurable.
- A transmit buffer can be 1 byte if the sketch never produces faster
than USB drains -- an echo doesn't. A sketch that writes in bursts is the
one that suffers:
write()then waits for a poll per byte.
DIGICDC_TX0_SIZE and DIGICDC_RX0_SIZE set port 0's buffers (or
DIGICDC_BUFFERS(tx, rx) in one of the sketch's files), DIGICDC_TX1_SIZE
and friends the others'. How all this was measured, and what was not, is in
docs/multiport.md.
Two serial ports spend every endpoint the driver has. A sketch that wants to
be something else as well has none left -- but control transfers need no
endpoint at all. Set USB_CFG_VENDOR_HOOK to 1 in src/usbconfig.h (in the
library: the Arduino IDE compiles it once, without the sketch's definitions)
and vendor requests go to a function the sketch defines:
extern "C" uchar digiCdcVendorSetup(usbRequest_t *rq)
{
static uchar answer;
if(rq->bRequest == MY_REQUEST){
answer = whatever();
usbMsgPtr = (usbMsgPtr_t)&answer;
return 1; /* bytes to send; 0 sends nothing */
}
return 0;
}Return what usbFunctionSetup() returns: a byte count to send from
usbMsgPtr (which may point at the sketch's own buffer, up to 254 bytes), 0,
or 0xff to take data through usbFunctionWrite(). Both serial ports are
unaffected and stay enumerated throughout.
Address the requests to the device rather than to an interface, and the host side needs no interface claimed, so it works while the kernel's serial driver holds the ports:
libusb_control_transfer(h, LIBUSB_REQUEST_TYPE_VENDOR |
LIBUSB_RECIPIENT_DEVICE | LIBUSB_ENDPOINT_IN,
MY_REQUEST, 0, 0, buf, sizeof buf, 1000);It runs inside the USB interrupt, so it should read state and set flags and
leave the work to loop(). Nothing is compiled in when the option is 0.
This came from DigiCDCFast 1.4.0, where
DigisparkRNG uses it to serve
random bytes while leaving that library's one serial port free; it has not
been exercised with two ports.
A CDC serial port has a communication interface, with an interrupt IN
endpoint for notifications, and a data interface, with a bulk OUT and a bulk
IN endpoint. So two ports want four IN endpoints and three want six, where
V-USB has transmit buffers for endpoint 1 and endpoint 3. This library adds
two more (endpoints 5 and 7, USB_CFG_HAVE_INTRIN_ENDPOINT5 and _7) and
gives every one of them to data:
Port 0 (SerialUSB) |
Port 1 (SerialUSB1) |
Port 2 (SerialUSB2) |
|
|---|---|---|---|
| Interfaces | 0 (communication), 1 (data) | 2, 3 | 4, 5 |
| Data OUT / IN | 0x01 / 0x81 | 0x03 / 0x83 | 0x05 / 0x85 |
| Notifications | 0x82: always NAK | 0x84: always NAK | 0x86: always NAK |
Every transmit buffer carries data, and the notification endpoints are
declared (Linux's cdc_acm refuses a communication interface without one,
with -22) but never send anything. V-USB used to answer an IN request to any endpoint it
didn't know with endpoint 1's data; it now answers NAK unless the endpoint is
1 or 3 (handleIn1 in src/asmcommon.inc). That check costs endpoint 1 two
cycles, which still leaves its reply two cycles earlier than in the original
V-USB, thanks to a DigiCDCFast change.
The data endpoints are declared bulk, which the USB specification does not allow at low speed. Linux converts them to interrupt endpoints polled every millisecond and says so in the kernel log ("endpoint 0x1 is Bulk; changing to Interrupt"); that is expected.
The library itself works with one port or several: the port header defines
the extra ports, a table of them in flash and the descriptor, and the library
finds them through weak references. Including only DigiCDCFast.h gives a
single port with DigiCDCFast's descriptor, byte for byte, at a cost of about
250 bytes of flash and 10 of RAM over DigiCDCFast itself -- so for one port,
use DigiCDCFast.
Measured on a Digispark (ATtiny85, 16.5 MHz) and a Digispark Pro (ATtiny167,
16 MHz), alike to within 0.1%, with Linux 6.8 on an xHCI controller,
echoing 20000 bytes per port (the DualEcho example, with a sketch-level
jump to the bootloader added for reflashing):
| Throughput, each way | |
|---|---|
| One port busy | 7990 bytes/s |
| Both ports busy, combined | 7996 bytes/s |
| Three ports (4-byte packets), one busy | 3988 bytes/s |
| Three ports, all busy, combined | 3986 bytes/s |
All data came back intact, and no USB errors were logged. The rate is the device's, not each port's: the ports share it. See Three ports for why three halves it.
Flash and RAM for global variables, Digistump core 1.7.5:
| Sketch | Digispark | Digispark Pro |
|---|---|---|
DualEcho |
3428 bytes, 336 RAM | 3408 bytes, 352 RAM |
DigiCDCFast 1.4.0's Echo, for comparison |
3216 bytes, 238 RAM | 3198 bytes, 246 RAM |
The second port costs its 64 bytes of buffers, the object that holds its state, and 58 bytes of descriptor in flash.
V-USB handles every USB transaction inside an interrupt that can't be
interrupted, so it delays every other interrupt in the sketch. Even when idle
the host polls the IN endpoint every millisecond, and each poll runs the
handler. extras/latency/LatencyProbe measures how late a timer interrupt
runs (3.88 us resolution, 20 s per case, extras/latency/latency.py on the
host); same board and host as above:
| USB activity | Delayed at all | 99% | 99.9% | Max |
|---|---|---|---|---|
| Idle | 3% | 27 us | 39 us | 39 us |
| Sketch sending 8000 bytes/s | 10% | 82 us | 93 us | 97 us |
| Sketch receiving 5500 bytes/s | 10% | 93 us | 105 us | 109 us |
| Both | 14% | 97 us | 105 us | 198 us |
For timing-sensitive code this is the budget. For example, a software UART that samples each bit in its middle after catching the start bit misreads a byte whenever the start bit is caught more than half a bit late. That happens to 1.4% of interrupts at 19200 bps with USB idle, and 4-6% at 9600 bps while data is flowing.
These are DigiCDCFast's measurements with one port. Two busy ports mean more transactions, so more of these delays; that has not been measured.
The 198 µs in the table is not one transaction but two. When the host runs transactions back to back -- which is what heavy two-way traffic looks like -- the driver finds the next packet already arriving as it finishes one, and handles the whole run without leaving the interrupt. Nothing else runs meanwhile, and there is no way to wait it out: on the way back the processor serves the pending USB interrupt before any interrupt with a higher vector number, so returning between transactions changes nothing (it was tried, and the latency distribution did not move).
For work that cannot wait that long, the driver calls usbTransactionEnd()
at the end of every transaction, a weak, do-nothing function a sketch may
replace:
extern "C" void usbTransactionEnd() __attribute__((naked, used));
void usbTransactionEnd()
{
asm volatile("lds r16, %[reg] \n" // read a byte the UART is holding,
"..." // stash it, and
"ret \n" // return; no prologue, no epilogue
:: [reg] "i"(_SFR_MEM_ADDR(LINDAT)));
}The rules are strict, because it runs inside the USB interrupt:
- Assembler, naked. The compiler saves nothing for a naked function and would happily use registers the driver has not saved.
- Only what the driver saved, which is not the same on both chips:
r16-r21,Yand the flags are saved on either. The ATtiny85 also savesr0but notr22; the ATtiny167 savesr22but notr0. Keep to the intersection unless you have checked the push list in the driver file your board uses. Everything else belongs to whatever the interrupt interrupted. - Short. If the host has already begun the next transaction, its packet is sending its sync pattern meanwhile and the driver has to be back in time to catch it. Hooks of 25 and of 33 cycles both measured clean at 16 MHz, so there is room, but a packet caught late is received corrupted rather than missed, and silently: at 16 and 16.5 MHz V-USB has no time to check a CRC.
- Don't try to be clever about when it runs. It is called on every transaction, and calling it only when the next packet was already arriving -- seemingly the thrifty choice, since that is the case the sketch cannot otherwise reach -- turned out to be far worse than having no hook at all, in the bridge that motivated this. The sketch's own handler then stays pending through the run of transactions and runs the moment the driver returns, which is exactly when the next packet arrives.
A sketch that defines no hook still pays for the call: four bytes of flash,
and seven cycles per transaction that delay the driver's return by 0.42 µs at
16.5 MHz. That is measurably nothing to both bridges written against this
library, so it is on by default, but a sketch whose own timing is built
around the moment the driver lets go can set USB_CFG_TRANSACTION_END_HOOK
to 0 in src/usbconfig.h. It has to be set there, in the library: the
Arduino IDE compiles a library once, without the sketch's definitions. A
sketch that needs the hook can say so with
#if !USB_CFG_TRANSACTION_END_HOOK
#error "this sketch needs USB_CFG_TRANSACTION_END_HOOK in src/usbconfig.h"
#endifDigisparkProBridge uses it to collect the byte its UART is holding, and to hand the transmitter its next one, which is what let it run 76800 bps in both directions at once without losing any -- the last rate low-speed USB can carry, 7680 of the 8000 bytes/s. It managed 38400 bps before.
On the Digispark Pro, packets sent to the host get lost unless the core is
fixed. The Digistump AVR core 1.7.5 runs millis() on Timer0 with a
blocking interrupt handler, SIGNAL(TIMER0_OVF_vect) in cores/pro/wiring.c.
It overflows at 976.56 Hz, beating against the host's 1000 Hz USB frames, so
every 42.7 ms it delays V-USB just as the host polls, and the transaction
fails. V-USB has already counted the data as sent, so it is lost. (The
ATtiny85 core uses a non-blocking handler for this reason.)
Change that line to
ISR(TIMER0_OVF_vect, ISR_NOBLOCK)Measured with usbmon while the sketch sent one byte per millisecond and the host flooded it: 259 failed IN transactions and 1.5% of the bytes lost with the original core, none of either with the change.
The same applies to a sketch's own interrupt handlers: any handler that keeps interrupts off for more than a few microseconds when USB traffic arrives makes transactions fail. V-USB has to start within about 40 CPU cycles (2.5 us) of a packet's first bits, and at 16 and 16.5 MHz it can't check CRCs (only its 18 MHz code has room for that), so starting too late can also let a packet through corrupted, accepted as valid. That includes a handler's exit: restoring registers takes 2 cycles each. Handlers that run often should mask their own interrupt and re-enable interrupts, and do the same before restoring their registers, as the USB-UART bridges built on this library do. In DigisparkBridge, a handler that restored its registers with interrupts off (over 100 cycles) let corrupted packets from the host through in some sessions and lost the host's line-setting requests.
Only Linux has been tested (see Tested).
Windows 10 refuses a USB CDC serial port on a low-speed device, whichever way its data endpoints are declared. This applies to DigisparkCDC and other V-USB serial devices as well. Tested in a Windows 10 virtual machine (VMware, USB passthrough), with usbmon on the Linux host recording every request:
| Data endpoints | What Windows does |
|---|---|
| Bulk (as here; the USB specification doesn't allow them at low speed) | Reads the device and configuration descriptors correctly, then rejects the configuration and resets the device, over and over: "USB device not recognized". |
| Interrupt (1 ms; an experiment, not in the library) | Configures the device and creates a COM port, but its serial driver, usbser.sys, refuses to start (Code 10) without sending a single request to the device. |
Every request reached the board and was answered correctly, so these are
Windows' decisions about the descriptors, not transfer problems. (Linux
treats both versions the same: it turns bulk endpoints into interrupt
endpoints itself, and all the tests pass either way.) V-USB serial projects
used a third-party filter driver on Windows: Digistump's driver package
installs lowcdc.sys (Osamu Tamura's Low Speed CDC Driver, 2009, signed,
64-bit) below usbser.sys, meant for interrupt data endpoints. On
Windows 10 it crashed the system (blue screen) as soon as the device with
interrupt endpoints was attached, with the virtual machine emulating a USB 3
controller and again with a USB 2 controller; with bulk endpoints Windows
rejects the device before the driver loads.
Untested. Hosts that follow the USB specification strictly may refuse the low-speed bulk endpoints, as Windows does.
- Digispark (ATtiny85) at 16.5 MHz (
digistump:avr:digispark-tiny:clock=clock165), Digistump AVR core 1.7.5, micronucleus 2.6, Linux 6.8 on xHCI: two ports and three echo intact, alone and together, at every buffer and packet size in docs/multiport.md. - Digispark Pro (ATtiny167, 16 MHz crystal,
digistump:avr:digispark-pro), with the core fix above, same host: the same, over 5 rounds of 20000 bytes per port, with the same throughput and no USB errors.
Not yet:
DIGICDC_PACKET_SIZEof 1, 3, 5 or 7, andsetDtrPin().- Four ports: the host refuses them (see Three
ports), so that path is code without a
home.
DIGICDC_PORTS=4builds and its descriptor parses, nothing more. - The single-port
HostTestsuite (extras/test) on this code; DigiCDCFast passes it. - Hubs, other host controllers, the Raspberry Pi, macOS.
Reports, good or bad, are welcome.
Install the Digistump AVR core first (on the Digispark Pro, with the fix
above). Then clone this repository into your Arduino/libraries folder, or
download a release ZIP and use Sketch > Include Library > Add .ZIP Library.
Build for the Digispark with the 16.5 MHz clock option: the core's default, "16 MHz - No USB", compiles but has no USB.
arduino-cli compile --fqbn digistump:avr:digispark-tiny:clock=clock165 MySketch
arduino-cli compile --fqbn digistump:avr:digispark-pro MySketchDon't install it next to DigiCDCFast if your sketches include
DigiCDCFast.h: both libraries have that header. (It was called DigiCDCDual
while it did only two ports.)
library.properties Arduino library metadata
src/ DigiCDCMulti.h (any count), DigiCDCDual.h and DigiCDCTriple.h,
DigiCDCFast.{h,cpp}, DigiCDCDescriptor.h, DigiCDCMedium.h,
DigiCDCBuffers.S and DigiCDCBufferSizes.h (default buffers),
and V-USB (with V-USB's Readme.txt and Changelog.txt)
docs/multiport.md what each port and each byte of buffer costs, measured
examples/DualEcho both ports send back what they receive
examples/TripleEcho three ports do
examples/MultiEcho any count, for measuring; rescues itself from a host that refuses it
extras/test/multiecho.py drives the echo examples on any number of ports
extras/test/ single-port hardware test (HostTest sketch and hosttest.py)
extras/latency/ interrupt latency probe (LatencyProbe sketch and latency.py)
License.txt V-USB license (GPL-2.0 or GPL-3.0)
CommercialLicense.txt V-USB's commercial license terms, as distributed with V-USB
The git history continues DigiCDCFast's, which starts from the unmodified DigisparkCDC files of the Digistump core, so every change since is visible.
DigiCDCMulti is free software under the same license as V-USB and
DigiCDCFast: the GNU General Public License version 2 or version 3, at your
choice. See
License.txt, which also has Objective Development's requests
for projects using V-USB. The commercial license in CommercialLicense.txt is
offered by Objective Development for V-USB only; it is kept because it is part
of the V-USB distribution.
- V-USB by OBJECTIVE DEVELOPMENT Software GmbH, https://www.obdev.at/vusb/.
- DigiCDC (DigisparkCDC) by Ihsan Kehribar (kehribar.me) and Digistump LLC (digistump.com), from the Digistump AVR core (https://github.com/digistump/DigistumpArduino, version 1.7.5 as distributed by https://github.com/ArminJo/DigistumpArduino).
- DigiCDCFast and DigiCDCMulti changes by Marco Carnut.