Add USB NCM link state control support

This adds the ability to dynamically control the network link state
for NCM devices. The host OS will see the network interface as
connected/disconnected based on the link state.

New API:
- tud_network_link_state(rhport, is_up): Set link up/down state

Example updates:
- Added button control to toggle link state
- Fixed LWIP integration to properly handle link state changes
- Added printf to show correct protocol (NCM vs RNDIS/ECM)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Andrew Leech
2025-05-27 15:06:05 +10:00
parent 8f077f9295
commit 5de4a23abe
5 changed files with 107 additions and 10 deletions

View File

@@ -58,6 +58,7 @@
#define LWIP_HTTPD_SSI_INCLUDE_TAG 0 #define LWIP_HTTPD_SSI_INCLUDE_TAG 0
#define LWIP_SINGLE_NETIF 1 #define LWIP_SINGLE_NETIF 1
#define LWIP_NETIF_LINK_CALLBACK 1
#define PBUF_POOL_SIZE 4 #define PBUF_POOL_SIZE 4

View File

@@ -31,6 +31,12 @@ this appears as either a RNDIS or CDC-ECM USB virtual network adapter; the OS pi
RNDIS should be valid on Linux and Windows hosts, and CDC-ECM should be valid on Linux and macOS hosts RNDIS should be valid on Linux and Windows hosts, and CDC-ECM should be valid on Linux and macOS hosts
The MCU appears to the host as IP address 192.168.7.1, and provides a DHCP server, DNS server, and web server. The MCU appears to the host as IP address 192.168.7.1, and provides a DHCP server, DNS server, and web server.
Link State Control:
- Press the user button to toggle the network link state (UP/DOWN)
- This simulates "ethernet cable unplugged/plugged" events
- The host OS will see the network interface as disconnected/connected accordingly
- Use this to test network error handling and recovery in host applications
*/ */
/* /*
Some smartphones *may* work with this implementation as well, but likely have limited (broken) drivers, Some smartphones *may* work with this implementation as well, but likely have limited (broken) drivers,
@@ -137,6 +143,12 @@ static err_t netif_init_cb(struct netif *netif) {
return ERR_OK; return ERR_OK;
} }
/* notifies the USB host about the link state change. */
static void usbnet_netif_link_callback(struct netif *netif) {
bool link_up = netif_is_link_up(netif);
tud_network_link_state(BOARD_TUD_RHPORT, link_up);
}
static void init_lwip(void) { static void init_lwip(void) {
struct netif *netif = &netif_data; struct netif *netif = &netif_data;
@@ -147,11 +159,19 @@ static void init_lwip(void) {
memcpy(netif->hwaddr, tud_network_mac_address, sizeof(tud_network_mac_address)); memcpy(netif->hwaddr, tud_network_mac_address, sizeof(tud_network_mac_address));
netif->hwaddr[5] ^= 0x01; netif->hwaddr[5] ^= 0x01;
netif = netif_add(netif, &ipaddr, &netmask, &gateway, NULL, netif_init_cb, ip_input); netif = netif_add(netif, &ipaddr, &netmask, &gateway, NULL, netif_init_cb, ethernet_input);
#if LWIP_IPV6 #if LWIP_IPV6
netif_create_ip6_linklocal_address(netif, 1); netif_create_ip6_linklocal_address(netif, 1);
#endif #endif
netif_set_default(netif); netif_set_default(netif);
#if LWIP_NETIF_LINK_CALLBACK
// Set the link callback to notify USB host about link state changes
netif_set_link_callback(netif, usbnet_netif_link_callback);
netif_set_link_up(netif);
#else
tud_network_link_state(BOARD_TUD_RHPORT, true);
#endif
} }
/* handle any DNS requests from dns-server */ /* handle any DNS requests from dns-server */
@@ -171,13 +191,16 @@ bool tud_network_recv_cb(const uint8_t *src, uint16_t size) {
if (size) { if (size) {
struct pbuf *p = pbuf_alloc(PBUF_RAW, size, PBUF_POOL); struct pbuf *p = pbuf_alloc(PBUF_RAW, size, PBUF_POOL);
if (p) { if (p == NULL) {
/* pbuf_alloc() has already initialized struct; all we need to do is copy the data */ printf("ERROR: Failed to allocate pbuf of size %d\n", size);
memcpy(p->payload, src, size); return false;
/* store away the pointer for service_traffic() to later handle */
received_frame = p;
} }
/* Copy buf to pbuf */
pbuf_take(p, src, size);
/* store away the pointer for service_traffic() to later handle */
received_frame = p;
} }
return true; return true;
@@ -194,12 +217,14 @@ uint16_t tud_network_xmit_cb(uint8_t *dst, void *ref, uint16_t arg) {
static void service_traffic(void) { static void service_traffic(void) {
/* handle any packet received by tud_network_recv_cb() */ /* handle any packet received by tud_network_recv_cb() */
if (received_frame) { if (received_frame) {
struct netif *netif = &netif_data;
// Surrender ownership of our pbuf unless there was an error // Surrender ownership of our pbuf unless there was an error
// Only call pbuf_free if not Ok else it will panic with "pbuf_free: p->ref > 0" // Only call pbuf_free if not Ok else it will panic with "pbuf_free: p->ref > 0"
// or steal it from whatever took ownership of it with undefined consequences. // or steal it from whatever took ownership of it with undefined consequences.
// See: https://savannah.nongnu.org/patch/index.php?10121 // See: https://savannah.nongnu.org/patch/index.php?10121
if (ethernet_input(received_frame, &netif_data)!=ERR_OK) { if (netif->input(received_frame, netif) != ERR_OK) {
pbuf_free(received_frame); printf("ERROR: netif input failed\n");
pbuf_free(received_frame);
} }
received_frame = NULL; received_frame = NULL;
tud_network_recv_renew(); tud_network_recv_renew();
@@ -216,6 +241,28 @@ void tud_network_init_cb(void) {
} }
} }
static void handle_link_state_switch(void) {
/* Check for button press to toggle link state */
static bool last_link_state = true;
static bool last_button_state = false;
bool current_button_state = board_button_read();
if (current_button_state && !last_button_state) {
/* Button pressed - toggle link state */
last_link_state = !last_link_state;
if (last_link_state) {
printf("Link state: UP\n");
netif_set_link_up(&netif_data);
} else {
printf("Link state: DOWN\n");
netif_set_link_down(&netif_data);
}
/* LWIP callback will notify USB host about the change */
}
last_button_state = current_button_state;
}
int main(void) { int main(void) {
/* initialize TinyUSB */ /* initialize TinyUSB */
board_init(); board_init();
@@ -243,15 +290,23 @@ int main(void) {
lwiperf_start_tcp_server_default(NULL, NULL); lwiperf_start_tcp_server_default(NULL, NULL);
#endif #endif
#if CFG_TUD_NCM
printf("USB NCM network interface initialized\n");
#elif CFG_TUD_ECM_RNDIS
printf("USB RNDIS/ECM network interface initialized\n");
#endif
while (1) { while (1) {
tud_task(); tud_task();
service_traffic(); service_traffic();
handle_link_state_switch();
} }
return 0; return 0;
} }
/* lwip has provision for using a mutex, when applicable */ /* lwip has provision for using a mutex, when applicable */
/* This implementation is for single-threaded use only */
sys_prot_t sys_arch_protect(void) { sys_prot_t sys_arch_protect(void) {
return 0; return 0;
} }

View File

@@ -85,6 +85,7 @@ extern "C" {
#endif #endif
// Use different configurations to test all net devices (also due to resource limitations) // Use different configurations to test all net devices (also due to resource limitations)
#ifndef USE_ECM
#if TU_CHECK_MCU(OPT_MCU_LPC15XX, OPT_MCU_LPC40XX, OPT_MCU_LPC51UXX, OPT_MCU_LPC54) #if TU_CHECK_MCU(OPT_MCU_LPC15XX, OPT_MCU_LPC40XX, OPT_MCU_LPC51UXX, OPT_MCU_LPC54)
#define USE_ECM 1 #define USE_ECM 1
#elif TU_CHECK_MCU(OPT_MCU_SAMD21, OPT_MCU_SAML21, OPT_MCU_SAML22) #elif TU_CHECK_MCU(OPT_MCU_SAMD21, OPT_MCU_SAML21, OPT_MCU_SAML22)
@@ -97,6 +98,7 @@ extern "C" {
#define USE_ECM 0 #define USE_ECM 0
#define INCLUDE_IPERF #define INCLUDE_IPERF
#endif #endif
#endif
//-------------------------------------------------------------------- //--------------------------------------------------------------------
// NCM CLASS CONFIGURATION, SEE "ncm.h" FOR PERFORMANCE TUNING // NCM CLASS CONFIGURATION, SEE "ncm.h" FOR PERFORMANCE TUNING

View File

@@ -110,6 +110,7 @@ typedef struct {
NOTIFICATION_DONE NOTIFICATION_DONE
} notification_xmit_state; // state of notification transmission } notification_xmit_state; // state of notification transmission
bool notification_xmit_is_running; // notification is currently transmitted bool notification_xmit_is_running; // notification is currently transmitted
bool link_is_up; // current link state
// misc // misc
bool tud_network_recv_renew_active; // tud_network_recv_renew() is active (avoid recursive invocations) bool tud_network_recv_renew_active; // tud_network_recv_renew() is active (avoid recursive invocations)
@@ -218,7 +219,7 @@ static void notification_xmit(uint8_t rhport, bool force_next) {
.direction = TUSB_DIR_IN .direction = TUSB_DIR_IN
}, },
.bRequest = CDC_NOTIF_NETWORK_CONNECTION, .bRequest = CDC_NOTIF_NETWORK_CONNECTION,
.wValue = 1 /* Connected */, .wValue = ncm_interface.link_is_up ? 1 : 0, /* Dynamic link state */
.wIndex = ncm_interface.itf_num, .wIndex = ncm_interface.itf_num,
.wLength = 0, .wLength = 0,
}, },
@@ -232,6 +233,7 @@ static void notification_xmit(uint8_t rhport, bool force_next) {
ncm_interface.notification_xmit_is_running = true; ncm_interface.notification_xmit_is_running = true;
} else { } else {
TU_LOG_DRV(" NOTIFICATION_FINISHED\n"); TU_LOG_DRV(" NOTIFICATION_FINISHED\n");
ncm_interface.notification_xmit_is_running = false;
} }
} // notification_xmit } // notification_xmit
@@ -755,6 +757,32 @@ static void tud_network_recv_renew_r(uint8_t rhport) {
tud_network_recv_renew(); tud_network_recv_renew();
} // tud_network_recv_renew } // tud_network_recv_renew
/**
* Set the link state and send notification to host
*/
void tud_network_link_state(uint8_t rhport, bool is_up) {
TU_LOG_DRV("tud_network_link_state(%d, %d)\n", rhport, is_up);
if (ncm_interface.link_is_up == is_up) {
// No change in link state
return;
}
ncm_interface.link_is_up = is_up;
// Only send notification if we have an active data interface
if (ncm_interface.itf_data_alt != 1) {
TU_LOG_DRV(" link state notification skipped (interface not active)\n");
return;
}
// Reset notification state to send link state update
ncm_interface.notification_xmit_state = NOTIFICATION_CONNECTED;
// Trigger notification transmission
notification_xmit(rhport, false);
}
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// //
// all the netd_*() stuff (interface TinyUSB -> driver) // all the netd_*() stuff (interface TinyUSB -> driver)
@@ -774,6 +802,12 @@ void netd_init(void) {
for (int i = 0; i < RECV_NTB_N; ++i) { for (int i = 0; i < RECV_NTB_N; ++i) {
ncm_interface.recv_free_ntb[i] = &ncm_epbuf.recv[i].ntb; ncm_interface.recv_free_ntb[i] = &ncm_epbuf.recv[i].ntb;
} }
// Default link state - can be configured via CFG_TUD_NCM_DEFAULT_LINK_UP
#ifdef CFG_TUD_NCM_DEFAULT_LINK_UP
ncm_interface.link_is_up = CFG_TUD_NCM_DEFAULT_LINK_UP;
#else
ncm_interface.link_is_up = true; // Default to link up if not set.
#endif
} // netd_init } // netd_init
/** /**

View File

@@ -87,6 +87,11 @@ void tud_network_init_cb(void);
// TODO removed later since it is not part of tinyusb stack // TODO removed later since it is not part of tinyusb stack
extern uint8_t tud_network_mac_address[6]; extern uint8_t tud_network_mac_address[6];
//------------- NCM -------------//
// Set the network link state (up/down) and notify the host
void tud_network_link_state(uint8_t rhport, bool is_up);
//--------------------------------------------------------------------+ //--------------------------------------------------------------------+
// INTERNAL USBD-CLASS DRIVER API // INTERNAL USBD-CLASS DRIVER API
//--------------------------------------------------------------------+ //--------------------------------------------------------------------+