Intro
This guide is meant to help you convert an old style NetBSD driver for a wifi device to the newer wifi stack based on FreeBSD's current version.
This is work in progress, as we are trying to create helper functions and libraries where usefull, and also the target API is a slightly moving target.
Overview
The main difference between old style and new style drivers is the VAP concept. VAP means Virtual Access Point, and it is used to split the configuration properties of a wlan (as visible on air) from the radio hardware. In many cases multiple wlans can be created using a single radio hardware, which is naturally modelled as multiple VAPs being created (by pure software) using the same physical radio device.
At the user level the radio hardware is hidden quite well - it only
shows up in dmesg
or sysctl net.wlan.devices
.
There is no network interface for the raw hardware, so ifconfig will
not show them.
To create a VAP for a radio device (and that way a network interface) use a command like:
ifconfig wlan0 create wlandev run0
This split between common radio hardware and per-VAP network properties reflects on the driver and causes most of the changes needed.
After creation of the wlan0
VAP, it will also show up under
sysctl net.wlan.wlan0
and in ifconfig output.
More complete instructions for testing are here: Testing new wifi. There also is an older paper describing the initial design in FreeBSD: FreeBSD wireless BSDCan 2005
Help available!
If your device is a usb(4) device, you can use a new helper facility "usbwifi" similar
to usbnet(9), which already handles a lot of new wifi stack specific details.
In the following text the items not applicable for drivers using usbwifi are marked
like this: usbwifi or // !usbwifi
if appearing in code sections.
Step by Step at the driver level
The data is split into the generic
struct ieee80211com
describing the radio hardware (often used asic
pointer), which is part of the drivers softc. The individual VAPs are in a TAILQ in the ic structure, and often are a driver defined structure 'derived' fromstruct ieee80211vap
(with some additional driver specific data). The VAP structure has the network interfacestruct ifnet
asiv_ifp
. Theif_softc
pointer in the ifnet structure points to the VAP, the VAP structure has a pointer to the iciv_ic
, the ic structure has aic_softc
pointer to the softc.MAC addresses are now stored in ic_macaddr instead of ic_myaddr (in struct ieee80211com). It is typically read from the device and cloned into each VAP. Typical code:
IEEE80211_ADDR_COPY(ic->ic_macaddr, rom->macaddr);
In the attach function, no
struct ifnet ifp
will be needed, so get rid of code like:struct ifnet *ifp = GET_IFP(sc);
and all initialization of ifp members. Move them to the vap_create function (see below) if needed instead, but typically none should be needed (as the generic code initializes the VAP's struct ifnet).
Set a direct backlink to the softc, like:
ic->ic_softc = sc;
Make sure the ic_caps get initialized correctly to the capabilities of the radio, especially you want to add IEEE80211_C_STA for standard AP (station) mode.
There is no state machine in the common radio part, it all moves into per-VAP state, so also remove initialization for it, like:
ic->ic_state = IEEE80211_S_INIT;
You may want a replacement for
IFF_OACATIVE
logic for the main device, as there is no interface so no flags to (ab)use for that.The way channels are set up has changed. If your driver had a
driver_init_channels()
function, you can use it mostly verbatim (but see caveats below formaxchans
andnchans
). Rename it todriver_get_radiocaps
and make it look like:static void urtwn_get_radiocaps(struct ieee80211com *ic, int maxchans, int *nchans, struct ieee80211_channel chans[]) { uint8_t bands[IEEE80211_MODE_BYTES]; memset(bands, 0, sizeof(bands)); setbit(bands, IEEE80211_MODE_11B); setbit(bands, IEEE80211_MODE_11G); setbit(bands, IEEE80211_MODE_11NG); // XXX see description below if your chip can use other ranges (like 5ghz) ieee80211_add_channels_default_2ghz(chans, maxchans, nchans, bands, 0); }
and also assign this function to
ic->ic_getradiocaps
in the attach function.An initial setup must happen before calling
ieee80211_ifattach(ic)
. You can just call thedriver_get_radiocaps()
function during attach like:urtwn_get_radiocaps(ic, IEEE80211_CHAN_MAX, &ic->ic_nchans, ic->ic_channels); ieee80211_ifattach(ic); [..] ic->ic_getradiocaps = urtwn_get_radiocaps;
If the hardware can use different frequency bands you may have to add them band by band instead of using
ieee80211_add_channels_default_2ghz
, also if the firmware knows about regulatory domains it may be required to select subsets of channels.An example of a multi-frequency driver doing this is iwm(4).
It uses a helper function to add single channels individually via
ieee80211_add_channel
. To add the channels for a band, it callsiwm_add_channel_band(ic, chans, maxchans, nchans, start, num, bands)
wherechans
,maxchans
andnchans
are the arguments passed to this callback,chans
collecting the resulting channels,nchans
pointing to the number of channels already collected,bands
is the bitset of bands to add (multipleIEEE80211_MODE_*
bits). The offsetstart
is the index of the first channel to add to the output (first call will always be with 0 asstart
), andnum
the number of channels you expectiwm_add_channel_band
to add. Theiwm_add_channel_band
helper function iterates over the channels, gets flags for the channels from firmware and callsieee80211_add_channel
.Overall the most important part in conversion of old drivers is to make sure that the out parameter
*nchans
is properly set to the number of channel data actually filled, and to respect themaxchans
limit. This callback is called from two places, once from the drivers attach function, wheremaxchan
is big enough (alwaysIEEE80211_CHAN_MAX
), and from ioctlIEEE80211_IOC_DEVCAPS
wheremaxchan
depends on the userland arguments and e.g. wpa_supplicant only asks for minmal data, as it is not interested in the actual channels available (maxchan
is 1). The ioctl handling code will KASSERT proper behaviour of the callback.If your driver used to override state machine functions (typical
newstate
to controll LEDs when associating or similar), remove all traces for this from your softc and override a custom VAP type instead (i.e. derive a struct fromstruct ieee80211vap
and use your own struct in yourvap_create
andvap_delete
methods).Remove the (likely to exist) old
sc_newstate
function from your drivers softc (previously used to store the frameworks newstate function, which now is per-VAP and moved into your derived per-VAP structure.Many functions will get a
struct ieee80211vap *vap
passed instead of a softc. A typical change to adapt for that looks like:- struct ieee80211com *ic = &sc->sc_ic; - struct ieee80211_node *ni = ic->ic_bss; + struct ieee80211com *ic = vap->iv_ic; + struct rtwn_softc *sc = ic->ic_softc; + struct ieee80211_node *ni = vap->iv_bss;
The hardware driver will need a global new outgoing packet queue (as the per-interface queues are attached to VAPs, not the single common transmit hardware). Add the new ifqueue like to your softc like this:
struct ifqueue sc_sendq;
If your hardware supports multiple transmit priorities you may consider doing more than one ifqueue. Usbwifi does this for devices with multiple transmit pipes (creating a ifqueue per hardware priority and in this case USB send endpoint).
While there, check that there is no
struct ethercom sc_ec
in your softc - it is not needed.This queue needs initialization in the attach function, like:
sc->sc_sendq.ifq_maxlen = ifqmaxlen; IFQ_LOCK_INIT(&sc->sc_sendq);
This device private send queue (writer: any of the current VAPs of this interface, consumer: the devices transmit interrupt handler) abuses the
mbuf rcvif
pointer to store the node pointerstruct ieee80211_node *
of the target node for this packet. When dequeuing a packet before handing it over to the output function, the node is extracted and thercvif
pointer set toNULL
. Yes - this is a hack.Also the attach function needs to set the name of the interface in the common radio device structure:
ic->ic_name = device_xname(self);
Many drivers query the hardware/firmware for the number of available input/output queues and store these in e.g.
sc->ntxchains
andsc->nrxchains
or similar variables. This information needs to be propagated to the common radio structure:ic->ic_txstream = sc->ntxchains; ic->ic_rxstream = sc->nrxchains;
Same for interface flags:
ic->ic_flags = IFF_BROADCAST | IFF_SIMPLEX | IFF_MULTICAST;
After calling
ieee80211_ifattach(ic);
some methods inic
need to be overwritten. The functions needed are explained below, here is a typical setup:/* override default methods */ ic->ic_newassoc = urtwn_newassoc; ic->ic_wme.wme_update = urtwn_wme_update; ic->ic_vap_create = urtwn_vap_create; ic->ic_vap_delete = urtwn_vap_delete; ic->ic_parent = urtwn_parent; ic->ic_scan_start = urtwn_scan_start; ic->ic_scan_end = urtwn_scan_end; ic->ic_set_channel = urtwn_set_channel; ic->ic_transmit = urtwn_transmit; // !usbwifi ic->ic_raw_xmit = urtwn_raw_xmit; // !usbwifi
detach does not deal with any interfaces any more, remove all traces of
struct ifnet *ifp
.Add a new function for VAP creation and move all interface specific setup there. It looks like:
static struct ieee80211vap * urtwn_vap_create(struct ieee80211com *ic, const char name[IFNAMSIZ], int unit, enum ieee80211_opmode opmode, int flags, const uint8_t bssid[IEEE80211_ADDR_LEN], const uint8_t macaddr[IEEE80211_ADDR_LEN]) { struct urtwn_softc *sc = ic->ic_softc; struct ifnet *ifp; struct ieee80211vap *vap; ... return vap; }
It allocates a new VAP with
kmem_zalloc
initializes it by callingieee80211_vap_setup
for it and then overriding whatever the driver needs, passes it toieee80211_vap_attach
and attaches BPF to the interface.If your interrupt handler is called in hard interrupt context (that probably means: for all devices that are not on USB) you need to initialize the per CPU interface queue:
/* Use common softint-based if_input */ ifp->if_percpuq = if_percpuq_create(ifp);
If your interrupt handler always is called in softint context, do not do this. If
if_percpuq
isNULL
the wifi framework will directly handle incoming packets, otherwise it will take a round trip via a per cpu softint handler.Add a new function for VAP destruction and move interface specific parts that you deleted in the detach function here. A very minimalistic example looks like:
static void urtwn_vap_delete(struct ieee80211vap *vap) { struct ifnet *ifp = vap->iv_ifp; struct urtwn_softc *sc __unused = vap->iv_ic->ic_softc; bpf_detach(ifp); ieee80211_vap_detach(vap); kmem_free(vap, sizeof(struct ieee80211vap)); }
Rate Adaption happens per VAP, so the ra_init call changes like this:
-static int urtwn_ra_init(struct urtwn_softc *); +static int urtwn_ra_init(struct ieee80211vap );
See above for recipes how to access the needed structs in there.
State is per VAP, so newstate changes the first parameter:
-static int urtwn_newstate(struct ieee80211com *, enum ieee80211_state, int); +static int urtwn_newstate(struct ieee80211vap *, enum ieee80211_state, int);
If there was a newstate_cb function previously, it is probably not needed any more.
Reset is per interface, so per VAP:
-static int urtwn_reset(struct ifnet *); +static int urtwn_reset(struct ieee80211vap *, u_long cmd);
A driver is not required to provide this method, if not filled in a default method will be used that always causes a full reset of the device.
Selecting TX rate for different frame types may need rework. When creating TX descriptors the driver checks the type of packet in the mbuf at hand, which has a
struct ieee80211_node *
associated, which has a member with transmit params:Example for management frames:
const struct ieee80211_txparam *tp = ni->ni_txparms; rate = tp->mgmtrate;
Similar values are available for multicast (
mcastrate
) and standard unicast frames (ucastrate
).Rates are either legacy/basic in 0.5 Mb/s or MCS/HT (with the
IEEE80211_RATE_MCS
bit set). No valid rate is encoded asIEEE80211_FIXED_RATE_NONE
. The macrosIEEE80211_IS_HT_RATE
,IEEE80211_HT_RC_2_MCS
andIEEE80211_HT_RC_2_STREAMS
can be useful to categorize them or extract physical details.Many drivers select rates based on the current channel - this is not needed here, the
ni_txparms
pointer is updated whenever the channel switches.The set channel function is generic now, so gets passed a
struct ieee80211com *ic
instead of a softc, and sets the radio hardware to the current channel in that structure,ic->ic_curchan
.The start function does not need a
struct ifnet *ifp
any more, but can use a softc orstruct ieee80211com *ic
instead.Typically, generic media change and status functions will be used:
ieee80211_media_change
andieee80211_media_status
. Old drivers often implement their own function - remove that (unless there is something actually chip specific in them).If there is, override media_change and/or media_status, and make sure to call the generic variant for all the real work. Also make sure to pass your override function to
ieee80211_vap_attach.
run(4) is an example of this.The overridden media_{change,status} functions are the only one in a wlan driver that should get a
struct ifnet *
argument passed. Checking for this is a good measurement to know you are done with conversion (besides the kernel build failing, of course).usbwifi Your driver needs a new transmit function, used to enqueue a mbuf to the hardware send queue (and start transmission if needed). A very simple example:
static int urtwn_transmit(struct ieee80211com *ic, struct mbuf *m) { struct urtwn_softc *sc = ic->ic_softc; int s; s = splnet(); IF_ENQUEUE(&sc->sc_sendq, m); splx(s); if (!sc->sc_oactive) urtwn_start(sc); return 0; }
A pointer to this function is assigned to the
ic->ic_transmit
member in the attach function.usbwifi Your driver needs a raw_xmit function which is close to what the old driver called *_tx(), it looks like:
static int urtwn_raw_xmit(struct ieee80211_node *ni, struct mbuf *m, const struct ieee80211_bpf_params *bpfp)
and the "missing" arguments are derived like:
struct ieee80211vap *vap = ni->ni_vap; struct ieee80211com *ic = ni->ni_ic; struct urtwn_softc *sc = ic->ic_softc;
The check for encrypting packets often was testing the
ieee80211_frame
wh->i_fc[1] & IEEE80211_FC1_WE
before encapsulating the mbuf viaieee80211_crypto_encap()
. The check now is forwh->i_fc[1] & IEEE80211_FC1_PROTECTED
, since WEP is a thing of the past and there are other protocols. Also theieee80211_crypto_encap
function lost its first argument (the radio common ic pointer).The transmit function queues the mbuf for transmit and makes sure when transmission is done by hardware to call
ieee80211_tx_complete
with bothmbuf
andni
. This function also needs to be called in all error cases, with an error value as status (last argument). The actual value does not matter, the stack just checks for 0 (success) and non zero (failure). This call will free the mbuf and the node, and in some cases also cause further state changes (like waking up VAPs, scheduling a new scan later, or even causing immediate state machine transitions and retrying in error cases).usbwifi If the
driver_activate()
function only passes deactivation requests on to if_deactivate() you can replace it by a shim that gets your softc and callsieee80211_activate()
, like:static int urtwn_activate(device_t self, enum devact act) { struct urtwn_softc *sc = device_private(self); return ieee80211_activate(&sc->sc_ic, act); }
When the hardware received a full frame, move it into a mbuf (with proper bound checks) and simply call
// fill mbuf m with the received data ieee80211_rx_enqueue(ic, m, rssi);
Old code often has tests for too short or too long packets and increments error counters in the struct ifnet of the interface that used to belong to the wlan driver. Since there is no such single interface anymore, errors are accounted later (at the VAP level) if a VAP/node is identified for the packet. Otherwise just increment the global
ic->ic_ierrors
counter for errors with incoming packets that can not be associated with a concrete network (or similaric->ic_oerrors
for output errors).Various BPF changes:
Many drivers avoid complex setup options for BPF listeners if there are none. With virtual APs this became a bit more complex, but the framework tracks it for the driver. To test if any VAP on the radio hardware is in MONITOR mode and has an active BPF peer, simply test the
ic->ic_montaps
counter - it will be zero if there are none.For most drivers it should not be necessary to do anything special for BPF, the taps are in the framework.
Some drivers provide special low level radiotap data, often via a driver specific
sc->sc_drvbpf
. This is replaced by a generic radiotap bpf listener created by the framework code and stored asvap-> iv_rawbpf
. A driver signals support for radiotap by makingic->ic_th
point to a radiotap transmit header (usually in the softc, initialized before attach and updated on transmit) and similaric->ic_rh
point to a receive tap header (updated on every received packet). Make sure to initialize the tap headersit_len
fields to the proper size (in little endian byte order) andit_present
flags (also in little endian byte order) to the relevantIEEE80211_RADIOTAP_*
bits.To avoid updating the receive/transmit tap headers unessecarily the driver can check the
ic->ic_flags_ext
for theIEEE80211_FEXT_BPF
bit being set - if not, there is no VAP with bpf listener on radiotap headers.The driver needs to update
ic_rh
(usually only the storage for a single header it points to) on every received packet. This is used as a side channel for the radiotap data when passing packet up the stack viaieee80211_rx_enqueue
. Or in the NetBSD case for USB drivers when callingusbwifi_enqueue
. The stack automatically dispatches this to the radiotap bpf tap associated with any relevant VAP and also makes sure it is seen by VAPs in MONITOR mode.Watchdog functions move to callout:
Since there is no 1:1 ifnet and its global watchdog mechanism, all driver watchdog functions are converted to a callout. A typical implementation looks like:
static void urtwn_watchdog(void *arg) { struct urtwn_softc *sc = arg; struct ieee80211com *ic = &sc->sc_ic; if (sc->tx_timer > 0) { if (--sc->tx_timer == 0) { aprint_error_dev(sc->sc_dev, "device timeout\n"); ieee80211_stat_add(&ic->ic_oerrors, 1); ieee80211_restart_all(ic); return; } callout_schedule(&sc->sc_watchdog_to, hz); } }
There is no
ieee80211_watchdog()
function any more.ioctl functions
Typicaly there are no ioctl functions in a wlan driver. The ioctls are handled in generic code and the ic_parent function is called to update the device state accordingly. XXXX - is this true? implemented? Or one of ic_scan_start/ic_scan_end/ic_set_channel
device_parent function
You need to add a new function to power up the radio when the first VAP is activated or shut it down after the last VAP goes down (or is destroyed). The function looks like:
static void rtwn_parent(struct ieee80211com *ic) { struct rtwn_softc *sc = ic->ic_softc; bool startall = false; mutex_enter(&sc->sc_tx_mtx); if (ic->ic_nrunning > 0 && !sc->sc_running) startall = true; else if (ic->ic_nrunning == 0 && sc->sc_running) rtwn_stop(sc); mutex_exit(&sc->sc_tx_mtx); if (startall) ieee80211_start_all(ic); }
Caveats:
When selecting a TX queue the driver is probably extracting a Quality Of Service value from the IEEE802.11 header. There are new variants out there now, so carefully check and update code that does it like this:
if ((hasqos = ieee80211_has_qos(wh))) { qos = ((struct ieee80211_qosframe *)wh)->i_qos[0]; } else { qos = 0; }
to something like this:
if ((hasqos = IEEE80211_QOS_HAS_SEQ(wh))) { uint8_t *frm; frm = ieee80211_getqos(wh); qos = le16toh(*(const uint16_t *)frm); } else { qos = 0; }