Lab Notebook

This lab notebook contains my notes, thoughts, and plans for the various projects I work on.

Sections

  • The Adafruit Airlift section presents the data I gathered from reverse engineering the Airlift driver libraries.

  • The Atomic Force Microscope section contains some posts from my old blog about designing a pre-amplifier for an atomic force microscope that I was building from scratch at one point.

Intro to the Airlift

The Adafruit Airlift is a WiFi coprocessor based on the ESP32 available for about US$10.

It's little more than an ESP32 on a breakout board with some extra hardware to let you share the SPI bus with multiple devices. Interestingly, it's running a custom firmware that's a fork of the NINA W102 firmware from u-blox. As a result, instead of using AT commands (an extremely cursed plaintext protocol often used to communicate with modems) the Airlift talks over custom binary protocol on SPI at up to 8 MHz.

The protocol isn't explicitly documented anywhere, but there are at least two separate implementations of a driver for the Airlift so it was possible to reverse engineer.

The WiFiNINA library was pretty difficult to follow and I didn't have much luck with reverse engineering it, but the CircuitPython ESP32SPI library is much better architected. I didn't have too much trouble figuring out once I found that library.

Communicating with the Airlift

As I mentioned above, the Airlift expects a custom binary protocol over an SPI bus at 8 MHz or less.

The WifiNINA/Airlift Protocol

As well as the basic pins one would expect with SPI (e.g. SCK, MISO, MOSI, and CS), communicating with the Airlift requires connecting to the BUSY and GPIO0 pins as well.

Before sending packets to the Airlift, the controller must initiate a reset of the Airlift. This is done with the GPIO0, CS, and RESET pins:




#![allow(unused)]
fn main() {
pub fn reset(&mut self) -> Result<(), E> {
    self.gpio0.set_high().ok();
    self.cs.set_high().ok();
    self.reset.set_low().ok();

    // delay 10ms, reset
    self.timer.delay_ms(10);

    self.reset.set_high().ok();

    // delay 750ms, wait for it to boot up
    self.timer.delay_ms(750);

    Ok(())
}
}

The protocol is packet-based and all commands are acknowledged. Before sending a packet, the driver should wait until the BUSY pin is low.

Constants used in commands:

NameValue
START_CMD0xE0
REPLY_FLAG1 << 7
END_CMD0xEE
ERR_CMD0xEF

The CMDs allowed:

NameValue
SetNet0x10
SetPassPhrase0x11
SetKey0x12
SetIPConfig0x14
SetDNSConfig0x15
SetHostname0x16
SetPowerMode0x17
SetAPNet0x18
SetAPPassPhrase0x19
SetDebug0x1a
GetTemperature0x1b
GetConnStatus0x20
GetIPAddress0x21
GetMACAddress0x22
GetCurrentSSID0x23
GetCurrentBSSID0x24
GetCurrentRSSI0x25
GetCurrentEncryption0x26
ScanNetwork0x27
StartServerTCP0x28
GetStateTCP0x29
DataSentTCP0x2a
AvailableDataTCP0x2b
GetDataTCP0x2c
StartClientTCP0x2d
StopClientTCP0x2e
GetClientStateTCP0x2f
Disconnect0x30
GetIndexRSSI0x32
GetIndexEncryption0x33
RequestHostByName0x34
GetHostByName0x35
StartScanNetworks0x36
GetFirmwareVersion0x37
SendUDPData0x39
GetRemoteData0x3a
GetTime0x3b
GetIndexBSSID0x3c
GetIndexChannel0x3d
Ping0x3e
GetSocket0x3f
SetClientCert0x40
SetCertKey0x41
SendDataTCP0x44
GetDataBufTCP0x45
InsertDataBuf0x46
WPA2EnterpriseSetIdentity0x4a
WPA2EnterpriseSetUsername0x4b
WPA2EnterpriseSetPassword0x4c
WPA2EnterpriseSetCACert0x4d
WPA2EnterpriseSetCertKey0x4e
WPA2EnterpriseEnable0x4f
SetPinMode0x50
SetDigitalWrite0x51
SetAnalogWrite0x52
SetDigitalRead0x53
SetAnalogRead0x54

Sending

The command header:

Bits0 ..= 78 ..= 1316 ..= 23
24START_CMDThe command | REPLY_FLAGNumber of parameters

Each command uses parameter lengths that are either 8 bits or 16 bits long.

Each parameter starts with the length in bytes, which is followed by the parameter data. The parameters are concatenated together. Each command is followed by the command footer:

Bits0 ..= 7
8END_CMD

Receiving

The controller should wait for the BUSY pin to go low before reading from the SPI bus.

The header received in the response is exactly the same as the header that was originally sent. The response can contain multiple items, each one starting with either 8 or 16 bits of the length (depending on the command), followed by the data associated with the response. The response will end with the command footer.

Abstractions

It ended up being possible to write a function that could send and receive essentially any data without allocation through the use of const generics:


#![allow(unused)]
fn main() {
fn send_cmd<const PARAMS: usize>(
    &mut self,
    cmd: Cmd,
    params: [&[u8]; PARAMS],
    config: SendConfig,
) -> Result<(), Error<E>> { ... }

fn receive_cmd<'a, const RESPONSES: usize>(
    &mut self,
    cmd: Cmd,
    responses: [&'a mut [u8]; RESPONSES],
    config: ReceiveConfig,
) -> Result<heapless::Vec<&'a [u8], RESPONSES>, Error<E>> { ... }

fn send_cmd_and_receive<'a, const PARAMS: usize, const RESPONSES: usize>(
    &mut self,
    cmd: Cmd,
    params: [&[u8]; PARAMS],
    responses: [&'a mut [u8]; RESPONSES],
    config: SendReceiveConfig,
) -> Result<heapless::Vec<&'a [u8], RESPONSES>, Error<E>> {
    self.send_cmd(cmd, params, config.send)?;
    self.receive_cmd(cmd, responses, config.receive)
}
}

Some of the commands

Get Firmware Version (GetFirmwareVersion)

ParametersResponses
0Firmware version string

In the library, the function that sends this command is written like this:


#![allow(unused)]
fn main() {
/// Retrieve the firmware version. Just guessing at the maximum length here.
pub fn get_firmware_version(&mut self) -> Result<heapless::String<10>, Error<E>> {
    let mut b = [0; 10];

    let resp = self.driver.send_cmd_and_receive(
        Cmd::GetFirmwareVersion,
        [],
        [&mut b],
        SendReceiveConfig::default(),
    )?;

    match str::from_utf8(resp[0]) {
        Ok(s) => Ok(s.into()),
        Err(_) => Err(Error::InvalidEncoding),
    }
}
}

Get MAC Address (GetMACAddress)

ParametersResponses
00xFF0MAC (6 bytes)

#![allow(unused)]
fn main() {
pub fn get_mac_address(&mut self) -> Result<[u8; 6], Error<E>> {
    let mut b = [0; 6];

    self.driver.send_cmd_and_receive(
        Cmd::GetMACAddress,
        [&[0xff]], // dummy data
        [&mut b],
        SendReceiveConfig::default(),
    )?;

    Ok(b)
}
}

Scan for Networks (StartScanNetworks)

This command can return any number of responses.

ParametersResponses
0SSID 0
......
NSSID N

Connect to an access point

With a password (SetPassPhrase)

ParametersResponses
0SSID00x1 if success
1password

Without a password (SetNet)

ParametersResponses
0SSID00x1 if success

There are a bunch more commands, if you'd like to see all of them, take a look at the implementation of the driver I've written: https://github.com/lachlansneff/airlift-driver.

The Pre-Amplifier Board has Arrived - December 7th, 2020

The pre-amplifier board has arrived from jlpcb. This is just an initial prototype (hence the "v0.1 - prototype" label on the board), but it should help me confirm whether my approach for an AFM will work. The components have also arrived, so I'll be putting this together when my semester ends in about two weeks.

When I get a chance, I'll also post about how I intend for the AFM to function (along with some very iffy drawings). It's different from most commerical AFMs, where a laser is used to detect the movement of a cantilever, but instead uses an oscillating, deconstructed quartz tuning fork (QTF) to detect surface detail.

This project is partially inspired by Dan Berard's fantastic Frequency-Modulated Atomic Force Microscope project on Hackaday. His ideas and schematics for the pre-amplifier were extremely useful in helping me design this.

Back to the Schematic... Board - December 11th, 2020

I found some time to heat up my reflow station and solder one of the boards. In the process of doing so, I realized I made a couple of mistakes that I should've caught before hand.

I picked giant capacitors for some reason. This isn't bad, just annoying. I forgot to order the trimmer potentiometer. I have to be careful about this. I used two of the same op-amp when I meant to use two different models that have different characteristics. Oops! I'll re-design and re-order it as soon as possible. I'm hoping that I won't have to go through more than a few iterations of this before getting it working.