ARTICLE DETAIL

资讯详情

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

IoT-For-Beginners 实战:Wio Terminal 麦克风音频采集——DMAC 双缓冲与 Flash 直写的 C++ 实现

IoT-For-Beginners 实战:Wio Terminal 麦克风音频采集——DMAC 双缓冲与 Flash 直写的 C++ 实现 IoT-For-Beginners 实战Wio Terminal 麦克风音频采集——DMAC 双缓冲与 Flash 直写的 C 实现【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners本篇文章是 IoT-For-Beginners 项目消费级智能设备6-consumer第 1 课用 IoT 设备识别语音的 Wio Terminal 硬件分支教程完整讲解如何用 C 在 Wio Terminal 上编写麦克风音频采集程序利用 ADC DMAC直接内存访问控制器定时采样、双缓冲轮转存储并将 PCM 音频以 WAV 格式写入板载 4MB Flash。学完本文你将掌握微控制器上无操作系统环境下直接操作寄存器采集音频、规避 RAM 容量瓶颈的完整工程实现为后续接入语音转文本服务打下数据基础。Wio Terminal 的音频采集挑战192KB RAM 与 4MB FlashWio Terminal 内置 MEMS 麦克风将声波振动转化为模拟电信号再由片内 ADC模数转换器转换为数字信号。与树莓派等单板计算机不同Wio Terminal 只有192KB RAM不足以缓存超过几秒钟的未压缩音频但它同时拥有4MB 板载 Flash可以替代 RAM 作为音频数据的落盘介质。先算一笔账参考 课程 README 的音频数据大小小节以 16-bit、16KHz 的采样规格采集语音每秒数据量为16-bit 每样本 2 字节16KHz 每秒 16,000 个样本16,000 × 2 字节 32,000 字节/秒约 32KB/s。即便程序代码占用极小192KB RAM 也撑不过 5 秒的录音。因此正确的做法是将麦克风采到的音频直接写入 Flash而不是在内存中堆积整段数据。这正是本课工程smart-timer的核心设计思路。采样时序与 DMAC用硬件时钟保证 16KHz 采样率采集音频对时序极其敏感要采集 16KHz 音频就必须精确地每秒采样 16,000 次且每次采样间隔严格相等。用普通的loop()代码很难保证这种确定性。解决办法是使用DMACDirect Memory Access Controller直接内存访问控制器——这是一种独立于 CPU 的电路能按固定间隔从外设如 ADC抓取信号并写入内存整个过程不打断处理器的运行。DMAC 采集流程如下内置麦克风的模拟信号进入ADC被转换为数字值DMAC 由定时器/计数器触发本例为 TC5 溢出事件以 16,000 次/秒的节奏从 ADC 结果寄存器搬移数据数据被写入预先分配的内存缓冲缓冲写满后交由你的代码处理。单缓冲有一个致命问题处理缓冲期间会错过新采样。解决方案是双缓冲double bufferingDMAC 先写缓冲 1写满后通知代码处理缓冲 1同时 DMAC 转向写缓冲 2缓冲 2 写满后再次通知代码DMAC 又回到写缓冲 1。只要处理一个缓冲的时间小于 DMAC 填满一个缓冲的时间数据就不会丢失。本课代码正是基于这个乒乓轮转模型实现的。Flash 的粒度约束擦写必须按块对齐音频数据最终要落到 Flash。与内存不同Flash 的擦除与写入必须使用指定地址、指定长度且按固定大小对齐——这个固定大小被称为粒度granularity。例如粒度是 4096 字节如果你请求在地址 4200 处擦除硬件可能实际擦除 40968192 的整段数据。因此写入 Flash 的音频数据必须以正确大小的分块进行这也是FlashWriter类存在的意义。任务一配置 Flash 存储创建工程并添加依赖用 PlatformIO 新建一个名为smart-timer的 Wio Terminal 工程board seeed_wio_terminal并在setup中配置串口。在 platformio.ini 中添加两个库依赖以访问板载 Flash[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-studio/Seeed Arduino SFUD 2.0.2提供 SFUDSerial Flash Universal Driver串行 Flash 通用驱动库它被设计为兼容各类 Flash 芯片仓库中的 platformio.ini 还通过build_flags -DSFUD_USING_QSPI显式启用 QSPI 模式。初始化 SFUD 并开启 QSPI 快速读在 main.cpp 顶部引入头文件#include Arduino.h #include sfud.h #include SPI.h然后在setup()中初始化 SFUDwhile (!(sfud_init() SFUD_SUCCESS)) ; sfud_qspi_fast_read_enable(sfud_get_device(SFUD_W25Q32_DEVICE_INDEX), 2);第一段循环阻塞等待 SFUD 初始化完成sfud_qspi_fast_read_enable打开快速读功能。板载 Flash 通过QSPIQueued Serial Peripheral Interface队列式串行外设接口访问——这是一种带队列的 SPI 控制器能以极低的处理器占用实现连续访问从而加快 Flash 读写。实现FlashWriter按粒度分块的 Flash 写入器在src下新建flash_writer.h定义FlashWriter类。完整实现见仓库 flash_writer.h。其私有字段byte *_sfudBuffer; // 内存缓冲写满后整块落 Flash size_t _sfudBufferSize; // 缓冲大小 Flash 擦除粒度 size_t _sfudBufferPos; // 缓冲内当前写入位置 size_t _sfudBufferWritePos; // 下次写入 Flash 的起始地址 const sfud_flash *_flash; // 指向 Flash 设备的指针部分 MCU 有多块 Flashinit()获取 Flash 设备表首项并按该芯片的erase_gran擦除粒度动态分配缓冲void init() { _flash sfud_get_device_table() 0; _sfudBufferSize _flash-chip.erase_gran; _sfudBuffer new byte[_sfudBufferSize]; _sfudBufferPos 0; _sfudBufferWritePos 0; }说明初始化放在init()方法而非构造函数中是因为它必须在setup()完成 SFUD 初始化之后才可调用。写入逻辑的核心是writeSfudBuffer(byte b)先写内存缓冲缓冲写满后调用sfud_erase_write一次性擦写整块 Flash同时擦除该位置旧数据再推进写入地址void writeSfudBuffer(byte b) { _sfudBuffer[_sfudBufferPos] b; if (_sfudBufferPos _sfudBufferSize) { sfud_erase_write(_flash, _sfudBufferWritePos, _sfudBufferSize, _sfudBuffer); _sfudBufferWritePos _sfudBufferSize; _sfudBufferPos 0; } }另有两个配套方法批量写入重载writeSfudBuffer(byte *b, size_t len)逐字节循环调用flushSfudBuffer()负责把未写满的尾部缓冲强制落盘——因为录音数据几乎不可能是粒度的整数倍最后一段必须显式 flush。尾部写入会产生少量多余数据但后续只读取需要的部分因此可接受。reset()则把缓冲位置与 Flash 写入位置都归零供每次录音结束后的重新录制使用void reset() { _sfudBufferPos 0; _sfudBufferWritePos 0; }任务二设置音频采集定义采集常量config.h在src下新建config.h仓库中该常量位于 mic.h 顶部定义录音规格#pragma once #include Arduino.h #include flash_writer.h #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各常量含义如下常量值说明RATE16000采样率16,000 即 16KHzSAMPLE_LENGTH_SECONDS4录音时长秒。如需更长的定时器请求增大此值即可其余值会自动重算SAMPLES64000总样本数 采样率 × 秒数BUFFER_SIZE128044音频缓冲总大小WAV 头 44 字节 128,000 字节音频数据每样本 2 字节ADC_BUF_LEN1600DMAC 采集使用的每个缓冲的元素个数定义Mic类录音状态机在mic.h中定义Mic类并声明全局实例Mic mic;见仓库 mic.h。公开接口的核心是两个状态标志class 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; };_isRecordingDMAC 一直在往内存缓冲写数据该标志决定audioCallback是处理还是忽略这些缓冲_isRecordingReady采满 4 秒后置位通知主循环音频就绪两个标志均声明为volatile因为它们会在中断上下文中被改写_writer是FlashWriter实例负责把数据持久化到 Flash。配置 DMAC ADC寄存器级的configureDmaAdc这是本工程最硬核的部分完整代码见 mic.h。它以寄存器操作为主要点拆解如下1) DMA 描述符与循环缓冲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)));定义了两个 16 位对齐的描述符数组_descriptor_section存放实际使用的描述符_wrb存放写回描述符。两个描述符通过descaddr互相指向对方构成环形描述符链实现缓冲 1 与缓冲 2 的自动轮换。2) 通道触发与传输属性DMAC-BASEADDR.reg (uint32_t)_descriptor_section; DMAC-WRBADDR.reg (uint32_t)_wrb; DMAC-CTRL.reg DMAC_CTRL_DMAENABLE | DMAC_CTRL_LVLEN(0xf); DMAC-Channel[1].CHCTRLA.reg DMAC_CHCTRLA_TRIGSRC(TC5_DMAC_ID_OVF) | // 由 TC5 定时器溢出触发 DMAC_CHCTRLA_TRIGACT_BURST; // 突发传输DMAC 通道 1 以TC5 定时器溢出作为触发源每溢出一次执行一次突发搬移。3) 两个描述符分别指向两个 ADC 缓冲第一个描述符把数据从ADC1-RESULT.reg搬到_adc_buf_0末尾dstaddr为数组首地址 长度 × 2 字节DMAC 的DSTINC会让目标地址递增_descriptor.descaddr (uint32_t)_descriptor_section[1]; // 指向下一个描述符环形 _descriptor.srcaddr (uint32_t)ADC1-RESULT.reg; // 数据源ADC 结果寄存器 _descriptor.dstaddr (uint32_t)_adc_buf_0 sizeof(uint16_t) * ADC_BUF_LEN; // 目标缓冲 0 _descriptor.btcnt ADC_BUF_LEN; // 节拍数转移数量 _descriptor.btctrl DMAC_BTCTRL_BEATSIZE_HWORD | // 节拍宽度 16 位 DMAC_BTCTRL_DSTINC | // 目标地址递增 DMAC_BTCTRL_VALID | // 描述符有效 DMAC_BTCTRL_BLOCKACT_SUSPEND; // 块传输完成后挂起 memcpy(_descriptor_section[0], _descriptor, sizeof(_descriptor));第二个描述符把descaddr指回_descriptor_section[0]目标换成_adc_buf_1完成环形链_descriptor.descaddr (uint32_t)_descriptor_section[0]; _descriptor.srcaddr (uint32_t)ADC1-RESULT.reg; _descriptor.dstaddr (uint32_t)_adc_buf_1 sizeof(uint16_t) * ADC_BUF_LEN; ... memcpy(_descriptor_section[1], _descriptor, sizeof(_descriptor));4) 中断与 ADC 配置NVIC_SetPriority(DMAC_1_IRQn, 0); // DMAC1 中断优先级设为 0最高 NVIC_EnableIRQ(DMAC_1_IRQn); // 挂接到 NVIC DMAC-Channel[1].CHINTENSET.reg DMAC_CHINTENSET_SUSP; // 启用挂起SUSP中断ADC 配置要点输入通道选择AIN12采样保持时长取SAMPLEN 0x00约 2.66μs时钟预分频PRESCALER_DIV12848MHz/128 375kHz分辨率 12 位FREERUN自由运行模式最后软件触发启动转换并等待同步ADC1-INPUTCTRL.bit.MUXPOS ADC_INPUTCTRL_MUXPOS_AIN12_Val; while (ADC1-SYNCBUSY.bit.INPUTCTRL) ; ADC1-SAMPCTRL.bit.SAMPLEN 0x00; while (ADC1-SYNCBUSY.bit.SAMPCTRL) ; ADC1-CTRLA.reg ADC_CTRLA_PRESCALER_DIV128; ADC1-CTRLB.reg ADC_CTRLB_RESSEL_12BIT | ADC_CTRLB_FREERUN; while (ADC1-SYNCBUSY.bit.CTRLB) ; ADC1-CTRLA.bit.ENABLE 1; while (ADC1-SYNCBUSY.bit.ENABLE) ; ADC1-SWTRIG.bit.START 1; while (ADC1-SYNCBUSY.bit.SWTRIG) ;5) TC5 定时器产生 16KHz 采样节拍GCLK-PCHCTRL[TC5_GCLK_ID].reg GCLK_PCHCTRL_CHEN | GCLK_PCHCTRL_GEN_GCLK1; // 连接 48MHz 通用时钟 TC5-COUNT16.WAVE.reg TC_WAVE_WAVEGEN_MFRQ; // 匹配频率MFRQ模式 TC5-COUNT16.CC[0].reg 3000 - 1; // 触发频率 16kHz源注释为 (4Mhz / 16000) - 1 while (TC5-COUNT16.SYNCBUSY.bit.CC0) ; TC5-COUNT16.CTRLA.bit.ENABLE 1; // 启动定时器TC5 以匹配频率模式运行其比较值决定了采样节拍源码注释中给出的公式为(4Mhz / 16000) - 1 3000 - 1DMAC 通道 1 正是在 TC5 每个溢出周期被触发一次从而以 16KHz 的节奏从 ADC 抓取样本。 这段代码比单板计算机上的实现复杂得多因为微控制器没有操作系统帮忙代码运行在极低层级、直接操作硬件寄存器。这也是嵌入式开发的典型代价社区虽有简化库但底层细节仍无法回避。WAV 文件头wavFileHeader与initBufferHeader麦克风采集的是裸 PCM 数据为了后续能被语音服务读取需要按WAV 容器格式封装。WAV 文件 44 字节文件头 原始音频数据。代码用结构体定义头部见 mic.hstruct wavFileHeader { char riff[4]; /* RIFF */ long flength; /* 文件总长度字节 */ char wave[4]; /* WAVE */ char fmt[4]; /* fmt */ long chunk_size; /* FMT 块大小通常为 16 */ short format_tag; /* 1PCM, 257Mu-Law, 258A-Law, 259ADPCM */ short num_chans; /* 1单声道, 2立体声 */ long srate; /* 采样率每秒样本数 */ long bytes_per_sec; /* 每秒字节数 srate * bytes_per_samp */ short bytes_per_samp; /* 216-bit 单声道, 416-bit 立体声 */ short bits_per_samp; /* 每个样本的位数 */ char data[4]; /* data */ long dlength; /* 数据长度文件长度 - 44 */ };initBufferHeader()填充各字段并写入 Flashvoid initBufferHeader() { wavFileHeader wavh; strncpy(wavh.riff, RIFF, 4); strncpy(wavh.wave, WAVE, 4); strncpy(wavh.fmt, fmt , 4); strncpy(wavh.data, data, 4); wavh.chunk_size 16; wavh.format_tag 1; // PCM wavh.num_chans 1; // 单声道 wavh.srate RATE; wavh.bytes_per_sec (RATE * 1 * 16 * 1) / 8; wavh.bytes_per_samp 2; wavh.bits_per_samp 16; wavh.dlength RATE * 2 * 1 * 16 / 2; wavh.flength wavh.dlength 44; _writer.writeSfudBuffer((byte *)wavh, 44); }可见本工程录音规格为16KHz / 16-bit / 单声道 PCM头 44 字节被FlashWriter最先写入 Flash随后才是音频数据。音频回调audioCallback与数据变换当 DMAC 填满一个缓冲并挂起时中断会触发audioCallback(uint16_t *buf, uint32_t buf_len)。其内部把 ADC 的12 位无符号值01023 区间注释原文如此转换为16 位有符号值再拆成高低两个字节写入 Flashvoid 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; } } }关键点静态变量idx从44起计数——这是为了跳过已写入的 WAV 头字节数只有当_isRecording true时才落盘否则直接忽略缓冲当累计字节数达到BUFFER_SIZE128,044时先flushSfudBuffer()写掉尾部残余再把_isRecording复位、_isRecordingReady置位宣告 4 秒音频采集完成。中断入口dmaHandler与DMAC_1_HandlerdmaHandler()检查通道 1 的挂起标志SUSP先恢复传输DMAC_CHCTRLB_CMD_RESUME并清除标志再根据计数奇偶交替把_adc_buf_0/_adc_buf_1交给audioCallbackvoid dmaHandler() { static uint8_t count 0; if (DMAC-Channel[1].CHINTFLAG.bit.SUSP) { DMAC-Channel[1].CHCTRLB.reg DMAC_CHCTRLB_CMD_RESUME; DMAC-Channel[1].CHINTFLAG.bit.SUSP 1; if (count) { audioCallback(_adc_buf_0, ADC_BUF_LEN); } else { audioCallback(_adc_buf_1, ADC_BUF_LEN); } count (count 1) % 2; } }中断向量函数按名称约定实现只需存在即可被调用void DMAC_1_Handler() { mic.dmaHandler(); }init与reset启动与复位Mic::init()依次完成设置麦克风引脚模拟参考电压AR_INTERNAL2V23、初始化 Flash 写入器、写入 WAV 头、配置 DMAC/ADC。reset()则在录音使用完毕后复位标志与 Flash 写入位置并重新写入 WAV 头为下一次录音做好准备void init() { analogReference(AR_INTERNAL2V23); _writer.init(); initBufferHeader(); configureDmaAdc(); } void reset() { _isRecordingReady false; _isRecording false; _writer.reset(); initBufferHeader(); }任务三编写主程序采集音频setup串口、Flash、按钮与麦克风初始化在 main.cpp 中void setup() { Serial.begin(9600); while (!Serial) ; // 等待串口就绪 delay(1000); while (!(sfud_init() SFUD_SUCCESS)) ; sfud_qspi_fast_read_enable(sfud_get_device(SFUD_W25Q32_DEVICE_INDEX), 2); pinMode(WIO_KEY_C, INPUT_PULLUP); // 按钮 C 用于触发录音 mic.init(); Serial.println(Ready.); }这里WIO_KEY_C即 Wio Terminal 顶部的 C 按钮位于左侧、最靠近电源开关。pinMode(WIO_KEY_C, INPUT_PULLUP)将其配置为上拉输入。loop按钮触发、等待完成、处理音频processAudio()目前为空函数本课后续将在这里接入语音转文本服务void processAudio() { }主循环逻辑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 按钮按下digitalRead LOW且当前未在录音 → 调用startRecording()置位_isRecordingDMAC/ADC 持续以 16KHz 采样audioCallback在中断中把数据写入 Flash直到 4 秒采满采满后_isRecording复位、_isRecordingReady置位主循环发现isRecordingReady()为真 → 打印 Finished recording、调用processAudio()处理音频、最后mic.reset()复位并写入新的 WAV 头等待下一次录音。构建、烧录与验证在 PlatformIO 中编译并烧录到 Wio Terminal打开串口监视器波特率 9600按 C 按钮并说话。预期输出--- Available filters and text transformations: colorize, debug, default, direct, hexlify, log2file, nocontrol, printable, send_on_enter, time --- More details at http://bit.ly/pio-monitor-filters --- Miniterm on /dev/cu.usbmodem1101 9600,8,N,1 --- --- Quit: CtrlC | Menu: CtrlT | Help: CtrlT followed by CtrlH --- Ready. Starting recording... Finished recording完整代码与后续衔接本课完整可运行的工程位于仓库 code-record/wio-terminal/smart-timer其中main.cpp串口、SFUD、按钮与主循环flash_writer.h按 Flash 粒度分块的写入器mic.h采集常量、Mic类、DMAC/ADC/TC5 配置与 WAV 封装platformio.iniPlatformIO 环境与 SFUD 依赖。麦克风与扬声器硬件接线Wio Terminal 自带 MEMS 麦克风可选配 ReSpeaker 扩展板参见 wio-terminal-microphone.md。音频采集就绪后下一步即可按课程路径继续学习如何把 Flash 中的 WAV 音频发送到语音服务转为文本wio-terminal-speech-to-text.md最终构建一个支持多语言的智能厨房定时器。小结本文完整还原了 Wio Terminal 上按钮触发、4 秒 16KHz/16-bit 单声道录音并写入 Flash的工程实现核心要点可归纳为四条RAM 不够、Flash 来凑192KB RAM 无法容纳长音频必须把 PCM 数据边采边写进 4MB 板载 FlashDMAC 保证时序用 TC5 定时器触发 DMAC 突发传输以硬件节拍实现严格的 16KHz 采样率CPU 几乎零参与双缓冲防丢数据环形描述符链让 DMAC 在缓冲 1/缓冲 2 间轮换处理一个缓冲的同时另一个缓冲继续采集按粒度分块写 FlashFlashWriter以erase_gran为块大小缓冲数据满块擦写、尾块 flush规避 Flash 粒度对齐约束并以 44 字节 WAV 头 PCM 数据完成标准封装。这套中断驱动 缓冲轮转 外部存储直写的模式是微控制器上处理流式传感数据的通用范式掌握后可迁移到语音、振动、环境传感等各类高频采样场景。【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表