基于 STM32 + FreeRTOS 的多路舵机控制系统:PCA9685 驱动与串口协议解析实战
基于 STM32 + FreeRTOS 的多路舵机控制系统:PCA9685 驱动与串口协议解析实战
作者:初心东雨
关键词:STM32F407、FreeRTOS、PCA9685、I2C、PWM 舵机、串口协议、多路控制
一、项目概述
1.1 为什么需要"多路舵机控制"?
在工业自动化、机器人、仪器仪表和智能硬件领域,舵机(Servo)是最常见的执行机构之一:自动阀门开关、机械臂关节转动、云台俯仰、门禁道闸、模型车转向……一个稍微复杂一点的系统,往往需要同时控制十几路甚至几十路舵机。
但问题来了——单片机的 PWM 资源是稀缺的:
| 方案 | 输出路数 | 缺点 |
|---|---|---|
| STM32 定时器直接输出 | 高级定时器 4~8 路 | 占满定时器资源,多路时捉襟见肘 |
| 软件模拟 PWM | 任意路 | 占用 CPU,抖动大,舵机精度差 |
| PCA9685 扩展 | 一片 16 路,级联无上限 | 仅占 2 根 I2C 引脚,性价比极高 ✅ |
PCA9685 的出现就是为了解决这个问题:用 2 根 I2C 引脚,换几十路高精度 PWM。这也是为什么它在舵机控制、LED 驱动、四轴飞行器、机器人项目中几乎成了标配芯片。

1.2 本项目做了什么
本项目基于 STM32F407 + FreeRTOS,搭建了一个 双 PCA9685 = 32 路 PWM 的多路舵机控制系统:
STM32F407 (FreeRTOS)
├── IIC1 ──→ PCA9685 #1 ──→ 舵机 1~16
├── IIC2 ──→ PCA9685 #2 ──→ 舵机 17~32
└── USART6 ←─ 上位机 ASCII 指令
- 下位机:STM32F407 通过两路 I2C 驱动两片 PCA9685,输出 50Hz PWM 精确控制舵机角度;USART6 实时接收上位机的 ASCII 指令,解析后按通道号路由到对应舵机
- 多任务架构:FreeRTOS 将系统划分为舵机刷新、串口解析、启动调度等多个独立任务,互不阻塞
- 协议简洁:一行 ASCII 指令
#编号P角度即可驱动任意一路舵机,调试方便、扩展容易 - 工程可扩展:加一路设备 = 加一个编号段,驱动代码同构复制,几乎零改动
1.3 系统能力一览
| 能力 | 参数 |
|---|---|
| PWM 通道数 | 32 路(双 PCA9685 × 16) |
| PWM 分辨率 | 12-bit(4096 级) |
| PWM 频率 | 50Hz(舵机标准,可配置) |
| 舵机角度范围 | 0 ~ 270°(连续舵机) |
| 控制接口 | USART6 @ 115200bps,ASCII 协议 |
| 实时系统 | FreeRTOS 多任务调度 |
| 扩展上限 | I2C 地址可配 62 片,通道数近乎无上限 |
1.4 读者能获得什么
本文不是泛泛而谈的"入门教程",而是基于实际可编译代码的工程级拆解,重点回答三个问题:
- PCA9685 怎么驱动? —— I2C 寄存器操作、PWM 输出原理、频率设置、角度换算,源码全量可复用
- 多路怎么管理? —— 双芯片如何分配通道、编号路由如何设计、如何扩展到更多路
- 上位机怎么控制? —— 串口协议如何设计、下位机如何解析、FreeRTOS 任务如何组织
无论是做毕业设计、机器人项目,还是工业设备的舵机控制,这套代码和思路都可以直接拿过去改。
二、系统架构
┌──────────────────────────────────────────────────────┐
│ 上位机 (PC 串口) │
│ 发送 ASCII 指令: #编号P角度 │
└───────────────────────┬──────────────────────────────┘
│ UART (115200bps)
┌───────────────────────┴──────────────────────────────┐
│ STM32F407 + FreeRTOS │
│ │
│ USART6 解析任务 ──→ 协议解析(sscanf) │
│ │ │
│ ├── 编号 1~9 ──→ IIC1 ──→ PCA9685 #1 (16路) │
│ │ └──→ setAngle(num, ang) │
│ └── 编号 10~25 ─→ IIC2 ──→ PCA9685 #2 (16路) │
│ └──→ setAngle2(num, ang) │
│ │
│ PCA9685_task ──→ 周期刷新舵机状态 │
│ MOTOR_task ──→ 步进电机控制(可扩展) │
└──────────────────────────────────────────────────────┘
FreeRTOS 任务划分:
| 任务 | 优先级 | 职责 |
|---|---|---|
| start_task | 1 | 启动阶段创建其余任务 |
| PCA9685_task | 1 | 第一路 PCA9685 舵机刷新 |
| PCA96852_task | 3 | 第二路 PCA9685 舵机刷新 |
| MOTOR_task | 2 | 步进电机控制 |
| USART6_task | 2 | 串口指令接收与解析(核心) |
三、PCA9685 驱动详解(源码可复用)
3.1 PCA9685 是什么
PCA9685 是 NXP(恩智浦)推出的 16 通道 12-bit PWM 控制器,属于 I2C 接口的"PWM 扩展芯片":
- 每个通道输出频率相同、占空比独立可调的 PWM(分辨率 4096 级),通道间互不干扰
- 通过 I2C 接口控制,从机地址可通过 A0~A5 引脚配置(0x40~0x7F,同一总线最多 62 片)
- 内置 25MHz 振荡器,无需外部时钟;支持 PWM 频率软件可调(典型 40Hz~1kHz)
- 典型应用:舵机驱动(50Hz,1~2ms 脉宽)、LED 调光、电机调速、呼吸灯等
- 舵机角度 → 脉宽换算:
OFF = 102 + 角度 × 1.52(针对 0~270° 连续舵机)
为什么选它而不是别的方案?
| 对比项 | 定时器直接输出 | 软件模拟 PWM | PCA9685 |
|---|---|---|---|
| 占用引脚 | 每个通道 1 个 | 每个通道 1 个 | 仅 2 根 I2C |
| CPU 占用 | 低(硬件) | 高 | 低(上电配置完即自主运行) |
| 通道扩展 | 换更大芯片 | 无 | 级联 + 地址区分,近乎无限 |
| 精度 | 16-bit | 依赖定时器中断 | 12-bit(舵机够用) |
一句话:PCA9685 用极小的引脚和 CPU 代价,换来几十路稳定的硬件 PWM——这正是多路舵机系统的理想选择。
3.2 头文件定义
/* PCA9685.h */
#ifndef __PCA9685_H
#define __PCA9685_H
#define PCA_Addr 0x80 /* I2C 设备地址(含读写位,0x80>>1=0x40) */
#define PCA_Model 0x00 /* MODE1 寄存器 */
#define PCA_LED0_ON_L 0x06 /* LED0 输出寄存器起始(每通道占 4 字节) */
#define PCA_LED0_ON_H 0x07
#define PCA_LED0_OFF_L 0x08
#define PCA_LED0_OFF_H 0x09
#define PCA_Pre 0xFE /* PRE_SCALE 预分频寄存器 */
void PCA9685_Init(float hz); /* 初始化(设置频率/模式) */
void PCA9685_Write(u8 addr, u8 data); /* 写寄存器 */
u8 PCA9685_Read(u8 addr); /* 读寄存器 */
void PCA9685_setPWM(u8 num, u32 on, u32 off); /* 设置某通道 PWM */
void PCA9685_setFreq(float freq); /* 设置 PWM 频率 */
void setAngle(u8 num, u16 angle); /* 设置某通道舵机角度 0~270° */
#endif
3.3 初始化:只配频率,上电不乱动
/**
* @brief 初始化 PCA9685
* @param hz: PWM 频率(舵机典型 50Hz)
*/
void PCA9685_Init(float hz)
{
iic_init(); /* 初始化 I2C 总线 */
PCA9685_Write(PCA_Model, 0x00); /* MODE1 = 0:正常模式 */
PCA9685_setFreq(hz);
/* 注意:初始化阶段不输出任何角度,防止舵机上电误动作 */
}
💡 经验:舵机系统最忌讳"上电瞬间乱转"。初始化时只配模式/频率、不写通道输出,等系统就绪后再由上位机指令驱动——这是工业控制的基本安全要求。
3.4 寄存器读写(I2C 基础操作)
void PCA9685_Write(u8 addr, u8 data)
{
iic_start();
iic_send_byte(PCA_Addr); /* 设备地址(写) */
iic_wait_ack();
iic_send_byte(addr); /* 寄存器地址 */
iic_wait_ack();
iic_send_byte(data); /* 数据 */
iic_wait_ack();
iic_stop();
}
u8 PCA9685_Read(u8 addr)
{
u8 data;
iic_start();
iic_send_byte(PCA_Addr); /* 设备地址(写方向,先指寄存器) */
iic_wait_ack();
iic_send_byte(addr);
iic_wait_ack();
iic_stop();
delay_us(10);
iic_start();
iic_send_byte(PCA_Addr | 0x01); /* 设备地址(读方向) */
iic_wait_ack();
data = iic_read_byte(0); /* 读 1 字节,末尾 NACK */
iic_stop();
return data;
}
3.5 设置通道 PWM(核心函数)
/**
* @brief 设置某通道 PWM
* @param num: 通道号 (0~15)
* @param on: 导通时刻 (0~4095)
* @param off: 关断时刻 (0~4095)
*/
void PCA9685_setPWM(u8 num, u32 on, u32 off)
{
iic_start();
iic_send_byte(PCA_Addr);
iic_wait_ack();
/* 每个通道 4 个寄存器连续写:LEDn_ON_L/H, LEDn_OFF_L/H */
iic_send_byte(PCA_LED0_ON_L + 4 * num);
iic_wait_ack();
iic_send_byte(on & 0xFF);
iic_wait_ack();
iic_send_byte(on >> 8);
iic_wait_ack();
iic_send_byte(off & 0xFF);
iic_wait_ack();
iic_send_byte(off >> 8);
iic_wait_ack();
iic_stop();
}
原理:PCA9685 的每个通道有 4 个寄存器(ON_L/ON_H/OFF_L/OFF_H)共 12 位,表示"在 4096 个时钟周期的哪个时刻导通、哪个时刻关断"。on=0, off=N 表示从周期起点开始导通 N 个时钟,即占空比 = off/4096。
3.6 设置 PWM 频率
void PCA9685_setFreq(float freq)
{
u8 prescale, oldmode, newmode;
double prescaleval;
freq *= 0.98f; /* 内部时钟偏差修正(实测值) */
prescaleval = 25000000; /* 内部振荡器 25MHz */
prescaleval /= 4096; /* 12-bit 分辨率 */
prescaleval /= freq;
prescaleval -= 1;
prescale = floor(prescaleval + 0.5f);
oldmode = PCA9685_Read(PCA_Model);
newmode = (oldmode & 0x7F) | 0x10; /* 进入 SLEEP 模式才能改分频 */
PCA9685_Write(PCA_Model, newmode);
PCA9685_Write(PCA_Pre, prescale); /* 写预分频 */
PCA9685_Write(PCA_Model, oldmode); /* 退出 SLEEP */
delay_ms(5); /* 等待振荡器稳定 */
PCA9685_Write(PCA_Model, oldmode | 0xa1); /* 恢复 + 使能输出 */
}
⚠️ 关键点:改频率必须先让芯片进 SLEEP 模式(MODE1 的 bit4=1),写完 PRE_SCALE 再退出,否则分频值不会生效。
0xa1= 0x20(自动增量) | 0x80(RESTART) | 0x01(ALLCALL)。
3.7 舵机角度换算(应用层封装)
/**
* @brief 设置某通道舵机角度
* @param num: 通道号 (0~15)
* @param angle: 角度 (0~270°)
*/
void setAngle(u8 num, u16 angle)
{
u32 off = (u32)(102 + angle * 1.52);
PCA9685_setPWM(num, 0, off);
}
102 + angle × 1.52 是舵机脉宽换算的经验公式:0° ≈ 102(≈0.5ms),270° ≈ 512(≈2.5ms),配合 50Hz(20ms 周期)正好覆盖典型舵机的脉宽范围。不同舵机型号需要微调这两个系数(用示波器标定最准)。
3.8 第二路 PCA9685(同理可得)
当需要更多通道时,第二片 PCA9685 挂到另一路 I2C 总线(iic2_init),代码结构与第一路完全一致,仅函数名加 _2 后缀、地址宏改为 PCA2_Addr:
/* PCA9685_2.c —— 与 PCA9685.c 完全同构,仅换 I2C 总线和宏名 */
void PCA9685_2_Init(float hz)
{
iic2_init(); /* 第二路 I2C 总线 */
PCA9685_2_Write(PCA2_Model, 0x00);
PCA9685_2_setFreq(hz);
}
void setAngle2(u8 num, u16 angle) /* 第二片的角度设置 */
{
u32 off = (u32)(102 + angle * 1.52);
PCA9685_2_setPWM(num, 0, off);
}
/* 其余 Write/Read/setPWM/setFreq 与第一路逐行相同,不再重复 */
💡 扩展思路:32 路还不够?每片 PCA9685 的 A0~A5 地址引脚可配置 62 个地址,同一路 I2C 总线最多挂 62 片;更常规的做法是多路 I2C 总线 + 地址区分,通道数几乎无上限。
setAngle加个片选参数(如setAngle(board, ch, angle))即可统一管理。
3.9 进一步构想:一路 I2C 控制多片 PCA9685
本文的方案是"两路 I2C,各挂一片"(简单直接、互不干扰)。但这里值得提出一个更进一步的构想——只用一路 I2C 总线,串联挂载多片 PCA9685:
STM32 I2C1 (SCL/SDA 仅 2 根线)
├── PCA9685 #1 (地址 0x40, A0~A5 全低)
├── PCA9685 #2 (地址 0x41, A0=1)
├── PCA9685 #3 (地址 0x42, A1=1)
└── ... 最多 62 片(62 × 16 = 992 路 PWM)
原理:PCA9685 的从机地址由硬件引脚 A0~A5 决定(0x40~0x7F),每片焊不同地址,就都能挂在同一条 I2C 总线上,主机通过"地址 + 寄存器"区分访问哪一片。这样:
- 引脚零增长:无论 1 片还是 10 片,都只占 STM32 的 2 个引脚(SCL/SDA)
- 布线简化:I2C 是总线结构,多片直接并联即可,无需为每片单独布线
- 代码统一:驱动只需把"片选"抽象为一个参数——
PCA9685_Write(dev_addr, reg, data),地址从宏变成变量,其余逻辑完全复用
代价与注意点(也是它没被本文采用的原因):
| 关注点 | 说明 |
|---|---|
| 地址冲突 | 每片 A0~A5 必须焊成不同组合,贴片生产时要管控物料 |
| 总线带宽 | 所有片共享一条 I2C(典型 400kHz),片数多时刷新速率下降 |
| 单点故障 | 总线上任一片拉死 SDA,整条总线瘫痪,隔离排查较麻烦 |
这个"一路 I2C 多片级联"的方向,在通道数需求大(如 64 路以上)、引脚紧张的场景下非常有价值,值得单独开一篇深入讲解(地址规划、多片批量初始化、总线仲裁与故障隔离等),本文就不展开解析了。
四、下位机串口协议解析(USART6 + FreeRTOS)
4.1 通信协议
上位机通过 USART6(115200bps)发送 ASCII 文本指令,两种格式:
#编号P角度\r\n → 舵机/阀门:编号 + 目标角度(0~270)
| 编号范围 | 路由 | 说明 |
|---|---|---|
| 1~9 | setAngle(num, ang) | 第一路 PCA9685(通道 0~8) |
| 10~25 | setAngle2(num-10, ang) | 第二路 PCA9685(通道 0~15) |
4.1.1 上位机发送端实现(与下位机解析一一对应)
协议是成对的——下位机怎么解析,上位机就怎么构造。以 Python(PyQt5 + pyserial)上位机为例,发送端分两层:指令构造 与 串口发送。
① 指令构造:UI 角度 → 硬件角度 → 协议文本
上位机界面操作的是直观的"UI 相对角度"(-135°+135°),需要换算成下位机认识的"硬件绝对角度"(0270°),再拼成协议文本:
# ui_process.py —— 阀门控制面板
def _send_mapped_valve_cmd(self, v_num, ui_angle):
ui_angle = int(ui_angle)
# ① UI 相对坐标(-135~+135) → 硬件绝对角度(0~270)
hw_angle = ui_angle + 135
if hw_angle < 0: hw_angle = 0
if hw_angle > 270: hw_angle = 270
# ② 逻辑阀号(V1~V19) → 物理通道号(硬件映射表,见 config.py)
hw_ch = VALVE_TO_HW_MAP.get(v_num, v_num - 1)
# ③ 拼成协议文本,入队发送 —— 与下位机 sscanf("#%dP%d") 严格对应
self.serial_manager.send_cmd(f"#{hw_ch}P{hw_angle}\r\n")
② 串口发送:指令队列 + 20ms 定时(防连发丢帧)
# hardware.py —— 串口管理
class SerialManager(QObject):
def __init__(self, log_callback=None):
...
self.cmd_queue = []
self.cmd_timer = QTimer()
self.cmd_timer.timeout.connect(self._process_cmd_queue)
self.cmd_timer.start(20) # 每 20ms 只发一条
def send_cmd(self, cmd_str):
self.cmd_queue.append(cmd_str) # 指令先入队,不立即发送
def _process_cmd_queue(self):
if self.cmd_queue and self.conn_status:
cmd_str = self.cmd_queue.pop(0)
self.ser.write(cmd_str.encode('ascii')) # 发出 #5P90\r\n
③ 指令文本 ↔ 下位机解析 对照关系
| 上位机(Python) | 下位机(C) | 说明 |
|---|---|---|
send_cmd(f"#{hw_ch}P{hw_angle}\r\n") | sscanf(buf, "#%dP%d", &num, &ang) | 协议格式完全一致 |
hw_angle = ui_angle + 135 | if (parsed_angle <= 270) | 角度换算后必须在 0~270 内 |
VALVE_TO_HW_MAP.get(v_num, ...) | if (num<10) setAngle(num); else setAngle2(num-10) | 编号路由一致 |
(可监听串口读回 Success:) | usart6_printf("Success: ...") | 确认帧闭环 |
💡 对齐要点:协议最怕上下位机各写各的。建议把"指令格式、编号范围、角度范围"定义成双方共用的文档/注释头,上位机构造时和下位机解析时对照同一张表,改协议时同步改两端。
4.2 解析任务(FreeRTOS)
/* USART6 串口指令处理任务 */
void USART6_task(void *pvParameters)
{
while (1) {
/* 帧接收完成标志(由串口中断置位) */
if (g_usart6_rx_sta & 0x4000) {
g_usart6_rx_buf[g_usart6_rx_sta & 0x3FFF] = '\0';
int parsed_num = 0, parsed_angle = 0;
/* 舵机指令: #编号P角度 */
if (g_usart6_rx_buf[0] == '#') {
if (sscanf(g_usart6_rx_buf, "#%dP%d", &parsed_num, &parsed_angle) == 2) {
if (parsed_angle <= 270) { /* 角度合法性校验 */
if (parsed_num < 10) {
setAngle(parsed_num, parsed_angle); /* 第一路 */
} else if (parsed_num >= 10 && parsed_num <= 25) {
setAngle2(parsed_num - 10, parsed_angle); /* 第二路 */
}
/* 回发确认帧,上位机据此判断执行成功 */
usart6_printf("Success: Valve %d -> %d degree\r\n",
parsed_num, parsed_angle);
}
}
}
/* 其他指令类型(步进电机等)在此扩展 */
else {
usart6_printf("Format Error\r\n");
}
g_usart6_rx_sta = 0;
memset(g_usart6_rx_buf, 0, USART6_REC_LEN);
}
vTaskDelay(20); /* 让出 CPU,20ms 轮询一次 */
}
}
4.3 设计要点
- 帧完成标志:串口接收中断里维护
g_usart6_rx_sta(bit14=接收完成),主循环轮询该标志——经典"中断收、任务解析"模式,不阻塞串口中断 sscanf格式化解析:一行代码搞定#5P90这类文本指令,简单可靠- 合法性校验:角度 >270 直接丢弃,防止非法值损坏舵机
- 确认帧回发:每条指令执行后回
Success: ...,上位机能及时发现"指令丢了/设备掉线" - 多路路由:编号分段(1~9 / 10~25)决定走哪片 PCA9685,逻辑清晰可扩展
五、FreeRTOS 集成与任务创建
int main(void)
{
sys_stm32_clock_init(336, 8, 2, 7); /* 168MHz 系统时钟 */
delay_init(168);
usart_init(84, 115200); /* 调试串口 */
usart6_init(84, 115200); /* 指令串口 */
PCA9685_Init(50); /* 第一路 PCA9685, 50Hz */
PCA9685_2_Init(50); /* 第二路 PCA9685, 50Hz */
Stepper_Init(); /* 步进电机(可选) */
key_init();
led_init();
g_usart6_rx_sta = 0;
memset(g_usart6_rx_buf, 0, USART6_REC_LEN);
printf("\r\nSystem Ready! FreeRTOS\r\n");
xTaskCreate(start_task, "start_task", START_STK_SIZE, NULL,
START_TASK_PRIO, &StartTask_Handler);
vTaskStartScheduler(); /* 启动调度器 */
while (1) { }
}
启动任务内再创建其余任务,采用 FreeRTOS 标准的"启动任务 + 子任务"模式,便于在临界区统一初始化:
void start_task(void *pvParameters)
{
taskENTER_CRITICAL();
xTaskCreate(PCA9685_task, "PCA9685_task", 128, NULL, 1, &PCA9685Task_Handler);
xTaskCreate(PCA96852_task, "PCA96852_task", 128, NULL, 3, &PCA96852Task_Handler);
xTaskCreate(MOTOR_task, "MOTOR_task", 128, NULL, 2, &MOTOR_Handler);
xTaskCreate(USART6_task, "USART6_task", 256, NULL, 2, &USART6Task_Handler);
vTaskDelete(NULL); /* 启动任务自我删除 */
}
六、踩坑与经验总结
- 改频率必须走 SLEEP:PCA9685 的 PRE_SCALE 只在 SLEEP 模式下可写,忘了退 SLEEP 会导致频率不生效
- 舵机脉宽系数要标定:
102 + angle × 1.52是通用值,不同舵机差异大,量产前务必用示波器逐个标定 - 上电不许乱动:初始化阶段只配频率不输出角度,防止设备上电瞬间舵机猛转(安全第一)
- 指令队列限速:上位机连发指令易导致串口缓冲区溢出,下位机 20ms 轮询 + 上位机侧指令队列(每 20ms 发一条)双保险
- 确认帧必须回:工业控制中"发指令不确认"等于盲操作,回
Success/Format Error让上位机能判断通信健康度 - I2C 总线区分:多片 PCA9685 要么用不同 I2C 地址(A0~A5),要么分挂多路 I2C 总线;本项目用"两路总线"方案,天然避免地址冲突
- 上下位机协议要成对维护:上位机"拼指令"和下位机"解析指令"必须严格对应(格式、编号范围、角度范围)。改协议时同步改两端,并在代码里用注释/文档标注协议定义,避免"上位机发了新格式、下位机还是旧解析"这类隐蔽 bug
七、总结
本文从零拆解了一个「STM32 + FreeRTOS + 双 PCA9685」的多路舵机控制系统:
- 驱动层:PCA9685 的 I2C 读写、PWM 输出、频率设置、角度换算,源码可直接复用
- 协议层:极简 ASCII 协议(
#编号P角度),sscanf一行解析,多路路由清晰 - 系统层:FreeRTOS 多任务隔离,串口"中断收 + 任务解析"经典模式
- 扩展性:加一路设备 = 加一个编号段,驱动同构复制,代码零改动
这套"单片机 + 串口协议 + 多路 PWM 扩展"的组合,是机器人、自动化、仪器控制领域的经典范式,值得完整走一遍。
附录:PCA9685 完整源码(可直接复制)
以下为第一路 PCA9685 的完整驱动源码(
.h+.c),依赖底层 I2C 函数(iic_init/iic_start/iic_stop/iic_send_byte/iic_wait_ack/iic_read_byte/delay_us/delay_ms,见项目 IIC 驱动)。第二路将函数名与宏加_2后缀、I2C 换iic2_init即可。
📄 PCA9685.h
#ifndef __PCA9685_H
#define __PCA9685_H
#define PCA_Addr 0x80 /* PCA9685 设备地址(含读写位,0x80>>1=0x40) */
#define PCA_Model 0x00 /* MODE1 模式寄存器 */
#define PCA_LED0_ON_L 0x06 /* LED0 通道输出寄存器起始地址 */
#define PCA_LED0_ON_H 0x07
#define PCA_LED0_OFF_L 0x08
#define PCA_LED0_OFF_H 0x09
#define PCA_Pre 0xFE /* PRE_SCALE 预分频寄存器(频率设置用) */
#include "sys.h"
void PCA9685_Init(float hz); /* 初始化 PCA9685(设置模式与频率) */
void PCA9685_Write(u8 addr, u8 data); /* 写寄存器 */
u8 PCA9685_Read(u8 addr); /* 读寄存器 */
void PCA9685_setPWM(u8 num, u32 on, u32 off); /* 设置某通道 PWM 占空比 */
void PCA9685_setFreq(float freq); /* 设置 PWM 频率 */
void setAngle(u8 num, u16 angle); /* 设置某通道舵机角度(通道 0~15,角度 0~270°) */
#endif
📄 PCA9685.c
#include "PCA9685.h"
#include "iic.h"
#include "delay.h"
#include <math.h>
/**
* @brief 初始化 PCA9685
* @param hz: PWM 频率(舵机典型 50Hz)
* @note 仅配置模式和频率,不输出任何角度,防止舵机上电误动作
*/
void PCA9685_Init(float hz)
{
iic_init(); /* 初始化 I2C 总线 */
PCA9685_Write(PCA_Model, 0x00); /* MODE1 = 0:正常模式 */
PCA9685_setFreq(hz);
}
/**
* @brief 向 PCA9685 写寄存器
* @param addr: 寄存器地址
* @param data: 要写入的数据
*/
void PCA9685_Write(u8 addr, u8 data)
{
iic_start();
iic_send_byte(PCA_Addr);
iic_wait_ack();
iic_send_byte(addr);
iic_wait_ack();
iic_send_byte(data);
iic_wait_ack();
iic_stop();
}
/**
* @brief 从 PCA9685 读寄存器
* @param addr: 寄存器地址
* @retval 读取到的数据
*/
u8 PCA9685_Read(u8 addr)
{
u8 data;
iic_start();
iic_send_byte(PCA_Addr);
iic_wait_ack();
iic_send_byte(addr);
iic_wait_ack();
iic_stop();
delay_us(10);
iic_start();
iic_send_byte(PCA_Addr | 0x01); /* 读方向 */
iic_wait_ack();
data = iic_read_byte(0); /* 读 1 字节,末尾 NACK */
iic_stop();
return data;
}
/**
* @brief 设置某通道 PWM
* @param num: 通道号 (0~15)
* @param on: 导通时刻 (0~4095)
* @param off: 关断时刻 (0~4095)
*/
void PCA9685_setPWM(u8 num, u32 on, u32 off)
{
iic_start();
iic_send_byte(PCA_Addr);
iic_wait_ack();
iic_send_byte(PCA_LED0_ON_L + 4 * num); /* 每通道 4 个寄存器 */
iic_wait_ack();
iic_send_byte(on & 0xFF);
iic_wait_ack();
iic_send_byte(on >> 8);
iic_wait_ack();
iic_send_byte(off & 0xFF);
iic_wait_ack();
iic_send_byte(off >> 8);
iic_wait_ack();
iic_stop();
}
/**
* @brief 设置 PCA9685 PWM 频率
* @param freq: 目标频率(舵机典型 50Hz)
* @note 改频率必须先进入 SLEEP 模式,写完 PRE_SCALE 后退出
*/
void PCA9685_setFreq(float freq)
{
u8 prescale, oldmode, newmode;
double prescaleval;
freq *= 0.98f; /* 内部时钟偏差修正 */
prescaleval = 25000000; /* 内部振荡器 25MHz */
prescaleval /= 4096; /* 12-bit 分辨率 */
prescaleval /= freq;
prescaleval -= 1;
prescale = floor(prescaleval + 0.5f);
oldmode = PCA9685_Read(PCA_Model);
newmode = (oldmode & 0x7F) | 0x10; /* 进入 SLEEP 模式 */
PCA9685_Write(PCA_Model, newmode);
PCA9685_Write(PCA_Pre, prescale); /* 写预分频 */
PCA9685_Write(PCA_Model, oldmode); /* 退出 SLEEP */
delay_ms(5); /* 等待振荡器稳定 */
PCA9685_Write(PCA_Model, oldmode | 0xa1); /* 恢复 + 使能输出 */
}
/**
* @brief 设置某通道舵机角度
* @param num: 通道号 (0~15)
* @param angle: 角度 (0~270°)
*/
void setAngle(u8 num, u16 angle)
{
u32 off = (u32)(102 + angle * 1.52);
PCA9685_setPWM(num, 0, off);
}
⚠️ 使用提示:
- 依赖底层 I2C 驱动(
iic.c)与延时函数(delay.c),需自行实现或复用工程内驱动102 + angle × 1.52为通用舵机脉宽系数,换舵机型号后请用示波器重新标定- 第二片 PCA9685:复制本文件 → 全局替换
PCA9685→PCA9685_2、PCA_→PCA2_、setAngle→setAngle2、iic_→iic2_即可
本文基于项目实际代码整理,适用于多路舵机/阀门/PWM 控制类应用的参考实现。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)