ESP32 开发基础(环境搭建及基础的RTOS概念) Published on Sep 18, 2024 in 日常 with 0 comment ## ESP32 开发学习记录 ### 一. 序论 #### 1.1 背景介绍 因为最近想做一部分关于超宽带部分的预研,原型产品主控打算选择一款蓝牙加 Wi-Fi 的二合一芯片,所以考虑采用乐鑫的相关方案。  #### 1.2 关于 ESP32 从官方介绍来看,这是集成 2.4 GHz Wi-Fi 和蓝牙的双模芯片方案,具有超高射频性能、稳定性、低功耗和可靠性,然后对于外设的支持度也不错。 #### 1.3 环境介绍 开发环境:VSCode 及 ESP-IDF。 开发板:乐鑫 ESP32-S3. 其中 S3 最高支持到 240 MHz 的主频,支持蓝牙5.0(LE)和 2.4 GHz Wi-Fi,SRAM 512 KB,ROM 384 KB,带有 45 个 GPIO。 ### 二. 开发环境搭建 #### 2.1 虚拟机与 Ubuntu 系统 早些时候其实已经在 Windows 中安装了 VSCode 和 IDF 的相关环境,但是实际测试发现编译框架时效率比 Linux 低很多,所以直接用 Oracle VM VirtualBox 和 Ubuntu 来进行开发。当然,理论上也可以不用安装额外的虚拟机,通过微软自带的 WSL 也可以实现,不过具体没测过。 - 虚拟机下载链接:[Oracle VM VirtualBox](https://www.oracle.com/cn/virtualization/technologies/vm/downloads/virtualbox-downloads.html "Oracle VM VirtualBox") - 乌班图官方镜像链接:[Ubuntu 24.04 LTS](https://mirrors.sdu.edu.cn/ubuntu-releases/24.04.1/ubuntu-24.04.1-desktop-amd64.iso "Ubuntu 24.04 LTS") 下载完成后,打开虚拟机,新建虚拟电脑。将文件夹路径、镜像位置等信息填入。然后点击下一步,在自动安装配置界面中修改用户名和密码。接着配置一下内存大小和处理器,我这里是分配了 4G 内存和 4 个核心给到虚拟机。最后分配一个合适大小的虚拟硬盘给到虚拟机,最后点击完成即可。  配置成功后,虚拟机启动并运行镜像,这里会跳入到可视化的安装界面。最开始选择中文,然后逐项确认安装即可。 #### 2.2 修改 Ubuntu 镜像源 如果直接使用默认镜像的话,在国内下载软件包会非常慢,所以建议在开始之前先把默认的镜像源备份并换成国内的。具体操作步骤如下:  (1)24.04 LTS 版本提示软件源配置文件路径为 /etc/apt/sources.list.d/ubuntu.sources,因而使用如下命令备份原有的镜像配置: `sudo cp /etc/apt/sources.list.d/ubuntu.sources /etc/apt/sources.list.d/ubuntu.sources.bak` (2) 使用 vim 编辑器修改 ubuntu.sources 文件: `sudo vim ubuntu.sources` (3)添加清华镜像源: ```shell Types: deb URIs: http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ Suites: noble noble-updates noble-security Components: main restricted universe multiverse Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg ``` (4)更新源列表: `sudo apt-get update` (5)因为在 Windows 系统和虚拟机中来回切换比较繁琐,所以直接在 Ubuntu 中开启 SSH,利用 FinalShell 来进行统一的操作。 `sudo apt install openssh-server` #### 2.3 IDF 环境安装 1、 进入系统后,打开命令行,然后安装前置的各项必备工具,命令如下: `sudo apt-get install git wget flex bison gperf python3-pip python3-venv cmake ninja-build ccache libffi-dev libssl-dev dfu-util libusb-1.0-0 net-tools ` 2、 安装完成后,新建一个目录,然后拉取一个 ESP 工具。 `sudo git clone https://gitee.com/EspressifSystems/esp-gitee-tools.git` 3、 进入 ESP 工具,然后执行一个脚本,这会对 git 上的镜像地址进行替换,从而加快速度。 `sudo ./jihu-mirror.sh set` 4、 回到上级目录,然后拉取 IDF 源码。 ``` sudo git clone --recursive https://github.com/espressif/esp-idf.git ``` 显示如下图所示,代表 IDF 已经拉取成功。  5、 进入 ESP-IDF 文件夹内,切换版本为 5.2 并重新更新子模块: `git checkout v5.2` `git submodule update --init --recursive` 6、 更换 pip 源: ``` pip config set global.index-url http://mirrors.aliyun.com/pypi/simple pip config set global.trusted-host mirrors.aliyun.com ``` 7、 安装编译工具: ../esp-gitee-tools/install.sh 8、 设置环境变量并将环境变量放到.bashrc中 ``` source export.sh echo "source ~/esp32/esp-idf/export.sh" >> ~/.bashrc ``` 9、 连接设备,然后将设备挂载到虚拟机上。然后 build 一个测试项目,看一下环境配置的是否成功。 ``` idf.py set-target esp32-s3 idf.py build idf.py flash ```  比如下面的 helloWorld 项目,烧录进设备后,可以通过以下命令查看输出: `idf.py monitor`  #### 2.4 使用 VSCode 远程到当前的 Ubuntu (1)安装 Remote - SSH 插件。 (2)进入远程资源管理器,输入远程虚拟机的地址和用户名、密码连接即可。 (3)在 VSCode 中,选择虚拟机安装 C/C++ 和 ESP-IDF 插件,安装完成后通过 ctrl + Shift + P,选择 ESP-IDF: Add vscode Configuration Folder,这样就可以查看具体的声明和定义了。 (4)因为每次都要用 SSH 登录,默认都是需要输入密码的。所以可以利用证书机制,通过 Windows PowerShell 生成一个证书,例如: `ssh-keygen -t ed25519 -C test@test.com` 在 C 盘的 .ssh 文件夹中找到刚才生成的公钥文件,用记事本打开然后复制里面的内容,到虚拟机中找到 ~/.ssh 文件夹,里面有个 authorized_keys 文件,把刚才复制公钥粘贴进去保存,此时再使用 VSCode 就不需要输入密码了。 #### 2.5 通过 MenuConfig 修改开发板配置 进入项目文件夹,运行 `idf.py menuconfig` 进入配置界面,由于默认主频只有 160 MHz,所以找到 找到 component config,里面有个 ESP System setting,里面有个 CPU 频率的配置选项,这里使用的 ESP32-S3 最高主频是 240 MHz,勾选即可。 重新编译后,重烧固件,发现刚才的配置内容已经生效:  ### 三. FreeRTOS 支持 #### 3.1 任务状态 - 运行态:任务实际执行时,被称为运行状态。任务当前正在使用处理器,如果 RTOS 的处理器只有一个内核,那么在任何给定时间内都只能有一个任务处于运行态。 - 准备就绪态:指那些能够执行,且不再阻塞或挂起状态但目前没有执行任务的状态,往往指因为同等或更高优先级的不同任务正处于运行态。 - 阻塞态:如果当前任务正在等待时间或外部事件,则被认为是阻塞状态。例如一个任务调用 `vTaskDelay()`,它将被阻塞直至延迟结束。任务也可以通过阻塞态来等待队列、信号量、通知等。处于阻塞状态的任务通常有一个超时期,超时后任务将被解除阻塞态。 - 挂起:与阻塞状态下的任务一样,挂起状态下的设备不能被选择进入运行态,但出于挂起状态的设备没有超时概念。相反,任务只有分别通过 `vTaskSuspend()` 和 `xTaskResume()` API 调用明确指令才会进入或退出挂起。 #### 3.2 基础例程 (1) 任务创建 ```c BaseType_t xTaskCreatePinnedToCore( TaskFunction_t pxTaskCode, // 任务函数指针 const char * const pcName, // 任务名称,打印调试用 const uint32_t usStackDepth, // 指定任务堆栈空间大小(字节) void * const pvParameters, // 任务参数 UBaseType_t uxPriority, // 优先级 TaskHandle_t * const pxCreatedTask, // 传回的任务句柄 const BaseType_t xCoreID ) // 内核 ``` (2) 阻塞 ```c // 延时xTicksToDelay 个周期 void xTaskDelay(const TickType_t xTicksToDelay) ``` 基础的演示代码: ```c #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_log.h" void taskA(void *param) { while(1) { ESP_LOGI("main", "RTOS TASK"); vTaskDelay(pdMS_TO_TICKS(500)); } } void app_main(void) { xTaskCreatePinnedToCore( taskA, "RTOS TASK", 2048, NULL, 3, NULL, 1 ); } ``` 烧录进去后,效果如下:  (3)任务间同步 RTOS 中的同步,是指不同任务之间或者任务与外部事件之间的协同工作方式,确保多个并发执行的任务按照预期的顺序或时机进行。它涉及到线程或者任务间的通信和协调,目的是为了避免数据竞争,解决竟态条件,并确保任务的正确行为。 互斥是指某一资源同时只允许一个访问者对其访问,具有唯一性和排他性。 (4)队列 这是一个先入先出结构,常用 API 如下: ```c xQueueCreate( uxQueueLength, uxItemSize ) // 创建队列,成功后返回句柄。参数 1 代表队列容量, 参数 2 代表队列项所占内存大小(字节) ``` ```c xQueueSend( xQueue, pvItemToQueue, xTicksToWait ) // 向队列头部发送一个消息,参数 1 代表 队列句柄, 参数 2 代表消息指针, 参数 3 代表等待时间 ``` ```c xQueueSendToBack( xQueue, pvItemToQueue, xTicksToWait ) // 向队列尾部发送一个消息, 参数 1 代表队列句柄,参数 2 代表要发送的消息指针, 参数 3 代表等待时间 ``` ```c BaseType_t xQueueReceive( QueueHandle_t xQueue, // 从队列接受一条消息,参数 1 为队列句柄 void * const pvBuffer, // 指向接收消息缓冲区的指针 TickType_t xTicksToWait ) // 等待时间 ``` 基础的演示代码: ```c #include #include #include "freertos/FreeRTOS.h" #include "freertos/queue.h" #include "freertos/task.h" #include "esp_log.h" QueueHandle_t queue_handle = NULL; typedef struct { int value; } queue_data_t; // Task func void task_A(void *param) { // 从队列接收数据并打印 queue_data_t data; while(1) { if (pdTRUE == xQueueReceive(queue_handle, &data, 100)) { ESP_LOGI("Queue", "Queue receive data : %d ", data.value); } } } void task_B(void *param){ // 间隔 1S 向队列发送数据 queue_data_t data; memset(&data, 0, sizeof(queue_data_t)); while (1) { xQueueSend(queue_handle, &data, 100); vTaskDelay(pdMS_TO_TICKS(1000)); data.value ++; } } void app_main(void) { queue_handle = xQueueCreate(10, sizeof(queue_data_t)); xTaskCreatePinnedToCore(task_A, "taskA", 2048, NULL, 3, NULL, 1); xTaskCreatePinnedToCore(task_B, "taskB", 2048, NULL, 3, NULL, 1); } ``` 编译烧录后的效果:  (5) 信号量 常用的信号量包括二值信号量、计数信号量以及互斥锁,常用的 API 如下: ```c xSemaphoreCreateBinary(void) // 创建二值信号量,成功则返回信号量句柄 xSemaphoreCreateCounting(UbaseType_t uxMaxCount, // 最大信号量数 UbaseType_t uxInitialCount) // 初始信号量数 ``` ```c xSemaphoreTake(xSemaphore, xBlockTime) // 获取一个信号量,如果获取则返回 pdTRUE xSemaphoreGive(SemaphoreHandle_t xSemaphore) // 释放一个信号量 void vSemaphoreDelete(SemaphoreHandle_t xSemaphore) // 删除一个信号量 ``` ```c // 创建互斥锁 SemaphoreHandle_t xSemaphoreCreateMutex(void) ``` 基础的演示代码: ```c #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/semphr.h" #include "driver/gpio.h" #include "esp_log.h" SemaphoreHandle_t bin_sem; void task_A(void* param) { // 释放信号量 while (1) { xSemaphoreGive(bin_sem); vTaskDelay(pdMS_TO_TICKS(1000)); } } void task_B(void* param) { // 等待信号量成功后打印 while(1) { if (pdTRUE == xSemaphoreTake(bin_sem, portMAX_DELAY)); ESP_LOGI("Semphr", "Get single success"); } } void app_main(void) { bin_sem = xSemaphoreCreateBinary(); xTaskCreatePinnedToCore(task_A, "task_A", 2048, NULL, 3, NULL, 1); xTaskCreatePinnedToCore(task_B, "task_B", 2048, NULL, 4, NULL, 1); } ``` 编译烧录后的效果:  本文由 Alen 创作,采用 知识共享署名4.0 国际许可协议进行许可本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名最后编辑时间为: Sep 18, 2024 at 05:55 pm