尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STC15F104W定时器中断PWM舵机控制实现

STC15F104W定时器中断PWM舵机控制实现 简介基于15F104W微控制器的舵机控制程序示例包面向嵌入式开发入门者与机器人/无人机等需要角度控制的爱好者演示了通过IO口高低电平变化驱动舵机以及串口十六进制转十进制并发送的配套例程可帮助理解微控制器定时器、PWM信号生成与串行通信的基本思路。压缩包共30个文件包含C源文件、Keil工程配置uvproj/uvopt、汇编启动文件、HEX烧录文件以及lst、obj、m51、lnp等编译辅助文件整体约55KB结构简短紧凑适合直接打开工程对照学习。目前已有390人学习下载。借助这两个示例读者可以快速掌握15F104W的工程搭建、PWM/IO端口控制逻辑、串口数据解析与发送流程也便于在此基础上扩展闭环控制或双机协同功能是学习嵌入式电机控制与单片机编程的实用参考资料。 15f104w这个芯片老玩单片机的人应该都不陌生8脚小封装内置时钟外围几乎什么都不用加一片IC就能跑完整个控制逻辑。我最近用15f104w写了一个舵机控制程序核心就一句话用定时器稳定产生20ms周期、0.5ms~2.5ms脉宽的PWM信号把0°到180°的角度值映射成舵机转动。这个方案做车模云台、小型机械臂、摄像头转向都挺实用也适合刚接触51内核、想彻底搞懂定时器中断的初学者当入门项目。整个项目做下来最深的感受是舵机控制本身不难难的是把“周期稳定”和“脉宽精确”这两件事同时做好。网上很多例程用delay产生PWM舵机也能转但一转快就抖挂上负载就卡原因都在于周期不稳定。这次我完全靠定时器中断来驱动波形把CPU从PWM产生的脏活里解放出来代码量不大效果却非常稳。下面把整个设计思路、参数计算、完整代码和踩过的坑都记录下来。1. 项目定位为什么选15f104w做舵机控制器1.1 需求与选型背景项目来源是我做一台小型摄像头云台需要一个迷你的舵机控制器塞进云台臂内部。当时待选的芯片有STM8S003、STM32F030和STC15F104W。STM32强但在这类单路舵机控制上属于杀鸡用牛刀STM8S003也不错但烧录器得额外准备。STC15F104W吸引我的点就三个SOP8封装足够小、内置11.0592MHz/12MHz时钟不需要外部晶振、STC-ISP串口下载一条USB-TTL线就搞定。最关键的是舵机控制本质是“定时器IO翻转”的任务对指令集和主频要求很低15f104w的1K Flash和64字节RAM虽然听起来寒酸但放一个PWM扫描程序绰绰有余。实际写完代码编译出来不到600字节资源还很宽裕。选型时还有一个容易被忽略的点15f104w内部没有硬件PWM模块只有两个16位定时器。这意味着PWM必须用软件模拟。不过正因为没有硬件PWM整个波形生成逻辑完全掌握在自己手里反而能把定时器中断的用法练透。如果你后续换别的单片机这套代码移植过去也非常快。1.2 15f104w关键资源盘点先列一下这颗芯片让我印象深刻的资源资源参数在本项目的用途封装SOP8带散热焊盘直接焊在转接板上体积很小可用IO最多6个P3.0~P3.5一路舵机信号一路LED可选按键程序存储1K Flash编译产物约600字节够用运行内存64字节SRAM全局变量不多注意不要大手大脚内置时钟12MHz可配置定时器初值好算精度足够舵机用复位内置上电复位可选外部复位默认P3.5是复位脚需注意定时器2个16位定时器本项目用定时器01T模式下载UART冷启动进入ISPSTC-ISP软件9600波特率这里特别提醒P3.5默认是复位输入引脚如果你想把它当普通IO用必须在使用STC-ISP烧录时把“复位脚用作IO口”的选项勾上。这个细节我后面还会反复强调因为它直接决定了一路IO能不能用。2. 舵机控制的基础原理与参数换算2.1 舵机脉冲信号怎么决定角度标准模拟舵机有三根线电源正、电源地、信号线。信号线上要持续送给舵机一个周期约20ms、高电平脉宽在0.5ms到2.5ms之间的重复脉冲。舵机内部电路会测量这个脉宽然后换算成对应的转角0.5ms 高电平 → 0°1.5ms 高电平 → 90°2.5ms 高电平 → 180°脉宽在0.5ms~2.5ms之间线性变化舵机角度也跟着线性变化。这里要理解一个概念舵机看的是“高电平持续了多长时间”不是看整个波形的占空比百分比。所以最合适的比喻是“每20ms问一次舵机你要转到哪儿”舵机答“我保持高电平多少多少毫秒”。正因为是查脉宽而不是查频率所以周期只要在舵机允许范围内一般是10ms~30ms稳定即可但必须周期稳定。2.2 定时器初值这样计算最省事本项目采用定时器0中断每0.1ms进入一次中断在中断里累加计数拼出20ms周期和0.5ms~2.5ms的脉宽。为什么选0.1ms而不是1ms因为1ms分辨率对应90°的步进角度太粗舵机转起来像一顿一顿的0.1ms分辨率对应约9°步进已经比较平滑了各品牌舵机本身也达不到0.1ms的重复精度。定时器初值计算过程如下系统时钟使用内部12MHz定时器0配置为1T模式即定时器计数频率等于12MHz。12MHz下每1us计数12次。要达到0.1ms也就是100us的定时周期需要计数100 × 12 1200次。16位定时器从0计到65535溢出所以初值 65536 - 1200 64336换算成16进制是0xFB50。所以中断服务函数里重新装载初值就是TH0 0xFB; TL0 0x50;。这里有一个选型小技巧为什么选12MHz内部时钟而不是11.0592MHz因为11.0592MHz是串口波特率友好的频率但计算定时器初值会产生小数点0.1ms需要计数1105.92次只能取整1106或1105导致定时有微小误差。而12MHz下1200正好是整数定时精度更高。既然本项目只用串口下载下载时9600波特率12MHz也能满足果断选12MHz。2.3 角度到脉宽的映射与限幅有了0.1ms时基脉宽就可以用整数表示0.5ms对应51.5ms对应152.5ms对应25。角度0~180线性映射到脉宽5~25的公式是duty 5 angle × (25 - 5) / 180也就是duty 5 angle × 20 / 180。如果angle0duty5angle90duty15angle180duty25。这个公式看起来简单实操时一定要加限幅。万一angle因为传感器异常变成200甚至255计算出的duty会超过25脉宽超过2.5ms甚至接近20ms舵机会直接打到底并堵转长时间堵转会烧舵机。所以我习惯在写进全局变量之前做双重保护角度限幅angle超过180就强制等于180。脉宽限幅最终duty小于5按5算大于25按25算。3. 固件代码与核心实现3.1 开发环境与工程配置代码使用Keil C51编写编程器就是STC-ISP软件加一块USB-TTL。新建工程时Keil里不一定有STC15系列型号可以直接选择Atmel的AT89C52作为替代然后手动包含STC官方提供的STC15F104W.H头文件。如果你嫌头文件难找可以用STC-ISP软件里的“头文件生成”功能一键生成。工程配置里需要注意晶振频率选项可以不写因为Keil只影响仿真时间实际运行以芯片内部IRC为准。编译优化建议选默认Level 8不会影响这个程序。烧录时在STC-ISP里选择型号STC15F104W波特率9600勾选“内部IRC时钟12MHz”然后点“下载/编程”给目标板断电再重新上电就能完成冷启动烧录。3.2 完整的定时器PWM代码下面是完整的单路舵机控制代码控制舵机在0°和180°之间来回摆动。所有变量和中断逻辑都做了注释。#include STC15F104W.H #define T100US_NUM 1200 // 12MHz、1T模式、0.1ms对应的计数次数 sbit SERVO P3^2; // 舵机信号线接P3.2 volatile unsigned char current_duty 15; // 当前脉宽单位0.1ms151.5ms90度 unsigned int tick_count 0; // 0.1ms计数器 unsigned char high_count 0; // 高电平持续时间计数 void Timer0_Init(void) { AUXR | 0x80; // 定时器0设为1T模式 TMOD 0xF0; // 清空T0模式位 TMOD | 0x01; // T0工作在模式116位定时器 TH0 (unsigned char)((65536 - T100US_NUM) 8); TL0 (unsigned char)(65536 - T100US_NUM); ET0 1; // 开定时器0中断 EA 1; // 开总中断 TR0 1; // 启动定时器 } void Timer0_ISR(void) interrupt 1 { // 重新装载初值 TH0 (unsigned char)((65536 - T100US_NUM) 8); TL0 (unsigned char)(65536 - T100US_NUM); tick_count; if (tick_count 200) // 200 × 0.1ms 20ms周期到了 { tick_count 0; SERVO 1; // 脉冲起始沿拉高 high_count 0; } else { high_count; if (high_count current_duty) // 到达目标高电平时间 { SERVO 0; // 拉低脉宽结束 } } } unsigned char angle_to_duty(unsigned char angle) { unsigned int duty; if (angle 180) angle 180; duty 5 (unsigned int)angle * 20 / 180; if (duty 5) duty 5; if (duty 25) duty 25; return (unsigned char)duty; } void delay_ms(unsigned int ms) { unsigned int i, j; for (i 0; i ms; i) for (j 0; j 1200; j); } void main(void) { unsigned char angle 0; unsigned char dir 1; Timer0_Init(); while (1) { current_duty angle_to_duty(angle); if (dir) angle; else angle--; if (angle 180) dir 0; if (angle 0) dir 1; delay_ms(30); // 保持当前位置30ms再变动 } }代码里的关键点都集中在Timer0_ISR中。整个波形生成完全依靠中断驱动主循环只负责修改current_duty这个全局变量。把角度换算放在主循环而不是中断里是为了避免中断里做乘除法导致中断时间过长、周期抖动。这种“中断只做波形、主循环只做逻辑”的架构是嵌入式实时控制惯用的写法以后写其他PWM控制项目也可以复用。另外current_duty特意用volatile修饰是防止Keil优化后主循环对它的赋值被延迟或乱序导致中断读到旧值。3.3 从单路到多路的软件分时思路如果只想控一路舵机上面的代码就够了。但很多场景需要两路或更多路比如云台的“水平俯仰”两个轴。15f104w最多有6个IO去掉下载占用的P3.0/P3.1还能做4路左右。多路扩展的思路是“软件分时”在一个20ms周期内依次为每路舵机分配一段脉冲输出时间。因为每路高电平最多2.5ms理论上一路周期内最多可以串上20 / 2.5 8路。实际考虑到余量控制在4~5路最稳妥。具体做法是为每路舵机设置一个“脉冲起点”和一个“脉宽值”。比如第0路从0ms开始输出高电平持续duty[0]个0.1ms第1路从2.5ms25个tick之后开始输出高电平第2路从5ms之后开始。中断里每进入一次就检查当前tick_count落在哪一路的脉宽区间内把对应引脚置高或置低。这样每个周期内各路舵机都能取到自己的脉宽信息互不干扰。需要注意的是这种分时方案的IO切换次数会增加中断里的判断逻辑也会变多主循环更新数组时最好关一下中断EA 0;更新完再EA 1;防止tick读取到一半数组被更新产生错乱。4. 硬件接线与实物调试验证4.1 最小系统接线表15f104w的最小系统非常克制一颗芯片加两个电容就能跑。下面是本项目实际接线表芯片引脚引脚名称接到哪里1P3.1/TxDUSB-TTL接收端下载用或扩展IO2P3.0/RxDUSB-TTL发送端下载用或扩展IO3P3.2舵机信号线4P3.3板载LED220Ω电阻到GND5GND电源地、舵机地6P3.4预留按键输入7P3.5/RST烧录时配置为IO可接第二路舵机8VCC5V电源正我实际板子上P3.3接了LED用来观察程序是否在循环运行P3.4预留了按键接口真正的舵机信号走P3.2。如果你做双轴云台可以把P3.5也配成IO输出第二路信号。4.2 供电与干扰处理的几个关键点舵机供电是整个项目最容易翻车的地方。舵机启动瞬间电流很大中小型舵机堵转时能抽到几百毫安甚至1A如果让单片机从同一个低压差线性稳压器取电电压会被瞬间拉低单片机直接复位舵机就会抽搐。我的处理原则是舵机电源和单片机电源必须分路但地线必须共地。具体做法舵机红线接独立5V电源正极比如2S锂电池UBEC降压或者4节五号电池。舵机黑线/棕线接电源地同时和单片机GND连在一起。单片机VCC可以由同一个5V经一只1N5819二极管降压后供电也可以用单独LDO但地仍然要共。在单片机VCC脚附近并联100uF电解电容和0.1uF陶瓷电容舵机电源端也并联一个大电容吸收瞬间电流。信号线方面舵机信号线从P3.2引出后尽量远离电机线、电源线不要平行走超过10cm。如果发现干扰大可以在信号线上串一个100Ω电阻与舵机内部输入电阻形成低通滤波能有效抑制高频毛刺。4.3 实测表现与精度分析我用实验室的示波器实测过P3.2输出周期稳定在20.00ms左右误差小于0.02ms这个精度对舵机来说非常理想。脉宽随主循环设定值变化1.5ms对应90°位置舵机停在正中没有抖动。让舵机从0°跑到180°整个过程行程顺滑没有顿挫感。供电正常的情况下舵机静态保持时几乎听不到“滋滋”声用手扳动舵机臂能感受到保持力说明PWM信号质量足够好。如果出现低频抖动绝大多数情况不是代码问题而是供电没到位。5. 常见问题与排查技巧5.1 问题排查速查表这个项目玩下来我把最容易遇到的问题整理成一张表方便以后直接对照现象可能原因处理方法舵机持续颤抖供电电流不足或电源纹波大换独立电源、加大滤波电容舵机一上电就转到底卡死脉宽超过2.5ms或小于0.5ms检查角度映射和限幅逻辑程序下载失败波特率过高、没冷启动、TX/RX接反降到9600点下载后重新上电交换串口线下载后运行异常P3.0/P3.1接了舵机/外设干扰ISP下载时断开外设至少断开信号线上电瞬间舵机猛转一下P3.5被当复位脚程序反复复位烧录时配置“复位脚用作IO”信号线串10k下拉多路控制某一路没反应分时脉冲起点重叠或引脚没配置确保每路起点间隔≥2.5ms检查IO初始化舵机工作一会变慢稳压器过热降流加大散热或换更大功率电源5.2 几个我踩过的坑和对应处理第一个坑就是P3.5复位脚。最初我把第二路舵机信号直接接到P3.5满以为它是个普通IO结果上电瞬间舵机总是不规则抖动。后来才发现默认状态下P3.5是复位输入只要引脚被拉低芯片就复位程序反复重启IO状态也是乱的。解决办法有两种一是在STC-ISP烧录时把复位脚配置为IO口二是在P3.5外接一个10kΩ上拉电阻避免误触发复位。我现在做板子都会默认给P3.5加个上拉保险。第二个坑是下载口冲突。有一次我把P3.0/P3.1当成扩展IO接了一个测试按键结果烧录死活不成功。原因是按键把串口电平拉低STC-ISP握手时无法正常识别。后来我养成一个习惯下载前断开所有外设只保留USB-TTL三根线TX、RX、GND下载完再接其他器件。第三个坑是中断里不能干重活。最初我把角度换算函数直接放在中断里想着反正0.1ms才一次结果舵机转速一快就出现脉宽抖动。原因是angle_to_duty里有乘除法和限幅判断执行时间太长下一次中断已经触发但还在处理上一轮周期自然就不稳了。把所有计算挪到主循环、只更新一个变量之后问题立刻消失。还有一个小细节程序里如果是用delay_ms让舵机逐步转动这个延时函数本身也会占用大量CPU但因为定时器中断优先级高主循环被卡住也不影响PWM波形。这反而是这套架构的最大优势——主循环随便写波形稳如泰山。实际调试验证后的一些建议如果你第一次做这个实验我建议按这个顺序来先不接舵机用示波器或者逻辑分析仪看P3.2的输出波形确认周期和脉宽变化都正常再给舵机供电把信号线接到P3.2最后跑动态扫描程序观察舵机来回摆动是否顺滑。没有示波器的话用一个LED接在P3.2也能粗略判断有没有脉冲输出但看不出精确脉宽。整块板子做下来成本也就十几块钱大部分时间都花在电源处理和接线检查上代码反而是最简单的一环。这个程序后续还可以扩展出很多玩法比如接一个电位器到P3.4的ADC引脚15f104w没有ADC需要外接或换STC15W系列或者通过UART接收上位机指令控制角度。作为第一个定时器中断练手项目15f104w这个方案性价比真的非常高。本文还有配套的精品资源点击获取
返回列表