Extracted Magnets from Broken iPad Case

This was an aftermarket protective cover case designed to fit 6th generation Apple iPad tablets. Purchased from the lowest bidder on Amazon that day (*) it did its job for several years absorbing abuse until a corner broke off and the case could no longer latch in place. It turned out a case that wouldn’t stay attached was worse than no case at all, as the iPad fell out causing a dent on the back.

While remaining three corners of this case were still attached, they all showed cracks in the plastic. Thus it was retired and replaced with a new case. Before this broken case is sent to landfill, though, I wanted to salvage its embedded magnets. This cover has the option to fold into a stand, held in shape by magnets. Closing the cover is also something detectable by the iPad, so there’s a Hall sensor inside the tablet picking up a precisely located magnet.

Using a screwdriver for its steel shaft, I could feel tugs at several locations indicating a magnet. Given how thin they must be, I expected to find a few tiny slivers of rare-earth magnets. And given the thin fabric construction, I decided to start by cutting an edge with scissors.

First magnet was quickly uncovered.

Pulling on the glued-on fabric, I uncovered the remaining magnets embedded within the first panel. Four small rectangular magnets near the middle of the edge. Surrounded by two larger rectangular magnets on either side. There’s a circular magnet as well, away from the rest. I had only expected one or two magnets, so this is an unexpected bounty.

The magnets weren’t attached to the yellow plastic backing at all, merely held in place by adhesive on either side. They could be peeled off with minimal residue. This is working out really well.

Continued peeling discovered another circular magnet in the middle panel, and another set of rectangular magnets on the third panel that matched the arrangement of magnets in the first panel. Those two arrays of rectangular magnets on outermost edges would implement the fold-into-a-stand function. The two circular magnets don’t line up to each other, I guess they are there for iPad “cover is closed” detection.

I cut into this cover expecting just a magnet or two, so I’m very happy I came out with a stack of 18 little magnets. They are very thin so it should be easy to fit them into places in future projects. In fact, they are so thin I need to worry about protecting them. This material is brittle: I broke that topmost magnet in half when peeling it off the adhesive layer, a lesson warning me to be careful with the rest.

(*) Disclosure: As an Amazon Associate I earn from qualifying purchases.

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.

Sample Code Gave Incomplete Picture of AS7341 SMUX Configuration

Reading through Adafruit AS7341 Arduino library implementation of readAllChannels(), I was happy to see it mostly confirmed information I understood from datasheet reading. It also had something else: how to configure AS7341 sensor multiplexor (SMUX). Details of which were critical information completely missing from the datasheet, mere a note that AMS provides sample code. I say sample code is a poor substitute for proper documentation and my position remains unchanged.

I thought I might have a good shot at figuring out SMUX configuration registers, given that I have found three sample points. DFRobot has an Arduino AS7341 library. Adafruit has their own implementation of an Arduino AS7341 library. And hidden in the examples folder of Adafruit’s library is an Arduino sketch that doesn’t use Adafruit’s library at all, written by an AMS application support engineer. Comparing and contrasting between them should tell me a lot!

Unfortunately, as soon as I took a closer look my expectation went down in flames. These three examples are writing the exact same bytes to the exact same registers. There’s nothing to compare and contrast at all. This feels like someone at AMS wrote a SMUX configuration at one point and it’s just been copied and pasted ever since. All we really have to work with are the skimpy comments in these configuration routines.

Based on these comments, each register corresponds to a single sensor, except when it controls two sensors. Bits in the register control which ADC is connected. It was easy to infer that zero representing no connection to any ADC at all, but other than that the bits are inconsistent. Setting register 0x0F to 0x30 supposedly connected to ADC2, but for register 0x0A it was 0x03 that connected to ADC2. Some of the sensors have a “left” and “right” but it’s not clear what that means. My best guess is from the datasheet figure showing sensor array layout:

Maybe “F3 left set to ADC2” means connecting only the left side F3 sensor, leaving the right side F3 sensor unused? That would mean we’re using half the available sensor area to gather light, which seems sub-optimal.

And most critically, the comments weren’t enough for me to figure out how I’d create a different SMUX configuration. There’s nothing about any of these in the datasheet, which remains my biggest complaint to AMS. The only description I found in the datasheet is that flicker detection sensor must be connected to ADC5.

One odd thing I noticed about this copied-and-pasted code snippet is the fact it sends twenty bytes of SMUX configuration as twenty write operations of single bytes to sequential registers 0x00 through 0x13 inclusive. Why do it in such an efficient way? For my project I converted that to a faster and more efficient single write operation of twenty bytes and it seems to work.

The good news is that these sample code snippets help us configure SMUX to do something useful in line with functionality demonstrated by these samples. The bad news is that we don’t have enough information to design and build our own SMUX configurations to suit specific needs. I guess if someone wants to use AS7341 professionally they can ask AMS application support team to build a custom SMUX configuration for them. For an electronics hobbyist like myself, these fixed configurations are sufficient for me to complete my project, but I am still grumpy with AMS about it.

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) {
  if (f1_f4) {
  } else {

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.

Notes on Adafruit AS7341 Arduino Library Example Code

I’ve purchased an AS7341 breakout board from Adafruit (#4698) and read through their guide (single-page view) on the board. It’s nice to see their library takes care of the complexity of this chip’s internals such as SMUX configuration. Examples that come bundled with a library is always a good overview of capabilities, and this is what I see from its examples directory:

  • basic_counts: “Basic Counts” is a concept described in the AS7341 calibration application note. A bit of math that takes raw sensor counts and compensates for gain and integration time. AMS recommends application logic work with “Basic Counts”. This lets us change gain and integration based on environment and still keep the rest of the application logic unchanged.
  • flicker_detection: Prime concern of flicker detection is with household AC power frequencies of 50Hz or 60Hz however this sensor is not limited to those rates. The datasheet says it can detect flicker frequencies up to 2kHz. This capability feels like a potential project all by itself, I’ll have to think more about it.
  • get_channel: Performs a read of all channels then iterates through 10 spectral sensing channels of 8 visible spectrum + clear + infrared to print their raw counts. (Flicker detection channel is ignored.)
  • gpio_example: Using the “GPIO” pin as a digital output. High for 3 seconds and low for half a second and repeat. Important note: according to board schematic published by Adafruit, the GPIO pin is brought out directly to the headers. There is no voltage level shifting or protection on this pin so be sure to consult datasheet and be nice to this 1.8V part.
  • gpio_input_example: Using the “GPIO” pin as digital input. Samples every half second and prints either “high” or “low” to serial console.
  • led_test: The canonical “blink a LED” test implemented with the LED Adafruit installed on the breakout board connected to AS7341’s LDR pin. Lights up at 4mA for 1/10th of a second, dark for half a second, light up to 100mA for 1/10th of a second, dark for another half a second, and repeat. Note while the AS7341 constant current control register goes up to 258mA, we shouldn’t go that far. According to board schematic published by Adafruit, the onboard LED is an EAHC2835WD6 with recommended ceiling of 150mA and absolute maximum of 180mA.
  • plotter_example: A clever variation on get_channel designed to work with Arduino IDE’s Serial Plotter feature. Five comma-separated sensor values are printed on a line so that when we turn on Serial Plotter, the plotter line drawing colors are close to the color wavelength of that sensor.
  • read_all_channels: This is almost identical to get_channel. The only difference is this sketch passes an array into the call to readAllChannels() and prints from sketch’s own array. get_channel does the exact same thing using the array stored in Adafruit library object.
  • reading_while_looping: Most of these examples call readAllChannels(), which is a blocking call that waits for sensor integration to complete before returning with results. Depending on integration time configuration, delay caused by waiting can be problematic. This sketch illustrates a way to initiate AS7341 integration and yield control so other things can happen until integration is complete. Note: the I2C operations involved are still blocking calls via Arduino’s Wire library.
  • smux_test: I didn’t understand this sketch. It has an empty loop() and I don’t see how setup() tests SMUX in any way.
  • spectral_interrupts: By its name I hoped to see a demonstration of using AS7341 INT pin to generate a hardware interrupt signal for Arduino microcontroller, triggering an ISR (interrupt service routine) for sensor read. I was wrong, the “interrupt” for this sketch is merely a register flag that we read from within loop().
  • synd_mode: By its name I hoped to see a demonstration of using GPIO pin to trigger start of AS7341 sensor integration. This time I think I got my wish! Unlike all of the other examples, this sketch doesn’t use Adafruit’s AS7341 library and was not written by anyone at Adafruit. It uses the Wire library directly and was written by an AMS application support engineer.

Despite mysteries like smux_test, I see a good set of examples here to help me become familiar with AS7341 capabilities. A sensible person would start their exploration with simple modifications to one of these sample sketches. I did not go down that path of sensibility.

Adafruit AS7341 Board (4698)

I wanted to play with the AMS AS7341 11-channel spectral color sensor and the easiest way is to buy a breakout board making it easier to work with that tiny little 1.8V surface-mount chip. DFRobot has reasonable looking products but I ended up going with Adafruit’s AS7341 board, item #4698.

Compared with DFRobot’s offering, the Adafruit board has only a single LED instead of two. Neither of them brought out LDR pin for controlling external illumination. They both include all the voltage level handling necessary to allow 3.3V and 5V microcontrollers to talk to this 1.8V chip. DFRobot offered two products: one with their solderless Gravity connector system and a different offering compatible with 0.1″ pitch headers. Physically, Adafruit’s #4698 is slightly larger, but it has both 0.1″ pitch headers and their STEMMA QT connector. These are also known as JST-SH connectors, and I’ve already purchased a set (*) originally intended for use with BeagleBone Blue. Having both options for connectivity down the line is appealing to me, and I’ve been a longtime Adafruit customer. Which meant there were other things I wanted to buy from Adafruit anyway. It was minimal additional effort and cost to add AS7341 to an order and wait for it to show up.

Once the sensor arrived (UPS required a few extra days beyond original estimated delivery) I connected it to an ATmega328P Arduino Nano on a breadboard. As is typical of Adafruit, they have developed an Arduino library to communicate with this sensor, and I could load up the led_test example sketch as a quick test to see if it worked.

Yes, it works! I will now look over Adafruit’s full set of examples.

(*) Disclosure: As an Amazon Associate I earn from qualifying purchases.

Window Shopping DFRobot AS7341 Board

I’m intrigued by the AMS AS7341 11-channel spectral color sensor, and I’ve reached the limits of what I can learn from its (somewhat disappointing) datasheet and application note documentation. It’s time to get some hands-on experience with the device, which hopefully would help me understand more of the documentation in a positive feedback cycle. Plus I just want to start playing with it.

I could buy the sensor chip itself from Digi-Key, which has published a product page highlighting AS7341. Cost is reasonable for single quantities ($11.42 at time of writing) but at 3.1mm x 2mm x 1mm it is far too small for me to handle directly. I need a breakout board. The most obvious choice is AMS’ own evaluation kit, but that is priced far too high ($189.38) for me to stomach. Thankfully there are other companies providing AS7341 breakout boards for electronics hobbyists like myself.

DFRobot is one of these companies, and it has not one but two AS7341 offerings that differ by physical interface. Product SEN0364 has connectivity via DFRobot’s “Gravity” connector. Its sibling product SEN0365 has generic 0.1″ pitch breadboard-compatible headers. They both have mounting holes and voltage level shifters/converters for both signal and power interface with 3.3V and 5V parts. (AS7341 itself is a 1.8V part.) The Gravity port only has I2C communication, but the 0.1″ header version also break out INT (optional signal for “reading complete”) and GPIO (optional signal for “start reading”). Both include a pair of white LEDs for illumination controlled by AS7341 LDR pin, but that also meant LDR was not brought out so we can’t use it to control external lights.

On the software front, DFRobot provides an Arduino library and several example projects. I thought their household lights comparison test was very interesting, using an AS7341 to show difference in emission spectra of various household lights. I understood the spectral plots, but they lost me when the math started going into CIE color spaces like the AMS calibration data sheet did. The biggest problem with this example project is that its online listing is incomplete: the Arduino sketch for interfacing with AS7341 is listed, but the Processing sketch was missing. It plots results on a desktop computer screen and screenshots were included in the project writeup. Without it we’d just have a bunch of numbers in a serial terminal. It shouldn’t be too hard to recreate using Processing’s Serial library if one felt motivated to do so. Still, its absence was annoying.

Interesting but incomplete example projects won’t change the fact DFRobot SEN0364 and SEN0365 seem like reasonable options for AS7341 breakout boards. Another option for AS7341 breakout board is Adafruit product #4698, which is what I eventually bought for myself.

AMS AS7341 Calibration Application Note

I’m learning the ropes of using an AMS AS7341 11-channel spectral color sensor, figuring out what I absolutely need and what I will wait to explore later. So far, I have found AMS product datasheet to be lacking. The topmost irritation was missing information on configuring the sensor multiplexor, a critical component inside the chip. It was also missing information on higher-level concepts for a color sensor and how the AS7341 implements them. I found some of this information in the document titled Spectral Sensor Calibration Methods which is listed as an Application Note Calibration Methods AS7341 EVKs.

This document taught me how an ideal spectral detector would activate on a specific wavelength, but real-world implementation is sensitive to a range of wavelengths near that target. This is why its documented response curve is a series of bell curves and not a set of spikes. Furthermore, there may be responses far from the target wavelength due to a variety of other factors.

This foundation led to an explanation of why, in addition to 8 different wavelengths within the visible spectrum, the sensor also had three additional sensors: A “Clear” sensor with no color filter, a near-infrared sensor, and a flicker detection system. Originally I had thought they were just thrown in for the sake of more features, but this application note explains they support the 8 color sensors by detecting problems that throw off readings.

I have to admit I did not understand much of the discussion about plotting sensor readings into CIE color space. Color science is a field with a long history and reading Wikipedia pages only taught me that there is a lot I don’t know. Clearly this application note is aimed at an audience who is already familiar with the topic.

But even if I was familiar with CIE color spaces, this document left a lot of gaps in information. This application note referenced a piece of software called “AS7341 Software GUI, running on a Windows personal computer” but that software is nowhere to be found on AMS website. It appears to be something I can obtain by contacting my AMS account representative, which I don’t have. There are also snippets of a large Excel spreadsheet, with the disclaimer “Tables were interrupted. See the full tables in the original MS Excel File.” And is this MS Excel file on the website? Apparently not: “Ask the ams support team for the original XLS file” And there was a section that ended with “For more details, please refer to the Sensor ECGs manual.” What is ECG? That was not explained. And so on.

I learned a lot from AMS documentation for AS7341 but I felt there were significant room for improvement. Nevertheless, I am confident enough in my understanding to get hands-on with AS7341 sensor.

Additional AMS AS7341 Sensor Functionality

I’m starting to get a handle on the basics of an AMS AS7341 11-channel spectral color sensor. I know how to control sensitivity and exposure time, and I know I lack information on configuring the sensor multiplexor (SMUX) within the device. These are critical parameters for taking any readings and I need to understand them up front. For the moment I’ll postpone the following auxiliary functions that I (hope) are not crucial to sensor operation.

Given this chip only has 8 pins to the outside world, I was surprised that one pin (LDR) was allocated to the task of sinking current for a LED. But it made sense once I thought about it: a source of illumination is a common need for spectral sensor applications. According to figure 37 describing the LED register, it is a constant-current sink that defaults to 12mA which is more than enough to illuminate a single LED. The register implies quite a range of current handling: we can set it to go as low as 4mA and as high as 258mA. A quarter of an amp is quite a lot for a single LED so I guess this is for driving multiple LEDs in parallel. For example, if we want to surround the sensor with a ring of lights. Even then, at a quarter of an amp I’d start to worry about thermal issues.

And heat is definitely a concern for this sensor, judging by its built-in capability to compensate for device temperature. This “autozero” capability takes about 15ms to perform and we can configure its frequency in the AZ_CONFIG register 0xD6. By default, autozero runs once before taking first reading and never again. We can configure it to run more frequently, in terms of once every X readings. I don’t know how important temperature compensation is for this sensor. There’s a chance it is only important for those who need absolute precision, but it is important enough to run at least once. (Datasheet recommends against setting register to zero.)

Back to pins on the chip: INT can be configured to signal that a sensor reading is complete, intended to be connected to a microcontroller pin that signals an interrupt. This is not required as we can optionally poll the STATUS2 register via I2C for a bit indicating sensor reading is complete. And finally, a pin can be used to start a sensor reading but can also be used for general input/output hence it is labeled GPIO. This is also optional: we can start a sensor reading via I2C by setting a bit in the ENABLE register.

These features and others are neat but are not required for taking spectral color sensor readings. Either because they’re not required at all (LED), or their default values are good enough to start (autozero) or we have alternatives (INT and GPIO.) I can come back to play with these later. For now, I want to learn some higher-level concepts about this sensor via its color calibration application note.

AMS AS7341 SMUX Configuration is Critical Yet Absent from Datasheet

I want to understand the capabilities of the AMS AS7341 11-channel spectral color sensor and started orienting myself with its datasheet. The sensor seems quite capable but also quite complex to operate. The biggest barrier on the critical path (I must understand it to do anything with the sensor) is SMUX or sensor multiplexor. The onboard ADC (analog to digital converter) only has 6 channels to serve 11 sensor channels, the SMUX decides which subset of 6 is read at any given time.

Given their importance, I was quite baffled to find no documentation in the datasheet describing SMUX configuration registers. The closest thing I found was in section 8.4:”ams provides reference code and an application note on how to configure the SMUX.” First of all, sample code is not a substitute for proper documentation. Second, I see neither sample code nor SMUX configuration application note on the product’s supporting documents section. This is… unsatisfactory.

I hope I can resolve that “WTF?” item later, so I set that aside and continued learning about sensor parameters. I started thinking of this sensor as a small digital camera and I could use photography analogies to understand AS7341 parameters. A digital camera has three important variables in every shot:

  1. Aperture size
  2. Shutter speed controlling duration of exposure
  3. Film ISO equivalent controlling sensor sensitivity.

For an AS7341:

  1. It is a point sensor, so its aperture size is a pinhole camera and not adjustable.
  2. Integration time parameters control duration of exposure, equivalent to shutter speed.
  3. Gain parameter control sensor sensitivity, equivalent to ISO.

Integration time is controlled via parameters ATIME and ASTEP. ATIME is a single register at 0x81, and the entire range of 8-bit values (0-255) are valid. ASTEP is a 16-bit value split across two registers: 0xCA (low byte) and 0xCB (high byte.) 65535 is a reserved value for ASTEP, but 0-65534 are valid. Together they control the ADCfullscale parameter, which is defined as (ATIME+1)*(ASTEP+1) and has this footnote:

The maximum ADC count is 65535. Any ATIME/ASTEP field setting resulting in higher ADCfullscale values would result in a full-scale of 65535

This I found curious: if the maximum is 65535, the maximum possible representation in 16-bits, why do we need ATIME at all? ASTEP can cover the entire 16 range all by itself rendering ATIME superfluous. There’s a story here missing from the datasheet.

As a starting point for exploration, the datasheet listed 50ms (ATIME 29 ASTEP 599) as the typical integration time. I’ll start there and go higher or lower as needed. And as I’m just starting out, I hope I can safely ignore some of the auxiliary features until later.

Notes on AMS AS7341 Core Features

I have some grand dreams about what I might do with an AS7341 spectral color sensor, but things are always easier in the fantasy world than in the real world. To turn ideas into reality (or to see if it’s realistic at all) I need to learn the nuts and bolts of the sensor. Which means starting with its datasheet downloadable from AS7341 product page on AMS website. It answered some questions but opened many more.

The first and most important data point is its I2C address: 0x39 and I found no way to change it. (Datasheet section 9.1) It means this sensor is not designed to work in conjunction with others of its kind on the same I2C bus, there can be only one. The sensor is still responsive to I2C traffic when in sleep mode, which might be useful. (Datasheet 8) Also useful is a device identification register to verify I2C communication is working properly. (Datasheet 10.2.4)

The next important note was the chip’s internal architecture (Datasheet 8.1): there are 11 different sensors that could be read, but onboard ADC (analog-to-digital converter) has only 6 channels and a sensor multiplexor (SMUX) which controls which sensor is connected to which ADC and which are left disconnected. In order to read all 11 sensors, we need to make one read operation with 6 sensors then reconfigure SMUX to read the remaining 5. This architecture hints at the challenges ahead.

Each of these ADC channels have 16-bit resolution, and some configuration parameters are 16-bit values as well. This chip organized its registers such that the low order byte comes first, immediately followed by the high order byte. Read operations latch these registers, so the value does not change between reading the low byte and reading the high byte. (Datasheet 9)

With this information, I think I have enough basics to understand how to take a reading with AS7341.

Successful LinuxCNC Stepper Motor Test

Setting up an old PC to explore LinuxCNC was pretty easy. I had most of the hardware sitting around, the only thing I had to buy was a PCIe parallel board built around a MCS9900 chip which I chose for its LinuxCNC support. The next step is to connect it to some mechanical hardware to see if it even works. In order to do that, I had to find the physical addresses assigned to the parallel port card. I’ve been playing around with PC hardware long enough to remember add-on cards that required fussing with jumpers to set hardware addresses, but that hasn’t been necessary for decades. PCIe cards are assigned their resources automatically and modern software had ways to enumerate and find those values. For LinuxCNC we had the first part — automatic assignment without jumpers or such — but for whatever reason we do not have the second part.

PCie NetMos 9900 parallel controller at e010

Instead of LinuxCNC automatically finding the parallel card and figuring out what to do with it, the setup person has to run Linux command line utilities to find these numbers and write them down for input to LinuxCNC later.

The next task is to find an old school parallel cable. This was surprisingly difficult in this day and age. Every longtime PC user would claim they have several in a box somewhere, but unable to find the right box because it’s been so long.

Parallel cable beheaded

Eventually one was found so I could cut it up to access wires within. I just need two signal wires (plus ground wire) for the first test, driving “step” and “direction” control signals of a motor controller.

The motor hardware visible here was a hybrid stepper motor system(*) that was sitting on a shelf and available for experimentation. The “hybrid” in this case meant something that could accept step/direction commands like a stepper motor, but unlike normal stepper motors this system has a closed-loop feedback system. Normal stepper motors are open-loop, meaning they just go through their motions but have no idea what the motor output shaft is actually doing. It might miss a step or two and the system wouldn’t know. This hybrid system includes a motor shaft position encoder, so it knows when a step is missed and can compensate. Such systems are more expensive but allows more efficient operation (use just enough electric power to deliver commanded steps) and more usable power (don’t need to allocate as much to error margin.)

This successful was completely within the LinuxCNC stepper motor configuration wizard and its “Test Axis” button. It established step/direction works as expected, and that acceleration/deceleration curves are smooth in practice. But this is very far from running a G-Code program in LinuxCNC. It doesn’t tell me if multiple axes will coordinate successfully in multi-axis motion, and it is far too short in duration to prove long-term reliability. Still, it’s a good start, and I’m a little sad I didn’t get to go further.

We had our pandemic lockdown shortly after this milestone. The workspace where I had been working on this homebrew CNC project was no longer available. I got my LinuxCNC PC back, but the hybrid stepper motor is now out of reach. I was given the Parker XY stage itself (including the two driver boxes) and the spindle I bought, but the rest of the gantry test system was disassembled and returned to their respective owners. I hope to resurrect this project at some point, but its future is uncertain.

(*) Disclosure: As an Amazon Associate I earn from qualifying purchases.

PCI Express Parallel Port Card for LinuxCNC

While putting together a PC to investigate LinuxCNC, I installed a wireless networking card that turned out to be unsupported. I was able to shrug that off as unimportant, but that would not be true of the next hardware project: adding a parallel port to the machine. Parallel ports have been around for as long as the PC platform has been around and is the closest thing a PC has to GPIO pins. (Actually, mostly just output.) There are many fancier options with add-on cards with programmable FPGA and such, but they all boil down to low-latency output signal pins and parallel port is the pioneer for all that followed.

The starting point for this investigation is the LinuxCNC wiki for parallel port cards. I would not have been surprised if they focused on ISA cards of the original IBM PC, but fortunately they aren’t quite that old school which is good. Most of the entries talk about far more modern PCI cards. As I understand it, something using the MCS9865 chip (*) is the gold standard, preferably a dual port version for double the pins. There are other pages written by people reporting good results, but many (like this page about Netmos 9815) are out of date and no longer available.

Unfortunately, the only expansion slot on this MSI AM1I Mini-ITX is a single PCI-Express (PCIe) slot intended for a GPU. Though handled by the same industry consortium, PCI and PCI-Express cards have physically incompatible shapes. I went to Amazon looking for PCI-Express parallel cards that explicitly mention Linux CNC. Thanks to the similar name, Amazon would show PCI (non-express) cards which I can’t use. And for reasons I don’t understand, some USB adapters were shown(*) even though they would not work for this purpose: USB adds an unpredictable latency unacceptable for direct machine control.

Another tack I tried is to search for LinuxCNC wiki for parallel port chip identifiers. AX99100 came up empty. WCH382L boards are available (*) but they have been problematic and no longer recommended. OXPCIe952 boards are available (*) and LinuxCNC support for this chip has apparently matured past an early problematic teething period. Out of chips found on PCIe boards, the MCS9900 appears to have the longest track record. So I decided to try this unit (*) as my starting point.

PCIe parallel port card installed

The interface card itself installed easily, just like countless other PC add-on cards I’ve installed. The real proof requires connecting it to some mechanical hardware.

(*) Disclosure: As an Amazon Associate I earn from qualifying purchases.

LinuxCNC has Limited Hardware Support: Wireless Card Example

An important part of my home-built CNC project is to learn more about what goes on under the hood of CNC equipment, arguably more interesting to me than actually getting a machine up and running doing something productive. Which is why I decided against buying an all-in-one CNC control system and started putting together a machine to play with LinuxCNC instead. I was surprised to learn conventional wisdom for LinuxCNC has a preference for older machines. Steady and predictable is better than infrequent bursts of high performance. Modern PCs tend to be optimized for the latter.

Another reason is hardware support. While LinuxCNC is indeed built on a Linux kernel, there is little motivation to adopt the latest and greatest Linux features. LinuxCNC aims for steady reliable machines, which means avoiding new features if they might make machines less reliable. Older code means support for older hardware, and fewer hardware. Something that runs on a modern commodity Linux distribution has no guarantee LinuxCNC cares about it. I learned this lesson up front with a wireless card.

The motherboard I chose to use for LinuxCNC exploration is the MSI AM1I board that had been the heart of my home FreeNAS (now TrueNAS) server for several years before being decommissioned. It is the oldest PC motherboard I have right now, and it has a proven history of reliability. While I had the motherboard accessible for installation in a steel tower case, I thought I’d add wireless Ethernet capability to the system. This motherboard has a Mini-PCIe slot intended for a wireless card, and I had salvaged an appropriately sized card from a retired laptop.

Intel wireless-N 1030 card installed

It installed easily.

Intel wireless-N 1030 card antenna

I had also salvaged two antenna that had connected to this card. In the laptop, the wires were routed through screen hinge to connect to these antennae sitting within upper left and right corners of the screen. Now I shall route them to a plastic faceplate covering for an absent optical drive.

This was a widely supported Intel wireless card that had worked in the laptop (before it died) and still worked when I booted Ubuntu on this computer. LinuxCNC recognized it as a piece of hardware on the PCIe bus, but there was no networking connectivity. I had a wired Ethernet backup option readily available, so I didn’t spend time diagnosing how to connect to a network with this hardware. I’ve learned my lesson and put more research into the next piece of hardware: a PCI-Express Parallel port card.

Window Shopping RATTMMOTOR CNC Controller with Digital Dream

I determined that a Mach 3 CNC pendant wasn’t going to work in LinuxCNC with merely configuration file changes, so I started looking at other pendant options. I found this bare-bones option(*) that had only a few controls and no display screen, and almost every single function is broken out into its own physical wire. This fits very well with the LinuxCNC model built on the idea every I/O is a pin. Even when it’s not literally a pin on a parallel port, there’s a logical pin underneath for LinuxCNC configuration.

And while looking at these pendants designed for hard wiring, I found a controller that bundles such a pendant with a complete self-contained CNC controller unit. It is more capable than Grbl on an Arduino, but less than full LinuxCNC on a PC. It calls itself an offline motion controller system(*), running software from Digital Dream Automation. At a quoted maximum pulse rate of 500 kHz, it significantly outperforms Arduino Grbl which is reported to top out at around 30 kHz. This unit can also read G-Code from a USB flash drive, eliminating the need to have a separate computer running some sort of G-Code sender program to Grbl. A nice self-contained (I guess that’s what they meant by “offline”?) system.

Minor problem: G-code visualization options on such a little screen seems to be limited to 2D plots, giving up one of three axes. I’ve seen old CNC with the same limitation so it’s not a huge deal, but it is less than what we can do with modern PC-based systems.

Moderate problem: How would I generate G-code for these units? This particular unit’s owner’s manual explains firmware updates would be downloaded from ddcnc.com, and that’s how I found Digital Dream and presumably they are the author of software running on these units. Unfortunately, a quick search for Digital Dream on Fusion 360 post processor library came up empty. I don’t know how owners of DDCNC-based controllers like this one generates their G-code programs. The user manual has a list of supported commands, but I’m not familiar enough with the various styles to recognize if DDCNC uses the same set of code as something else.

Major problem: Buying such a product instead of setting up LinuxCNC comes with a drawback: I can’t dig into the code and learn how CNC controllers work under the hood. And that is the primary point of my CNC controller explorations. If someone asks me for a ready-made upgrade over Grbl on Arduino or ESP32, I’m happy to let them know these controllers exist. But for myself, I’ll resume my LinuxCNC explorations.

(*) Disclosure: As an Amazon Associate I earn from qualifying purchases.

Brief Look at a LinuxCNC Pendant

Trying to build a little CNC is definitely a learn-as-I-go project. Moving the motor control box was a simple (though necessary) mechanical change, but not the only idea prompted by initial test runs. I also thought it would be nice to have a handheld pendant to help with machine setup, instead of going to the laptop all the time. I got a chance to look over a CNC pendant to see how I might integrate one.

This particular unit was purchased from this eBay vendor listing, but there are many other similar listings across different online marketplaces. Judging by this listing’s title, the popular keyword salad included: CNC Mach3 USB MPG Pendant Handwheel. I knew what CNC, USB, pendant and handwheel referred to. MPG in this context means “Manual Pulse Generator” referring to the handwheel that generates pulses to signal the CNC controller to move individual steps. And finally, Mach3 is a Windows software package that turns a PC into CNC machine controller.

My first draft CNC controller was built on an ESP32 without USB host capability, so there’s little hope of integrating this USB pendant. The most likely path would involve LinuxCNC, a freeware alternative to Mach3. Poking around documentation for control pendants, the first hit was this link which seems to be talking about units that connected via parallel port. Follow-up searches kept coming across this link for wireless pendants which I didn’t think was relevant. After coming across it for the fifth or sixth time, I decided to skim the page and saw that it also included information about a wired USB pendant. It’s not a direct match, though. Here’s information from Ubuntu’s dmesg tool after I plugged in this pendant.

[ 218.491640] usb 1-1: new full-speed USB device number 2 using xhci_hcd
[ 218.524920] usb 1-1: New USB device found, idVendor=10ce, idProduct=eb93
[ 218.524926] usb 1-1: New USB device strings: Mfr=1, Product=0, SerialNumber=0
[ 218.524931] usb 1-1: Manufacturer: KTURT.LTD
[ 218.538131] generic-usb 0003:10CE:EB93.0004: hiddev0,hidraw3: USB HID v1.10 Device [KTURT.LTD] on usb-0000:00:10.0-1/input0

The key here are USB identifiers idVendor and idProduct, 0x10CE and 0xEB93. I could change those values in the associated udev rule:

ATTRS{idVendor}=="10ce", ATTRS{idProduct}=="eb93", MODE="666", OWNER="root", GROUP="users"

But that was not enough. I dug deeper to find relevant source code and it is explicitly looking for idVendor:idProduct of 0x10CE:0xEB70.

dev_handle = libusb_open_device_with_vid_pid(ctx, 0x10CE, 0xEB70);

Oh well, getting this to run would go beyond just configuration files, there will need to be code changes and recompiles. Looks like some people are already looking at it, a search for eb93 found this thread. I don’t know enough LinuxCNC to contribute or even understand what they are talking about. I returned this USB pendant to its owner and set this idea aside. There are plenty of CNC pendant offerings out there I can revisit later, some of which are even bundled with an entire CNC control package.

Moving CNC Spindle Control To Equipment Panel

Thinking about CNC milling circuit boards might have been looking too far ahead. But I also made some mechanical changes after the successful engraving session. There was a distinct buzzing sound of vibration caught my attention. Unlike earlier tests with an endmill, this engraving tip removed very little material and I thought overall noise would be reduced. Most of it were, but one particular sound stayed the same and I wanted to know what it was.

A little bit of investigation found the source of the buzz inside my spindle motor controller box. Bolted near the spindle up on our Z-axis gantry beam, it was installed in that location purely out of convenience. A decision that was apparently not well thought out and contributing to headaches. Earlier we found the box had contributed electrical noise to the system, now I realized it contributed mechanical noise as well.

In addition to those problems, its current position also blocked the most promising path for us to install a dust collection system. And even if it was not directly blocking, we wouldn’t want it to be near dust path anyway.

All of those factors motivated a move from its current gantry-mounted position down below decks to the equipment plate where the Parker motion control X/Y stepper driver modules are mounted. In additional to longer wiring to cover the distance, a few other enhancements were made. We had a single capacitor installed on the motor wire to help our previous episode of electrical noise. Now the wire is longer and even more likely to turn into an antenna, so now we have one capacitor at each end. There’s a ferrite core added to each end as well, and the ground wire is now bolted to the equipment panel plate. All of these should give us better electrical noise resistance than before.

In addition to the motor wire, I extend wire connecting enable pin to E-stop so spindle power is cut when E-stop is pressed. I also extended the control wire for speed. Automated operation would require automatic speed control via Grbl, but this will do until I get around to it.

Spindle control mounted below

Side amusement: since this project involved mains voltage, I unplugged this box before moving and rewiring it. Then I immediately forgot I had unplugged it.

Once I plugged it back in, things started running as expected. This is a small step forward, something I like to interleave with investigations looking further ahead.