基于 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 读者能获得什么

本文不是泛泛而谈的"入门教程",而是基于实际可编译代码的工程级拆解,重点回答三个问题:

  1. PCA9685 怎么驱动? —— I2C 寄存器操作、PWM 输出原理、频率设置、角度换算,源码全量可复用
  2. 多路怎么管理? —— 双芯片如何分配通道、编号路由如何设计、如何扩展到更多路
  3. 上位机怎么控制? —— 串口协议如何设计、下位机如何解析、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_task1启动阶段创建其余任务
PCA9685_task1第一路 PCA9685 舵机刷新
PCA96852_task3第二路 PCA9685 舵机刷新
MOTOR_task2步进电机控制
USART6_task2串口指令接收与解析(核心)

三、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° 连续舵机)

为什么选它而不是别的方案?

对比项定时器直接输出软件模拟 PWMPCA9685
占用引脚每个通道 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~9setAngle(num, ang)第一路 PCA9685(通道 0~8)
10~25setAngle2(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 + 135if (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 设计要点

  1. 帧完成标志:串口接收中断里维护 g_usart6_rx_sta(bit14=接收完成),主循环轮询该标志——经典"中断收、任务解析"模式,不阻塞串口中断
  2. sscanf 格式化解析:一行代码搞定 #5P90 这类文本指令,简单可靠
  3. 合法性校验:角度 >270 直接丢弃,防止非法值损坏舵机
  4. 确认帧回发:每条指令执行后回 Success: ...,上位机能及时发现"指令丢了/设备掉线"
  5. 多路路由:编号分段(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);                    /* 启动任务自我删除 */
}

六、踩坑与经验总结

  1. 改频率必须走 SLEEP:PCA9685 的 PRE_SCALE 只在 SLEEP 模式下可写,忘了退 SLEEP 会导致频率不生效
  2. 舵机脉宽系数要标定102 + angle × 1.52 是通用值,不同舵机差异大,量产前务必用示波器逐个标定
  3. 上电不许乱动:初始化阶段只配频率不输出角度,防止设备上电瞬间舵机猛转(安全第一)
  4. 指令队列限速:上位机连发指令易导致串口缓冲区溢出,下位机 20ms 轮询 + 上位机侧指令队列(每 20ms 发一条)双保险
  5. 确认帧必须回:工业控制中"发指令不确认"等于盲操作,回 Success/Format Error 让上位机能判断通信健康度
  6. I2C 总线区分:多片 PCA9685 要么用不同 I2C 地址(A0~A5),要么分挂多路 I2C 总线;本项目用"两路总线"方案,天然避免地址冲突
  7. 上下位机协议要成对维护:上位机"拼指令"和下位机"解析指令"必须严格对应(格式、编号范围、角度范围)。改协议时同步改两端,并在代码里用注释/文档标注协议定义,避免"上位机发了新格式、下位机还是旧解析"这类隐蔽 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);
}

⚠️ 使用提示

  1. 依赖底层 I2C 驱动(iic.c)与延时函数(delay.c),需自行实现或复用工程内驱动
  2. 102 + angle × 1.52 为通用舵机脉宽系数,换舵机型号后请用示波器重新标定
  3. 第二片 PCA9685:复制本文件 → 全局替换 PCA9685PCA9685_2PCA_PCA2_setAnglesetAngle2iic_iic2_ 即可

本文基于项目实际代码整理,适用于多路舵机/阀门/PWM 控制类应用的参考实现。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐