作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
伊万·沃拉斯博士的头像

Ivan Voras, PhD

Ivan有18年以上的工作经验, 从后端和区块链架构到DBA ops, kernel development, and embedded software.

Years of Experience

21

Share

ESP32是支持WiFi和蓝牙的下一代产品 microcontroller. 它是总部位于上海的快运非常受欢迎的“和”的后继产品, for the hobbyist audience, revolutionary—ESP8266 microcontroller.

作为微控制器中的庞然大物,ESP32的规格包括除了厨房水槽之外的所有东西. 它是一个片上系统(SoC)产品,实际上需要一个操作系统来利用它的所有功能.

本ESP32教程将解释并解决从定时器中断中采样模数转换器(ADC)的特定问题. 我们将使用Arduino IDE. 即使它在功能集方面是最糟糕的ide之一, Arduino IDE至少易于设置和用于ESP32开发, 它还拥有最多的用于各种常用硬件模块的库. 但是,我们也将使用许多本机ESP-IDF api来代替 Arduino 1,出于性能原因.

ESP32音频:定时器和中断

ESP32包含四个硬件定时器,分为两组. 所有计时器都是相同的,具有16位预算器和64位计数器. 预刻度值用于将硬件时钟信号(来自进入定时器的内部80mhz时钟)限制为每n个刻度. 最小预调量值为2,这意味着中断最多可以在40 MHz正式启动. This is not bad, 这意味着在最高的时间分辨率下, 处理器代码必须在最多6个时钟周期内执行(240 MHz核心/40 MHz). 计时器有几个相关的属性:

  • divider-频率预标值
  • counter_en-定时器关联的64位计数器是否启用(通常为true)
  • counter_dir-计数器是递增还是递减
  • alarm_en—whether the “alarm”, i.e. 计数器的动作是启用的
  • auto_reload-告警触发时,计数器是否复位

一些重要的计时器模式是:

  • The timer is disabled. 硬件完全不运转了.
  • 定时器使能,告警未使能. 计时器硬件在滴答作响, 它可选地递增或递减内部计数器, 但除此之外什么都没有发生.
  • 使能定时器并使能其告警. Like before, 但是这一次,当计时器计数器达到一个特定的, 配置值:计数器复位和/或中断产生.

计时器的计数器可以被任意代码读取, but in most cases, 我们感兴趣的是周期性地做一些事情, 这意味着我们将配置定时器硬件来生成中断, 我们将编写代码来处理它.

中断处理程序函数必须在下一个中断生成之前完成, 这就给了我们一个函数复杂度的上限. 通常,中断处理程序应该尽其所能完成最少的工作.

来完成任何稍微复杂的事情, 它应该设置一个由非中断代码检查的标志. 任何比读取单个引脚或将单个引脚设置为单个值更复杂的I/O,通常最好将其卸载到单独的处理程序中.

在ESP-IDF环境下,FreeRTOS函数 vTaskNotifyGiveFromISR() 可以用来通知任务,中断处理程序(也称为中断服务程序, (ISR)有一些事情要做. The code looks like this:

portmux_dram_attr timerMux = portMUX_INITIALIZER_UNLOCKED; 
TaskHandle_t complexHandlerTask;
hw_timer_t * adcTimer = NULL; // our timer

void complexHandler(void *param) {
  while (true) {
    //休眠,直到ISR给我们一些事情做,或者1秒
    uint32_t tcount = ulTaskNotifyTake(pdFALSE, pdMS_TO_TICKS(1000));  
    if (check_for_work) {
      //执行一些复杂的cpu密集型操作
    }
  }
}

void IRAM_ATTR onTimer() {
  //互斥锁保护处理程序不被重入(这是不应该发生的,只是以防万一)
  portENTER_CRITICAL_ISR(&timerMux);

  // Do something, e.g. read a pin.
  
  if (some_condition) { 
    //通知complexHandlerTask缓冲区已满.
    BaseType_t xhigherprioritytaskoken = pdFALSE;
    vTaskNotifyGiveFromISR (complexHandlerTask &xHigherPriorityTaskWoken);
    if (xhigherprioritytaskoken) {
      portYIELD_FROM_ISR();
    }
  }
  portEXIT_CRITICAL_ISR(&timerMux);
}

void setup() {
  xTaskCreate(complexHandler, "Handler Task", 8192, NULL, 1, &complexHandlerTask);
  adcTimer = timerBegin(3, 80, true); // 80 MHz / 80 = 1 MHz hardware clock for easy figuring
  timerAttachInterrupt (adcTimer &onTimer, true); // Attaches the handler function to the timer 
  timerAlarmWrite(adcTimer, 45, true); // Interrupts when counter == 45, i.e. 22.222 times a second
  timerAlarmEnable (adcTimer);
}

注意:本文中代码中使用的函数都是用 ESP-IDF API and at the ESP32 Arduino核心GitHub项目.

CPU缓存和哈佛体系结构

需要注意的很重要的一点是 IRAM_ATTR 条款中的定义 onTimer() interrupt handler. 这样做的原因是CPU内核只能执行来自嵌入式RAM的指令(和访问数据), 不是从闪存,其中程序代码和数据通常存储. To get around this, 总520kib内存的一部分专用于IRAM, 一个128kb的缓存,用于从闪存中透明地加载代码. ESP32使用单独的总线来传输代码和数据(“Harvard architecture”),所以它们在很大程度上是分开处理的, 这延伸到内存属性:IRAM是特殊的, 并且只能在32位地址边界上访问.

事实上,ESP32内存是非常不均匀的. 它的不同区域用于不同的目的:最大连续区域的大小约为160 KiB, 用户程序可访问的所有“正常”内存总计只有316kib左右.

从闪存加载数据是缓慢的,可能需要SPI总线访问, 因此,任何依赖于速度的代码都必须注意适应IRAM缓存, 而且通常要小得多(小于100kib),因为它的一部分被操作系统使用. Notably, 当中断发生时,如果中断处理程序代码没有加载到缓存中,系统将生成一个异常. 当中断发生时,从闪存中加载一些东西将是非常缓慢和逻辑上的噩梦. The IRAM_ATTR specifier on the onTimer() 处理程序告诉编译器和链接器将此代码标记为特殊代码——它将静态地放置在IRAM中,永远不会交换出来.

However, the IRAM_ATTR 只适用于它指定的函数——从该函数调用的任何函数都不受影响.

从定时器中断中采样ESP32音频数据

从中断中对音频信号进行采样的通常方法包括维护一个采样的内存缓冲区, 用采样数据填充它, 然后通知处理程序任务数据可用.

The ESP-IDF documents the adc1_get_raw() 测量第一个ADC外设上特定ADC通道上的数据的功能(第二个由WiFi使用). However, 在计时器处理程序代码中使用它会导致程序不稳定, 因为它是一个复杂的函数,它调用了大量其他IDF函数——特别是那些处理锁的函数——但两者都不调用 adc1_get_raw() 它调用的函数也没有标记 IRAM_ATTR. 一旦执行了一段足够大的代码,导致ADC函数被交换出iram(这可能是WiFi-TCP/IP-HTTP堆栈),中断处理程序就会崩溃, 或SPIFFS文件系统库, or anything else.

注意:一些IDF函数是特别设计的(并标记为。 IRAM_ATTR),以便可以从中断处理程序中调用它们. The vTaskNotifyGiveFromISR() 上面例子中的函数就是这样一个函数.

解决这个问题的最适合idf的方法是,中断处理程序在需要采集ADC样本时通知任务, 让这个任务做采样和缓冲管理, 可能还有另一项任务用于数据分析(或压缩或传输或其他情况). 不幸的是,这是非常低效的. 处理程序端(通知任务有工作要做)和任务端(选择要做的任务)都涉及与操作系统和正在执行的数千条指令的交互. This approach, 虽然理论上是正确的, 可以使CPU陷入如此严重的困境,以至于几乎没有多余的CPU功率用于其他任务.

挖掘IDF源代码

从ADC中采样数据通常是一项简单的任务, 所以下一个策略就是看看以色列国防军是怎么做的, 并直接在我们的代码中复制它, 而无需调用所提供的API. The adc1_get_raw() 函数中实现 rtc_module.c file of the IDF, 它有八种左右的功能, 只有一个是对ADC进行采样, 哪个是通过调用来完成的 adc_convert(). Luckily, adc_convert() 是一个简单的函数,通过操纵外围硬件寄存器通过一个全局结构命名 SENS.

调整此代码,使其在我们的程序中工作(并模仿的行为) adc1_get_raw()) is easy. It looks like this:

IRAM_ATTR local_adc1_read(int channel) {
    uint16_t adc_value;
    SENS.sar_meas_start1.sar1_en_pad = (1 << channel); // only one channel is selected
    while (SENS.sar_slave_addr1.meas_status != 0);
    SENS.sar_meas_start1.meas1_start_sar = 0;
    SENS.sar_meas_start1.meas1_start_sar = 1;
    while (SENS.sar_meas_start1.meas1_done_sar == 0);
    adc_value = SENS.sar_meas_start1.meas1_data_sar;
    return adc_value;
}

下一步是包含相关的标题,以便 SENS 变量变得可用:

#include 
#include 

Finally, since adc1_get_raw() 在采样ADC之前执行一些配置步骤, 应该直接调用它, 就在ADC设置好之后. 这样就可以在计时器启动之前执行相关配置.

这种方法的缺点是它不能很好地与其他IDF函数配合使用. 还有其他外围设备, driver, 或者调用一个随机的代码片段,它重置ADC配置, 我们的自定义函数将不再正常工作. 至少WiFi、PWM、I2C和SPI不会影响ADC的配置. 万一有什么事影响到它,就调用 adc1_get_raw() 会重新正确配置ADC吗.

ESP32音频采样:最终代码

With the local_adc_read() 函数中,我们的计时器处理程序代码看起来像这样:

#define ADC_SAMPLES_COUNT 1000
int16_t abuf [ADC_SAMPLES_COUNT];
int16_t abufPos = 0;

void IRAM_ATTR onTimer() {
  portENTER_CRITICAL_ISR(&timerMux);

  abuf[abufpo++] = local_adc1_read(ADC1_CHANNEL_0);
  
  if (abufPos >= ADC_SAMPLES_COUNT) { 
    abufPos = 0;

    //通知adcTask缓冲区已满.
    BaseType_t xhigherprioritytaskoken = pdFALSE;
    vTaskNotifyGiveFromISR (adcTaskHandle &xHigherPriorityTaskWoken);
    if (xhigherprioritytaskoken) {
      portYIELD_FROM_ISR();
    }
  }
  portEXIT_CRITICAL_ISR(&timerMux);
}

Here, adcTaskHandle 是FreeRTOS任务,将被实现来处理缓冲区,下面的结构 complexHandler 函数在第一个代码片段中. 它会在本地复制音频缓冲区,然后在空闲时处理它. For example, 它可能会在缓冲区上运行FFT算法, 或者它可以压缩并通过WiFi传输.

矛盾的是,使用Arduino API而不是ESP-IDF API (i.e. analogRead() instead of adc1_get_raw())会起作用,因为Arduino函数被标记为 IRAM_ATTR. 然而,它们比ESP-IDF慢得多,因为它们提供了更高层次的抽象. 说到性能,我们定制的ADC读取函数的速度大约是ESP-IDF读取函数的两倍.

ESP32项目:到OS还是不到OS

我们在这里所做的——重新实现操作系统的API来解决一些如果我们不使用操作系统甚至不会出现的问题——很好地说明了首先使用操作系统的利弊.

较小的微控制器是直接编程的, 有时在汇编代码中, 开发人员可以完全控制程序执行的每一个方面, 每条CPU指令和芯片上所有外设的所有状态. 随着程序变得越来越大,使用越来越多的硬件,这自然会变得乏味. 一个复杂的微控制器,如ESP32, 使用大量的外围设备, two CPU cores, and a complex, non-uniform memory layout, 从头开始编程是否具有挑战性和费力.

虽然每个操作系统对使用其服务的代码都有一些限制和要求, 这样做的好处通常是值得的:更快、更简单的开发. 然而,有时我们可以(在嵌入式空间中通常应该)绕过它.

Understanding the basics

  • ESP32的用途是什么?

    ESP32是一款带有WiFi和蓝牙的微控制器,用于创建物联网产品. 它是一款功能强大的设备,拥有双核CPU和大量功能,包括硬件加密卸载, 520 KiB RAM, and a 12-bit ADC. 它用于复杂的产品,其功能集使开发更有效.

  • 什么设备使用express?

    expressif是一家公司,其最知名的产品是ESP8266和ESP32 wifi微控制器. 这些产品用于物联网设备,可以从其庞大的功能集中受益.

  • 什么是微控制器定时器中断?

    定时器是微控制器外设(内部模块), 与内部或外部时钟信号配对, 在每个时钟滴答时,哪个值增加或减少. 计时器可以在计数到一定数量后产生中断, 哪一段代码会被执行.

  • What is ESP programming?

    ESP32是一款支持WiFi和蓝牙的微控制器,用于创建物联网产品. 它是一款功能强大的设备,拥有双核CPU和大量功能. 编写ESP32固件通常依赖于供应商提供的名为ESP-IDF的开发框架, 它是在FreeRTOS之上实现的.

  • 片上系统是如何工作的?

    片上系统是一种构建硬件的方法,它包括用于构建工作计算机的许多(或全部)组件, on a single chip package. 确切的规格各不相同,可能包括也可能不包括系统内存, graphics processors, I/O controllers, network controllers, flash memory, and others.

聘请Toptal这方面的专家.
Hire Now
伊万·沃拉斯博士的头像
Ivan Voras, PhD

Located in Zagreb, Croatia

Member since August 26, 2014

About the author

Ivan有18年以上的工作经验, 从后端和区块链架构到DBA ops, kernel development, and embedded software.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Years of Experience

21

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Toptal Developers

Join the Toptal® community.