前言

​ 有些时候需要同时驱动多个舵机,需要为每一个舵机调制出对应的PWM脉冲信号;如果舵机很多的话,会占用主控的很多资源。幸运的是有这样一款模块,只需要两个GPIO口,便可以通过I^2^C通信协议控制16路舵机(事实上可以串联多个模块,控制更多舵机)。在这里记录一下其使用方法和代码实现方法。

I^2^C通讯协议

一篇介绍的很详细的文章:一文看懂I2C协议 - 知乎 (zhihu.com)

此篇笔记则是结合《TI_I2C_slva704》代码简单记录一些基本功能。

概述

IIC使用两个接口(SCL和SDA)进行半双工通信。分为主机(Master)从机(Slave device),每个设备有自己特定的地址,一个设备有一个或多个寄存器储存数据,主机通过IIC总线对设备及其寄存器进行读写和配置。

主机访问从机的一般流程

发送数据:

  1. 主机发送START信号和设备地址来指定一个设备
  2. 主机发送数据
  3. 主机发送STOP信号

接收数据:

  1. 主机发送START信号和设备地址来指定一个设备
  2. 主机指定要读取的寄存器
  3. 主机读取数据
  4. 主机发送STOP信号

START和STOP信号

  • STATRT:在SCL拉高的情况下,SDA电平由高变低,即下降沿
  • STOP:在SCL拉高的情况下,SDA电平由低变高,即上升沿

IIC_1.png

代码实现

START

1
2
3
4
5
6
7
8
9
10
void IIC_Start(void)
{
SDA_OUT();
IIC_SDA=1;
IIC_SCL=1;
delay_us(4);
IIC_SDA=0;
delay_us(4);
IIC_SCL=0;
}
  • SDA_OUT();:将SDA端口设置为输出模式;在切换读写状态的时候,也要修改IO口配置
  • IIC_SCL=0;:IIC总线接上拉电阻,故默认高电平状态为总线空闲状态;主机接下来要发送指令,所以要拉低电平,主机控制住总线。

STOP

1
2
3
4
5
6
7
8
9
10
void IIC_Stop(void)
{
SDA_OUT();//sdaÏßÊä³ö
IIC_SCL=0;
IIC_SDA=0;//STOP:when CLK is high DATA change form low to high
delay_us(4);
IIC_SCL=1;
IIC_SDA=1;//·¢ËÍI2C×ÜÏß½áÊøÐźÅ
delay_us(4);
}

ACK/NACK 有无应答

​ 在完成一个字节的信息传输后,接收方可以发送ACK以告知发送方数据被成功接收;也可以选择不发送。在接收方发送ACK之前,发送方必须释放SDA线;接收方拉低SDA以发送ACK。

​ 倘若在ACK位SDA仍然为高电位,则认为是NACK——无应答,无应答的一些情况:

  • 接收方没有做好接收的准备
  • 接收方无法理解接收到的数据
  • 接收方无法再接收更多数据
  • 主机作为接收方完成了数据接收

​ 在这样的规定下,如果出现了主机数据接收失败的情况的话该如何判定呢?是不是冲突了呢?或许主机可以通过编程判定接受失败,进而选择是否重新请求数据。

IIC_2.png

WRITE写操作

  • 数据传输以一个字(8bits)节为单位,最先发送MSB

  • STOP和START信号外需保证SDA只在SCL低电平时发生跳变;SCL高时保证SDA稳定以读取数据。

注:

MSB & LSB

MSB stands for most significant bit, while LSB is least significant bit. In binary terms, the MSB is the bit that has the greatest effect on the number, and it is the left-most bit. For example, for a binary number 0011 0101, the Most Significant 4 bits would be 0011. The Least Significant 4 bits would be 0101.

协议流程

  1. 主机发送START信号
  2. 主机发送7bits设备地址
  3. R/W位置0——写0,读1
  4. 等待应答
  5. 主机发送从设备寄存器地址
  6. 等待应答
  7. 写入数据
  8. 等待应答
  9. 主机发送STOP信号,操作结束。

IIC_3.png

代码实现

此处仅为发送一个字节的流程,完整流程请参考下方PCA9685实战代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void IIC_Send_Byte(u8 txd)
{
u8 t;
SDA_OUT();
IIC_SCL=0;//À­µÍʱÖÓ¿ªÊ¼Êý¾Ý´«Êä
for(t=0;t<8;t++)
{
IIC_SDA=(txd&0x80)>>7;
txd<<=1;
delay_us(2);
IIC_SCL=1;
delay_us(2);
IIC_SCL=0;
delay_us(2);
}
}

READ读操作

协议流程

  1. 前两个字节与写操作相同
  2. 主机重复发送一次START信号
  3. 主机发送从设备地址
  4. R/W置1——读操作
  5. 等待应答
  6. 主机读取数据
  7. 主机不发送应答信号
  8. 主机发送STOP信号

IIC_4.png

代码实现

同样是只读取一个字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
u8 IIC_Read_Byte(unsigned char ack)
{
unsigned char i,receive=0;
SDA_IN();
for(i=0;i<8;i++ )
{
IIC_SCL=0;
delay_us(2);
IIC_SCL=1;
receive<<=1;
if(READ_SDA)receive++;
delay_us(1);
}
if (!ack)
IIC_NAck();
else
IIC_Ack();
return receive;
}

PCA9685芯片

芯片手册:https://semitia.top/upload_flies/PCA9685.pdf

芯片手册的内容比较多,所以在这里只记录一些使用指导性相对更高的一些内容(主要参考第七小节)。

地址

设备地址

IIC_5.png

在默认情况下设备地址是0x40,如果算上最后R/W位的话就是0x80

在模块上能够看到六个留空的焊点,可以选择把部分焊点焊上,设备的地址对应位就会被置一。比如把第0位焊上,地址就变为了1000001

寄存器地址

这是部分寄存器总览表,一共有16个led。各个寄存器的地址也都很清楚地列了出来

IIC_6.png

部分寄存器介绍

MODE1寄存器

IIC_7.png

比较常用的是第四位,即SLEEP位,因为在读写其他寄存器的时候会需要将芯片休眠,待会在原码介绍中也会见到。

IIC_8.png

LED_ON_OFF_H_L

每个PWM(led)输出端口的配置对应四个寄存器

  • LEDn_ON_H
  • LEDn_ON_L
  • LEDn_OFF_H
  • LED_OFF_L

每个寄存器是8位寄存器,但是ONOFF只会分别储存十二位的数据,也就是L存8位,H存4位

IIC_9.png

具体的寄存器数据和PWM波形的关系如下

ONOFF分别存储着范围0~4096的值(确实是4096而非4095),我们把两个值记作cnt_on, cnt_off吧。有一个计数器从0计数到4095,我们把计数器的值记作CNT

一般情况下,都是cnt_on < cnt_off

  • CNT == cnt_on时,PWM波由低变高
  • CNT == cnt_off时,PWM波由高变低

cnt_off < cnt_on时,在第一个周期内CNT == cnt_off不做变化。

可以看出,控制精度是1/4096周期。

具体的PWM调制方法我们结合官方手册里面给出的两个例子就很容易理解了:

IIC_10.png

(这里我感觉 Fig 7 图里的819是不是应该是410

IIC_11.png

PRE_SCALE

IIC_12.png

PRE_SCALE寄存器用以调制PWM频率。驱动舵机的话我们需要20ms的脉冲,频率为50Hz

​ 如果学习过STM32用定时器输出PWM的话应该很容易理解,只不过这里的自动重装载值固定为4096。

​ 时钟给出的是25MHz的频率,如果我们设置n分频,那么计数器就会每过$\frac{n}{25000000}$秒计数一次,那么$T_n = \frac{4096n}{25000000}$秒就是一个周期。假如我们要50Hz的脉冲,那么周期应该是20ms,令$T_n == 0.02s$,解得n,那么分频系数设置为n-1即可。

​ 为什么要减去一呢?因为不分频其实也就是一分频,但是默认prescale value == 0时是不分频。

代码实现

Write写数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void pca_write1(u8 adr,u8 data)
{
IIC_Start();

IIC_Send_Byte(pca_adr1);
IIC_Wait_Ack();

IIC_Send_Byte(adr);
IIC_Wait_Ack();

IIC_Send_Byte(data);
IIC_Wait_Ack();

IIC_Stop();
}

IIC_3.png

这里贴上之前的流程图方便比对。

Read读数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
u8 pca_read1(u8 adr)
{
u8 data;
IIC_Start();

IIC_Send_Byte(pca_adr1);
IIC_Wait_Ack();

IIC_Send_Byte(adr);
IIC_Wait_Ack();

IIC_Start();

IIC_Send_Byte(pca_adr1|0x01);
IIC_Wait_Ack();

data=IIC_Read_Byte(0);
IIC_Stop();

return data;
}

IIC_4.png

设置分频系数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void pca_setfreq1(float freq) 
{
u8 prescale,oldmode,newmode;
double prescaleval;
freq *= 0.92;
prescaleval = 25000000;
prescaleval /= 4096;
prescaleval /= freq;
prescaleval -= 1;
prescale =floor(prescaleval + 0.5f);

oldmode = pca_read1(pca_mode1); //获取之前mode寄存器配置状态

newmode = (oldmode&0x7F) | 0x10; // 睡眠模式

pca_write1(pca_mode1, newmode); // 进入睡眠

pca_write1(pca_pre, prescale); // 设置分频系数

pca_write1(pca_mode1, oldmode); //写回原来
delay_ms(2);

pca_write1(pca_mode1, oldmode | 0xa1);
}

​ 注意手册里也多次强调,只有在设备进入睡眠模式之后才能修改分频系数

这里需要了解一下MODE1寄存器里的RESTART

不停止PWM通道输出的情况下将设备设置为sleep模式,RESTART位会在一个PWM周期后被置1,LED寄存器里面的数据会被保存。想要重启,向RESTART位写1即可,此时该位会被自动置0

​ 当然,在这段代码中还设置了ALLCALL、AL位。

调制PWM占空比

1
2
3
4
5
6
7
void pca_setpwm1(u8 num, u32 on, u32 off) 
{
pca_write1(LED0_ON_L+4*num,on);
pca_write1(LED0_ON_H+4*num,on>>8);
pca_write1(LED0_OFF_L+4*num,off);
pca_write1(LED0_OFF_H+4*num,off>>8);
}

每个“LED”有四个寄存器,所以第n个led地址为LED0地址加4n

写在最后

感谢大家耐心看完了本篇博客,欢迎大家分享;完整的工程可以在我的GiHub仓库找到。由于本人水平有限,可能有不完善之处,欢迎大家批评指正。