很多STM32初学者在点亮第一个LED后,下一步想驱动蜂鸣器时,往往会遇到一个看似简单却容易卡住的“坑”:为什么我的蜂鸣器不响?是代码写错了,还是硬件接错了?更让人困惑的是,网上教程里提到的“有源”和“无源”蜂鸣器,到底有什么区别?直接给高电平不响,用PWM驱动也不对,问题到底出在哪里?
这篇文章要解决的,正是这个从“知道”到“做到”的关键一步。我们将以宇创未来启航开发板为硬件平台,手把手带你从零开始,彻底搞懂STM32控制蜂鸣器的原理、电路和代码。你不仅会学会如何让蜂鸣器发出“嘀”的一声,更重要的是,你会理解背后的硬件原理,掌握用PWM驱动无源蜂鸣器演奏简单旋律的方法,并避开那些新手最容易踩的“坑”。
读完本文,你将获得:
- 清晰的硬件认知:彻底分清有源/无源蜂鸣器,看懂驱动电路原理图。
- 完整的工程实践:基于STM32标准外设库或HAL库,完成从GPIO控制到PWM驱动的代码编写。
- 实用的调试技能:学会使用万用表、逻辑分析仪(或示波器仿真)验证信号,快速定位“不响”的问题。
- 能力的进阶延伸:了解如何用定时器PWM生成不同频率的方波,为后续制作音乐盒、报警器等项目打下基础。
我们不止步于让蜂鸣器响起来,而是要让你明白它为什么会响,以及如何让它按你的想法“歌唱”。
1. 蜂鸣器不响?先搞清楚你手上的是“有源”还是“无源”
在动手写代码之前,这是最重要的一步。很多人的项目卡住,就是因为用错了驱动方式。
有源蜂鸣器:
- 内部结构:内部集成了振荡电路。你可以把它理解为一个“完整的发声模块”。
- 驱动方式:非常简单。只要在它的两个引脚上加上合适的直流电压(例如3.3V或5V),它就会以固定的频率(如2.5KHz)持续发声。
- 声音特点:声音频率固定,无法改变。只能发出一种音调。
- 在电路中的标识:通常标有“+”和“-”极性。长脚或标有“+”的为电源正极。
- 类比:就像一个插电就响的电子门铃,你无法控制它响的音调。
无源蜂鸣器:
- 内部结构:内部没有振荡源,本质上是一个微型扬声器,核心是一个电磁线圈和振动膜。
- 驱动方式:必须外部提供交变信号。需要单片机输出一个不断高低变化的方波(PWM信号)来驱动。方波的频率决定了音调(Do、Re、Mi),方波的占空比和持续时间会影响音量和节奏。
- 声音特点:可以通过改变方波频率来发出不同音调,因此可以演奏音乐。
- 在电路中的标识:没有极性(但通常有正负标记,接反了也能响,但可能影响音质)。
- 类比:就像一个大喇叭,你需要不停地向前推、向后拉(提供交变电流)它才能振动发声。推拉的快慢(频率)决定了音高。
宇创未来启航开发板上的蜂鸣器是哪一种?这是关键。你需要查看开发板的原理图。通常,为了教学和功能的扩展性,很多开发板会选用无源蜂鸣器。假设我们的启航开发板也是如此,那么本文的重点将放在如何用STM32的PWM功能来驱动它。
核心判断:如果你的蜂鸣器直接接3.3V就响,那就是有源的,用普通GPIO输出高电平即可。如果不响,那大概率是无源的,必须用PWM驱动。在不确定的情况下,请务必优先查阅你的开发板原理图或手册。
2. 硬件连接与电路原理分析
理解了蜂鸣器类型,我们来看硬件怎么接。单片机GPIO口的驱动能力有限(通常只有几十mA),无法直接驱动蜂鸣器(工作电流可能在几十mA)。因此,我们需要一个“开关”来放大电流——这就是三极管。
下图是一个典型的NPN三极管驱动无源蜂鸣器的电路(假设启航开发板采用类似设计):
VCC (3.3V/5V) | | | | BEEP (蜂鸣器) | | | C (集电极) | NPN三极管 (如S8050) | | E (发射极) --- GND | B (基极) | R (限流电阻,1K-10K) | | GPIO (如PA8)电路工作原理:
- 当STM32的GPIO(PA8)输出高电平时,电流经过电阻R流入三极管的基极(B),三极管导通。此时,蜂鸣器的一端通过三极管连接到地(GND),与电源VCC形成回路,电流流过蜂鸣器,它开始振动发声(前提是GPIO输出的电平在不断翻转,形成方波)。
- 当GPIO输出低电平时,三极管截止,蜂鸣器回路断开,停止发声。
- 电阻R的作用是限制基极电流,保护GPIO口和三极管。
在启航开发板上:这个电路可能已经集成在板子上。你只需要找到原理图中连接蜂鸣器的那个GPIO引脚编号(例如BEEP_GPIO_Pin对应PA8)。你的代码只需要控制这个引脚即可,无需自己搭建电路。
3. 开发环境准备与工程创建
在开始写代码前,确保你的“战场”已经布置好。
硬件准备:
- 宇创未来启航开发板(或其他STM32开发板,需对应修改引脚)
- ST-Link调试器(或板载的调试器)
- USB数据线
- 电脑
软件准备:
- IDE:Keil MDK-ARM (uVision5) 或 STM32CubeIDE。本文示例将基于Keil和标准外设库,因为其对于理解底层寄存器操作更有帮助。使用HAL库的原理是相通的。
- STM32固件库:标准外设库(StdPeriph Lib)或从ST官网下载对应你芯片型号的HAL库/LL库支持包。
- 串口调试助手:如SecureCRT、Putty、或者STM32CubeIDE自带的串口终端,用于打印调试信息(可选,但强烈推荐)。
创建工程: 如果你不熟悉如何从零创建STM32工程,可以按照以下简化步骤,或直接使用开发板提供的示例工程模板。
- 在Keil中新建一个Project,选择你的芯片型号(例如STM32F103ZE)。
- 管理运行时环境(RTE)或手动添加必要的库文件:
core_cm3.c,startup_stm32f10x_hd.s(启动文件,根据芯片容量选择),system_stm32f10x.c,以及stm32f10x_gpio.c,stm32f10x_rcc.c,stm32f10x_tim.c(我们将用到定时器做PWM)。 - 在工程选项中,设置正确的宏定义(如
USE_STDPERIPH_DRIVER,STM32F10X_HD)和头文件路径。 - 新建
main.c文件。
4. 方案一:驱动有源蜂鸣器(简单GPIO控制)
如果你的板子是有源蜂鸣器,或者你想先验证最基本的GPIO输出功能,这个方案最简单。
核心思想:将连接蜂鸣器的GPIO引脚配置为推挽输出模式,输出高电平则蜂鸣器响,输出低电平则停止。
代码实现:
// 文件:main.c #include "stm32f10x.h" // 假设蜂鸣器连接在GPIOA的Pin8, 低电平有效(根据实际电路调整) #define BEEP_GPIO_PORT GPIOA #define BEEP_GPIO_PIN GPIO_Pin_8 #define BEEP_GPIO_CLK RCC_APB2Periph_GPIOA // 蜂鸣器状态控制宏(假设高电平响,低电平停) #define BEEP_ON() GPIO_SetBits(BEEP_GPIO_PORT, BEEP_GPIO_PIN) #define BEEP_OFF() GPIO_ResetBits(BEEP_GPIO_PORT, BEEP_GPIO_PIN) #define BEEP_TOGGLE() GPIO_WriteBit(BEEP_GPIO_PORT, BEEP_GPIO_PIN, \ (BitAction)(1 - GPIO_ReadOutputDataBit(BEEP_GPIO_PORT, BEEP_GPIO_PIN))) /** * @brief 初始化蜂鸣器使用的GPIO * @param 无 * @retval 无 */ void BEEP_GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; /* 开启GPIO时钟 */ RCC_APB2PeriphClockCmd(BEEP_GPIO_CLK, ENABLE); /* 配置GPIO为推挽输出模式,速度50MHz */ GPIO_InitStructure.GPIO_Pin = BEEP_GPIO_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(BEEP_GPIO_PORT, &GPIO_InitStructure); /* 初始状态关闭蜂鸣器 */ BEEP_OFF(); } /** * @brief 简单延时函数(仅供测试,实际项目用定时器) * @param count: 延时计数 * @retval 无 */ void Delay(__IO uint32_t count) { for(; count != 0; count--); } int main(void) { /* 初始化蜂鸣器GPIO */ BEEP_GPIO_Config(); while(1) { BEEP_ON(); // 蜂鸣器响 Delay(0xFFFFF); // 延时一段时间 BEEP_OFF(); // 蜂鸣器停 Delay(0xFFFFF); // 延时一段时间 // 如此循环,蜂鸣器会“嘀...嘀...嘀”地间隔响 } }代码解释:
BEEP_GPIO_Config函数完成了GPIO的初始化:使能时钟、配置引脚模式为推挽输出、初始化为低电平。main函数中的while循环,周期性地打开和关闭蜂鸣器,形成一个简单的报警声。- 重要提示:
Delay函数是非常低效的软件延时,会占用CPU。这里仅用于演示。在实际项目中,请使用定时器中断或系统滴答定时器(SysTick)来实现精确延时。
如果烧录此代码后蜂鸣器能规律鸣响,说明你的硬件连接和基础GPIO控制是正常的。如果是有源蜂鸣器,到这里就结束了。但如果是无源蜂鸣器,它可能只会发出轻微的“嗒”一声,或者完全不响。接下来,我们进入核心部分——PWM驱动。
5. 方案二:驱动无源蜂鸣器(PWM方波驱动)
要让无源蜂鸣器持续发声并控制音调,我们必须产生一个特定频率的方波。STM32的通用定时器(TIMx)的PWM输出功能是完成此任务的绝佳工具。
核心原理:
- 定时器:像一个自动的“节拍器”。我们设置一个计数频率(例如72MHz / 分频值),定时器就会按照这个频率递增计数。
- 自动重装载寄存器(ARR):决定计数器的上限。计数器从0数到ARR,然后归零,循环往复。这个循环的周期决定了输出波形的频率。
- 捕获/比较寄存器(CCRx):与计数器值进行比较。当计数器值小于CCRx时,输出一种电平(如高);大于等于CCRx时,输出另一种电平(如低)。CCRx的值决定了输出波形的占空比。
- PWM模式:通过配置,让定时器通道的输出引脚根据上述比较结果自动翻转电平,从而生成稳定的PWM波。
计算频率: 假设系统时钟SYSCLK = 72MHz,定时器挂在APB1上(时钟为72MHz)。
- 定时器时钟
TIMx_CLK = 72MHz。 - 我们设置预分频器
PSC = 71,则计数器实际时钟CK_CNT = TIMx_CLK / (PSC+1) = 72MHz / 72 = 1MHz。即计数器每1微秒加1。 - 要产生一个频率为
f的PWM波,则波形的周期T = 1/f。计数器需要在这段时间内完成从0到ARR的计数。 - 所以,
ARR = CK_CNT / f - 1。- 例如,要产生
f = 1000Hz的方波,ARR = 1,000,000 / 1000 - 1 = 999。
- 例如,要产生
- 占空比 =
CCRx / (ARR + 1)。对于蜂鸣器驱动,我们通常使用50%的占空比,即CCRx = (ARR + 1) / 2。
代码实现(使用TIM3通道2,对应引脚PA7):
// 文件:beep_pwm.c #include "beep_pwm.h" #include "stm32f10x.h" /** * @brief 初始化定时器TIM3的通道2(PA7)输出PWM * @param 无 * @retval 无 */ void BEEP_PWM_Init(uint16_t arr, uint16_t psc) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; /* 1. 开启时钟 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 开启GPIOA时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // 开启TIM3时钟 /* 2. 配置GPIOA Pin7 为复用推挽输出 */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); /* 3. 配置定时器时基单元 */ TIM_TimeBaseStructure.TIM_Period = arr; // 自动重装载值,决定频率 TIM_TimeBaseStructure.TIM_Prescaler = psc; // 预分频值 TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 时钟分频 TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数模式 TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); /* 4. 配置PWM输出模式(通道2) */ TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; // PWM模式1 TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; // 使能输出 TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; // 输出极性高 TIM_OCInitStructure.TIM_Pulse = 0; // 初始占空比为0,先不响 TIM_OC2Init(TIM3, &TIM_OCInitStructure); // 初始化通道2 TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable); // 使能预装载寄存器 /* 5. 使能定时器 */ TIM_Cmd(TIM3, ENABLE); } /** * @brief 设置蜂鸣器发声频率 * @param freq: 目标频率(Hz) * @retval 无 */ void BEEP_SetFreq(uint32_t freq) { uint32_t arr, psc; uint32_t tim_clk = 72000000; // TIM3时钟假设为72MHz // 简化计算:固定预分频psc=71,则计数器时钟CK_CNT=1MHz psc = 71; if(freq == 0) { TIM_SetCompare2(TIM3, 0); // 占空比设为0,停止发声 return; } arr = 1000000 / freq - 1; // 计算ARR值 if(arr > 65535) arr = 65535; // 防止溢出 TIM_SetAutoreload(TIM3, arr); // 动态修改ARR值改变频率 TIM_SetCompare2(TIM3, (arr+1)/2); // 设置占空比为50% } /** * @brief 蜂鸣器开关 * @param cmd: ENABLE 或 DISABLE * @retval 无 */ void BEEP_Cmd(FunctionalState cmd) { if(cmd == ENABLE) { TIM_Cmd(TIM3, ENABLE); TIM_CtrlPWMOutputs(TIM3, ENABLE); } else { TIM_CtrlPWMOutputs(TIM3, DISABLE); TIM_Cmd(TIM3, DISABLE); } }// 文件:beep_pwm.h #ifndef __BEEP_PWM_H #define __BEEP_PWM_H #include "stm32f10x.h" void BEEP_PWM_Init(uint16_t arr, uint16_t psc); void BEEP_SetFreq(uint32_t freq); void BEEP_Cmd(FunctionalState cmd); #endif /* __BEEP_PWM_H */// 文件:main.c (使用PWM驱动的主函数) #include "stm32f10x.h" #include "beep_pwm.h" #include "delay.h" // 假设你有一个基于SysTick的精确延时库 // 定义中音C(Do)到高音B(Si)的频率(单位:Hz) const uint16_t tone_freq[] = { 0, // 0: 静音 262, // 1: 中音 Do 294, // 2: Re 330, // 3: Mi 349, // 4: Fa 392, // 5: So 440, // 6: La 494, // 7: Si 523 // 8: 高音 Do }; // 示例乐谱:《小星星》片段 (1 1 5 5 6 6 5 -) const uint8_t little_star[] = {1, 1, 5, 5, 6, 6, 5, 0}; const uint8_t beat_duration[] = {4, 4, 4, 4, 4, 4, 8, 8}; // 节拍时长(相对值) int main(void) { uint8_t i; Delay_Init(); // 初始化延时函数(基于SysTick) // 初始化PWM,ARR和PSC先给个初始值,后面会动态改 BEEP_PWM_Init(999, 71); // 初始1KHz频率 while(1) { // 播放《小星星》片段 for(i=0; i<sizeof(little_star); i++) { if(little_star[i] != 0) { BEEP_SetFreq(tone_freq[little_star[i]]); // 设置频率 BEEP_Cmd(ENABLE); // 打开蜂鸣器 } else { BEEP_Cmd(DISABLE); // 静音 } Delay_ms(5000 / beat_duration[i]); // 根据节拍延时,假设四分音符500ms } BEEP_Cmd(DISABLE); // 播放完一段后停止 Delay_ms(2000); // 间隔2秒 } }代码解释与操作流程:
BEEP_PWM_Init函数完成了定时器PWM输出的所有硬件配置。你需要根据开发板原理图,确认蜂鸣器连接的引脚对应的定时器通道(本例为TIM3_CH2 -> PA7)。BEEP_SetFreq函数是关键,它通过公式动态计算并设置ARR值,从而改变PWM输出的频率,即改变音调。main函数中,我们定义了一个简单的音调频率表tone_freq,并编写了《小星星》的乐谱。通过循环,依次设置频率、开启蜂鸣器、持续特定节拍的时间,从而实现音乐播放。- 注意:
Delay_ms函数需要你自己实现一个基于SysTick的精确延时,或者使用HAL库的HAL_Delay。简单的for循环延时在这里会导致节奏严重不准。
6. 运行结果与效果验证
将代码编译、下载到启航开发板后,你应该能听到蜂鸣器开始播放《小星星》的旋律。
如何验证和调试?
- 听声音:这是最直接的验证。如果能听到清晰的、不同音调的声音,恭喜你,成功了!
- 万用表测量:将万用表打到直流电压档,测量连接蜂鸣器的GPIO引脚(如PA7)对地电压。当蜂鸣器发声时,你应该能看到电压值在一个中间值附近波动(例如1.65V左右,因为50%占空比的方波平均电压是VCC/2)。静音时电压应为0V或3.3V(取决于你的电路是低电平有效还是高电平有效)。
- 逻辑分析仪/示波器:这是最专业的调试手段。将探头连接到驱动引脚,你可以清晰地看到一个方波波形。调整
BEEP_SetFreq的参数,观察波形的频率是否随之准确变化。例如,设置freq=1000,你应该能看到周期为1ms的方波。
7. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 蜂鸣器完全不响 | 1. 电源未接通或电压不对。 2. 蜂鸣器类型判断错误(无源当有源驱动)。 3. GPIO引脚配置错误(模式、时钟)。 4. 三极管驱动电路故障或接反。 5. 蜂鸣器本身损坏。 | 1. 用万用表测量蜂鸣器两端电压。 2. 直接给蜂鸣器两端加3.3V直流电,看是否响(判断类型)。 3. 检查代码中GPIO和定时器时钟使能语句。 4. 用万用表测量GPIO引脚在程序运行时的电压变化。 | 1. 确保开发板供电正常。 2. 确认蜂鸣器类型,改用正确的驱动方式。 3. 仔细核对原理图,确认控制引脚。 4. 编写一个简单的GPIO翻转测试程序,验证硬件通路。 |
| 蜂鸣器只响一声或轻微“嗒”一声 | 几乎可以确定是无源蜂鸣器被用直流电平驱动了。直流电只能让振片动一下,无法持续振动发声。 | 用示波器或逻辑分析仪查看驱动引脚波形。如果是一条直线,说明是直流。 | 必须改用PWM方波驱动。参考本文方案二。 |
| 声音很小或沙哑 | 1. 驱动电流不足(限流电阻过大或三极管放大倍数不够)。 2. PWM频率不在蜂鸣器最佳响应范围(通常2K-5KHz)。 3. 蜂鸣器质量问题。 | 1. 测量流过蜂鸣器的电流。 2. 尝试调整PWM频率。 | 1. 减小基极限流电阻R(如从10K改为1K),或更换放大倍数更高的三极管。 2. 尝试将频率设置为蜂鸣器谐振频率(查数据手册,通常约2.5KHz)。 |
| PWM驱动时,声音音调不准或跑调 | 1. 定时器时钟源和分频计算错误,导致实际频率不对。 2. 系统时钟(SYSCLK)配置错误。 3. 延时函数不精确,导致节拍紊乱。 | 1. 用示波器测量实际输出的PWM频率,与理论值对比。 2. 检查 SystemInit()函数或CubeMX的时钟树配置。 | 1. 重新计算PSC和ARR值,确保CK_CNT和freq计算正确。2. 使用SysTick或硬件定时器实现精确的 Delay_ms函数。 |
| 代码编译通过,但下载后没反应 | 1. 下载器连接或驱动问题。 2. 芯片型号选错。 3. 启动文件选错。 4. 没有在 main函数中调用初始化函数。 | 1. 尝试下载一个最简单的LED闪烁程序,验证下载链路。 2. 检查Keil工程配置中的Device选项。 3. 单步调试,看程序是否卡在某个初始化函数。 | 1. 重新插拔ST-Link,安装正确驱动。 2. 核对开发板主控芯片型号。 3. 确保 main函数里调用了BEEP_PWM_Init和BEEP_Cmd(ENABLE)。 |
8. 最佳实践与工程建议
当你掌握了基础驱动后,以下建议能让你的代码更健壮、更易维护:
- 模块化设计:如示例所示,将蜂鸣器驱动相关的函数(初始化、设置频率、开关)独立放在
beep.c和beep.h文件中。main.c只负责业务逻辑(如乐谱)。 - 使用硬件定时器做延时:放弃低效的
for循环延时。使用SysTick或一个基本定时器(如TIM6/TIM7)产生精确的1ms中断,在此基础上实现非阻塞的延时函数,让CPU可以处理其他任务。 - 乐谱数据结构化:定义清晰的数据结构来存储音乐。例如,可以定义一个结构体数组,每个元素包含
音调、节拍、持续时间。typedef struct { uint8_t note; // 对应 tone_freq 的索引 uint8_t beat; // 节拍类型,如4(四分音符),8(八分音符) } MusicNote; const MusicNote song[] = {{1,4}, {1,4}, {5,4}, {5,4}, {6,4}, {6,4}, {5,8}, {0,8}}; - 支持多首歌曲:设计一个播放器状态机,可以加载不同的歌曲数据,并支持播放、暂停、停止操作。
- 考虑功耗:在不需要蜂鸣器时,彻底关闭定时器时钟(
RCC_APB1PeriphClockCmd(DISABLE)),以降低功耗。 - PWM频率范围:人耳可听范围约为20Hz-20KHz。但蜂鸣器有其最佳响应频率(查看数据手册)。通常中音区(261Hz - 987Hz)效果较好。频率过低(<50Hz)会听到明显的“咔嗒”声,过高(>10KHz)可能声音很尖或听不见。
- 占空比影响:50%占空比音量最大。降低占空比可以减小音量,但可能会引起音色变化。可以通过修改
TIM_SetComparex的值来动态调节音量。
9. 总结与进阶思路
通过本文,我们从“蜂鸣器为什么不响”这个具体问题出发,深入剖析了有源与无源蜂鸣器的本质区别,并基于宇创未来启航开发板,给出了从GPIO直接驱动到PWM频率驱动的两套完整解决方案。你不仅学会了让蜂鸣器响,更掌握了通过定时器PWM生成任意频率方波的核心技能。
下一步可以探索的方向:
- 复杂的音乐播放器:结合SD卡或Flash,存储和播放更长的乐谱文件(如MIDI简化格式)。
- 报警与提示音系统:为你的智能家居、物联网设备设计不同的“嘀嘀”声模式,代表不同状态(如开机声、警告声、完成声)。
- 与传感器联动:例如,结合超声波测距模块,让蜂鸣器发出频率随距离变化的警报声;或者结合温湿度传感器,超出阈值时报警。
- 探索其他发声器件:无源蜂鸣器原理类似微型扬声器。理解了PWM驱动,你可以尝试驱动更大的喇叭,配合DAC(数模转换器)输出更复杂的音频波形,迈向真正的音频应用。
驱动一个蜂鸣器是STM32学习路上的一个经典里程碑。它串联了GPIO、时钟系统、定时器、中断(如果使用中断更新频率)等多个核心知识点。希望你在实现功能的同时,能真正理解其背后的硬件原理和软件设计思想,这才是从“入门”到“精通”的关键。建议你将本文的代码和思路收藏,在未来的项目中随时复用和优化。