
在实际嵌入式项目中把舵机和时钟结合起来是一个很有代表性的综合练习。它涉及定时器、PWM、中断、角度计算、硬件驱动等多个知识点而且完成后的效果直观适合作为单片机进阶学习的里程碑。本文围绕“基于STM32舵机时钟”这个主题从舵机控制原理说起逐步搭建硬件电路配置STM32CubeMX生成工程编写核心驱动代码并给出调试方法和常见问题处理。最终实现一个用三个舵机分别模拟时针、分针、秒针的模拟时钟全程使用STM32标准库或HAL库均可但以HAL库为例进行讲解。1. 先理解舵机控制原理再看如何映射到时钟舵机是一种角度伺服电机通过PWM信号控制转动角度。最常见的SG90舵机其控制信号周期为20ms50Hz高电平脉宽在0.5ms到2.5ms之间对应0°到180°。不同脉宽对应的角度关系如下脉宽ms理论角度实际可能范围0.50°0°1.045°约45°1.590°90°2.0135°约135°2.5180°180°实际使用中不同品牌或个体舵机的中位90°可能略有偏差需要校准。时钟的时间映射秒针每分钟转一圈360°即每秒走6°分针每小时转一圈即每分钟走6°时针每12小时转一圈即每小时走30°每分钟走0.5°。我们需要将当前时分秒转换为对应的舵机角度。假设三个舵机分别负责秒针、分针、时针那么每个时间点对应的角度计算公式为秒针角度 当前秒数 * 6分针角度 当前分钟 * 6 当前秒数 * 0.1因为分针每秒也移动0.1°时针角度 (当前小时 % 12) * 30 当前分钟 * 0.5这些角度需要映射到舵机的PWM脉宽。通常定义一个线性映射函数输入角度0~180输出脉宽单位微秒或定时器比较值。例如使用TIM定时器产生50Hz PWMARR设置为1999920msCCR值通过公式计算CCR 500 (角度/180)*2000单位是微秒因为STM32定时器计数周期通常设置为1us。2. 硬件选型与连接必须注意供电2.1 所需物料器件数量说明STM32开发板如STM32F103C8T6最小系统板1块主控含晶振、复位电路SG90舵机或MG996R等3个分别代表时针、分针、秒针5V/2A以上电源适配器1个舵机供电STM32可通过USB供电但舵机需独立电源杜邦线公对母、母对母若干连接信号线、电源、地面包板可选1个方便搭建电路2.2 连接方式三个舵机的信号线橙色分别接到STM32的三个支持PWM输出的GPIO引脚例如PA0、PA1、PA2对应TIM2的通道1、2、3。舵机电源线红色接5V电源正极地线棕色接电源负极和STM32的GND共地。注意STM32的VCC一般用3.3V但舵机需要5V所以必须共地但信号线3.3V对舵机也是兼容的SG90的TTL电平3.3V可识别。注意不要用STM32的3.3V给舵机供电否则电流不足且电压不够舵机无法正常工作。必须用外部5V电源且电流应大于1A三个舵机同时转动峰值电流可能达到1.5A。2.3 硬件连接表STM32引脚舵机信号线备注PA0 (TIM2_CH1)秒针舵机信号橙色线PA1 (TIM2_CH2)分针舵机信号橙色线PA2 (TIM2_CH3)时针舵机信号橙色线5V外部电源正极舵机电源线红色三个舵机并联外部电源负极舵机地线棕色三个舵机并联并接STM32 GND3. 软件环境STM32CubeMX配置与工程生成3.1 使用CubeMX生成初始化代码打开STM32CubeMX选择芯片型号如STM32F103C8。配置时钟源使用外部晶振HSE并配置时钟树使系统时钟为72MHzAPB1定时器时钟为72MHz注意TIM2挂载在APB1上如果APB1预分频器不为1则定时器时钟频率为APB1的两倍需确认。配置定时器TIM2通道1、2、3设置为PWM Generation CH1/CH2/CH3。预分频器PSC设置为71因为定时器时钟72MHz分频后得到1MHz即1us计数一次。自动重装载值ARR设置为1999920ms。初始脉冲宽度Pulse设置为1500对应1.5ms即90°。GPIO配置PA0、PA1、PA2自动设置为推挽输出复用功能。NVIC中断可以不开启因为PWM由硬件自动产生不需要中断。3.2 生成MDKKeil工程选择HAL库在Project Manager中设置项目名称、路径选择Toolchain为MDK-ARM生成代码。之后打开工程在main.c中编写用户代码。4. 核心代码实现从PWM输出到角度映射4.1 启动PWM输出在main函数中调用HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1)等函数启动三个通道的PWM。注意需要先使能定时器并初始化。int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM2_Init(); HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1); // 秒针 HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_2); // 分针 HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_3); // 时针 while (1) { // 更新舵机角度 } }4.2 角度与脉宽映射函数我们需要一个函数输入角度0~180输出对应的CCR值脉宽单位us。根据SG90特性0°对应500us180°对应2500us线性关系。uint16_t AngleToPulse(uint8_t angle) { // 限制角度范围0~180 if (angle 180) angle 180; // 映射500 (angle/180)*2000 // 为避免浮点运算使用整数运算500 angle * 2000 / 180 return 500 (uint16_t)((uint32_t)angle * 2000 / 180); }4.3 设置舵机角度函数通过__HAL_TIM_SET_COMPARE宏直接修改CCR值。void SetServoAngle(TIM_HandleTypeDef *htim, uint32_t Channel, uint8_t angle) { __HAL_TIM_SET_COMPARE(htim, Channel, AngleToPulse(angle)); }4.4 时间获取与角度计算在while循环中我们需要一个时间源。最简单的方法是使用HAL库的HAL_GetTick()它返回系统启动后的毫秒数。我们可以通过它模拟一个RTC计时器但精度有限。更好的做法是使用STM32内部的RTC外设或者使用外部DS3231模块。这里先用软件定时器模拟便于演示。定义一个全局变量保存当前时间秒、分、时每1000ms累加一次。注意实际项目中应该使用RTC。volatile uint32_t sys_second 0; volatile uint8_t sec 0, min 0, hour 0; void TimeUpdate(void) { static uint32_t last_tick 0; uint32_t now HAL_GetTick(); if (now - last_tick 1000) { last_tick now; sys_second; sec sys_second % 60; min (sys_second / 60) % 60; hour (sys_second / 3600) % 12; } }在main循环中调用TimeUpdate然后计算角度并设置舵机。while (1) { TimeUpdate(); // 计算角度 uint8_t sec_angle sec * 6; // 秒针每秒6° uint8_t min_angle min * 6 sec * 0.1; // 分针每分钟6°每秒移动0.1° uint8_t hour_angle (hour % 12) * 30 min * 0.5; // 时针每小时30°每分钟0.5° // 由于角度函数接受整数需要处理小数部分。最简单做法四舍五入或直接取整。 // 这里使用整数计算但会丢失精度。更好的方法用浮点或定点数。 // 为简化分针和时针角度直接取整。 uint8_t min_angle_int min * 6 (sec * 10) / 100; // 近似 uint8_t hour_angle_int (hour % 12) * 30 min / 2; // 每分钟0.5°近似 SetServoAngle(htim2, TIM_CHANNEL_1, sec_angle); SetServoAngle(htim2, TIM_CHANNEL_2, min_angle_int); SetServoAngle(htim2, TIM_CHANNEL_3, hour_angle_int); HAL_Delay(50); // 稍作延迟避免频繁更新 }注意这里的分针和时针角度计算使用了取整会导致步进不平滑。实际应用中秒针每秒钟跳一次分针每分钟跳一次时针每小时跳一次这样更符合真实时钟。但为了模拟连续运动可以每秒钟更新一次让分针和时针也微调。如果追求精确可以在定时器中断中更新但这里作为入门示例先实现基本功能。5. 调试与校准常见问题及排查方法5.1 舵机不转或抖动现象可能原因检查方法解决方法舵机完全不动未供电或信号线接错用万用表测舵机电源电压5V信号线对地电压正常有PWM波形正确连接电源和信号线确认STM32已启动PWM舵机抖动供电不足或PWM频率不对检查电源容量是否≥1A用示波器测PWM频率是否为50Hz换大功率电源重新配置定时器ARR和PSC舵机转到极限位置卡住角度映射超出范围串口打印角度值观察是否超出0~180在AngleToPulse中限制角度范围三个舵机相互干扰共地不良检查所有GND是否连通确保所有GND共地5.2 时间不准确软件模拟时间依赖于HAL_GetTick()但HAL_GetTick()本身是由SysTick中断1ms维护的如果中断被频繁阻塞可能产生误差。另外while循环中的HAL_Delay(50)也会导致时间累积误差。建议使用RTC外设或外部时钟芯片DS3231来获得精确时间。5.3 角度校准每个舵机的中位90°可能不同可以通过单独测试来校准。在初始时让舵机转到90°如果实际指针没有指向90°则调整AngleToPulse函数中的偏移量。例如如果实际角度比理论偏大5°则修改公式为500 angle * 2000 / 180 - 偏移量。6. 最佳实践与扩展方向6.1 使用定时器中断更新角度避免在main循环中频繁调用HAL_Delay改用定时器中断如TIM3每1秒触发一次在中断中更新角度。这样主循环可以处理其他任务。但注意中断服务函数要尽量短。6.2 加入RTC模块使用STM32内部RTC或外部DS3231通过I2C读取时间实现掉电保持。这样时钟上电后自动显示正确时间不需要每次从0开始。6.3 加入OLED显示在LCD或OLED上显示数字时间同时舵机显示模拟指针形成双显示。使用I2C或SPI驱动OLED时间数据共享。6.4 加入按键调时使用两个按键分别调整小时和分钟按一次增加1。在中断中检测按键长按可连续调整。6.5 低功耗设计如果使用电池供电可考虑让STM32进入待机模式RTC唤醒但舵机在大部分时间不需要供电可通过MOS管控制舵机电源只在整点或需要显示时上电。6.6 代码优化清单使用宏定义或枚举定义引脚和通道。角度计算使用定点数或查找表避免浮点运算。添加看门狗防止程序跑飞。使用串口打印调试信息便于观察时间值和角度值。7. 常见排查场景速查表问题快速检查步骤程序下载后没反应1. 检查LED或串口打印是否正常确认MCU运行。2. 用示波器测PA0是否有50Hz方波。3. 用万用表测舵机供电电压。舵机只能转到0°或180°脉宽计算错误检查CCR最小值是否接近500最大值是否接近2500。秒针走一步后退一步舵机控制信号不稳定检查电源滤波电容在舵机电源端并联100~470uF电解电容。舵机发热严重长时间处于堵转状态可能角度超出机械范围或PWM频率不对应50Hz。时间走得太快或太慢检查HAL_GetTick()精度或改用RTC。8. 硬件电路改进建议舵机启动瞬间电流很大尤其是从静止到运动时。建议在每个舵机电源引脚并联一个470uF电解电容并在总电源入口并联一个1000uF电容可有效抑制电压跌落。另外STM32与舵机之间建议加一个电平转换电路如74HCT245但一般3.3V直接驱动SG90也能工作只是信号线不宜过长。9. 完整代码框架HAL库提供main.c核心代码片段省略了初始化和头文件但结构清晰。#include main.h #include tim.h static uint8_t sec 0, min 0, hour 0; uint16_t AngleToPulse(uint8_t angle) { if (angle 180) angle 180; return 500 (uint16_t)((uint32_t)angle * 2000 / 180); } void SetServoAngle(TIM_HandleTypeDef *htim, uint32_t Channel, uint8_t angle) { __HAL_TIM_SET_COMPARE(htim, Channel, AngleToPulse(angle)); } void TimeUpdate(void) { static uint32_t last_tick 0; uint32_t now HAL_GetTick(); if (now - last_tick 1000) { last_tick now; if (sec 60) { sec 0; if (min 60) { min 0; if (hour 12) hour 0; } } } } int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM2_Init(); HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_2); HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_3); // 初始位置全部回中 SetServoAngle(htim2, TIM_CHANNEL_1, 90); SetServoAngle(htim2, TIM_CHANNEL_2, 90); SetServoAngle(htim2, TIM_CHANNEL_3, 90); HAL_Delay(1000); while (1) { TimeUpdate(); // 秒针角度每秒6° uint8_t sec_angle sec * 6; // 分针角度每分钟6°但为了连续每秒加0.1° // 这里简化为每分钟跳一次更省事 uint8_t min_angle min * 6; // 时针角度每小时30°每分钟0.5° uint8_t hour_angle hour * 30 (min / 2); SetServoAngle(htim2, TIM_CHANNEL_1, sec_angle); // 分针和时针只在秒针为0时更新避免频繁抖动 if (sec 0) { SetServoAngle(htim2, TIM_CHANNEL_2, min_angle); SetServoAngle(htim2, TIM_CHANNEL_3, hour_angle); } HAL_Delay(20); } }注意上面的代码中分针和时针只在秒针归零时更新这样更接近真实石英钟的步进行为。如果要实现连续运动如秒针带动分针缓慢移动则需要更复杂的计算并在每秒钟更新所有舵机。10. 学习路径推荐从零开始完成这个项目建议按顺序掌握以下技能STM32开发环境搭建Keil CubeMX。GPIO输出和定时器基本概念。PWM产生原理及配置。舵机规格书阅读和驱动函数编写。时间处理RTC或软件计时。中断和延时控制。调试工具使用示波器、逻辑分析仪、串口。每掌握一项就可在项目中实践。遇到问题先从供电和信号线查起再检查软件参数。这个项目虽然简单但综合了硬件、驱动、算法和调试是嵌入式入门阶段很好的一个综合训练。