Hi everyone, my name is Darian and my presentation today is titled ESP-IDF Getting Started, a beginner's guide to key concepts and resources. I'm a Senior Embedded Software Engineer at Espressif and I'm part of the Software Platforms team. So what was the motivation for this presentation? Over the years, we've noticed a significant number of users who had difficulty getting started with IDF. Thus, if any of these Reddit threads resonate with you, then this presentation is for you. The goal of this presentation is to briefly cover the main concepts you need to be familiar with in order to get started using IDF. So what am I going to be covering in this presentation? First I'll take a look at why you may want to use IDF. And next, I'll go over the general idea of workflows such as building and flashing. Then I'll briefly talk about the IDF programming model. And afterwards, I'll go over how to find IDF features for your project. And lastly, I'll give a brief introduction to IDF's build system and where else to get resources. So, before we begin, here are some notes about this presentation. I've embedded some hyperlinks in the slides, so if you see this icon in the slide, there's a hyperlink. Next, all of the information about the IDF presentation is based on IDF v5.1. And for any demos of using IDF, I'll be showing them via command line on Ubuntu and also via the Eclipse IDE on Windows. And lastly, all the demos are run on an ESP-WROVER-KIT. So without further ado, let's begin. So let's first talk about why you may want to use IDF. Let's say you've just decided to develop an ESP-based project and you're choosing which SDK to use. What are some of the things you might want to consider when choosing an SDK? Firstly, which development language does the SDK support? Do you want to develop your project in C and C++? Or do you prefer a higher level language like Python or JavaScript? Next, what features does the SDK provide? Does it have all the libraries and networking stacks that you want? And you also have to consider how configurable the SDK is. Are you able to fine-tune the SDK to your product's requirements? And how well-maintained the SDK? Will you be able to receive support and updates during development? And how easy is the SDK to use? Does it support a development environment that you're comfortable with? And what types of testing and debugging tools as the SDK offer. So what is ESP-IDF? If you don't know already, ESP-IDF stands for Espressif IoT Development Framework and is one of the many SDKs you can use to develop ESP-based projects. It's entirely hosted on GitHub and it's open source as well. On the surface, it just seems like a GitHub repository, but as we'll see later, the repository is just a centerpiece of a much wider SDK containing various tools and libraries. So given the considerations we've just discussed, Here are some of the reasons why you may want to use IDF. Firstly, IDF projects and IDF itself is written in C and C++. IDF contains a lot of components including a Wi-Fi stack, a network stack, a Bluetooth stack, etc. And it also has a Component Manager to download more components if the ones in IDF aren't enough. In terms of configurability, IDF can be configured at the component level. And IDF is also actively maintained with constant bug fixes and new feature releases. So, generally speaking, new chips and features are supported on IDF-first before any of the other SDKs. And in terms of ease of use, IDF can be used from command line or through one of the supported IDEs. And finally, IDF supports a wide range of tools for debugging and testing, such as GDB for debugging and the Unity test framework for testing. So, let's say you've chosen IDF, you might be wondering next, how do you use it? So let's look at the basic workflow of an IDF project. First things first, you need to decide which development environment to use. What are the options? Firstly, you can use it from command line paired with a text editor of your choice. Or, IDF also supports integration with Eclipse as a plugin or a stand-alone IDE. And there's also integration with VS Code as an extension. For this presentation, I'll be demonstrating the workflow in command line and the Eclipse IDE. So next, we need to install IDF. The exact instructions for installations would depend on your environment, but the underlying process is pretty similar. First, we need to install the system package dependencies such as Python, Git, and CMake. Next, we use Git to clone the IDF repository from GitHub, and once cloned, we'll run the IDF install script which handles the rest of the installation. The script will install the toolchain and a bunch of Python packages inside a virtual environment. And at this point, the installation is complete, so let's create a project. Next thing we need to do is run the export script which exposes all the tools we've just installed. And finally, when compiling a project, the IDF Component Manager will also help download any external components that your project may use. So what does IDF installation actually look like in practice? If you're using command on Linux, the installation will go something like this. First, we install the system package dependencies such as Git, Python, and CMake. Next, we create an ESP directory and we clone the IDF repository into that directory. Then we run the install script, the export script, and finally we should be able to call the idf.py command. This is the main command for most IDF operations in command line. And just to expand a bit about idf.py, here are some of the sub commands it offers. You'll see that it covers most of the operations you need during development such as creating a project, configuring the project, building it, flashing it, and also monitoring it when it executes. As for the installation on Eclipse, it's done through a Windows installer. And once you've stepped through the installer, you should be able to use IDF through Eclipse's native user interface. So now that we're done with installing, let's create a project. Well this is quite simple. In command line you just use the create project command, and in Eclipse you just press the create project button. The result is a new project with the following files and directories. But you might be wondering what are those files and directories for? So let's take a look. So first of all we have the main directory. And this basically represents something called the main component. And within the main component there's a CMake file to register the main component and there's also a source file for the main component. And this source file basically contains the app_main() function which serves as the entry point for our project. And lastly at the project directory [level] we have another CMake file which declares the entire project. So now that we've created the project, what do we do? The general workflow of IDF can be split into four steps. First, we need to configure the project to generate a configuration file. Next, we need to build the project to generate binary files. Afterwards, we need to write those binary files into ESP's flash memory. This is known as flashing. And finally, the ESP executes those binary files from flash where we can monitor the execution's output. So let's look at configuration in practice. In command line, you first have to set which ESP chip you're using. This is also known as the target. Then you call the menuconfig command, and by doing that, it will launch the Kconfig user interface, which allows you to set all the configuration options. In Eclipse, similarly, we first have to set our targets. However, we have to build the project first. And then afterwards, if we click on the sdkconfig file, this will launch the configuration menu inside Eclipse. So after configuring a project, you should see two new things in your project directory. First, is the build directory. And this is basically a directory to store all of the temporary files generated during your typical IDF workflow. And secondly, there's the SDK config file, and this basically stores all of the configuration values. So if we take a look inside the sdkconfig file, we'll see all the configuration values are grouped by components, and every configuration option that is offered by a component will have a value inside this file. So now that we've configured the project, we need to build the project. In command line we simply use the "all" command and in Eclipse we simply press the build button. In either case, the build should generate an output log similar to this. So some things to notice about the build log is that the build system compiles all source files of all components. But on repeated builds, it will only build the source files that have changed. So you notice that on first build, the build might take a very long time. But if you try to rebuild it, it's much quicker the second time around. And the second thing to notice is that the build generates three binary files. And these are the partition table, the bootloader, and the application binary. You don't need to understand what the three binary files do for now, just know that our project's code is compiled into the application binary. So we now need to flash the three binary files to an ESP. But before we cover flashing, it's useful to understand the ESP's hardware. So here we have an ESP32 Development kit. And on the development kit is an ESP32 module. Inside the module is an ESP32 chip that's connected via SPI to a flash chip and optionally a RAM chip. Note that all of these chips are inside the module. So how does the ESP communicate with the PC? This is done through a "UART to USB" bridge which routes through the USB port to the PC. And you also notice that the bridge is connected to the Enable ("EN") and "IO0" pins. This is used by the PC to reset the ESP from software and also to put the ESP into download mode. However, the Enable ("EN") and "IO0" pins are also routed to two buttons for you to control manually. So what does the flashing process look like? We have three binary files, and we need to flash them to the ESP SPI Flash chip. The first step is to put the ESP into download mode, and this can be done using the Enable ("EN") and "IO0" pins. Next, we need to transmit the binary files over UART to the ESP. The ESP being in download mode will then forward the data to the SPI flash. And when all the binary files get flashed, the flashing process is complete. So what does flashing look like in practice? In command line, we first have to find the ESP serial port. It's usually one of the dev/ttyUSB files. And then we call the flash command with the serial port as an argument. Similarly, in Eclipse, we have to find the ESP serial port using the Windows device manager. And it's usually one of the COM ports. Next we create a new launch target in Eclipse with the correct port number. And then we press the Run button to flash. And the resulting flash logs should look something like this. Notice that the "esptool" gets invoked by the IDF build system to handle the flashing. And once the flashing is complete, the ESP is reset back into execution mode. So let's monitor the execution of the ESP. So how do we do that? In command line, we use the monitor command with the same port argument. In Eclipse, you simply open a new terminal tab. In both cases the IDF monitor is launched in order to monitor the UART output of the ESP. So typically your monitor logs should look something like this. Let's take a deeper look. Here the ESP has just been reset into execution mode and then afterwards it immediately jumps to the first stage bootloader and all the first stage bootloader does is to load the second stage bootloader and once loaded we jump to the second stage bootloader. Next, now we're in the second stage bootloader. And the second stage bootloader reads the partition table in order to find an application binary. Then we load the application binary, and then we jump to the application binary. So now we're in the app binary. And here we can actually see the application's name. The application does some system initialization, such as starting the operating system. And finally, it calls app_main. Now, we finally arrived in the app_main function where we call a "Hello World" and it gets printed. This log basically outlines a typical IDF boot process. And if you want to know more about the boot process, go visit the link in this slide. Now, that we've covered the basics of IDF project workflow, we now need to learn how to program an IDF project. So here's the app_main entry function, where do we go from here? What if we use a superloop, similar to what you'd find in Arduino or STM32? While this may be valid, it's not actually optimal. To illustrate this, let's use a very simple example. Let's say I'm trying to build a project where I press a button, the ESP32 reads an I2C sensor, then outputs the sensor value to an LCD screen. In this case, my superloop would look something like this. Here I check for a GPIO press, then handle the GPIO, then read from the I2C sensor, and then finally push the sensor value to the LCD. It's simple enough, what happens if one of the stages takes too long, for example, the LCD update? then the entire loop will be stuck while LCD is updating. To illustrate this, let's use a very simple example. Let's say I'm trying to build a simple project where I can press a button, it causes the ESP32 to read from an I2C sensor, then the sensor value gets pushed to an LCD screen. In this case, my superloop would look something like this, where I check for a GPIO press, handle the debounce, read from the I2C sensor, and then push the sensor value to the LCD screen. That's simple enough, but what happens if one of the stages takes too long, for example, the LCD update, then the entire loop we stuck while the LCD is updating. And what's more is if we had more subsystems, like a file system or a Wi-Fi stack, then each new iteration would get quite long. Also, how do we pass data between the different subsystems? So these are some of the common issues with a superloop. But what if we could run each subsystem in parallel? What if each subsystem was its own thread running in its own loop, and each loop could then block on or wait for some event. And then also, each thread could also give events to other threads. This is, in essence, what multi-threading is. And our application will look something like this. But multi-threading requires an operating system. And lucky for you, IDF supports FreeRTOS out of the box. Well, what is FreeRTOS, you might ask? It's a real-time operating system, AKA an RTOS, that is integrated directly into IDF. And you can use RTOS API. And you can use the FreeRTOS API to make a project multi-threaded. But what are some of the key features of FreeRTOS? It's a kernel that is very small and simple. It's free and open source, and it provides all the basic features you might expect out of a real-time operating system. So what would multi-threading look like using FreeRTOS? Well, firstly, FreeRTOS refers to threads as tasks, and each task is implemented as a C function. And each task will generally have an infinite loop, and inside the infinite loop, each task can call FreeRTOS API to block on some event. And you can also call FreeRTOS API to give some event. And finally, when the task is done running, the task needs to delete itself and never return. So FreeRTOS tasks can be in one of four states at any one time, these states being running, ready, blocked, and suspended. A running task is a task that's currently being executed by the CPU. A ready task is a task that's ready to execute, but it's not currently being executed because the CPU is executing another task. And a block task is basically a task that's waiting for some event. So it won't be executed by the CPU, thus won't consume any CPU time. And finally, a suspended task is just a task that's blocked indefinitely. So some of you might be asking, how can a task be ready? Won't all ready tasks just run in parallel? Well, in actuality, a CPU can only ever execute one task at a time. But if the CPU switches between ready tasks fast enough, it will look like they're running in parallel. So if FreeRTOS is switching between tasks, how does it choose which task to switch to? And that's when we come to so this brings us to the FreeRTOS scheduling algorithm. In other words, how does FreeRTOS choose which task to switch to? The FreeRTOS scheduling algorithm can basically be summarized as a Fixed-Priority Preemptive Scheduler with Time Slicing. Fixed Priority means that every task is assigned a priority, and the scheduler always chooses the highest priority ready state task to execute. Reemptive means that the scheduler can switch out an executing task at any moment. This means if a higher priority task suddenly becomes ready, the scheduler executes the higher priority task immediately. And lastly, time-slicing means that if there are multiple ready tasks of the same priority, the scheduler will switch between them in a round robin fashion. And the frequency of this switching is governed by a periodic system tick. So this basically summarizes all of the core concepts of FreeRTOS. Unfortunately I won't have time in this presentation to go further in depth, so I'm leaving some links to the extra resources here. The next important concept of the programming model is memory. Though this is a traditional C memory model that you might have learned in school, there's a text section for code, data/bss for static data, and the heap and stack grow in opposite directions. But what are some issues with this model? If there are multiple tasks in FreeRTOS, shouldn't each task have its own stack? And what if the memory in an ESP32 is not uniform? So here's a simplified version of IDF's memory model. There are a couple of things to notice here. Firstly, an ESP's physical memory is not uniform. There are different types of internal memory, such as DRAM, IRAM, and RTC memory. There's also external memory, such as SPI flash and SPI RAM. And these are accessed through an internal cache. As a result, the memory model would look something like this, where there are multiple bss and data sections for each different type of memory. There's also a heap that can map to different types of memory. And finally, there are multiple stacks can be allocated statically or dynamically. If you didn't understand any of this, don't worry. The key takeaways that I want you to keep in mind are that the ESP has different types of memory, multiple tasks and stacks exist at the same time, and furthermore, these tasks can be allocated statically and dynamically. Unfortunately, I don't have time to go further into memory, so I'll also link some resources here. So now, let's move on to IDF features. How would you know which features to use and where to find them? So to demonstrate this, let's walk through a previous example involving the button, an I2C sensor, and an LCD. How do I know which features to use? First, I'm going to break down my project into subsystems. In this case, they're the button, the sensor, and the LCD. Next, for each subsystem, I'll have to look around for a suitable component. In this case, the GPIO driver, I2C driver, and LGVL component will be useful. Afterwards, for each component, I'm going to look at the component's API reference and then figure out how to implement all of my glue logic. In this case, I might use two tasks, where the sensor task blocks on a button press from the GPIO driver, and then when the button press arrives, it triggers an I2C sensor read to get a sensor value. And finally, it passes that sensor value to the LCD task using a FreeRTOS Queue. Likewise, the LCD task blocks on the FreeRTOS Queue, and as soon as it receives a new sensor value, it triggers an LCD update. So, now you might be wondering, where do I find these components? They are three main sources for IDF components. Firstly, there are components within an IDF. To see what IDF has, simply go to the API reference, and this should list all of the components and APIs you can call within your project. But if IDF doesn't have a component you want, you should also take a look at the IDF Component Registry. These are components maintained outside of IDF and can be pulled into your project using the Component Manager. And finally, you have the final option of just writing your own component. This doesn't mean writing from scratch. You could also find some third-party library or third-party source code, and then turn that into an IDF component. So now some of you might be wondering, what are components? Thus, let's talk a bit about our build system and what components are. The IDF build system is based on CMake. This basically means that we've added some custom features on top of pure CMake. What also are these additions? Well, firstly, IDF has a concept of components. And you can think of them as libraries, but with extra features. And to accommodate these components, IDF has had to add some CMake wrapper commands for you to call. And thirdly, CMake is integrated into the idf.py command, which means you don't have to call CMake directly. When you call idf.py, CMake gets launched automatically for you. And finally, it's still possible to use pure CMake, but some extra steps need to be taken. So what are components? An IDF component is basically a library with extra "stuff" in it. Let's look at what an IDF component typically consists of. First and foremost, a component includes source files include directories. This is the same with the CMake library as well, but a component can also include linker fragments. These are basically scripts to specify the memory, placement of data, and functions of the component. And some components also embed binary data, other components can directly contain pre-compiled libraries, almost all of the components have a configuration file, and most components also contain unit tests. While it's possible to incorporate all of these extra things using pure CMake commands, the amount of boilerplate code will be significant. Thus, IDF has provided wrapper commands to conveniently wrap all of these extra things into a single command. So how does registering a component compare to registering a CMake library? In pure CMake, we might register a library and its source files and then specify other library dependencies. With the component, it's actually quite analogous. There's the IDF Component Registry command where you specify all of the source files and include directories for this component. Then you might specify other component dependencies, and finally any linker fragments for this component. Likewise, how does declaring an IDF project compare to declaring a CMake executable, for example? In pure CMake you might create your project and add executable and then list all of the library dependencies for this executable. In IDF, in the top level CMake file, we first pull in all IDF CMake wrapper commands, then we declare the directories of where to look for components, and finally we declare a project. And that's pretty much it. Hopefully I've summarized the very basics of the IDF build system. If you want to learn more about the build system, there's a very thorough build system document linked in the slide. So now we've come to our last section, which covers where to get more help and resources. While you're developing with IDF, you're eventually bound to run into something called a Guru Meditation Error. So some of you are asking what that is and basically it just means a fatal error. In other words an error that's non-recoverable. So let's do a simple demo here to recreate a fatal error and you guess what's going to happen here. So hopefully you've noticed that this is a NULL pointer access which causes a fatal error. As a result you should get a panic log that looks something like this. So when you see one of these panic logs it's important that you not panic (pun intended). there's actually a lot of information that can be gleaned from these panic logs. so let's analyze this panic log. So typically a log will contain four pieces of useful information. firstly, there's a fatal error cause. In other words, what was the reason for this error? In this case, we accessed a NULL pointer so we should get a StoreProhibited error. Next, there's a register dump. These are the CPU register values at the moment that the error occurred. And because we access a NULL pointer, the exception virtual address register is of particular interest. This basically shows the address accessed that caused the error, and in our case it's the NULL pointer. And clearly there's a back trace. This basically prints the addresses of the function calls that led up to the error. And finally if we're running IDF monitor, IDF monitor will decode those addresses automatically. So hopefully that basically covers how to understand these fatal errors. If you want more details about fatal errors, please click the link above in the slide. So now, let's say you've run into a bunch of fatal errors and you still can't find the root cause. In this case, IDF offers numerous debugging tools. For runtime debugging, we have GDB, which allows you to set breakpoints and also step over the code line by line. However, this requires a JTAG interface. If you can't use a JTAG interface for whatever reason, we also have application tracing and also GDB Stub, which just require UART. And for Post-Mortem debugging, which means debugging after a device has crashed, we have GDB Stub and also Core Dump. Core dump basically dumps some critical information about the ESP32 at the time of the crash, such as the backtraces of all the tasks and some register dumps as well. So finally, let's say you've debugged and you still can't find the issue and you're totally stuck, where can you get help? Firstly, you can consider reporting an issue on our IDF's GitHub repo. If you have a feature request, you can also request on GitHub, but please for both issues and feature requests, please follow the issue template. Or if you've written your own feature, you can also consider contributing it to our IDF repo. Secondly, we also have the ESP32 Forum where you can ask questions and discuss issues with other users. So unfortunately, that's all the time I have for today. I hope this presentation has provided enough for you to get started using IDF. Thanks for watching and I'm happy to take any questions.