Lab 5
Please Log In for full access to the web site.
Note that this link will take you to an external site (https://shimmer.mit.edu) to authenticate, and then you will be redirected back to this page.
Learning Objectives
Today's lab will focus on assembling and testing your sensor boards. We'll assemble your boards and then undertake various activities with them.
Our accelerometer
This first board of yours is a 3-axis accelerometer, meaning it measures motion in X-, Y-, and Z-directions simultaneously. We'll also refer to it occasionally as an IMU, or inertial measurement unit.
We'll talk in lecture at some point about what's inside this chip, but it's pretty amazing. There is an actual tiny little mass on tiny little springs, and when that mass experiences acceleration, it moves (F=mA, remember?), deforming the springs, which we can measure electrically. These devices are insanely sensitive1
True IMUs measure not just acceleration but also rotation, magnetometry (to find North, for example). Ours is pretty basic (just acceleration), but it is still a super-duper powerful IC. It has alot on on-board computation to add value to an overall system. We're only going to touch the surface of what it can do, but if you end up using an IMU in your semester project, you'll want to dig in a bit deeper into what it can do.
OK, let's get started.
Plugin Installation
KiCad has a really neat plugin that can be activated which provides an easy-to-read "GUI" of your board and its components and elements. It can be very helpful in the assembly that you'll be doing. To install, first go into your main KiCad page and Tools -> Plugin and Content Manager.
In there, look for the Interactive HTML BOM plugin. Install it.
Once installed open up your PCB that you want to use with the plugin. In the top panel there should now be a bright-green button you can push corresponding to this plugin. Click it.
In the window that comes up, make sure to check the options about including nets and traces. When ready, then go and click Generate BOM.
You should be transferred to your web browser (hopefully it doesn't block this) and it'll present you with a web page that is your board along with all your components. There's different features you can look at including parts that need to be assembled:
As well as your traces and other angles of your board.
This tool can be really help when assembling since it will tell you what parts go where in a very easy-to-read and visualize manner.
Sensor board assembly
We have our sensor boards back! This is very cool -- these are designs you made and you laid out and you messed up (jk...hopefully). Right now we just have the boards themselves, so we'll need to solder components onto them to make them functional circuits. We've already had several rounds of board assembly and component soldering, so you are all pros.
Surface Mount Assembly
As we did for the LDO board in lab01, we're going to assemble the surface mount components first, then do the through hole component.
First, get the parts!
-
Mark down the values for the passives that you need from your KiCAD BOM. The passives (resistors and caps) are either laid out in the middle table in the 6.9000 parts organizer or in the reels of parts on the windowsills of EDS.
-
Cut and take only what you need! But before you cut, use a sharpie to label the section of reel, since they all look the same after you cut them! Some things like the resistors will have codes on them to read and figure out values, but the capacitors will basically be un-figure-outable. Get the LED from the staff table.
-
Get one FXLS8962 IC from the bin at the staff table. It's tiny. Do not lose it.
Apply Solder paste
-
The next step is to place a thin bit of solder paste on the exposed pads of the SMT components with a syringe, same as in lab01. The main difference is that our passives are a bit smaller (0805 vs 1206), so put down less paste.
-
The pads for the FXLS8962 IC are really close together. You won't be able to apply paste to each individual pad. So just smear a tiny bit of paste across the pads. As long as you don't put down too much, you'll be ok.
Feel free to ask the staff if you'd like a quick check on your paste job!
Place the parts
-
Place the Rs and Cs on the pads with tweezers. The orientation of the resistors and (non-polarized) capacitors does not matter for our board.
-
Place the LED. The LED does have an orientation. Most of our LEDs are from Broadcom (datasheet), but our green LEDs are from Lite-On (datasheet). They each have a line or mark denoting the cathode (negative) terminal. If you get the orientation wrong, it won't work!
-
Place the IC. Take care to orient the chip so that the pin 1 marker on the chip is aligned with the pin 1 marker on the silkscreen of your board. You can look at the chip datasheet to see where pin 1 is. Get the pads pretty well aligned with the pads on the PCB, even though the solder paste will smush around.
Reflow
- Place it on the hot plate and reflow, like you did in lab01. Remember that the reflow isn't complete until a bit after the smoke appears. You want to wait for the paste to get shiny and liquid and the parts to snap into position.
Inspect
- Before attaching the through-hole components, inspect your solder job under a microscope. In particular, look at the FXLS8962 pads at and angle and ensure that there are no solder bridges between pads.
-
A good way to tell if there are solder bridges is to probe adjacent pins. Take a look at your board layout. Pin 1 on the IC is VCC. Pin 2 is GND. Take a multimeter and probe those two nets (use convenient TPs, or connector pins, whatever), you should read either open or something fairly large, like ~MOhm. If you see just a few Ohms or less, you have a short. Then go probe between pins 2 and 3, pins 3 and 4, and so on.
-
If there are still solder bridges, you can go and try to reflow again. Or you can go to a soldering iron and suck up some of the solder with solder wick.
Thru-hole assembly
- Obtain a 2x3 IDC connector header and place it in your board in the correct orientation. Pin 1 on the connector should be oriented toward pin 1 on your PCB.
- Solder the connector header using a soldering iron.
Make a cable
In the last lab we made up some JST cables. In this lab we're going to show you a different style of cable. Specifically, we will make a 6-conductor ribbon cable using IDC (insulation displacement contact) connectors.
Ribbon cables with IDC connectors are neat because you don't have to peal the insulation -- the connector has little teeth that cut thru the insulation to make the connection. It's super fast.
Making Our Own Cables!
- Peel off 6 conductors from a ribbon cable and cut into a ~6" (15 cm) long segment. You can use the gray ribbon cable or the multicolored one. I like to use colors because it helps me keep track of orientation, but you do you.
- Grab two 6-pin IDC connectors. We have two types. One has a little triangle indicating pin 1. The other has two little bars that polarize the connector, allowing the connector to fit into the housing in only one orientation. I prefer the latter as it prevents connection mistakes and frying my board.
- Insert the cable into the opening of one connector, past the end, and keep it straight. Orient the cable & connector as shown in the image below. This orientation is ideally suited for the mainboard IDC connector housing.
- Clamp down on the connector to make close the contact. I like to use a set of pliers to do it. It's very satisfying.
- Now insert the second connector.
-
Double-check the orientation of the second connector. Now is your change to fix it if it's backwards.
-
Clamp down on the second connector. When you're finished, your cable will look like so:
- Finally, because we are not animals, use a diagonal cutter to cut the overhanging cable bit flush with the connector.
Final assembly
Some soldering
Our 2x3 IDC connector has an SDA pin, and SCL pin, and an INT1 pin. We need to connect those to the appropriate pins on our ESP32C3. In the middle of your mainboard are little solder jumpers that allow you to connected SDA and SCL to IO3..7, and INT1 to IO0 or IO1.
-
For I2C, choose the same I2C pins as you've been using for your BH1750 board. Use a soldering iron and some solder to connect the relevant solder jumpers.
-
For IN1, choose a free pin, i.e., nothing should be connected to it on the breadboard. Solder the jumper for that pin.
Attachment and cabling
-
Attach the accelerometer board to the last remaining attachment post on your mainboard. By now you know how to do this: one (1) M3 screw and you should be good.
-
Attach the cable between the 2x3 IDC connector on your mainboard and the 2x3 IDC connector on your accelerometer board.
Add an LED
For the exercises that we'll undertake below, we'll need an LED.
- Find an LED in the lab (red is fine), and attach it and a suitable current-limiting resistor (~200 Ohm) to a free GPIO (I used GPIO4, but there are plenty of others) and your mini-breadboard.
Ask a staff member to look over your assembled board.
Testing
Proof of life
OK, let's run some code to see this accelerometer working.
-
Fire up VS Code.
-
Instantiate a new ESP-IDF project, set the target to esp32c3.
-
Unzip this fxls8974 library and place it into a
Componentsfolder in your project directory. -
Update the main
CMakeLists.txtto:
idf_component_register(SRCS "main.cpp"
INCLUDE_DIRS "."
REQUIRES fxls8974
PRIV_REQUIRES esp_driver_gpio)
- Replace your
main.cppwith the unzipped version of this code. Open it up and let's take a look.
// Initialize IMU with I2C configuration
if (!imu.begin(false, I2C_SCL, I2C_SDA)) {
ESP_LOGE(TAG, "Failed to initialize FXLS8974");
return;
}
Here we're initialize the accelerometer, using the I2C pins above.
// Proof of life!
ESP_LOGI(TAG, "FXLS8974 Product ID: 0x%02X", imu.getProductID());
// Set some acceleration parameters
imu.setFSR(2);
imu.setMagMode(1);
imu.setMode(1);
vTaskDelay(pdMS_TO_TICKS(10)); //tiny delay needed
AccelData accelData; // instantiate data structure
Now our first method call, on getProductID. This is the one difference we've found between the FXLS8974 and the FXLS8962 that we are using -- they have different product IDs. No biggie.
Then we set some accelerometer parameters. setFSR sets the Full scale range in G's. So the max this accelerometer will measure in this setting is +/-2Gs. There are plenty of other settings. +/-2Gs is the most sensitive setting, but there are plenty of use cases where you don't want to be so sensitive, or you want to be able to measure larger accelerations (up to +/-16G for this part)
Then we setMagMode to 1, which causes the accelerometer to calculate the magnitude of the acceleration, in addition to the X-, Y-, and Z- components. So that we don't have to do floating-point math. Thanks, little dude.
Then is the important setMode method, which when set to 1 basically tells the IMU to get going and start measurements. We need a tiny delay afterward, but basically we're good to go.
accelData is a useful data structure to hold all the data from the IMU.
Finally, a while loop:
while(1) {
if (imu.readAccelData(accelData) != 0) ESP_LOGW(TAG, "Data not ready");
if (imu.readAccelMag(accelData) != 0) ESP_LOGW(TAG, "Mag data not ready");
ESP_LOGI(TAG, "Raw Accel: %d %d %d %d",
accelData.rawX, accelData.rawY, accelData.rawZ, accelData.rawMag);
ESP_LOGI(TAG, "Accel: %.2f %.2f %.2f %.2f",
accelData.x, accelData.y, accelData.z, accelData.mag);
vTaskDelay(pdMS_TO_TICKS(delayTime));
}
When we call the readAccelData and readAccelMag methods, we also check if the accelerometer has new data ready to read. This should rarely be an issue for our code, as the overall while loop is executing every 500 ms, much much slower than the accelerometer can measure (it's maximum data rate (also the default), called the "output data rate" is 3200 Hz). That said, I see occasional warning about the mag not being ready. Not a big deal.
Then we read the print the acceleration raw data (in bits) and the data in Gs.
- Build and flash the code and open the serial monitor. If everything worked, you should start to see accelerometer readings:
I (269) FXLS8974: FXLS8974 initialized successfully
I (269) IMU: FXLS8974 Product ID: 0x62
I (289) IMU: Raw Accel: -40 29 1020 1004
I (289) IMU: Accel: -0.04 0.03 1.00 0.98
I (789) IMU: Raw Accel: -40 20 984 992
I (789) IMU: Accel: -0.04 0.02 0.96 0.97
I (1289) IMU: Raw Accel: -40 49 1004 997
I (1289) IMU: Accel: -0.04 0.05 0.98 0.97
I (1789) IMU: Raw Accel: -16 8 1008 962
I (1789) IMU: Accel: -0.02 0.01 0.98 0.94
Make sure you understand all these numbers! The datasheet can help explain.
When you get things working, find a staff member and allow them to join you in celebration of your first working PCB! Then explain what all those numbers mean.
Build a tilt detector, the hard way
One common use case for an accelerometer in the semester projects is to tell if the sensor node has tipped over. We can figure this out from the accelerometer readings, because gravity always points down, and the if your IMU is horizontal, then down is along the Z-axis of the chip. '' Throughout the next set of exercises, we want to evaluate different ways of detecting tilt, and in particular issues of latency. The LED will help us assess latency.
In our first approach to sensing tilt, write a function that returns true if the accelerometer orientation (and hence the mainboard) has tilted at least 45 degrees from horizontal.
bool isTilted(AccelData current) {
}
Now, insert that code into this slightly modified version of main.cpp (or create a new project if you prefer to keep the old code around).
You may need to update the ledPin variable depending on what you used for your LED.
In this code, when the isTilted function returns true, the code prints a message to the serial monitor and turns on the LED. The LED then turns OFF after a delay given by delayTime, which is currently 1000 ms. If the device is still tiled the next time through the loop, the LED will get turned back on. And so on.
Build, flash, and open the serial monitor. Now, if you did it correctly, the ESP32C3 should report if you tilt the mainboard and the LED should turn ON when that occurs. Try it and make sure it works when you tilt in various directions.
There are a few limitations of this approach. First, it assumes that the device was set up to be perfectly horizontal to begin with. It would be a bit smarter to take an initial reading of the angle and then check whether that angle changes substantially.
There are three bigger issues, though. First, we are detecting tilt via polling, meaning we keep looping and checking, which means the processor is always working, wasting power. Second, notice that there can be a delay between when you tilt and when the LED turns ON. This is due to the 1000 ms delay that we've inserted into our loop. Third, we are not taking full advantage of the IMU's capability, because, as it turns out, the IMU has an innate ability to detect tilt directly.
Build a tilt detector, a somewhat better way
The FXLS8974 (or, ok, the FXLS8962) has a built-in orientation detection subsystem. You can tell it exactly how much of an orientation change you'd like to detect (the default is 45 degrees) and it will set a register bit if that change occurs. It's very nice.
To use the orientation detection feature, we need to set bits across a few registers. Luckily for you, our library makes that easy.
If you add the following lines to the beginning your app_main where you configure the IMU:
imu.orientEnableDbcn(1);
imu.setorientDbCount(100);
imu.orientEnable(1);
The first two lines enable a debouncer, meaning that the orientation detection will only fire if the orientation persists for a period of time, in this case for 100 readings (which, at 3200 Hz, is around 31 ms). Then the last line turns on the orientation detection.
You'll also want to initialize an instance of the OrientData data structure (and you can get rid of the AccelData one, which we don't need anymore):
OrientData orient;
The key method call is getOrientation. Take a look at the library code for this method in fxls8974.cpp. Like many other methods in the library, we are either getting a register value or setting a register value.
In this case, what is the name of the variable corresponding to the register we are reading?
And what is the name of the corresponding register as per the data sheet?
We next set a bunch of OrientData structure variables using various bitmasks.
Which bit is read to update the NewOrient variable?
Look through the datasheet to make sure you understand what that bit corresponds to. Also read and make sure you understand the other bits in that register (LO, LAPO, BAFRO). Basically, the internal computation of the IMU enables us to measure and read-out various types of orientation changes. It's pretty powerful.
Create a new ESP-IDF project. Within that project, write a new variant of your code that:
- Uses the
getOrientationmethod to detect tilt using polling - Writes the message "Orientation change detected!" and the new orientation upon detecting an orientation change, using the ESP_LOG library
- If there is no orientation change, it does not print anything
- Set the
delayTimeto around 50 ms to make the system more responsive
This code improves on our first version because we're using the IMU's on-board orientation detection, offloading floating point math from the MCU. In addition, now the LED will not stay ON if we're tilted. It turns ON during any change in orientation, which is pretty cool. However, we are still polling, and there is still substantial latency between tilting and the LED turning ON.
We can do better.
Build a tilt detector, a gooder way
Interrupts
It's pretty cool that we can use the IMU's on-board orientation-detection algorithm to avoid having to do any math on the ESP32C3, but the fact that we're polling means either we can never sleep, or, if we sleep, we'll potentially delay our ability to detect a change.
There's a better way, and that's to use interrupts. Interrupts are a signal to the processure to pause (aka interrupt) the current execution and execute a separate piece of code. They enable asynchronous execution of code. When the processor gets an interrupt request, it stops executing the current code, saves the relevant registers, executes the interrupt service routine (ISR) and then reloads the registers and continues on with the previous code.
We can have interrupts that are based on timers (interrupt every x milliseconds), GPIOs (when a GPIO input goes high, or low, or changes state), and so on. It can get tricky when multiple interrupts fire, but we'll keep things simple here.
To use interrupts on the ESP32C3, we need to set up a few things.
Create a new ESP-IDF project. Copy over your old main.cpp and make the following changes.
Telling the IMU to use INT1
First, we need to instruct the IMU to change the INT1 pin logic level upon an orientation change. The two library methods to do that are:
imu.setOrientInterruptPin(0); // use INT1 as the interrupt pin
imu.OrientInterruptEnable(1); // enable interrupts based on orientation changes
The first line says to use INT1 for the interrupt. You might recall that the FXLS8974 has two interrupt pins, INT1 and INT2. We've left INT2 unused on our boards, so only INT1 is connected.
The second line tells the IMU to enable interrupts, so that INT1 in this case will be driven high when the IMU wants to send an interrupt condition.
Place these where you have the other IMU setup methods (but before you turn on the readings with setMode).
So now with these two lines, if the ORIENT_STATUS register signals a NEW_ORIENT, the IMU raises the INT1 pin high2.
Telling the MCU how to handle interrupts
Next, we need to do a few things on the ESP32C3.
First, we need to tell the ESP32C3 that the pin connected to INT1 is an input, and that we want to use it for an interrupt. You can place these near where we set up the ledPin.
gpio_set_direction(accelPin, GPIO_MODE_INPUT);
gpio_set_intr_type(accelPin, GPIO_INTR_POSEDGE);
// Install GPIO ISR service
gpio_install_isr_service(0);
// Attach the interrupt handler to the GPIO pin
gpio_isr_handler_add(accelPin, orientationChange, NULL);
The first line tells the ESP32C3 that the accelPin connection is an input.
The second line tells the ESP32C3 that we'd like the interrupt to trigger when there is a rising edge. We can also trigger based on falling edge or logic level.
Then we "install" the ISR service for GPIOs, with a parameter that for advanced uses allows to, for example, set the priority level of the interrupt.
Finally, we tell the ESP32 that when the accelPin raises an interrupt, that we should execute the orientationChange function, which is our ISR.
The ISR
The third bit is our actual ISR. This is a short piece of code that gets called when the ISR fires.
Here's a simple one:
volatile bool orientChange = 0;
static void IRAM_ATTR orientationChange(void *arg) {
orientChange = 1;
gpio_set_level(ledPin, 1);
}
This ISR does only two things: it sets the global variable orientChange to 1, and turns on the LED. Which is great, because a common design pattern for an ISR is to set a flag or do some other very minimal processing and return. In general, ISRs should:
- be short and fast, to allow the CPU to get back to what it was doing as quickly as possible.
- avoid calling other functions, especially any blocking functions
- use the
volatilekeyword for any global variables that we are changing, hereorientChange. That way the compiler will know to always read that variable fresh (rather than cached) when we want to access it, which is good because it can change suddenly.
xQueueCreate , xQueueSendFromISR, etc. We're going to keep it simple here.You'll notice one other line, doing basically nothing with the arg. That's because ESP32 requires that particular function signature for ISRs and that (void) arg prevents issues.
Finally, notice that we declared the ISR using static void IRAM_ATTR. static restricts scope to this particular file, while IRAM_ATTR forces the compiler to put this ISR into the RAM of the ESP32C3 (instead of Flash), ensuring it can be accessed quickly. And quick is the name of the game here.
But here's the critical thing. Because we now turn on that LED in the ISR, and the ISR fires asynchronously, it should light up after tilt with no discernable latency.
Update your code
Now incorporate ISRs into your code. Either make a new project or edit your previous one. You'll be wanting to check with orientChange variable rather than orient.NewOrient, which also means that you don't need to query the IMU with imu.getOrientation(orient) until after you sense the orientChange. So no more polling, and when you get it to run, there should be no more latency for turning on the LED. Pretty neat.
To sleep, perchance to dream
A final use of our IMU that you may want to employ is to wake up a sleeping ESP32C3 if your system gets knocked over. And we can do that. We've already learned how to sleep and wakeup based on time. But we can also set the ESP32C3 to wake up based on GPIO input levels.
We have a final piece of code that does this here. Creat a final ESP-IDF project and put this main.cpp in there. It's quite similar to our other code, with a few key lines:
gpio_wakeup_enable(accelPin, GPIO_INTR_HIGH_LEVEL);
When setting up the GPIO, we also set it as a wakeup source.
Then, in our while(1) loop, we include the following:
esp_err_t err = esp_light_sleep_start();
if (esp_sleep_get_wakeup_cause()==ESP_SLEEP_WAKEUP_GPIO)
orientChange = 1;
After we start light sleep, the ESP32C3 pauses on that line. Upon wakeup, we retrieve the wakeup source, and if it's due to a GPIO, then we know it was due to an orientation change, and we set the appropriate variable to 1.
Because some people's serial monitors have issues with light sleep, we blink the LED after wakeup to signify that we did in fact wake up.
Note that we're not using interrupts here. There is no ISR. But the effect is similar. We can spend almost all our time sleeping, and when the IMU detects an orientation change, it forces the ESP32C3 to wake up, upon which time the ESP32C3 writes a few lines to the serial monitor and goes back to sleep.
Build and flash this code. You should see the LED flash when you tilt your IMU. It might even print to the serial monitor (or it might not). But now we know how to use the IMU (or any other source that can raise a GPIO) to wake up the ESP32C3.
Show your three different pieces of code working, and explain how they work.