简介STM32F103C8T6 与 PCA9685 组成的 16 路舵机驱动方案面向嵌入式开发者、机器人爱好者和电子竞赛学生解决多路舵机独立、同步控制的工程问题。资源基于 Keil μVision 开发环境工程文件与源码组织清晰涵盖硬件驱动层、I2C 通信、寄存器配置以及 PWM 控制代码可直接下载编译并烧录到最小系统板验证。包内共 196 个文件主要包含 c/h 源码文件、o/d 编译中间文件、crf 列表文件、s 启动文件、uvprojx 工程文件以及 hex/axf 烧录文件压缩包大小 6.93MB目录按 System、User、Hardware 划分便于定位核心代码和输出文件。已有 1185 人浏览学习。对于想掌握 STM32 标准库编程、深入理解 PCA9685 预分频器、模式寄存器和占空比寄存器配置或搭建机械臂、机器人底盘等 16 路舵机控制系统的开发者这是一份可直接运行并继续扩展的实用例程同时也能作为学习 I2C 外设驱动与多路 PWM 控制的参考资料。1. 项目概述与硬件选型思路做舵机控制的朋友应该都懂一旦舵机数量超过四五个主控的定时器资源立刻捉襟见肘。STC、Arduino这类平台还能靠库硬撑但到了 STM32F103C8T6 上如果直接拿 TIM 输出 PWM一个定时器通道也就对应一路舵机16路舵机意味着你得占用至少4个定时器而且还得忍受中断优先级冲突、占空比更新抖动这些破事。我在一个仿生机械臂项目里就是被这个问题卡了一周最后换了 PCA9685 模块一口气全解决了。PCA9685 是 NXP 出品的 I2C 接口16通道 PWM 驱动器内部带了一个25MHz的振荡器输出频率可调范围大概在24Hz到1526Hz之间正好覆盖舵机需要的50Hz标准频率。最核心的优势是它只需要两根信号线SCL、SDA挂到 STM32 的 I2C 总线上就能同时驱动16路舵机且每一路 PWM 的占空比分辨率是12位也就是0到4095控制精度远超普通定时器方案。再说说为什么选 STM32F103C8T6 这块板子。蓝 pills 或者国产兼容板现在十几块钱就能拿下Cortex-M3 内核72MHz 主频20KB RAM64KB Flash跑个舵机控制算法绰绰有余。更关键的是它有多个 I2C 外设接口和丰富的 GPIO可以同时挂载多块 PCA9685 级联理论上能扩展出上百路舵机输出。配合国产替代芯片如 GD32F103C8T6引脚兼容、代码零修改真正的低成本高灵活方案。如果你是刚入门的新手或者正在为机械臂、六足机器人、仿生蝴蝶这类多自由度项目发愁这个方案值得认真看看。下面我按“硬件接线 → 驱动器原理 → 代码实现 → 实战调参 → 避坑记录”的顺序把完整流程拆开讲清楚。2. PCA9685 模块工作原理与通信协议2.1 芯片内部结构速览PCA9685 内部大致可以分成三块I2C 从机接口、寄存器组、PWM 生成逻辑。它从 I2C 总线接收指令把数据写入对应的 LED0 到 LED15 通道寄存器中然后由内部 25MHz 时钟分频产生16路独立的 PWM 波形。说人话就是你在 STM32 里算好“这一路舵机要输出多大的脉宽”然后往 PCA9685 的寄存器里写两个数字——一个控制脉冲起点ON一个控制脉冲终点OFF芯片自己就把波形输出出来了完全不需要主控实时干预。主控可以干别的事比如读传感器或者跑运动学解算舵机波形不会受影响。2.2 I2C 寄存器读写要点PCA9685 的默认 I2C 地址是0x407位地址如果焊接了 A0-A5 地址跳线可以扩展到 0x40 到 0x7F最多支持62块模块级联。I2C 时钟频率支持最高 1MHz但 STM32F103 的 I2C 外设跑 400kHz 快速模式比较稳妥。初始化时必须要配置的核心寄存器主要有三个寄存器地址功能说明MODE10x00芯片运行模式控制需将 SLEEP 位置0并设置 AI 位使能自动递增PRE_SCALE0xFE输出频率分频值决定 PWM 频率LEDn_ON / LEDn_OFF0x06起每通道的脉冲起点和终点占用4个字节PRE_SCALE 的计算公式是PRE_SCALE round(25MHz / (4096 × 频率)) - 1比如目标频率是50Hz舵机标准频率PRE_SCALE round(25000000 / (4096 × 50)) - 1 round(122.07) - 1 121写入 0x79121的十六进制即可。这里有个工程细节PRE_SCALE 寄存器只能在 SLEEP 模式下写入所以正确的初始化顺序必须是把 MODE1 的 SLEEP 位拉高、写分频值、再退出 SLEEP最后等待振荡器稳定一般延时几百微秒就行。我见过不少新手直接往 PRE_SCALE 写数据结果怎么调都不出波形就是这个原因。2.3 为什么要用 PCA9685 而不是定时器直出很多人在论坛里问“STM32F103C8T6 不是有 TIM1、TIM2、TIM3、TIM4 吗为什么还要外挂驱动板”简单算笔账F103C8T6 总共只有4个定时器即便每个定时器都能输出4路 PWM也就是16路但每个定时器只能共用一个频率寄存器而舵机频率统一是50Hz没问题可如果你同时还想要一路 PWM 驱动蜂鸣器或者 LED 呼吸灯频率就得分开定时器就不够用了。再说精度。定时器直出 PWM16位计数器在72MHz主频下每计数一个数大约需要13.8ns要输出50Hz周期预分频后单周期计数也就 4096 左右调节步长确实够细。但如果你把主频降下来或者中断负载重波形会抖动。PCA9685 的好处在于它是纯硬件生成波形输出稳定性和一致性比软件翻转 GPIO 好太多而且它是级联设计以后项目扩展不用换主控再加一块板子就行。3. STM32F103C8T6 硬件接线与工程搭建3.1 引脚连接表PCA9685 模块常见的是红色或蓝色 PCB 板接口从上到下一般是 VCC、GND、SCL、SDA部分板子还引出了 OE输出使能脚。STM32F103C8T6 的两个 I2C 外设中我用的是 I2C1引脚对应关系如下PCA9685 引脚STM32F103C8T6 引脚说明VCC3.3V 或 5V必须统一逻辑电压推荐3.3VGNDGND共地SCLPB6I2C1_SCL时钟线需上拉SDAPB7I2C1_SDA数据线需上拉OE可不接或接PB0低电平有效悬空也没事这里有一个特别重要的区别PCA9685 的VCC 是芯片逻辑电源而V 才是舵机电源输入。V 一般独立引出通常是绿色接线端子专门接舵机电源如 6V 或 7.4V 电池。很多新手把舵机直接接在 VCC 上一上电就烧模块因为舵机启动瞬间电流可达 1A 以上I2C 芯片根本扛不住。建议的供电方案是STM32 通过 USB 或者 ST-Link 供 5VPCA9685 的 VCC 接 STM32 的 3.3V逻辑电平一致V 接独立电源4.8V 至 6V 之间用 1000μF 电解电容并联在 V 和 GND 之间滤波舵机电源和逻辑电源必须共地否则 I2C 信号没有参考电平通信会随机失败。如果你手头只有一节锂电池电压在 3.7V 到 4.2V 之间带 SG90 这种 9g 舵机勉强可以但如果是 MG996R 这种金属齿轮大扭矩舵机必须上 5V/6V 的 BEC 或者降压模块。3.2 工程创建方式标准库还是 HAL这个问题每次都会被问。我的个人建议如果你在做产品原型或者比赛项目用HAL 库代码可读性好、后期移植到其他型号芯片方便如果你是做学习验证或者老项目的维护用标准库更直接寄存器操作也透明一些。不过无论用哪个库PCA9685 的驱动本质就是 I2C 读写底层 API 不同上层逻辑完全一致。下面的代码我以寄存器操作为主方便你粘贴到任何框架里都能看懂。以 Keil MDK 工程为例需要配置的 I2C 初始化参数是I2C_InitTypeDef I2C_InitStructure; I2C_InitStructure.I2C_Mode I2C_Mode_I2C; I2C_InitStructure.I2C_DutyCycle I2C_DutyCycle_2; // 快速模式 I2C_InitStructure.I2C_OwnAddress1 0x00; I2C_InitStructure.I2C_Ack I2C_Ack_Enable; I2C_InitStructure.I2C_AcknowledgedAddress I2C_AcknowledgedAddress_7bit; I2C_InitStructure.I2C_ClockSpeed 400000; // 400kHz如果不想和 STM32 的硬件 I2C 锁死问题较劲也可以用 GPIO 模拟 I2C两根线随便挑两个引脚就行。实测下来用 400kHz 的软件模拟 I2C 驱动 PCA9685 完全没问题还能省掉因为硬件 I2C 而踩的坑后面我单独讲。4. 驱动代码实现与关键函数解析4.1 PCA9685 初始化函数初始化流程说穿了就是四步复位芯片、设置频率、退出休眠、确认输出。我把标准库版的初始化代码贴出来注释写得很详细void PCA9685_Init(void) { // 1. 软件复位 PCA9685_WriteReg(0x00, 0x06); // MODE1 写入 RESTART 和 AI 位 delay_ms(10); // 2. 设置 PWM 频率为 50Hz PCA9685_SetFreq(50); // 3. 退出 SLEEP 模式并启用自动递增 PCA9685_WriteReg(0x00, 0xA1); // MODE1 1010 0001 delay_ms(5); // 4. 清空所有通道默认无输出 for (uint8_t i 0; i 16; i) { PCA9685_WriteReg(0x06 i * 4, 0x00); PCA9685_WriteReg(0x07 i * 4, 0x00); PCA9685_WriteReg(0x08 i * 4, 0x00); PCA9685_WriteReg(0x09 i * 4, 0x00); } }这里第一步和第三步看起来有点重复但实际含义不同。第一次写 0x06 是把芯片复位到默认状态此时 SLEEP 位是 1第二步在 SLEEP 状态下写 PRE_SCALE第三步才把 SLEEP 位清 0 并同时开启 AI 位自动寄存器地址递增这样才能连续写多个字节而不需要每次都发送寄存器地址。有朋友问过为什么初始化完成后还要清一次所有通道因为 PCA9685 上电默认输出的是全部低电平不算意外。但在实际项目中如果之前程序跑过寄存器里可能残留旧值上电瞬间舵机会抽动一下。清一次通道寄存器能避免舵机上电乱跳。4.2 舵机角度到 PWM 脉宽的换算舵机的控制信号是一个周期 20ms50Hz的脉冲脉宽范围一般是 0.5ms 到 2.5ms对应 0° 到 180°。PCA9685 是 12 位分辨率也就是一个周期分成 4096 份所以每一份对应的时间是周期20ms - 20000us / 4096 ≈ 4.88us那么 0° 对应的计数值就是 500us / 4.88us ≈ 102180° 对应 2500us / 4.88us ≈ 512。不过不同品牌舵机的脉冲宽度范围不太一样有的可能是 1ms 到 2ms我一般做成可配置的映射结构体方便换舵机时只改参数不改逻辑typedef struct { uint16_t min_pulse; // 0° 对应的计数值 uint16_t max_pulse; // 180° 对应的计数值 } ServoConfig_t; ServoConfig_t sg90_config {102, 512}; // 实测校准后的值转换为角度就是线性插值uint16_t Servo_AngleToPulse(float angle, ServoConfig_t *cfg) { if (angle 0) angle 0; if (angle 180) angle 180; return cfg-min_pulse (uint16_t)((angle / 180.0f) * (cfg-max_pulse - cfg-min_pulse)); }有些教程会直接用角度比例换算比如 “0 度写 10290 度写 307”这种做法在要求不高的场合能用。但如果你做的是机械臂末端抖动一毫米都可能影响抓取精度这个线性映射的起止值就一定要按实际舵机标定。标定方法也不复杂先用程序让舵机转到 0 度对应的脉宽用直尺或者量角器记录真实角度再微调 min_pulse 让舵机真的停在 0°。4.3 设置单路 PWM 输出PCA9685 的每个通道有 4 个寄存器分别是 LEDn_ON_L、LEDn_ON_H、LEDn_OFF_L、LEDn_OFF_H。根据芯片手册如果想让 PWM 在周期开始时立即变高可以把 ON 设为 0OFF 设为目标计数值如果想让某个通道常亮或者常灭还有特殊的 0x1000 标志位。常规舵机驱动的写法是void PCA9685_SetPWM(uint8_t channel, uint16_t off_value) { if (channel 15) return; PCA9685_WriteReg(0x06 channel * 4, 0x00); // LEDn_ON_L PCA9685_WriteReg(0x06 channel * 4 1, 0x00); // LEDn_ON_H PCA9685_WriteReg(0x06 channel * 4 2, off_value 0xFF); // LEDn_OFF_L PCA9685_WriteReg(0x06 channel * 4 3, (off_value 8) 0x0F); // LEDn_OFF_H }注意 OFF_H 寄存器只有低 4 位有效第 5 位到第 12 位是 12 位计数值但寄存器长度只有 8 位所以高 4 位要放到字节的低半字节里。很多从 Arduino 转过来的朋友在这里被坑过以为 OFF_H 直接存高 8 位结果舵机角度完全不对。正确做法是(off_value 8) 0x0F。4.4 扩展级联多块 PCA9685刚才提到级联这里直接给出方法。每块 PCA9685 的 A0-A5 地址引脚可以通过焊锡短接来改变地址。比如第一块全不焊地址 0x40第二块把 A0 焊上地址 0x41第三块焊 A1地址 0x42以此类推。代码层面只需要抽象一层void Servo_Init(void) { PCA9685_Init(0x40); // 板1 PCA9685_Init(0x41); // 板2 } void Servo_SetAngle(uint8_t board_addr, uint8_t channel, float angle) { uint16_t pulse Servo_AngleToPulse(angle, sg90_config); PCA9685_SetPWM(board_addr, channel, pulse); }这样一块 STM32F103C8T6 最多挂 62 块从设备但实际情况受限于 I2C 总线的电容和信号完整性建议最多级联 4 到 8 块。超过之后I2C 线上拉电阻需要相应调小比如从 4.7kΩ 换成 2.2kΩ。5. 舵机控制实战与参数调校5.1 单舵机转动测试先把最简单的情况跑通。我习惯用 STM32F103C8T6 的串口1来和电脑通信发送指令转动指定舵机到指定角度这样不用反复烧录程序就能调参。void USART1_IRQHandler(void) { uint8_t ch; static uint8_t buf[16]; static uint8_t index 0; if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { ch USART_ReceiveData(USART1); if (ch \n) { buf[index] \0; // 解析格式: s0 90 表示 0 号舵机转到 90 度 int servo_id, angle; sscanf((char*)buf, s%d %d, servo_id, angle); Servo_SetAngle(0x40, servo_id, angle); index 0; } else if (index 15) { buf[index] ch; } } }实测下来SG90 舵机在 0° 到 180° 范围里转动非常流畅角度误差在 ±2° 以内。如果你发现舵机在目标角度附近来回抖动大概率是舵机供电不足。PCA9685 的输出只是信号不提供电流舵机的动力完全来自 V。用一个手机锂电池直接驱动 SG90 时启动瞬间电压跌落严重波形会变形这时需要给电源加一个大电容或者换成稳压输出能力更强的电源。5.2 多舵机同步运动如果只是让多个舵机各自转到目标角度写一个 for 循环就完事了。但机械臂、六足机器人这类项目更关心的是“所有关节同步到达目标位置”而不是一个一个依次动。PCA9685 的硬件特性刚好支持这一点它所有通道的 PWM 波形是同时生成的寄存器更新是异步的写入新值后下一个 PWM 周期就生效。所以只要把所有通道的角度换算成脉宽之后连续写寄存器就能做到视觉上的同步运动。如果想做更平滑的插值运动比如让舵机在 1 秒内从 0° 均匀转到 90°不能简单地在 for 循环里延时因为延时会让舵机一顿一顿地走。正确做法是用一个定时器中断每 10ms 更新一次目标角度void TIM3_IRQHandler(void) { static float current_angle[16] {0}; float target_angle[16] {90, 45, 135, ...}; if (TIM_GetITStatus(TIM3, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); for (int i 0; i 16; i) { if (current_angle[i] target_angle[i]) current_angle[i] 1.0f; // 每秒移动 100° else if (current_angle[i] target_angle[i]) current_angle[i] - 1.0f; Servo_SetAngle(0x40, i, current_angle[i]); } } }这里把速度控制抽象成“每 10ms 增加 1°”换算后是 100°/s。实际项目中可以把速度值也做成可配置的这样就能同时控制不同关节的角速度做出来的动作会自然很多。5.3 供电系统设计经验供电是整个舵机项目里最容易出问题的一环我花了不少时间才理清楚。先说结论舵机数量少于 4 个、型号为 SG90 等小型舵机可以用 5V/2A 的 USB 电源直接供 V注意把 USB 电源的地和 STM32 的地连在一起舵机数量 4 到 8 个、或者使用 MG996R 等大扭矩舵机建议用 6V/5A 以上的开关电源或者 2S 锂电池加 UBEC 降压模块舵机数量超过 8 个最好分开多路供电比如左臂一路电源、右臂一路电源避免某一路过载拖垮整个系统另外一个容易被忽略的点是PCA9685 的 V 端子上最好并联一个 470μF 以上的电解电容和 104 陶瓷电容。因为这个模块的 PCB 上往往只放了小容量的去耦电容当舵机急停或者反转瞬间电流突变时V 上的电压会出现毛刺这个毛刺通过地线耦合到 I2C 信号上轻则通信错误重则让 STM32 复位。5.4 用逻辑分析仪验证 PWM 输出在我把程序烧进板子之前习惯先做一个额外检查——利用 STM32 的内部定时器测量 PCA9685 输出的脉宽是否准确。这个方法特别适合验证时序是否出现问题。我的验证方法是把 PCA9685 的任意一路输出接到 STM32 的一个输入捕获引脚然后通过输入捕获算出高电平时间期望 1.5ms 脉宽实测 1.48ms 到 1.52ms 都算正常如果偏差超过 0.1ms需要检查 PRE_SCALE 的设置如果同一块板子上不同通道的脉宽不一致说明芯片或者模块焊接有问题换一块模块再试对一般项目来说PCA9685 内部 25MHz 振荡器的精度是 ±1% 左右这个误差对舵机控制完全可以接受。但如果你的项目需要多块板卡同步输出可以用 SYNC 引脚或者用同一路外部时钟驱动所有板卡。6. 常见问题与排查技巧实录6.1 舵机不转动这是最多人遇到的问题。排查顺序按照“信号 → 电源 → 通信”三步走用万用表量 PCA9685 的 VCC 引脚是否有 3.3VV 引脚是否有舵机工作电压用逻辑分析仪或者示波器接在舵机信号线上看是否有 50Hz 方波输出确认 STM32 与 PCA9685 之间的 I2C 通信是否正常可以用一个简单的方法读取 PCA9685 的 MODE1 寄存器正常复位后的值应该是 0x01SLEEP 置位如果 I2C 通信不稳定最常见的原因是上拉电阻缺失。PCA9685 模块板上有些版本没有焊上拉电阻需要你自己在 SCL、SDA 上各接一个 4.7kΩ 到 3.3V 的上拉电阻。判断方法也简单只接上拉电阻后量 SCL 和 SDA 引脚静态电压应该是接近 3.3V 的高电平低电平不到 0.4V。如果电压是浮动的或者接近 0V说明上拉没接好。6.2 舵机只能转一半角度这个问题的根源基本都是脉宽范围设置不对。比如你用的是 MG996R它的实际脉宽范围可能是 0.5ms 到 2.5ms但也有部分型号是 1ms 到 2ms。如果程序里写死了 0.5ms 到 2.5ms而舵机本身只能响应 1ms 到 2ms那它的物理行程就只有 0 到 90 度。解决办法是每个舵机独立标定。写一个调试函数用串口发送脉冲值观察舵机在最小脉宽和最大脉宽时的物理位置再把这两个值更新到 ServoConfig_t 结构体中。这个工作虽然繁琐但对精度有要求的项目绝对不能省。6.3 多个舵机互相干扰出现这个现象比如舵机 A 转动的时候舵机 B 会轻微抖动基本可以判定是电源问题。因为 PCA9685 输出 PWM 的是信号地舵机驱动大电流时会在电源线上产生压降如果 STM32 和 PCA9685 的地不是单独走线而是和舵机电源地串在一起干扰就会传导过来。处理方式有几个层次把舵机电源的地和逻辑电源的地在电源输入端“一点接地”而不是在 PCB 上到处相连舵机电源线上套一个磁环或者走双绞线减少 EMI 辐射在舵机电源输入处并联一个大电流的 TVS 管吸收浪涌如果做了这些还是干扰考虑换一个质量更好的 PCA9685 模块。市面上十几块钱的模块有的布线比较随意地线很细大电流下地电位抬升明显这种硬件问题靠软件是没法完全解决的。6.4 关于“国产替代”和调试器选择的补充STM32F103C8T6 的国产替代型号 GD32F103C8T6、APM32F103C8T6 在 I2C、定时器等外设上基本兼容我之前在 GD32 上直接跑 STM32 的 PCA9685 驱动代码没有做任何修改就通了。但要注意的是GD32 的 Flash 执行速度比 STM32 略慢在高频率中断里做浮点运算时时间预算要留足余量。调试器方面如果用的是 ST-Link V2常见问题是电脑不识别驱动。优先去 ST 官网下载最新驱动Win10 以上系统一般插上就能免驱识别。如果是第三方的国产 J-Link 或者 ST-Link 兼容版可能会遇到固件版本过低提示下载对应厂商的工具升级固件就行。串口模块方面CP2102、CH340 这类芯片的驱动都比较成熟安装正常就能在设备管理器里看到 COM 口。6.5 我的几点经验总结项目做完之后回看最想强调的还是那句老话先把硬件和供电折腾明白再写代码。PCA9685 的 I2C 时序并不复杂SPI 和 I2C 都是成熟的协议真正的坑基本都在“信号电平不匹配”和“电源地线处理不当”这两个层面。如果你打算从零开始复现这个项目我的建议是不要一上来就画原理图做板子。先用现成的 STM32F103C8T6 最小系统板加一块 PCA9685 模块在面包板上把 I2C 通信调通再用串口控制舵机转到不同角度最后才考虑把东西集成到自己的 PCB 上。一步一步来踩坑成本最低。最后分享一个小技巧如果你手头有几块 PCA9685可以先在模块的 A0 地址跳线片上焊一根排针然后用杜邦线短接或者断开这样测试级联功能时就不用反复焊接了。而且在不同项目之间切换时地址配置也可以灵活调整非常方便。本文还有配套的精品资源点击获取
