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
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 statemachine 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 for
maxchans
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 (multiple IEEE80211_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 from struct ieee80211vap and use your own struct in your vap_create and vap_delete method).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 ifque like to your softc like this:
struct ifqueue sc_sendq;
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 pointer
struct ieee80211_node *
of the target node for this packet. When dequeing a packet before handing it over to the output function, the node is extracted and the rcvif pointer set to NULL. 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 in ic 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; ic->ic_raw_xmit = urtwn_raw_xmit;
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 ovverriding 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 is NULL 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 acces 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.
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.Typicall 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 overriden 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).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.
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 thembuf
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 callieee80211_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 != 0 (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).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 calls ieee80211_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 and call (with proper frame bound checks)
struct ieee80211_node *ni; // XXX unclear - same on NetBSD? if (ieee80211_radiotap_active(ic)) mactime = rx->mactime; // or: if (ieee80211_radiotap_active(ic)) struct urtw_rx_radiotap_header *tap = &sc->sc_rxtap; tap->wr_tsf = mactime; tap->wr_flags = 0; tap->wr_dbm_antsignal = (int8_t)rssi; } ni = ieee80211_find_rxnode(ic, mtod(m, struct ieee80211_frame_min *)); if (ni != NULL) { ieee80211_input(ni, m, rssi, nf); ieee80211_free_node(ni); } else { ieee80211_input_all(ic, m, rssi, nf); }
If a node is found, the data is passed on to that VAP, otherwise it is a general management packet.
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 similar ic->ic_oerrors for output errors).
Watchdog functions 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 update the device state when one of the active VAPs requires something special, like switch to promiscous mode. 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); }