Lab 4

The questions below are due on Tuesday March 04, 2025; 05: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.

Overview

Today you're going to get some experience with the Joulescope in a number of situations. Power should be at the front of everyone's mind on this project even if you're not "doing the power part." So everyone in the team should be fluent in these concepts. The lab uses the Joulescopes, of which we currently have three (and will soon have five), and those are to stay in lab. Anybody caught removing a Joulescope from lab will be punished.

We have a limited number of Joulescopes, so please sign up for a time here when you can use one.

We'll study power using the Joulescope in the following situations:

  • A LED being driven by 5V USB with an in-series variable resistor.
  • The power consumed by our ESP32-C3 with some basic LED flashing code.
  • The power consumed by our ESP32-C3 in different modes of operation (WiFi on, WiFi transmitting, light sleep, deep sleep)
  • The power curve of a photovoltaic cell.

Joulescope Setup

Here's the Joulescope. There are three of them in lab. They are very nice system tools. Treat them with respect.

The Joulescope

As we discussed in lecture, the Joulescope is a very nice system which, when hooked up correctly will at a very rapid rate utilize an internal ADC to make measurements of the voltage across a region of a circuit and the current into that system. Of course, we have to hook it up correctly for it to do this (discussed below). It might seem like what this is doing is not that hard, but the devil is in the details. It is pretty easy to make a power measurement system that works ok-ish for a limited range of power. It is much, much harder to make one that can dynamically adjust itself for a wide-range of powers as the Joulescope does; it has some fantastic engineering in it.

The Joulescope

First task at hand is to get the software to work with our Joulescope. Now maybe I just have really low standards from a lifetime of using hardware design and measurement tools, but I'm blown away by how well this Joulescope software works. Go to their website here. Download the software for the JS220 for your appropriate system. It should just work, but let us know if you have any issues installing. I'm sure Windows will somehow make it a challenge. (Note from Joel: No, Windows is a modern operating system and will work just fine. Now, MacOS, who knows...)

Once you've got it, open it up and then plug in your Joulescope. For lab today we'll largely be concerned with using two "widgets": (1) the multimeter and (2) the waveform (turn them on from the Widgets pull-in in the top menu.)

These all kinda show the same data, but they give it to us in different formats to make it easier to consume. If you just want rough ideas of power being consumed (or current or whatever), you can look at the "multimeter". If you want to see and measure tiny "blips" of power consumption like from a transfer of data over WiFi, the waveform viewer (which can be saved btw) would be the choice.

The widgets should just start working when you plug in the Joulescope. Again let us know if there's any issues. The readings won't make much sense until you're actually hooked up.

As mentioned in class, there's two ways to measure power consumption (and specifically the current portion), a high-side measurement or a low-side measurement. The two options are shown below, but for today we'd recommend you measure using a high-side configuration.

High-Side Measurement Hookup

Low-Side Measurement Hookup

A Simple First Circuit

For a first circuit, just to get our feet wet, let's build the following:

Baby's first test circuit.

The v_{USB} source should be from the Power boards that we used earlier in the class (use the VU and GND connections to get the ~5V from the USB). Use a fixed 1K resistor and a variable 10K resistor to regulate the flow of current into a red-colored LED. What the circuit will do, at a high level, is vary the brightness of the LED. Build it, power it from your computer or whatever, and just verify it works. When that is done, make a high-side measurement of the circuit's power consumption (resistors and all) by integrating the Joulescope into it.

First:

  • Measure the voltage across the entire resistor-LED system and the current through it. In this placement, the voltage should largely be constant since it is what the USB is providing. You should see the current, and therefore power, vary significantly as you sweep the potentiometer.

Then:

  • Measure the voltage across the LED specifically while still measuring the current through the whole system. This should now tell you what power the LED on its own is consuming. It should, hopefully if there is a rational god, be less than the number you're getting when you measure what the entire circuit is consuming. Ok noice. Onto the next circuit.

The ESP32-C3

The ESP32-C3 is capable of some pretty interesting behavior related to its power consumption. Unfortunately, some of this capability can be obfuscated when on the Devkit M1 we've been using in class. In addition to the ESP32 C3 (the SOC itself), there are several other parts on the board that are also consuming power including:

  • The CP2102 USB-to-UART programming chip (consuming about 0.9 mA @5V)
  • The always-on RED power supply indicator LED (consuming about 0.6 mA at @5V), though if you're not powering the board as a whole with 5V it doesn't do anything
  • That weird RGB LED that sometimes comes on and can be very bright...(if it is bright it is pulling at least several mA at 3.3V if not more)
  • The SGM2212 on-board linear regulator which seems to pull a non-negligible leakage current when not actively powered

They're shown in the Devkit schematic below:

The schematic highlighting some potential power-consuming parts other than the ESP32 C3.

We ideally don't want those parts there. They're distractions, but we don't need you destroying your boards in the process of removing them so we'll just make some measurements knowing that they are tainted by their presence.

Where the potential power-consuming parts other than the ESP32 C3 are on the dev board.

For a programming platform we're basically going to go with exactly what we had before. Have the one of those micro:

  • 3V should connect to ESP 3.3V
  • G should connect to ESP GND
  • D+ should connect to ESP GPIO 19
  • D- should connect to ESP GPIO 18

In addition, get a blue or white LED and hook it up to GPIO 8 on the ESP32 like shown below. We're going to flash some LED since the serial port will be problematic in our testing.

Blinking the LED.

Make a new platform.io project. Make sure you select an Arduino one, select the ESP32-C3 Devkit M1, yada yada, etc... Once in the project, make sure to add the following lines to your platform.ini file:

build_flags =
  -DARDUINO_USB_MODE
  -DARDUINO_USB_CDC_ON_BOOT

For code, let's use this short, sweet simple program that flashes the LED. Program it to make sure it is even working.

#include <Arduino.h>

int state;
void setup() {
  Serial.begin(115200);
  while (!Serial);
  Serial.printf("Starting up!\n");
  pinMode(8,OUTPUT);
  state = 0;
  digitalWrite(8,state);
}

void loop() {
  state = !state;
  if (state){
    Serial.printf("LED ON\n");
  }else{
    Serial.printf("LED OFF\n");
  }
  digitalWrite(8,state);
  delay(1000);
}

Now once you're sure the flashing is good, integrate the Joulescope in so that you're measuring the power consumed by the ESP32-C3. Watch the plot of the voltage, current, and power. You should see very distinctively different regions of power consumption. What do the high and low regions correspond to?

Once that makes sense, go in and change your code so that the LED is flashing 100 times a second instead of once a second (also comment out the two print statements so you don't clog your serial monitor). Once this is done, your eye will barely be able to keep up and instead see a seemingly steady light, or perhaps, if looked at indirectly, just the slighest soupçon of a flicker using the rods of your peripheral vision. But the Joulescope should be able to keep up. It should be able to give you a plot of the instantaneous power being used by your ESP32 because of it either driving or not driving the LED. Very cool.

The ESP32 Doing Other Things

Flashing an LED is fine but a little artificial. What I wanted the last part to demonstrate is that software can cause differences in the power being used by the ESP32. Let's see this taken to the extreme.

The code below does a few things. It is a variant of the same code you used in Week 1 but with some modifications. Roughly speaking it:

  • Upon powering up it flashes an LED five times.
  • It runs on a cycle.
  • On the first cycle of every ten, it connects to the WiFi and gets a number fact from numbersapi.com.
  • For most cycles it sends the ESP32 C3 into light sleep for 3 seconds. After waking the LED flashes three times.
  • Every tenth cycle, it instead sends the ESP32 C3 into a deep sleep for 10 seconds. Coming back from sleep puts the code back into setup resulting in a LED flashing five times.

This code is a very simple (and perhaps uselessly short) example of putting the ESP32 C3 through its paces for different power modes and how you may be able to utilize it for the project. Run the updated code and study the power consumption in the different modes. Pay particular attention to the different sleep modes and the activity during the WiFi transactions. Make notes of the different numbers (lowest power, highest power, etc). Try to tie them up with their different modes! Get a plot of the ESP32 performing WiFi requests and going into different sleep modes.

See how your computer behaves with the ESP32 C3's USB port. Because the USB PHY on the ESP is getting either clock-gated or power-gated your computer may respond to it weird and/or ignore it. This is why using a indicator like an LED is a nice tool here since it can tell you roughly what area/state of behavior it is in.

#include <Arduino.h>
#include <WiFi.h> //Connect to WiFi Network
#include <string.h>  //used for some string handling and processing.

void do_http_GET(char* host, char* request, char* response, uint16_t response_size, uint16_t response_timeout, uint8_t serial);
uint8_t char_append(char* buff, char c, uint16_t buff_size);

const int RESPONSE_TIMEOUT = 30000; //ms to wait for response from host
const int GETTING_PERIOD = 5000; //periodicity of getting a number fact.
const uint16_t IN_BUFFER_SIZE = 1000; //size of buffer to hold HTTP request
const uint16_t OUT_BUFFER_SIZE = 1000; //size of buffer to hold HTTP response
char request_buffer[IN_BUFFER_SIZE]; //char array buffer to hold HTTP request
char response_buffer[OUT_BUFFER_SIZE]; //char array buffer to hold HTTP response

uint32_t last_time=0; //used for timing

//wifi network credentials for 6.08 Lab (this is a decent 2.4 GHz router that we have set up...try to only use for our ESP32s)
char network[] = "EECS_Labs";
char password[] = "";

void test_message(){
  WiFi.begin(network, password);
  //if using channel/mac specification for crowded bands use the following:
  //WiFi.begin(network, password, channel, bssid);
  uint8_t count = 0; //count used for Wifi check times
  Serial.print("Attempting to connect to ");
  Serial.println(network);
  while (WiFi.status() != WL_CONNECTED && count < 6) { //can change this to more attempts
    delay(500);
    Serial.print(".");
    count++;
  }
  //delay(2000);  //acceptable since it is in the setup function.
  if (WiFi.isConnected()) { //if we connected then print our IP, Mac, and SSID we're on
    Serial.println("CONNECTED!");
    delay(500);
    //formulate GET request...first line:
    sprintf(request_buffer, "GET http://numbersapi.com/%d/trivia HTTP/1.1\r\n", random(200));
    strcat(request_buffer, "Host: numbersapi.com\r\n"); //add more to the end
    strcat(request_buffer, "\r\n"); //add blank line!
    //submit to function that performs GET.  It will return output using response_buffer char array
    do_http_GET("numbersapi.com", request_buffer, response_buffer, OUT_BUFFER_SIZE, RESPONSE_TIMEOUT, true);
  } else { //if we failed to connect just Try again.
    Serial.println("Failed to Connect :/  Will try next time.");
    Serial.println(WiFi.status());
    //ESP.restart(); // restart the ESP (proper way)
  }
}

int state;
void setup() {
  Serial.begin(9600);
  delay(500);
  Serial.printf("Starting up!\n");
  pinMode(8,OUTPUT);
  state = 0;
  randomSeed(analogRead(A0)); //"seed" random number generator
  //five flashes on waking up from deep sleep or setup
  for (int i=0; i<5; i++){
    digitalWrite(8,1);
    delay(100);
    digitalWrite(8,0);
    delay(100);
  }
}

void loop() {
  Serial.printf("I'm up!\n");
  if (state==0){ //once in ten cycles get a number fact over WiFi
    test_message();
  }
  state+=1;
  state%=10;
  Serial.printf("%d\n",state);
  Serial.printf("going to sleep\n");
  if (state==9){
    esp_sleep_enable_timer_wakeup(10000000); // how long to sleep in us
    esp_deep_sleep_start();
    //deep sleep will effectively restart the system (everything turns off)
  }else{
    esp_sleep_enable_timer_wakeup(3000000); // how long to sleep in us
    esp_light_sleep_start();
    //light sleep *should* just come back here when finished, but note
    //two flashes on waking up from deep sleep or setup
    for (int i=0; i<2; i++){
      digitalWrite(8,1);
      delay(100);
      digitalWrite(8,0);
      delay(100);
    }
  }
  //may not see this printed depending on how your computer handles...
  // USB port coming and going
  Serial.printf("Back from sleeping!\n");
}



/*----------------------------------
   char_append Function:
   Arguments:
      char* buff: pointer to character array which we will append a
      char c:
      uint16_t buff_size: size of buffer buff

   Return value:
      boolean: True if character appended, False if not appended (indicating buffer full)
*/
uint8_t char_append(char* buff, char c, uint16_t buff_size) {
  int len = strlen(buff);
  if (len > buff_size) return false;
  buff[len] = c;
  buff[len + 1] = '\0';
  return true;
}

/*----------------------------------
   do_http_GET Function:
   Arguments:
      char* host: null-terminated char-array containing host to connect to
      char* request: null-terminated char-arry containing properly formatted HTTP GET request
      char* response: char-array used as output for function to contain response
      uint16_t response_size: size of response buffer (in bytes)
      uint16_t response_timeout: duration we'll wait (in ms) for a response from server
      uint8_t serial: used for printing debug information to terminal (true prints, false doesn't)
   Return value:
      void (none)
*/
void do_http_GET(char* host, char* request, char* response, uint16_t response_size, uint16_t response_timeout, uint8_t serial) {
  WiFiClient client; //instantiate a client object
  if (client.connect(host, 80)) { //try to connect to host on port 80
    if (serial) Serial.print(request);//Can do one-line if statements in C without curly braces
    client.print(request);
    memset(response, 0, response_size); //Null out (0 is the value of the null terminator '\0') entire buffer
    uint32_t count = millis();
    while (client.connected()) { //while we remain connected read out data coming back
      client.readBytesUntil('\n', response, response_size);
      if (serial) Serial.println(response);
      if (strcmp(response, "\r") == 0) { //found a blank line! (end of response header)
        break;
      }
      memset(response, 0, response_size);
      if (millis() - count > response_timeout) break;
    }
    memset(response, 0, response_size);  //empty in prep to store body
    count = millis();
    while (client.available()) { //read out remaining text (body of response)
      char_append(response, client.read(), OUT_BUFFER_SIZE);
    }
    if (serial) Serial.println(response);
    client.stop();
    if (serial) Serial.println("-----------");
  } else {
    if (serial) Serial.println("connection failed :/");
    if (serial) Serial.println("wait 0.5 sec...");
    client.stop();
  }
}

Checkoff 1:
Show your working Number-getting-power-saving system.

In general, with the ESP32 when it goes to light sleep and comes back, it will pick up right where it left off (you can almost think of esp_light_sleep_start() used in conjunction with a timed wakeup as a lowpower delay call.) Potential issues with the USB aside, it should just start again. The deep sleep functionality nukes the SRAM, however so the entire state of the system is gone. As a result if you do a timed deep sleep, when the system wakes back up it essentially is almost like you just pushed the reset button. The system starts anew at the beginning. This sucks since state and things are all gone.

The ESP32 C3 does have a special small portion of memory (8 KB vs the ~400 KB) that will stay powered during deep sleep. This memory, known as the RTC Memory can be used just as any other memory. The easiest way to get access to it is to use a select set of prefixes when declaring variables. Let's say we wanted a variable to live in the RTC memory. All you need to do is declare and define it like so in the global scope:

RTC_DATA_ATTR int my_variable = 7;

On first power up the variable will start at value 7. You can then do whatever you want to it in your code. If you set my_variable to 11 and then have the ESP32 C3 take some Nyquil (aka esp_deep_sleep_start()) when it wakes up my_variable will still have 11 in my_variable. You can create whatever type of variables you want in this memory (within size limitations which are...again...8KB).

There are other prefixes you can use like the following if you don't care to set a value with your variable initially:

RTC_NOINIT_ATTR int my_variable;

Mini Assignment

Your assignment, if you choose to accept it (which you don't really have a choice since it is required) is to write a program that does the following:

  • Starts up and runs.
  • First time through the loop it flashes its LED one time (on-then-off at 100 ms each).
  • Then it goes to deep sleep for five seconds on a timer.
  • Then the device wakes up, flashes its LED two times (also at the 100ms rate)...then back to deep sleep for five seconds.
  • Then the device wakes up, flashes its LED three times (also at the 100ms rate)...then back to deep sleep for five seconds.
  • Then the device wakes up, flashes its LED four times (also at the 100ms rate)...then back to deep sleep for five seconds.
  • etc...
  • etc...
  • All the way to:
  • Then the device wakes up, flashes its LED twelve times (also at the 100ms rate)...then back to deep sleep for five seconds.
  • Before resetting back to 1 flash

You will need to use a RTC variable for this since sleep-wake cycle to sleep-wake cycle, information needs to be retained. Remember every time it comes back from deep sleep it will start in setup again.

Remove all the WiFi stuff from your code so it compiles faster. Get the code working and when done, submit just your main.cpp file below:

Submit your main.cpp file once working as specified above.
 No file selected

Solar Cell Analysis

As a final task, grab one of the photovoltaic cells we have already pre-soldered wires onto. Notice the specs on the back of this particular cell say that its Pmax is 1.29W. That is a very hopeful number. This panel will likely deliver 1.29W when under very bright, direct sunlight and when you are using the element at its maximum power point that we talked about in class. In lab today we're going to use the cell under room lighting and you will be super surprised at just how awful that is. If you get a few mW out of it, that'll probably be something to celebrate.

A nice little photovoltaic cell.

A Variable Load

So as a first circuit, grab a potentiometer (I think a 50K is usable), and measure the power consumed by it (or alternatively the power supplied by the PV cell, same difference). While monitoring the power, vary the potentiometer from one extreme to another. At both extremes you should see minima of power (it might not quite be 0, but it'll be down there). Somewhere in the middle of the potentiometer range, you should be able to find a point where maximum power is being delivered to the resistor. This is the maximum power point.

Make note of this MPP and also try to see how it varies as you put more light on it (perhaps your cellphone, or if the new Schwarzman College of Computing is reflecting the blinding rays sunshine into the room) maybe you can actually get some actual power out of it.

Have the photovoltaic cell drive a varying load.

A Fixed Load

In the previous circuit you actually adjusted the load to satisfy the PV cell's characteristics. Not all devices in life can be varied like the potentiometer. Consider another diode...this time an LED, a cousin of the PV cell, just used differently and designed to give off light from electrical energy rather than make electrical energy from light. This LED has its own I-V curve that is non-variable. When you assemble a circuit of the PV cell driving this LED you will now have essentially two somewhat stuborn devices trying to exchange energy, and their ability to do so with a high efficiency (such as at the MPP of the cell) may just not be possible.

Build the circuit below. Don't worry about a current-limiting resistor. Unless you are outside or getting beams of light reflected from the Schwarzman building, there's no way the cell will be generating enough to burn the LED out. Try to see what power you can harvest from the cell. Can you get the LED to light? How do you feel about this system?

Powering an LED with a solar panel. Using light to make light. If you point the LED back onto the solar panel, you'll be able to make infinite energy.

Checkoff 2:
Show your working photovoltaic system powering an LED to a staff member. Make sure Joulescope is hooked up to give you power measurements.