nuttx/Documentation/components/net/netdriver.rst
raul_chen 0d2993dd87 Documentation/net: document lower-half driver performance tuning
Add a "Performance tuning" section to the network driver guide covering the
knobs that matter most for throughput on Wi-Fi lower-half drivers whose MAC
runs on a companion core: RX quota as backpressure, keeping the RX thread
priority at or below the vendor packet-delivery task, and capping the TCP
window / send buffer (backed by the shared IOB pool) on lossy wireless paths.

Signed-off-by: raul_chen <raul_chen@realsil.com.cn>
2026-07-13 11:53:26 +02:00

286 lines
11 KiB
ReStructuredText

.. _netdriver:
===============
Network Drivers
===============
The NuttX network driver is split into two parts:
#. An "upper half", generic driver that provides the common network
interface to application level code, and
#. A "lower half", platform-specific driver that implements the
low-level timer controls to implement the network functionality.
Files supporting network driver can be found in the following locations:
- **Interface Definition**. The header file for the NuttX network
driver resides at ``include/nuttx/net/netdev_lowerhalf.h``. This
header file includes the interface between the "upper half" and
"lower half" drivers.
- **"Upper Half" Driver**. The generic, "upper half" network driver
resides at ``drivers/net/netdev_upperhalf.c``.
- **"Lower Half" Drivers**. Platform-specific network drivers reside
in ``arch/<architecture>/src/<hardware>`` or ``drivers/net``
directory for the specific processor ``<architecture>`` and for
the specific ``<chip>`` network peripheral devices.
**Special Note**: Not all network drivers are implemented with this
architecture. Known lower-half drivers:
``arch/sim/src/sim/sim_netdriver.c``, ``drivers/virtio/virtio-net.c``
How to change full network driver into lower-half one
=====================================================
We have many network drivers that are implemented as full network drivers
with ``include/nuttx/net/netdev.h``, we can change them into lower-half
drivers to remove the common code (which is already in upper-half driver).
Here is a guide to do so:
1. Change ``struct net_driver_s`` to ``struct netdev_lowerhalf_s`` in
the network driver structure. If you really need to touch some fields
inside ``struct net_driver_s`` like MAC address, you can access them
through ``struct netdev_lowerhalf_s::netdev``.
2. Change the function names called in the network driver file to the names
with prefix ``netdev_lower_``, e.g. ``netdev_lower_register`` and
``netdev_lower_carrier_on``.
3. Change the core functions called by work queue like ``txpoll`` as
``transmit`` and ``receive`` in the ``netdev_ops_s`` structure. You may
need to change ``memcpy`` for ``d_buf`` into ``netpkt_copyin`` and
``netpkt_copyout``.
- Note that the ``receive`` function just need to return the received
packet instead of calling functions like ``ipv4_input`` or doing reply.
The upper-half will call ``receive`` to get all packets until it
returns ``NULL`` and send these packets into the network stack.
- Also remember to call ``netpkt_free`` for the transmitted packets.
4. Remove work queues related to send and receive, and replace them
with calling ``netdev_lower_txdone`` and ``netdev_lower_rxready``.
Then the upper-half driver will call ``transmit`` and ``receive`` to
send/get packets.
5. Remove any buffer related to ``d_buf``, and make sure ``d_buf`` is not
used in the lower-half driver.
6. Remove ``txavail`` function, the upper-half driver will call ``transmit``
when it has packets to send.
7. Remove the statistics macros like ``NETDEV_TXPACKETS``, ``NETDEV_TXDONE``,
``NETDEV_RXPACKETS`` or ``NETDEV_RXDROPPED``, these macros are well
handled in upper-half. But you may still keep macros like
``NETDEV_TXTIMEOUTS`` and ``NETDEV_RXERRORS`` because the upper-half
cannot know whether these error happens.
8. Find a suitable ``quota`` for the driver, and set it in the driver
initialization function. The quota is the maximum number of buffers
that the driver can hold at the same time. For example, if the TX quota
is set to 5, it means that if the driver has 5 unreleased packets
(``netpkt_free``), the upper-half will not call ``transmit`` until they
are released.
9. Find a suitable ``rxtype`` for the driver, and set it in the driver
initialization function. There are some types of receive notification
methods, defined in ``enum netdev_rx_e``, like ``NETDEV_RX_WORK``,
``NETDEV_RX_THREAD`` and ``NETDEV_RX_THREAD_RSS``. Choose the one that
fits your driver best.
10. Find a suitable ``priority`` for the driver, and set it in the driver
initialization function. This is the priority for the receive
notification work queue or thread. when ``rxtype`` is ``NETDEV_RX_WORK``,
it is the work queue ``qid``; when ``rxtype`` is ``NETDEV_RX_THREAD``,
it is the thread priority.
- Note: An exception is that if the net stack is replying for RX packet,
this replied packet will always be put into ``transmit``, which may
exceed the TX quota temporarily.
Performance tuning
==================
The following knobs matter most for throughput, especially for Wi-Fi drivers
where the MAC runs on a companion core and packets are delivered to the host
over an IPC:
- **RX quota as backpressure.** The RX quota bounds how many received packets
the driver holds before the upper half drains them. It matters most for
traffic without transport flow control (e.g. UDP): when the peer sends
faster than the socket is drained, a large quota lets the driver copy many
packets into the stack only to drop them at the bounded socket receive
buffer -- wasted work that also steals CPU from the packets that do get
delivered. A smaller quota drops the excess earlier and cheaply, at
``netpkt_alloc``. For TCP the advertised window already throttles the peer,
so the quota rarely fills.
- **RX thread priority.** With ``NETDEV_RX_THREAD``, keep ``priority`` at or
below the vendor task that moves packets from the device (companion core)
into the driver. A higher priority (e.g. ``HPWORK``) starves that task,
the device-to-host ring backs up, and packets are dropped upstream of the
driver before ``receive`` ever sees them.
- **Socket receive buffer.** ``CONFIG_NET_RECV_BUFSIZE`` is the default
SO_RCVBUF: it both caps the advertised TCP window and bounds the UDP
read-ahead, both drawn from the shared IOB pool (``CONFIG_IOB_NBUFFERS`` x
``CONFIG_IOB_BUFSIZE``). On a lossy wireless path a large TCP window lets
the peer put more in flight than the link can smooth (bursty loss and RTO
stalls), so cap it near the out-of-order reassembly capacity and bound TX
with ``CONFIG_NET_SEND_BUFSIZE``. That same cap limits UDP read-ahead, so a
UDP-throughput-critical socket should raise its own buffer via
``setsockopt(SO_RCVBUF)`` rather than changing the TCP-safe default.
Enlarging the buffer helps only up to the rate the socket can be drained;
sustained overload is dropped regardless. Keeping out-of-order reassembly
(``CONFIG_NET_TCP_OUT_OF_ORDER``) enabled lets a single loss recover in one
round trip.
"Lower Half" Example
====================
.. code-block:: c
struct <chip>_priv_s
{
/* This holds the information visible to the NuttX network */
struct netdev_lowerhalf_s dev;
...
};
static const struct netdev_ops_s g_ops =
{
.ifup = <chip>_ifup,
.ifdown = <chip>_ifdown,
.transmit = <chip>_transmit,
.receive = <chip>_receive,
.addmac = <chip>_addmac,
.rmmac = <chip>_rmmac,
.ioctl = <chip>_ioctl
};
/* The Wi-Fi driver registration function can be implemented as follows,
* where <chip> refers to the chip name. netdev_lower_register() is the
* network device interface provided by upper-half drivers to register
* network device drivers.
*/
int <chip>_netdev_init(FAR struct <chip>_priv_s *priv)
{
FAR struct netdev_lowerhalf_s *dev = &priv->dev;
dev->ops = &g_ops;
/* The maximum number of buffers that the driver can hold
* at the same time. For example, if the TX quota is set to 5, it
* means that if the driver has 5 unreleased packets (netpkt_free),
* the upper layer will not call transmit until they are released.
* After the rx quota is used up and no new buffer can be allocated
* (netpkt_alloc), it needs to notify the upper layer
* (netdev_lower_rxready) and restore the quota by submitting buffer
* back through receive function.
* If the driver processes each packet individually (without
* accumulating multiple packets before sending/receiving), it can be
* set to 1.
*/
dev->quota[NETPKT_TX] = 1;
dev->quota[NETPKT_RX] = 1;
dev->rxtype = NETDEV_RX_WORK;
dev->priority = HPWORK;
return netdev_lower_register(dev, NET_LL_ETHERNET);
}
/* The transmit function can be implemented as follows, where <chip>
* refers to the chip name.
*/
static int <chip>_transmit(FAR struct netdev_lowerhalf_s *dev,
FAR netpkt_t *pkt)
{
FAR struct <chip>_priv_s *priv = (FAR struct <chip>_priv_s *)dev;
unsigned int len = netpkt_getdatalen(dev, pkt);
#if you want to do offloading
if (!netpkt_is_fragmented(pkt))
{
/* Contiguous memory, just use data pointer */
FAR uint8_t *databuf = netpkt_getdata(dev, pkt);
FAR uint8_t *devbuf = databuf - sizeof(struct <chip>_txhead_s);
/* Do Transmit. Note: `databuf` points to the L2 data, and there is
* a reserved memory with size of `CONFIG_NET_LL_GUARDSIZE` before
* databuf to be used for driver header, drivers can just fill data
* there (`devbuf`) and start the transmission.
*/
...
}
else
#endif
{
/* Copyout the L2 data and transmit. */
uint8_t devbuf[1600];
netpkt_copyout(dev, devbuf, pkt, len, 0);
/* Do Transmit */
...
}
return OK;
}
static void <chip>_txdone_interrupt(FAR struct <chip>_priv_s *priv)
{
FAR struct netdev_lowerhalf_s *dev = &priv->dev;
/* Perform some processing in the driver (if necessary) */
...
/* Free the buffer and notify the upper layer */
netpkt_free(dev, pkt, NETPKT_TX);
netdev_lower_txdone(dev);
}
/* The receive function can be implemented as follows, where <chip>
* refers to the chip name.
*/
static void <chip>_rxready_interrupt(FAR struct <chip>_priv_s *priv)
{
FAR struct netdev_lowerhalf_s *dev = &priv->dev;
netdev_lower_rxready(dev);
}
static FAR netpkt_t *<chip>_receive(FAR struct netdev_lowerhalf_s *dev)
{
/* It is also possible to allocate the pkt and receive the data in
* advance, and then call rxready and return pkt through receive
*/
FAR netpkt_t *pkt = netpkt_alloc(dev, NETPKT_RX);
if (pkt)
{
#if NETPKT_BUFLEN > 15xx && you want to do offloading
/* Write directly to the buffer inside pkt, len corresponds to the
* length of L2 data (need the NETPKT_BUFLEN to be large enough to
* hold the data). The `<chip>_rxhead_s` is the driver header before
* the actual data (maybe you don't have).
*/
len = receive_data_into(netpkt_getbase(pkt));
netpkt_resetreserved(&priv->dev, pkt, sizeof(struct <chip>_rxhead_s));
netpkt_setdatalen(&priv->dev, pkt, len);
#else
uint8_t devbuf[1600];
/* Copy from src, len corresponds to the length of L2 data, you can
* always use this method to receive data. The `<chip>_rxhead_s` is
* the driver header before the actual data (maybe you don't have).
*/
len = receive_data_into(devbuf);
netpkt_copyin(dev, pkt, devbuf + sizeof(struct <chip>_rxhead_s), len, 0);
#endif
}
return pkt;
}