刚完成机械臂的3D打印件组装,却在STM32F103C8T6上卡壳——4个定时器要控制8个舵机,PWM资源根本不够分配。这种硬件资源瓶颈,正是我三年前做六足机器人时遇到的真实困境。当时试过定时器复用、PWM分频各种方案,直到发现这颗$2.5美元的PCA9685芯片,才真正打开多自由度控制的新世界。
在STM32上直接生成PWM控制舵机,需要满足三个硬性条件:
以STM32F103C8T6为例:
| 资源类型 | 总数 | 已用 | 剩余 |
|---|---|---|---|
| 定时器 | 4 | 2 | 2 |
| PWM通道 | 12 | 6 | 6 |
当项目需要控制8个MG996R舵机时,常规方案立即面临两大难题:
这款来自NXP的16通道PWM控制器,通过I²C接口仅需两根线(SCL/SDA)即可扩展出:
实测对比数据:
| 指标 | STM32直连 | PCA9685扩展 |
|---|---|---|
| 最大舵机数 | 4-6 | 16 |
| CPU占用率 | 15%-20% | <1% |
| 布线复杂度 | 高 | 极低 |
| 角度分辨率 | 8位 | 12位 |
提示:PCA9685的I²C地址可通过硬件引脚配置,单个总线最多支持62个模块级联(992路PWM输出)
所需材料清单:
接线示意图:
code复制STM32F103C8T6 PCA9685模块
PB6(SCL) -----> SCL
PB7(SDA) -----> SDA
3.3V -----> VCC
GND -----> GND
_____
5V电源 ---| |---> 舵机电源正极
| |
GND ------|___|---> 舵机电源负极
多舵机系统最易忽视的是电源设计,实测数据:
| 舵机型号 | 空载电流 | 堵转电流 | 建议配置 |
|---|---|---|---|
| SG90 | 100mA | 650mA | 1A/路 |
| MG996R | 200mA | 2.5A | 3A/路 |
推荐电路设计:
c复制// 电源路径管理代码示例
void Power_Management(void) {
// 上电顺序控制
HAL_GPIO_WritePin(PWR_EN_GPIO_Port, PWR_EN_Pin, GPIO_PIN_SET);
HAL_Delay(100);
PCA9685_Init();
HAL_Delay(50);
Servo_EnableAll();
}
将以下文件添加到现有STM32工程:
code复制Drivers/
├── PCA9685/
│ ├── pca9685.c
│ ├── pca9685.h
│ ├── i2c_hal.c
│ └── i2c_hal.h
关键移植步骤:
i2c_hal.h中的硬件抽象层接口pca9685.h中的I²C地址定义main.c中添加初始化调用PCA9685的PWM频率计算公式:
python复制def calc_prescale(target_freq):
osc_clock = 25000000 # 25MHz内部振荡器
prescale_val = osc_clock
prescale_val /= 4096 # 12位分辨率
prescale_val /= target_freq
prescale_val -= 1
return int(prescale_val + 0.5)
# 计算50Hz对应的预分频值
print(calc_prescale(50)) # 输出: 121
实际代码实现:
c复制void PCA9685_SetFreq(float freq) {
uint8_t prescale = (25000000 / (4096 * freq * 0.92)) - 1;
uint8_t oldmode = PCA9685_ReadReg(PCA_MODE1);
PCA9685_WriteReg(PCA_MODE1, (oldmode & 0x7F) | 0x10); // 进入睡眠模式
PCA9685_WriteReg(PCA_PRESCALE, prescale);
PCA9685_WriteReg(PCA_MODE1, oldmode);
HAL_Delay(2);
PCA9685_WriteReg(PCA_MODE1, oldmode | 0xA1); // 自动递增+重启
}
改进后的舵机控制算法:
c复制void Servo_SetAngle(uint8_t ch, float angle) {
// 参数校验
angle = angle > 180 ? 180 : (angle < 0 ? 0 : angle);
// 脉宽计算 (单位:us)
float pulse_width = 500 + (angle / 180.0) * 2000;
// 转换为PCA9685计数值
uint16_t off_count = (uint16_t)(pulse_width * 4096 / 20000);
PCA9685_SetPWM(ch, 0, off_count);
}
典型参数对照表:
| 角度 | 理论脉宽(us) | 实际计数值 | 误差 |
|---|---|---|---|
| 0° | 500 | 102 | ±2 |
| 90° | 1500 | 307 | ±1 |
| 180° | 2500 | 512 | ±3 |
六自由度机械臂控制示例:
c复制typedef struct {
uint8_t channel;
float current_angle;
float target_angle;
uint16_t speed; // °/s
} Servo_Axis;
void Servo_MoveSync(Servo_Axis axes[], uint8_t count) {
float max_time = 0;
// 计算最慢轴所需时间
for(uint8_t i=0; i<count; i++) {
float delta = fabs(axes[i].target_angle - axes[i].current_angle);
float t = delta / axes[i].speed;
if(t > max_time) max_time = t;
}
// 分步插补
uint16_t steps = max_time * 50; // 50Hz控制
for(uint16_t s=0; s<=steps; s++) {
for(uint8_t i=0; i<count; i++) {
float ratio = (float)s / steps;
float angle = axes[i].current_angle +
(axes[i].target_angle - axes[i].current_angle) * ratio;
Servo_SetAngle(axes[i].channel, angle);
}
HAL_Delay(20); // 50Hz周期
}
}
PCA9685_SetPWM(ch, 0, 0)进行校准最后分享一个调试技巧:用逻辑分析仪捕获I²C波形时,可以同时监测PWM输出,这样能直观看到从指令发出到舵机响应的完整时序链。记得在机械结构极限位置设置软件限位,避免昂贵的舵机因过载损坏。