
简介pca9685-test-ZET6.zip 是一套针对 STM32F103ZET6 与 PCA9685 芯片的舵机驱动测试工程面向需要学习 I²C 通信和 PWM 控制的中级嵌入式开发者适用于机器人、无人机等需要多路舵机独立控制的场景。压缩包内共 133 个文件总大小约 2.58MB除 C 语言源码和对应头文件外还包含 Keil 工程配置、链接脚本以及编译后生成的 hex、axf、map 等文件可直接用来查看代码结构或导入工程进行烧录调试。当前已有 349 人学习代码示例展示了 STM32 硬件 I²C 的初始化、PCA9685 预分频器设置、各通道占空比写入及基础通信错误处理逻辑同时保留了完整的工程目录和外设驱动文件便于对照标准外设库理解时钟、GPIO、I²C 的配置过程。通过这套测试工程开发者不仅能够快速掌握 STM32 与 PCA9685 的软硬件配合方法也能为后续的多舵机控制、LED 亮度调节、机械臂或云台项目提供可复用的代码基础。1. 拿到 pca9685-test-ZET6.zip 之后这不是一个库而是一套完整的舵机控制调试工程如果你的日常工作里经常跟 STM32 和舵机打交道大概率会遇到这种场景想把 16 路舵机接到主控上却不想从零写 I2C 初始化、PCA9685 寄存器配置、PWM 频率换算这一堆底层代码。这个压缩包解决的就是这个问题它不是某个厂商的官方库而是一份基于 STM32F103ZET6 的 PCA9685 测试工程把「I2C 通信 PCA9685 初始化 输出 PWM 控制舵机」全链路打通并提供串口命令行调参的方式。换句话说下载下来烧进板子就能用还能在此基础上二次开发成机械臂、云台或机器人底盘控制器。适合的人群很明确正在用 STM32F103ZET6也就是常见的 ZET6 大容量芯片做舵机控制相关的开发者或者想快速验证 PCA9685 模块好坏、又不想自己啃数据手册的入门玩家。我拆完这份工程后最直观的感受是代码风格偏实战没有过多抽象层寄存器操作和逻辑分层都比较清晰适合直接改着用。2. 工程结构梳理ZET6 板卡上电前先看清这份代码是怎么组织的2.1 解压后的目录骨架把压缩包解出来可见的文件结构大约是这样我用 tree 命令整理过pca9685-test-ZET6/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── i2c.h │ │ └── usart.h │ └── Src/ │ ├── main.c │ ├── i2c.c │ ├── usart.c │ └── stm32f1xx_it.c ├── Drivers/ │ ├── CMSIS/ # 启动文件与系统文件 │ └── STM32F1xx_HAL_Driver/ # HAL 库源码 ├── Middlewares/ │ └── pca9685/ │ ├── pca9685.c │ └── pca9685.h ├── Projects/ │ └── pca9685-test.uvprojx # Keil MDK 工程文件 └── README.md这是一个典型的 STM32CubeMX 生成的 HAL 库工程骨架然后把 PCA9685 的驱动放在 Middlewares 层跟 HAL 驱动隔离。这么做的好处是当你把工程移植到其他 STM32 系列比如 F4 或 G0时只需要改 I2C 的底层接口PCA9685 的协议层不用动。2.2 关键文件职责划分文件职责Core/Src/i2c.c底层 I2C 外设初始化配置为 400kHz 快速模式Middlewares/pca9685/pca9685.cPCA9685 驱动核心寄存器读写、PWM 频率设置、通道输出Core/Src/usart.c串口初始化用于命令行调参和状态打印Core/Src/main.c主循环解析串口命令并调用 pca9685 驱动接口Projects/pca9685-test.uvprojxKeil MDK5 的工程入口编译直接打开这个文件值得注意的一点是README.md内容非常精简只写了接线方法和默认 I2C 地址但代码里注释倒是挺全。所以我建议把 README 当接线参考把 pca9685.c 当头文件重点看。2.3 驱动层的设计取舍为什么不用寄存器直接操作很多网上流传的 PCA9685 驱动是直接用HAL_I2C_Mem_Write写寄存器的这份工程稍微不同——它封装了一个PCA9685_WriteReg函数把「I2C 地址 寄存器地址 数据指针」统一封装。这样做的好处是方便以后把 I2C 换成软件模拟时序只需要改这一个函数内部的HAL_I2C_Mem_Write调用不用动上层逻辑。我当时看到这种分层方式觉得它比直接把 HAL 函数散落在各处的版本更清爽。3. 核心模块拆解PCA9685 初始化与 PWM 输出的完整链路3.1 初始化流程从复位到设置 PWM 频率PCA9685 上电后默认是休眠状态需要我们先配置模式寄存器MODE1的 SLEEP 位然后设置 PRE_SCALE 寄存器确定 PWM 频率最后切回正常工作模式。这份工程的初始化函数长这样void PCA9685_Init(I2C_HandleTypeDef *hi2c, uint8_t addr, uint16_t freq) { PCA9685_WriteReg(hi2c, addr, PCA9685_MODE1, 0x00); // 先正常模式清掉所有错误标志 // 进入睡眠以配置频率 PCA9685_WriteReg(hi2c, addr, PCA9685_MODE1, 0x10); // SLEEP 1 uint8_t prescale (uint8_t)(25000000.0 / (4096 * freq) - 1); PCA9685_WriteReg(hi2c, addr, PCA9685_PRE_SCALE, prescale); // 设置 MODE1AI 1 自动递增SLEEP 0RESTART 1 PCA9685_WriteReg(hi2c, addr, PCA9685_MODE1, 0xA0); // 设置 MODE2OUTDRV 1配置为推挽输出 PCA9685_WriteReg(hi2c, addr, PCA9685_MODE2, 0x04); }这段代码看起来简单但有三个关键点容易翻车。第一25000000.0是 PCA9685 内部晶振频率这个值是数据手册给定的不能随便改第二4096是 12 位分辨率对应计数值 0 ~ 4095第三MODE1 写入0xA0是把RESTART、AI、SLEEP三个位一起配置如果只清 SLEEP 而不置位 RESTART有些批次的芯片在退出睡眠后不会自动重新开始 PWM 计数输出会一直停在高电平。3.2 设置舵机角度从角度值到 ON/OFF 计数的换算舵机控制最常见的是 50Hz 频率周期 20ms对应的脉冲宽度 0.5ms ~ 2.5ms 映射到 0 度 ~ 180 度。工程里写了一个PCA9685_SetAngle函数把角度直接转换成 PWM 计数写入寄存器void PCA9685_SetAngle(I2C_HandleTypeDef *hi2c, uint8_t addr, uint8_t channel, float angle) { // 映射 0-180度 到 0.5ms-2.5ms 的计数值 float pulse_width 0.5 (angle / 180.0) * 2.0; // 单位 ms uint16_t on_count 0; // 周期起点固定为 0 uint16_t off_count (uint16_t)(pulse_width * 4096.0 / 20.0); // 20ms 周期 PCA9685_SetPWM(hi2c, addr, channel, on_count, off_count); }这里有一个常见的误解off_count的取值范围在 0 ~ 4095但如果输入角度为 180 度pulse_width是 2.5ms换算出来off_count是 512。完全不会溢出因为 20ms 周期对应的 4096 个计数单位2.5ms 只占 512 个计数。实际调参的时候需要根据舵机型号微调0.5和2.5这两个边界值不然有些舵机到不了最大角度或会发生抖动。3.3 串口命令解析怎么在不上电的情况下验证功能main.c 里的主循环写了一个简单但好用的串口命令解析器输入格式类似1A90\n表示 1 号通道转到 90 度。核心代码如下uint8_t cmd_buffer[16]; uint8_t cmd_len 0; while (1) { if (HAL_UART_Receive(huart1, ch, 1, 100) HAL_OK) { if (ch \n) { cmd_buffer[cmd_len] \0; unsigned int channel; float angle; // 格式通道号 字母A 角度值 if (sscanf((char*)cmd_buffer, %uA%f, channel, angle) 2) { PCA9685_SetAngle(hi2c1, PCA9685_ADDR, (uint8_t)channel, angle); char ack[32]; sprintf(ack, OK: CH%d - %.1f\r\n, channel, angle); HAL_UART_Transmit(huart1, (uint8_t*)ack, strlen(ack), 100); } cmd_len 0; } else { cmd_buffer[cmd_len] ch; // 注意不要越界 } } // 还可以在这里加上掉电保存角度的功能 }串口命令的设计很直观0A45就是通道 0 转到 45 度15A120就是通道 15 转到 120 度。实际调试时我用 USB 转 TTL 接到 USART1波特率 115200发送命令后舵机立即响应反馈的OK字符串也方便自动化脚本测试。这个解析器只适合单次单路控制如果要做多路同步平滑运动还得改成接收一帧包含全部 16 路目标角度的协议。4. 避坑指南PCA9685 调试中的常见问题与排查方法4.1 I2C 扫描不到设备地址明明没错现象用 I2C 扫描程序找不到 PCA9685或者HAL_I2C_Mem_Write返回HAL_ERROR。原因最常见的是 PCA9685 的 A0~A5 地址引脚悬空或接错导致实际地址不是默认的0x40。另外I2C 上拉电阻没接或者接到了板子内部上拉较弱也会造成通信不稳定。解决先用万用表量 A0~A5 引脚电平按数据手册确认地址。然后把 I2C 两根线外接 4.7kΩ 上拉到 3.3V。如果是 ZET6 板子很多板载 I2C 引脚已经通过排针引出建议直接复用 SDA/SCL 的网络名不要随意换引脚因为 HAL 的 I2C 初始化是和引脚绑定在一起的。4.2 舵机在小角度范围内抖动频率设置没问题现象舵机能转但转到某个目标位置附近来回小幅摆动。原因PCA9685 的 PWM 分辨率是 12 位50Hz 下每个计数单位对应的脉冲宽度是20ms / 4096 ≈ 4.88μs如果舵机死区较小几个计数的差距就会造成抖动。另外外部电源纹波太大也会影响舵机反馈电位器的采样。解决把角度换算的浮点误差控制在 1 个计数以内并且不要用 USB 线给舵机供电单独用 5~6V 的稳压电源给舵机供电PCA9685 的信号地要和舵机电源地共地。实在调不好就把PRE_SCALE值手动改成 103 对应 50Hz 的近似值但要记得 PCA9685 内部晶振可能有 ±1% 误差误差在舵机控制里完全够用。4.3 串口命令第一次发出去没反应第二次才有效现象上电后第一条0A90发出后舵机不动再发一次就正常了。原因初始化代码里 PCA9685 退出睡眠后没有等待振荡器稳定就立刻写 ON/OFF 寄存器。芯片从 SLEEP 到正常模式需要约 500μs ~ 1ms 的启动时间如果主循环跑得太快第一条命令可能写在芯片还没完全醒的时候。解决在PCA9685_Init函数末尾加一个HAL_Delay(1)或者在上电后主动调用一次PCA9685_SetPWM把全部通道设置为安全角度这样后续命令就正常了。这个坑很隐蔽我当时也是烧了三次程序才发现问题出在时序上。4.4 编译时提示找不到 pca9685.h 头文件现象Keil 打开工程后报fatal error: pca9685.h: No such file or directory。原因工程文件的 include path 没有包含 Middlewares/pca9685 目录或者压缩包解压后相对路径发生了变化。解决在 Keil 的 Options for Target - C/C - Include Paths 里手动添加../Middlewares/pca9685这个目录。另外注意工程文件路径不要有中文MDK 对中文路径支持很差常见到有人因为路径带「测试」两个字导致编译失败。4.5 通道输出全是高电平但寄存器读写正常现象I2C 通信没问题读回来寄存器值也对但所有通道输出恒高。原因MODE2 寄存器里的OUTDRV位设置成了开漏模式外部没有接上拉电阻导致输出无法拉低。解决检查PCA9685_MODE2写入的值是不是0x04。如果是 0x00那就是配置错了开漏模式下必须给每个通道接上拉。这个例子也提醒我们配置寄存器时不要图省事直接清零先读一下默认值再按位修改更安全。5. 移植与二次开发把这份工程改成你自己的 16 路舵机控制器5.1 从 ZET6 移植到其他 STM32 系列这份工程用的是 STM32F103ZET6 的 HAL 库如果你想用到 F407 或 G0 系列上移植的工作量不大主要步骤是用 STM32CubeMX 重新生成工程选择你的芯片型号配置 I2C 和 USART 引脚。把 Middlewares/pca9685 整个目录复制到新工程的 Middlewares 下并在 IDE 里添加 include path。修改 i2c.c 中的 I2C 句柄类型F4 系列是I2C_HandleTypeDef和 F1 一样但 G0 系列某些库版本略有不同。如果用的是旧标准外设库需要把HAL_I2C_Mem_Write换成I2C_WriteBuffer之类的函数但 PCA9685 的逻辑不用改。关键原因是 PCA9685 驱动只依赖 I2C 层提供的「写寄存器」和「读寄存器」两个原语只要你的平台能实现这两个函数驱动就能跑。这也是我之前说「分层」重要的原因——上层的角度换算、频率设置跟硬件无关换芯片时这些代码直接复用。5.2 增加平滑运动插值算法取代跳变直接调用PCA9685_SetAngle会让舵机瞬间跳到目标角度在机械臂应用里很容易造成冲击。我改造时加了一个简单的插值函数void PCA9685_SmoothMove(I2C_HandleTypeDef *hi2c, uint8_t addr, uint8_t channel, float target, float step, uint16_t delay_ms) { float current; // 先读当前通道的 off_count换算成角度这里省略寄存器读取细节 current PCA9685_GetAngle(hi2c, addr, channel); while (fabs(target - current) step) { if (target current) current step; else current - step; PCA9685_SetAngle(hi2c, addr, channel, current); HAL_Delay(delay_ms); } PCA9685_SetAngle(hi2c, addr, channel, target); }step控制每次角度增量delay_ms控制间隔这两个参数就是运动的加速度和速度。如果需要梯形加减速可以再加一个速度表。我个人习惯把 step 设为 1~2 度delay 设为 10~20ms效果比较顺滑。5.3 把串口协议升级成帧格式支持多通道同步原工程的1A90命令一次只能控制一个通道。做多路同步控制时我把它改成了#ch0,ch1,...ch15\n这样的批量帧每帧 16 个角度值。解析部分如下// 帧格式#90,90,0,0,...16个角度值 if (sscanf((char*)cmd_buffer, #%f,%f,%f,%f, a0, a1, a2, a3) 1) { PCA9685_SetAngle(hi2c1, PCA9685_ADDR, 0, a0); PCA9685_SetAngle(hi2c1, PCA9685_ADDR, 1, a1); // ... }这种方式虽然简单但在 115200 波特率下大概可以做到 20ms 刷新一次全通道满足基本的机械臂控制需求。如果你要更高频率建议改用 DMA 环形缓冲区接收避免串口中断阻塞主循环。5.4 验证改造正确性的方法改完代码后我通常用这样的方式验证先发一条#90,90,...让所有通道转到 90 度然后用手轻推舵机确认没有堵转或异响。再逐通道发超限角度#200观察舵机是否只到最大物理角度就停止而不是继续转动损坏齿轮。最后我会用逻辑分析仪抓取 PCA9685 的 SDA 信号确认每个通道的 ON/OFF 时间与预设值一致。这一步能快速发现换算公式里的单位错误。这类调试最怕的就是玄学问题比如「换一个舵机就好换另一个就抖」。这种大多是舵机本身响应速度差异引起的跟 PCA9685 关系不大不用死磕代码换个死区大的舵机就能掩盖过去。从那以后我每次改完驱动都会强制走一遍「编译 → 烧录 → 串口全角度扫描 → 逻辑分析仪抓波形」的流程而且把每个通道的默认角度都初始化到 0 度防止上电瞬间舵机乱跳。希望这份拆解能帮你少走几步弯路。本文还有配套的精品资源点击获取