AS7341 Spectral Color Sensor with Mozzi on AVR Arduino

Looks like I will never learn how to build custom configurations for AS7341 SMUX, but at least I have presets I could copy/paste and that’s good enough for my hobbyist level project. The goal is to convert I2C communication from Arduino’s standard Wire library to the twi_nonblock library bundled as part of Mozzi library. Arduino Wire library would block execution while I2C communication is ongoing, which causes audible glitches with Mozzi sketches. Converting to twi_nonblock avoids that issue.

Using Adafruit’s AS7341 Arduino library and sample sketches as my guide, it was relatively straightforward to converting initialization routines. Partly because most of them were single-register writes, but mostly because non-blocking isn’t a big concern for setup. I could get away with some blocking operations which also verified I2C hardware was working before adding the variable of nonblocking behavior into the mix.

To replicate Adafruit’s readAllChannels() behavior in a nonblocking manner, I split the task into three levels of state machines to track asynchronous actions:

  • Lowest level state machine tracks a single I2C read operation. Write the address, start a read, and copy the results.
  • Mid level state machine pulls data from all 6 AS7341 ADC. This means setting up SMUX to one of two presets then waiting for that to complete. Start sensor integration, then wait for that to complete. And finally copy sensor results. Each read operation is delegated to the lower level state machine.
  • High level state machine runs the mid-level state machine twice, once for each SMUX preset configuration.

Once that high level state machine runs through both SMUX preset configurations, we have data from 10 spectral sensors. The flicker detector is the 11th sensor and not used here. This replicates the results of Adafruit’s readAllChannels() without its blocking behavior. Getting there required an implementation which is significantly different. (I don’t think I copied any Adafruit code.)

While putting this project together, my test just printed all ten sensor values out to serial console. In order to make things a little more interesting, I brought this back full circle and emulated the behavior of Emily Velasco’s color organ experiment. Or at least, an earlier version of it that played a single note depending on which sensor had returned highest count. (Emily has since experimented with vibrato and chords.) I added a lot of comments my version of the experiment hopefully making it suitable as a learning example. As a “Thank You” to Emily for introducing me to the AS7341, I packaged this project into a pull request on her color organ repository, which she accepted and merged.

This is not the end of the story. twi_nonblock supports only AVR-based Arduino boards like the ATmega328P-based Arduino Nano I used for this project. What about processors at the heart of other Arduino-compatible boards like the ESP32? They can’t use twi_nonblock and would have to do something else for glitch-free Mozzi audio. That’ll be a separate challenge to tackle.


Code for this project is publicly available on GitHub.

Unrolling Adafruit AS7341 readAllChannels()

After successfully reading an AS7341’s product ID via a non-blocking I2C API, I have gained confidence I can replicate any existing AS7341 library’s actions with the non-blocking I2C API. This way it will play nice with Mozzi library. I decided to focus on Adafruit_AS7341::readAllChanels() because it seemed like the most useful one to convert to non-blocking operation. It was used in multiple examples as well as Emily’s color organ project.

bool Adafruit_AS7341::readAllChannels(uint16_t *readings_buffer) {

  setSMUXLowChannels(true);        // Configure SMUX to read low channels
  enableSpectralMeasurement(true); // Start integration
  delayForData(0);                 // I'll wait for you for all time

  Adafruit_BusIO_Register channel_data_reg =
      Adafruit_BusIO_Register(i2c_dev, AS7341_CH0_DATA_L, 2);

  bool low_success = channel_data_reg.read((uint8_t *)readings_buffer, 12);

  setSMUXLowChannels(false);       // Configure SMUX to read high channels
  enableSpectralMeasurement(true); // Start integration
  delayForData(0);                 // I'll wait for you for all time

  return low_success &&
         channel_data_reg.read((uint8_t *)&readings_buffer[6], 12);
}

Examining this code, we can see it does the same thing twice, differing only in a single boolean parameter to setSMUXLowChannels. This reflects AS7341 architecture where we have more sensors than ADCs so we have to configure SMUX to read a subset then reconfigure SMUX to read remaining sensors.

void Adafruit_AS7341::setSMUXLowChannels(bool f1_f4) {
  enableSpectralMeasurement(false);
  setSMUXCommand(AS7341_SMUX_CMD_WRITE);
  if (f1_f4) {
    setup_F1F4_Clear_NIR();
  } else {
    setup_F5F8_Clear_NIR();
  }
  enableSMUX();
}

Digging into setSMUXLowChannels we see the following actions:

  1. enableSpectralMeasurement disables a register bit to turn off spectral measurement, necessary preparation for SMUX reconfiguration.
  2. setSMUXCommand flips a different register bit to notify AS7341 that a new SMUX configuration is about to be uploaded.
  3. Upload one of two SMUX configurations, depending on boolean parameter.
  4. enableSMUX repeatedly reads a register bit until it flips to 0, which is how AS7341 signifies that SMUX reconfiguration is complete.

Steps 1-3 above are I2C writes and can be done quickly. Step 4 will add complication: not only is it an I2C read, but we might also need to read it several times before the bit flips to zero.

Backing out to readAllChannels, we see spectral measurement bit is flipped back on after SMUX reconfiguration. According to the datasheet, flipping this bit back on is a signal to start a new reading. How do we know when sensor integration is complete? That’s indicated by yet another register bit. delayForData repeatedly reads that until it flips to 1, clearing the way for us to read 12 bytes of sensor data. Representing data from all six ADCs channels, each of which gives us 16 bits (2 bytes) of data.

Unrolling all of the above code in terms of I2C operations, readAllChannels breaks down to:

  1. I2C register write to turn OFF spectral measurement.
  2. I2C register write to notify incoming SMUX configuration data.
  3. I2C write to upload SMUX configuration for ADC to read sensors F1 through F4, plus Clear and NIR channels.
  4. Repeated I2C read for “SMUX reconfiguration” flag until that bit flips to 0.
  5. I2C register write to turn ON spectral measurement. (Starts new sensor integration.)
  6. Repeated I2C read for “Sensor integration complete” flag until that bit flips to 1.
  7. I2C read to download sensor data
  8. Repeat 1-7, except step #3 uploads SMUX configuration for sensors F5 through F8 instead of F1 through F4.

I like this plan but before I roll up my sleeves, I wanted to take a closer look at SMUX configuration.


Code for this project is publicly available on GitHub.

Hello AS7341 ID via Non-Blocking I2C

I’ve refreshed my memory of Mozzi and its twi_nonblock API for non-blocking I2C operations, the next step is to write a relatively simple Hello World to verify I can communicate with an AS7341 with that API. While reading AS7341 datasheet I noted a great candidate for this operation: product ID and revision ID registers.

In order to use twi_nonblock we need to break a blocking read() up to at least three non-blocking steps in order to avoid glitching Mozzi sound:

  1. We start with an I2C write to tell the chip which register we want to start from. Once we set parameters, I2C hardware peripheral can do the rest. In the meantime, we return control to the rest of the sketch so Mozzi can do things like updateAudio(). During each execution of updateControl() we check I2C hardware status to see if the write had completed. If it’s still running, we resume doing other work in our sketch and will check again later.
  2. If step 1 is complete and address had been sent, we configure I2C hardware peripheral to receive data from A7341. Once that has been kicked off, we return control to the rest of the sketch for updateAudio() and such. During each execution of updateControl() we check I2C hardware status to see if data transfer had completed. If it’s not done yet, we resume running other code and will check again later.
  3. If data transfer from AS7341 is complete, we copy the transferred data into our sketch and our application logic can take it from there.

My MMA_7660 sketch tracked above states #1-3 within updateControl(), following precedence set by Mozzi’s ADXL354 example. But that was a simple sensor to use, we just had to read three registers in a single operation. AS7341 is a lot more complex with multiple different read operations, so I pulled that state machine out of updateControl() and into its own method async_read(). The caller can keep calling async_read() on every updateControl() until the state reaches ASYNC_COMPLETE, at which point the process stops waiting for data to be copied and processed. Whenever the caller is ready to make another asynchronous read, they can set the state to ASYNC_IDLE and the next async_read() will start the process again.

As a test of this revamped system, I used it to read two bytes from AS7341 starting at register 0x91. 0x91 is for revision ID and the following byte 0x92 is the product ID. I wasn’t sure what to expect for revision ID, I got zero. But according to the datasheet product ID is supposed to be 0x09, and that matches what I retrieved. A great start! Now I can dig deeper and figure out how to read its sensors with nonblocking I2C.


Code for this project is publicly available on GitHub.

Refresher on Mozzi Timing Before Tackling AS7341

I’ve decided to tackle the challenge of writing a Mozzi-friendly way to use an AS7341 sensor, using nonblocking I2C library twi_nonblock. At a high level, this is a follow-up to my MMA7660 accelerometer Mozzi project several years ago. Due to lack of practice in the meantime I have forgotten much about Mozzi and need a quick refresher. Fortunately, Anatomy of a Mozzi sketch brought most of those memories back.

I connected a salvaged audio jack to the breadboard where I already had an Arduino Nano and my Adafruit AS7341 breakout board. (The AS7341 will sit idle while I refamiliarize myself with Mozzi before I try to integrate them.) After I confirmed the simple sine wave sketch generated an audible tone on my test earbuds, I started my first experiment.

I wanted to verify that I understood my timing constraints. I added three counters: the first is incremented whenever loop() is called. The second when Mozzi calls the updateControl() callback, and the third for updateAudio() callback. Inside loop(), I check millis() to see if at least one second has passed. If it had, I print values of all three counters before resetting them back to zero. This test dumps out the number of times each of these callbacks occur every second.

loop 165027 updateControl 64 updateAudio 16401
loop 164860 updateControl 63 updateAudio 16384
loop 165027 updateControl 64 updateAudio 16401
loop 164859 updateControl 64 updateAudio 16384
loop 165027 updateControl 64 updateAudio 16401
loop 164860 updateControl 63 updateAudio 16384
loop 165028 updateControl 64 updateAudio 16400
loop 164859 updateControl 64 updateAudio 16385
loop 165027 updateControl 64 updateAudio 16400
loop 164858 updateControl 64 updateAudio 16384
loop 165029 updateControl 63 updateAudio 16401
loop 164858 updateControl 64 updateAudio 16384
loop 165027 updateControl 64 updateAudio 16401
loop 164858 updateControl 64 updateAudio 16384
loop 165029 updateControl 64 updateAudio 16400
loop 164860 updateControl 63 updateAudio 16384

Arduino framework calls back into loop() as fast as it possibly can. In the case of this Mozzi Hello World, it is called roughly 165,000 times a second. This represents a maximum on loop() frequency: as a sketch grows in complexity, this number can only drop lower.

In a Mozzi sketch, loop() calls into Mozzi’s audioHook(), which will call the remaining two methods. From this experiment I see updateControl() is called 63 or 64 times a second, which lines up with the default value of Mozzi’s CONTROL_RATE parameter. If a sketch needs to react more quickly to input, a Mozzi sketch can #define CONTROL_RATE to a higher number. Mozzi documentation says it is optimal to use powers of two, so if the default 64 is too slow we can up it to 128, 256, 512, etc.

We can’t dial it up too high, though, before we risk interfering with updateAudio(). We need to ensure updateAudio() is called whenever the state of audio output needs to be recalculated. Mozzi’s default STANDARD mode runs at 16384Hz, which lines up with the number seen in this counter output. If we spend too much time in updateControl(), or call it too often with a high CONTROL_RATE, we’d miss regular update of updateAudio() and those misses will cause audible glitches. While 16 times every millisecond is a very high rate of speed by human brain standards, a microcontroller can still do quite a lot of work in between calls as long as we plan our code correctly.

Part of a proper plan is to make sure we don’t block execution waiting on something that takes too long. Unfortunately, Arduino’s Wire library for I2C blocks code execution waiting for read operations to complete. This wait is typically on the order of single-digit number of milliseconds, which is fast enough for most purposes. But even a single millisecond of delay in updateControl() means missing more than 16 calls to updateAudio(). This is why we need to break up operations into a series of nonblocking calls: we need to get back to updateAudio() between those steps during execution. Fortunately, during setup we can get away with blocking calls.

New Project: Mozzi + AS7341

After looking at Adafruit’s AS7341 library and its collection of example sketches, I will now embark on a new project: make Mozzi play nice with AMS AS7341 sensor. If successful, it would solve a problem for my friend Emily Velasco who got me interested in the AS7341 to begin with. Her project uses Mozzi library to generate sounds but calls into AS7341 API would cause audible pops.

The problem is that AS7341 libraries from Adafruit and DFRobot both use Arduino’s Wire library for I2C communication. (Adafruit actually goes to their BusIO library which then calls into Wire.) These are blocking calls meaning the microcontroller can’t do anything else until the operation completes. This is especially problematic for I2C reads: We have to perform a write to send the register address we want to read from, then start a read operation, and wait for the data to come in. This operation is usually faster than an eyeblink, but that’s still long enough to cause a pop in Mozzi audio.

To help solve this problem, Mozzi includes a library called twi_nonblock. It allows us to perform I2C operations in a non-blocking manner so Mozzi audio can continue without interruption. Unfortunately, it is a lot more complicated to write code in this way, and it only supports AVR-based Arduino boards, so people usually don’t bother with it. Mozzi includes an example sketch, altering sounds based on orientation of an ADXL345 accelerometer. A few years ago, I took that example and converted it to run with a MMA7660 accelerometer instead.

If I could interface with an AS7341 using Mozzi’s twi_nonblock, it would allow color-reactive Mozzi sketches like Emily’s to run on AVR-based Arduino boards without audio glitches. But the AS7341 is far more complex than a MMA7660 accelerometer. I’ve got my work cut out for me but if it works, I will gain a great deal of confidence and experience on the AS7341 as well as a refresher on working with Mozzi.


Code for this project is publicly available on GitHub.

V-USB For Super Basic USB On AVR Chips

One of the gifts to Supercon attendees was a Sparkfun Roshamglo badge. While reading documentation on writing software for it, one detail that stood out about this Arduino-compatible board was the lack of a USB-to-serial bridge. Such a component is common on Arduino boards. The only exceptions I’m aware of are the Arduino Leonardo line using the ATmega32u4 chip which has an integrated USB module.

The ATtiny84 on the Roshamglo is far too humble of a chip to have an integrated USB functionality, so that deviation from standard Arduino caught my interest. In fact, not only does the board lack a serial-to-USB bridge, the ATtiny84 itself doesn’t even have a UART peripheral for serial communication with a serial-to-USB bridge. Now we’re missing not one but two things commonly found in Arduino-compatible boards.

What’s the magic?

vusb-teaserThe answer is something called V-USB, a software-only implementation of basic USB fundamentals. It is not a complete implementation, most notably it does not handle all the error conditions a full implementation must gracefully handle. But it does enough USB to support the Micronucleus boot loader. Which creates a very basic way to upload Arduino sketches to an ATtiny84 without an USB serial interface engine (SIE), or even a UART, on the ATtiny84 chip.

Yes, it requires its own custom device driver and upload tool, but there are instructions on how to make all that happen. The point is minimizing hardware requirements – no modification on the host computer, and minimal supporting components for the ATtiny84.

It looks like a huge hack, and even though SparkFun cautions that it is not terribly reliable and won’t work on every computer, it is still impressive what the V-USB people have done under such limits.