• 正文
  • 相关推荐
申请入驻 产业图谱

定时器通道不够用?一颗LED芯片帮你搞定16路舵机

16小时前
165
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

前段时间在做一个八个舵机并用的项目,兴冲冲地把代码写完,结果一数STM32的定时器通道——不够了。要知道一个定时器就四个通道,多路复用还得跟其他外设抢引脚,八个舵机一上,定时器直接告急,更别提后面还想加其他功能什么的。

这个问题其实挺普遍的,做机器人、做云台、做多足,只要舵机超过三四个,定时器 PWM 的方案就开始捉襟见肘。

这时候PCA9685就该出场了,一颗芯片I2C两根线,怼出16路12位精度的 PWM,关键是还能级联,理论上你挂一堆芯片,控制上百路舵机都没问题。

所以这篇文章,我们就手把手把 PCA9685 调通,从硬件接线到I2C通信,再到舵机角度控制,让舵机先动起来。

1、PAC9685的芯片手册

严格来说,PCA9685 这颗芯片,人家 NXP 官方的定位压根不是给舵机用的,它的数据手册上写的是——16通道 12位PWM的LED调光控制器

对,它本来是拿来调 LED 亮度的。

但你仔细看它的参数就会发现这玩意太适合驱动舵机了。12位分辨率,意味着每个通道的PWM可以分出 4096 个台阶,舵机角度控制绰绰有余。PWM 频率可调,范围从 24Hz 到 1526Hz,通过一个可编程分频器来设,舵机标准的50Hz刚好落在中间。16路独立通道,每路 ON和 OFF 时间各有一对寄存器独立控制,互相不打架。通信走I2C,最快1MHz Fast-mode Plus,两根线搞定,不占用定时器资源。而且I2C地址有6个硬件可编程位,意味着你可以在同一条总线上挂最多62 颗PCA9685——理论上控制将近一千路舵机。

供电方面也分离得很清楚,逻辑部分吃2.3V 到5.5V,PWM 输出驱动部分可以单独供电,最高耐到 5.5V。输出脚是开漏结构,25mA的灌电流能力,直接驱动舵机的信号线完全够,舵机的动力电走外接电源就行。

所以虽然是颗 LED 芯片,但它干的活恰好就是舵机要的——高精度、多通道、可调频率的PWM,在一颗不到一块钱的芯片里全给你了。

PCA9685的I2C寻址,其实就记住一个公式:7 位地址 = 0x40 + A5~A0的硬件编码值。

芯片上A0到A5六个引脚,就是给你手动设地址用的。每个引脚接低电平算0,接高电平算1,六个引脚组成一个 6 位二进制数,加上基地址 0x40,就是这颗芯片的最终 I2C 地址。

举个例子。把A0拉高,其余A1~A5全拉低,那 7 位地址就是 0x40 + 0b000001 = 0x41。六个引脚全拉低,就是 0x40 的默认地址。全拉高,就是0x40 + 0b111111 = 0x7F。0x40到0x7F,一共64个地址,意味着同一条 I2C 总线上你最多可以挂64 颗 PCA9685。

实际用的时候注意一点——写代码时很多 HAL 库函数里填的 I2C 地址是 7 位左移一位之后的 8 位地址,比如 0x40 左移一位变成 0x80。如果你的 HAL 库函数传进去的是 7 位地址就直接填 0x40,传 8 位就填 0x80,这块别搞混,不然 I2C 死活读不到应答。

还有一个很容易踩的坑:所有 PCA9685 模块出厂时A0~A5默认全部拉低,地址清一色0x40想挂同一根 I2C 总线上,不好意思,地址冲突,两个器件同时应答,读回来的数据全乱。解决办法就是拿烙铁改模块背面的 A 脚跳线电阻,把其中一块的某个 A 脚改接到高电平,让两块地址不一样。

总结一下,PCA9685 的寻址就这么三件事:基地址 0x40、六个硬件引脚拼偏移量、多模块记得错开地址。

从寄存器 0x00 到 0x45,一共 70 个字节,按功能分成四块:

模式与控制区(0x00-0x01)MODE1(0x00)管全局状态,bit4的SLEEP位控制芯片休眠,bit5的AI位开启自动递增——这个AUTO-INCREMENT很重要,开了之后你连续写寄存器,地址自动往下跳,不用每次重新发地址,一口气灌完 16 个通道的数据。

MODE2(0x01)管输出配置,比如输出是推挽还是开漏、OE 脚的极性这些。

LED通道区(0x06 - 0x45)这是占大头的地方。每路    LED占4个字节:LEDx_ON_L、LEDx_ON_H、LEDx_OFF_L、LEDx_OFF_H,存的是 PWM 计数器从 0 计到 4095 的过程中,ON 和 OFF 分别发生在哪个计数值。16 个通道依次排下来,从 LED0 的第一组地址 0x06 开始,到 LED15 的最后一组地址 0x45 结束,整整齐齐,看一眼表就能直接算出每个通道对应的寄存器偏移。

全通道同步区(0xFA - 0xFD)ALL_LED_ON_L/H 和 ALL_LED_OFF_L/H,往这里写一次数据,16 个通道同时更新。不是替代通道寄存器的,而是触发同步更新用的。比如你做机械臂,需要多个舵机同时到位而不是一个一个转过去,就先把各通道数据准备好,最后一笔写到 ALL_LED 寄存器,16 路 PWM 统一刷新。

预分频器(0xFE)PRE_SCALE 决定了 PWM 的整体频率。公式上一节提过了,但它有个操作前提——必须先写MODE1让芯片进SLEEP模式,才能动 PRE_SCALE,改完再退SLEEP。这是个硬件设计上的保护逻辑,不是bug,但第一次玩的人很容易在这里浪费半小时查代码。直接 I2C 写进去,芯片就开始干活了。

2、代码实现

我们直接采用了淘宝买的PCA9685模块,在使用STM32F103C8T6最小系统板来控制,开启一组硬件I2C和USART进行通讯。

#include"pca9685.h"#include"i2c.h"HAL_StatusTypeDef PCA9685_Init(I2C_HandleTypeDef *hi2c, uint8_t addr){    uint8_t data;    /* Enter sleep mode + enable auto-increment */    data = PCA9685_MODE1_SLEEP | PCA9685_MODE1_AI;    if (HAL_I2C_Mem_Write(hi2c, addr, PCA9685_MODE1, I2C_MEMADD_SIZE_8BIT, &data, 1, 100) != HAL_OK)        return HAL_ERROR;    /* Set prescaler for 50Hz (25MHz / 4096 / 50 ≈ 121.9, prescale = 121) */    data = 121;    if (HAL_I2C_Mem_Write(hi2c, addr, PCA9685_PRE_SCALE, I2C_MEMADD_SIZE_8BIT, &data, 1, 100) != HAL_OK)        return HAL_ERROR;    data = 0x04;    if (HAL_I2C_Mem_Write(hi2c, addr, PCA9685_MODE2, I2C_MEMADD_SIZE_8BIT, &data, 1, 100) != HAL_OK)        return HAL_ERROR;    data = PCA9685_MODE1_AI;    if (HAL_I2C_Mem_Write(hi2c, addr, PCA9685_MODE1, I2C_MEMADD_SIZE_8BIT, &data, 1, 100) != HAL_OK)        return HAL_ERROR;    HAL_Delay(1);    data = PCA9685_MODE1_AI | PCA9685_MODE1_RESTART;    if (HAL_I2C_Mem_Write(hi2c, addr, PCA9685_MODE1, I2C_MEMADD_SIZE_8BIT, &data, 1, 100) != HAL_OK)        return HAL_ERROR;
    return HAL_OK;}HAL_StatusTypeDef PCA9685_SetPWMFreq(I2C_HandleTypeDef *hi2c, uint8_t addr, float freq){    uint8_t prescale = (uint8_t)(25000000.0f / (4096.0f * freq) + 0.5f) - 1;    if (prescale < 3) prescale = 3;    uint8_t old_mode;    if (HAL_I2C_Mem_Read(hi2c, addr, PCA9685_MODE1, I2C_MEMADD_SIZE_8BIT, &old_mode, 1, 100) != HAL_OK)        return HAL_ERROR;    uint8_t sleep_mode = (old_mode & ~PCA9685_MODE1_RESTART) | PCA9685_MODE1_SLEEP;    if (HAL_I2C_Mem_Write(hi2c, addr, PCA9685_MODE1, I2C_MEMADD_SIZE_8BIT, &sleep_mode, 1, 100) != HAL_OK)        return HAL_ERROR;    if (HAL_I2C_Mem_Write(hi2c, addr, PCA9685_PRE_SCALE, I2C_MEMADD_SIZE_8BIT, &prescale, 1, 100) != HAL_OK)        return HAL_ERROR;    if (HAL_I2C_Mem_Write(hi2c, addr, PCA9685_MODE1, I2C_MEMADD_SIZE_8BIT, &old_mode, 1, 100) != HAL_OK)        return HAL_ERROR;    HAL_Delay(1);    old_mode |= PCA9685_MODE1_RESTART;    if (HAL_I2C_Mem_Write(hi2c, addr, PCA9685_MODE1, I2C_MEMADD_SIZE_8BIT, &old_mode, 1, 100) != HAL_OK)        return HAL_ERROR;    return HAL_OK;}HAL_StatusTypeDef PCA9685_SetPWM(I2C_HandleTypeDef *hi2c, uint8_t addr, uint8_t channel, uint16_t on, uint16_t off){    uint8_t data[4];    data[0] = on & 0xFF;    data[1] = (on >> 8) & 0x0F;    data[2] = off & 0xFF;    data[3] = (off >> 8) & 0x0F;    return HAL_I2C_Mem_Write(hi2c, addr, PCA9685_LED0_ON_L + 4 * channel,                             I2C_MEMADD_SIZE_8BIT, data, 4, 100);}

先看PCA9685_Init,这段代码的核心目标只有一个:让芯片从"未配置状态"进入"50Hz PWM就绪状态"。

仔细看函数里的操作顺序,它不是随便写的,每一步都对应芯片手册里的一个硬件要求。

第一步,写MODE1,同时干两件事:进SLEEP 模式 + 开自动递增。进SLEEP才能改预分频器,这是 PCA9685 的硬性规定,不睡不给改。开自动递增是为了后面连续写通道数据时地址自动往后跳,效率拉满。

第二步,把PRE_SCALE设成121。这个121怎么来的:PCA9685内部振荡器25MHz,PWM一个周期计4096个数,要得到50Hz的输出频率,分频系数 prescale = round(25MHz / (4096 × 50)) - 1 ≈ 121。50Hz是模拟舵机的标准频率,高了低了舵机都会抖或者不动。

第三步,MODE2写入0x04,把输出配置为推挽模式。为什么要推挽?因为舵机信号线是高低电平切换,不需要开漏输出的线与特性,推挽的驱动力更好,信号沿更陡,舵机响应更利索。

第四步,退出SLEEP,写MODE1把 SLEEP 位清零,然后延时 500μs——这个延时不是玄学,是芯片手册明确要求的,振荡器起振需要稳定时间。

最后一步,把RESTART位置 1,让内部PWM 计数器重新从零开始跑。加上之前开的自动递增,芯片现在处于"频率50Hz、自动递增、可以接收通道数据"的完整就绪状态。

PCA9685_SetPWMFreq这个函数是给你运行时动态调频率用的。比如你后面想接一个数字舵机跑 333Hz,就不用重新走一遍完整的 Init 流程,直接调这个就行。逻辑跟 Init 里一样——读旧模式→进SLEEP→改分频→恢复旧模式→等稳定→RESTART。注意old_mode是读回来的原始值,恢复的时候把 SLEEP 之前的状态原样还回去。

HAL_StatusTypeDef PCA9685_SetServoAngle(I2C_HandleTypeDef *hi2c, uint8_t addr, uint8_t channel, uint8_t angle){    if (angle > 180) angle = 180;    uint16_t pulse = 75 + (uint16_t)((uint32_t)angle * 520 / 180);    return PCA9685_SetPWM(hi2c, addr, channel, 0, pulse);}

PCA9685_SetServoAngle就是最终会在主循环里反复调用的函数了。参数三个:I2C 句柄、芯片地址、通道号、角度(0 到 180)。核心就是那一行换算:

pulse = 75 + angle × 520 / 180

这个公式解释一下。50Hz 的 PWM 周期是 20ms,舵机的 0° 对应约 0.5ms 脉宽,180° 对应约 2.5ms。在一个 4096 计数的周期里,0.5ms 对应的计数值是 4096 × 0.5ms / 20ms ≈ 102,2.5ms 对应 4096 × 2.5ms / 20ms ≈ 512。所以脉宽范围是 102 到 512,跨度 410。

102 和 512 是理论值,实际上 PCA9685 的输出级有微小的延迟,而且不同舵机的脉宽范围也略有偏差。经过实测,调成75~595(跨度 520)能让绝大多数SG90、MG996R这类常见舵机跑满 0°~180°。所以公式是75+angle × 520/180,不是书上的理论值,是实测出来的经验值。你用的时候如果发现舵机端点有偏差,微调75和520这两个数就行。

主函数里写个简单的摆头循环验证一下——0°到180°再回来,步进5°,每步延时20ms给舵机响应时间。烧进去舵机开始来回扫,就说明整条链路通了。

相关推荐