在机器人开发和多自由度机械臂项目中,舵机控制往往是核心需求之一。然而,对于许多使用STM32F1/F4等资源受限型号的开发者来说,硬件PWM通道数量不足常常成为项目推进的瓶颈。本文将介绍如何通过I2C接口连接PCA9685芯片,实现多达16路舵机的精确控制,彻底解决PWM资源焦虑。
当我们需要控制多个舵机时,传统方案面临几个明显挑战:
PCA9685作为一款I2C接口的16通道PWM控制器,完美解决了这些问题:
| 特性 | 传统STM32 PWM | PCA9685扩展方案 |
|---|---|---|
| 通道数量 | 有限(通常4-16) | 16路独立PWM |
| 控制方式 | 硬件/软件PWM | 专用PWM生成芯片 |
| 同步性 | 各通道独立 | 所有通道同步 |
| CPU负载 | 高(软件PWM) | 极低 |
| 精度 | 取决于定时器 | 12位(4096级) |
提示:PCA9685的12位分辨率意味着每个PWM周期被分为4096个时间片,比大多数MCU内置PWM精度更高。
PCA9685与STM32的连接非常简单,仅需4根线:
典型连接示意图:
code复制STM32F103C8T6 PCA9685
PB6(SCL) ------> SCL
PB7(SDA) ------> SDA
3.3V ------> VCC
GND ------> GND
首先需要配置STM32的I2C外设。以下是基于HAL库的初始化代码示例:
c复制I2C_HandleTypeDef hi2c1;
void I2C1_Init(void)
{
hi2c1.Instance = I2C1;
hi2c1.Init.ClockSpeed = 100000; // 100kHz
hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
hi2c1.Init.OwnAddress1 = 0;
hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
hi2c1.Init.OwnAddress2 = 0;
hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
if (HAL_I2C_Init(&hi2c1) != HAL_OK)
{
Error_Handler();
}
}
舵机通常需要50Hz(周期20ms)的PWM信号。PCA9685内部使用25MHz时钟,通过预分频器产生所需频率。
c复制#define PCA9685_MODE1 0x00
#define PCA9685_PRESCALE 0xFE
void PCA9685_SetPWMFreq(float freq)
{
uint8_t prescale, oldmode, newmode;
double prescaleval;
// 调整频率计算值
freq *= 0.92; // 经验修正值
prescaleval = 25000000; // 25MHz
prescaleval /= 4096; // 12-bit resolution
prescaleval /= freq;
prescaleval -= 1;
prescale = (uint8_t)(prescaleval + 0.5);
// 进入睡眠模式以修改预分频器
HAL_I2C_Mem_Read(&hi2c1, PCA9685_ADDRESS, PCA9685_MODE1, 1, &oldmode, 1, 100);
newmode = (oldmode & 0x7F) | 0x10; // 设置睡眠位
HAL_I2C_Mem_Write(&hi2c1, PCA9685_ADDRESS, PCA9685_MODE1, 1, &newmode, 1, 100);
// 设置预分频器
HAL_I2C_Mem_Write(&hi2c1, PCA9685_ADDRESS, PCA9685_PRESCALE, 1, &prescale, 1, 100);
// 恢复模式
HAL_I2C_Mem_Write(&hi2c1, PCA9685_ADDRESS, PCA9685_MODE1, 1, &oldmode, 1, 100);
HAL_Delay(5);
// 启用自动递增和重启
newmode = oldmode | 0xA0;
HAL_I2C_Mem_Write(&hi2c1, PCA9685_ADDRESS, PCA9685_MODE1, 1, &newmode, 1, 100);
}
PCA9685每个通道有四个寄存器控制PWM输出:
c复制void PCA9685_SetPWM(uint8_t channel, uint16_t on, uint16_t off)
{
uint8_t data[4];
data[0] = on & 0xFF;
data[1] = on >> 8;
data[2] = off & 0xFF;
data[3] = off >> 8;
HAL_I2C_Mem_Write(&hi2c1, PCA9685_ADDRESS, 0x06 + 4*channel, 1, data, 4, 100);
}
标准舵机控制信号特性:
对于PCA9685的4096级分辨率:
将角度(0-180°)转换为PCA9685的计数值:
c复制void Servo_SetAngle(uint8_t channel, uint8_t angle)
{
uint16_t pulse_width;
// 将角度转换为脉宽(单位:us)
pulse_width = 500 + angle * (2500 - 500) / 180;
// 将脉宽转换为PCA9685计数值
uint16_t pwm_value = pulse_width * 4096 / 20000;
PCA9685_SetPWM(channel, 0, pwm_value);
}
PCA9685的一个显著优势是所有通道共享同一个PWM发生器,因此可以完美实现多舵机同步:
c复制void Servo_SyncMove(uint8_t channels[], uint8_t angles[], uint8_t count)
{
for(int i=0; i<count; i++) {
uint16_t pulse_width = 500 + angles[i] * (2500 - 500) / 180;
uint16_t pwm_value = pulse_width * 4096 / 20000;
PCA9685_SetPWM(channels[i], 0, pwm_value);
}
}
通过控制PWM变化速率,可以实现舵机运动的平滑过渡:
c复制void Servo_SmoothMove(uint8_t channel, uint8_t target_angle, uint16_t duration_ms)
{
uint8_t current_angle = GetCurrentAngle(channel); // 需要实现角度记录
int16_t step = (target_angle > current_angle) ? 1 : -1;
uint16_t delay = duration_ms / abs(target_angle - current_angle);
while(current_angle != target_angle) {
current_angle += step;
Servo_SetAngle(channel, current_angle);
HAL_Delay(delay);
}
}
不同舵机可能存在个体差异,可以通过校准提高精度:
c复制typedef struct {
uint16_t min_pulse; // 0°对应的脉宽(单位: PCA9685计数值)
uint16_t max_pulse; // 180°对应的脉宽
} Servo_Calibration;
Servo_Calibration servo_calib[16]; // 16个通道的校准数据
void Servo_Calibrate(uint8_t channel, uint16_t min_pulse, uint16_t max_pulse)
{
servo_calib[channel].min_pulse = min_pulse;
servo_calib[channel].max_pulse = max_pulse;
}
void Servo_SetAngleCalibrated(uint8_t channel, uint8_t angle)
{
uint16_t pulse = servo_calib[channel].min_pulse +
angle * (servo_calib[channel].max_pulse - servo_calib[channel].min_pulse) / 180;
PCA9685_SetPWM(channel, 0, pulse);
}
单个PCA9685支持16路PWM输出,通过设置不同的I2C地址,可以级联多个模块:
c复制#define PCA9685_BASE_ADDRESS 0x40
void PCA9685_SetAddress(uint8_t old_address, uint8_t new_address)
{
// 设置MODE1寄存器
uint8_t mode1 = 0x10; // 进入睡眠模式
HAL_I2C_Mem_Write(&hi2c1, old_address, PCA9685_MODE1, 1, &mode1, 1, 100);
// 设置新地址
new_address = (new_address << 1) | (PCA9685_BASE_ADDRESS << 1);
HAL_I2C_Mem_Write(&hi2c1, old_address, 0xFD, 1, &new_address, 1, 100);
// 恢复模式
mode1 = 0xA0; // 自动递增, 重启
HAL_I2C_Mem_Write(&hi2c1, new_address, PCA9685_MODE1, 1, &mode1, 1, 100);
}
在实际项目中,我发现PCA9685的稳定性很大程度上取决于电源质量。当驱动多个舵机时,建议为PCA9685和舵机提供独立的电源,并在VCC和GND之间添加适当的去耦电容。此外,I2C总线的上拉电阻值也需要根据总线长度和负载情况仔细选择,通常4.7kΩ是一个不错的起点。