news 2026/9/11 15:51:24

STM32电动牙刷实战:PWM频率匹配、RTC计时与彩屏状态机

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32电动牙刷实战:PWM频率匹配、RTC计时与彩屏状态机

简介:这是一份基于STM32F103C8T6主控的简易智能牙刷完整工程,面向单片机课程设计、毕业设计以及STM32入门进阶开发者,解决RTC时钟掉电保持、PWM电机调速与多模式计时等综合实践问题。压缩包共742个文件,含程序源码工程与原理图/接线示意图,核心代码以h/c源文件、Keil工程文件为主,并附带hex、uvprojx、map等构建与调试文件,整体约23.52MB。已有121人学习下载。内附4个递进式源码工程,从界面设计出发,逐步加入年月日时分秒的RTC实时时钟(VBAT接3.3V掉电不丢失)、各刷牙模式计时,以及基于PWM控制震动电机强弱的最终版本,方便对照学习每一阶段实现思路。配套原理图可快速完成硬件连线,也便于后续扩展物联网、手机APP或更换主控芯片二次开发。

1. 震动幅度不是电压问题,是 PWM 与机械谐振匹配问题

第一台电动牙刷样机做出来不震动,十有八九不是电机坏了,而是 PWM 占空比和频率没有匹配上电机本身。电压给得再足,开关频率落在直流电机机械响应带宽之外,转子只会吱吱叫不出力。这类以 STM32 为核心的小型产品级项目——TFT 彩屏显示时钟日期、PWM 精准调控震动电机、附带 2 分钟刷牙计时——把 MCU 开发里基础但高频的几块外设全串在了一起,SPI 彩屏、定时器 PWM、RTC 实时时钟、按键状态机,一份工程练完基本能独立做小家电原型。适合刚做完点灯想进阶的开发者,也适合要搭完整产品逻辑的工程师。下面按硬件规划、PWM 参数、RTC 与计时状态机、彩屏反馈这条链路逐步拆开,所有参数均按可直接烧录验证的方式给出。

2. STM32+彩屏+震动电机的硬件链路与引脚规划

2.1 为什么选 STM32F103C8T6 而不是普通带 PWM 的 MCU

这类小项目市面上常见的替代方案有 51 系列和 STC 系列,它们也能输出 PWM,但差距主要体现在三处。第一,51 的 PWM 通常靠定时器中断模拟,占空比调节到高频段会占用大量 CPU 时间,彩屏刷新和按键扫描就卡顿;STM32 的定时器 PWM 是硬件输出,CCR 寄存器一改,波形立刻变,不需要 CPU 持续干预。第二,RTC 和 SPI 外设在 HAL 库里有完整现成的驱动模板,开发效率高,排查问题也容易。第三,将来想加低功耗停机、ADC 采集电池电压、I2C 陀螺仪测姿态,F103 的外设数量都留有余量。

引脚规划最关键的一条原则是:先确认复用功能,别把 PWM 输出引脚和按键、彩屏控制脚挤在一起。很多人第一步就翻车在这里。用 STM32CubeMX 配管脚时,看到 TIM2_CH2 默认在 PA1,就不要又把按键放到 PA0 以外的 ADC 通道上导致冲突,虽然 PA0 和 PA1 紧挨着,但前者复用不了 TIM2_CH2。我在这个项目里常用的引脚分配如下。

2.2 震动电机驱动电路与完整引脚分配表

震动电机不能由 STM32 GPIO 直接驱动。一个典型的扁平纽扣电机额定电流在 80mA 到 250mA 之间,启动瞬间可以到 500mA 左右,而 STM32 GPIO 最大输出电流约 25mA,强行驱动轻则转速不足,重则烧毁引脚。常见做法是使用一颗 NPN 三极管或 N-MOS 管做低边开关,PWM 信号接基极或栅极,电机接在电源正极和开关管漏极之间。

电机是感性负载,PWM 关断瞬间会产生反向电动势,必须在这个电机两端反向并联一颗续流二极管,比如 1N5819 或 SS14,否则高压尖峰会击穿开关管,严重时干扰彩屏显示和 RTC 走时。

功能引脚说明
按键 1(模式切换)PA0低电平有效,内部上拉
按键 2(开始/暂停)PA10低电平有效,内部上拉
PWM 输出(震动电机)PA1 / TIM2_CH2通过 1kΩ 电阻接 NPN 三极管基极
TFT 彩屏 SCKPA5SPI 时钟
TFT 彩屏 MOSIPA7SPI 数据线,只写不读
TFT 彩屏 CSPA4片选,低电平有效
TFT 彩屏 DCPB0数据/命令选择
TFT 彩屏 RSTPB1复位控制
RTC 晶振PC14 / PC1532.768kHz 外部低速晶振
串口调试PA9 / PA10USART1,注意与按键 2 复用,按键 2 改到 PB3 更稳妥

上面表格里藏着一个典型坑:PA10 默认是 USART1_RX,如果同时用于按键 2,串口调试会互相干扰。我实际做的时候会把按键 2 挪到 PB3,USART1 完整保留给调试输出。

下面是一段在 HAL 库中初始化引脚并启动 PWM 的代码,适用于已经用 CubeMX 生成工程的情况。

// main.c 中用户代码区 1 GPIO_InitTypeDef GPIO_InitStruct = {0}; // 使能 GPIOD 和 GPIOA 时钟,这里仅示例电机控制引脚 __HAL_RCC_GPIOA_CLK_ENABLE(); // PA1 配置为复用推挽输出,复用功能 AF1 对应 TIM2 GPIO_InitStruct.Pin = GPIO_PIN_1; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // 将 PA1 复用为 TIM2_CH2 __HAL_AFIO_REMAP_TIM2_ENABLE(); // 注意:F103 系列部分封装需要检查重映射 // 启动 PWM 输出 HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2);

代码里__HAL_AFIO_REMAP_TIM2_ENABLE()这一行需要特别说明:F103C8T6 的 TIM2 默认映射就在 PA0 到 PA3,并不需要重映射。只有当你把 PWM 放到 PB10、PB11 这些重映射引脚时才需要打开。工程环境不同,CubeMX 自动生成的代码不一定包含这行,手动加之前务必查一下当前芯片的 datasheet 引脚复用表,否则可能编译通过但 PWM 就是不出波形。

2.3 电源分配与启动电流对彩屏的干扰

震动电机启动瞬间电流大,实测中如果锂电池直接同时给电机和彩屏供电,屏背光会出现明显闪烁。常见做法是电源走线采用星型接地,电机驱动部分的地线和 MCU 逻辑地先在电源输入端单点汇合,不要让电机的大电流回流路径穿过彩屏和晶振下方。NPN 三极管的基极串联电阻取 1kΩ,在 3.3V 逻辑下基极电流约 2.5mA,让三极管进入饱和区即可,阻值太小会增加 MCU 引脚负担,太大则驱动不足。

3. PWM 驱动震动电机的核心:占空比、频率与故障保护

3.1 用定时器产生 PWM:占空比计算公式与参数设置

STM32 的 PWM 输出本质上是用定时器计数器和一个比较寄存器做比较。计数器从 0 计数到 ARR(自动重装载值),每计一个数的时间由预分频器 PSC 决定。比较寄存器 CCR 的值决定了输出电平翻转的时刻,于是输出频率和占空比分别是:

PWM 频率 = 72MHz / ((PSC + 1) * (ARR + 1)) 占空比 = CCR / (ARR + 1)

以 1kHz、初始占空比 70% 为例,PSC 取 71,则72MHz / 72 = 1MHz,也就是计数器每微秒加一;ARR 取 999,计数周期正好 1000 微秒,输出 1kHz;CCR 取 700,输出高电平 700 微秒,占空比 70%。

温度变化、负载变化都不会自动改变这个占空比,所以调节电机力度只需要一条赋值语句:

__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, duty);

这里duty的取值范围是 0 到 ARR,也就是 0 到 999。如果只给 0 到 100 的百分比数值,效果会变成最大只输出 10% 占空比,这是一个非常隐蔽的初级错误。

3.2 高频不一定有力:扫频法找到电机的机械谐振点

扁平震动电机内部是一个偏心转子,它的震动强度不仅取决于占空比,还取决于 PWM 频率是否接近机械系统的自然频率。很多人在这一步有个直觉误区:觉得频率越高震动越细腻、噪音越小,于是直接设成 20kHz 避开人耳可闻范围。结果是电机转子根本跟不上这么快的通断切换,平均下来只有很小的电流通过,振幅反而明显下降。

常见做法是做一个自动扫频测试。从 200Hz 开始,每 50Hz 为一档,每档运行 200ms,同时用手感或加速度计判断震动强度。下面这段代码放在主循环中执行:

// 扫频测试:从 200Hz 到 2000Hz,步进 50Hz uint16_t freq; uint32_t psc = 72 - 1; // 预分频固定,让计数频率为 1MHz for (freq = 200; freq <= 2000; freq += 50) { // ARR = 1MHz / freq - 1 uint16_t arr = (1000000 / freq) - 1; __HAL_TIM_SET_AUTORELOAD(&htim2, arr); // 保持占空比 70%,CCR 跟随 ARR 同步变化 __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, (uint16_t)(arr * 0.7)); // 每档持续 200ms,串口打印当前频率 printf("freq=%dHz duty=%d%%\r\n", freq, 70); HAL_Delay(200); }

注意代码里先改ARR再改CCR的顺序:如果先改 CCR 后改 ARR,在中间那一个计数周期里 CCR 可能大于 ARR,PWM 输出会瞬时变成全高电平,电机猛地抽动一下。顺序反过来的原因在于__HAL_TIM_SET_AUTORELOAD会在更新事件时把影子寄存器同步过去,而 CCR 的值随后写入,避免了超界问题。实测中大多数扁平电机的最佳频率落在 500Hz 到 1.5kHz 区间,你手头那颗具体的最佳点必须通过扫频确认,不同厂家的电机机械特性差异很大。

3.3 占空比分档对应刷牙模式与 PWM 堵转保护

扫频找到最佳频率后,就该把它固化成几个刷牙模式。日常刷牙不是一路猛震就好,轻柔模式用于牙龈敏感人群,强力模式用于清除牙菌斑。

模式名称PWM 频率占空比适用场景
轻柔模式1kHz35%牙龈敏感、儿童
日常清洁1kHz60%日常标准刷牙
强力模式1kHz80%烟渍、茶渍清洁
间歇脉冲1kHz20%按摩牙龈,周期性通断

占空比不建议直接拉到 95% 以上。电机在极高占空比下连续运转,发热量大,而且开关管始终处于导通状态,一旦发生堵转,电流持续上升,几分钟内就可能烧毁电机线圈。

针对堵转场景,最实用的保护方案是加 ADC 采样。在电机供电回路中串联一个 0.1Ω 采样电阻,用 STM32 的 ADC 测量电阻两端压降,换算成电流。堵转电流通常能达到正常工作电流的 3 倍以上,软件里一旦检测到连续 500ms 超阈值,就立刻关闭 PWM 并进入保护状态。

uint32_t adc_current_mA; if (adc_current_mA > 900) // 正常电流约 250mA,堵转 900mA 以上 { __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, 0); // 立即停 PWM // 如果用的是高级定时器,还可以用 TIM_BreakPWMStop 硬件刹车 // HAL_TIM_PWM_Stop(&htim2, TIM_CHANNEL_2); fault_flag = 1; // 置故障标志,彩屏显示错误提示 printf("motor stalled, pwm stopped\r\n"); }

ADC 的采样率和 PWM 频率之间有讲究。如果采样率过快,PWM 通断产生的纹波会让电流读数剧烈跳变,误判堵转;如果采样过慢,500ms 的判定窗口内只采了几次,又可能漏掉短时冲击。实际操作时我习惯用 DMA 循环采样 32 次求平均值,既平滑了纹波又不会把响应拖得太慢。这里暂停的只是电机 PWM,MCU 和彩屏保持正常运行,这样用户能在屏幕上看到堵转报警,而不是整机黑屏。

4. 时钟日期与刷牙计时:RTC 走时与状态机数据流

4.1 外部 32.768kHz 晶振的负载电容与走时偏差

STM32 内部 RTC 需要外部 32.768kHz 晶振提供时基,而晶振电路里两颗负载电容的选择直接影响走时精度。最常见的问题有两个:一是忘记焊电容或焊了贴片电容但容值偏差太大,导致一天慢几十秒;二是晶振起振失败,RTC 完全不走。

负载电容的经验计算方式是:CL = (C1 * C2) / (C1 + C2) + 杂散电容。杂散电容来自 PCB 走线和 MCU 引脚,一般在 2pF 到 5pF。如果 C1 和 C2 都取 10pF,等效负载电容约10*10/(10+10) + 3 = 8pF,适配大部分 32.768kHz 晶振标称的 6pF 到 9pF 负载范围。出现走时偏差时,优先把两个电容同步更换为 7pF 或 12pF 试验,不要只换其中一颗,不对称电容会让振荡波形占空比失衡。

HAL 库的 RTC 初始化代码在 CubeMX 中自动生成,核心初始化参数如下:

// 启用 RTC 时钟,选择 LSE 作为时钟源 RTC_HandleTypeDef hrtc; hrtc.Instance = RTC; hrtc.Init.HourFormat = RTC_HOURFORMAT_24; hrtc.Init.AsynchPrediv = 127; // 异步预分频 hrtc.Init.SynchPrediv = 255; // 同步预分频,配合得到 1Hz HAL_RTC_Init(&hrtc);

AsynchPredivSynchPrediv两个参数的乘积决定秒脉冲频率。对于 32.768kHz 的 LSE 时钟,(127 + 1) * (255 + 1) = 32768,恰好产生 1Hz 的秒更新事件。CubeMX 默认生成的这两个数不需要手动修改,但如果 LSE 停了而代码仍尝试用它配置 RTC,初始化函数会返回 HAL_TIMEOUT。排查顺序是:先量晶振引脚波形,再用逻辑分析仪确认秒中断有没有在触发,最后才考虑改参数。

4.2 刷牙计时状态机的迁移条件与超时恢复

刷牙计时和简单的 HAL_Delay 倒计时完全不同。刷牙中途用户可能停下来漱口,可能按暂停,也可能刷到一半把牙刷放下,系统必须区分这些场景才能正确累计有效刷牙时间。

常见做法是把一次刷牙过程建模为状态机:空闲(IDLE)、刷牙中(ACTIVE)、暂停(PAUSE)、完成(DONE)。标准刷牙时间是 2 分钟,每 30 秒为一个区间,对应口腔的左上、左下、右上、右下四个区域,彩屏到点提示换区域。

typedef enum { STATE_IDLE, STATE_ACTIVE, STATE_PAUSE, STATE_DONE } BrushState; BrushState state = STATE_IDLE; uint32_t total_ticks = 0; // 累计有效刷牙时间,单位 100ms uint32_t pause_ticks = 0; // 暂停持续时长 void brush_10ms_tick(void) { switch (state) { case STATE_IDLE: // 等待按键触发启动,不做任何计时 break; case STATE_ACTIVE: total_ticks++; if (total_ticks == 1200) // 2 分钟 = 1200 * 100ms { state = STATE_DONE; __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, 0); } break; case STATE_PAUSE: pause_ticks++; if (pause_ticks >= 900) // 暂停超过 90 秒视为放弃 { state = STATE_IDLE; total_ticks = 0; } break; case STATE_DONE: // 等待按键复位到 IDLE,或 10 秒后自动复位 break; } }

这段代码的关键设计思想是:计时只累加有效刷牙时间,暂停期间电机停转,时间不增长。第 1200 次 tick 对应 120 秒整,误差来自 10ms 中断本身的抖动,但中断优先级和系统时钟稳定后这个误差可以忽略。暂停超时 90 秒自动退出到 IDLE,同样是防止用户把牙刷放桌上忘了关,电机虽然停了但状态机要自己回到初始状态。

这里最容易被忽略的是按键消抖和状态机跳动的关系。如果按键在 ACTIVE 和 PAUSE 之间切换时没有 20ms 左右的消抖,用户按一次可能被识别成两次,状态机会在激活和暂停之间来回跳,计时结果全乱。消抖建议在按键中断里先启动一个 20ms 软件定时器,延时后再读取电平并切换状态。

4.3 计时不等同于 RTC:跨天与掉电保持的边界

刷牙倒计时不应该直接基于 RTC 的秒数做减法。理由有两个:用户在 23:59:50 开始刷牙,如果代码里用目标时间 = 当前时间 + 120秒,跨过零点时日期变化,计算就会出错;另外 RTC 可能出现校时操作,比如通过串口对时,绝对值跳变会打断倒计时逻辑。正确做法是只记录开始刷牙时的 RTC 时间戳,在每次 tick 里把当前 RTC 时间与开始时间做差,差值和日期无关,只依赖时间戳数值的单调递增。

// 开始刷牙时记录 RTC 秒数 uint32_t start_rtc_sec = HAL_RTC_GetTime(&hrtc, &time, FORMAT_BINARY); // 每次刷新计算已用时间 uint32_t now_sec = HAL_RTC_GetTime(&hrtc, &time2, FORMAT_BINARY); uint32_t elapsed = now_sec - start_rtc_sec; // 无符号减法天然处理跨天

无符号整数减法在这里是个值得写进注释的技巧:如果 now_sec 比 start_rtc_sec 小,说明经过了午夜,unsigned 减法会自动回绕得到正确差值,前提是两次读取时间间隔不超过 2 的 32 次方秒,这对 RTC 场景来说完全成立。刷牙状态本身不需要掉电保持,因为刷到一半断电再开机,用户大概率会重新开始,强行恢复旧状态反而奇怪。RTC 的掉电保持才是重点,需要外部电池或超级电容给 VBAT 引脚供电,工程里 VCC 掉电后 VBAT 依然维持 LSE 和 RTC 寄存器的供电。

5. 彩屏端刷牙界面与整机验证技巧

5.1 在 SPI 彩屏上绘制倒计时进度条

彩屏和电机的联动是这个项目体验感所在。状态机每 10ms 跑一次,但屏幕不需要每 10ms 刷新一次,那样既浪费 SPI 带宽又造成闪烁。常见折中方案是:时间文本每秒刷新一次,进度条每 100ms 更新一格。绘制进度条的核心代码不复杂:

void draw_progress_bar(uint16_t x, uint16_t y, uint16_t width, uint16_t height, uint8_t percent) { // 先擦除旧进度条背景 fill_rect(x, y, width, height, TFT_BLACK); // 已刷时间比例填充白色矩形 uint16_t fill_width = (uint16_t)((uint32_t)width * percent / 100); fill_rect(x, y, fill_width, height, TFT_WHITE); // 外框 draw_rect(x, y, width, height, TFT_GREEN); }

进度条百分比由状态机的total_ticks / 1200 * 100计算。为避免整数除法精度丢失,应该先乘 100 再除总 tick 数,即(total_ticks * 100) / 1200。屏幕刷新放在主循环里,不要放在 10ms 中断里,因为 SPI 传输是阻塞式的,放在中断里会拖垮整个定时器节拍。

5.2 无示波器条件下的 PWM 与整机验证顺序

没有示波器时,验证 PWM 输出最直接的手段是串口打印。在扫频和模式切换的每个关键分支加一行printf,把当前 ARR、CCR、占空比和目标频率打出来,用耳朵听电机转速变化和声音粗细,能大致判断频率切换是否生效。有万用表的话,测电机驱动管基极电压,占空比越高,平均电压越高,可以粗算占空比:基极平均电压除以高电平电压,比如测到 2.1V,高电平 3.3V,占空比约 63%。

电机不转的排错顺序固定为:先查代码轴,确认 PWM 通道已启动且 CCR 不为 0;再查 GPIO 复用配置,PA1 是否被 CubeMX 配成了普通输出;排除软件后用万用表从 MCU 引脚量到三极管基极的通断;最后单独给电机接 3V 电源确认电机本体没有损坏。彩屏花屏则优先检查 SPI 速率和杜邦线长度,超过 20cm 的廉价杜邦线在 18MHz 时钟下很容易产生信号反射,把 SPI 分频系数调到 8 或 16 通常能解决。

走到这里,你已经拥有了验证这台 STM32 牙刷样机的完整手段。更进一步的做法是给整机做堵转测试:用手捏住电机轴,观察彩屏显示的错误码和串口日志是否按预期触发保护,再用慢动作视频拍摄电机启动瞬间,逐帧对比实际响应与状态机的时间戳,这是排查软硬件时序争议最直接的办法。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 15:51:21

WorkBuddy实战:用SenseNova U1.5 Lite实现免费AI生图与4K编辑全流程

1. 从“工具链”到“工作台”&#xff1a;为什么我盯上了 WorkBuddy 跑 U1.5 Lite先交代背景。我平时做内容创作和运营&#xff0c;最耗时间的事情不是写稿&#xff0c;而是配图。以前团队里养着一两个设计岗&#xff0c;后来预算收紧&#xff0c;活儿全回到自己手里。我试过 C…

作者头像 李华
网站建设 2026/9/11 15:51:19

2026自考必备:AI检测工具测评与写作避坑指南

1. 为什么自考考生需要关注AI检测工具&#xff1f;在2026年的自学考试环境中&#xff0c;AI辅助写作工具的普及率已经达到惊人的87%&#xff08;数据来源&#xff1a;2025年全球教育技术调查报告&#xff09;。这带来一个严峻问题&#xff1a;如何区分考生原创内容和AI生成内容…

作者头像 李华
网站建设 2026/9/11 15:50:28

论文加上参考文献和脚注后AI率会变吗:三款检测工具同稿前后对比

论文加上参考文献和脚注后AI率会变吗&#xff1a;三款检测工具同稿前后对比 在毕业论文定稿自查的最后关头&#xff0c;很多同学都会遇到一个令人困惑的细节问题&#xff1a;论文加上参考文献和脚注后AI率会变吗&#xff1f;很多同学在修改初稿时习惯把文末上百篇参考文献&…

作者头像 李华
网站建设 2026/9/11 15:50:15

从零实现Viola-Jones人脸检测:Haar特征+AdaBoost+滑动窗口

简介&#xff1a;本资源是一套完整的人脸检测毕业设计实现方案&#xff0c;面向计算机视觉初学者与本科毕设学生&#xff0c;聚焦Haar-like特征提取与AdaBoost级联分类器的工程落地。项目基于MIT人脸数据库&#xff08;2429张2020人脸样本3542张非人脸样本&#xff09;完成训练…

作者头像 李华