本文概述
- ESP32音频:定时器和中断
- 从定时器中断采样ESP32音频数据
- ESP32音频采样:最终代码
- ESP32专案:至OS或不至OS
ESP32是微控制器中的庞然大物, 其规格包括厨房水槽以外的所有东西。它是片上系统(SoC)产品, 实际上需要操作系统才能使用其所有功能。
本ESP32教程将解释并解决从计时器中断中采样模数转换器(ADC)的特定问题。我们将使用Arduino IDE。即使就功能集而言, 它是目前最差的IDE之一, 但Arduino IDE至少易于设置和用于ESP32开发, 并且具有用于各种常见硬件模块的最大的库集合。但是, 出于性能原因, 我们还将使用许多本机ESP-IDF API而不是Arduino API。
ESP32音频:定时器和中断ESP32包含四个硬件计时器, 分为两组。所有定时器都相同, 具有16位预分频器和64位计数器。预分频值用于将硬件时钟信号(来自进入计时器的内部80 MHz时钟)限制为每N个滴答声。最小预分频值是2, 这意味着中断最多可以在40 MHz处正式触发。这还不错, 因为这意味着在最高的计时器分辨率下, 处理程序代码必须在最多6个时钟周期(240 MHz内核/ 40 MHz)中执行。计时器具有几个关联的属性:
- 分频器—频率预分频值
- counter_en-计时器的关联64位计数器是否已启用(通常为true)
- counter_dir-计数器是递增还是递减
- alarm_en-是否启用了” 警报” , 即计数器的操作
- auto_reload-触发警报时是否重置计数器
- 计时器已禁用。硬件完全没有滴答声。
- 启用了计时器, 但禁用了警报。计时器硬件正在滴答, 可选地是递增或递减内部计数器, 但没有其他反应。
- 计时器已启用, 并且其闹钟也已启用。像以前一样, 但是这次是在计时器计数器达到特定的配置值时执行一些操作:计数器被重置和/或产生了中断。
中断处理函数必须在下一个中断产生之前完成, 这给我们提供了一个复杂的函数上限。通常, 中断处理程序应做的工作最少。
为了实现任何复杂的远程操作, 它应该设置一个标志, 该标志由不间断代码检查。任何比读取或将单个引脚设置为单个值更复杂的I / O, 通常最好分担给单独的处理程序。
在ESP-IDF环境中, 可以使用FreeRTOS函数vTaskNotifyGiveFromISR()来通知任务中断处理程序(也称为” 中断服务程序” , 即ISR)要执行的操作。代码如下:
portMUX_TYPE DRAM_ATTR timerMux = portMUX_INITIALIZER_UNLOCKED;
TaskHandle_t complexHandlerTask;
hw_timer_t * adcTimer = NULL;
// our timervoid complexHandler(void *param) {
while (true) {
// Sleep until the ISR gives us something to do, or for 1 second
uint32_t tcount = ulTaskNotifyTake(pdFALSE, pdMS_TO_TICKS(1000));
if (check_for_work) {
// Do something complex and CPU-intensive
}
}
}void IRAM_ATTR onTimer() {
// A mutex protects the handler from reentry (which shouldn't happen, but just in case)
portENTER_CRITICAL_ISR(&
timerMux);
// Do something, e.g. read a pin.if (some_condition) {
// Notify complexHandlerTask that the buffer is full.
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
vTaskNotifyGiveFromISR(complexHandlerTask, &
xHigherPriorityTaskWoken);
if (xHigherPriorityTaskWoken) {
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和ESP32 Arduino核心GitHub项目进行了记录。
CPU缓存和哈佛架构
需要注意的非常重要的一点是onTimer()中断处理程序的定义中的IRAM_ATTR子句。原因是CPU内核只能从嵌入式RAM执行指令(和访问数据), 而不能从通常存储程序代码和数据的闪存中执行指令。为了解决这个问题, 总共520 KiB的RAM的一部分专门用作IRAM, 这是一个128 KiB的高速缓存, 用于透明地从闪存中加载代码。 ESP32为代码和数据使用单独的总线(“ 哈佛体系结构” ), 因此它们在很大程度上是分开处理的, 并扩展到了内存属性:IRAM是特殊的, 只能在32位地址边界访问。
实际上, ESP32内存非常不均匀。它的不同区域专用于不同的目的:最大连续区域的大小约为160 KiB, 并且用户程序可访问的所有” 正常” 存储器总计约为316 KiB。
【使用ESP32音频采样】从闪存存储中加载数据的速度很慢, 并且可能需要SPI总线访问, 因此任何依赖于速度的代码都必须小心以适合IRAM缓存, 并且通常要小得多(小于100 KiB), 因为其中的一部分被内存使用。操作系统。值得注意的是, 如果在发生中断时未将中断处理程序代码加载到缓存中, 则系统将生成异常。当发生中断时, 从闪存中加载某些东西既很慢, 也很麻烦。 onTimer()处理程序上的IRAM_ATTR说明符告诉编译器和链接器将此代码标记为特殊代码-它将被静态放置在IRAM中, 并且永远不会被换出。
但是, IRAM_ATTR仅适用于在其上指定的功能-从该功能调用的任何功能均不受影响。
从定时器中断采样ESP32音频数据从中断中采样音频信号的通常方法包括维护采样的存储缓冲区, 将采样数据填充到其中, 然后通知处理程序任务数据可用。
ESP-IDF记录了adc1_get_raw()函数, 该函数测量第一个ADC外设上特定ADC通道上的数据(第二个外设由WiFi使用)。但是, 在计时器处理程序代码中使用它会导致程序不稳定, 因为它是一个复杂的函数, 会调用大量其他IDF函数(尤其是处理锁的函数), 而且adc1_get_raw()和这些函数均不会它的调用都标有IRAM_ATTR。一旦执行了足够多的代码片段, 中断处理程序将崩溃, 这将导致ADC函数从IRAM中交换出来, 这可能是WiFi-TCP / IP-HTTP栈或SPIFFS文件系统库, 或其他任何东西。
注意:一些IDF函数是特制的(并标记有IRAM_ATTR), 以便可以从中断处理程序中调用它们。上例中的vTaskNotifyGiveFromISR()函数就是这样的一个函数。
解决此问题的最IDF友好方法是让中断处理程序在需要获取ADC样本时通知任务, 并让该任务进行采样和缓冲区管理, 并可能将另一个任务用于数据分析(或压缩或传输或任何情况)。不幸的是, 这效率极低。处理程序端(用于通知任务有待完成的任务)和任务端(用于接任务的任务)都涉及与操作系统的交互以及正在执行的数千条指令。这种方法在理论上是正确的, 但它会使CPU陷入瘫痪, 以致于几乎没有多余的CPU电源用于其他任务。
挖掘IDF源代码
从ADC采样数据通常是一项简单的任务, 因此下一个策略是查看IDF的工作方式, 然后直接在我们的代码中复制它们, 而无需调用提供的API。 adc1_get_raw()函数是在IDF的rtc_module.c文件中实现的, 在执行的八项操作中, 实际上只有一个是对ADC采样, 这是通过调用adc_convert()完成的。幸运的是, adc_convert()是一个简单的函数, 它通过一个名为SENS的全局结构通过操作外围硬件寄存器来对ADC进行采样。
修改此代码使其在我们的程序中起作用(并模仿adc1_get_raw()的行为)很容易。看起来像这样:
int 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 = http://www.srcmini.com/SENS.sar_meas_start1.meas1_data_sar;
return adc_value;
}
下一步是包括相关的标头, 以便SENS变量可用:
#include <
soc/sens_reg.h>
#include <
soc/sens_struct.h>
最后, 由于adc1_get_raw()在对ADC进行采样之前执行了一些配置步骤, 因此应在ADC刚建立之后直接调用它。这样, 可以在计时器启动之前执行相关配置。
这种方法的缺点是, 它不能与其他IDF功能配合使用。一旦调用了其他一些外设, 驱动器或随机代码来重置ADC配置, 我们的自定义功能将不再正常工作。至少WiFi, PWM, I2C和SPI不会影响ADC配置。如果确实有影响, 则对adc1_get_raw()的调用将再次适当地配置ADC。
ESP32音频采样:最终代码有了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[abufPos++] = local_adc1_read(ADC1_CHANNEL_0);
if (abufPos >
= ADC_SAMPLES_COUNT) {
abufPos = 0;
// Notify adcTask that the buffer is full.
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
vTaskNotifyGiveFromISR(adcTaskHandle, &
xHigherPriorityTaskWoken);
if (xHigherPriorityTaskWoken) {
portYIELD_FROM_ISR();
}
}
portEXIT_CRITICAL_ISR(&
timerMux);
}
在这里, adcTaskHandle是FreeRTOS任务, 将按照第一个代码段中complexHandler函数的结构执行以处理缓冲区。它将创建音频缓冲区的本地副本, 然后可以在空闲时对其进行处理。例如, 它可以在缓冲区上运行FFT算法, 也可以对其进行压缩并通过WiFi传输。
矛盾的是, 使用Arduino API代替ESP-IDF API(即, 用AnalogRead()代替adc1_get_raw())将可行, 因为Arduino函数标有IRAM_ATTR。但是, 它们比ESP-IDF慢得多, 因为它们提供了更高级别的抽象。说到性能, 我们的自定义ADC读取功能大约是ESP-IDF的两倍。
ESP32专案:至OS或不至OS我们在这里所做的工作-重新实现操作系统的API以解决一些如果不使用操作系统就不会出现的问题-很好地说明了在操作系统中使用操作系统的优缺点。第一名。
较小的微控制器可以直接进行编程, 有时使用汇编代码进行编程, 并且开发人员可以完全控制程序执行的各个方面, 每个CPU指令以及芯片上所有外围设备的所有状态。随着程序变大以及使用越来越多的硬件, 这自然会变得乏味。复杂的微控制器(例如ESP32)具有大量外围设备, 两个CPU内核以及复杂, 不一致的内存布局, 从头开始编程将非常困难且费力。
尽管每个操作系统对使用其服务的代码都设置了一些限制和要求, 但通常值得这样做的好处是:更快, 更简单的开发。但是, 有时我们可以并且通常应该在嵌入式空间中绕开它。
相关:我如何制作功能齐全的Arduino气象站
推荐阅读
- 各种可能性(Ruby模式匹配指南)
- 存款,支付(如何创建市场)
- 选择技术栈替代方案-跌宕起伏
- 语言服务器协议教程(从VSCode到Vim)
- Android 查阅博客记录内容
- 区块链资产支持多币种存储手机钱包app开发
- android中RecyclerView控件的列表项横向排列
- Android 查阅博客2_APT
- android中RecyclerView控件的使用