LD3320的工作原理
作成日: 2025年4月27日
作成日: 2025年4月27日
LD3320的工作原理
LD3320的工作原理
以下内容按“信号流-算法-接口”三个层面,把 LD3320 这颗离线语音识别 IC 的工作机理拆解说明,方便做原理理解或单片机移植。
环节 | 关键硬件 | 作用与特点 |
---|---|---|
① 音频采集 | 内置 Mic 前置放大 + 16-bit ADC(MIC P/N 引脚) | 直接接驻极体麦克风,不用额外 CODEC;采样率固定 8 kHz(ASR 模式)。 |
② 预处理 | DSP 前端 | 自动增益、噪声门、带通滤波,做端点检测,保证唤醒-关键字落在有效窗口内。 |
③ 特征提取 | DSP 专用指令 | 经典 MFCC 管线:窗函数→FFT→Mel 滤波→对数→DCT;特征维数 13 – 26。 |
④ 模型匹配 | 片内 24-bit DSP + ROM 模型 | 基于 HMM + Viterbi(官方只给“快速优化算法”描述)。最多一次装载 50 条候选句,每句≤10 汉字或 79 Byte 拼音。非特定人识别,无需训练,官方给出 ≈95 % 识别率。 |
⑤ 结果输出 | 中断 INTB* + 寄存器 B2/B3 | DSP 将命中句子的 Index 写入寄存器并触发 INTB* 低电平,MCU 读取后再清中断。 |
以上全部电路都集成在单芯片里,不需要外接 Flash/RAM/Codec,这也是 LD3320 相比 LD2450、HMC5883 那些早期方案最大的卖点。 CSDN Blog Electrodragon
模式 | 入口寄存器 | 主要流程 | 输出 |
---|---|---|---|
ASR(语音识别) | 0x71/0x04…06 | ① 写入关键词串 ➜ ② start ASR ➜ ③ 等待 INTB* | 命中条目索引 + 事件类型 |
MP3 播放 | FIFO 0x08 | MCU 通过并/串口把 MP3 框传进 FIFO ➜ DSP 解码 ➜ 片内 16-bit DAC ➜ SPO、HP 输出 | INTB* 在 Load/End 处中断 |
待机/休眠 | 0x17 位 7 | 关掉 PLL 与模拟前端,功耗降到 < 20 μA | – |
注意:ASR 与 MP3 互斥,切换模式前必须重置寄存器并重新跑时钟校准流程。 GitHub
完整握手示意(SPI 模式):
textMCU LD3320 ---- ------ CSB* ─────┐ ┌─────── SDCK ───┐ │ Addr │ ┌───── 先写地址 SDI --->├─┴────────▶│ │Data │ └───── 再写数据 └──────────▶│ (WRB* 并口同理) INTB* <────┐ 命中后拉低
优势 | 局限 |
---|---|
● 真正单芯片,外围只要时钟、电容、电阻; | ● 关键词数量受限(≤50),不能作连续语音或 NLU; |
● 中文、拼音、数字混搭,可动态修改; | ● 固定 8 kHz 采样,远场拾音效果一般; |
● 片内 DAC 带 0.55 W 功放,可推 8 Ω/0.5 W 小喇叭; | ● 算法模型封闭,无法自行训练或替换; |
● 功耗低,3 × AA 电池可用数月(休眠时); | ● ASR 与 MP3 不能同时启用,要切模式。 |
cld3320_reset(); // 1. 硬复位 ld3320_pll_calibrate(22_1184_MHz); // 2. 校准 PLL ld3320_set_mode(ASR); // 3. 进入 ASR ld3320_load_keywords(list, n); // 4. 写入关键词 ld3320_start_asr(); // 5. 下达识别开始命令 while(!int_flag) ; // 6. 等待 INTB* idx = ld3320_read_result(); // 7. 读取结果寄存器
绝大部分寄存器写操作只在初始化、关键词下发、模式切换时使用,识别运行过程中 MCU 几乎无负担,非常适合 51、STM32F0 等低端 MCU。 Electrodragon
LD3320 把模拟音频前端 + 特征提取 + HMM 识别引擎 + MP3 解码 + DAC 功放封装进 7 × 7 mm QFN-48 中,实现了“免训练、离线识别、动态字典编辑”的本地语音接口。理解其工作原理的要点是:
只要围绕这四点设计,就能在家电、灯控、玩具等场景里快速落地一个离线语音控制方案。
vTaskNotifyGiveFromISR(task3_key_handle,
&xNeedSwitch);
portYIELD_FROM_ISR(xNeedSwitch); 这两句有什么用
这俩语句是 “在中断里唤醒任务,并在需要时立刻切走” 的固定写法。
把它们拆开来看就明白了:
语句 | 作用 | 关键点 |
---|---|---|
vTaskNotifyGiveFromISR(task3_key_handle,&xNeedSwitch); | <br>1. 把 task3_key_handle 对应任务的 “任务通知值”加 1(或直接置 1,具体见 ulTaskNotifyTake /xTaskNotifyWait 的用法)。<br>2. 如果那个任务正因为等通知而阻塞,并且它的 优先级 ≥ 当前正在运行的任务,就把第二个参数 xNeedSwitch 设成 pdTRUE 。 | ✔ 只能在 ISR 中调用的 “…FromISR” 版本;<br>✔ xNeedSwitch 必须是局部变量,类型 BaseType_t 。 |
portYIELD_FROM_ISR(xNeedSwitch); | 根据 xNeedSwitch 的值 决定是否马上做一次“异常出口级”的上下文切换:<br>- pdTRUE → 触发 PendSV(在 Cortex-M)或等效软中断,让调度器在退出中断时立即切到优先级更高/同优先级的就绪任务(往往就是刚被唤醒的那个);<br>- pdFALSE → 什么也不做,照常返回到被中断的任务。 | ✔ 有些移植层把它定义成 portEND_SWITCHING_ISR() ,含义相同;<br>✔ 只有在 中断优先级 ≤ configMAX_SYSCALL_INTERRUPT_PRIORITY 的 ISR 里才能安全调用。 |
xNeedSwitch
就保持 pdFALSE
,省一次上下文切换开销。portYIELD_FROM_ISR()
处理。cvoid EXTI0_IRQHandler(void) { BaseType_t xNeedSwitch = pdFALSE; /* 1. 通知任务,有机会把 xNeedSwitch 置 TRUE */ vTaskNotifyGiveFromISR(task3_key_handle, &xNeedSwitch); /* 2. 根据 xNeedSwitch 决定是否 PendSV −> 立即切任务 */ portYIELD_FROM_ISR(xNeedSwitch); }
如果省掉第二句,调度器直到 下一个 SysTick 或别的可抢占点才会调度到
task3
,实时性变差。
所以,这两行配合使用才能保证:
不懂,在解释一下
再来一遍,这次把过程拆成三个小故事,每一步只说一件事——看完你就能知道那两行到底谁干了什么、为什么非要连着用。
vTaskNotifyGiveFromISR(...)
做的事:
- 把
task3_key_handle
代表的任务叫一声(把它的“通知计数 +1”)。- 如果那任务正堵在“门口”(也就是在
ulTaskNotifyTake()
等你的通知),函数顺便看一眼它的优先级:
- 比现在正在运行的任务高——把
xNeedSwitch
设成pdTRUE
(告诉操作系统“他比我更急”)。- 优先级低或一样——
xNeedSwitch
还是pdFALSE
。
你要记住两点:
...FromISR
结尾的 API 专门给中断服务函数用;普通任务里用的是不带 *FromISR 的版本。xNeedSwitch
只是个标志位,真正切不切走还没决定——这留给下一行。portYIELD_FROM_ISR(xNeedSwitch)
做的事:
- 看
xNeedSwitch
是不是pdTRUE
。- 如果是,就触发一次 PendSV(Cortex-M)或别的软中断,让 FreeRTOS 在“退出中断”的那个瞬间立刻换到优先级最高的就绪任务。
- 如果不是,就什么都不干,继续返回原来被打断的任务。
所以它的本质就是:“要不要马上让路?”
好处 | 原因 |
---|---|
ISR 足够短 | 中断里只发信号,不跑业务逻辑,避免长时间关抢占。 |
只在必要时上下文切换 | 低优先级的唤醒不触发切换,省一次开销。 |
满足 FreeRTOS 设计 | 所有 “可能解阻塞任务”的 ISR API 都让你自己传回 xNeedSwitch ,再用 portYIELD_FROM_ISR() 处理,这样代码清晰、可移植。 |
如果你把第二句删掉:
vTaskNotifyGiveFromISR()
portYIELD_FROM_ISR()
就这么简单:一行通知任务,一行决定要不要马上切过去。
ld3320的工作原理,串口版的
ld3320的工作原理,串口版的
下面说的“串口版 LD3320”指的是 “LD3320 芯片 + 一颗 8051/STM8 小 MCU + 电源/功放等外围” 做成的商品化小板。
LD3320 本身只提供并口或 SPI,之所以能直接接 UART,是因为这颗桥接 MCU 把底层寄存器操作都包了起来,再把结果/命令通过 TTL 串口转出去。
text┌───────────────┐ INT/并SPI ┌───────────┐ UART-TTL │ 麦克风 │──► 音频前端 ─► │ LD3320 │──► │ 51/STM8 │──► TXD └───────────────┘ │ (ASR) │ │ 桥接MCU│◄── RXD ▲ ▲DAC └───────────┘ └───────────┘ │ │ ▲ │ │ 8 Ω喇叭 │配置/控制 └─┴─────────────────────────播放(MP3)─────────────┘
步骤 | 串口主机发什么 | 桥接 MCU 做什么 | LD3320 内部 |
---|---|---|---|
上电复位 | – | 拉低 RST > 10 µs,做 PLL 校准 | 时钟锁定、FIFO 清零 |
装词表 | AT+DICT=开灯,关灯,… <br>或二进制帧 AA 55 0x01 N 字节… | 逐条把关键词写进 0x04~0x06 寄存器 | 把文本转 Pinyin,存入 ASR RAM |
开始识别 | AT+RUN (或 AA 55 0x02 00 ) | 置位 0x35,等待 INT | 寻找端点→MFCC→Viterbi |
命中 | (被动)接收 {"VoiceCommandCode":2} <br>或 AA 55 0xB1 00 02 CS | 读寄存器 B2/B3,查表 | 输出索引并拉低 INT |
MP3 播放 | AT+PLAY file.mp3 | 切换 MP3 模式,扒 SD 卡数据写 FIFO | 片内解码→DAC |
不同厂商固件的 协议格式不完全一致:有用纯 ASCII AT 指令的,也有用 0xAA 55 头 + CMD + CS 的二进制帧。无论格式怎样,核心动作都是“把词写进去→启动→等中断→读索引”。
字节 | 含义 |
---|---|
0 | 0xAA |
1 | 0x55 |
2 | CMD(0x01=写词,0x02=运行,0xB1=结果…) |
3 | LEN 高 |
4 | LEN 低 |
5… | 数据负载 |
末 | 校验(累加和 & 0xFF) |
这套帧结构在开源 STM32 演示工程里也能看到:收到 0xB1 就解析第 6 字节的索引,用
cJSON
或switch
分发动作。eet-china.com CSDN Blog
实时性
串口版靠桥接 MCU 轮询 LD3320 INT,再发 UART;比直连并口最快也就多 1~2 ms,家电级应用足够。
词条限制照旧
底层还是 LD3320——一次最多 50 条,最长 10 字;想做连续语音/自训练就得换芯。
升级固件
模块经常用 STC15/8,拿厂商的 HEX 重新烧录就能改协议或字库;实测把波特率改成 115200 也没问题。CSDN Blog
cvoid uart_isr(void) // 串口中断 { static uint8_t buf[32], p=0; uint8_t b = USART_RXD(); if(p<sizeof buf) buf[p++]=b; if(p>=4 && buf[p-1]==checksum_ok(buf,p)){ // 收满一帧 if(buf[2]==0xB1){ // 结果帧 uint8_t id = buf[5]; // 识别索引 switch(id){ case 1: open_light(); break; case 2: close_light();break; } } p=0; // 继续收下一帧 } } int main(){ uart_init(9600); send_cmd("AT+DICT=开灯,关灯"); send_cmd("AT+RUN"); while(1); // 剩下都在中断里 }
拿到手只要接 VCC、GND、TX、RX 四线,很快就能把开灯、关门之类的离线语音命令跑起来。
bh1750的工作原理