
简介这是一份基于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 挪到 PB3USART1 完整保留给调试输出。下面是一段在 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 微秒输出 1kHzCCR 取 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 可能大于 ARRPWM 输出会瞬时变成全高电平电机猛地抽动一下。顺序反过来的原因在于__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 次求平均值既平滑了纹波又不会把响应拖得太慢。这里暂停的只是电机 PWMMCU 和彩屏保持正常运行这样用户能在屏幕上看到堵转报警而不是整机黑屏。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/(1010) 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);AsynchPrediv和SynchPrediv两个参数的乘积决定秒脉冲频率。对于 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 牙刷样机的完整手段。更进一步的做法是给整机做堵转测试用手捏住电机轴观察彩屏显示的错误码和串口日志是否按预期触发保护再用慢动作视频拍摄电机启动瞬间逐帧对比实际响应与状态机的时间戳这是排查软硬件时序争议最直接的办法。本文还有配套的精品资源点击获取