New lwIP-based Ethernet library for Teensy 4.1

Don't worry about any of the driver specifics. Most of that will be rewritten and was rushed through just to get it to the point of working. e.g. for the MTU I need a class function like get_mac_address to use a driver_msg to pull it from the device thread, same for max_frame_len except for the latter case the device simply LIES when queried, so instead I need to use init_cmplt->MaxTransferSize, divide by init_cmplt->MaxPacketsPerTransfer and subtract sizeof(rndis_packet)... which unsurprisingly always works out to 1536 for all the devices I have.
 
Last edited:
Gotcha. I also need to fix up some of the MAC-setting logic when the driver doesn't or can't do that.
 
My comments are actually better suited to a PR or code review, but here's a few more of them:
I think it might be a good idea to answer these anyway even though most of the code will be redone, just to check a few things.
1. driver_has_hardware() appears to use link detection. The purpose of this function is to identify whether the hardware is physically attached.
The problem here is that it's a USB/hotpluggable device: it can be present at one time, gone the next, and then back again later - maybe as different hardware with different parameters. It looked like this function was only intended to be a sanity check before calling driver_init().

3. In driver_proc_input(), I'd set an upper bound for the while loop.
Technically there is, once the USB_RNDIS class receives a single block of data from the device (which may contain multiple packets but typically only one) it won't attempt to fetch any more until it's all drained by recv() being called. Ideally I want to pass empty pbufs to USB_RNDIS so it can receive data directly into them to avoid copying, but this will involve some trickery balancing packet lengths with endpoint sizes to make sure there are no overruns.

4. driver_is_unknown() returns whether hardware presence hasn't yet been probed.
There is no "probe" as such; when a USB device matching the RNDIS class is inserted, it gets picked up by the USB_RNDIS instance (via offer() and attach() ). That automatically starts the initialization procedure which retrieves the MAC address, MTU, etc.
So I'm not really sure what this driver function should do; maybe it should return true at any time after a device is connected/disconnected to indicate driver_has_hardware() needs to be rechecked?

5. driver_link_speed() is called by Ethernet.linkSpeed(). Is there no way to get the link speed? If not, I might need to add to the docs.
The link speed is available, I just didn't bother coding it. Although for RNDIS the link speed seems to be more reliable as an indicator of the link state; android always says the link is up if the state is queried directly, but it will return 0 for link speed if it's actually down.
It's also not very accurate i.e. for most implementations it seems to be a hardcoded value rather than having real world significance. The official description of the OID_GEN_LINK_SPEED parameter is "link speed in units of 100 bps". Android returns something like 4.125e6 so it seems to be using the USB bus speed (480mbps) and old windows mobile devices are hardcoded to return 1e5 (10mbps) which would have been a miracle considering most of those used GSM.
There is also a OID_GEN_MAC_OPTIONS parameter that can indicate if the link is full duplex and if the MAC address can be overriden, but I'm pretty sure they will just return hardcoded values on nearly all devices.
 
The joys of Windows CE/mobile:
Initialization complete:
MajorVersion: 00000001
MinorVersion: 00000000
DeviceFlags: 00000001
Medium: 00000000
MaxPacketsPerTransfer: 00000020
MaxTransferSize: 00002000
PacketAlignmentFactor: 00000003
MAC Address: 80:00:60:0F:E8:00
MTU: 8050
MAX_FRAME_LEN: 8064

Link Speed: 100000
Packet filter was set to 0x0F
It's a software link so... I guess the MTU/frame length can just be whatever it feels like. Looks like it's up so let's ask for an IP:
Code:
    Local IP    = 169.254.2.2
    Subnet mask = 255.255.255.0
    Gateway     = 0.0.0.0
    DNS         = 0.0.0.0
*sad trombone noise*
(The device itself has internet access, over wifi... it just doesn't feel like sharing it.)
 
I think having this discussion is great because it exposes new and different use cases. Writing a driver API is hard because there's so many different needs, split up in different ways. Hopefully getting this working well will make the driver API even better and workable for more use cases.

I think it might be a good idea to answer these anyway even though most of the code will be redone, just to check a few things.

The problem here is that it's a USB/hotpluggable device: it can be present at one time, gone the next, and then back again later - maybe as different hardware with different parameters. It looked like this function was only intended to be a sanity check before calling driver_init().

I would say that always returning true here might be the way to go. Here's how I'm thinking about it: I'd argue that the USB host really is always present, which is kind of like a proxy to the things connected to it. Now, initialization might fail if something isn't actually connected, but the hardware might "technically" be there.

The function's (driver_has_hardware()) existence is to be able to determine whether to avoid even attempting to initialize the Ethernet device.

On Teeny 4.1, I don't always return true because a Teensy 4.1 can exist without a PHY chip, either because it was removed, broke off, or it's the no-Ethernet version.

Technically there is, once the USB_RNDIS class receives a single block of data from the device (which may contain multiple packets but typically only one) it won't attempt to fetch any more until it's all drained by recv() being called. Ideally I want to pass empty pbufs to USB_RNDIS so it can receive data directly into them to avoid copying, but this will involve some trickery balancing packet lengths with endpoint sizes to make sure there are no overruns.

I just trust the driver writer to do what's best, and if that's the best, then cool. It's just something I try to do for robustness, to set upper bounds on things. You'll note that in the Teensy 4.1 driver, I only loop up to "RX_SIZE*2" so that other parts of the system get a chance to run too, in a guaranteed way.

There is no "probe" as such; when a USB device matching the RNDIS class is inserted, it gets picked up by the USB_RNDIS instance (via offer() and attach() ). That automatically starts the initialization procedure which retrieves the MAC address, MTU, etc.
So I'm not really sure what this driver function should do; maybe it should return true at any time after a device is connected/disconnected to indicate driver_has_hardware() needs to be rechecked?

In this case, I think returning false, as you have it, is the best choice for this driver.

The link speed is available, I just didn't bother coding it. Although for RNDIS the link speed seems to be more reliable as an indicator of the link state; android always says the link is up if the state is queried directly, but it will return 0 for link speed if it's actually down.
It's also not very accurate i.e. for most implementations it seems to be a hardcoded value rather than having real world significance. The official description of the OID_GEN_LINK_SPEED parameter is "link speed in units of 100 bps". Android returns something like 4.125e6 so it seems to be using the USB bus speed (480mbps) and old windows mobile devices are hardcoded to return 1e5 (10mbps) which would have been a miracle considering most of those used GSM.
There is also a OID_GEN_MAC_OPTIONS parameter that can indicate if the link is full duplex and if the MAC address can be overriden, but I'm pretty sure they will just return hardcoded values on nearly all devices.

The link speed should be the Ethernet link speed and not the USB<->Teensy link speed. (As you know. Driver writers do weird stuff sometimes.) If that's not possible to get, I might add a driver_is_link_speed_detectable() or something. (Along with some other "is detectable/settable" functions like driver_is_mac_settable(), etc.) (But it sounds like the functionality, in theory, should be there.)

MTU: 8050
MAX_FRAME_LEN: 8064
Link Speed: 100000

...

The joys of Windows CE/mobile:

It's a software link so... I guess the MTU/frame length can just be whatever it feels like. Looks like it's up so let's ask for an IP:
Code:
    Local IP    = 169.254.2.2
    Subnet mask = 255.255.255.0
    Gateway     = 0.0.0.0
    DNS         = 0.0.0.0
*sad trombone noise*
(The device itself has internet access, over wifi... it just doesn't feel like sharing it.)

The 14 bytes for MAX_FRAME_LEN on top of the MTU seems right (if not including the 4-byte FCS (frame check sequence)). It just doesn't include the 4-byte 802.1Q tag. 8050 might be the actual MTU, but since that's a few times larger than a USB frame, I'm also curious about that. I'm curious what the datasheet for that USB device says about that.

I'm curious: Does sending/receiving traffic work with a static IP? Note that getting a self-assigned IP may just mean that DHCP isn't being relayed to wherever it needs to get relayed to, or that there isn't a DHCP server. Is there a DHCP server accessible to that network?
 
Last edited:
The link speed should be the Ethernet link speed and not the USB<->Teensy link speed. If that's not possible to get, I might add a driver_is_link_speed_detectable() or something. (Along with some other "is detectable/settable" functions like driver_is_mac_settable(), etc.)
For mobile devices, there technically is no actual Ethernet - it's a virtual network consisting only of two nodes. They use NAT to forward packets to/from their wifi or 3G/4G/5G networks. So the USB bus really is the "link".
The 14 bytes for MAX_FRAME_LEN on top of the MTU seems right (if not including the 4-byte FCS (frame check sequence)). It just doesn't include the 4-byte 802.1Q tag. 8050 might be the actual MTU. I'm curious what the datasheet for that USB device says about that.
It's purely a software implementation, just like the "rndis gadget" that linux/android uses. But MS apparently decided to implement jumbo frames... with a weird size that nothing else uses.
I'm curious: Does sending/receiving traffic work with a static IP? Note that getting a self-assigned IP may just mean that DHCP isn't being relayed to wherever it needs to get relayed to, or that there isn't a DHCP server. Is there a DHCP server accessible to that network?
That's the DHCP lease that the device gives out. It has wifi access but doesn't know how to forward data between USB and that interface, only to the 3G connection which it doesn't have because there's no sim card in it. I think a windows PC (if it's old enough) can still use activesync to connect to certain ports to sync contacts, folders etc.
 
Some questions: In pure RNDIS (i.e. theoretical):
1. Is the MAC address settable?
2. Are link parameters, such as duplex mode and link speed, detectable? (It feels like they are.)

Some posts I've been reading imply that the driver chooses a random value for the MAC address. To me, this means it's settable, in theory.
 
Last edited:
I haven't found a device yet that accepts setting the MAC address. Modern android devices will return a randomized MAC address for "security". Even though the phone has to already be unlocked for the USB connection to enumerate...
There isn't really a clear explanation anywhere why there are separate mandatory OIDs for CURRENT_ADDRESS and PERMANENT_ADDRESS, but no requirement to allow changing the current one.
The documentation for OID_GEN_MAC_OPTIONS says there is a NDIS_MAC_OPTION_SUPPORTS_MAC_ADDRESS_OVERWRITE flag, but I haven't seen anything supporting it. The same OID has a NDIS_MAC_OPTION_FULL_DUPLEX flag but it's marked deprecated.
 
Sounds like I need these:
1. driver_is_mac_settable()
2. driver_is_link_speed_detectable() (have to think about the other parameters such as duplex and crossover)
 
1. Is the MAC address settable?
I just thought of something: you are talking about the main (unicast) MAC address, and not adding multicast addresses like what driver_set_mac_address_allowed() is intended for?
The latter is possible, I just didn't implement it yet because android ignores it; it has a multicast list that can hold one single address which it ignores anyway and passes all multicast traffic through regardless.

(Maybe that function could do with a clearer name, like driver_filter_multicast_address ?)
 
driver_set_mac() is for setting the MAC that makes the device unique on the network. This is the same thing that'll be set as a transmitted Ethernet frame's "source address". driver_set_mac_address_allowed() is for non-promiscuous devices to allow traffic to specific MAC addresses in; eg. for receiving multicast or monitoring. This is the same thing that'll be set in a received Ethernet frame's "destination address".

See lwip_driver.h for more information. If you don't feel the docs in there are sufficient, I can improve them; let me know. I'm also open to naming suggestions for those functions. (No guarantees I'll take the suggestions. :))

Do you still need "driver_is_mac_settable()"?
 
I can see what they're for, I just think it will be a bit confusing to have two functions named "driver_set_mac_address_allowed" and "driver_is_mac_settable".

If I had to implement a driver_is_mac_settable function for RNDIS I would just make it return false, unless I can find a device that supports it and can test firsthand that it's detectable.
 
Understood. I'm glad we're on the same page. (Mostly I just write my responses assuming zero knowledge, especially to be helpful for future readers.) I also hear you on the confusability. I'm in agreement, after staring at this code while tired. I was hoping the docs in lwip_driver.h help.

Regarding driver_is_mac_settable(), yes, that was my thinking too, that you'd just return false for that.
 
It's not pushed yet, but I just renamed `driver_set_mac_address_allowed()` to `driver_set_incoming_mac_address_allowed()`. I also updated some of the docs in lwip_driver.h.
 
Last edited:
When you added the commit to check for an external driver, did you forget a line to #include <qnethernet_external_driver.h> after checking for its existence?
 
@jmarsh When you use the <qnethernet_external_driver.h> file, are you copying it over to the project directory? That is, a project that uses QNEthernet. I just tried to add your driver (inside the Arduino libraries/ folder), first in a "RNDIS_QNEthernet" folder, and then in a "qnethernet_external_driver" folder, and the header wasn't found. In an attempt to get the compiler to notice it, I copied the file into my project. I added a "#pragma message" to where QNEthernet includes <qnethernet_external_driver.h>, and I never see that message.

Could you outline the steps someone needs to do to include an external driver?
 
I have a subdirectory named RNDIS_QNEthernet in my arduino libraries directory, containing the contents of the git repository.
At the top of the sketch I only have #include <RNDIS_QNEthernet.h>, since that also includes the USB Host library and QNEthernet.

The qnethernet_external_driver.h file has to be inside a library in order to get picked up as a "system" include. And that library has to be included by the main sketch before QNEthernet, so that it gets added to the include search list before it.
 
I just released v0.29.0. Here's the Changelog:

Markdown (GitHub flavored):
## [0.29.0]

### Added
* Added protected access to the internal `std::FILE*` stream in the
  `StdioPrint` utility class.
* Added more unit tests:
  * test_ethernet:
    * test_server_zero_port
    * test_server_accept
    * test_server_construct_int_port
* Added `printf` format string checking for `Print`-derived classes. As of this
  writing, Teensyduino (1.59) and other platforms don't do compiler checking
  for `Print::printf`.
* Added more support for `errno`. Appropriate functions will set this after
  encountering an error.
* Added tests for the Arduino-API `begin(...)` functions.
* Added a way to utilize an externally-defined driver.
* Added `driver_is_mac_settable()` to the driver API. This checks if the MAC
  address can be set.

### Changed
* Updated and improved _PixelPusherServer_ example.
* Call `qnethernet_hal_get_system_mac_address(mac)` in the unsupported driver's
  `driver_get_system_mac(mac)` implementation. This enables MAC address
  retrieval for more platforms when communication isn't needed; Teensy 4.0,
  for example.
* Turned the internal MAC address into an optional and simplified the `Ethernet`
  constructor. This change should make it easier to initialze a MAC address from
  a custom driver.
* Changed Arduino-API non-DHCP `begin(...)` functions to return `bool`.
* Improved driver logic so that lwIP options can be included in the
  driver headers.
* Improved `OSCPrinter` example to use the UDP data directly.
* Renamed `driver_set_mac_address_allowed()` to
  `driver_set_incoming_mac_address_allowed()`.
* Changed `driver_proc_input(netif)` to return a `pbuf*`.

### Fixed
* Fixed `EthernetServer::port()` to return the system-chosen port if a zero
  value was specified.
* Fixed `EthernetUDP::stop()` to leave any multicast group joined when starting
  to listen on a multicast address.
* Fixed MAC address restore if an Arduino-API non-DHCP `begin(...)` call fails.
* Fixed `EthernetClient::read()` and `peek()` to return -1 instead of 0 when
  there's no internal state.
* Properly initializing multicast filtering so that it happens before
  `igmp_start()`, which sets up the all-systems group.

Highlights:
* Added a way to add an externally-defined driver
* Fixed receiving multicast

Link: https://github.com/ssilverman/QNEthernet/releases/tag/v0.29.0
 
Wow, you guys really keep at it. Thanks

Installed v0.29 over 28 just now using the automatic update in Arduino 2.3.2. Having an issue where replies from a teensy 4.1 server are taking 4-5 seconds for some reason. Went back to v0.28 and all is well again with replies seemingly instantaneous.
 
Wow, you guys really keep at it. Thanks

Installed v0.29 over 28 just now using the automatic update in Arduino 2.3.2. Having an issue where replies from a teensy 4.1 server are taking 4-5 seconds for some reason. Went back to v0.28 and all is well again with replies seemingly instantaneous.

Thanks for the note. I’ll have a look when I have a chance to see what might have affected that.
 
Back
Top