
IoT-For-Beginners 实战在 Wio Terminal 上通过 DMAC 双缓冲与 Flash 存储捕获 16kHz 音频【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners本篇技术指南取自物联网入门项目IoT-For-Beginners第 21 课「用 IoT 设备识别语音」的硬件侧实践聚焦于在 Seeed Wio Terminal 上编写 C 代码、利用板载麦克风捕获 4 秒 16kHz 单声道音频并落盘到 4MB Flash 存储的完整过程。读完本文你将掌握SFUD 串行 Flash 通用驱动的初始化与擦写对齐原理、DMAC 与 ADC 的寄存器级协同配置定时器触发 双缓冲循环描述符、WAV 文件头的内存结构构造以及用板载按键 C 控制录音状态机的完整可运行代码。为什么 Wio Terminal 捕获音频要绕开 RAMWio Terminal 只拥有192KB RAM而 16kHz 采样率、16bit 位深、单声道的一段音频每秒就要产生 32000 字节16 000 × 2数据短短几秒就会耗尽内存因此不足以在内存中直接缓存一段可用的录音。但它的板载4MB Flash 内存足够保存数秒甚至数十秒的原始音频数据这正是本节方案的核心思路把捕获到的音频实时写入 Flash而不是存放在 RAM 中。与此同时音频是连续的模拟信号需要以精确的固定间隔反复采样——例如要得到 16kHz 音频就必须每秒恰好采样 16 000 次且每次采样间隔完全均匀。若用用户代码逐次轮询完成既占用 CPU 又难以保证时序精度。Wio Terminal 采用的 ATSAMD51 微控制器提供了DMACDirect Memory Access Controller直接内存访问控制器这是一种可以不打断 CPU 运行、直接将信号源数据搬入内存的外设电路。捕获链路原理ADC → DMAC → 双缓冲 → Flash完整的音频捕获链路如下板载麦克风输出模拟信号 →ADC模数转换器将模拟电压转换为数字值 →DMAC按固定节拍从 ADC 结果寄存器搬移数据 → 数据写入内存缓冲区→ 缓冲区写满后交由用户代码处理 → 处理后的数据按 Flash 粒度块写入 Flash。DMAC 的采样节拍由一个定时器/计数器产生本文代码使用TC5定时器溢出事件作为 DMAC 触发源从而保证 16kHz 的精确采样间隔。直接写入内存缓冲区会带来一个经典问题处理缓冲区期间可能错过后续采样数据。解决方式是双缓冲DMAC 先写入缓冲区 1缓冲区 1 写满后DMAC 通知用户代码处理缓冲区 1同时自己转向写入缓冲区 2缓冲区 2 写满后再次通知用户代码DMAC 再转回写入缓冲区 1如此循环。只要每个缓冲区的处理耗时小于填满一个缓冲区的时间就不会丢失任何采样数据。本项目中每个 ADC 缓冲区长度为ADC_BUF_LEN 1600个采样在 16kHz 采样率下约 0.1 秒填满一次而中断处理中仅做数据格式转换与 Flash 缓冲写入耗时远小于该间隔。Flash 的擦写对齐问题是另一个必须处理的细节Flash 内存必须通过定义好的地址写入且其粒度granularity要求擦除与写入操作不仅大小固定还必须按该大小对齐。例如若粒度为 4096 字节而你请求擦除地址 4200 处的数据实际可能会擦除 4096 到 8192 的全部数据。因此在把音频数据写入 Flash 时必须以正确大小的块来组织数据。任务一配置 Flash 内存访问SFUD 驱动本环节的目标是让代码能够通过SFUDSerial Flash Universal Driver串行 Flash 通用驱动库访问板载 W25Q32 Flash 芯片。SFUD 是设计用于兼容各类 Flash 芯片的通用库。创建项目与添加库依赖用 PlatformIO 新建一个名为smart-timer的 Wio Terminal 工程并在setup函数中配置串口仓库完整工程见 code-record/wio-terminal其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其中lib_deps中两个依赖分别提供文件系统抽象与 SFUD 驱动build_flags中的-DSFUD_USING_QSPI使 SFUD 启用 QSPI 访问路径源码见 platformio.ini。打开main.cpp在文件顶部加入 Flash 库的头文件#include sfud.h #include SPI.h在setup函数中初始化 SFUD 库并开启快速读取while (!(sfud_init() SFUD_SUCCESS)) ; sfud_qspi_fast_read_enable(sfud_get_device(SFUD_W25Q32_DEVICE_INDEX), 2);第一行循环等待 SFUD 库初始化成功第二行对板载 W25Q32 Flash 开启快速读取。板载 Flash 可通过QSPIQueued Serial Peripheral Interface队列式 SPI访问——这是一种通过队列提供连续访问、仅需极少处理器参与的 SPI 控制器类型能让 Flash 的读写更快。编写 FlashWriter 类新建src/flash_writer.h文件其作用是把字节写入内存缓冲凑满一个 Flash 粒度块后再整块擦写进 Flash。文件头部引入必要头文件#pragma once #include Arduino.h #include sfud.h定义FlashWriter类骨架class FlashWriter { public: private: };在private区添加缓冲与 Flash 指针字段byte *_sfudBuffer; size_t _sfudBufferSize; size_t _sfudBufferPos; size_t _sfudBufferWritePos; const sfud_flash *_flash;各字段含义_sfudBuffer字节数组先在此收集待写入数据满后整体写入 Flash_sfudBufferSize缓冲大小取自 Flash 芯片的擦除粒度erase_gran_sfudBufferPos当前在缓冲内的写入位置_sfudBufferWritePos当前在 Flash 中的写入目标地址_flash指向目标 Flash 芯片的指针——部分微控制器上存在多块 Flash 芯片需要通过设备表定位。在public区添加初始化方法void init() { _flash sfud_get_device_table() 0; _sfudBufferSize _flash-chip.erase_gran; _sfudBuffer new byte[_sfudBufferSize]; _sfudBufferPos 0; _sfudBufferWritePos 0; }该方法取设备表中的第一块 Flash下标 0按芯片擦除粒度动态分配缓冲。之所以放在init方法而非构造函数中是因为它必须在setup函数完成 SFUD 初始化之后调用。在public区添加写入与冲刷方法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; } }其工作机制为逐字节写入内存缓冲当缓冲填满_sfudBufferPos _sfudBufferSize时调用 SFUD 的sfud_erase_write执行先擦除再写入并推进 Flash 写地址、复位缓冲位置。由于录音数据的总字节数不一定是粒度块的整数倍flushSfudBuffer负责把最后一次未满的残量缓冲也写入 Flash。这一尾部写入会顺带多写一些无用数据但无妨——后续读取时只会读取所需长度的数据。仓库中该类的完整实现含reset方法将缓冲位置与 Flash 写位置归零以便重复录音见 flash_writer.h。任务二配置音频捕获常量、Mic 类与 DMACconfig.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各常量含义如下表常量值说明RATE16000音频采样率。16000 即 16kHzSAMPLE_LENGTH_SECONDS4要捕获的音频时长默认 4 秒如需更长录音可调大SAMPLES64000将被捕获的音频采样总数等于采样率 × 秒数BUFFER_SIZE128044捕获音频的缓冲区大小。音频将以 WAV 文件形式保存44 字节头 128000 字节音频数据每个采样 2 字节ADC_BUF_LEN1600从 DMAC 捕获音频所使用的缓冲区大小 若觉得 4 秒不够完成一次语音指令只需调大SAMPLE_LENGTH_SECONDS其余常量SAMPLES、BUFFER_SIZE会自动按宏表达式重算。完整的常量定义亦可参考后续课程代码 config.h。Mic 类的状态骨架新建src/mic.h头部引入config.h与flash_writer.h#pragma once #include Arduino.h #include config.h #include flash_writer.h定义Mic类及其全局实例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; }; Mic mic;两个volatile标志位是录音状态机的核心_isRecordingDMAC 一旦配置完成就会持续不断地向内存缓冲写入数据因此该标志决定这些缓冲应被处理还是被忽略_isRecordingReady当 4 秒音频捕获完成后被置位_writer用于把音频数据写入 Flash 的FlashWriter实例。随后声明全局实例mic供中断处理函数与main.cpp共用。configureDmaAdc寄存器级配置 DMAC、ADC 与 TC5在Mic类的private区加入如下硬件配置代码。首先是 DMA 描述符结构体与全局缓冲声明typedef struct { uint16_t btctrl; uint16_t btcnt; uint32_t srcaddr; uint32_t dstaddr; uint32_t descaddr; } dmacdescriptor; // Globals - DMA and ADC 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 配置、ADC 配置、定时器 TC5 配置与中断使能。1. DMAC 配置与循环描述符void configureDmaAdc() { // Configure DMA to sample from ADC at a regular interval (triggered by timer/counter) DMAC-BASEADDR.reg (uint32_t)_descriptor_section; // Specify the location of the descriptors DMAC-WRBADDR.reg (uint32_t)_wrb; // Specify the location of the write back descriptors DMAC-CTRL.reg DMAC_CTRL_DMAENABLE | DMAC_CTRL_LVLEN(0xf); // Enable the DMAC peripheral DMAC-Channel[1].CHCTRLA.reg DMAC_CHCTRLA_TRIGSRC(TC5_DMAC_ID_OVF) | // Set DMAC to trigger on TC5 timer overflow DMAC_CHCTRLA_TRIGACT_BURST; // DMAC burst transfer _descriptor.descaddr (uint32_t)_descriptor_section[1]; // Set up a circular descriptor _descriptor.srcaddr (uint32_t)ADC1-RESULT.reg; // Take the result from the ADC0 RESULT register _descriptor.dstaddr (uint32_t)_adc_buf_0 sizeof(uint16_t) * ADC_BUF_LEN; // Place it in the adc_buf_0 array _descriptor.btcnt ADC_BUF_LEN; // Beat count _descriptor.btctrl DMAC_BTCTRL_BEATSIZE_HWORD | // Beat size is HWORD (16-bits) DMAC_BTCTRL_DSTINC | // Increment the destination address DMAC_BTCTRL_VALID | // Descriptor is valid DMAC_BTCTRL_BLOCKACT_SUSPEND; // Suspend DMAC channel 0 after block transfer memcpy(_descriptor_section[0], _descriptor, sizeof(_descriptor)); // Copy the descriptor to the descriptor section _descriptor.descaddr (uint32_t)_descriptor_section[0]; // Set up a circular descriptor _descriptor.srcaddr (uint32_t)ADC1-RESULT.reg; // Take the result from the ADC0 RESULT register _descriptor.dstaddr (uint32_t)_adc_buf_1 sizeof(uint16_t) * ADC_BUF_LEN; // Place it in the adc_buf_1 array _descriptor.btcnt ADC_BUF_LEN; // Beat count _descriptor.btctrl DMAC_BTCTRL_BEATSIZE_HWORD | // Beat size is HWORD (16-bits) DMAC_BTCTRL_DSTINC | // Increment the destination address DMAC_BTCTRL_VALID | // Descriptor is valid DMAC_BTCTRL_BLOCKACT_SUSPEND; // Suspend DMAC channel 0 after block transfer memcpy(_descriptor_section[1], _descriptor, sizeof(_descriptor)); // Copy the descriptor to the descriptor section // Configure NVIC NVIC_SetPriority(DMAC_1_IRQn, 0); // Set the Nested Vector Interrupt Controller (NVIC) priority for DMAC1 to 0 (highest) NVIC_EnableIRQ(DMAC_1_IRQn); // Connect DMAC1 to Nested Vector Interrupt Controller (NVIC) // Activate the suspend (SUSP) interrupt on DMAC channel 1 DMAC-Channel[1].CHINTENSET.reg DMAC_CHINTENSET_SUSP;关键点解读触发源DMAC_CHCTRLA_TRIGSRC(TC5_DMAC_ID_OVF)将 DMAC 通道 1 的触发源设为 TC5 定时器溢出事件配合DMAC_CHCTRLA_TRIGACT_BURST突发传输实现每溢出一次搬移一批数据循环描述符两个描述符互为对方的descaddr后继0 → 1 → 0形成环形链表每个描述符的srcaddr固定指向ADC1-RESULT.regADC 结果寄存器dstaddr分别指向_adc_buf_0与_adc_buf_1的尾部地址 sizeof(uint16_t) * ADC_BUF_LEN配合DMAC_BTCTRL_DSTINC目的地址递增实现向数组尾部回写块传输完成即挂起DMAC_BTCTRL_BLOCKACT_SUSPEND使通道在搬完一个btcnt即ADC_BUF_LEN个半字后挂起随后DMAC_CHINTENSET_SUSP打开挂起中断把控制权交回中断处理函数中断优先级通过 NVIC 将 DMAC1 中断优先级设为 0最高。2. ADC 配置// Configure ADC ADC1-INPUTCTRL.bit.MUXPOS ADC_INPUTCTRL_MUXPOS_AIN12_Val; // Set the analog input to ADC0/AIN2 (PB08 - A4 on Metro M4) while (ADC1-SYNCBUSY.bit.INPUTCTRL) ; // Wait for synchronization ADC1-SAMPCTRL.bit.SAMPLEN 0x00; // Set max Sampling Time Length to half divided ADC clock pulse (2.66us) while (ADC1-SYNCBUSY.bit.SAMPCTRL) ; // Wait for synchronization ADC1-CTRLA.reg ADC_CTRLA_PRESCALER_DIV128; // Divide Clock ADC GCLK by 128 (48MHz/128 375kHz) ADC1-CTRLB.reg ADC_CTRLB_RESSEL_12BIT | // Set ADC resolution to 12 bits ADC_CTRLB_FREERUN; // Set ADC to free run mode while (ADC1-SYNCBUSY.bit.CTRLB) ; // Wait for synchronization ADC1-CTRLA.bit.ENABLE 1; // Enable the ADC while (ADC1-SYNCBUSY.bit.ENABLE) ; // Wait for synchronization ADC1-SWTRIG.bit.START 1; // Initiate a software trigger to start an ADC conversion while (ADC1-SYNCBUSY.bit.SWTRIG) ; // Wait for synchronization // Enable DMA channel 1 DMAC-Channel[1].CHCTRLA.bit.ENABLE 1;要点将 ADC 输入多路选择器指向板载麦克风对应的模拟引脚AIN12采样保持时长设为最短2.66µs时钟预分频DIV128使 ADC 时钟从 48MHz 降至 375kHz分辨率选12bit开启自由运行模式FREERUN使 ADC 持续转换。每一步配置后都通过轮询SYNCBUSY等待同步完成这是 ATSAMD51 上操作外设寄存器的标准安全写法。最后使能 DMAC 通道 1。3. 定时器 TC5 配置// Configure Timer/Counter 5 GCLK-PCHCTRL[TC5_GCLK_ID].reg GCLK_PCHCTRL_CHEN | // Enable peripheral channel for TC5 GCLK_PCHCTRL_GEN_GCLK1; // Connect generic clock 0 at 48MHz TC5-COUNT16.WAVE.reg TC_WAVE_WAVEGEN_MFRQ; // Set TC5 to Match Frequency (MFRQ) mode TC5-COUNT16.CC[0].reg 3000 - 1; // Set the trigger to 16 kHz: (4Mhz / 16000) - 1 while (TC5-COUNT16.SYNCBUSY.bit.CC0) ; // Wait for synchronization // Start Timer/Counter 5 TC5-COUNT16.CTRLA.bit.ENABLE 1; // Enable the TC5 timer while (TC5-COUNT16.SYNCBUSY.bit.ENABLE) ; // Wait for synchronization }TC5 使用 16bit 计数器的MFRQ匹配频率模式将比较值CC[0]设为3000 - 1即以48MHz / 16000 3000为计数周期产生 16kHz 的溢出节拍从硬件层面精确保证每秒 16000 次采样。两个 ADC 缓冲区数组定义在方法之后uint16_t _adc_buf_0[ADC_BUF_LEN]; uint16_t _adc_buf_1[ADC_BUF_LEN]; 微控制器开发的一大难点就在这里没有操作系统代为管理硬件代码运行在极低层次直接操作寄存器。这段代码相比单板计算机或桌面程序要复杂得多虽然有库可以简化部分工作但底层交互的复杂性依然存在。WAV 文件头44 字节的结构化内存布局WAV 文件以 44 字节头开始。用结构体描述这一布局// WAV files have a header. This struct defines that header 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) */ }; void 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; // mono 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); }要点写入RIFF/WAVE/fmt/data魔数format_tag 1表示PCM编码num_chans 1表示单声道srate RATE16000bits_per_samp 16表示每采样 16bit即 2 字节。整块 44 字节头通过writeSfudBuffer写入 Flash 缓冲作为后续音频数据的起始偏移。audioCallback采样数据格式转换与落盘当 DMAC 每填满一个缓冲区并触发中断后需要把 16bit 缓冲区数组转换为可直接落盘的字节流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 返回的是12bit 无符号值0–1023而 WAV 数据需要16bit 有符号值。这里通过((int16_t)buf[i] - 2048) * 16完成偏移与放大转换再拆分出低字节与高字节两个字节依次写入 Flash 缓冲形成小端序的原始二进制数据写入起点从idx 44开始正是 WAV 头的偏移量当累计字节数达到BUFFER_SIZE128044说明 4 秒音频全部捕获完毕先flushSfudBuffer冲刷最后的残量缓冲然后复位idx并把_isRecording置 false、_isRecordingReady置 true通知主循环录音完成。dmaHandler 与 DMAC 中断服务函数在Mic类的public区添加 DMA 中断处理入口void 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; } }该函数由 DMAC 挂起中断调用先检查SUSP挂起标志随即发出RESUME命令让 DMAC 继续搬移数据并清除中断标志然后依据count在_adc_buf_0与_adc_buf_1之间交替把写满的那个缓冲区交给audioCallback处理count以 2 为模翻转实现双缓冲轮换。在类外、Mic mic;声明之后添加中断服务函数void DMAC_1_Handler() { mic.dmaHandler(); }DMAC_1_Handler是 ATSAMD51 定义的 DMAC 通道 1 中断向量函数按函数名解析只需存在即可被硬件调用。init 与 reset生命周期管理最后在Mic类的public区添加void init() { analogReference(AR_INTERNAL2V23); _writer.init(); initBufferHeader(); configureDmaAdc(); } void reset() { _isRecordingReady false; _isRecording false; _writer.reset(); initBufferHeader(); }init为麦克风引脚设置正确的内部参考电压AR_INTERNAL2V232.23V初始化 Flash 写入器写入 WAV 头并完成 DMAC/ADC/TC5 的全部硬件配置reset在一次录音被消费后清除状态标志、复位 Flash 写位置FlashWriter::reset将_sfudBufferPos与_sfudBufferWritePos归零并重新写入 WAV 头准备下一轮录音。任务三在 main.cpp 中组装录音流程回到main.cpp把上述组件接入 Arduino 主程序。引入mic.h#include mic.h在setup函数中初始化按键 C 与麦克风。Wio Terminal 顶部的按键中C 键左侧、最靠近电源开关的那颗被选为录音触发键void setup() { Serial.begin(9600); while (!Serial) ; // Wait for Serial to be ready 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); mic.init(); Serial.println(Ready.); }完整代码见 main.cpp。按键使用INPUT_PULLUP上拉模式按下时为低电平。定义音频处理占位函数processAudio本环节为空实现课程后续将在此把语音发送到云端做语音转文字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置 true此后 DMAC 中断中的audioCallback开始把采样数据写入 Flash当 4 秒音频捕获完成_isRecordingReady true且_isRecording false时调用processAudio()处理音频随后mic.reset()复位全部状态等待下一次按键。构建、烧录与串口验证在 PlatformIO 中构建工程并烧录到 Wio Terminal打开串口监视器波特率9600与Serial.begin(9600)一致按下C 键并对着板载麦克风说话程序将捕获 4 秒音频。预期串口输出如下--- 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 recordingReady.表示 SFUD 初始化成功、Flash 写入器与 DMAC 已就绪Starting recording...表示检测到按键按下并开始录音Finished recording表示 4 秒音频已全部写入 FlashprocessAudio被调用。至此一个完整的按键触发、DMAC 双缓冲采样、WAV 头写入、Flash 块对齐存储的 Wio Terminal 音频录制程序宣告成功。捕获到的音频以 16kHz、16bit、单声道 PCM 格式的 WAV 文件布局保存在板载 Flash 中后续课程将在此基础上读取 Flash 中的音频数据并通过 Azure 语音服务完成语音转文字实现一个多语言智能厨房定时器。完整可参考代码位于仓库的 code-record/wio-terminal 目录录音部分与 code-speech-to-text/wio-terminal 目录接入语音转文字后对应的教学主文档为 wio-terminal-audio.md。【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考