New lwIP-based Ethernet library for Teensy 4.1

I just released v0.28.0. Here's the Changelog:
Markdown (GitHub flavored):
## [0.28.0]

### Added
* Added raw frame loopback support and a `QNETHERNET_ENABLE_RAW_FRAME_LOOPBACK`
  macro to enable.
* Added a fourth step to the _MbedTLSDemo_ example instructions: modify
  the config.
* New `EthernetFrameClass` functions: `destinationMAC()`, `sourceMAC()`,
  `etherTypeOrLength()`, and `payload()`.
* Consolidated all the hardware abstraction layer (HAL) functions into one
  place: `qnethernet_hal.cpp`.
* New `NullPrint` and `PrintDecorator` utility classes in the
  `qindesign::network::util` namespace.
* Added `driver_is_link_state_detectable()` function to the driver. This is for
  detecting whether the hardware is able to read the link state.
* Added `EthernetClass::isLinkStateDetectable()` to detect whether the driver is
  capable of detecting link state.
* Added `setOutgoingDiffServ(ds)` and `outgoingDiffServ()` functions for
  modifying and accessing the differentiated services (DiffServ) field,
  respectively, in the outgoing IP header, to `EthernetClient`
  and `EthernetUDP`.
* Added `EthernetUDP::receivedDiffServ()` for retrieving the DiffServ value of
  the last received packet.
* Added `EthernetFrameClass::clear()` for clearing the outgoing and
  incoming buffers.

### Changed
* Updated the Mbed TLS version in the README and comments to 2.28.8
  (was 2.28.6).
* Updated `mbedtls_hardware_poll()` in _MbedTLSDemo_ example for
  other platforms.
* Renamed `QNETHERNET_ENABLE_CUSTOM_WRITE` to `QNETHERNET_CUSTOM_WRITE`.
* Improved the _PixelPusherServer_ example.
* The address-changed callback is now called for independent IP address,
  netmask, and gateway changes.
* Improved link function documentation for `linkStatus()`, `linkState()`,
  and `isLinkStateDetectable()`.
* Updated `EthernetClient::setNoDelay(flag)` to return whether successful.
* Add another 2 to `MEMP_NUM_SYS_TIMEOUT` option for mDNS, for a total of an
  additional 8. Timeout exhaustion was still observed with 6. Why 8 and not 7:
  * https://lists.nongnu.org/archive/html/lwip-users/2024-05/msg00000.html
  * https://savannah.nongnu.org/patch/?9523#comment18
* Updated `EthernetClient::connect()` to return a Boolean value. (The function
  signatures don't change; they still return an `int`.) This matches the new
  definition at
  [Ethernet - client.connect()](https://www.arduino.cc/reference/en/libraries/ethernet/client.connect/).
* Changed `EthernetClient::connectNoWait()` return types to `bool`.

### Fixed
* Improved marking of unused parameters.
* Fixed up use of `__has_include()`, per:
  [__has_include (The C Preprocessor)](https://gcc.gnu.org/onlinedocs/cpp/_005f_005fhas_005finclude.html)
* In mDNS, ensure there's at least an empty TXT record, otherwise the SRV record
  doesn't appear.
* Make the alternative `yield()` implementation `extern "C"`.
* Fixed `EthernetUDP` data copy for zero-length outgoing packets. `pbuf_take()`
  considers NULL data an error, so only copy the data if the packet's size is
  not zero.

Highlights:
* DiffServ field support
* Address-changed callback now called for every change
* EthernetClient::connect() now returns a Boolean value (but still returns an int type), to match the new Arduino doc change
* Fixed mDNS responses

Link: https://github.com/ssilverman/QNEthernet/releases/tag/v0.28.0
 
I'm trying to write a new driver and running into a few issues.
It's a USB based device (actually not one specific device but any RNDIS class interface) so may not be present when the EthernetClass object is initialized. This causes several problems:
- the MAC address isn't known until the device is plugged in and initialized. Assigning a MAC address to the adapter isn't possible; you have to use what the RNDIS gives you (and in the case of android devices, it's randomised each time). This can also cause a static initialization order issue (since my driver is an object and may get created after the static EthernetClass) but I at least have a workaround for that.
- the MTU and max frame length aren't fixed values. I can query for them so I know what they are, but they can vary between devices and can't be hardcoded.

The MAC address issue is the main showstopper, since ideally the library would transition to the point where it's waiting for the link to come up (calling driver_poll()) and just sit there until the USB device was ready to go. But it can't get to that point without knowing the correct MAC.
 
I can certainly help with this. First, I don’t love the Arduino-functions-are-members-of-a-singleton nonsense. I’ve already started experimenting with turning everything in a singleton into a static function.

You can set a MAC address with Ethernet.setMACAddress(), if that helps any, but it would be nicer to do this at the driver level. I’ll see what I can do.

Let me think about how to make MTU and max. frame size non-constants.
 
Last edited:
I should probably also mention the hacky way I'm making it use my driver without modifying any of the QNEthernet source: I put "dot_a_linkage=true" in the library properties and provide my own versions of all the driver functions. So the library thinks its using the Teensy41 driver, but that object file gets dropped by the linker because all of its external functions have already been provided by my own code.
 
Sooo I have a couple of comments...
Firstly there's this: https://github.com/ssilverman/QNEth...9156fc9a19125f0ded26e2495/src/lwipopts.h#L116
Code:
#if !defined(QNETHERNET_DRIVER_W5500)
#define ETH_PAD_SIZE                  2  /* 0 */
#endif  // !defined(QNETHERNET_DRIVER_W5500)
There is some reference to this setting in the documentation, but the way it's guarded with !defined(QNETHERNET_DRIVER_W5500) means it will be defined for all other drivers... seems it would be better to guard using the driver that explicitly wants it (TEENSY41).

Likewise there is this: https://github.com/ssilverman/QNEth...9156fc9a19125f0ded26e2495/src/lwipopts.h#L377
Code:
#if !defined(QNETHERNET_DRIVER_W5500)
#define CHECKSUM_GEN_IP              0  /* 1 */
#define CHECKSUM_GEN_UDP             0  /* 1 */
#define CHECKSUM_GEN_TCP             0  /* 1 */
#define CHECKSUM_GEN_ICMP            0  /* 1 */
// #define CHECKSUM_GEN_ICMP6           1
#define CHECKSUM_CHECK_IP            0  /* 1 */
#define CHECKSUM_CHECK_UDP           0  /* 1 */
#define CHECKSUM_CHECK_TCP           0  /* 1 */
#define CHECKSUM_CHECK_ICMP          0  /* 1 */
// #define CHECKSUM_CHECK_ICMP6         1
#endif  // !defined(QNETHERNET_DRIVER_W5500)
This really tripped me up, and I ended up dumping the entire contents of the DHCP DISCOVER packets to try and figure out why the device was ignoring them. As soon as the checksums were enabled it all started working...
Code:
control response: 52 80000002
Initialization complete:
    MajorVersion: 00000001
    MinorVersion: 00000000
    DeviceFlags: 00000001
    Medium: 00000000
    MaxPacketsPerTransfer: 00000003
    MaxTransferSize: 00001284
    PacketAlignmentFactor: 00000000
control response: 30 80000004
MAC Address: 02:50:50:56:31:61
control response: 20 00000007
RNDIS Indicate_Status: MEDIA_CONNECT
control response: 28 80000004
MTU: 1500
control response: 28 80000004
MAX_FRAME_LEN: 1558
control response: 16 80000005
control response: 28 80000004
Packet filter was set to 0x0F
control response: 28 80000004
Link Speed: 4259840
RNDIS device is initialized
RNDIS thread drivermsg: 4
MAC = 02:50:50:56:31:61
Starting Ethernet with DHCP...
driver_has_hardware
driver_init
Setting link up
    Local IP    = 192.168.42.4
    Subnet mask = 255.255.255.0
    Gateway     = 192.168.42.129
    DNS         = 192.168.42.129
Sending SNTP request to time.google.com...
SNTP reply: 2024-06-25 18:32:22

Apparently TP-LINK UE300 gigabit ethernet adapters (they are USB3 but obviously backwards compatible with USB2) use RNDIS, I would like to get hold of one and see how the speed compares to the onboard 100mbps ethernet...
 
Last edited:
@jmarsh, first, I agree with your points. And second, I’m glad you’re writing a driver for something I haven’t tried yet. I would have gotten there and seen these hiccups, but having other people do this exposes them faster. So thank you for trying this out.

A bit about my thinking: My approach to this stuff in the QNEthernet library has so far been to make changes and design decisions when I need to, and to not over-design. Thus far, I haven’t backed myself into a corner, and any changes I’ve made to accommodate these “hiccups” have gone smoothly. :)
This approach lets me be more dynamic and respond to changes faster, while still keeping, I think, a reasonable design. (That’s the balance: one has to have _some_ sort of small framework; the trick is knowing how big or small to impose that; there isn’t a right answer and all projects are different.)

I’ve already made some changes that should address the MAC address issues. I’ll be pushing that probably tonight (my time). Upcoming are changes that will help the constant-ness of MTU and MAX_FRAME_LEN.

Point number 6 in lwip_driver.h says, “Update lwipopts.h with appropriate values for your driver.” I acknowledge that might need to be more specific, but I was hoping people would see the “QNETHERNET_DRIVER_XXX” checks in lwipopts.h and change things as appropriate. I’ll point this out in some updated instructions.

Thanks again. I really appreciate the experience you’re bringing here. Pull requests are welcome.
 
I also have some ideas to improve the situation for additional drivers and appropriate options in lwipopts.h.
 
Would it be possible to use PBUF_LINK_ENCAPSULATION_HLEN instead of ETH_SIZE?
Can you explain further? Do you mean ETH_PAD_SIZE? Those two options are for different purposes. Don’t forget you can always modify lwipopts.h. (And tell me what you needed to change and why.)
 
To clarify: PBUF_LINK_ENCAPSULATION_HLEN and ETH_PAD_SIZE operate at different layers. ETH_PAD_SIZE is at the raw incoming bytes layer that performs a certain optimization with the PHY and/or CPU registers and internal frame processing, below the lwIP layer, while PBUF_LINK_ENCAPSULATION_HLEN is for when the packet has already been received and it needs to be analyzed.

What are you trying to do, and what are you trying to get around?

Note: I just pushed some changes to both the `master` and `no-driver-constants` branches.

Update: I just thought of something: Are you trying to avoid having to modify the QNEthernet library at all? I'm not sure it's quite ready to add drivers to it without modification. I'll need a few more driver types for that to better understand it.
 
Last edited:
Yes I meant ETH_PAD_SIZE.
PBUF_LINK_ENCAPSULATION_HLEN is for when the packet has already been received and it needs to be analyzed.
I'm not sure that's accurate? It's used for layer PBUF_RAW_TX (and upwards), with the documentation stating that it's intended for buffers that will be transmitted so extra headers can be prepended to them. Which is what I would like to use it for, to add space for an RNDIS packet header.

I thought it would simplify things if one setting could be used for both purposes, but ETH_PAD_SIZE is better for the Teensy4.1 driver.
Update: I just thought of something: Are you trying to avoid having to modify the QNEthernet library at all? I'm not sure it's quite ready to add drivers to it without modification. I'll need a few more driver types for that to better understand it.
I could get away with it if the default driver (Teensy4.1) didn't disable the checksums, and the library gets built with dot_a_linkage. But it's still ugly and sub-optimal - I want to be able to use my own options like above. The inability to use anything but the default driver without modifying core files (to be able to override global definitions) is annoying though.

I did come up with this, to move driver specific options into their own files rather than clogging up lwipopts.h with #ifdefs: https://github.com/A-Dunstan/QNEthernet/commit/52b35792cbf1b26dd9ecc3476c35d11a38ddc76d
 
Yes I meant ETH_PAD_SIZE.

I'm not sure that's accurate? It's used for layer PBUF_RAW_TX (and upwards), with the documentation stating that it's intended for buffers that will be transmitted so extra headers can be prepended to them. Which is what I would like to use it for, to add space for an RNDIS packet header.

Whoops. You're right. It's for sending. But it's still not related to ETH_PAD_SIZE.

I thought it would simplify things if one setting could be used for both purposes, but ETH_PAD_SIZE is better for the Teensy4.1 driver.

It's not that it's just better, it provides a speed improvement so that frames are aligned on a 32-bit boundary when being received by the CPU's MAC. Other PHYs and "frame processors" (eg. USB Ethernet or the W5500 chip) don't always have this.

I could get away with it if the default driver (Teensy4.1) didn't disable the checksums, and the library gets built with dot_a_linkage. But it's still ugly and sub-optimal - I want to be able to use my own options like above. The inability to use anything but the default driver without modifying core files (to be able to override global definitions) is annoying though.

I updated the logic (fixed, actually) in lwipopts.h to match the driver choice logic in lwip_driver.h. If you add a new driver type and modify lwipopts.h per the current instructions in lwip_driver.h, then you won't see this issue.

I actually don't consider lwipopts.h a "core" file. I view more of a "between core and non-core" file because I expect advanced users to modify it, especially driver writers.

As I implied before, once this driver is working as you like, I'll make an effort to make sure it's easier to incorporate, even including it with the distribution if you like.

I did come up with this, to move driver specific options into their own files rather than clogging up lwipopts.h with #ifdefs: https://github.com/A-Dunstan/QNEthernet/commit/52b35792cbf1b26dd9ecc3476c35d11a38ddc76d

I'll have a look...
 
I did come up with this, to move driver specific options into their own files rather than clogging up lwipopts.h with #ifdefs: https://github.com/A-Dunstan/QNEthernet/commit/52b35792cbf1b26dd9ecc3476c35d11a38ddc76d

I like the direction of your thinking here. Let me ponder the ideas you present. The specific difficulty with the way you have it currently is that the lwipopts.h file is included inside the lwIP code itself, and the changes you make in the driver header won't necessarily propagate to the stack.
 
I like the direction of your thinking here. Let me ponder the ideas you present. The specific difficulty with the way you have it currently is that the lwipopts.h file is included inside the lwIP code itself, and the changes you make in the driver header won't necessarily propagate to the stack.
I don't see how they can't - lwipopts.h includes qnethernet_opts.h, which will include the appropriate driver_XXX.h file. The driver is specified in either qnethernet_opts.h itself or via a command line definition, so how can it fail?
 
May I suggest running the test suite with different drivers and systems? This way you know the defaults still work, in addition to your new stuff.

In PlatformIO, the command is: pio test -v -e teensy41-test -f test_ethernet

Note that the teensy41-test environment is defined in the platformio.ini file. You can select other environments (or omit to run all of them) and/or modify or add new ones as you see fit to test with different options. For example, to test with the W5500 driver, add -DQNETHERNET_DRIVER_W5500 to the teensy41-test environment.

The Ethernet test source is in test/test_ethernet/test_main.cpp.
 
I don't see how they can't - lwipopts.h includes qnethernet_opts.h, which will include the appropriate driver_XXX.h file.
So it does... my energy is low at the moment. I'm going to think about your proposal some more.
 
I actually don't consider lwipopts.h a "core" file. I view more of a "between core and non-core" file because I expect advanced users to modify it, especially driver writers.
What I meant was even if my driver gets added to the library, users won't be able to just load up a sketch in Arduino IDE to use it - it has to be selected at compile time which means either modifying the library, or the Teensyduino platform files to allow overriding flags.
 
What I meant was even if my driver gets added to the library, users won't be able to just load up a sketch in Arduino IDE to use it - it has to be selected at compile time which means either modifying the library, or the Teensyduino platform files to allow overriding flags.

True. They'd only have to modify one thing in qnethernet_opts.h, though. (Or modify build options, of course, with some custom platform.local.txt[*], etc.). This is one of the reasons I added that qnethernet_opts.h file. But this should be a compile-time setting, in my opinion.

Were you thinking of having them modify the library.properties file with the above-mentinoed "dot_a_linkage=true" change? In both cases, the user still needs to modify at least one file.
 
Last edited:
Were you thinking of having them modify the library.properties file with the above-mentinoed "dot_a_linkage=true" change? In both cases, the user still needs to modify at least one file.
No, because that's not going to work when the Teensy4.1 driver defaults to disabling checksums. I can replace all the driver functions with my own variants but it won't work without checksum support compiled in.

True. They'd only have to modify one thing in qnethernet_opts.h, though. (Or modify build options, of course, with some custom platform.local.txt[*], etc.). This is one of the reasons I added that qnethernet_opts.h file. But this should be a compile-time setting, in my opinion.
What if the #include statement for qnethernet_opts.h was changed to a system include i.e. used < > instead of " " ? It should still work with Arduino because it adds the library's src dir to the include path, but if another library with its own options file had already been picked up it could potentially override it?
So you could potentially have external drivers in their own libraries, and the main sketch would #include those before qnethernet.
 
Would be cool if that worked. Arduino gets weird and restrictive sometimes. If you decide to experiment with that, I’d love to hear about any results. Shoehorning this dynamism into the Arduino system can be a challenge.
 
I haven't tried the include solution you propose, but here's some things of note:
1. In https://en.cppreference.com/w/cpp/preprocessor/include, it states "Searches a sequence of implementation-defined places for a header"
2. GCC's search path: https://gcc.gnu.org/onlinedocs/cpp/Search-Path.html

It's possible this will work, but I hesitate to rely on both implementation-defined behaviour and whatever Arduino is doing. It's possible it will work one day, things change, and then the user needs to modify a file or two again. This means that if ever that happens, the library will only work for one of those ways, especially since it takes a while for people to upgrade or switch versions, and we're back to the original problem.

My current priorities are to support first "library-hood" with standard practices, and second, Arduino, favouring the former if there's a conflict.
 
Last edited:
I don't see how they can't - lwipopts.h includes qnethernet_opts.h, which will include the appropriate driver_XXX.h file. The driver is specified in either qnethernet_opts.h itself or via a command line definition, so how can it fail?
Implemented the options-in-driver-header thing. Thanks for the suggestion. See the latest push to `master`.
 
I came up with something a little bit better, maybe it will change your mind. There is a working proof-of-concept, here's all the requirements:
- The QNEthernet library, using the no-driver-constants branch with one additional change:
Code:
@@ -6,11 +6,13 @@
 
 #pragma once
 
 #include "qnethernet_opts.h"
 
-#if defined(QNETHERNET_DRIVER_W5500)
+#if defined __has_include && __has_include(<qnethernet_external_driver.h>)
+#include <qnethernet_external_driver.h>
+#elif defined(QNETHERNET_DRIVER_W5500)
 #include "drivers/driver_w5500.h"
 #define QNETHERNET_INTERNAL_DRIVER_W5500
 #elif defined(ARDUINO_TEENSY41)
 #include "drivers/driver_teensy41.h"
 #define QNETHERNET_INTERNAL_DRIVER_TEENSY41
- The teensy4_usbhost library
- A library I made just for the external driver

Inside the USBHost library under examples/network/rndis, build rndis.ino using Arduino. It should pick up the external driver rather than any of the internal ones (you can tell because the compiler spits out "#pragma message: Using RNDIS QNEthernet Driver"). If you connect an android phone to the Teensy's USB host port and activate USB tethering, it should fetch the time using Google's NTP.
Using __has_include will prevent Arduino from "gathering" a library that contains <qnethernet_external_driver.h> because it doesn't throw a compiler error. It will only be used in the case where the sketch itself (or a previously linked library) uses a library that contains it, and the #include for that library occurs before any QNEthernet #include statements.

The include search order is implementation-defined, meaning the compiler defines it rather than the c++ standard. For gcc (and friends) that definition is "sequentially search the include directories list followed by some system-specific dirs", which is the behaviour that loads of code relies upon so it's not likely to change any time soon. If it does we may as well give up completely because the method Arduino uses to link libraries on-demand will break down.
 
I came up with something a little bit better, maybe it will change your mind.

I like it. Let me think about the vagaries.

I have some comments and notes about the driver itself, but one initial thing I noticed was that the MTU and max-frame-len values are constants.

My comments are actually better suited to a PR or code review, but here's a few more of them:
1. driver_has_hardware() appears to use link detection. The purpose of this function is to identify whether the hardware is physically attached.
2. I should probably have driver_set_mac() return a bool, especially if setting the MAC isn't possible.
3. In driver_proc_input(), I'd set an upper bound for the while loop.
4. driver_is_unknown() returns whether hardware presence hasn't yet been probed.
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.
 
Back
Top