ESP8266 上的 String() 函数影响模拟到数字采样率?

Analog to digital sampling rate affected by String() function on ESP8266?

我正在使用 ESP8266 NodeMCU 12-E 开发板从预放大的驻极体麦克风捕获音频,然后将其上传到网络,然后将其转换为 wav 文件。我的第一个想法是在 ESP8266 上将 analogRead(A0) 的整数值转换为 String 类型,然后将它们连接成一个更长的字符串有效负载,我可以将其发布到 MQTT 代理。

我的 MQTT 客户端订阅者似乎没有获得正确的声音文件,因为我听到的只是一系列有节奏的流行音乐。

我决定调查一下我在 ESP8266 板上的代码是否能够正确捕获数据。我将代码剥离到这几行似乎会导致问题:

#include <ESP8266WiFi.h>

const char *ssid =  "____";  // Change it
const char *pass =  "____";  // Change it

void setup()
{
  Serial.begin(115200);
  Serial.println(0);      //start
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, pass);
}


void loop()
{
    int analog = analogRead(A0);

    if (analog > 255) {
      analog = 255;
    }
    else if (analog < 0){
      analog = 0;
    }

    Serial.print(String(analog));
    Serial.print(" ");

}

以下是我如何使用上面的代码生成一个 wav 文件来检查声音是否符合我的预期:

- I start up the ESP8266 development board
- I turn on the Serial Monitor and clear all previous output
- I power up my electret microphone and speak into it
- I power down my electret microphone
- I copy the contents of the Serial Monitor (which is a series of integers) into a text file called `audio.raw`
- I copy `audio.raw` to a linux machine that has ffmpeg installed
- I issue the command `ffmpeg -f u8 -ar 11111 -ac 1 -i audio.raw -y audio.wav` on the linux machine

当我听audio.raw文件时,我听到了我的声音,但速度可能比正常情况下快5-10倍。 (我也有很多噪音和失真,但这可能是输入信号质量的一个单独问题。)

然后我尝试将这一行代码 Serial.print(String(analog)) 更改为 Serial.print(analog)。然后我重复了上面的步骤。但是这一次,我的声音听起来比平时快了大约2倍。

为什么将这一行从 Serial.print(String(analog)) 更改为 Serial.print(analog) 会产生如此大的差异?

是否因为String()函数是一个非常耗时的操作?当脚本需要更多时间来处理每一行代码时,脚本捕获足够 analogRead(A0) 数据点的时间就更少了?如果我 运行 使用所有相同标志的相同 ffmpeg 命令,那么 ffmpeg 会尝试通过加快音频播放来满足 -ar 11111 要求吗?这意味着我的采样率取决于脚本的执行速度?这意味着由于制造精度、环境温度等的可变性,我必须考虑同一型号的其他板的可变执行速度...?

由于您使用Serial.begin(115200) ESP8266 微控制器将通过串行端口每秒传输 115200 位。即每秒 115200 / 8 = 14400 字节,这意味着由于您对音频使用 u8(无符号 8 位)格式,因此每个样本都包含一个字节。只需将 ffmpeg -ar 参数更改为 14400.

我没有可以连接到 MCU 进行测试的麦克风,但它应该可以正常工作。另一个 -ac 参数是正确的,因为它是单声道音频。

编辑:打印到 Serial 时也不要使用 String() 构造函数。

虽然使用 Serial() 构造函数声音速度提高了大约 5 倍,因为 String 将您的 1 字节值转换为 3 字节,例如; byte : 255 -> String : "2", "5", "5" ,你不用考虑微控制器的执行速度,它会像你定义的那样每秒输出115200位。你只需要考虑它的输出。

最后删除行

Serial.print(" ");

也改

int analog = analogRead(A0);

byte analog = (byte)analogRead(A0);

由于 int 由 4 个字节组成,您不希望向串行发送额外的 3 个字节。

并且在将 int 更改为 byte 之后,您可以删除此代码块

if (analog > 255) {
  analog = 255;
}
else if (analog < 0){
  analog = 0;
}

如果你通过带有 ffmpeg 的 USB 将 ESP8266 连接到 linux 设备,你可以使用

ttylog -b 115200 -d /dev/ttyUSB0 | ffmpeg -f u8 -ar 14400 -ac 1 -i - -y audio.wav

从ESP8266实时采集音频数据。

您的采样率与您的循环实现相关联(如您所见)。这也会导致采样率抖动,因为不同的代码路径将花费不同的时间,并且中断服务例程也会窃取 CPU 个周期。

这种抖动将是导致输出失真的原因之一。

When I listen to the audio.raw file, I hear my voice, but the speed is maybe 5-10 times faster than normal.

ESP8266 有一个硬件 UART,因此代码加载 UART 的 FIFO 缓冲区的速度可能比它输出的速度更快。这将是感知到更快采样率的来源,但也会在缓冲区填满时导致抖动或数据丢失。根据实施情况,当缓冲区填满时,它将丢弃数据或阻塞(导致抖动)。

Why does changing this one line from Serial.print(String(analog)) to Serial.print(analog) make such a big difference?

Is it because the String() function is a very expensive operation that takes up a lot of time? And when the script needs more time to process each line of code, the script then has less time to capture enough analogRead(A0) data points?

是的,是的,是的。

造成性能差异的原因之一是 String() 涉及在堆上分配和管理内存以存储字符。

Serial.print(analog) 在堆栈上使用固定大小的缓冲区,因为代码知道显示 int 所需的最大字符数。

And if I run the same ffmpeg command using all the same flags, then ffmpeg will try to meet the -ar 11111 requirement by speeding up the audio play?

是的。 ffmpeg 假定样本具有固定的采样率,但这与正在打印的样本不匹配。

Which would imply that my sampling rate is dependent on execution speed of my script?

是的!

Which means I have to consider variable execution speeds across other boards of the same model due to variability in manufacturing precision, environmental temperature, etc...?

是的。将有大量变量影响执行速度。

你能做什么?

将数据采样与代码执行分离。

这可以通过实施 Interrupt Service Routine 来完成。将 ISR 绑定到硬件定时器,使其以固定采样率执行并避免抖动。

ISR 可以写入 loop() 中的代码通过串行连接传输的缓冲区。 ISR 和串行传输代码需要管理缓冲区以确保两者都不会溢出。这样做的一种方法是使用 ISR 和传输代码使用的备用缓冲区。