Zerowi bare-metal WiFi driver part 6: joining a network

It has been a long & complicated journey to get to this point, and you might be thinking that it’ll take a lot more effort to join a network, particularly if it involves WPA security.

However, this isn’t true; because we’re dealing with an intelligent network interface, all the complexities of WPA are handled by the on-chip firmware, so it only takes a few simple IOCTL commands to join a secure network.

For the purposes of this blog, I’m assuming there is an existing infrastructure network with an Access Point (AP) using a Service Set Identifier (SSID) of ‘testnet’, and a WPA or WPA2 password of ‘testpass’ – this is purely for test purposes, and must be changed.

IOCTL calls

IOCTLs were described in detail in the previous part, but to recap: there are over 300 possible calls, covering all aspects of the configuration and monitoring of the network interface. Some calls can take a significant amount of time to process, so the host CPU has to poll the SDIO interface, checking for a response.

In the previous part, I put a ‘while’ loop round some of the calls to account for this delay, but this was getting rather messy, so I’ve expanded the IOCTL calls to include a millisecond retry time; if this is non-zero, the code will keep polling for a response until the time is exceeded.

An example of this is the way the country is set:

    if (!ioctl_set_data("country", 100, &country_struct, sizeof(country_struct)))
        printf("Can't set country\n");

After sending the command, the code waits for up to 100 milliseconds for a response, returning its length; zero if there was an error or no response.

There isn’t a standard data format for all the IOCTLS, so it is very easy to have a mismatch between the command & data; ideally we’d attach a diagnostic function to each call, to report any errors. To do this, I’ve created a simple macro to invoke the IOCTL function, with token-pasting to report the function name and first argument in the event of an error. For example, the code to set the pre-shared key is:

#define CHECK(f, a, ...) {if (!f(a, __VA_ARGS__)) \
                          printf("Error: %s(%s ...)\n", #f, #a);}

CHECK(ioctl_wr_data, WLC_SET_WSEC_PMK, 0, &wsec_pmk, sizeof(wsec_pmk));

When all is OK, this acts the same as:

ioctl_wr_data(WLC_SET_WSEC_PMK, 0, &wsec_pmk, sizeof(wsec_pmk));

If the data is incorrect, for example the key string is too short, an error will be reported on the console at run-time:

Error: ioctl_wr_data(WLC_SET_WSEC_PMK ...)

I’ve found this really helpful in tracking down IOCTL programming errors and timing issues.

Joining an insecure network

We’ll start with a simple test network that has security disabled; at the risk of stating the obvious, this mode presents a major security risk, so should be used with great caution.

All we need to set is a two-letter country, and an SSID (network name), which are used to populate the IOCTL structures, e.g.

#define SSID            "testnet"
#define COUNTRY         "GB"
#define COUNTRY_REV     -1
#define SECURITY        0  // Zero to disable security
wlc_ssid_t ssid={sizeof(SSID)-1, SSID};
wl_country_t country_struct = {.ccode=COUNTRY, .country_abbrev=COUNTRY, .rev=COUNTRY_REV};

The country defines which radio channels may be used. A few countries have multiple revisions of their channel settings; to use the default revision, a value of -1 is entered.

Joining the network just involves setting ‘infrastructure’ mode, disabling the various forms of security, then sending a ‘SET_SSID’ IOCTL command to join the network:

    CHECK(ioctl_wr_int32, WLC_SET_INFRA, 50, 1);
    CHECK(ioctl_wr_int32, WLC_SET_AUTH, 0, 0);
    CHECK(ioctl_wr_int32, WLC_SET_WSEC, 0, SECURITY);
    CHECK(ioctl_wr_int32, WLC_SET_WPA_AUTH, 0, 0);
    ioctl_enable_evts(join_evts);
    CHECK(ioctl_wr_data, WLC_SET_SSID, 100, &ssid, sizeof(ssid));

The joining process takes a few seconds, then success or failure is signalled by asynchronous events, hence the ‘enable_evts’ line – this specifies which events we’d like to receive when the device is connecting, and after it has connected. The event messages are described below.

Joining a secure network

There are IOCTL commands covering a wide range of security options; I’ve tested WPA-TKIP and WPA2 in pre-shared key (PSK) mode as they are in widespread use.

The settings aren’t complex; the hard work is done by the chip firmware. I’ve used a compile-time definition to select the security mode:

// Security settings: 0 for none, 1 for WPA_TKIP, 2 for WPA2
// The hard-coded password is for test purposes only!!!
#define SECURITY        2
#define PASSPHRASE      "testpass"
wsec_pmk_t wsec_pmk = {sizeof(PASSPHRASE)-1, WSEC_PASSPHRASE, PASSPHRASE};

#if SECURITY
CHECK(ioctl_wr_int32, WLC_SET_WSEC, 0, SECURITY==2 ? 6 : 2);
CHECK(ioctl_set_intx2, "bsscfg:sup_wpa", 0, 0, 1);
CHECK(ioctl_set_intx2, "bsscfg:sup_wpa2_eapver", 0, 0, -1);
CHECK(ioctl_set_intx2, "bsscfg:sup_wpa_tmo", 0, 0, 2500);
CHECK(ioctl_wr_data, WLC_SET_WSEC_PMK, 0, &wsec_pmk, sizeof(wsec_pmk));
CHECK(ioctl_wr_int32, WLC_SET_WPA_AUTH, 0, SECURITY==2 ? 0x80 : 4);
#endif

ioctl_enable_evts(join_evts);
CHECK(ioctl_wr_data, WLC_SET_SSID, 100, &ssid, sizeof(ssid));

Some of the IOCTL calls require two 32-bit integer arguments, the first being an interface number that is zero in this implementation. The function ‘set_intx2’ was created to simplify the handling of the 2 arguments:

// Set 2 integers in IOCTL variable
int ioctl_set_intx2(char *name, int wait_msec, int val1, int val2)
{
    int data[2] = {val1, val2};

    return(ioctl_cmd(WLC_SET_VAR, name, wait_msec, 1, data, 8));
}

Response when joining network

The status reports are in the form of asynchronous event messages; prior to joining, we did an ioctl_enable_events() call to specify which events we’re interested in receiving. This call also stores a string value for each enabled event, to simplify decoding.

The messages are obtained by polling the SDIO interface with a command 53. If one is available, the first 2 bytes are a (little-endian) length, the next 2 are the bitwise inverse of that length; otherwise the returned values are zero if there is no message.

My code first loads and decodes the 12-byte event header, then the remainder of the message. For example, this is the response if the Access Point (AP) doesn’t support the requested security scheme:

67 00 98 ff 10 01 00 0e 00 20 00 00
len=67 seq=10 chan=01 hdrlen=0E flow=00 credit=20

00 00 20 00 00 01 00 01 00 00 b8 27 eb 6b 3d 7c ba 27 eb 6b 3d 7c 88 6c 80 01 00 3f 00 00 10 18
00 01 00 02 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 07 61 79 54 65 6b 20
77 6c 30 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 74 65 73 74 6e 65 74 00 00
dest=B827EB6B3D7C srce=BA27EB6B3D7C type=886C sub=8001 len=3F oui=1018 usr=01
ver=02 flags=00 type=00 status=01 reason=00 auth=00 dlen=07 addr=617954656B20
SET_SSID FAIL

The total length (including the 12-byte header) is 67 hex, 103 decimal; the bitwise inverse of this is 98FF hex. Some care is needed when doing the decoding; the first part of the message is in the usual little-endian byte order, while the later parts are in network (big-endian) format. The most important part of the decode is the last line, giving the name of the event (0, SET_SSID) and the status code (1, FAIL).

It would be nice if all the responses were as easy to understand as this, but they can be a bit misleading. This is the response when the password is incorrect:

AUTH SUCCESS
LINK SUCCESS
SET_SSID SUCCESS
PSK_SUP PARTIAL
PSK_SUP PARTIAL
DEAUTH_IND SUCCESS

I don’t understand why so much success is reported, when there is an obvious error. In contrast, a successful network join looks like:

AUTH SUCCESS
LINK SUCCESS
SET_SSID SUCCESS
PSK_SUP UNSOLICITED

Another indication of success is that you may see some blocks of binary data, such as:

52 00 ad ff 14 02 00 0e 00 20 00 00
len=52 seq=14 chan=02 hdrlen=0E flow=00 credit=20

00 00 20 00 00 01 00 01 f5 00 ff ff ff ff ff ff 68 17 29 f6 b8 54 08 06 00 01 08 00 06 04 00 01
68 17 29 f6 b8 32 0a 01 01 cc 00 00 00 00 00 00 0a 01 01 c7 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00
dest=FFFFFFFFFFFF srce=681729F6B854 type=806

The event fields haven’t been decoded because this isn’t really an event; it is a network frame. The all-1s destination address shows that it is a broadcast transmission; it is quite usual for a network to carry lots of broadcasts, that have to be decoded by all the attached systems, so the wireless interface is dutifully passing them on to our software for processing.

These network frames are a handy way of showing that our wireless interface is connected to a real network, but to understand them we’ll need to tackle the TCP/IP protocols that underpin all our network communications; that’ll be in the next part.

If you want to run the code so far, see the end of the previous part for instructions; there is a batch file ‘make_join.bat’ for Windows, and ‘make_join’ for Linux.

Interrupts

Thinking ahead to the TCP/IP stack, we’ll need a quick way of detecting when network data (a network ‘frame’) has arrived; continually sending out 12-byte SDIO data requests, in the hope of receiving something back, is really inefficient.

If you search the CYW43438 data sheet for the word ‘interrupt’, you’ll see that SDIO data line 1 also acts as an interrupt line – there is no separate pin for that function. This works because generally the 4 data lines are just floating (pulled high by resistors); they’re only driven high or low during block transfers. So while they are idle, the wireless chip can pull one of them (DATA1) low to request a data transfer; as soon as the transfer starts, the line reverts to carrying data bits.

It is quite easy to set this up, just write to the Interrupt Enable register:

    sdio_cmd52_writes(SD_FUNC_BUS, BUS_INTEN_REG, 0x07, 1);

The value of 7 includes a master interrupt enable, plus enabling functions 1 & 2. [I’m not clear on what these 2 functions are: the network traffic seems to use function 1 only].

Handling the interrupt should just be a case of checking the I/O pin, then doing the necessary read cycle:

if (gpio_in(SD_D1_PIN) == 0)
{
    while ((n=ioctl_get_event(&ieh, eventbuff, sizeof(eventbuff))) > 0)
        ..process the network data..
}

Unfortunately this approach doesn’t work very well; the interrupt line has a tendency to remain low, causing lots of unnecessary attempts to get non-existent data. The reason is that the interrupt needs to be explicitly acknowledged, so that the I/O pin goes high again. The Broadcom driver handles this situation by disabling asynchronous messaging; when the interrupt pin is asserted, it does read & write cycles to the interrupt status register, to check for the interrupt, and acknowledge it. Here is my interpretation of this method, which seems to work well:

// After security is set up, enable async events
ioctl_enable_evts(join_evts);

// Start joining a network
CHECK(ioctl_wr_data, WLC_SET_SSID, 100, &ssid, sizeof(ssid));

// Enable interrupts
sdio_cmd52_writes(SD_FUNC_BUS, BUS_INTEN_REG, 0x07, 1);

// ..wait until the events show we have joined the network, then..

// Disable events
ioctl_enable_evts(no_evts);

// Fetch a network frame
if (gpio_in(SD_D1_PIN) == 0)
{
    sdio_bak_read32(SB_INT_STATUS_REG, &val);
    if (val & 0xff)
    {
        sdio_bak_write32(SB_INT_STATUS_REG, val);
        if ((n=ioctl_get_event(&ieh, eventbuff, sizeof(eventbuff))) > 0)
            ...process the network data...
    }
}

In the early stages of code development, I tend to use polling in place of interrupts, as it makes debugging much easier. When the code is debugged, I’ll switch to using real CPU interrupts: any I/O pin can be configured to trigger an interrupt, so the change shouldn’t be difficult.

[Overview] [Previous part]

Copyright (c) Jeremy P Bentham 2020. Please credit this blog if you use the information or software in it.

4 thoughts on “Zerowi bare-metal WiFi driver part 6: joining a network”

  1. Very intersting blog entries for wifi on baremetal.
    I am thinking of integrating Wifi to my own baremetal setup – and am thus interested if you
    continued your adventure and have the TCP/IP stuff also lying around and perhaps do anather fanatstic blog entry as a flolowup?

    Like

    1. I’m glad you find my project interesting. To be honest, there is an unsolved bug in the low-level receive code, that means it is a bit unreliable; it sometimes ignores an incoming packet. I intended to fix this, but had already invested a huge amount of time in this project, and needed to move on to other things. So the first job would be to fix that bug, then get TCP/IP working; I have written a lot of code over the years, but would probably be tempted to use LwIP, to save myself a lot of debug & documentation work.

      Like

  2. Hi,

    Thank you for this article! Really helps to understand how (standalone) wifi chips work.
    I would like to know if you have resovled the bug you mentioned in the above comment?
    Thank you!

    Like

    1. No, I’m afraid I haven’t; the chip is so complex and difficult to drive that I’ve switched to the ESP32 in my embedded projects, though that isn’t completely without its issues…

      Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: