简介:面向STM32舵机控制与I²C外设扩展的实战压缩包,围绕STM32F103ZET6和PCA9685十六通道PWM控制器的集成应用展开,涵盖硬件接线、I²C通信配置、寄存器操作与程序框架。对于需要多舵机联动的机器人、无人机或模型制作项目,可直接参考其初始化流程、PWM频率设定和占空比控制思路。压缩包共133个文件,以C源码、H头文件为主,同时包含编译生成的d、o、crf、axf和map中间文件,以及uvprojx工程配置、hex烧录文件、sct链接脚本等,完整呈现从源码到固件的工程链;整体约2.58MB,结构清晰,便于直接打开工程查看或移植。已有349人加入学习,适合具有基础STM32开发经验、希望快速掌握PCA9685驱动方法的嵌入式开发者,也可作为I²C总线实战教学的辅助案例。
1. 舵机一多就卡死:让 stm32 外挂一路 16 通道 PWM 发生器
stm32 驱动舵机数量一多,定时器的输出比较通道就不够用了。pca9685-test-ZET6 这个测试包给的方案很直接:用 PCA9685 把 16 路 PWM 全部外置,单片机全程只管 I2C 发寄存器指令。ZET6 本体的定时器资源省下来做逻辑控制,舵机刷新交给专用芯片,两边互不拖累。适合做 6 轴机械臂、双足机器人或者 16 路灯效的人,也适合刚学完 GPIO 点灯、想试试 I2C 外设的新手——这块芯片把 PWM 的脏活全接了,代码量比对着定时器寄存器配 PWM 简单得多,而且调起来很直观。
2. stm32 和 PCA9685 各管什么:I2C 帧与寄存器模型
要玩转这个测试包,先得把「谁干活」这件事想清楚。ZET6 是控制大脑,PCA9685 是勤恳的 PWM 输出劳工,两者之间只有一条 I2C 总线。这一章把分工逻辑和芯片的寄存器地图讲透,后面写代码才不会像是抄答案。
2.1 为什么选外置 PWM 而不是堆定时器
先盘一下 ZET6 的定时器家底:高级定时器 2 个,通用定时器 4 个,理论输出通道加起来接近 30 路,看着带 16 路舵机绰绰有余。但真做起来不是这么回事。高级定时器的通道要兼顾互补输出和刹车输入,通用定时器的 PWM 占空比更新还得错开时序,16 路舵机意味着 16 个独立占空比要同时维护,初始化代码翻几页就乱了,跑起来只要有一路更新时机不对,舵机就开始抖。
PCA9685 把这件事简化成「MCU 只写寄存器,芯片自己出波」。这颗芯片内置 25MHz 振荡器,内部做分频产生 PWM 周期,16 路输出独立可调占空比,更新一路和更新十六路对单片机来说都是 I2C 写几个字节。I2C 标准模式 100kHz 下,写完整 16 路 ON/OFF 寄存器大约耗时 2 毫秒多一点,舵机刷新周期是 20 毫秒,余量非常充足。所以关键结论是:带宽不是瓶颈,寄存器管理才是,PCA9685 把这一层复杂度全部拿走了。
2.2 三个寄存器域,一份 PRE_SCALE 换算表
PCA9685 的寄存器空间不大,用到的核心就三块:
| 寄存器 | 地址 | 作用 |
|---|---|---|
| MODE1 | 0x00 | 芯片总开关、SLEEP 位、RESTART 位、AI 自动递增位 |
| MODE2 | 0x01 | 输出极性、输出驱动模式(开漏/推挽)、OE 脚行为 |
| PRE_SCALE | 0xFE | PWM 频率分频系数,主频 25MHz 除以它得到实际频率 |
| 通道 ON/OFF | 0x06 起每通道 4 字节 | 决定每个通道高电平的起始与结束 tick |
PRE_SCALE 的换算公式很简单:prescale = round(25MHz / (4096 * 目标频率)) - 1。要注意必须先让芯片进入 SLEEP 状态才能写这个寄存器,写完后用 RESTART 位唤醒。常用频率和写回值我习惯直接查表:
| 目标频率 | 计算值 | 实际写回 PRE_SCALE |
|---|---|---|
| 50 Hz(舵机标准) | 121.07 | 121 |
| 60 Hz | 100.77 | 101 |
| 100 Hz | 60.04 | 60 |
| 200 Hz | 29.52 | 30 |
| 330 Hz | 17.50 | 17 或 18 |
| 1000 Hz | 5.10 | 5 |
实际写回值跟计算值存在取整偏差,50Hz 场景下 PRE_SCALE 取 121 得到实际频率 50.01Hz,误差不到万分之一,舵机完全感受不到。但如果你把 121.07 四舍五入成 122,频率会变成 49.72Hz,舵机在极限角度会发出细微的滋滋声,这就是后面避坑章要展开的取整方向问题。
注意:ZET6 是 3.3V 逻辑电平,PCA9685 的逻辑电源 VLOGIC 直接接 3.3V 就行,SCL/SDA 不需要额外加电平转换电路。舵机电源 V+ 则要单独接 5V 或 6V,不要跟逻辑电源混在一起。
3. 工程拆解:从 I2C 初始化到 16 路舵机全跑起来
解压 pca9685-test-ZET6.zip 之后,第一步不是急着编译,而是先把引脚和初始化代码对照一遍。这个测试包通常把硬件相关代码集中在两个文件里:I2C 底层驱动和 PCA9685 上层驱动。我会先看接线定义,再顺着初始化流程走一遍代码。
3.1 接线与引脚:ZET6 侧其实只用了四个脚
这类测试包里最常见的接法是:PB6 做 SCL、PB7 做 SDA,PCA9685 的 VLOGIC 接 3.3V,GND 共地。OE 引脚实验时直接接地使能输出,V+ 接舵机电源。我一般会在 V+ 上并一个 1000uF 的电解电容,因为多个舵机同时启动瞬间电流可能到 2A 以上,电源电压一掉,PCA9685 逻辑部分和舵机一起罢工,表现就是所有通道输出瞬间归零。
为了让这块板子以后能复用,我还会把 OE 引出来接到 ZET6 的一个普通 GPIO 上,软件里先初始化输出低电平,等系统完全跑起来再拉高使能。这个小习惯后面避坑章会细说,它能直接解决「上电瞬间舵机猛抽一下」的经典问题。
3.2 I2C GPIO 初始化与 PCA9685 初始化
引脚初始化要特别注意复用开漏模式,I2C 总线是靠外部上拉电阻把电平拉高的,内部推挽输出会和外部上拉打架。代码一般是这样的:
// 初始化 I2C1 引脚:PB6 = SCL,PB7 = SDA void I2C1_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; // 复用开漏,外部上拉决定高电平 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); }开漏模式是 I2C 硬性要求,如果误用推挽复用模式,总线仲裁时两个设备同时输出相反电平就可能损坏引脚。速度选 50MHz 是为了让上升沿足够陡,标准模式 100kHz 下问题不大,但快速模式 400kHz 下摆率太慢会导致通信偶发失败。
接着是 PCA9685 的初始化函数。这里有个顺序不能乱:先复位、再 SLEEP、写 PRE_SCALE、最后 RESTART:
// 初始化 PCA9685,设置 PWM 频率 void PCA9685_Init(uint8_t freq_hz) { uint8_t prescale = (uint8_t)(25000000 / (4096 * freq_hz)) - 1; PCA9685_WriteReg(MODE1, 0x00); // 先清空 MODE1,芯片复位到默认状态 PCA9685_WriteReg(MODE1, 0x10); // SLEEP = 1,允许写入 PRE_SCALE PCA9685_WriteReg(PRE_SCALE, prescale); // 写入分频系数 PCA9685_WriteReg(MODE1, 0x80); // RESTART = 1,唤醒芯片让振荡器起振 PCA9685_WriteReg(MODE2, 0x04); // OUTDRV = 1,输出配置为推挽模式 }MODE1 那行 0x80 值得多说一句:芯片从 SLEEP 唤醒后,RESTART 位会自动清掉,如果后面你重新初始化,直接清 SLEEP 位不会重启振荡器,必须先把 RESTART 置 1。不少测试包代码在这一步偷懒,结果就是第二次初始化后 PWM 完全不输出。MODE2 写 0x04 是把输出脚配成推挽而不是开漏——PCA9685 的推挽输出才能给舵机信号线足够的驱动能力,开漏模式挂舵机时高电平会被舵机内部电路拉得很软。
3.3 WriteReg 内部实现与读写差异
PCA9685 的寄存器写入本质是 I2C 写帧:起始位 + 设备地址 + 寄存器地址 + 一个字节数据 + 停止位。设备地址默认是 0x40(7 位地址),对应 8 位写地址 0x80。测试包里常见的写法有两种,一种直接操作寄存器地址,一种封装了 I2C 底层:
// 向 PCA9685 指定寄存器写入一个字节 void PCA9685_WriteReg(uint8_t reg_addr, uint8_t data) { I2C_Start(); I2C_SendByte(0x80); // 0x40 << 1,写方向 I2C_SendByte(reg_addr); // 寄存器地址 I2C_SendByte(data); // 数据 I2C_Stop(); }7 位地址和 8 位地址混用是最容易看晕的地方。芯片手册给的是 7 位地址 0x40,I2C 总线上实际发送的是左移一位后的 0x80。我在调试时验证过,某些现成驱动库内部已经帮你左移了,你再左移一次就变成 0x100,超出 8 位范围直接发不出去。遇到通信失败第一件事就是拿逻辑分析仪抓起始位后面的第一个字节,确认是 0x40 还是 0x80,这一点都不丢人,丢人的是照着错误地址排查了半天硬件。
4. 角度换算与多通道协同:把 0°~180° 写进 PWM 寄存器
PCA9685 输出 PWM 的粒度是 4096 个 tick,舵机角度要换算成 tick 数填进 ON/OFF 寄存器。这一章给出一套可以直接抄的换算函数,再讲清楚为什么理论映射只能当起点,标定才是舵机控制的真正功课。
4.1 从角度到 ON/OFF tick 的映射
标准舵机吃 50Hz PWM 信号,周期 20 毫秒,其中高电平时间 0.5ms 对应 0°,2.5ms 对应 180°,中值 1.5ms 对应 90°。PCA9685 的计数范围是 0 到 4095,一个 tick 的长度是周期除以 4096,算下来 20ms 周期下每个 tick 约 4.88 微秒。
于是 0° 对应的 tick 数 = 0.5ms / 20ms × 4096 ≈ 102,180° 对应的 tick 数 = 2.5ms / 20ms × 4096 ≈ 512。角度到 tick 的线性映射就是:tick = 102 + angle × 410 / 180。为什么是 410 而不是理论上的 409.6?因为整数运算里保留一点余量,避免除出来的值因为取整误差跨越整数边界。
4.2 set_angle 实现与通道使能
把上面的公式落成函数,顺手做一个多通道批量设置:
// 设置某个通道的角度(0~180) void PCA9685_SetServoAngle(uint8_t channel, uint16_t angle) { uint16_t off_tick; if (angle > 180) angle = 180; // 输入保护,防止超出机械极限 off_tick = 102 + (uint16_t)(angle * 410 / 180); // ON 寄存器写 0 表示从周期起点开始计数 // OFF 寄存器写 off_tick,到达此值后该通道输出低电平 PCA9685_WriteReg(LED0_ON_L + 4 * channel, 0x00); PCA9685_WriteReg(LED0_ON_H + 4 * channel, 0x00); PCA9685_WriteReg(LED0_OFF_L + 4 * channel, off_tick & 0xFF); PCA9685_WriteReg(LED0_OFF_H + 4 * channel, (off_tick >> 8) & 0x0F); } // 批量更新 16 路角度,传入长度为 16 的数组 void PCA9685_SetAllAngles(uint16_t *angles, uint8_t len) { uint8_t i; for (i = 0; i < len; i++) { PCA9685_SetServoAngle(i, angles[i]); } }这个函数每次调用都要写 4 次 I2C,16 路全部更新就是 64 次写操作,标准模式 100kHz 下大约耗时 6 毫秒。如果你对刷新时序有要求,可以把 16 路的 ON/OFF 数据整理成连续 32 字节,开 MODE1 的 AI 自动递增位一次性写完,耗时能砍掉三分之一。测试包里的逐通道写法虽然慢一点,但好处是中间某一帧出错只影响一个通道,调试时更容易定位。
4.3 舵机标定:把理论映射换成实测映射
理论换算 102 到 512 tick 是建立在「舵机真的按 0.5ms 到 2.5ms 线性响应」这个假设上的。实际情况是不同厂家的舵机对脉宽的响应范围不一样,有的标称 0° 实际对应 0.45ms,有的 180° 要 2.6ms 才推满。直接用理论映射,角度偏差 10° 并不少见。
我拿到新舵机惯用三点标定:先写 102 tick,量实际角度是多少;写 512 tick 再量一次;中间写 307 tick 验证线性度。然后把实测的两个端点值替换代码里的 102 和 512,角度映射立刻准很多。这套标定流程在测试包里没有,但每次换舵机型号都得做一遍,属于血泪换来的经验。
5. 避坑记录:I2C 卡死、舵机抖动的五个根因
两个月前我调一块舵机驱动板,现象是 16 路输出只亮 8 路,排查了一整天,最后发现是地址位配置的问题。这类问题有很强的共性,我按「现象 → 原因 → 解决」整理成五条,按排查顺序排列。
5.1 通信完全失败,SCL 波形异常
现象:I2C 启动后 SCL 时钟只出来了几个脉冲就停住,逻辑分析仪上能看到 SDA 一直低电平不释放。
原因:总线被某个设备拉死,最常见的是 PCA9685 的 SDA 输出模式和外部电路冲突,或者总线上的上拉电阻阻值不对。
解决:先量 SCL 和 SDA 对地电阻,正常应该在 3.3V 到 5V 之间有 2kΩ 到 4.7kΩ 的上拉。很多开发板的 I2C 上拉电阻是给板上传感器用的,你外接 PCA9685 后相当于并联了第二条总线,等效阻值可能低于 1kΩ,总线驱动能力不够。我一般外接板子上自带 4.7kΩ 上拉,不需要额外加,但如果你的 PCA9685 模块没有上拉电阻,一定要在 SCL 和 SDA 上各接一个 4.7kΩ 到 3.3V。
5.2 地址写 0x40 还是 0x80 的千古谜题
现象:I2C 发送地址后没有 ACK 应答,或者数据偶发写入成功偶尔失败。
原因:PCA9685 的 7 位地址是 0x40,I2C 帧里必须左移一位带上读写位,变成 0x80 写方向或 0x81 读方向。有些第三方驱动库内部做了左移,你再左移就溢出了。
解决:在你自己的 WriteReg 函数里统一约定用 8 位地址,见 3.3 节的代码,所有上层调用只传 7 位地址,底层统一左移。这样不管换什么库,接口层面不会混。如果怀疑地址错了,逻辑分析仪看第一帧的第一个字节最直接。
5.3 PRE_SCALE 写入被 SLEEP 挡住
现象:PRE_SCALE 写进去了,PWM 频率却还是默认的约 200Hz,舵机一直在抖。
原因:PCA9685 手册明确要求写 PRE_SCALE 前必须把 MODE1 的 SLEEP 位置 1。很多参考代码先写 PRE_SCALE 再初始化 MODE1,芯片在运行状态直接改分频系数,新值根本不生效。
解决:初始化顺序严格按 3.2 节的五步走——复位、SLEEP、写 PRE_SCALE、RESTART、配 MODE2。你可以在最后读一次 PRE_SCALE 验证写回值,我见过个别芯片写 121 读回来是 122 的,这种玄学问题只能靠读回校验兜底。
5.4 上电瞬间舵机猛抽一下
现象:系统上电时所有舵机瞬间往一个方向打满,然后才回到程序设置的角度。
原因:PCA9685 的输出寄存器默认全部为 0,对应输出占空比 0%,严格说不是打满。真正的原因是单片机上电瞬间 GPIO 处于浮空输入状态,PCA9685 的 OE 引脚如果悬空或电平不确定,输出端可能直接使能,而此时芯片还没收到任何有效配置。
解决:OE 引脚不要直接接地,接到 ZET6 的普通 GPIO,初始化代码里先把这个 GPIO 配置为推挽输出低电平,让输出保持禁用;等 I2C 初始化完成、各通道角度写入一次之后再拉高 OE。这套时序逻辑让「舵机先待命、再听指令」成为可能,多轴机器人调试时特别重要。
5.5 输出通道串扰,先动的舵机影响后动的
现象:连续更新 4 个以上通道时,后更新的舵机偶尔出现一次明显抖动,频率一高抖得更厉害。
原因:逐通道写寄存器时,每个通道的 ON/OFF 是独立写的。先写的通道已经按新占空比输出,后写的通道还在跑旧值,中间那几十毫秒里各通道的脉宽不一致。如果舵机负载较大,先动的舵机瞬间拉低电源电压,其他还没更新的通道跟着被干扰。
解决:不要在循环里逐通道反复写,攒一批数据一次性更新。用 MODE1 的 AI 自动递增位,连续写某个通道的 4 个寄存器地址,I2C 每发完一个字节寄存器地址自动加一,一轮写完 4 字节 PWM 配置再发停止位。这样所有通道的高级电平 tick 值在同一个 I2C 事务里更新,串扰概率大幅降低。如果还不够,把 I2C 时钟从 100kHz 提到 400kHz,刷新周期能缩短到原来的四分之一。
6. 验证方法:示波器校准脉宽,再谈多板级联
舵机这东西,光看代码跑起来不算数,脉宽差 0.1ms 就是好几度角度偏差。这一章讲两个实用技巧:示波器校准输出脉宽,以及多块 PCA9685 级联时的硬件边界。
6.1 用示波器看两个关键波形
先把程序改成只输出固定 tick 值,不要跑角度函数:
// 校准模式:OUT0 输出固定 0.5ms 脉宽 PCA9685_SetPWM(0, 0, 102); // 期望值:0.5ms @ 50Hz while (1);探头夹在 PCA9685 的 OUT0 和 GND 之间,示波器时间轴调到 2ms 每格,看高电平宽度是不是 0.5ms。如果量到 0.45ms,说明实际频率偏高,把 PRE_SCALE 加 1;量到 0.55ms 就减 1。调完后再写 512 tick 验证 2.5ms 端点。这套校准做完,角度换算才有意义——纸面上的 102 tick 是理论值,每块板子配的晶振或振荡器都有制造偏差,校准一次记下这块板子的实际偏差值,下次直接补偿。
6.2 多板级联与 I2C 负载边界
一块 PCA9685 有 16 路,机器人项目动辄 30 路以上舵机,就需要级联。级联方式是通过 A0 到 A5 六个地址引脚,每块板子拨码设不同地址,第一块保持 0x40,第二块设 0x42,依此类推。理论上 7 位地址可用范围能挂 62 块,但实际上 I2C 总线的电容负载先到极限,每块模块的引脚电容加上 PCB 走线,超过 400pF 信号沿就开始变形。我实测过挂 8 块 PCA9685、总线长度超过 30cm 时,400kHz 模式已经不稳定,降回 100kHz 才可靠。
示波器验证多板级联时,除了看 OUT 通道的 PWM 脉宽,还要看总线末端的 SCL 上升沿。如果上升沿超过 1 微秒,就说明上拉电阻偏大或者总线过长,解决办法是把上拉电阻从 4.7kΩ 换成 2.2kΩ,或者把 I2C 速率降到 100kHz。从那以后我每次拿到舵机控制板,都强制自己先量一遍 102 tick 的实际脉宽,再谈角度标定,这套验证流程省下的调试时间远比写代码的时间多。希望帮到你。
本文还有配套的精品资源,点击获取