项目介绍
双音多频 DTMF(Dual Tone Multi Frequency),双音多频,由高频群和低频群组成,高低频群各包含4个频率。一个高频信号和一个低频信号叠加组成一个组合信号,代表一个数字。DTMF信号有16个编码。简单的说这是通过一个高频和低频组成一个音频的技术,广泛应用于电话系统中,下图可以帮助容易理解这个概念。
这次的项目就是基于这种技术:通过上述方法合成一段音频,其中包含多个数字,然后将这个音频设置为手机的铃声。当有来电之后,Seeed XIAO ESP32S3 Sense 通过板子上的麦克风获得数据,然后使用Goertzel算法进行识别。识别到指定的数字后,会启动声光报警器。
硬件介绍:
1. 最核心的部件就是 Seeed XIAO ESP32S3 Sense。这款开发板是seeedstudio退出的 Seeed Studio XIAO 系列中的一款,这个系列是迷你型开发板,具有类似的硬件结构,尺寸仅为拇指大小。代码名称 "XIAO" 代表其特点之一“微小”,而另一特点则是“强大”。Seeed Studio XIAO ESP32S3 Sense 集成了摄像头传感器、数字麦克风和支持 SD 卡的功能。这次我们使用的就是它的数字麦克风功能;
2. 外部使用一个声光报警器来告知来电。选择的是4-12V音量可调的报警器。最高音量可达120分贝。
3. 为了更好的让Seeed XIAO ESP32S3 Sense发挥作用,特地设计一个电路板:
板子上带有三个接口:
1.最左侧H2 是一个按钮接口,这里我们放置一个按钮,用于触发后用户可以通过按钮关闭报警;
2.中间U1 是XIAO ESP32S3 的接口,我们可以把板子直接插入使用
3.最右侧的是一个CH217K芯片,这是WCH出品的可调限流门限的 USB 端口电源开关芯片。芯片内部集成了过流保护、过温保护、欠压保护等模块,支持 5V 电压下不超过 2.7A 的可编程电流,在 VOUT 输出端发生短路等情况时可以限制输出电流从而保护供电系统。 我们用作 5V的开关。
电路非常简单,设计出来的PCB 也不复杂:
3D预览如下:
项目工作流程
软件流程和关键代码
软件负责从麦克风获得音频数据,并且识别之。然后判断是否出现指定的序列,如果出现就改变指定 GPIO 的状态驱动报警器。
关键代码解说如下:
1.获得音频数据的关键代码如下, 获得的数据格式是 16K 采样率,16Bits 单声道数据
// 设置 42 PDM 时钟和 41 PDM 数据引脚
I2S.setPinsPdmRx(42, 41);
// 以 16 kHz 和 16 位每样本启动 I2S
if (!I2S.begin(I2S_MODE_PDM_RX, 16000, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO)) {
Serial.println("初始化 I2S 失败!");
while (1); // 什么都不做
}
2.声音识别算法是Goertzel算法,它一种高效的数字信号处理算法,主要用于检测特定频率成分的信号,相比FFT(快速傅里叶变换)在单频点检测场景中具有显著优势。
核心特点
- 计算效率高
- 仅计算目标频率点的能量,无需计算整个频谱
- 复杂度仅为 O(N)(N为采样点数),远低于FFT的O(N log N)
- 资源占用少
- 只需存储少量中间变量
- 适合嵌入式系统和实时处理
- 实现简单
- 核心为二阶IIR滤波器结构
- 无需复数运算
有兴趣的朋友可以深入研究。
Goertzel算法关键代码如下:
// 修改能量计算阈值(因采样率变化)
void detect_dtmf(float* energy) {
int low_max = 0, high_max = 4;
for (int i = 1; i < 4; i++) if (energy[i] > energy[low_max]) low_max = i;
for (int i = 5; i < 8; i++) if (energy[i] > energy[high_max]) high_max = i;
// 调整阈值(16kHz时能量分布不同)
if (energy[low_max] > 50.0 && energy[high_max] > 50.0) {
char digit = get_dtmf_digit(low_max, high_max);
Serial.print("R["); Serial.print(digit); Serial.print("]");
// 只有当前得到的值和前一个不同才继续检测
// 避免多次识别到同一个数字的误判
if (digit != Previous) {
if (digit == DIGITALPATTEN[Index - 1]) {
Serial.print("I["); Serial.println(Index);
Index++;
Previous=digit;
} else {
if (digit == DIGITALPATTEN[0]) {
Index = 2;
Serial.print("P["); Serial.println(Index);
} else {
Index = 1;
Serial.print("Z["); Serial.println(Index);
}
Previous = 255;
}
if (Index == DIGITALPATTEN.length() + 1) {
Serial.println("Assert!");
digitalWrite(SPEAKCTRL,LOW);
Index = 1;
Previous = 255;
}
}
}
}
void process_samples() {
// 重置Goertzel状态
for (int i = 0; i < NUM_BANDS; i++) {
bands[i].q1 = bands[i].q2 = 0;
}
// 运行Goertzel算法
for (int n = 0; n < SAMPLE_BUFFER_SIZE; n++) {
float sample = samples[n] / 32768.0;
for (int i = 0; i < NUM_BANDS; i++) {
float q0 = bands[i].coeff * bands[i].q1 - bands[i].q2 + sample;
bands[i].q2 = bands[i].q1;
bands[i].q1 = q0;
}
}
// 计算能量
float energy[NUM_BANDS];
for (int i = 0; i < NUM_BANDS; i++) {
energy[i] = bands[i].q1 * bands[i].q1 + bands[i].q2 * bands[i].q2 - bands[i].coeff * bands[i].q1 * bands[i].q2;
}
// 检测DTMF数字
detect_dtmf(energy);
}
3.同样的,触发之后是在上述的 detect_dtmf 函数中处理的,只是一个简单的触发GPIO 为 Low的动作
最终的使用方法是:
- 生成铃声,我这里使用的是https://onlinesound.net/dtmf-generator 提供的功能,生成一个序列号用于识别。需要注意的是:必须是序列不能有连续的同一个数字,比如: 1123 ,这种无法识别。
- 将生成的铃声下载到手机中,可以作为通用铃声,也可以指定用户,这样只有指定人的来电会触发报警。
项目遇到的难点和解决方法
1.经过查找资料,确定了识别DTMF 音频的通用方法是Goertzel算法,代码中的这部分算法是通过AI 生成的,节省了大量时间提升了效率;
2.实践发现,提升单个 DTMF 音频持续时间(默认150ms),能够显著提升音频识别率。
报警器除了声音之外还有闪光报警,有兴趣的朋友可以直接观看工作的视频。