☰
单片机语音数字化存储与回放:采样、Flash存储与PWM还原
2026/10/15 15:56:51 网站建设 项目流程

简介:这份基于单片机的数字化语音存储与回放系统设计资源,围绕电子竞赛典型选题,面向单片机应用、嵌入式系统及数字信号处理方向的开发者与参赛学生。系统完整涵盖语音采集、模数转换、PCM/ADPCM编码、存储管理、解码回放及控制交互等环节,包含硬件外围电路与软件程序框架,适合用来快速搭建同类功能原型或作为课程设计、竞赛项目的参考方案。资源包共40个文件,大小约750KB。核心内容包括asm汇编源文件与c语言程序、h/inc头文件及包含文件、obj目标文件和lst列表文件,以及pdf原理图文档、spj/makefile工程配置、map/sym符号映射、s37烧录文件等,从源码到编译烧录均有涉及,便于按工程流程对照学习。目前已有515人学习下载,资料内附NE5532音频前端滤波与系统整体设计两份PDF,可帮助理解语音信号处理链路中从模拟调理到数字存储回放的具体实现,并对低功耗、存储优化和播放控制等实际问题获得可落地的处理思路。

1. 基于单片机的数字化语音存储与回放:一个按键驱动的录音机

做过录音笔的都知道,语音数字化这件事听起来高深,拆开看就是一道算术题:采样率决定音质,存储容量决定时长,两者一乘,整块系统的设计边界就出来了。这套基于单片机的数字化语音存储与回放系统设计,核心是把模拟语音信号通过 ADC 采样成数字量,存进 Flash 芯片,再在需要时读出来经过 DAC(或 PWM)还原成声音。整机不带操作系统,一个单片机芯片加几个外围器件,按一次键录音,再按一次键回放,断电后数据不丢。它特别适合课程设计、电子竞赛入门和想搞懂采样-存储-还原全链路的人——比纯仿真有手感,比买现成模块能学到东西,而且成本压到几十块钱以内。

2. 硬件架构与采样参数:为什么是 8kHz、1 分钟的黄金组合

2.1 主控选型:从 ADC 位数和 Flash 接口说起

这类系统的主控选型,常见的是两类:带片上 ADC 的 51 内核单片机和带硬件 SPI 的 Cortex-M 内核单片机。51 内核的优点是上手快、资料多、寄存器少,但其 ADC 通常是 10 位逐次逼近型,输入阻抗不高,直接采集麦克风信号会明显拉低电平;STM32 这类 M 核芯片 ADC 位数更高(12 位),自带 DMA,适合连续采样,可开发环境重一点,新手容易被 HAL 库的层层封装绕晕。我一般建议课程设计选 STC 系 51 内核单片机,原因很实际:片上 Flash 自带,不用另接编程器,Keil 里写 C 和汇编都行,而且 ADC 采样率做到 8kHz 完全在它的能力范围内。

关键参数要从需求倒推。人耳对语音的感知范围是 300Hz~3.4kHz 左右,按采样定理,采样率至少是最高频率的两倍,工程上留余量,8kHz 是语音系统最常见的选择。8kHz 采样意味着每秒产生 8000 个采样点,每点如果用 8 位存储,就是 8KB/s 的数据量。那么 1 分钟语音约需 480KB,2 分钟约 960KB。市面上常见的 SPI NOR Flash 芯片 W25Q64 容量为 8MB(64Mbit),存 1 分钟绰绰有余,还能留出几十段录音的索引区。这套系统的典型配置就是:8bit 采样精度 + 8kHz 采样率 + W25Q64 存储,录音时长上限约 17 分钟。

2.2 存储容量计算:一张表看懂数据量和时长

写代码前先花十分钟算容量,能避开后期改板子的麻烦。计算公式是:

存储容量(字节) = 采样率(Hz) × 量化位数(字节) × 录音时长(秒)

以 W25Q64(8MB)为例,各配置下的理论录音时长如下表:

采样率量化位数每秒数据量8MB 可录时长
8kHz8bit8KB约 17 分钟
8kHz16bit16KB约 8.5 分钟
16kHz8bit16KB约 8.5 分钟
16kHz16bit32KB约 4 分钟

表里能直接看出三个结论:第一,想把录音时间拉长,优先降采样率而不是降位数,8bit 降到 4bit 音质会断崖式劣化,但 8kHz 降到 6kHz 感知不明显;第二,16bit 的收益在回放端没有对应的高质量 DAC 时是浪费的,后面会讲到为什么;第三,存储芯片选择上有个隐藏坑:Flash 的擦除单位是扇区(通常 4KB),如果每段录音长度不是扇区的整数倍,最后一个扇区会产生碎片浪费,设计文件索引时必须考虑。具体到这套系统,选 8kHz + 8bit,然后每段录音固定分配 512KB(128 个扇区),能存 16 段,文件管理最简单。

2.3 模拟前端与回放电路:运放不是玄学,是刚需

麦克风输出的信号幅度大概在 20~100mV 级别,ADC 的输入范围是 0~5V(或 0~3.3V),直接采样会得到一个只用了 2% 量程的信号,量化噪声会被放大几十倍。所以模拟前端至少需要一个两级放大:第一级用运放搭同相放大器把信号提到 0.5V 级别,第二级加直流偏置把信号抬到 ADC 量程中点(比如 2.5V),这样信号负半周不会被截掉。语音信号是交流的,若无偏置,负半周被 ADC 削掉,回放时的声音会明显发闷。

回放端也一样,单片机内部没有真 DAC 的型号(多数 51 用 PWM 模拟 DAC),直接驱动耳机阻抗不够。常见方案是 PWM 输出后接二阶低通滤波器,把 PWM 的高频分量滤掉,再用小功率运放缓冲后推耳机。低通滤波器的截止频率建议设在采样率的 1/4 左右——8kHz 采样时取 2kHz,这样既能保留语音能量集中的频段,又能有效滤除 PWM 载波。R 和 C 的取值用公式 f = 1/(2πRC) 估算,实调时可以测 PWM 引脚在无输入时是否还有丝丝声,有就说明滤波器阶数不够或截止频率偏高。

3. 三段式软件架构:ADC 采集、Flash 分配与 PWM 回放的 C 代码拆解

3.1 主程序框架与 ADC 连续采样

这套系统的软件主框架可以简单粗暴地设计为一个大循环加两个中断。按键触发开始/停止录音,定时器中断负责固定 8kHz 采样率。这么做的好处是:主循环不用复杂的状态机,录音和回放分别对应两个布尔标志,代码逻辑一目了然。下面给出录音侧的核心采样代码:

// 8kHz 定时采样:定时器0重装值计算 // 晶振 11.0592MHz,模式1(16位定时器) // 每次中断间隔 125us,即 8000Hz #define FOSC 11059200L #define TIMER0_RELOAD (65536 - FOSC/12/8000) volatile unsigned char sample_ready = 0; // 采样完成标志 volatile unsigned char sample_value = 0; // ADC 转换结果 void timer0_isr(void) interrupt 1 { TH0 = (unsigned char)(TIMER0_RELOAD >> 8); TL0 = (unsigned char)(TIMER0_RELOAD & 0xFF); ADC_CONTR = 0x88; // 启动 ADC,通道0 // ADC 转换完成后会置位 ADC_FLAG,在 ADC 中断里读结果 } void adc_isr(void) interrupt 5 { ADC_CONTR &= ~0x10; // 清除 ADC_FLAG sample_value = ADC_RES; // 取 8 位转换结果 sample_ready = 1; // 通知主循环可以存 Flash }

这段代码里有两个容易理解错的细节。第一个是TIMER0_RELOAD的计算:11.0592MHz 晶振、12T 模式下,定时器每计数一次耗时 12/11059200 秒,要得到 125us 的中断周期,计数值就是 125us × 11059200/12 = 921.6,取整后重装值就是 65536-922。之所以选 11.0592MHz 晶振,是因为这个频率能和 8kHz 形成整除关系,中断周期的误差比 12MHz 晶振小一个数量级。第二个是ADC_CONTR = 0x88:高四位是 ADC 使能和启动位,低四位是通道选择,0x88 表示使能 ADC 并启动通道 0 的一次转换。STC 的 ADC 不是连续转换模式,每次都要软件触发一次,所以放在定时器中断里正好和采样节奏对齐。

3.2 Flash 存储与地址分配:先擦后写是铁律

采集到的 sample_value 不能直接往 Flash 里写。SPI NOR Flash 的物理特性决定了「写操作只能把 1 变成 0」,所以必须先把目标扇区整块擦成 0xFF,才能再写入数据。这跟 SRAM 完全不同,刚上手的人十有八九在这里翻车——表现为录音时写入正常,回放时开头一段全是乱码,因为没有处理跨扇区的边界。

下面是一段带地址分配的写入代码:

#define SECTOR_SIZE 4096 // W25Q64 扇区大小 #define RECORD_SECTORS 128 // 每段录音占 128 个扇区 = 512KB #define RECORD_BASE_ADDR 0x00 // 录音数据起始地址 unsigned long record_offset = 0; // 当前写入位置偏移 void flash_write_byte(unsigned long addr, unsigned char dat) { // 常见做法:先发写使能命令,再发页编程命令 SPI_CS_LOW(); spi_write_byte(0x06); // 写使能 WREN SPI_CS_HIGH(); SPI_CS_LOW(); spi_write_byte(0x02); // 页编程 PP spi_write_byte((addr >> 16) & 0xFF); spi_write_byte((addr >> 8) & 0xFF); spi_write_byte(addr & 0xFF); spi_write_byte(dat); SPI_CS_HIGH(); // 等待写完成:Flash 内部擦/写需要时间,必须轮询 BUSY 位 SPI_CS_LOW(); spi_write_byte(0x05); // 读状态寄存器 while (spi_read_byte() & 0x01); // WIP=1 时忙 SPI_CS_HIGH(); } void save_sample_to_flash(unsigned char val) { unsigned long current_addr = RECORD_BASE_ADDR + record_offset; // 若到达扇区边界,先擦除下一扇区 if ((current_addr & (SECTOR_SIZE - 1)) == 0) { flash_erase_sector(current_addr); } flash_write_byte(current_addr, val); record_offset++; // 超出录音长度,关闭录音标志 if (record_offset >= RECORD_SECTORS * SECTOR_SIZE) { record_en = 0; } }

这段代码的逻辑核心是save_sample_to_flash函数。每次写入前判断当前地址是否落在扇区首地址,如果是就先把该扇区擦掉。这里有个效率问题:录满一个扇区 4096 字节只需 0.5 秒,但擦除一个扇区的时间在 50~400ms 之间,如果恰好在数据写入的过程中去擦扇区,会有十几毫秒的语音丢失。规避办法有两个:一是把扇区擦除提前到录音开始时做(预先擦除后续所有扇区),二是用双缓冲——把采样数据先暂存到单片机 RAM,凑满 256 字节(Flash 页大小)再一次性写页,这样擦扇区的时机完全可控。256 字节×32 页刚好一个扇区,推荐用后一种,代码改起来也不大。

3.3 回放:PWM 输出与低通滤波还原

回放就是把 Flash 里的采样值按同样的 8kHz 节奏送出去。没有片上 DAC 时,最直接的做法是用定时器输出 PWM,通过调节占空比来等效输出电压。注意:单片机 PWM 的分辨率由定时器重装值决定,比如 8 位 PWM 对应占空比步进是 1/256,这和 ADC 的 8 位刚好对齐,属于很自然的配对。回放核心代码:

volatile unsigned char play_value = 0x80; // PWM 占空比初值,对应 50% 中点 void timer1_isr(void) interrupt 3 { // 8kHz 节拍,和录音采样率一致 TH1 = (unsigned char)(TIMER0_RELOAD >> 8); TL1 = (unsigned char)(TIMER0_RELOAD & 0xFF); // 从 Flash 读一个采样点并更新占空比 play_value = flash_read_byte(play_addr++); PWM_DUTY = play_value; } void start_playback(unsigned long start_addr, unsigned long length) { play_addr = start_addr; // 先把第一扇区读出来(Flash 读不需要擦除) // 注意:回放启动瞬间,PWM 引脚电平应处于中点,避免爆音 PWM_DUTY = 0x80; TR1 = 1; // 启动回放定时器 }

实现回放有两个容易被忽略的问题。第一个是爆音:回放开始的瞬间,PWM 寄存器如果保持上次的值(比如 0x00),扬声器会收到一个阶跃电压,表现为「啪」一声。解决的常见做法是启动回放前先把 PWM 占空比设到 0x80(中点),让扬声器膜片处在平衡位置再开始输出。第二个是读取速度跟不上:flash_read_byte每次要拉低 CS、发读命令、读状态、拉高 CS,这个操作耗时在微秒级,但若 SPI 是软件模拟的,主频 12MHz 下一次读操作可能消耗 20us 以上,8kHz 节拍间隔是 125us,理论够用,但千万别在回放中断里再叠加上一步说的 Flash 擦除判断,否则必掉采样点。

3.4 按键防抖与状态机:录放切换的逻辑陷阱

录放切换如果用「按下—释放都触发」的逻辑,会出现一个恼人的问题:录音刚停就立刻开始回放,或者回放时按一下键变成了暂停而不是停止。我常用的状态机设计是四态:空闲态、录音态、回放态、暂停态,然后用按键事件(按下沿)驱动状态迁移,释放事件只做消抖计时,不参与迁移。伪代码如下:

typedef enum { IDLE, RECORDING, PLAYING, PAUSED } sys_state_t; sys_state_t state = IDLE; void key_scan(void) { // 每 10ms 扫描一次,两次读数一致才认为按键有效 static unsigned char last_key = 1; unsigned char cur_key = KEY_PIN; if (cur_key == 0 && last_key == 1) { // 下降沿触发 switch (state) { case IDLE: state = RECORDING; start_recording(); break; case RECORDING: stop_recording(); state = IDLE; break; case PLAYING: stop_playback(); state = IDLE; break; default: break; } } last_key = cur_key; }

这段逻辑里最重要的细节是:录音状态下按一下是停止而不是暂停。暂停的需求在带存储介质的系统中其实很鸡肋——因为 Flash 写入是顺序流,暂停再恢复意味着要记录断点地址,要么在 Flash 预留一个断点索引区,要么把暂停设计为主循环暂停 flush,成本都不小,课程设计或入门 Demo 完全没必要做。另一个容易踩的坑是扫描周期和按键持续时间的关系:10ms 扫描一次,每次判断「两次读数一致」,意味着按键至少需要持续约 20ms 才能被识别,这能有效过滤机械抖动(通常 5~10ms),但又不会让快速连按失效(人手最快连按也要 100ms 以上)。

4. 避坑手册:上电乱音、地址越界与擦写顺序的三个翻车现场

4.1 上电瞬间扬声器乱响,吓得以为板子烧了

现象:每次上电,扬声器先是一阵刺耳的噪声,持续几百毫秒后才安静;按键开始录音后再回放,开头会有一段类似电流声的杂音。

原因:单片机上电复位期间,I/O 引脚是高阻态,PWM 引脚电平不确定,此时如果功放已经供电,扬声器上会叠加一个足够响的瞬态。同时 Flash 芯片还没初始化,回放中断若在主循环初始化完成前被触发,读出来的全是随机数,播放出来自然全是噪声。这个问题的根子不在代码逻辑,而在上电时序。

解决:硬件上在功放使能引脚和单片机之间加一个 RC 延时(如 10kΩ + 10uF,约 100ms 延时),或者直接把功放使能引脚挂在单片机某个 I/O 上,进入主循环后软件置高。我一般用后者:写一个audio_power_init(),先初始化 Flash 和定时器,最后才拉高功放使能。这样即使 PWM 引脚漏出随机电平,功放没使能时也不会出声。上电爆音根除后,回放开头的杂音用 3.3 节说的「播放前占空比归中」一并解决。

4.2 录到第 129 段录音时数据整体乱掉,之前的录音还能放

现象:当录音段数超过某个阈值,新录制的语音会出现「开头一段是上上次录音的尾巴」,甚至整段变成数据错乱。检查扇区擦除日志,发现擦除动作发生在写入过程中,而不是录音开始前。

原因:录音代码按「边写边擦」策略,当record_offset走到扇区边界时,save_sample_to_flash里的擦除分支被触发。擦除期间 Flash 对写命令无响应,如果此时单片机没有做任何停顿、继续发写命令,数据就丢了;更隐蔽的是擦除完成后,Flash 内部地址指针可能归零,后续数据写到 0x000000 而不是预期的下个扇区,把已有的录音索引区覆盖掉了。

解决:改掉「边写边擦」策略,采用 3.2 节末尾说的双缓冲。具体做法是用一个 256 字节的 RAM 缓冲,采样中断只往缓冲里塞数,缓冲满时才把整页写入 Flash,写入前统一判断并擦除下一个扇区。这样擦除动作只发生在一个页写完毕、下个页未开始的间隙,既不会打断采样流,也不会在擦除期间丢数据。代码改动量不超过 30 行,换来的是录音可靠性的大幅提升。顺带一个排查技巧:这类问题肉眼很难定位,我在flash_erase_sector里加了一个串口打印,每次擦除输出当前地址,对照录音时长就能算出擦除是否发生在录音停顿处。

4.3 回放声音速度忽快忽慢,但录音时没有任何异样

现象:播放一段固定录音,声音节奏不稳定,有时像磁带被拉长,有时又像快进。用示波器测 PWM 输出频率,发现相邻两个输出周期之间偶尔会插入一个明显更短的周期。

原因:8kHz 采样节拍完全依赖定时器中断,而 Flash 的页读操作如果在中断里执行,耗时是波动的——命中缓存块时几十个时钟周期,未命中时要整个 SPI 事务,耗时飙升。中断里执行时间一旦超过 125us,下一次定时器溢出就会被漏掉,等效于采样间隔变成 250us,回放自然变慢了一会。更麻烦的是,如果代码里在播放中断里加了扇区边界判断(想提前擦除写好的数据),时间波动会更明显。

解决:把所有 Flash 读操作移出中断,改成双缓冲机制——定时器中断只从 RAM 缓冲取数,主循环负责从 Flash 把下一块数据搬到缓冲。一块的大小建议取 256 字节(刚好一页),这样主循环每 32ms 才需要执行一次页读,时间充裕。如果不想大改结构,还有一个讨巧的办法:把定时器重装值从固定值改成实测校准值。先让定时器自由跑 1 秒,用另一个定时器计数溢出次数,反推实际重装值。但这是治标不治本,中断里只要还有 Flash 操作,波动就一定会回来。我在这个坑上折腾了两个晚上,最后老老实实改双缓冲,一次到位。

4.4 写入数据校验失败,Flash 内容读回来总是差几个字节

现象:录音完成后,将 Flash 中的数据读出与 RAM 中的原值比对,发现固定偏移处有连续 8~16 个字节对不上,且每次都发生在 4096 字节边界后面一点点。

原因:这是 SPI 时序问题中的经典。Flash 页编程一次最多写入 256 字节,如果请求的写入长度跨越页边界,Flash 会拒绝执行整条命令或者只写前半段。flash_write_byte虽然单字节写不会越页,但前面的「先擦后写」逻辑在扇区边界擦除时,状态寄存器轮询结束的判断可能提前了——Flash 擦除完成后 WIP 位清零,但芯片内部还在做数据搬移(对某些型号而言)。此时立刻发写命令,写操作和内部搬移冲突,数据就错位了。

解决:擦除和编程命令发出后,不光轮询 WIP 位,还要再加一次「写使能状态确认」。标准做法是每次写操作前都发送 WREN(0x06),然后紧接着读状态寄存器确认 WEL 位为 1,再发页编程命令。这样虽然增加了几条 SPI 指令,但把「假成功」的概率压到最低。我在现场排查时用的是对比法:把 WEL 位检查去掉和保留,分别录 10 段语音做比对,保留检查的那组一次错都没有。从那以后,凡是涉及 NOR Flash 擦写的代码,我都强制走一遍 WEL 确认流程。

5. 验证三板斧:示波器测模拟前端、串口核对采样值、耳听回放判断音质

5.1 示波器看模拟前端:输入信号是否真的加到了 ADC 全量程

代码写完、板子焊完,第一步不是跑整机功能,而是先把模拟前端和 ADC 输入端的信号测一遍。用信号发生器在麦克风输入端注入一个 1kHz 正弦波(幅度 100mV),用示波器探头分别测运放输出和 ADC 输入引脚。

正常的波形应该是:运放输出端是幅度约 2V 的正弦波,叠加在 1.25V(若供电 2.5V 偏置)直流电平上。若看到波形顶部或底部被削平,说明第一级增益过高,信号幅度超过 ADC 量程;若幅度极小(不足 0.5V),说明增益不够,后续会浪费 8bit 量化精度。增益调整时联动改反馈电阻 Rf,另一只手里拿着万用表量静态工作点——运放输出直流电平应该稳定在供电电压的一半,偏差超过 0.2V 就检查偏置电阻分压是否对称。这个验证做完,模拟前端才算过关。

5.2 串口打印采样数据:用数值而不是耳朵判断精度

听感是玄学,数据是实据。录音开始后,在adc_isr里加一行串口输出(只在调试版开启),把sample_value通过 UART 发到上位机,波特率选 115200。需要留意的是:115200bps 下每秒最多传 11.5KB,8kHz×1 字节 = 8KB/s,正好能实时传输不丢数据。在串口助手里看到的数据应当呈现一个近似直流偏置在 128 附近的交流波形,用 Excel 导入后画出波形,和信号发生器注入的波形形状对应上。

如果发现采样值全在 0~20 之间跳动,说明输入信号幅度太小;如果大量集中在 250~255,说明信号已经削顶。常见做法是用一个 8 位 DAC 在回放前先输出一段已知数据(比如 0x40、0x80、0xC0 交替方波),再用 ADC 读回比对,能快速判断 ADC 的线性度有没有问题。这一步比听声判断精准得多,因为人耳对幅度失真不敏感,但对数据连续性极其敏感。

5.3 耳听回放 + 时长核对:最后的整机确认

前面两步都过了,最后才把信号源断开,用麦克风实际说话录音再回放。这一步要验证三件事:音色是否自然、有无明显机械噪声、录音时长是否精确等于设定值。时长核对的方法很巧妙:录音开始时把定时器计数值清零,录音结束时读回累计中断次数,用这个次数除以 8000 得到实际秒数,和预期时长对比。误差在±10ms 以内正常,误差超过 100ms 说明中断有漏执行(回到 4.3 节的坑)。

回放音质的验收标准建议定得实际一点:能听清 90% 以上的语音内容即可,不要追求 HiFi。原因有两个:一是 8kHz 采样率的上限就摆在那里,带宽 4kHz,不可能还原出饱满的低频;二是扬声器本身频响在 300Hz~3kHz 之间,超出这个范围的声音本来就会被衰减。验收时我习惯说一句固定的话,比如「第 3 版固件,8kHz,8bit」,然后回放时听辨数字是否清晰——这段话覆盖了齿音(高频)、鼻音(中频)和爆破音(低频瞬态),哪段糊了能直接定位到频段。实测中多数音质问题出在 FPGA 滤波器截止频率设置偏高,或者 PWM 分辨率不足,逐项排查按 5.2 的串口数据倒推即可。

6. 从能用到好用:中断采样效率、分段索引与低功耗三招

功能通了之后,再往前一步就是把它做成一个「像样」的系统。第一招是让 ADC 采样不再占用 CPU。51 内核单线程执行 Flash 写入时,若用查询方式等待 ADC 完成,8kHz 采样率下每秒有 8000 次等待,每次约 10us,CPU 开销接近 10%。改法是用中断代替查询:ADC 转换完成后自动触发中断,中断里直接启动下一次转换,把数据传输放在主循环。STC 的 ADC 中断标志位只有一个,可以在中断里判断当前是在录音还是回放,分别送 Flash 写入缓冲或 PWM 占空比寄存器,这样整个采样过程只占 CPU 的 5% 不到。

第二招是给每段录音加一个文件头。在每段录音数据前预留 16 字节,存放采样率、量化位数、录音时长和起始地址。回放时先读文件头,再根据文件头里的参数动态配置定时器重装值,就能实现同一套硬件支持 8kHz 和 16kHz 两种模式切换。代价是容量计算从「固定段数」变成「时长可变」,索引表需要额外管理,但换来的是系统通用性大幅提升。文件头格式建议用固定字节序(小端),方便将来移植到 ARM 平台。

第三招是空闲自动关机。用单片机的外部中断唤醒按键,主循环里做一个 5 分钟无操作的计时器,超时后进入掉电模式,电流降到微安级。唤醒后必须先重新初始化 Flash,否则读回来的数据会乱——Flash 在掉电期间内部状态不保存。注意掉电前要把当前录音的索引写入 Flash 持久化区,否则录音文件头会丢失。这三招做完,整个系统从「能跑」变成「能交付」。我曾经在一次课程竞赛评审现场看到很多类似项目,功能都能演示,但能做到「开机即待机、按键有回退、闪断不丢录音」的极少。从那以后,我每做一个单片机存储类项目,都会强制走一遍上电时序、按键状态机和掉电保存这三项检查,最后补一句:希望这些经验能帮你在自己的系统上少走两晚弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询