Skip to content

Conversation

@functionpointer
Copy link

Overview

NCM (Network Control Model) is the newest of three protocols for Ethernet over USB.

This implementation creates an Ethernet connection with a host PC over USB without any extra hardware.
Other uses of USB like Serial, Mouse or Keyboard continue to work in parallel.

Use case is for smart home devices that live close to a PC or server. NCM gives the reliability of a wired connection without extra hardware like an Ethernet NIC and without the downsides of classic solutions like a custom protocol over serial. Instead, existing software like ESPHome can be used.

Testing

I have tested on Windows and Linux. No driver installation is required. MacOS should also work.
The example sketch provides 1ms ping latency.
grafik

Usage

Compile the provided example sketch.
Modify DHCP and logging as desired.

Once running, the Pico shows up as a new network interface on the host PC.
It must be configured before use. Example for Linux:

ip link set enp1s0u1u4i2 up
ip addr add 192.168.137.1/24 dev enp1s0u1u4i2 

On Windows, Internet Connection Sharing can be configured: https://superuser.com/a/1899168
That even includes a DHCP server. In my tests, it works with LWIP's built-in DHCP client.

Implementation details

The PR cleanly hooks into existing USB and Ethernet infrastructure provided by USB.h and LwipIntfDev.h. The only global change is in include/tusb_config.h to include the NCM implemetation of tinyUSB.

The class NCMEthernet.cpp interfaces with tinyusb by defining the callbacks tud_network_recv_cb(), tud_network_init_cb() and tud_network_xmit_cb().

Receiving is the hardest part, as it involves passing a packet between the execution context of tinyUSB to Lwip without deadlocks or copying. The proposed solution uses a new asnc_when_pending_worker to solve this problem. When tinyUSB calls tud_network_recv_cb() we cannot call lwip directly. Instead, the packet's pointer is stored, and the worker is set to be pending.
Later, the worker calls LwipIntfDev::_irq() which fetches the packet and sends it to Lwip.
The existing interrupt infrastructure in LwipIntfDev cannot be used, as it expects a physical pin to be toggled.
Polling was tested but dismissed due to high latency.

Transmitting packets does not present the same challenges. We simply call tinyUSB directly in NCMEthernet::sendFrame.

functionpointer and others added 3 commits December 10, 2025 18:27
of three protocols for Ethernet over USB.

This implementation in arduino-pico
enables an Ethernet connection with a host PC
over USB without any extra hardware.
Other uses of USB like Serial,
Mouse or Keyboard continue to work in parallel.

It has been tested on Windows and Linux.
MacOS should also work.
@earlephilhower
Copy link
Owner

Very nice work! And thanks for the use case description.

Do you know what the delta RAM usage is for this on, say, Blink.ino? TinyUSB does everything statically allocated so I think enabling this will cost all applications some amount and want to understand how much that'd be.

Also, on quick glance, I think this might have issues under FreeRTOS due to the async context threadsafe. I will dig a bit more to understand how you're using it in place of IRQ callbacks, and how it would interact with the existing network IRQ/packet injection which happens in a separate task.


NCMEthernet::NCMEthernet(int8_t cs, arduino::SPIClass &spi, int8_t intrpin) {
(void) cs;
(void) spi;

Check warning

Code scanning / CodeQL

Expression has no effect Warning

This expression has no effect.
NCMEthernet::NCMEthernet(int8_t cs, arduino::SPIClass &spi, int8_t intrpin) {
(void) cs;
(void) spi;
(void) intrpin;

Check warning

Code scanning / CodeQL

Expression has no effect Warning

This expression has no effect.
}

bool NCMEthernet::begin(const uint8_t* mac_address, netif *net) {
(void) net;

Check warning

Code scanning / CodeQL

Expression has no effect Warning

This expression has no effect.

if (data_len > bufsize) {
// Packet is bigger than buffer - drop the packet
discardFrame(data_len);

Check warning

Code scanning / CodeQL

Expression has no effect Warning

This expression has no effect (because
discardFrame
has no external side effects).
uint16_t readFrame(uint8_t* buffer, uint16_t bufsize);

void discardFrame(uint16_t ign) {
(void) ign;

Check warning

Code scanning / CodeQL

Expression has no effect Warning

This expression has no effect.
@earlephilhower
Copy link
Owner

Looks like you need to define a weak callback for TinyUSB somewhere in cores/rp2040, per CI:

/home/runner/work/arduino-pico/arduino-pico/system/arm-none-eabi/bin/../lib/gcc/arm-none-eabi/14.3.0/../../../../arm-none-eabi/bin/ld: /home/runner/arduino_ide/hardware/pico/rp2040/lib/rp2350/libpico.a(ncm_device.c.o): in function `recv_transfer_datagram_to_glue_logic':
/home/runner/work/arduino-pico/arduino-pico/pico-sdk/lib/tinyusb/src/class/net/ncm_device.c:645:(.text.tud_network_recv_renew+0x3a): undefined reference to `tud_network_recv_cb'

@functionpointer
Copy link
Author

I have tested Blink.ino on this branch (7aa537172c9aa3) and on master (c5a1255413fbaeb4) and there does seem to be some overhead. Program memory stays at 2% usage, but RAM jumps from 3% to 6%.
Here more details:

  master usb_ncm_new delta
PROGMEM 57128 Bytes 58424 Bytes 1296 Bytes
RAM 9788 Bytes 15896 Bytes 6108 Bytes


extern "C" bool tud_network_recv_cb(const uint8_t *src, uint16_t size) __attribute__((weak));
extern "C" bool tud_network_recv_cb(const uint8_t *src, uint16_t size) {
(void) src;

Check warning

Code scanning / CodeQL

Expression has no effect Warning

This expression has no effect.
extern "C" bool tud_network_recv_cb(const uint8_t *src, uint16_t size) __attribute__((weak));
extern "C" bool tud_network_recv_cb(const uint8_t *src, uint16_t size) {
(void) src;
(void) size;

Check warning

Code scanning / CodeQL

Expression has no effect Warning

This expression has no effect.
@earlephilhower
Copy link
Owner

The ROM's not too big a deal, I'd say, but losing 6KB of RAM for all sketches feels kind of painful. Let me see if there's a simple SDKOverride we can do to lazy allocate the network buffers (looking at the MAP, the ncm_epbuf is 6056 bytes in size).

FWIW I was able to test the net connection from the Pico to my Ubuntu 22.04 box (for some reason IP forwarding is not quite working, and I don't want to mess with things because I have several office VPN connections and Virtmgr/Docker rules). Just moving the fortune to the GW 192.168.137.1 of the Pico and I can ping it as well as receive it's fortune requests.

@earlephilhower
Copy link
Owner

I'm getting lots of crashes when a USB serial connection is active and the network interface is line

#0  _exit (status=status@entry=1) at /home/earle/Arduino/hardware/pico/rp2040/cores/rp2040/sdkoverride/newlib_interface.c:45
#1  0x100102b2 in panic (fmt=0x10020529 "ep %02X was already available") at /home/earle/Arduino/hardware/pico/rp2040/pico-sdk/src/rp2_common/pico_platform_panic/panic.c:82
#2  0x20000c80 in _hw_endpoint_buffer_control_update32 (ep=ep@entry=0x20002c00 <hw_endpoints+160>, and_mask=and_mask@entry=0, or_mask=or_mask@entry=37888) at /home/earle/Arduino/hardware/pico/rp2040/pico-sdk/lib/tinyusb/src/portable/raspberrypi/rp2040/rp2040_usb.c:108
#3  0x20000cfc in _hw_endpoint_buffer_control_set_value32 (ep=0x20002c00 <hw_endpoints+160>, value=37888) at /home/earle/Arduino/hardware/pico/rp2040/pico-sdk/lib/tinyusb/src/portable/raspberrypi/rp2040/rp2040_usb.h:122
#4  hw_endpoint_start_next_buffer (ep=ep@entry=0x20002c00 <hw_endpoints+160>) at /home/earle/Arduino/hardware/pico/rp2040/pico-sdk/lib/tinyusb/src/portable/raspberrypi/rp2040/rp2040_usb.c:191
#5  0x10015086 in hw_endpoint_xfer_start (ep=0x20002c00 <hw_endpoints+160>, buffer=buffer@entry=0x20001dcc <_cdcd_epbuf+64> "\r\nStarting NCM Ethernet port", total_len=total_len@entry=2) at /home/earle/Arduino/hardware/pico/rp2040/pico-sdk/lib/tinyusb/src/portable/raspberrypi/rp2040/rp2040_usb.c:216
#6  0x10014fa4 in hw_endpoint_xfer (ep_addr=<optimized out>, buffer=0x20001dcc <_cdcd_epbuf+64> "\r\nStarting NCM Ethernet port", total_bytes=2) at /home/earle/Arduino/hardware/pico/rp2040/pico-sdk/lib/tinyusb/src/portable/raspberrypi/rp2040/dcd_rp2040.c:148
#7  dcd_edpt_xfer (rhport=<optimized out>, ep_addr=<optimized out>, buffer=buffer@entry=0x20001dcc <_cdcd_epbuf+64> "\r\nStarting NCM Ethernet port", total_bytes=total_bytes@entry=2) at /home/earle/Arduino/hardware/pico/rp2040/pico-sdk/lib/tinyusb/src/portable/raspberrypi/rp2040/dcd_rp2040.c:517
#8  0x10012ab8 in usbd_edpt_xfer (rhport=<optimized out>, rhport@entry=0 '\000', ep_addr=<optimized out>, buffer=buffer@entry=0x20001dcc <_cdcd_epbuf+64> "\r\nStarting NCM Ethernet port", total_bytes=total_bytes@entry=2) at /home/earle/Arduino/hardware/pico/rp2040/pico-sdk/lib/tinyusb/src/device/usbd.c:1343
#9  0x1001344e in tud_cdc_n_write_flush (itf=itf@entry=0 '\000') at /home/earle/Arduino/hardware/pico/rp2040/pico-sdk/lib/tinyusb/src/class/cdc/cdc_device.c:213
#10 0x100137bc in cdcd_xfer_cb (rhport=0 '\000', ep_addr=130 '\202', result=<optimized out>, xferred_bytes=28) at /home/earle/Arduino/hardware/pico/rp2040/pico-sdk/lib/tinyusb/src/class/cdc/cdc_device.c:494
#11 0x100130d6 in tud_task_ext (timeout_ms=<optimized out>, in_isr=<optimized out>) at /home/earle/Arduino/hardware/pico/rp2040/pico-sdk/lib/tinyusb/src/device/usbd.c:659
#12 0x1000d83a in tud_task () at /home/earle/Arduino/hardware/pico/rp2040//pico-sdk/lib/tinyusb/src/device/usbd.h:69
#13 USBClass::usbIRQ () at /home/earle/Arduino/hardware/pico/rp2040/cores/rp2040/USB.cpp:436
#14 USBClass::usbIRQ () at /home/earle/Arduino/hardware/pico/rp2040/cores/rp2040/USB.cpp:431
#15 <signal handler called>
#16 0x1000ecf6 in interrupts () at /home/earle/Arduino/hardware/pico/rp2040/cores/rp2040/wiring_private.cpp:68
#17 0x100043a4 in ethernet_arch_lwip_gpio_mask () at /home/earle/Arduino/hardware/pico/rp2040/libraries/lwIP_Ethernet/src/LwipEthernet.cpp:133
#18 0x1000441a in ethernet_timeout_reached (context=<optimized out>, worker=<optimized out>) at /home/earle/Arduino/hardware/pico/rp2040/libraries/lwIP_Ethernet/src/LwipEthernet.cpp:242
#19 0x1001538e in async_context_base_execute_once (self=self@entry=0x20001770 <lwip_ethernet_async_context>) at /home/earle/Arduino/hardware/pico/rp2040/pico-sdk/src/rp2_common/pico_async_context/async_context_base.c:96
#20 0x1000c8fe in process_under_lock (self=self@entry=0x20001770 <lwip_ethernet_async_context>) at /home/earle/Arduino/hardware/pico/rp2040/cores/rp2040/sdkoverride/../../../pico-sdk/src/rp2_common/pico_async_context/async_context_threadsafe_background.c:257
#21 0x1000ca6c in low_priority_irq_handler () at /home/earle/Arduino/hardware/pico/rp2040/cores/rp2040/sdkoverride/../../../pico-sdk/src/rp2_common/pico_async_context/async_context_threadsafe_background.c:299
#22 <signal handler called>
#23 0x10003aec in NCMEthernet::usbInterfaceCB (this=0x20001948 <eth>, itf=itf@entry=2, dst=0x200127f3 "", len=85) at /home/earle/Arduino/hardware/pico/rp2040/libraries/lwIP_USB_NCM/src/utility/NCMEthernet.cpp:94
#24 0x10003bc8 in NCMEthernet::_usb_interface_cb (itf=2, dst=<optimized out>, len=<optimized out>, param=<optimized out>) at /home/earle/Arduino/hardware/pico/rp2040/libraries/lwIP_USB_NCM/src/utility/NCMEthernet.h:104
#25 0x1000dc4c in USBClass::setupUSBDescriptor (this=<optimized out>) at /home/earle/Arduino/hardware/pico/rp2040/cores/rp2040/USB.cpp:375
#26 USBClass::setupUSBDescriptor (this=<optimized out>) at /home/earle/Arduino/hardware/pico/rp2040/cores/rp2040/USB.cpp:331
#27 0x1000dd5e in USBClass::connect (this=this@entry=0x20001c44 <USB>) at /home/earle/Arduino/hardware/pico/rp2040/cores/rp2040/USB.cpp:483
#28 0x10003a8c in NCMEthernet::begin (this=this@entry=0x20001948 <eth>, mac_address=mac_address@entry=0x20001a4d <eth+261> "N1\033~\2276", net=net@entry=0x200019ec <eth+164>) at /home/earle/Arduino/hardware/pico/rp2040/libraries/lwIP_USB_NCM/src/utility/NCMEthernet.cpp:78
#29 0x100037be in LwipIntfDev<NCMEthernet>::begin (this=this@entry=0x20001948 <eth>, macAddress=macAddress@entry=0x0, mtu=mtu@entry=1500) at /home/earle/Arduino/hardware/pico/rp2040/libraries/lwIP_Ethernet/src/LwipIntfDev.h:396
#30 0x100038de in NCMEthernetlwIP::begin (this=this@entry=0x20001948 <eth>, macAddress=macAddress@entry=0x0, mtu=mtu@entry=1500) at /home/earle/Arduino/hardware/pico/rp2040/libraries/lwIP_USB_NCM/src/NCMEthernetlwIP.cpp:12
#31 0x100034f2 in setup () at /tmp/arduino_modified_sketch_29224/WiFiClient-NCMEthernet.ino:45
#32 0x1000ea58 in main () at /home/earle/Arduino/hardware/pico/rp2040/cores/rp2040/main.cpp:181

If I don't have a serial monitor/minicom active things are fine, but OTW I get something like the above almost every time. There may be some weird interaction between USB and Ethernet processing (which were never intertwined before) when this is actuve...

@functionpointer
Copy link
Author

Oh, I have not seen that before. I will try to replicate. Maybe the packet handoff is breaking some rules still

@earlephilhower
Copy link
Owner

I can't seem to find the magic incantation to make a PR against your PR here, but if you look at the last 3 commits to #3266 you'll see where I was able to make the ncd_device not use any RAM (and almost no ROM) when not used by making a dummy sdkoverride, using the original ncd_device.c inside your library folder (so only compiled when used), and removing the precompiled file from libpico.

@functionpointer
Copy link
Author

functionpointer commented Dec 10, 2025

Wouldn't that cause a dependency headache when upgrading lwip tinyusb?

@earlephilhower
Copy link
Owner

earlephilhower commented Dec 11, 2025

Wouldn't that cause a dependency headache when upgrading lwip tinyusb?

It's a very minor one. We have hand modified files from the SDK in cores/rp2040/sdkoverrides already, while this one is just a direct copy over (with an include path change which could be avoided by tweaking platform_inc.txt). It could be automated as part of makelibpico.sh in fact. TinyUSB is also pretty mature so I don't expect much churn in the USB networking side...

If you have a better way, though, of not needing the extra unused 6K of RAM for most users, I'm happy to hear it. But OTW, while I think this is pretty neat. I don't think I could make all other apps lose that space.

-edit- Now that I found what seems to be a reasonable way to do this, I might also do the same for MIDI and HID which takes 1/2 KB on every sketch. With RAM prices increasing 300% in the last 30 days every byte counts. 😆 -edit-

Copy link
Owner

@earlephilhower earlephilhower left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get your hesitation on doing the TUSB hackage, but 6K lost on every sketch is a real problem.

I'll take this as-is, with the 6k, and then combine the TUSB weak override for this and the MIDI, HID, and MSD devices in a follow-up.

One request, though, because getting this working is a little complicated. Can you add a small bit of documentation in the docs/ folder with the instructions you've got for Win and Linux? That way there's a way for a user to discover how to set it up without going through a (closed) PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants