Discover the power and functionality of the Ultra Low Power (ULP) co-processor on the ESP32. Learn how to read values from GPIO pins, wake up the main processor, and share data between ULP and the main processor.
[0:00] In this video, we’re going to be looking at the Ultra Low Power or ULP co-processor on
[0:05] the ESP32.
[0:07] This is a really interesting and powerful feature on the ESP32 that offers some very
[0:11] exciting functionality.
[0:14] It’s worth having a watch of the previous deep sleep video - there’s a link in the description
[0:16] and it contains some really useful information.
[0:20] So, why am I even looking at the ULP?
[0:24] One of my latest projects is a DIY e-reader - for this, I want to use deep sleep to preserve
[0:29] battery life.
[0:30] The board I’ve got comes with a set of buttons and I’ve written the software so that it can
[0:34] be used with just 3 buttons, up, down and select.
[0:38] EXT1 sleep mode would be ideal for this - it can monitor multiple buttons and can tell
[0:43] you which button was pushed - but, it can only do this if the buttons are active high.
[0:48] Looking at the schematic for the board I have, the buttons all have external pull up resistors
[0:53] - this means that they are active low and can’t be used with EXT1 deep sleep mode.
[0:58] With the ULP coprocessor, we have a lot more flexibility - we can write code that runs
[1:03] on the ULP coprocessor waiting for one of our buttons to go low.
[1:07] We can then wake the device up when that happens, and we can tell it which button was pushed.
[1:12] It’s pretty cool!
[1:13] So what are going to cover in this video?
[1:16] I’ll describe what the ULP processor is.
[1:19] We’ll do a quick run-through of the instruction set that can be used to program the ULP.
[1:24] I’ll show how you use it in PlatformIO and the Arduino IDE.
[1:28] I’ll then run through some examples that will show you how to:
[1:32] Read values from the GPIO pins
[1:34] Wake up the main processor
[1:35] Share data between the ULP coprocessor and the main processor
[1:39] And then we’ll write values to the GPIO pins - we’ll write a blink sketch that runs entirely
[1:44] on the ULP processor
[1:47] There are some advanced use cases including reading from the ADC and I2C devices that
[1:52] we’ll cover in future videos.
[1:54] We’ll get started after this quick word about the channel sponsor PCBWay - They’ve been
[1:59] sponsoring the channel for a while and we’ve built a few PCBs with them.
[2:03] We’ll be making some more PCBs in some future videos as I want to learn a bit more about
[2:07] KiCad.
[2:08] They also do 3D printing and CNC work.
[2:11] Check out the link to PCBWay in the description.
[2:14] So, what is this ULP coprocessor thing anyway?
[2:18] The Espressif docs describe it as the following:
[2:20] “The Ultra-Low Power coprocessor is a simple Finite State Machine which is designed to
[2:26] perform measurements using the ADC, temperature sensor, and external I2C sensors, while the
[2:32] main processors are in deep sleep mode.”
[2:35] Although they describe it as a simple finite state machine it’s slightly easier to think
[2:39] of it as a very basic low power processor that can run code even when the main processors
[2:45] are asleep.
[2:46] The ULP processor does not run all the time, you schedule it to run with a set time period
[2:51] - it runs and then when the ULP program completes it stops running until the next scheduled
[2:56] time.
[2:57] To program the ULP we use some simple assembly language - don’t let this put you off, it
[3:02] really is very simple and not something to be afraid of.
[3:05] If you’re using Arduino then there is a very basic C-compiler that you can use as well.
[3:10] The ULP coprocessor has 4 16 bit general purpose registers labelled R0, R1, R2 and R3.
[3:19] It also has an extra 8-bit register called “STAGE_CNT” which can be used to implement
[3:24] loops and has some special instructions.
[3:27] It may help to imagine these registers as variables in a normal C program.
[3:31] There are 26 instructions in total.
[3:34] I’ve tried to group these into logical areas.
[3:37] There are three instructions for getting values into our registers.
[3:41] The move instruction will move a value from one register to another, or it will load an
[3:45] immediate value into a register.
[3:48] The STORE instruction will store the contents of a register into a memory address contained
[3:52] in another register with an optional signed offset.
[3:56] The LOAD instruction reads from the contents of a memory address in a register with an
[4:02] optional signed offset.
[4:04] We have two arithmetic instructions - ADD and SUB - as the names suggest these instructions
[4:09] let you add and subtract values.
[4:11] We can either add or subtract registers.
[4:14] Or we can add and subtract numbers from registers.
[4:17] We also have a set of bitwise operators - AND, OR, Logical shift left and logical shift right.
[4:23] These all behave in the same way as the bitwise operators in C and C++.
[4:28] We then have three jump commands - we have the absolute jump command.
[4:32] This can jump to the address in a register or to an immediate address.
[4:36] It also supports two conditions, EQ - which is true if the last ALU operation resulted
[4:42] in a 0 or OV which will be true if the last ALU operation resulted in an overflow.
[4:48] In this example, we set R0 to 1 and then AND it with the value 1, the result of this is
[4:54] non zero so the first jump fails, the second jump has no condition so jumps.
[5:00] This sample code is the equivalent of a C if-else construct.
[5:04] Then we have a relative JUMPR command, this will jump based on the contents of the R0
[5:10] register and a threshold.
[5:11] And the last jump instruction is the JUMPS command, this will jump based on the contents
[5:16] of the STAGE_CNT register and a threshold.
[5:19] This leads us to the specific STAGE_CNT instructions.
[5:23] We have STAGE_RST which will reset the stage count, STAGE_INC which will increment the
[5:28] STAGE_CNT register and finally STAGE_DEC which will decrement the STAGE_CNT register.
[5:33] You can use these instructions in conjunction with the JUMPS command to implement simple
[5:37] for loops.
[5:39] We then have the commands for controlling the ULP execution.
[5:42] The HALT command will end the ULP program and the ULP processor will stop running until
[5:47] it’s woken up again by the timer.
[5:49] The WAKE command wakes up the main processor - we’ll talk about this in a bit with some
[5:54] caveats on when the main processor can be woken.
[5:57] The WAIT command delays for a number of cycles.
[6:00] We then have a set of instructions for reading from peripherals - TSENS will read from the
[6:05] internal ESP32 temperature sensor, ADC will read a value from the analogue to digital
[6:11] converter and we have a couple of functions for reading from an I2C slave device.
[6:16] We’ll cover these instructions in future videos.
[6:18] You can find more detailed documentation at this link - I’ve added this to the video description.
[6:24] So how do we actually create a program for the ULP and run it?
[6:28] Let’s have a look at the process with PlatformIO first:
[6:31] PlatformIO supports ULP programming, but only if you are targetting the ESP-IDF.
[6:36] If you want to use Arduino code then you’ll need to use the Arduino framework as a component.
[6:42] Adding support for the ULP is pretty straightforward.
[6:45] We need to create a folder and add a file containing our ULP code - this must have a
[6:49] capital “S” as the extension.
[6:52] Here I’ve added a very simple program that will wake up immediately.
[6:55] We need to make the project aware of this file by modifying the CMakeLists.txt file
[7:00] in the src folder.
[7:01] The first line defines a name for our ULP app.
[7:05] The second line tells the compiler which files contain our ULP code.
[7:08] The third line tells the compiler which of our files will be using any variables exported
[7:14] by the ULP code.
[7:16] And the fourth line actually embeds the ULP binary blob in our project.
[7:20] If we try and build now we’ll get an error - we need to allocate space for our ULP program and enable the ULP processor.
[7:27] We do this by running menu config.
[7:30] In PlatformIO you do this by opening up the PlatformIO terminal and then running:
[7:35] pio run -t menuconfig
[7:38] We then need to go to Component config -> ESP32 specific and enable the Ultra Low Power ULP
[7:45] Coprocessor.
[7:46] By default, this will reserve 512 bytes of RTC memory for our code.
[7:51] With that saved our project now builds successfully.
[7:54] Our last job is to actually load the embedded ULP code into the coprocessor and run it.
[8:00] In our main.cpp file we can now include an automatically generated header file ulp_main.h
[8:06] - this contains any variables that have been exposed by our ULP program - we’ll talk about
[8:11] this in a bit.
[8:12] We have two external constants that point to the start and end of our embedded ULP binary.
[8:18] If we weren’t woken by the ULP processor then we load up our ULP binary.
[8:22] I’ve then set up the code to wait for a button press.
[8:25] As soon as the button is pressed we go into deep sleep.
[8:29] We enable ULP wakeup.
[8:31] Set the period - in this case, I’m just setting the period to be around 5 seconds as my ULP
[8:35] program wakes up the main processor immediately.
[8:38] We then run our ULP program and go into deep sleep.
[8:41] If we run this up on the device and then push the button, you can see that after about 5
[8:46] seconds our device wakes up again and the wake cause is the ULP processor.
[8:51] If you’re using the Arduino IDE then there is a plugin that you can install a plugin
[8:56] called ulptool - there are instructions on the README for this.
[8:59] I won’t run through the setup as it varies depending on the operating system you are
[9:04] using.
[9:05] The main difference between this and the PlatformIO/ESP-IDF approach is that the files must have a lowercase
[9:11] “s” for the file extension.
[9:12] So, how do we wake up the main processor from the ULP coprocessor - this is as simple as
[9:18] issuing the WAKE instruction - however, there is a bit of boilerplate code that needs to
[9:23] be run before this.
[9:25] We need to make sure that the RTC controller is ready to wake up the main processor.
[9:30] To check for this we need to monitor the ready for wakeup bit on the RTC control low power
[9:35] status register.
[9:37] The READ_RTC_FIELD is a helper macro that you can use to read a set of bits from the
[9:42] register.
[9:43] If the bit is low then we are not ready to wake the main processor.
[9:46] We check for the low bit by anding the value in R0 with 1 - if the result of this is zero
[9:52] then the EQ flag will be true and we’ll try again.
[9:55] If the result is not zero then the EQ flag will be false and we’ll issue the WAKE instruction.
[10:01] We make use of the stage counter register here so that we only try to wake up 10 times
[10:05] before giving up.
[10:06] This prevents our ULP program from getting stuck forever.
[10:10] Reading from GPIO pins is pretty straightforward with a couple of gotchas for the unwary.
[10:15] We need to set up the GPIO pins in our main program so that they can be used by the RTC
[10:20] controller.
[10:21] We covered this in more detail in the previous deep sleep video, but you do this using the
[10:25] rtc_gpio commands.
[10:27] In our ULP code, we use the READ_RTC_REG macro - we use this to read values from the RTC_GPIO_IN_REG.
[10:36] If we look in the ESP32 technical manual we can see that this register contains the values
[10:41] for the RTC GPIO pins in bits 14 up to bit 31.
[10:46] The bit will be 1 if the GPIO is high and 0 if the GPIO is low.
[10:52] These bits correspond to the RTC_GPIO0 in bit 14 up to RTC_GPIO17 in bit 31.
[11:00] We can only read 16 bits at a time as our registers R0-R3 are all 16 bits.
[11:07] The command shown on the screen will read the 16 bits from the register starting from
[11:11] the bit position specified by RTC_GPIO_NEXT_S into register R0.
[11:17] RTC_GPIO_NEXT_S is defined to be 14 so it starts reading at bit 14.
[11:23] As we can only read 16 bits at a time if we want to read RTC GPIOS 16 and 17 we need to
[11:30] adjust the bit position that we start reading from.
[11:33] This will place the value for RTC GPIO 16 in bit 14 and RTC GPIO 17 in bit 15.
[11:41] Armed with this knowledge we can now write a ULP program that will monitor the pins on
[11:45] my e-paper board.
[11:46] The pins I want to monitor are: GPIO_NUM_34, GPIO_NUM_35 and GPIO_NUM_39.
[11:53] You’ll recall from our previous video that there is a mapping from RTC pins to GPIO pins.
[12:00] We can use this mapping to work out which bits we need to check for our GPIO pins.
[12:05] GPIO_NUM_39 is RTC GPIO 3, GPIO_NUM_34 is RTC GPIO 4, GPIO_NUM_35 us RTC GPIO 5.
[12:18] The code for checking if these bits are low is very similar to the code that checks if
[12:22] we are ready to wake up.
[12:23] We read in the current GPIO values from the register and then we and the value with the
[12:28] bit that corresponds to the RTC pin we are interested in.
[12:31] GPIO_NUM_34 is pin 4, if the result of this is zero then the pin must be low.
[12:38] We do this for the other GPIO pins we are interested in.
[12:41] GPIO_NUM_39 is pin 3 and GPIO_NUM_35 is pin 5.
[12:46] This is all very interesting and we can now wake up the device when the user pushes the
[12:50] buttons - but this is no different from the EXT1 mode that we used in the previous video
[12:55] - we need some way of knowing which button actually woke us up.
[12:59] We need to get data from the ULP coprocessor to the main processor.
[13:04] This is actually surprisingly easy to do, in our ULP code we can mark labels as being
[13:09] “global” doing this exposes them to our application code as global variables prefixed with the
[13:14] value “ulp_”.
[13:16] We can read and write into these addresses and the values will be available in our main
[13:20] code.
[13:21] We can also put values into the variables in our main code and the values will be available
[13:26] to the ULP.
[13:28] With that done we now detect which button actually woke us in our application code.
[13:32] In our ULP code we’re writing the value we read from the GPIO register and then our application
[13:38] code we’re checking to see if the GPIO pins were low.
[13:42] One thing to be aware of is that only the bottom 16 bits of the variable will contain
[13:46] data as our ULP registers are all 16 bits.
[13:50] You can check out my e-reader code to see a more detailed and flexible example.
[13:54] So, how about writing to GPIO pins?
[13:57] Is that possible - let’s make a blink sketch that runs on the ULP.
[14:02] We can see from the serial output that we’ve gone into deep sleep.
[14:05] Looking at our code we don’t have anything writing to the LED - everything is happening
[14:10] on the ULP.
[14:11] To set output values we use the RCT_GPIO_OUT_REG, GPIO2 which is where our built-in LED is connected
[14:19] maps onto RTC012 which is bit 26 of the register.
[14:24] There’s a couple of extra hoops to jump through, in order to maintain the state of our output
[14:28] when the ULP program is not running we need to set the hold bit of the pad.
[14:32] So before we change the value we need to clear the hold bit, we then read the current value
[14:37] of the pin.
[14:38] We use the READ_RTC_REG macro and use the RTC_GPIO_OUT_DATA_S macro adding on 12 to
[14:46] get the bit for the RTC GPIO 12.
[14:49] If the bit is currently set then we clear it, if the bit is not set then we set it.
[14:55] We then put the hold bit back onto the pad and finish our program.
[14:59] In our application code, we’ve scheduled the ULP program to run every 500ms so it keeps
[15:04] blinking until we reset the device.
[15:07] Check out my previous video that explores deep sleep and wakeup using the built-in functionality
[15:11] of the ESP32, you often don’t need to use the ULP at all.
[15:15] There’s a whole bunch more really interesting applications that we can do using the ADC
[15:19] and I2C peripherals and the ULP, we’ll be covering these in a future video.
[15:24] See you soon!