ESP32使用CLion+IDF无痛起步指南

Espressif,乐鑫,是近年来崛起的优秀国产MCU品牌,它们的产品线比较狭窄,只包括MCU级别的产品,但是不得不说他们在嵌入式AI、嵌入式IoT以及无线技术方面确实给开发者提供了许许多多的的便利。ESP32早年从ESP8266起家的时候首先支持的Arduino的开发框架,然而在较深入和较复杂的开发之中这个框架的灵活度、易用性、可扩展性都展示出了许许多多的不足之处,所以笔者决定抛弃ESP-Arduino转而使用ESP-IDF进行开发,本文旨在探索一种合适笔者的开发环境。

1. ESP产品线与开发框架介绍

当我们讨论某个SoC或者MCU产品系列的开发环境,我们首先需要明确这个产品系列究竟有什么细分,这些细分是否影响开发环境和支持库的配置,这些配置是否易于移植?截止到笔者写这篇文章的时候ESP系列的产品大致可以这样进行两个层次的分类:

  • 基于内核和实现功能不同的ESP芯片系列分类:
    • ESP8266:这是乐鑫最早的也是最基础的系列,使用Xtensa LX106架构,仍然是32-bit的MCU系列,并不是很多人认为的AVR或者其他什么框架。这个系列的MCU是不带有BLE的,只包括2.4G的WiFi无线功能,天线输出功率也比较克制。外设方面只支持简单的GPIO, PWM, ADC, SPI, I2C, I2S, UART这些最基础的功能。
    • ESP32:这个系列是ESP-8266的升级,也是所有ESP-32之中的基础款,俗称水桶系列。这个系列的MCU任然延续Xtensa架构,只不过除了ESP32-S0WD之外全部升级为双核处理器,这成为ESP系列产品的默认配置,多核调动也是ESP32的特色。在无线功能上增加了Bluetooth/BLE的支持,时钟频率也提升到了最高240MHz。在这个系列之中ESP特色的触摸传感器接口被添加了进来。除此之外还支持了高速SPI,霍尔传感器,SDIO接口,Ethernet这些略微高级一点的外设。
    • ESP32-S2:在ESP32的基础上再次升级,笔者认为这个系列是在传统的嵌入式开发评判标准之中具备最全功能的选择。首先引入了USB-OTG,完善的AD,DA接口,LCD接口,摄像头接口。其次ESP在这款芯片上开始发力IoT和AI赛道,添加了强大的安全机制,例如eFuse和Secure Boot,特别是在加密算法和散列码计算上,支持了SHA、RSA、AES这些在网络传输和AI应用之中非常常见但是MCU不容易实现的功能。同样的继续精简了产品线,整个ESP32-S系列上每个子系列只有两种芯片,具体的型号说明可以到官网查找手册。
    • ESP32-S3:对比ESP32-S2不能说是全面升级,只能是说在某些特化接口上做了取舍,例如增加了内置的SRAM和ROM,增加了SPI(各种高速SPI)的接口数量,将常用的红外管控制、灯条控制这些功能封装为RMT,增加了他TWAI车载控制总线(其实就是CAN)。这个系列之中ESP真正发力AIoT赛道,添加了图像AI识别、语音AI识别这些”不是很底层嵌入式“的功能。总体来说假设你需要开发智能家居、物联网MESH、基于AI的一套硬件框架,这个系列是最好的。
    • ESP32-C:包括ESP32-C,ESP32-C3,ESP32-C6,这几个产品线是ESP家的新锐产品,功能上看起来没有ESP32-S系列强大,又没有ESP32基础——但是迁移到了RISC-V框架呀!可能是迁移到这个框架之后ESP并没有能够立即做出合适的方案,所以这些系列目前还都是单核。C系列主打就是一个轻便便宜的轻量化应用,主要面向通信强化、低功耗强化、安全极值强化方面进行优化。
    • ESP32-H:最新的产品系列,笔者不怎么了解……
  • 在芯片这个层面上,ESP官方还推出了另外两个分类的产品:模组和开发板。后者大家都好理解,在学习一套硬件的使用方式和做可行性验证的时候拿来折腾是经常有的,重点在于模组这个分类。众所周知,各国对于射频信号的功率、噪声等等都有一套严格的无线电认证标准,而且MCU的正常启动工作需要一些外部辅助电路的支持——这些事情如果你使用模组级别的产品你可以完全忽略!所谓的模组我们可以认为是集成了一些外部电路、射频天线的PCBA子系统,可以直接焊接到你的PCB板材上工作而不需要考虑额外的硬件配置问题。

什么是ESP-IDF?IDF也就是物联网开发框架(IoT Developing Framework)。在对于ESP的硬件系列有了一个大致了解之后,我们就需要思考这样的一个问题:在MCU级别的嵌入式设备开发之中整个开发流程是什么样的,什么才算的上是一个开发框架呢?笔者以为所谓的开发框架就是一套能够应对下图展示的开发流程的工具链和环境:

由于ESP产品至今为止没有SoC级别的成熟方案,并且很少涉及超过100MHz高速信号的处理(其实笔者对于ESP32的WiFi功能尤为喜欢,但是说真的如果要涉及到上下速率超过50Mbps的通信笔者恐怕会选择直接封装一个子系统,在其中使用现有的无线网卡模块之类的),所以在电子EDA设计领域使用立创EDA是足够的,并且ESP32系列产品在立创商城和立创EDA之中都有售卖和封装已经画好能够节省我们很多进行封装的时间。如果读者们有处理高速信号的需求,最推荐的还是OrCAD或者Cadence Allegro这种较为专业一点的EDA设计软件比较好。

这里笔者想要吐槽也是想给ESP提提建议的就是,反观STM32系列芯片,在海内外都有广大的包括笔者在内的忠实用户,并且随着正点原子的崛起STM32成为了很多人学习ARM架构和入门嵌入式开发的首选——事实上STM32的产品对比国内一些同类型或者干脆说山寨抄袭产品主频不高、Flash和RAM刀法精准、原厂一手芯片价格昂贵……但是架不住STM32性能稳定而且真的有一套极好的生态和开发工具。ESP的黄色部分常常让习惯了ST官网选型工具和STM32CubeMX的笔者感到深深的痛苦!

所以我们事实上需要配置的”舒适开发环境“也就是红色部分加上蓝色部分,ESP官方首推的是VSCode+插件支持IDF的形式,但是我!不!喜!欢!VSCode!熟悉笔者的朋友们都知道笔者是一个JetBrains的骨灰级用户和忠诚Fellow,因此本文所要实现的就是CLion+IDF的开发环境的实现。不过首先还是回到正题:让我们来理解一下IDF究竟在干什么。

2. IDF生态与ESP32启动方式

笔者可以预想到,很多像笔者一样多年沉浸在ARM舒适区里面的开发者在看到RISC-V和Xtensa的时候就已经萌生退意:好家伙我可能就是要一个WiFi模块我还得学两个新框架?这还不如我直接买个模块呢!更何况连接WiFi和蓝牙并不是我们的最终目的,所有的互联网应用一定要在应用层落地实现功能才可以,例如常见的HTTP协议,WebSocket,MQTT协议等等。所幸,IDF就是ESP推出的这样一种框架,包装和抽象程度极高,生态完整。

2.1 ESP32硬件框架理解

首先,流浪地球名梗:没有硬件狗的软件工程师屁也不是!我们需要先了解一下我们要用到的ESP32芯片的工作形式和外围电路支持,就以最基础的ESP32系列为例子,笔者手头上的一块开发板用的MCU型号是ESP32-D0WDQ6。其中D代表搭载双核处理器,0代表没有片内Flash,WD代表同时支持2.4GWiFi和BLE无线功能,Q6代表使用6*6的QFN封装,没有R[X]项代表没有片内PSRAM。

使用过STM32的朋友们可能立即要跳脚了:没有片内Flash你玩个锤子!QFN6*6封装的芯片难不成还能给你整个FSMC接口让你拓展Flash么!没有片内PSRAM又是个什么意思??笔者当年第一次接触到ESP32的时候也是这样的迷茫之中带着期待,期待之中带着迷茫——让我们一起来见证一下国产厂商ESP的邪术,请看笔者自制的一张伪架构图,描述了ESP32芯片与相关外设的交互和组织形式:

ESP32的组织形式和以STM32为代表的MCU十分不同,首先在内核部分有可能含有两个CPU,在后面的软件架构之中我们会详细讲这两个CPU是如何启动如何工作的。这两个CPU通过系统总线连接到片内存储,所谓的片内存储只包括两部分,第一个是SRAM,作为系统运行时的内容接入,这个有嵌入式开发经验的朋友都熟悉。另一个是位于掩膜上面的ROM,这是一个真正的只读空间,这里面的代码是ESP32芯片出场时就写入的,包括了Bootloader 1(至于为什么是Bootloader”1″)我们之后在软件部分细谈。这个Bootloader决定了如何装载代码、引导程序启动或者进行程序烧录工作。

那么肯定有朋友注意到了我提到了”装载代码“,听起来十分像是PC机启动时主板的BIOS从硬盘中加载操作系统数据到内存之中的过程——是的!ESP32芯片需要执行的代码根本不存储在片内,它存储在灰色的SPI总线所连接的存储设备之中。这条SPI总线不能够挪作他用,并且即使芯片封装自带Flash和PSRAM也不能将这条SPI对应的针脚挪作他用——本质上ESP只是将存储电路封装在了芯片内部,而没有放到真正的”片内存储”之中,也就是说即使封装内部自带FLASH和PSRAM,这条总线也被封装内的存储设备使用——你只能够在这个基础上继续拓展存储空间而不能改变总线本身。

系统启动时,无论如何Bootloader 1都会首先最小化初始化芯片总线,并且将SPI-FLASH和PSRAM映射到统一的内存地址之中便于内核工作。此时根据RCC模块之中的STRAPPING引脚上面的电平的状态——这也对应了相关寄存器的状态,Bootloader就可以决定是直接加载Flash运行代码还是烧录Flash或者进入JTAG调试模式。RCC外接电路主要包括三部分:

  • ESP32需要外接一个晶振用作系统时钟树的时钟源,当然了在不同的工作模式下可能用到不同的时钟源,例如低功耗模式下就用到RTC时钟源,但是一般而言这个位置需要接入一个晶振。可以有源也可以无源,这部分和其他MCU例如STM32的外围电路设计相同。
  • ESP32的CHIP-PU引脚相当于RESET引脚,这个引脚电平拉高(Vcc)的时候认为芯片使能,进入复位中断处理流程,当这个引脚上的电平跌落到-0.25~0.3\(\times\)Vcc的时候芯片不使能,待电平回升之后重新使能进入复位流程。ESP32的硬件设计手册明确指出了这个引脚上电平上升的时间点要延后于芯片上电,所以一般我们在这个引脚和地之间接一个电容。
  • STRAPPING引脚在ESP32的手册中会详细标注出来,这些引脚对应STM32之中的BOOT引脚,这些引脚的电平决定了对应寄存器的值,从而控制了Bootloader的行为——究竟是运行现有的Flash之中的程序还是执行从串口下载新程序的代码?

而ESP32另一个“保留使用”的部分就是UART0的TX、RX引脚以及JTAG调试接口。前者用来下载程序,输出默认调试信息——这往往需要在电路之中使用一个串口桥接芯片实现能用的功能,后者用来进行内核调试,这也是我十分想要吐槽的一点,ESP32到现在居然还不支持SWD调试!

2.2 ESP-IDF软件框架理解

相信有过开发经验的朋友在理解了上述硬件架构之后已经开始有些明白ESP32在干什么了——只要好好利用这个Bootloader就可以了!就像是ARM架构的STM32将程序存储在地址0x08000000的FLASH之中,随后在复位中断到来时照着0x08000004的中断向量表开始按图索骥的执行代码……中断……寻址……PC指针……只要通过高级语言编译器生成可执行的汇编代码写入到这个CPU的对应位置就好了!然而ESP-IDF事实上在这个基础上更进一步:

如我们所见,ESP-IDF进行了一个套娃的操作:既然芯片的Bootloader指定从0x1000的外设启动程序,那么将程序和Flash再次划分不就好了么?根据不同的复位中断来源(可能是硬件重启、软件重启或者看门狗重启这些选择)进入不同的启动流程,而图中描述的是最为复杂而且全面的硬件复位流程。首先一级引导程序将会开启时钟,使能基础的中断,开启SPI总线和UART0总线以备挂载外部存储或者从串口烧录程序。其次将验证STRAPPING引脚电平或者说寄存器的值,这样就能够决定是烧录程序还是启动代码。

进入Flash之中写的程序之后第一段就是二级引导程序,这部分引导程序主要进行:读取分区表和加载MMU以及进行OTA升级。首先说Flash MMU,也就是Flash Memory Manage Unit,很多朋友看到MMU就会说啊这不是SoC级别芯片封装才有的东西么,然而这个MMU只是用来管理Flash的。这个组件负责管理和映射外部SPI Flash存储到内部的数据地址总线上,主要作用就是使得CPU能够直接从外部Flash执行代码,也就是所谓的原地执行XIP技术,一般而言无论是什么情况一旦MMU加载完毕ESP32就将立即执行目标代码,进入OTA或者Factory APP流程。

上面我们提到Factory APP和OTA,这二者其实并无本质区别,只是ESP做的一种区分而已,代表着不同的代码和不同的运行逻辑,在此之前我们需要先明确分区表这一概念。分区表指示了SPI-Flash之中应当怎样划分空间给不同的数据或者代码,并且说明了每个部分的起始地址和长度。分区表在SPI-Flash之中以二进制形式存储,但是我们仍然可以事先写出文字版本并且使用IDF工具转换为二进制,例如:

# ESP-IDF Partition Table
# Name,   Type, SubType, Offset,  Size,   Flags
nvs,      data, nvs,     0x9000,  0x6000,
phy_init, data, phy,     0xf000,  0x1000,
factory,  app,  factory, 0x10000, 1M,

上面的分区表是一个经典的不使用OTA的IDF构建的ESP32代码的Flash分区表,一条分区记录含有这样几个字段:Name字段表示分区的名称,这个字段只是方面人类理解和区分,在代码中只是一个标记,这个字段不得超过15字节,事实上是16字节,但是最后一个字节要用于字符串的\0字符;Type字段代表了这个分区存储的究竟是什么数据,目前0x00代表data也就是数据分区,0x01代表app也就是说分区之中存储的是可执行代码,0x00~0x3F官方可能推出其他的功能,所以不建议使用,如果要建立自己的分区类型可以从0x40~0xFE之中进行选择;SubType是大的分类下的二级分类,其中app可以选择factory或者test,前者就是默认的程序分区,后者是测试分区,当然如果有OTA信息将不会启动factory分区而是按照OTA策略进行选择。data之下的子分类较多,nvs代表Non-Volatile Storage也就是存储一些掉电不丢失的数据、phy代表配置射频电路等外设的物理参数、ota代表OTA更新信息分区,本文对于OTA功能不做详细展开,最后是nvs_key代表了存放加密密钥的分区;Offset代表起始地址,Size代表以Byte为单位的大小;最后是Flags,这个字段目前仅仅支持encrypted这个标志,代表着分区需要遵循secure boot原则进行安全加密。

那么上述分区表就十分好理解了,对于包含OTA功能的分区表,经典的形式是两个OTA App互相更新支持回滚,还有factory app作为ESP32运行的最后保障,通过ota data分区指定更新策略和流程,以下是包含OTA功能的一个分区表实例:

# ESP-IDF Partition Table
# Name,   Type, SubType, Offset,  Size, Flags
nvs,      data, nvs,     0x9000,  0x4000,
otadata,  data, ota,     0xd000,  0x2000,
phy_init, data, phy,     0xf000,  0x1000,
factory,  app,  factory, 0x10000,  1M,
ota_0,    app,  ota_0,   0x110000, 1M,
ota_1,    app,  ota_1,   0x210000, 1M,

等待MMU启动之后,二级引导程序就会加载Flash之中的代码开始执行,代码执行时会首先进行多核管理调度(假设使用的芯片是双核),而后分配好FreeRTOS的堆栈,最后启动FreeRTOS核心和调度器,最后启动FreeRTOS主线程的主任务的主函数app_main()。

这里需要明确说的是IDF框架不存在裸机编程这个说法,它的所有代码和动作都必须置于FreeRTOS的框架之中,从好的方面来说这避免了我们重新学习底层掌握寄存器的麻烦,并且让ESP-IDF构建的代码具有了极高的可移植性——你不需要手动进行管理,构建框架会自动选取合适于芯片的代码库进行编译,甚至有些芯片可以一次编译多次使用,这对于STM32这种较为传统的MCU实现起来确实需要一番功夫;而缺点就是对于不熟悉FreeRTOS但是对于底层原理掌握熟练的工程师来说非常痛苦,并且这不利于我们从寄存器和芯片指令的角度去理解嵌入式程序的实现过程!真心期待ESP32可以拥有一套更加底层和明晰的HAL或者LL库。

2.3 ESP-IDF框架生态与组件

首先,ESP-IDF框架是一个完全开源的框架,它在GitHub和Gitee上都有仓库,如果仅仅查看他们的代码仓库似乎与别的代码库没有区别,但实际上IDF有一个笔者十分推崇的概念:高内聚低耦合实现的非常好,通过组件(Component)的方式我们可以进行模块化的开发,下图展示了ESP-IDF的生态:

  • FreeRTOS:此组件是ESP-IDF的核心,ESP对FreeRTOS的底层进行了内核调度上的优化,更加适配ESP32而且能够很好的支持双核系统,对于官网宣传的多核,在他们没有更多核心的产品出现之前笔者不做评价,并且对于双核调度有时候比单核性能还差的表现有所质疑。
  • 编程接口标准化:虽然ESP-IDF大部分依赖于C语言实现,但是它支持C/C++混合编程,POSIX API和BSD Socket这种标准化的接口,这使得很多时候移植代码到ESP32平台更加容易。
  • 针对它们自己芯片上搭载的外设像是UART、SPI、IIC、ADC、DAC等,虽然没有成熟的LL或者HAL级别的代码库或者SDK,但是在ESP-IDF之中都将硬件电路抽象封装成代码实现,算是某种意义上的HAL级别的硬件接口,如果需要深入了解学习,还是避不开寄存器编程的。
  • 无线功能:这一开始并且直到现在也是ESP32的核心竞争力——ESP-IDF一如既往的像是ESP-Arduino一样实现了2.4G WiFi、传统蓝牙、BLE这些涉及到射频、信号处理等等诸多麻烦问题的简单接口,对此功能笔者赞不绝口,简单而不简陋。
  • 网络协议栈:这是ESP-IDF最值得用的功能,它完整的实现了LwTCP/IP、DHCP、mDNS、HTTP包括2代HTTP、MQTT、WebSocket、SNTP、SMTP、TLS这些协议栈的全面移植,虽然有些地方逻辑成谜存在一些小小的BUG,但是整体来说瑕不掩瑜属于最棒的功能之一。
  • 低功耗:ESP-IDF对于ESP32芯片本身支持的睡眠和深度睡眠模式有极好的支持,果然Arduino还是外人终归是自家人的平台支持好。这在一些IoT领域的开发之中不可或缺,你很难想象一个智能插头或者智能灯泡的控制器需要消耗W级别的功率不是么?
  • 安全管理:eFuse功能的移植实现了secure boot这一在传统硬件上非常少见的功能(话说ST这两年也有在强推它们的安全强化版本的芯片),并且支持AES、RSA、SHA等等散列码校验大大减少了我们在开发过程中对这些算法无用但是必要的移植。
  • 构建工具:使用GCC交叉编译器,CMake构建工程,这一举措使得ESP-IDF开发框架超越了Arduino这种一家之言级别的入门级SDK,成为了通用化体系化的工具,而模块化的组件理念和CMake搭配起来更加行之有效——大大的提升了开发速度、开发标准度和代码复用率。
  • 烧录与调试:使用OpenOCD作为烧录工具,支持JTAG调试,这属于基本功,但是本着支持国产的角度还是夸一下打个80分——什么时候支持SWD调试就打90分。
  • IDE支持:对于使用Eclipse和VSCode的朋友来说的确不错,但是作为JetBrains骨灰级用户的笔者对于没有官方开发的JetBrains插件深感不满!虽然通过OpenOCD和CMake已经能够成熟对接CLion开发,但是在CLion这两年朝着嵌入式开发方向深入优化的背景下这样做应该更好吧?

3. ESP-IDF+CLion开发环境搭建

搭建ESP-IDF+CLion的开发环境思路是,使用CLion编写代码,代入ESP-IDF的库支持。编写完成后使用CMake进行构建,其中CMakeList.txt文件可以直接导入CLion变成可运行步骤,最后使用OpenOCD烧录和调试,而ESP-IDF自带OpenOCD并且很多时候我们都是通过串口+Bootloader的方式下载代码。这里讨论三种安装的情况。

3.1 Windows下使用散装方式安装

看起来这种方式不会有下一小节之中介绍的安装包直接安装的方式好,但是笔者比较喜欢亲历亲为的做所有的动作,尤其是在构建开发环境的时候。所以和笔者一样具有对自动安装程序的未知性强烈不安全感的朋友们最好也选择这种方式。下面是一些准备工作,如果您已经安装了下列工具可以直接跳过。

  • 安装Python,要求版本必须高于3.6,笔者虚拟机测试安装流程使用的是3.11.8,注意不要使用最新的版本3.12.x,这会导致menuconfig打不开!可以直接访问官网下载页面地址。加载和下载速度都不算很高,记得安装后要将Python的环境变量添加到系统路径之中。在这里安装的时候最好选择管理员模式,在安装结束的时候有一个Disable Path Length Limit选项最好勾选,否则后面就要特意用命令行改掉,这是为了防止过长的目录导致NTFS不正常工作
  • 安装Git,散装方式需要使用git clone获取代码,笔者测试安装使用的是2.43.0,没有详细的Git版本要求,在官网下载地址选择Standalone即可,同样也要添加到系统路径,这里如果Git安装程序没有自动添加到系统路径则需要手动添加。
  • 安装CLion,直接从JetBrains CLion的官网下载即可,下载就有30天试用期。有学信网认证的朋友可以申请免费的白嫖License,如果手头特别紧可以考虑破解,但是请记住破解永远是不正确的行为

现在选一个你喜欢的位置使用Git拉取你想要的ESP-IDF版本,如果你使用的ESP8266或者更早的ESP系列产品那么很遗憾你不能够使用ESP-IDF进行开发,你只能使用ESP-RTOS-SDK进行开发,ESP-IDF是没有对这些早期芯片进行兼容性开发的,下图展示了ESP-IDF版本支持:

虽然笔者目前的主力开发环境是master的尝鲜版本,但是master分支上经常有很多莫名其妙的BUG,在落笔的当下最新的版本是5.2,最新的稳定版本是5.1.2,所以本次使用5.1.2版本进行安装:

#执行命令所处目录: C:/espressif/idf
git clone -b v5.1.2 https://github.com/espressif/esp-idf.git --recursive
#如果使用国内的Gitee仓库克隆
git clone -b v5.1.2 https://gitee.com/EspressifSystems/esp-idf.git --recursive
#下载的目录名称默认叫做esp-idf,我这里改成版本号
mv ./esp-idf ./v5.1.2

在上面执行克隆代码的时候建议加上–recursive选项,这表示将递归的克隆所有附属的仓库,也就是安装所有官方的组件——如果你不这样做的话确实下载数据量将会小很多,但是当你用到某个组件的时候你可能需要用IDF的组件管理器手动注入。如果GitHub的速度太慢,可以考虑使用ESP-IDF在国内Gitee的仓库也是可以的,两个仓库是镜像的关系。

Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled" -Value 1 -Type DWord

如果你之前安装的Python的时候没有勾选解除长目录字符串限制,那么你就需要在命令行之中(PowerShell)以管理员权限执行上述语句修改系统注册表添加对于长目录字符串的支持,否则当你所使用的工具和组件包含层次过深文件系统会报错。接下来进入克隆好的仓库目录,并且执行工具链安装脚本,这里面包括交叉编译器、面向ESP32-JTAG功能魔改过的OpenOCD等:

#工具包的默认安装目录是用户目录下的隐藏文件夹.espressif,如果你想改变则需要定义环境变量IDF_TOOLS_PATH
#不同于下载地址的改变,这种改变需要作用于整个开发途中,因此可以使用设置>更改系统环境变量的方法也可以使用命令行
[System.Environment]::SetEnvironmentVariable('IDF_TOOLS_PATH', 'C:\espressif\idf', [System.EnvironmentVariableTarget]::Machine)
#我这里将安装路径设置为C:\espressif\idf,那么就会安装在tools目录下,生成dist目录和env.json目录
#执行此命令后需要重开PowerShell否则在当前PowerShell之中不会生效

cd ./v5.1.2
#接触对于PowerShell脚本不执行的严格限制
Set-ExecutionPolicy Unrestricted

#执行安装脚本,可以选择需要开发的芯片系列
#例如只想开发s3 ./install.ps1 esp32s3
#例如指向开发s3和c2 ./install.ps1 esp32s3 esp32c2

#如果从GitHub官方地址下载较慢,可以考虑使用ESP的私有服务器下载,需要使用环境变量
$env:IDF_GITHUB_ASSETS="dl.espressif.com/github_assets"

#我这里安装所有芯片的开发工具
./install.ps1 all
#在这个过程之中Python和Pip可能因为网络问题报错,重复尝试或可解决

#不要忘记将PowerShell的严格限制加回来,保证系统安全
Set-ExecutionPolicy Restricted

在上述安装个过程之中,除了最后的Pip安装的Python包,主要包含了idf.py运行时需要的各种监视器、下载器等等支持性程序,还有一个非常漫长的下载和解压过程。我们不妨打开tools文件夹查看一下都安装了些什么工具,如下图所示。

以xtensa和riscv开头的是各个系列芯片的交叉编译器和调试工具,ninja和cmake用于构建IDF框架下的APP程序,ccache用于加快编译速度,idf-exe是idf.py封装成的exe文件。除了这些编译工具之外,dfu-utils是用于使用USB方式以DFU文件更新固件的驱动包,openocd-esp32是ESP魔改的OpenOCD作为下载和调试工具使用。esp32ulp是针对超低功耗(Ultra Low Power)协处理器的工具链,而esp-rom-elfs是一组预编译的函数库,用于提供对ESP32芯片掩膜ROM之中代码的访问接口,包括基本的系统功能,数学运算,硬件操作等等。到这里安装工作就已经完成了,接下来在正式开始联合CLion一起工作之前,除了其他安装方式,我们还需要了解一点IDF之中的常规工作流程。

3.2 其他平台和方法的安装

首先就是比较喜欢自动化和图形化操作的朋友们可以直接使用ESP打包好的IDF安装器进行安装,访问安装器的下载网址可以选择不同的安装器类型。如图所示,除了第一个是一个在线安装器(所有的资源通过安装器访问网络获取),其他的都是携带了所有安装所需的文件资源的版本,如果你的网络状况不是很好建议选择已经封装好的离线安装器:

下载好安装器之后,建议以管理员权限执行,上文提到的所有配置在其中都有图形化接口,只不过需要我们细心寻找相关配置项。这种安装方式虽然方便快捷,但是总是叫人一头雾水,所幸其中的Git、Python等等工具都是集成化安装的。具体安装过程之中注意的配置如下所示:

从一开始的选择语言-系统检测-同意协议-选择安装位置基本都是无脑下一步,图一的第二次选择位置事实上就是指定IDF Tools的安装位置,这个文件夹不能够是IDF源代码本身的文件夹。图二的红色框出的选项的Framework指的是IDF开发所需要的IDF源代码本身,Git、Python等内建工具都会安装在Tools之中,蓝色部分建议不要勾选,当我们使用CLion开发的时候这些选项不会增加我们的便捷性,而卸载的时候又不会自动去除。图三之中的红色部分我们之前没有提到,后文会讲到,这主要是监视/调试过程需要的驱动,蓝色部分就是让我们选择我们想要开发的芯片都有什么呢,绿色部分对应了是否使用Gitee代替GitHub,是否使用ESP的镜像源,是否只下载单分支也就是不进行–recursive去下载所有组件的源代码。如果使用离线版本的安装器,可能选项略有不同,总体来说选项变得比在线版本更少更简单了。例如没有选版本的步骤、Tools和IDF源代码是直接指定在一个目录之中安装的、不必关心从哪个服务器哪个来源下载的问题等等。

MacOS和Linux的安装没有官方提供的下载器,必须像是3.1之中介绍的一样使用命令行进行散装式手动安装,Linux的兼容度和操作难度比较低,MacOS容易报错的地方比较多并且很多问题都十分诡异,第一步是准备安装,也就是装载支持库和安装过程用到的工具的部分:

#安装支持时所有类型都要安装Git和Python(3.6+),相关教程网络上流传甚广,不赘述

#Ubuntu/Debian
sudo apt-get install git wget flex bison gperf python3 python3-pip python3-venv cmake ninja-build ccache libffi-dev libssl-dev dfu-util libusb-1.0-0

#CentOS 7/8
sudo yum -y update && sudo yum install git wget flex bison gperf python3 python3-setuptools cmake ninja-build ccache dfu-util libusbx

#Arch-Linux
sudo pacman -S --needed gcc git make flex bison gperf python cmake ninja ccache dfu-util libusb

#MacOS用户需要首先安装HomeBrew或者MacPorts这两个包管理器其中的一种
#HomBrew用户
brew install cmake ninja dfu-util ccache
#MacPorts用户
sudo port install cmake ninja dfu-util ccache

#如果安装报错信息为或者类似于:
#xcrun: error: invalid active developer path (/Library/Developer/CommandLineTools), missing xcrun at: /Library/Developer/CommandLineTools/usr/bin/xcrun
#需要首先安装XCode命令行工具
xcode-select --install 

#如果您的Mac是M系列的ARM架构并且报错:
#WARNING: directory for tool xtensa-esp32-elf version esp-2021r2-patch3-8.4.0 is present, but tool was not found
#ERROR: tool xtensa-esp32-elf has no installed versions. Please run 'install.sh' to install it.
#zsh: bad CPU type in executable: ~/.espressif/tools/xtensa-esp32-elf/esp-2021r2-patch3-8.4.0/xtensa-esp32-elf/bin/xtensa-esp32-elf-gcc
#那么需要首先安装Apple Rosetta 2解决兼容性问题
/usr/sbin/softwareupdate --install-rosetta --agree-to-license

后续的步骤大致类似Windows下散装安装的操作,首先创建一个目录用于存放IDF和IDF Tools,其次拉取源代码,最后安装相关的工具链即可,相关命令大同小异,甚至说基本一样,给出如下一个命令执行步骤:

mkdir -p /usr/local/espressif/idf
cd /usr/local/espressif/idf

git clone -b v5.1.2 --recursive https://github.com/espressif/esp-idf.git --recursive
#使用gitee:git clone -b v5.1.2 https://gitee.com/EspressifSystems/esp-idf.git --recursive
mv ./esp-idf ./v5.1.2

cd ./v5.1.2
#默认Tools目录为 ~/.espressif
echo 'export IDF_TOOLS_PATH="/usr/local/espressif/idf/"' >> /etc/profile
source /etc/profile

#如果设置镜像下载源:
export IDF_GITHUB_ASSETS="dl.espressif.com/github_assets"

#可以设置其他你想要的开发芯片
sh ./install.sh all

#如果MacOS用户报错:
#<urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:xxx)
#这是因为没有安装证书
#找到Python目录下的 Install Certficates.command运行安装证书即可

3.3 ESP-IDF命令行使用——理解工具

在完成了ESP-IDF的下载和安装之后,似乎下一步就应该是对接CLion开始快乐的Coding时间,但是笔者始终坚持一点:任何开箱即用的产品面向的都是用户而不是开发者,如果你想在某个领域称为合格的开发者你就一定要理解你所使用的工具和环境究竟如何工作,本文笔者使用的样例开发板是合宙之前9.9搞活动推出的ESP32C3开发板,具体资料可以查看他们的官网说明,首先我们来分析一下原理图:

首先来看芯片部分,像是C1,C3,C4这些都是芯片的滤波电容,有些芯片还有CAP引脚,这表示这个引脚上需要接入电容提供给芯片内部的滤波电路或者电荷泵等等功能使用,黄色方框之中笔者认为是一个文档失误,这个位置应该接了两个滤波电容,但是这与Flash相关,这里不画的原因后续说明。绿色方框中是一个$latest \pi$型滤波器,用于对PCB天线进行滤波,通常来说只要你Layout的时候的EMC控制好,而且ESP32的PHY参数配置好是不用到这个滤波器的,因此我们发现这两个电容默认都是不接的,电阻为\(0\Omega\)用作跳线。蓝色方框连接到了CHIP_PU引脚也就是复位引脚,由于默认高电平使能所以使用R3上拉,电容既提供了稳定电压的功能,防止电平变化时反复触发复位,又提供了上电后CHIP_PU上的电压需要晚于电源上电的功能。红色部分之中是一个经典的无源晶振电路,用于ESP32-C3的主时钟。

上图是UART0转USB通信的串口桥,使用TypeC母座和CH34x的串口桥。这里需要说明的是,在一体化安装器安装之中ESP-IDF安装器自动安装了FDTI,CP2102,CH340三个驱动,然而这并不是万能的,假设你用的开发板使用了其他厂商生产的串口桥那么仍然需要安装对应的驱动才行,这里合宙用的是CH343,那么安装CH340的驱动即可正常工作。红色部分是通信线,蓝色部分是用于烧录固件的UART流控线,注意此款串口桥在内部已经集成了USB差分线的上拉电阻和串联阻抗匹配电阻,绿色方框是用来防ESD的TVS管,右侧的两个电阻都是\(0\Omega\),起到也就是一个跳线的作用。黄色部分两个5.1K的下拉电阻接在CC线上保证了这个TypeC端口连接到电脑时会被识别为一个从机设备。

关于FLASH芯片没有注明具体是什么型号,但是根据笔者的经验和官网的说明“1路SPI FLASH,板载4MB,支持最高 16MB”,笔者认为这是一个经典的W25Q SPI-Flash。但是此处的原理图设计并不是按照ESP官方给出的参考,这里有几点需要注意说明:

  • IO2和IO3实际上一个引脚是WP(Write Protection)写保护引脚,一个是HD(Hold)保持引脚,这两个引脚是低电平有效,这里合宙可能是认为用不到这两个功能所以直接上拉默认不用了,如果需要使用的话应当接入SPI_WP和SPI_HD这两条线。
  • 笔者手上的板子丝印表明了ESP32-C3,这款芯片内部不带Flash所以可以也需要外接Flash,如果使用内部带有Flash的芯片那么不可以外接Flash,只可以外接一个PSRAM。
  • Flash的供电本身应该通过GPIO11所在的VDD_SPI接入,在使用该引脚供电的前提下确实应当如前文提到的接入两个滤波电容,但是合宙这里直接使用了3V3对Flash进行供电而不是使用该引脚,那么也就不需要接入那两个滤波电容,同样的这个引脚也可以解放出来作为GPIO11使用。如果想要使用这个引脚参见合宙官网说明,这是一次性操作。

上图是BOOT自动下载电路,这也是IDF通过串口烧录的重要方式。C3芯片的STRAPPING引脚有好几个,但是真正影响Bootloader 1选择是运行程序还是烧录SPI的引脚就是GPIO9,通过流控信号DTR和RTS就能够实现自动进入烧录模式和自动重新运行Flash之中的代码:

  • DTR和RTS都处于低电平,此时两个三极管都是截止的,因此两个信号由于各自上拉电阻的存在(BOOT的上拉电阻在下文中提到)都是高电平。
  • DTR保持低电平而RTS拉高时Q2导通,也就是BOOT将会被拉到低电平
  • RTS保持低电平而DTR拉高时Q1导通,也就是RESET将会被拉到低电平
  • 二者同时保持高电平时,虽然两个三极管都导通,但是由于二者本身是高电平,所以BOOT和RESET两个引脚同样也是高电平的状态。

那么具体是如何通过这几个逻辑映射实现了选择进入Bootloader的呢?我们不难想象重启的流程:将RESET拉低,保持一段时间之后再拉高,芯片将会收到硬重启复位中断信号,而进入Bootloader就是需要再设备重启时保证BOOT引脚的电平是拉低的,esp-tool之中是这样实现的:

class ClassicReset(ResetStrategy):
    """
    Classic reset sequence, sets DTR and RTS lines sequentially.
    """

    def reset(self):
        self._setDTR(False)  # IO0=HIGH
        self._setRTS(True)  # EN=LOW, chip in reset
        time.sleep(0.1)
        self._setDTR(True)  # IO0=LOW
        self._setRTS(False)  # EN=HIGH, chip out of reset
        time.sleep(self.reset_delay)
        self._setDTR(False)  # IO0=HIGH, done

代码之中的setDTR和setRTS的参数False代表设置为高电平,True代表设置为低电平。第一个步骤将DTR拉高而RTS拉低,此时RESET线被拉低,等待释放。sleep之后的第二个阶段将电平反转,DTR拉低而RTS拉高那么BOOT将会被拉至低电平并且释放了RESET线出现了复位中断。再次sleep等待芯片上电之后将DTR拉高两个高电平对应了BOOT和RESET都是高电平的正常状态。但是有的朋友可能会怀疑,当第一个sleep执行完毕之后实际上让BOOT引脚通过Q2导通拉低到DTR的动作是在释放DTR信号之后执行的,这里不会引起复位信号到达时BOOT还处于高电平的状态么?

精髓就在于原理图的第一部分之中接在CHIP_PU上的电阻和电容之中,它们构成了一个RC充电电路,这个电路具有延迟效果,CHIP_PU的拉高是需要充电时间的!我们来计算一下这个时间,ESP32芯片认为当使能引脚上的电平达到了0.75VDD的时候是使能状态,那么从0充电到这个值需要多长时间呢?

$$
\begin{align}
0.75\times V_{DD}&=V_{DD}\times(1-e^{-\frac t{RC}})\\
e^{-\frac t{RC}}&=0.25\\
t&=RC\ln 4=10^4\times10^{-7}\times\ln 4\\
&\approx 1.4\times10^{-3}
\end{align}
$$

经过计算需要毫秒级的时间——而如果将充点电容从\(0.1\mu F\)提高一些这个时间还能增长!那么这就是说当复位信号触发阈值时我们一定能够保证BOOT引脚处于低电平的状态,就这样完成了进入烧录固件Bootloader的流程。而直接复位执行代码更加简单,只要放弃对于BOOT的所有操作即可。原理图之中还有一部分是关于手动触发BOOT和RESET的按钮和两个LED的没在这个部分中可以看到供电LDO和上文提到的BOOT引脚的上拉电阻:

现在我们就可以来愉快的写代码了,在创建工程之前我们首先需要加载环境。这里只讨论命令行安装的Windows下的IDF环境加载、MacOS和Linux的环境加载。使用Windows安装器打包安装的版本照猫画虎即可,但是在配置过程中可能出现很多诡异的问题,如果您碰到这种问题还请遵照笔者推荐的散装化的安装方式,我们这样测试idf.py的功能:

#假设你的IDF装在IDF_PATH,你想测试的位置在TEST_PATH

#Windows下加载环境变量的方式
IDF_PATH/export.ps1
#MacOS和Linux下加载环境变量的方式
. IDF_PATH/export.sh

#从样例工程之中提出sample_project
cp -r IDF_PATH/examples/get-started/sample_project TEST_PATH/test_project
cd TEST_PATH/test_project

#选择芯片系列,这里应当选择你所使用的开发板
idf.py set-target esp32c3
#进入命令行图形化配置
idf.py menuconfig

建议不要将这个exports脚本写入系统环境变量文档,而是在每次编译和配置工程之前引入,当我们将这个环境变量配置脚本写入IDE之后整个操作就会变得流畅起来了。拷贝并且简单配置工程(具体怎么配置ESP这方面做的还不错,图形接口非常傻瓜化,看着开发板的原理图就能做,如果你看不懂原理图那就什么都不需要配置)之后,我们修改源代码,写入一个我们自己的点灯效果,例如笔者的开发板上有两个LED那么我就想要做一个频率10Hz,两个LED交替闪烁的Demo:

//将如下内容写入工程下的main/main.c,这里应当选择你使用的开发板对应的LED
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"

#define LED1 12
#define LED2 13

void app_main(void)
{
	gpio_set_direction(LED1,GPIO_MODE_OUTPUT);
	gpio_set_direction(LED2,GPIO_MODE_OUTPUT);

	while(1){
		gpio_set_level(LED1,1);
		gpio_set_level(LED2,0);
		vTaskDelay(50/portTICK_PERIOD_MS);

		gpio_set_level(LED1,0);
		gpio_set_level(LED2,1);
		vTaskDelay(50/portTICK_PERIOD_MS);
	}
}

将程序写入文件之后,将开发板连接到电脑,并且在设备管理器(MacOS在USB详情中找到,Linux在/dev目录下查找tty设备)之中找到对应串口号,例如笔者测试时的串口号是COM3,那么我们编译并且写入开发板就能够看到我们想要实现的效果了。

#编译工程
idf.py build
#通过端口COM3以115200波特率写入开发板
idf.py -p COM3 -b 115200 flash

如果编译不报错,并且下载效果也正常,那么到这里我们就算是顺利完成了IDF框架的安装和正常工作的设置,接下来就是让CLion接入IDF的工具链,随后就可以愉快的使用CLion进行开发工作了。

3.4 CLion对接ESP-IDF配置

事实上CLion是可以从开发流程之中替换出来的——本文的主要目的也不是非要将ESP-IDF的开发流程完美的和CLion结合起来,只是笔者不是很喜欢VSCode和Eclipse——ESP官方有面向二者专门开发的插件,适配度很好,几乎不用怎么配置。本文的主要目的是梳理ESP-IDF的开发流程、软硬件组织形式和开发理念以及实现方式。当我们使用CLion打开一个未经过任何配置的sample_project的时候,首先需要关闭掉下方左图的默认配置界面,转到设置>构建、执行、部署>工具链,设置一个加载了环境变量BAT文件(如果你是在MacOS或者Linux上那么就是对应的export.sh文件)的MinGW/GCC,因为CLion本身带有MinGW我们就使用默认的版本即可,CMake可以选择自带的,也可以像是笔者一样选择IDF工具集之中带有的CMake,配置如下中图所示:

配置好工具链之后,需要再新建一个CMake工具,指定使用我们刚刚建立的工具链,采用Ninja方式构建,构建目录设置为build,这是为了不和idf.py的指令冲突,其余参数不动。最后需要在下方的环境变量一栏写入一个全新的环境变量IDF_TARGET,这个值就是我们所用的芯片系列,例如笔者就需要写esp32c3。这样配置完毕后点击确认保存,随后右键项目根目录下的CMakeList.txt选择重新加载CMake项目,即可完成基础的配置。

配置后的工程如下左图所示,我们可以看到除了原本的sample_project的文件之外还拉入了整个IDF的源码,这是因为CMakeList.txt之中include其中的.cmake文件。随之而来的是Git上的问题——工程默认使用了IDF源码的Git信息,如果我们新建Git仓库反而还会出问题,这时候需要先像如下中途一样删除掉CLion自动识别的Git仓库,而后我们就可以新建自己的Git仓库并且将根目录设置为sample_project这样就能够进入正常的IDE图形化管理Git的模式了。

此外,在我们使用Git和CMake的途中,还会不断的报错,因为我们克隆的仓库的用户识别码和本机的识别码是对不上的,这时候我们需要告诉Git我们的IDF仓库是安全的,那么就需要在全局设置之中做一个赦免声明如下所示:

git config --global --add safe.directory C:/espressif/idf/v5.1.2

随后我们开始修改main.c,这次将交替闪烁的LED频率设置为5Hz,并且每秒向串口发送一个Hello World!字符串,用于编译、烧录、测试串口输出的效果是否正常,相关测试代码如下所示。并且在编写代码的过程之中我们发现各种代码提示、补全、函数原型查看等等CLion的功能都正常实现了。

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "driver/uart.h"
#include "string.h"

#define LED1 12
#define LED2 13

#define TX 21
#define RX 20

void app_main(void)
{
	gpio_set_direction(LED1,GPIO_MODE_OUTPUT);
	gpio_set_direction(LED2,GPIO_MODE_OUTPUT);

    uart_config_t uartConfig = {
        .baud_rate = 115200,
        .data_bits = UART_DATA_8_BITS,
        .parity = UART_PARITY_DISABLE,
        .stop_bits = UART_STOP_BITS_1,
        .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,        
    };
    uart_driver_install(UART_NUM_0,1024*2,0,0,NULL,0);
    uart_param_config(UART_NUM_0,&uartConfig);
    uart_set_pin(UART_NUM_0,TX,RX,UART_PIN_NO_CHANGE,UART_PIN_NO_CHANGE);
    
    int i = 0;
    const char* msg = "Hello World!\n";
    
	while(1){
        if(i%5==0)
            uart_write_bytes(UART_NUM_0,msg,strlen(msg));
        i = (i+1)%5;
        
		gpio_set_level(LED1,1);
		gpio_set_level(LED2,0);
		vTaskDelay(100/portTICK_PERIOD_MS);

		gpio_set_level(LED1,0);
		gpio_set_level(LED2,1);
		vTaskDelay(100/portTICK_PERIOD_MS);
	}
}

点击右上方的运行设置之中main.elf选项选中,随后点击Build按钮即可开始构建项目,第一次构建时间都比较长,之后构建时间会随着代码改变量而变化。构建完成后点击切换选中flash即可下载代码。但是这里笔者需要指出IDF与CLion在这个部分的严重不适配,导致这些具体的工具性步骤还是要依赖于命令行操作,因此在命令行开始执行时仍旧需要加载export环境变量脚本。

  • menuconfig的图形化配置效果在CLion自带的终端之中根本不能正常工作。
  • 串口下载和监视对应的一个flash一个monitor两个动作无法识别通过CLion运行配置界面输入的ESPPORT和ESPBAUD两个指代串口名称和串口波特率的环境变量,你必须要返回CMake配置界面设置相关的两个环境变量。
  • 像是flash和monitor这种动作居然被设定为CMake动作——它们根本不需要执行什么可执行文件,他们的真正动作是被分配在Build阶段执行的!!所以你不但必须选择main.elf作为它们的可执行文件,而且你不能点Run按钮——因为elf无法直接执行会报错!
  • ……槽多无口

笔者目前使用的方法是menuconfig尽量一次配置完毕,其他的动作除了编译和烧录都在一个CLion自带的中断子窗口之中执行。笔者能够想到的解决这种对于厂商来说只不过多加一组工具接口的小问题但是对开发者造成极大烦躁的问题的方案无非这几种:

  • 预先写好一套脚本,通过新建Shell类型的配置完成目标
  • 外挂一个命令行窗口(目前方法)
  • ESP你行行好,开发个接口或者JetBrains插件吧!

关于IDF开发框架的探索基本上到此告一段落了,总体来说IDF面向物联网方向的开发是一套很不错的SDK,其软硬件组织方式大有可取之处,笔者个人觉得完全超越了ESP-Arduino或者PlatformIO提供的其他方案,对此想要了解更多了解更深入的朋友可以参照IDF文档官网

You May Also Like

About the Author: Fenice

本人及开发团队主要兴趣领域为:自动控制理论、网站开发、移动端开发、嵌入式系统、机器人相关项目、电力电子技术、电动机控制。以及,兼任北京海淀区北下关街道反卷委员会常务委员长并且获得“全年度中国最佳懒狗”称号。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注