ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

IoT-For-Beginners 智能语音计时器:Wio Terminal 基于 DMAC 与 Flash 的音频采集实战

IoT-For-Beginners 智能语音计时器:Wio Terminal 基于 DMAC 与 Flash 的音频采集实战 IoT-For-Beginners 智能语音计时器Wio Terminal 基于 DMAC 与 Flash 的音频采集实战【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners本指南聚焦 GitHub Trending 项目 IoT-For-Beginners 中消费者物联网Consumer IoT项目的语音识别课程6-consumer/lessons/1-speech-recognition完整讲解如何在 Wio Terminal 上编写 C 代码采集麦克风音频利用 4MB 板载 Flash 弥补 192KB RAM 的不足通过直接内存访问控制器DMAC 双缓冲实现 16KHz 精确定时采样并将音频以 WAV 格式写入 Flash为后续语音转文本Speech to Text奠定数据基础。读完本文你将掌握 SFUD Flash 写入封装、DMA/ADC/定时器寄存器级配置、WAV 头结构与 Wio Terminal 按键触发录音的完整实现可直接运行于你的smart-timer项目。为什么 Wio Terminal 采集音频需要特殊方案Wio Terminal 板载麦克风将模拟声波信号转换为数字信号供处理器使用。但采集音频面临两个硬性约束RAM 极其有限Wio Terminal 仅有192KB RAM无法缓存超过几秒的未压缩音频。按照 16KHz、16-bit即每秒 32KB 数据计算即便程序本身很小RAM 也无法支撑超过约 5 秒的录音。Flash 充足Wio Terminal 拥有4MB Flash 存储可用来持久化保存采集到的音频数据。因此正确的做法是边采集边写 Flash数据流经缓冲区直接落盘而不是整体驻留内存。这正是本课smart-timer项目的核心设计思想最终代码位于仓库 code-record/wio-terminal/smart-timer。精确定时采样为什么不能靠 CPU 轮询录制 16KHz 音频意味着每秒必须等间隔采样 16,000 次每次采样间隔必须绝对均匀。若依赖 CPU 代码循环任何中断或调度抖动都会导致采样失真。解决方案是使用DMACDirect Memory Access Controller直接内存访问控制器——一个能在不打断处理器执行代码的前提下将信号从外设搬移到内存的硬件电路。DMAC 可以按固定间隔如每秒 16,000 次从 ADC 读取音频并写入预分配的内存缓冲区。为避免单缓冲带来的录音延迟教程采用双缓冲double bufferingDMAC 写入缓冲 1满了后通知你的代码处理缓冲 1同时 DMAC 转写缓冲 2缓冲 2 满后再次切换回缓冲 1。只要每个缓冲的处理时间小于其被填满的时间就不会丢失任何数据。Flash 的粒度约束Flash 写入有**粒度granularity**限制擦除和写入操作必须以固定尺寸对齐。例如粒度为 4096 字节时请求擦除地址 4200 处实际可能擦除 40968192 的整块数据。因此音频数据必须以正确尺寸的块写入 Flash这正是后续FlashWriter类使用与擦除粒度等大的内存缓冲的原因。任务一配置 Flash 内存创建项目并添加库依赖使用 PlatformIO 新建 Wio Terminal 项目命名为smart-timer在setup函数中配置串口。然后在 platformio.ini 中添加如下依赖[env:seeed_wio_terminal] platform atmelsam board seeed_wio_terminal framework arduino lib_deps seeed-studio/Seeed Arduino FS 2.1.1 seeed-studio/Seeed Arduino SFUD 2.0.2 build_flags -DSFUD_USING_QSPI两个库的作用Seeed Arduino SFUDSerial Flash Universal Driver一个面向多种 Flash 芯片的统一驱动库本项目的 Flash 读写全部经由它完成Seeed Arduino FS文件系统支持库为后续课程中从 Flash 读取音频文件提供基础。注意build_flags中的-DSFUD_USING_QSPIWio Terminal 板载 Flash 支持 QSPIQueued SPI访问这种 SPI 控制器变体允许通过队列进行连续访问处理器占用极小可显著加快 Flash 的读写速度。初始化 SFUD 并启用 QSPI 快速读取在 main.cpp 顶部引入头文件#include Arduino.h #include sfud.h #include SPI.h在setup函数中初始化 Flash 存储库Serial.begin(9600); while (!Serial) ; // 等待串口就绪 delay(1000); while (!(sfud_init() SFUD_SUCCESS)) ; // 循环直到 SFUD 初始化成功 sfud_qspi_fast_read_enable(sfud_get_device(SFUD_W25Q32_DEVICE_INDEX), 2);sfud_init()会探测并初始化 Flash 芯片代码用空循环阻塞直到返回SFUD_SUCCESS。随后sfud_qspi_fast_read_enable对板载 W25Q32 Flash 启用 QSPI 快速读取模式参数2为线数2 线 QSPI。这两行必须在任何 Flash 读写之前完成。编写 FlashWriter 写入器在src文件夹新建 flash_writer.h头部包含必要头文件#pragma once #include Arduino.h #include sfud.h定义FlashWriter类。私有字段用于管理写 Flash 前的内存缓冲class FlashWriter { public: private: byte *_sfudBuffer; size_t _sfudBufferSize; size_t _sfudBufferPos; size_t _sfudBufferWritePos; const sfud_flash *_flash; };字段含义_sfudBuffer内存缓冲字节数组数据先写入这里满后一次性写入 Flash_sfudBufferSize缓冲大小取自 Flash 的擦除粒度_sfudBufferPos缓冲内的当前写入位置_sfudBufferWritePosFlash 中的写入位置偏移量_flash指向 Flash 设备的指针部分微控制器有多颗 Flash 芯片这里取设备表中的第一颗。init()方法按擦除粒度分配缓冲void init() { _flash sfud_get_device_table() 0; _sfudBufferSize _flash-chip.erase_gran; _sfudBuffer new byte[_sfudBufferSize]; _sfudBufferPos 0; _sfudBufferWritePos 0; }它必须放在init方法而非构造函数中因为必须先执行sfud_init()才能取得 Flash 设备信息而sfud_init()是在setup函数里调用的。写入与冲刷方法void writeSfudBuffer(byte b) { _sfudBuffer[_sfudBufferPos] b; if (_sfudBufferPos _sfudBufferSize) { sfud_erase_write(_flash, _sfudBufferWritePos, _sfudBufferSize, _sfudBuffer); _sfudBufferWritePos _sfudBufferSize; _sfudBufferPos 0; } } void writeSfudBuffer(byte *b, size_t len) { for (size_t i 0; i len; i) { writeSfudBuffer(b[i]); } } void flushSfudBuffer() { if (_sfudBufferPos 0) { sfud_erase_write(_flash, _sfudBufferWritePos, _sfudBufferSize, _sfudBuffer); _sfudBufferWritePos _sfudBufferSize; _sfudBufferPos 0; } }writeSfudBuffer(byte b)逐字节填充内存缓冲缓冲区满时调用sfud_erase_write一次性擦写一整块到 Flash自动擦除该位置旧数据然后推进 Flash 写位置并复位缓冲游标。writeSfudBuffer(byte *b, size_t len)是批量写入的便捷封装。flushSfudBuffer()用于收尾由于录音总字节数往往不是擦除粒度的整数倍缓冲尾部的残量需要主动冲刷。末尾可能多写一些无关字节但这无碍——后续读取时只需取用需要的长度即可。reset()方法则把两个游标归零为下一次录音复用同一块 Flash 区域。任务二搭建音频采集定义采集参数 config.h在src文件夹新建 config.h定义录音常量#pragma once #define RATE 16000 #define SAMPLE_LENGTH_SECONDS 4 #define SAMPLES RATE * SAMPLE_LENGTH_SECONDS #define BUFFER_SIZE (SAMPLES * 2) 44 #define ADC_BUF_LEN 1600各常量含义如下表| 常量 | 值 | 说明 | | --------------------- | ------: | - | | RATE | 16000 | 采样率16,000 即 16KHz | | SAMPLE_LENGTH_SECONDS | 4 | 录音时长秒设为 4录制更长音频可增大此值 | | SAMPLES | 64000 | 总采样数 采样率 × 秒数 | | BUFFER_SIZE | 128044 | 音频缓冲总大小WAV 44 字节头 128,000 字节音频数据每采样 2 字节 | | ADC_BUF_LEN | 1600 | DMAC 采集音频所用的缓冲区长度 |由于所有宏均由RATE与SAMPLE_LENGTH_SECONDS推导若觉得 4 秒太短只需改大SAMPLE_LENGTH_SECONDS其余值会自动重算。封装 Mic 类与 DMA 配置在src文件夹新建 mic.h包含config.h与flash_writer.h。核心是Mic类用volatile bool标记录音状态持有FlashWriter _writerclass Mic { public: Mic() { _isRecording false; _isRecordingReady false; } void startRecording() { _isRecording true; _isRecordingReady false; } bool isRecording() { return _isRecording; } bool isRecordingReady() { return _isRecordingReady; } private: volatile bool _isRecording; volatile bool _isRecordingReady; FlashWriter _writer; }; Mic mic;DMAC 会持续向内存缓冲写入数据_isRecording标志决定这些数据是否需要处理录满 4 秒后_isRecordingReady置位。类外声明全局实例Mic mic;。configureDmaAdc寄存器级底层配置Mic的私有区定义了 DMA 描述符结构体与三组描述符存储均 16 字节对齐并实现了 configureDmaAdc()typedef struct { uint16_t btctrl; uint16_t btcnt; uint32_t srcaddr; uint32_t dstaddr; uint32_t descaddr; } dmacdescriptor; volatile dmacdescriptor _wrb[DMAC_CH_NUM] __attribute__((aligned(16))); dmacdescriptor _descriptor_section[DMAC_CH_NUM] __attribute__((aligned(16))); dmacdescriptor _descriptor __attribute__((aligned(16)));configureDmaAdc()的工作分四步配置 DMAC设置描述符基址DMAC-BASEADDR、写回描述符基址DMAC-WRBADDR使能 DMAC并将通道 1 的触发源设为 TC5 定时器溢出DMAC_CHCTRLA_TRIGSRC(TC5_DMAC_ID_OVF)传输方式为突发传输DMAC_CHCTRLA_TRIGACT_BURST。构建循环描述符两个描述符互为 nextdescaddr互相指向对方源地址固定为 ADC1 结果寄存器ADC1-RESULT.reg目的地址分别在_adc_buf_0与_adc_buf_1btcnt ADC_BUF_LENbtctrl设为 16-bit 半字HWORD、目的地址自增、描述符有效、块传输完成后挂起SUSPEND。由此形成写满 A → 挂起中断 → 恢复后写 B → 挂起中断的乒乓循环。配置 NVIC 与中断NVIC_SetPriority(DMAC_1_IRQn, 0)将 DMAC1 中断优先级设为最高NVIC_EnableIRQ(DMAC_1_IRQn)使能中断再使能通道 1 的 SUSP 挂起中断。配置 ADCMUXPOS选择板载麦克风模拟输入SAMPLEN 0x00设定采样时间CTRLA预分频 12848MHz/128 375kHzCTRLB设为 12 位分辨率并开启自由运行模式FREERUN随后使能 ADC 并软件触发首次转换。配置 TC5 定时器使能 TC5 外设时钟48MHz设为 Match FrequencyMFRQ模式TC5-COUNT16.CC[0].reg 3000 - 1即4MHz/16000 - 1产生精确的 16kHz 触发脉冲最后使能定时器。两个 DMA 缓冲定义在函数之后uint16_t _adc_buf_0[ADC_BUF_LEN]; uint16_t _adc_buf_1[ADC_BUF_LEN];这段代码直接操作 SAMD51 寄存器DMAC-...、ADC1-...、TC5-...比桌面或单板计算机上的代码复杂得多——因为没有操作系统辅助代码必须运行在最底层与硬件直接交互。教程原样注释保留了每个寄存器的用途便于对照数据手册。WAV 文件头与音频回调WAV 文件由 44 字节头部 原始音频数据构成。教程将头部建模为结构体struct wavFileHeader { char riff[4]; /* RIFF */ long flength; /* file length in bytes */ char wave[4]; /* WAVE */ char fmt[4]; /* fmt */ long chunk_size; /* size of FMT chunk in bytes (usually 16) */ short format_tag; /* 1PCM, 257Mu-Law, 258A-Law, 259ADPCM */ short num_chans; /* 1mono, 2stereo */ long srate; /* Sampling rate in samples per second */ long bytes_per_sec; /* bytes per second srate*bytes_per_samp */ short bytes_per_samp; /* 216-bit mono, 416-bit stereo */ short bits_per_samp; /* Number of bits per sample */ char data[4]; /* data */ long dlength; /* data length in bytes (filelength - 44) */ };initBufferHeader()填充头部并写入 Flashformat_tag 1PCM、num_chans 1单声道、srate RATE、bytes_per_samp 2、bits_per_samp 16、bytes_per_sec RATE * 16 / 8最后通过_writer.writeSfudBuffer((byte *)wavh, 44)将 44 字节头部写入 Flash。audioCallback是音频处理的核心回调void audioCallback(uint16_t *buf, uint32_t buf_len) { static uint32_t idx 44; if (_isRecording) { for (uint32_t i 0; i buf_len; i) { int16_t audio_value ((int16_t)buf[i] - 2048) * 16; _writer.writeSfudBuffer(audio_value 0xFF); _writer.writeSfudBuffer((audio_value 8) 0xFF); } idx buf_len; if (idx BUFFER_SIZE) { _writer.flushSfudBuffer(); idx 44; _isRecording false; _isRecordingReady true; } } }关键转换逻辑ADC 返回12 位无符号值01023需转成 16 位有符号 PCM——先减去 204812 位中点偏移再乘 16 放大到 16 位动态范围最后按小端序拆成低字节与高字节写入 Flash。写入从索引 44 开始正是 WAV 头的偏移。idx累积到BUFFER_SIZE时冲刷 Flash、复位索引并置位_isRecordingReady宣告一段 4 秒录音完成。中断处理与初始化/复位dmaHandler()检查 DMAC 通道 1 的 SUSP 标志先写CHCTRLB.reg DMAC_CHCTRLB_CMD_RESUME恢复传输并清除标志再根据count奇偶交替把_adc_buf_0或_adc_buf_1交给audioCallback处理。类外的中断入口void DMAC_1_Handler() { mic.dmaHandler(); }DMAC_1_Handler按名称被中断向量表自动识别无需显式注册。Mic的init()与reset()方法void init() { analogReference(AR_INTERNAL2V23); _writer.init(); initBufferHeader(); configureDmaAdc(); } void reset() { _isRecordingReady false; _isRecording false; _writer.reset(); initBufferHeader(); }init()将 ADC 参考电压设为内部 2.23V匹配麦克风信号幅度、初始化 FlashWriter、写入 WAV 头并启动 DMA 采集reset()在录音被处理后复位状态、重置 Flash 写位置并重新写入头部为下一轮录音做准备。任务三用按钮触发录音回到 main.cpp在setup中初始化 C 按钮与麦克风pinMode(WIO_KEY_C, INPUT_PULLUP); mic.init(); Serial.println(Ready.);WIO_KEY_C是 Wio Terminal 顶部三个按钮中位于左侧靠近电源开关的那个INPUT_PULLUP使其按下时读到LOW。定义暂空的音频处理函数void processAudio() { // 本课留空后续课程将在此调用语音转文本服务 }loop中的状态机驱动整段录音流程void loop() { if (digitalRead(WIO_KEY_C) LOW !mic.isRecording()) { Serial.println(Starting recording...); mic.startRecording(); } if (!mic.isRecording() mic.isRecordingReady()) { Serial.println(Finished recording); processAudio(); mic.reset(); } }逻辑闭环按下 C 按钮且未在录音 →startRecording()置位_isRecording此后audioCallback持续把 DMA 缓冲写入 Flash4 秒录满后_isRecordingReady置位、_isRecording复位loop检测到未录音且已就绪时调用processAudio()本课为空实现后续课程将在此把 Flash 中的 WAV 数据发往 Azure Speech Service 做语音转文本随后mic.reset()复位等待下一次按键。构建并上传到 Wio Terminal打开串口监视器波特率 9600按下 C 按钮开始说话设备将录制 4 秒音频并写入 Flash串口输出大致如下Ready. Starting recording... Finished recording完整可运行代码位于仓库 code-record/wio-terminal/smart-timer若你想继续完成语音转文本链路可参考同一课程下的 wio-terminal-speech-to-text.md 与进阶版代码 code-speech-to-text/wio-terminal/smart-timer其中flash_stream.h展示了如何从 Flash 流式读取录音。延伸与调试建议调节录音时长只改config.h中的SAMPLE_LENGTH_SECONDSSAMPLES、BUFFER_SIZE会自动重算注意更长录音会占用更多 Flash 空间4 秒约 128KB192KB RAM 完全绕开但 4MB Flash 足够存放约 30 段录音。内存对齐DMA 描述符使用__attribute__((aligned(16)))强制 16 字节对齐这是 SAMD51 DMAC 硬件要求不可省略。录音失真排查若回放音频有噪声或音量异常优先检查analogReference(AR_INTERNAL2V23)参考电压设置与audioCallback中 12 位到 16 位的换算-2048与*16。前后台分工录音期间主循环不参与采样只做按键检测与状态翻转采样完全由 TC5 定时器 DMAC 中断驱动这正是保证 16KHz 采样精度的关键。本课实现的按键触发录音 → DMAC 双缓冲 → Flash 持久化流水线是后续把语音送入云端识别详见 README.md 的 Speech to text 章节的可靠数据底座也为 TinyML 唤醒词等边缘场景的音频处理提供了可复用的工程范式。【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表