STM32定时器PWM控制震动电机:时钟树与占空比计算及彩屏显示实践 📅 发布时间:2026/9/11 19:24:07 👁 浏览次数: 简介基于STM32F103C8T6主控芯片联合ST7735S TFT彩屏实现界面显示通过PWM调控震动电机并完成时钟日期年月日时分秒显示与掉电不丢失、刷牙模式计时等功能是一个完整可上手的单片机实物项目。资源包共742个文件压缩后23.52MB以C语言源码.c/.h、Keil工程文件.uvprojx及编译产物.hex/.axf为主另附原理图/接线示意图便于直接编译与硬件接线。工程内按开发阶段拆分四个程序源码版本从基础界面设计到加入RTC掉电保存、刷牙计时再到最终PWM电机控制层次清晰适合毕业设计、课程设计或STM32进阶学习参考。已有121人学习下载资料不仅包含最终程序还保留了各阶段代码可对照理解RTC初始化、定时器中断和PWM调压的实现思路对快速完成类似项目很有帮助。1. 给“STM32彩屏PWM震动电机”牙刷项目画边界先从定时器时钟下手把震动电机接到 STM32 上最常出问题的不是 PWM 波形本身而是定时器时钟树没理清导致频率怎么算都不对。这个基于 STM32、彩屏、PWM 调控震动电机的简易牙刷项目本质上是一条完整链路定时器输出 PWM 驱动偏心震动电机TFT 彩屏显示时钟日期和刷牙倒计时再用状态机守住 2 分钟刷牙周期。它不涉及复杂算法却覆盖了时钟树配置、定时器分频、电机驱动电路、TFT 显示和电源抗干扰这几道硬工序适合做毕业设计或宿舍桌面小实物。下面按实际做实物时会踩的顺序把整套方案拆开讲。2. STM32 与彩屏选型震动电机驱动的引脚分配和 CubeMX 最小工程2.1 为什么优先选 F103C8T6 和 SPI 接口 TFT 彩屏主控选 STM32F103C8T6 是我在这个项目上的默认做法。这颗芯片的定时器资源足够TIM1/TIM8 是高级定时器TIM2/TIM3/TIM4 是通用定时器输出几路 PWM 绰绰有余CubeMX 的代码生成和 Keil5 的芯片包支持都非常成熟遇到问题可以查到的资料最多。如果手头只有更大封装的 F103RCT6 或国产兼容芯片工程基本不用改。彩屏建议选 SPI 接口的 ST7789 或 ILI9341 驱动 TFT常见尺寸是 1.8 寸或 2.4 寸SPI 屏总共只需要 SCK、MOSI、CS、DC、RST 五根信号线。并口屏虽然刷新快但至少占用 16 个 GPIOF103C8T6 只有 48 脚接完屏幕再给电机、RTC 和按键留引脚会非常紧张。屏幕读方向通常用不到MISO 可以不接这也是 SPI 彩屏在这个项目里最大的优势。2.2 震动电机驱动三极管、MOS 管和驱动模块怎么选震动电机本质是带偏心质量的直流电机单向旋转即可不需要换向所以不像舵机那样需要 50Hz 周期信号也不一定需要 H 桥。常见的驱动方案有三种我在不同板子上都用过差异集中在饱和压降、接线复杂度和抗堵转能力上。驱动方式典型器件优点缺点适合场景NPN 三极管2N2222、SS8550便宜、好买饱和压降大3.3V 下功率损耗明显小电流震动马达临时验证N-MOS 管AO3400、SI2302导通电阻低压降低需要栅极电阻和下拉焊接要求稍高电池供电、电流较大的实物电机驱动模块L9110S、DRV8833接线简单自带续流保护体积大、成本略高快速出实物或做课程设计我做实物更倾向用 AO3400 这类逻辑电平 MOS 管。STM32 的 GPIO 输出 3.3VAO3400 的 Vgs 门槛电压在 1V 到 2V 之间可以直接被 3.3V 驱动。栅极串一个 100Ω 电阻抑制振铃再并一个 10kΩ 下拉电阻保证上电瞬间不误开通。L9110S 模块适合不想碰分立元件的人缺点是多一块板子走线反而麻烦。2.3 CubeMX 时钟树与引脚分配定时器时钟是 PWM 频率的地基在 CubeMX 中新建工程后要勾选 RCC、SPI1、TIM3以及 GPIO 输出。时钟树是关键F103 系统时钟 72MHzAPB1 分频为 2 时挂在 APB1 上的 TIM3 时钟会自动乘以 2 变成 72MHz。很多人直接把 APB1 的 36MHz 拿去算 PWM 频率结果差了一倍。我用的引脚分配如下注意 PWM 通道避免和 SPI 引脚冲突功能引脚说明SPI1_SCKPA5彩屏时钟SPI1_MOSIPA7彩屏数据SPI1_CSPA4彩屏片选软件拉低LCD_DCPB0数据/命令选择LCD_RSTPB1复位可接 GPIO 控制TIM3_CH1PA6输出 PWM 到电机 MOS 管栅极LCD_BLPB10背光可独立 PWM 调亮度LCD 的 MISO 不接SPI1 只做写方向。若后续要读屏幕 ID再把 PA6 挪去做 MISO但这样就会占用 TIM3_CH1所以引脚定案前要先把优先级想清楚。2.4 最小工程 main 骨架先让时钟、SPI 和 PWM 各自跑起来CubeMX 生成工程后建议先在 main 函数里只做初始化然后分别验证屏幕亮、PWM 波形、时钟走字再合并业务逻辑。下面的骨架是我常用的起点int main(void) { HAL_Init(); SystemClock_Config(); /* 72MHz 主频APB1/APB2 分频决定定时器时钟 */ MX_GPIO_Init(); /* GPIO 模式与复用功能配置 */ MX_TIM3_Init(); /* TIM3 输出 PWM先算好 PSC 和 ARR */ MX_SPI1_Init(); /* SPI1 驱动 TFT 彩屏 */ lcd_init(); /* 初始化 ST7789/ILI9341 */ while (1) { lcd_show_time(Get_Current_Time()); /* 每秒刷新时钟日期 */ motor_level_update(brush_level); /* 按刷牙模式调整占空比 */ HAL_Delay(100); } }这段代码的逻辑说明初始化顺序必须是时钟 → GPIO → 外设因为所有外设寄存器都依赖系统时钟SPI 初始化要放在 LCD 初始化之前否则屏幕发指令时时钟还没有建立。motor_level_update放在主循环是为了让按键切换模式后能快速响应刷牙过程中的渐变则放到定时器中断里做具体代码在下一章给。3. PWM 调控震动电机的频率、占空比和驱动电路设计3.1 PWM 频率对电机的影响为什么不能照抄舵机的 50Hz很多人拿到“PWM 控电机”第一反应是查舵机例程但舵机 50Hz 是位置伺服信号电机接收的是脉冲宽度信息直流震动电机接收的是等效电压两者思路完全不同。直流电机的机械时间常数通常在几十毫秒量级PWM 频率太低时转速会跟着通断周期波动手感上就是一阵一阵的顿挫感。下面是直流震动电机不同 PWM 频率下的典型表现频率范围现象评价50Hz 200Hz能转但转速明显波动伴随周期性嗡嗡声不推荐1kHz 左右有可闻的啸叫部分电机声音尖锐有些廉价牙刷用这个区间5kHz 10kHz转速平滑噪声低我一般优先选这里20kHz 以上听不见但 MOS 开关损耗明显上升需要驱动管和布线配合我把 PWM 频率定在 10kHz这个频率对大多数震动电机都合适。计算前先把定时器时钟确认好F103 的 TIM3 时钟是 72MHz公式为PWM 频率 定时器时钟 / ((ARR 1) * (PSC 1))占空比 CCR / (ARR 1)如果 PSC 7ARR 899那么频率 72MHz / (8 * 900) 10kHz占空比分辨率是 1/900足够做 1% 精度的调速。注意占空比算的是 CCR 除以 ARR1而不是除以 ARR这一步写错会带来很小的误差但在电机调速这种场景里不影响。3.2 PWM 占空比计算公式与动态调速代码占空比公式的意义在于把“目标电压”映射成 CCR。假设电机额定电压 5V当前电源 5V想输出 60% 电压CCR 就是 (ARR1) * 60 / 100。HAL 库里用__HAL_TIM_SET_COMPARE更新比较值下面这段代码可以直接用在刷牙模式切换里void Motor_Set_Duty(uint8_t percent) { uint32_t arr __HAL_TIM_GET_AUTORELOAD(htim3) 1; uint32_t ccr arr * percent / 100; __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, ccr); } void Motor_Ramp_To(uint8_t target, uint16_t step_ms) { uint8_t now Motor_Get_Duty(); while (now ! target) { now (now target) ? now 1 : now - 1; Motor_Set_Duty(now); HAL_Delay(step_ms); } }逻辑说明Motor_Set_Duty的入参是百分比内部根据 ARR 自动换算 CCR这样上层业务不需要关心定时器细节Motor_Ramp_To是渐变函数步长step_ms控制每变化 1% 的延时。启动时如果直接给 80% 占空比电机启动电流会瞬间冲到稳态的好几倍用渐变更安全。这个函数后面做软停机和模式切换都会复用。3.3 驱动电路防反电动势以及 PWM 故障保护与刹车MOS 管驱动电机时最容易被忽略的是续流二极管。MOS 关断瞬间电机电感会让电流继续流动漏极电位会瞬间飙高可能击穿 MOS 管。正确做法是在电机两端反向并联一个肖特基二极管比如 SS14接法是二极管阴极接电机正极电源端阳极接 MOS 管漏极这样 PWM 关断时电流可以从电机负极经二极管回到电源正极形成续流回路。“PWM 故障保护”和“刹车”这两个词在电机控制里经常一起出现。如果用的是 F103 的高级定时器 TIM1/TIM8可以启用 Break 输入把硬件过流比较器的输出接到 BKIN 引脚一旦电机堵转引起过流硬件立刻封锁 PWM 输出不需要 CPU 响应。具体到震动电机项目堵转时间通常很短风险不如大功率电机高所以这个功能可以做成预留接口。若驱动板是 L9110S 这类 H 桥模块死区和保护由芯片内部处理单片机侧无需额外配置。3.4 刷牙模式的 PWM 曲线刷牙模式本质上就是预设占空比序列。轻柔和强力模式的区别不是频率而是等效电压模式占空比等效电压(5V)适用场景轻柔30%1.5V敏感牙龈标准60%3.0V日常清洁强力85%4.25V烟渍茶渍模式切换时我会先停 100ms 再启动防止切换瞬间产生电流尖峰。这个暂停可以用关闭定时器通道实现__HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, 0)会把输出拉低但占空比归零和关闭输出在逻辑上是有区别的。占空比归零后电机因为惯性还会继续转但要不了多久就停关闭输出则连驱动脉冲都停了更适合紧急刹车场景。4. 时钟日期显示与刷牙计时状态机在 STM32 上的落地4.1 时钟日期方案外部 RTC 模块和片内 RTC 怎么选“时钟日期”在这个项目里有两条路片内 RTC 加外部 32.768kHz 晶振或者外接 DS3231/DS1302 模块。片内 RTC 省成本但 F103 的 RTC 本质是一个秒计数器要显示年月日得自己做日期换算掉电保持还需要 VBAT 引脚接电池否则断电时间就丢了。方案精度掉电保持代码量适合场景片内 RTC LSE VBAT取决于晶振需要纽扣电池中日期需换算电池常驻的成品DS3231 模块 I2C温补晶振精度高模块自带电池低直接读寄存器实物演示、毕业设计我建议做实物直接用 DS3231。它内部自带电池时间走准了之后拔掉 USB 线也不丢代码只要通过 I2C 读寄存器即可省去 F103 RTC 的 BKP 寄存器和日期换算逻辑调试时间能少一半。4.2 用 I2C 读取时钟日期并在 TFT 彩屏上格式化显示DS3231 的寄存器是 BCD 编码读取后需要转成十进制。下面的函数读取时间和日期结果可以直接用于 LCD 字符串输出void RTC_Read_Now(RTC_TimeTypeDef *t, RTC_DateTypeDef *d) { uint8_t regs[7] {0}; uint8_t addr 0x00; /* 从秒寄存器开始连续读 7 字节 */ HAL_I2C_Mem_Read(hi2c1, 0x68 1, addr, 1, regs, 7, 100); t-Hours ((regs[2] 4) 0x03) * 10 (regs[2] 0x0F); t-Minutes ((regs[1] 4) 0x07) * 10 (regs[1] 0x0F); t-Seconds ((regs[0] 4) 0x07) * 10 (regs[0] 0x0F); d-Date ((regs[4] 4) 0x03) * 10 (regs[4] 0x0F); d-Month ((regs[5] 4) 0x01) * 10 (regs[5] 0x0F); d-Year ((regs[6] 4) 0x0F) * 10 (regs[6] 0x0F); } char time_buf[32]; sprintf(time_buf, %02d-%02d-%02d %02d:%02d:%02d, d.Year, d.Month, d.Date, t.Hours, t.Minutes, t.Seconds);这段代码的关键是 BCD 转十进制的公式高四位乘以 10 加上低四位。DS3231 的从机地址是 0x68I2C 读写时需要左移一位变成 8 位地址也就是0x68 1。寄存器的位宽限制了范围比如秒寄存器最高位是 CH 标志读取时只取低 7 位这也是 0x07的原因。4.3 刷牙计时状态机倒计时、暂停和 30 秒分区提醒刷牙逻辑不建议用延时函数硬等 120 秒而是把状态机放在主循环里用秒级 tick 驱动。状态分为空闲、刷牙、暂停三态转换关系如下当前状态触发事件下一状态电机动作空闲按下开始键刷牙软启动到目标占空比刷牙按下暂停键暂停软停机保持倒计时剩余值刷牙倒计时归零空闲停机并提示暂停按下开始键刷牙从剩余时间继续软启动状态机的代码骨架可以这样写typedef enum { STATE_IDLE, STATE_BRUSH, STATE_PAUSE } brush_state_t; brush_state_t state STATE_IDLE; uint16_t remain_sec 120; uint8_t last_mark 0; void Brush_Task(void) { uint8_t key Key_Scan(); switch (state) { case STATE_IDLE: if (key KEY_START) { state STATE_BRUSH; remain_sec 120; last_mark 4; /* 需要提醒 4 次90/60/30/最后 */ Motor_Ramp_To(brush_level, 5); } break; case STATE_BRUSH: if (key KEY_PAUSE) { state STATE_PAUSE; Motor_Ramp_To(0, 5); break; } if (remain_sec % 30 0 remain_sec ! 120 last_mark ! remain_sec) { last_mark remain_sec; /* 每 30 秒短促震动一次 */ Motor_Set_Duty(100); HAL_Delay(200); Motor_Set_Duty(brush_level); } break; case STATE_PAUSE: if (key KEY_START) { state STATE_BRUSH; Motor_Ramp_To(brush_level, 5); } break; } if (state STATE_BRUSH remain_sec 0) { if (--remain_sec 0) { state STATE_IDLE; Motor_Ramp_To(0, 5); Buzzer_Beep(); } } }这里的last_mark是为了防止每次循环都触发% 30判断。倒计时从 120 秒开始减到 90、60、30 时各提醒一次每次脑内模拟一次“换区”正好把口腔分成四个区域。状态机里没有用阻塞延时做长等待短促震动提醒里的HAL_Delay(200)是可以接受的因为 200ms 内不处理按键不会造成逻辑错误。4.4 TFT 彩屏绘制倒计时进度条时间显示可以直接刷字符串但倒计时进度用条状图更直观。下面这个函数画一个带背板的进度条百分比按已刷时间计算void Draw_Progress_Bar(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint8_t percent) { uint16_t filled (uint16_t)(w * percent / 100); lcd_fill_rect(x, y, filled, h, COLOR_GREEN); /* 已刷区域 */ lcd_fill_rect(x filled, y, w - filled, h, COLOR_DARKGRAY); }进度条刷新频率不需要太高每秒一次足够否则屏幕 SPI 持续占用总线会影响其他外设。percent的计算是(总秒数 - 剩余秒数) * 100 / 总秒数刷满时进度条到达右侧边缘同时状态机进入停止流程。5. 实物接线、示波器验证和最后收尾的电机软停机技巧5.1 实物接线清单与晶振电容计算实物接线时电机一定要和单片机分开供电。单片机用 3.3V电机用 5V 或电池直供两者只共地。如果电机和屏幕共用一个电源电机启动瞬间的电压跌落会把 TFT 拉花屏甚至导致 RTC 走时异常。模块引脚接 STM32备注TFT SCK / MOSI / CS / DC / RSTPA5 / PA7 / PA4 / PB0 / PB1背光可接 PB10MOS 栅极PA6经 100Ω 电阻栅极再加 10kΩ 下拉MOS 漏极电机负极端二极管阳极接此处电机正极电源 5V二极管阴极接此处DS3231 SDA / SCLPB7 / PB6I2C1需接上拉 4.7kΩ晶振电容也是一个常见坑。负载电容公式是 CL C1 * C2 / (C1 C2) CstrayCstray 是 PCB 走线和芯片引脚引入的杂散电容大约 2 到 3pF。如果晶振规格的负载电容是 12pF取 Cstray 为 2pF那么 C1 C2 2 * (12 - 2) 20pF。直接套数据手册上的典型值不一定准市售 8MHz 晶振常见 12pF 和 18pF 两种负载买晶振时看清规格再选电容。5.2 用逻辑分析仪验证 PWM 占空比和排错清单PWM 不输出时先用示波器或逻辑分析仪测量 PA6 引脚而不是先怀疑程序。逻辑分析仪上可以直接量高电平时间和低电平时间占空比等于高电平时间除以整个周期。注意接地夹要夹在 MCU 的 GND 附近夹在电机地线会造成共地干扰。现象优先检查点PA6 无波形CubeMX 中 TIM3 通道是否 EnableGPIO 是否配置为 AF 复用PWM 频率差一倍APB1 定时器是否自动 ×2PSC 和 ARR 是否按 72MHz 计算电机有波形但不转占空比是否太低电机电源是否独立MOS 栅极电压够不够TFT 花屏SPI 分频降到 8 或 16DC 引脚模式是否设置成输出推挽RTC 时间复位DS3231 的电池是否装好I2C 上拉电阻是否缺失另外 Keil5 打开新工程时如果 Device 列表里找不到 STM32F103C8说明芯片包还没安装要在 Pack Installer 里安装对应 DFP。这个问题在多人协作发工程时很常见。5.3 最后一个技巧用软停机代替直接关 PWM项目收尾时我会给停机动作加一层保护不让占空比直接从 80% 跳到 0而是复用前面的Motor_Ramp_To(0, 5)慢慢降下来。这样做的直观收益是电机的反电动势冲击变小电池电压不会突然跳动TFT 彩屏和 DS3231 的供电更稳定。如果后面想升级自动刷牙流程比如轻揉 30 秒再自动切到标准模式可以把占空比序列放进数组用定时器的 DMA 请求自动更新 CCR。F103 的 TIM3 支持更新事件触发 DMA配置时把内存地址指向数组、外设地址指向TIM3-CCR1并设置为循环模式。不过对震动牙刷来说 CPU 负载很低DMA 属于可选项真正的收益是让 PWM 切换过程和主循环解耦这一点在以后做多路电机控制时会更明显。本文还有配套的精品资源点击获取