ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32 PCA9685舵机控制工程详解:从I2C到PWM的完整实战

STM32 PCA9685舵机控制工程详解:从I2C到PWM的完整实战

简介: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); // 设置 MODE1:AI = 1 自动递增,SLEEP = 0,RESTART = 1 PCA9685_WriteReg(hi2c, addr, PCA9685_MODE1, 0xA0); // 设置 MODE2:OUTDRV = 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 系列上,移植的工作量不大,主要步骤是:

  1. 用 STM32CubeMX 重新生成工程,选择你的芯片型号,配置 I2C 和 USART 引脚。
  2. 把 Middlewares/pca9685 整个目录复制到新工程的 Middlewares 下,并在 IDE 里添加 include path。
  3. 修改 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 度,防止上电瞬间舵机乱跳。希望这份拆解能帮你少走几步弯路。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进