Lab 1

The questions below are due on Monday February 09, 2026; 06:00:00 PM.
 
You are not logged in.

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.

Goals: In this lab you will hook up an initial set of components (MCU and sensor) that will give you experience useful for quickly prototyping and testing for the 6.900 project. By the end of this lab, you will have a battery-powered cloud-connected portable weather monitor. Pretty cool!

We ask that you allocate time during each lab for clean up.

Before you leave, it's very important to get into the habit of leaving the lab space ready for the next person. So important that we will have future checkoffs specifically for this purpose.

Steps for cleanup:

  • Carefully pick up your system and place into a yellow paper envelope.
  • Please return the tools (wire strippers, etc.) to staff table.
  • Throw away loose wires in your work area.
  • Throw away paper, food, etc. in your work area.

Getting Started

You'll need to use a personal laptop to access the course website and to interface with the microcontroller. The computers in 38-530 do not have the right software on them.

You should have already installed the software toolchain on your laptop. If you haven't, please do so now from our software install page.

Assembling the embedded system

Even though we'll soon divide into teams, each person in the class should build up their own system. That way each teammate will be able to prototype different aspects of the overall system as we go through the course.

Today we'll hook up the initial elements of the system.

We're using a custom 6.900-specific board. It's is mostly ready to go, but not quite.

Get the relevant parts from the stash in the middle of the room:

  • ESP32C3 microcontroller Mainboard
  • Li-ion 18650 battery
  • (3) 2-pin jumpers
  • 6.900 backplane
  • (3) M3 x 6-mm screws
  • USB-C cable
  • mini-breadboard
  • LDO board
  • AP7363-33D LDO chip
  • (2) 10 uF 1206 capacitors
  • Right-angle 1x3 female header
  • BH1750 breakout board

You'll want access to the datasheets, pinouts, and so on for the parts while you do this lab:

Let's do some soldering

LDO board

We'll usually power our system with a Li-Ion battery or the USB connector. These are either at ~3.7 V (battery) or 5.2 V (USB). However, the rest of our system wants 3.3 V, so we will use a regulator to turn that unregulated input voltage into 3.3 V.

That's the little board you have that is labeled "LDO rev25.12". LDO is a low-dropout voltage regulator. It takes a varying input voltage and outputs a constant well-defined voltage for the rest of the circuit. Since our ESP32C3 and peripherals run on 3.3V, that's what we want to create. And lo and behond, our AP7363-33D chip does just that (maybe you can guess now what that -33D suffix means).

You will have to solder up that board, though. It's pretty straightforward, with four components: the AP7363 chip, a three-pin right-angle female header, and two 10 uF capacitors.

Safety

There are a few hazards in soldering:

  • Hot Stuff: The hot plate and soldering iron are really hot, like moreso than Donna Summer's 1979 single. Lead-free solder melts at around 220°C (~420 °F). As a result, the soldering iron and hot plate have to be at least that temperature, so be careful to not burn yourself! If you drop a soldering iron just let it fall and get out of the way, resist the urge to catch it.

Also be sure to wear safety glasses - no need to go blind because you get some hot beads of solder flicked into your eyes.

  • Solder Paste: We'll be using solder paste today to solder our tiny surface mount components. It's super cool and easy to use, but it's easy to get everywhere. We recommend that you wear gloves when handling the paste.

Technique

We'll be using two methods to assemble our LDO board - one for the surface-mount components, and one for the right-angle through-hole connector. You may have done some through-hole soldering in the past...but the surface-mount will be newer for many of you. We'll solder the surface mount parts first, then solder the through hole part.

Surface Mount Assembly

First, make sure you have the parts. This picture below shows what you need:

Parts for the LDO board.

OK the next step is to place a thin bit of solder paste on the exposed pads of those components with a syringe. We don't want this layer to be super thick; otherwise, we may short pads by applying too much solder. If you do, you can simply use a paper towel to wipe off the excess. We also have some zipties in the 6.9000 parts drawer that can be used to easily scrape solder paste off the boards. It also doesn't need to be super precise.

Here's an image of the board after applying paste.

Example of pasting.

Feel free to ask the staff if you'd like a quick check on your paste job!

Place and Heat

Next, place the parts on the pads with tweezers. The orientation of the (non-polarized) capacitors does not matter for our board. When you place components with tweezers, it's good to push the parts into the paste a bit so they are embedded.

After adding components.

Now you need to heat up the solder. This video shows what will happen, in around 15-20 seconds or so:

Real-time soldering using the lead-free paste on a hot plate.

So bring your board to the hotplate. It should already be set up, but if not, set the temperature to 250 C. This is a bit above the solder melt temperature, which is what we want.

Once your board is on the hotplate, it will start to smoke a bit, and in around 15-20 seconds or so, you'll see the solder do what it does in the video above. Once that occurs, carefully remove the board with the tweezers and gently place it on the bench. Don't jossle it, else the parts might get dislodged while the solder is still melted!

Here's an image of what it should look like after reflow:

After reflowing the solder on the hotplate.

Hand-solder the right-angle header

Finally, attach the right-angle header to the board and solder it from the back. You may want to add a little header or something to prop up the other side of the board so it all lays flat.

Here's an image of the board when it's all done:

All done!.

Solder the BH1750 board header

The other bit of soldering we have to do is to solder the 5-pin header strip to the BH1750 board. Go ahead and do that.

And now for some mechanical assembly

To secure your embedded system and make everything as nice as possible, we're going to attach it to a backplane. This will also give you examples of how to attach your project PCBs to your enclosure.

Today we're going to use screws that will tap into aligned holes on the backplane. Basically, we've designd the 3D-printed backplane so that it has holes that align with the 3 holes drilled into the PCB. The holes are sized so that when you screw M3 screws into them, they will cut threads into the plastic (so-called "tapping"). This assembly technique is not great if you want to repeatedly attach/detach a board, but for one-time (or two-time) assembly it's quick and easy.

  • Take the mini-breadboad and attach it to the mainboard header pins.

  • Align the top-left screw hole on the PCB with the screw-hole on the backplane, and screw in the M3 screw. You will need to apply pressure and go slow to cut the threads. Leave it a bit loose -- do not tighten fully.

  • Next repeat with the bottom-left screw. Again, do not tighten fully.

  • Next, tilt up the mainboard/breadboard and pull off the adhesive liner on the back of the breadboard.

  • Press the breadboard down onto the backplane to attach.

  • Screw in the central screw. Tighten hand-tight.

  • Finally, tighten the two left screws hand-tight.

Add the 6.900 mainboard jumpers

For today's lab, you are going to power your system from the LiPo battery. So go ahead and insert it into your mainboard. It's a tight fit, but you can get it in there. Be aware of the orientiation! The "button" on the battery goes to the left (+ terminal).

If you insert the battery backwards, a red LED on the board will light up. If that occurs, remove the battery and re-insert the other way around.

After inserting the battery, do not remove it. Just leave it there forever. Although this is a protected 18650 cell, meaning it comes with internal circuitry for over-current, over-voltage, or under-voltage protection, it can still deliver substantial current if accidentially short-circuited. So the safest place to be is in its holder. Related, be careful when connecting anything to the VBAT terminal.

Now, with the battery installed, you might be wondering why the mainboard isn't "ON". If we take a look at the circuit schematic, we can see why. You can look at the entire schematic linked above, but here we've zoomed in on the two relevant regions.

In the first schematic, you can see that positive lead of BT1 (the battery) is connected to the Q1 PMOS transistor, and then becomes VBAT, and in the second schematic you can see that VBAT connects to JP1, which is a jumper. It allows us to choose to connect the battery directly to the LDO (Conn_LDO) or to a battery manager (Conn_BMS). Today we want to connect the battery directly to the LDO, so we need to bridge that jumper.

What's Q1 for? It's to protect your system if anyone happens to install the battery backward. Without it, if you install your battery backward, anything connected to it, like the LDO (or BMS board later on), will see -3.7V on it's power pin. Some ICs will immediately self-destruct if that happens. This would be bad. The FET that is Q1 is arranged so that it acts as nearly a short circuit if the battery is installed correctly, and as an open circuit if the battery is installed backward. It is one way to get reverse-polarity protection. You're welcome.

Find JP1 on the pcb layout. You want to add a jumnper between the input (middle) pin and the "No BMS" pin, since we're not using a BMS today.

There are two more jumpers that you need to add, to JP2 and JP11. Those close the circuit so that the power from the battery can flow to the LDO and then to the rest of the system. Add those. Your mainboard should now have three jumpers on it.

Assemble the starter system

Hook up the ESP32 + LDO + BH1750

Next let's finish hooking up the boards.

The LDO board gets attached to the ESP32C3 mainboard via the REG connector. Insert the LDO board with the components facing up!

The green status LED on the mainboard should now light up after you insert the LDO board. If it doesn't, check in with staff.

As a fun sensor to use for this first lab, we've chose a BH1750 light sensor. This sensor outputs a digital value proportional to the ambient light. You know how your phone screen gets brighter in the sun, or how your map software turns to dark mode when it's, um, dark out? Well, they use a similar sensor to do that.

Attach the BH1750 breakout board to the mini-breadboard using its header. Cause it's hard to remember what the pins are since they are labeled on the back of the board, here's a pic to help you out.

BH1750 pin definitions.

Wire up 3V3 and ground.

The other two connections to make are the SDA and SCL connections for the I2C communications bus. While you can choose most of the ESP32C3 GPIOs for these signals, we recommend choosing GPIO4 thru GPIO7 -- one for SDA, one for SCL.

Some of the GPIO pins on the ESP32C3 have dual purposes, or are really intended for specific functions, so you can get in trouble if you don't check first. GPIO4..7 are safe to use for I2C.

When you make your connections, please keep your wires flat, neat, and short. Use color coding for ground, 3V3, signals, etc.

Checkoff 1:
Show your wired-up system to a staff member.

Getting some code running

Start a new ESP-IDF project in VS Code

Mac Users: We have noticed that the ESP-IDF extension for VS code is very buggy on MacOS. We highly suggest that if you are having issues with the VS code extension, use the command line instead. For every part that uses the VS code extension, we will include a command line alternative as well.

Now let's actually get something running on our microcontroller to see if our software is working.

In Visual Studio Code:

  • Navigate to View > Command Palette.

  • Type ESP-IDF: New Project and select the command to launch the New Project wizard.

  • Wait a bit. At some point, near the top of the windows you'll see a box asking which ESP-IDF framework to use. Click the box to select the default (ESP-IDF v5.5.1 or similar).

  • Now you'll see the New Project dialogue wizard.

Enter a project name, like lab01, and select a suitable Project directory to use.

  • For "ESP-IDF Target", select "esp32c3".

  • For "ESP-IDF Board", select "ESP32-C3 chip (via builtin USB-JTAG)"

  • It should auto-select the right serial port.

  • Now select "Choose Template" and on the next screen under "ESP-iDF Templates" choose "sample_project", which is basically a blank app.

  • Now select "Create project using template template-app"

And now again we wait a bit while it does some stuff.

  • At some point, you'll see a little window pop up asking if you want to open the project: Hit "Yes".

ESP-IDF project organization

For this very simple project, you can see that we have the following directory structure:

- lab01/
    - CMakeLists.txt
    - main/       - CMakeLists.txt
                  - main.c

There are some other files but these are the main ones. main.c is our program code. The two CMakeLists.txt files direct the compilation process.

One important thing to change is that we want to run a C++ file, not just C. To do that:

  • rename main.c to main.cpp by right-clickng on main.cpp in the VS Code Explorer.

  • If you do that and then navigate to the CMakeLists.txt file in the \main directory, you'll see it automatically updated the idf_component_register... line.

  • If you rename via the command line or elsewhere, you'll need to manually update CMakeLists.txt. Not a big deal.

main.cpp, where your program lives

Now select main.cpp in VS Code. It's a short stub right now:

#include <stdio.h>

void app_main(void)
{

}

app_main is the main function that runs.

Since main.cpp is a C++ file, but ESP-IDF expects C code, we need to add an extern statement bracketing app_main. And then let's put in a short, simple program just to get things going.

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <esp_system.h>

extern "C" {
void app_main(void)
{
  while(1) {
    printf("Hello World\n");
    vTaskDelay(pdMS_TO_TICKS(1000));
  }
}
}

We've added a few #includes to bring in some of the libraries that ESP-IDF needs to run.

Beyond that, there are only three statements to this code:

  while(1) {

This line makes sure we repeat this code block forever.

    printf("Hello World\n");

Prints Hello World (with newline). Where does it print to? ESP-IDF automatically sets up the serial port as the default endpoint to print to.

    vTaskDelay(pdMS_TO_TICKS(1000));

This line adds in a 1000 ms delay. the pdMS_TO_TICKS macro converts the argument from milliseconds into clock cycles, while vTaskDelay is the ESP-IDF delay function.

As opposed to the Arduino delay function, vTaskDelay is non-blocking.

Build the project

Let's "use" this code. First, we need to build the code.

At the bottom fo the VS code window is a "Build Project" icon. Hit that.

If you run into issues using the build button at the bottom or are using the command line instead of the VS code extension, you can instead open an ESP-IDF terminal and run idf.py build or use Ctrl/CMD+E, B.

Now VS Code will proceed to build the project. It can take a while the first time. Patience.

Various useful information will pass by, ending with a colorful table showing you how big your code is.

If you get no errors, then let's upload.

Upload/flash the project

Next, making sure that you have physically connected your ESP32 to your computer, select the ESP-IDF:Build, Upload, and Monitor icon at the bottom of the VS Code window. You can also run idf.py flash followed by idf.py monitor in the ESP-IDF terminal.

Again, it will build. After a bit, a message at the top of the Visual Studio Code window will ask you to "Select the flash method...". Select "UART", since we are using the serial port to upload the code.

Then after a few moments, the screen will change and you should see messages indicating it is uploading the code to your ESP32C3.

If you get an error on upload, you may need to put your board in a mode ready to receive new firmware. Press and hold the FLASH button, then press the RESET button once, then release the FLASH button.
These are not the only issues you can have, of course. You may need to check/adjust your COM port, install drivers, etc.

Serial monitor

The code above should be printing to the serial monitor. You should see Hello World printed over and over.

You may have to reboot *aka* RESET your ESP32C3 in order to get it going.
If you don't see a message getting printed repeatedly in the serial monitor, please ask for help.

Let there be light

Incorporating the BH1750 component

OK, now that we have our Hello World out of the way, let's do something more interesting -- let's sense light levels.

Rather than write our own I2C library to interface with the BH1750, we're going to use a pre-written one by Espressif. In the Espressif ecosystem, they use the term "component" for what we would refer to as a "library", basically a set of files (.c, .h) and the associated CMakeLists.txt for you to use peripherals, etc.

To use the library, in VS Code:

  • Navigate to View > Command Palette.

  • Type ESP-IDF: Show ESP Component Registry.

  • In the UI that launches, search for "BH1750".

  • Select the "espressif/bh1750" component.

  • On the next screen, select "Install".

If you are using the command line instead, run idf.py add-dependency espressif/bh1750

This will bring in the bh1750 component, which is not part of the core ESP-IDF system. You'll now see a new directoy "managed_components" in your project directory, and some new files.

Examine our new project code

Now, let's get some basic measurement code running. Copy/paste the following into your main.cpp:

#include <stdio.h>
#include "freertos/FreeRTOS.h" // ESP32 RTOS
#include "freertos/task.h"
#include <esp_system.h>
#include "esp_log.h"

#include "driver/i2c_master.h"   // I2C driver
#include "bh1750.h"              // espressif/bh1750 component


// I2C definitions
#define BH1750_ADDR     0x23    // BH1750 default I2C address
gpio_num_t sda = GPIO_NUM_4;
gpio_num_t scl = GPIO_NUM_5;

static const char *TAG = "LAB01";

extern "C" {
void app_main(void)
{
    // Create I2C master bus
    i2c_master_bus_handle_t bus = NULL;
    i2c_master_bus_config_t bus_cfg = {};
    bus_cfg.i2c_port = -1;      // ESP32C3 only has one I2C port
    bus_cfg.sda_io_num = sda;
    bus_cfg.scl_io_num = scl;
    bus_cfg.clk_source = I2C_CLK_SRC_DEFAULT;
    ESP_ERROR_CHECK(i2c_new_master_bus(&bus_cfg, &bus));

    // Create BH1750 device on the bus
    bh1750_handle_t bh = NULL;
    ESP_ERROR_CHECK(bh1750_create(bus, BH1750_ADDR, &bh));

    // Power on and set measurement mode (continuous, 1 lx resolution)
    ESP_ERROR_CHECK(bh1750_power_on(bh));
    ESP_ERROR_CHECK(bh1750_set_measure_mode(bh, BH1750_CONTINUE_1LX_RES));

    // Log message
    ESP_LOGI(TAG, "BH1750 initialized at addr 0x%02X; reading lux every 1s...", BH1750_ADDR);


  while(1) {
        float lux = 0;
        esp_err_t err = bh1750_get_data(bh, &lux);
        if (err == ESP_OK) {
            printf("Ambient light: %.2f lux\n", lux);
        } else {
            ESP_LOGE(TAG, "bh1750_get_data failed: %s", esp_err_to_name(err));
        }
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}
}


This code is obviously a bit more elaborate than what we had earlier. Let's take a look at a few parts:

    // Create I2C master bus
    i2c_master_bus_handle_t bus = NULL;
    i2c_master_bus_config_t bus_cfg = {};
    bus_cfg.i2c_port = -1;      // ESP32C3 only has one I2C port
    bus_cfg.sda_io_num = sda;
    bus_cfg.scl_io_num = scl;
    bus_cfg.clk_source = I2C_CLK_SRC_DEFAULT;
    ESP_ERROR_CHECK(i2c_new_master_bus(&bus_cfg, &bus));

    // Create BH1750 device on the bus
    bh1750_handle_t bh = NULL;
    ESP_ERROR_CHECK(bh1750_create(bus, BH1750_ADDR, &bh));

This section sets up the I2C peripheral and the BH1750 object. There are more parameters one can set on the bus_cfg struct, but here we try to be somewhat minimalist.

You'll also notice the ESP_ERROR_CHECK macros encapsulating many of the statements we run. Espressif writes many of their functions so that they return error codes, described in detail in their docs. If the function returns without error, the return code is ESP_OK, which is just an enum for zero.

ESP_ERROR_CHECK is a macro that reads the returned error codes and if it's not ESP_OK, prints an error message to the serial port.

You don't need to encapsulate all these statements in ESP_ERROR_CHECK, but having these extra checks helps in debugging.

    ESP_LOGI(TAG, "BH1750 initialized at addr 0x%02X; reading lux every 1s...", BH1750_ADDR);

This statement shows our first use of the ESP Logging Library. This is a pretty powerful logging library that you'll want to use in your projects.

Each log message has an associated TAG, which is just a descriptor so you know which file the message came from. Then it has a variable number of fields that allow you to print statements and variable values.

Here we are using the ESP_LOGI macro, which an "Info" level log message. There is also ESP_LOGW (warning), ESP_LOGE (error), and so on. You can control the log level at compile time, so that you see fewer or more messages.

You can direct log messages from UART to another endpoint. Like an SD Card. That might be useful for your project!
  while(1) {
        float lux = 0;
        esp_err_t err = bh1750_get_data(bh, &lux);
        if (err == ESP_OK) {
            printf("Ambient light: %.2f lux\n", lux);
        } else {
            ESP_LOGE(TAG, "bh1750_get_data failed: %s", esp_err_to_name(err));
        }
        vTaskDelay(pdMS_TO_TICKS(1000));
    }

Finally, we run our infinite loop where we repeatedly acquire data from the BH1750 and print it to the serial port as long as there is no error, then wait one second.

Go thru the code yourself and make sure you understand what each statement is doing. You'll want to update the code to your exact I2C pins. The numbers refer to the GPIO pins, aka "GPIO_NUM_6" is GPIO6.

Build, flash, and run the code

Build the project (note this can take a while the first time because it has to build these new libraries). Then flash the project like before, and look at the serial monitor. If all is well (wiring, library, etc.) you should see the light intensity printing out like so (obviously different based on whether you are inside, outside, sunny, dark, and so on).

I (249) main_task: Started on CPU0
I (249) main_task: Calling app_main()
W (249) i2c.master: Please check pull-up resistances whether be connected properly. Otherwise unexpected behavior would happen. For more detailed information, please read docs
I (269) LAB01: BH1750 initialized at addr 0x23; reading lux every 1s...
Ambient light: 780.83 lux
Ambient light: 781.67 lux
Ambient light: 783.33 lux
Ambient light: 785.00 lux
...

You can see the ESP_LOGI message that we talked about earlier.

If it isn't printing or giving errors in readout, double check your wiring, and all the other usual suspects one would think about with a sysytem like this. Also, of course, reach out for help on Piazza or with staff!

Do some stuff to make sure those readings make sense and change as a result of perturbations.

Checkoff 2:
Show your system measuring light and outputting to the serial monitor.

To the internet!

Create a new project via the command line

Ok instead of having our device read a local sensor, let's instead talk to a server via the internet.

First, though, let's put this new code in a new project, and let's this time use the command line to do our work, so you can see both command line and menu-driven approaches.

In VS Code, click the icon at the bottom to "Open ESP-IDF Terminal". In the terminal, cd into the directory that you'd like the project directory to live in (not your lab01 directory), and run:

...\6.900-S26\code> idf.py create-project lab01b

Then, cd into that directory and set the target as the esp32c3:

...\6.900-S26\code> cd lab01b
...\6.900-S26\code\lab01b> idf.py set-target esp32c3

This will run for a few moments. Now, if you go to VS Code and File > Open Folder... and select lab01b, you should see the same directory structure as we had before for lab01 when we started.

For this project we need some additional libraries. We're going to use a couple of libraries we've written for this class, which are not on the public ESP-IDF Component Registry.

Download this zip file.

Place the components folder and its subfolders in the \lab01b directory, and replace your existing lab01b.c with the main.cpp in the zip file.

Your directory structure should now look like this:

── CMakeLists.txt
├── build
    ├── lots of files
├── components
    ├── 6900_http_client
    └── 6900_wifi
├── main
    ├── CMakeLists.txt
    └── main.cpp
└── sdkconfig

Next, open CMakeLists.txt in the \main directory in VS Code and edit the idf_component_register line so it so it looks like:

idf_component_register(SRCS "main.cpp"
                    INCLUDE_DIRS "."
                    REQUIRES 6900_wifi 6900_http_client)

OK, so what have we done? The two libraries that we need, which we call the 6900_wifi library and the 6900_http_client library, live within the \components directory. Any libraries that are user-created live there, using a similar directory structure. Ultimately those libraries are just .h and .cpp files that provide a set of wrapper methods to make our life easier.

To ensure that ESP-IDF can find those "components", we update the CMakeLists.txt file to add those two components as requirements.

Next, we have to deal with certificates. The ESP32 can interact with web servers using HTTPS, which is more secure than HTTP, but in order to do so it needs root certificates to verify the servers' identity. There are more than 130 different root certificates in what is known as the x509 Certificate Bundle. We could download the entire bundle, and this would be no big deal for the browser on your laptop to handle, but it can bloat the binary for your MCU, which we don't want. So instead, we will set the ESP32C3 to store a smaller set of 42 root certificates that covers 90%+ of websites.

To do this, in VS Code:

  • Ctrl + Shift + P > ESP-IDF: SDK Configuration Editor (menuconfig).

  • When it opens (it takes a bit), in the search bar at the top, type in mbedtls.

  • On the right panel, scroll down to the "Certificate Bundle" section, and under "Default certificate bundle options", change from "Use the full certificate bundle" to "Use only the most common certificates from the default bundles".

  • Hit Save.
Ultimately for your project, your ESP32C3 will be communicating with a well-defined set of servers, maybe one or two only. In that case, you can create an even smaller custom set of root certificates that will save more space. Which will make your firmware team happy.
If you are not using the VS Code extension and instead using the command line, look ahead a little to the first part of the "Connecting our sensor to the internet" section to see how to do this in the terminal instead.

Examine the new project code

Now, let's take a look at the main.cpp file. There's more going on in this code than the previous two.

First, the code connects to a WiFi AP. This requires you to put in the valid credentials for a 2.4GHz network (the ESP32C3 does not have a 5 GHz radio). The good ones on the 5th floor lab space (hopefully where you are) are either EECS_Labs (with no password) or 608_24G with password 608g2020.

To use the campus-wide `MIT` network, you must first register for a personal password at the MIT wifi onboarding portal. Once you register you can put your personal password in the ESP code along with the `MIT` ssid.

Once connected this code will send simple HTTP GET requests to a remote server that provides year-based facts in response.

Build and flash the code

Now, let's build and upload the code. Open a new ESP-IDF terminal. In the terminal, make sure you are in the \lab01b directory, and type:

...\lab01b> idf.py build

As before, the first build takes. a. while. Even longer than before because we have to build in the entire WiFi stack.

Next, to upload (flash) the code, type:

...\lab01b> idf.py flash

Finally, open the serial monitor:

...\lab01b> idf.py monitor

In the serial monitor, you should see random number facts printing out every few seconds. Your ESP32 does not internally contain all those facts, so the only way it could be printing those would be if it was getting them through the "ether"...so WiFi must be working. Nice. If you have that, that means you're good to move on!

Connecting our sensor to the internet

Specifications for this code

Now let's combine these two prior pieces of code to be able to post your sensor data to a server on the internet. You'll need to read through and understand what the code is doing in order to know how to combine. Simply concatenating the code will not work!

Here are the specs:

  • You code should set up the bh1750 sensor and take a reading every 10 seconds.

  • To do this, you'll need to add the bh1750 component to this project. If you'd like to do that via command line, the command is: idf.py add-dependency espressif/bh1750. After adding the component you'll want to run idf.py reconfigure to update the project configuration.

  • You want to then send that lux data to our server.

  • You'll want to update your while loop to create a POST request instead of a GET request.

  • The POST request will require extra parameters for the http_request method. Hovering over the method call, you can see what those optional extra parameters are. Or you can right-click and "Go to Declaration" to inspect the code itself. The code is located under \components\6900_http_client if the "Go to Declaration" button doesn't work.

    • In particular, you'll want to incorporate the data as a POST body, of the form lux=value1&kerberos=value2. Replace your actual data for value1, value2
    • You'll also need to provide a content type parameter, which should be application/x-www-form-urlencoded.
    • The server endpoint url is https://efpi-10.mit.edu/efi_test/lux_logger

Change the change the project configuration settings to include only the filter certificate bundle. You can do that via the GUI as above, but if you'd prefer to do it via the command line, in an ESP-IDF terminal, type:

>idf.py menuconfig

Then in the text-based GUI that shows up: Component config > mbedTLS > Certificate Bundle > Default certificate bundle options > Use only the most common certificates from the default bundle.

Once you've worked out what the code should be, go ahead and build.

Adjusting the partition size

Probably what will happen is that you'll get a build error of this sort:

Error: app partition is too small for binary lab01b.bin size 0x150590:
  - Part 'factory' 0/0 @ 0x10000 size 0x100000 (overflow 0x50590)

What's happening is that your code size is 0x150590 bytes, which is around ~1.4 MB. But the default block of flash allocated to storing code (the partition) is 0x10000 bytes, or 1 MB. So the code doesn't fit.

What we need to do is to increase the partition size. The ESP32C3 module that we are using has 4 MB of flash, so we can support a larger firmware. But we need to set that up explicitly.

You can't use the entire 4 MB for your code because you need room for the bootloader, wifi calibration data, etc. In particular, for your project you will likely want to allow "over-the-air" updates (OTA), and in that case you need at least two code partitions in addition to bootloader, etc. You'll probably want to set a custom partition table at that stage. But not today.

If you are using the command line, open the menuconfig:

>idf.py menuconfig

Then select Partition table > Partition Table (Single factory app, no OTA) > Single factory app (large), no OTA.

Quit and save the configuration. This gives us space for a 1.5 MB binary.

Now when you try to build it should work.

Finally, flash and then see what happens in the serial monitor. Eventually, your code should repeatedly start printing this:

Sending POST request: https://efpi-10.mit.edu/efi_test/lux_logger
I (20113) HTTP_CLIENT: HTTP_EVENT_ON_FINISH
I (20113) HTTP_CLIENT: HTTP Status = 200, content_length = 8
Response body:
"posted"
Sending POST request: https://efpi-10.mit.edu/efi_test/lux_logger
I (25333) HTTP_CLIENT: HTTP_EVENT_ON_FINISH
I (25333) HTTP_CLIENT: HTTP Status = 200, content_length = 8
Response body:
"posted"

Once you are getting a "posted" return message, you can check to see your data on the server. Point your browser to:

http://efpi-10.mit.edu/efi_test/lux_reporter?kerberos=None

And all your data should list out.

Some people get a 502 Bad Gateway error in their browser. We're not sure why, but a simple workaround seems to be to use Incognito mode. Or Microsoft Edge (if you dare).

Checkoff 3:
Show your system posting to the server to a staff member.

Look ma no wires

It may not be apparent, but you are one step away from being fully wireless. Just unplug your USB cable. Yes, that's it.

So unplug your board from your laptop and then check the website. If this is all working, you should still be posting data. Congrats, you have a battery-powered IoT light monitor!

But there's an issue. That battery is discharging as we use it. And when you plug your board into your laptop, the USB data signals are transmitting back-and-forth, but no energy is being delivered to recharge the battery. So your battery will discharge and then...turn off. What we need is a way to re-charge the battery when we plug it into the laptop. We'll design and incorporate that part in EX01.

For now, to avoid overly discharging the battery, remove the BMS/No-BMS jumper when not working on the system. That will turn off your system. I just put the jumper on one of the pins so I don't lose it.

Sensing battery voltage

Measuring the battery voltage

At this point we should have a battery powered portable light sensor. One key element of battery-powered systems is knowing how much charge is left in the battery. This is known as the battery state of charge (SOC), and ranges between 0% and 100%. It's super annoying if the only way to know the battery SOC is when the system just dies (which just tells you the SOC is 0%, great).

So let's add battery sensing capabilities to our system.

Measuring the charge left in a LiPo battery is actually not trivial. In fact, it's so difficult to do that companies make dedicated ICs (called fuel gauges) to do so.

But we can get a rough sense by just measuring the battery voltage. The battery's voltage can range between 4.2 volts (100% SOC) and 3.4 volts (~0% SOC).

Where can we get access to the battery voltage directly? Not the 3.3V pin on the ESP32C3, as that is a regulated output. But you'll notice there's a VBAT pin on our mainboard, and, as you might remember, it is directly connected to the battery. So we just need to measure that voltage!

But wait. One of the challenges with doing this directly is that the ESP32's adc can only measure voltages between 0V and 2.5V. But the battery voltage is going between 3.4 V and 4.2 V. We need to somehow scale that voltage down to a max of 2.5V (otherwise our ESP would just read 2.5V).

We can use a pair of resistors to make a voltage divider. We can then safely measure the scaled down voltage in our code apply the inverse of our divider ratio, and arrive at the true pre-divider voltage measurement.

So lets just grab any two resistors off the floor, hook up our battery, and fix it in software. Not so fast.

  • We can't just use any two resistors. These two resistors will be chronically attached to our battery. If their resistance is too low, they will quickly discharge our battery, shorten our run-time, and may even turn into an LER. We should design our divider to draw less than 1 mW of power (which is still kind of a lot, but ok for now).

  • The ESP's adc will draw a (very) small amount of current from the output of the resistor divider. Make sure that the equivalent Thévenin resistance of the divider presents a negligible (<10mV) drop when the ESP's adc draws it's sampling current (1uA).

Before hooking up your resistors, double-check the values and where you are hooking them up. Because you are accessing the VBAT pin, if you short-circuit that pin, many amps of current will flow, potentially damaging your board or the battery.

One last hardware thing. We should add a capacitor to the output of the voltage divider to smooth out the voltage measurement. This value is not as critical as the resistors from above. Anything between 1nF and 10uF should work.

Pay attention to the voltage rating of the capacitor and its polarity (if it is polarized).

Analog to digital conversion code on the ESP32C3

Now that we have conditioned the voltage signal, connect the output of the divider to an available analog GPIO of our ESP32.

To measure those voltages with the ADC, we wrote some helper code that you can use to extract the raw ADC value from the ESP and convert it to a voltage.

/* NOTE: place the following lines near the top of your main.cpp*/
#include "rom/ets_sys.h"
#include "esp_adc/adc_oneshot.h"
#include "esp_adc/adc_cali.h"
#include "esp_adc/adc_cali_scheme.h"

/* NOTE: place the following declarations and prototypes before your app_main() */
// ADC parameters and function prototypes
adc_channel_t adc_channel = ADC_CHANNEL_2;    // GPIO2, can be changed
const int num_average = 10;
static bool adc_calibration_init(adc_unit_t unit, adc_channel_t channel, adc_atten_t atten, adc_cali_handle_t *out_handle);
static void adc_calibration_deinit(adc_cali_handle_t handle);

/*NOTE: place these declarations in your app_main() */
adc_oneshot_unit_handle_t adc1_handle;
adc_oneshot_unit_init_cfg_t init_config1 = {
	.unit_id = ADC_UNIT_1,
};
ESP_ERROR_CHECK(adc_oneshot_new_unit(&init_config1, &adc1_handle));

//-------------ADC1 Config---------------//
adc_oneshot_chan_cfg_t config = {
            .atten = ADC_ATTEN_DB_12,
            .bitwidth = ADC_BITWIDTH_DEFAULT,
        };
adc_oneshot_config_channel(adc1_handle, adc_channel, &config);

adc_cali_handle_t adc1_cali_chan0_handle = NULL;
adc_calibration_init(ADC_UNIT_1, adc_channel, ADC_ATTEN_DB_12, &adc1_cali_chan0_handle);

/* NOTE: the following code will take num_average readings from the ADC and average them to reduce noise, then turn the raw values into voltage */
// Read Battery voltage
int adc_raw = 0;
int voltage = 0;
uint32_t adc_reads = 0;
for (int i=0; i < num_average; i++) {
    ESP_ERROR_CHECK(adc_oneshot_read(adc1_handle, adc_channel, &adc_raw));
    ets_delay_us(10);       // blocking delay in us
    adc_reads += adc_raw;
}
adc_reads = adc_reads / num_average;

printf("ADC Raw Data: %d\n", (int)adc_reads);
//maybe handle any errors later:
ESP_ERROR_CHECK(adc_cali_raw_to_voltage(adc1_cali_chan0_handle, adc_reads, &voltage));


/* Note: place these functions at the end of your main.cpp, after the app_main() block */

/*---------------------------------------------------------------
        ADC Calibration (put outside the extern C block)
---------------------------------------------------------------*/
static bool adc_calibration_init(adc_unit_t unit, adc_channel_t channel, adc_atten_t atten, adc_cali_handle_t *out_handle)
{
    adc_cali_handle_t handle = NULL;
    esp_err_t ret = ESP_FAIL;
    bool calibrated = false;


    if (!calibrated) {
        ESP_LOGI(TAG, "calibration scheme version is %s", "Curve Fitting");
        adc_cali_curve_fitting_config_t cali_config = {
            .unit_id = unit,
            .chan = channel,
            .atten = atten,
            .bitwidth = ADC_BITWIDTH_DEFAULT,
        };
        ret = adc_cali_create_scheme_curve_fitting(&cali_config, &handle);
        if (ret == ESP_OK) {
            calibrated = true;
        }
    }

    *out_handle = handle;
    if (ret == ESP_OK) {
        ESP_LOGI(TAG, "Calibration Success");
    } else if (ret == ESP_ERR_NOT_SUPPORTED || !calibrated) {
        ESP_LOGW(TAG, "eFuse not burnt, skip software calibration");
    } else {
        ESP_LOGE(TAG, "Invalid arg or no memory");
    }

    return calibrated;
}

static void adc_calibration_deinit(adc_cali_handle_t handle)
{
    ESP_LOGI(TAG, "deregister %s calibration scheme", "Curve Fitting");
    ESP_ERROR_CHECK(adc_cali_delete_scheme_curve_fitting(handle));
}

One more thing is that you will have to add 'esp_adc' to the REQUIRES list of your CMakeLists.txt in the \main directory.

You can change the GPIO you use for the ADC, just make sure to use one of the GPIOs that are connected to ADC1, basically GPIO0 thru GPIO4, though we recommend using GPIO2..4.

This piece of code returns the voltage present on the specified analog GPIO pin. Make sure to "undivide" the voltage divider ratio to get the actual battery voltage.

Incorporate this functionality into your code, and make sure your system is correctly measuring voltage (get a multimeter and actually confirm). It should be reasonably close (within 5%). Depending on your battery's charge, your system should measure a voltage between 3.4 and 4.2 volts.

Going from voltage to SOC is not trivial, so for now, we're just going to report the battery voltage.

Tying it all together

Let's finish this lab.

Your final system should still make POST requests every 10 seconds with your kerberos (kerberos), lux (lux) and battery voltage (bat).

Once you see your data being posted to the server and can view it on the webpage, you'r finished! In EX01 we'll change from the efpi-10 endpoint to your own server, and use those logs to verify that your system is working.

Checkoff 4:
Show your battery-powered system posting your data including battery voltage to the server to a staff member.