PCA9685-16路舵机驱动模块
当你在一个项目中碰到了微控制器芯片的PWM输出引脚不够用的情况,那么这款PCA968516路舵机就能很快帮助您解决这个问题了。只要你的主控芯片具备了I2C通信,就能够让主控芯片和PCA9685通信,实现多个舵机的同时控制了。PCA9685 16路舵机是一个采用I2C通信,内置了PWM驱动器和一个时钟,这个意味着,这将和TLCG940系列有很大不同,你不需要不断发送信号占用你的单片机。它是5V的兼容,这意味你还可以用3.3V单片机控制并且安全地驱动到6V输出(当你想要控制白色或蓝色指示灯用3.4+正电压也是可以的)。地址选择引脚使你可以把62个驱动板挂在单个l2C总线上,总共有992路PWM输出,那将是非常庞大的资源,约1.6Khz可调频PWM输出,为步进电机准备输出12位分辨率,可配置的推拉输出或开路输出,输出使能引脚能够快速禁用所有输出。
模块来源
采购链接:
16路PWM Servo 舵机驱动板机器人控制器IIC接口驱动器模块PCA9685
资料下载:
https://pan.baidu.com/s/1FjoAuJm387bxaZxS6g9HEg
提取码:8888
规格参数
**输入电压:**3.3V~5V
**额定电流:**15mA
**控制方式:**串口
尺寸: 21(长)*21(宽)[单位:mm]
查看资料
I2C器件地址
PCA9685 是一个I2C 从设备,有个设备ID,或者叫从 地址。从地址是如下确定的: Board 0: Address = 0×40 Offset = binary 00000 (默认) Board 1: Address = 0×41 Offset = binary 00001 (A0接上拉) Board 2: Address = 0×42 Offset = binary 00010 (接上A1上拉) Board 3: Address = 0×43 Offset = binary 00011 (A0和A1上拉) Board 4: Address = 0×44 Offset = binary 00100 (A2上拉) 以此类推;
PCA9685的I2C总线从地址如下图所示。为了节约电力,硬件可选地址引脚上没有内部上拉电阻,它们必须被拉高或拉低。但是我们使用的是模块,而模块上已经为我们接好了上拉电阻。
地址字节的最后一位定义要执行的操作。当设置为逻辑1时,将选择读操作,而逻辑0则选择写操作。
在原理图中,地址线全部接0,所以slave address是0x40。对应Fig 4上的位置,则为
则IIC地址是 0x80 ,写入时是0x80,读取时是0x81。
设置PWM频率
舵机控制所需的 PWM 周期为20 ms. 在用 PCA9685 作为多舵机控制器时,需要将 其 PWM 输出周期设定为20 ms,即PWM 波的频率设定为50 Hz,PCA9685 输出频率与振荡器有关,频率的设置值refresh_rate见下面的公式;
其中,EXTCLK是PCA9685的内部时钟频率为25Mhz;prescale是要设置的频率,我们设置为50Hz;
refresh_rate = 25,000,000 /( 4096 * ( 50 + 1 )) refresh_rate = 25,000,000 / 4096 / (50 + 1) refresh_rate = 6,103.52 / (50 + 1) refresh_rate = 6,103.52 / 51 refresh_rate = 119.68
所以我们需要设置的值是119.68,取整数就是120。
需要注意的是,频率的更改只能在 PCA9685 芯片处于休眠状态下进行。
以下加粗字体是数据手册内容:
要使用EXTCLK引脚,该位必须按以下顺序设置:
在mode1中设置SLEEP位。这就关闭了内部振荡器,使芯片处于休眠状态。
将逻辑1写入MODE1中的SLEEP和EXTCLK位。这样就转换完成了。外部时钟可以在切换期间处于活动状态,因为设置了SLEEP位。
这个位是一个"粘性位",也就是说,它不能通过写入逻辑0来清除。EXTCLK位只能通过电源循环或软件重置来清除。
占空比或者脉冲宽度的设定
每个PWM引脚输出的开启时间和PWM的占空比可以通过LEDn_ON和LEDn_OFF寄存器独立控制。
每个PWM引脚输出将有两个12位寄存器。这些寄存器将由用户编程。两个寄存器都将保存从0到4095的值。一个12位寄存器将保存ON时间的值,另一个12位寄存器将保存OFF时间的值。将ON和OFF时间与12位计数器的值进行比较,该计数器将从0000h持续运行到0FFFh(0到4095十进制)。
ON时间是可编程的,它是PWM输出ON的时间,OFF时间也是可编程的,它是PWM输出OFF的时间。这样相移就完全可编程了。相移的分辨率为目标频率的1 / 4096。表7列出了这些寄存器。
以下用一个例子说明如何计算要加载到这些寄存器中的值。
(假设使用LED0输出,(延时时间)+ (PWM占空比)<=100%)
延迟时间 = 10%;PWM占空比= 20% (LEDON电平= 20%;LEDOFF时间= 80%)。延迟时间= 10% = 4096 * 0.1 = 409.6 ~ 410,计数= 410(十进制) = 19Ah(十六进制)
因为计数器从0开始,到4095结束,我们将减去1,所以延迟时间 = 199h 个数。
LED0_ON_H = 1h;LED0_ON_L = 99h (LED开始打开后,这个延迟计数到409)
LED开机时间= 20% = 819.2 ~ 819次
LED关闭时间= 4CCh(十进制410 + 819-1 = 1228)
LED0_OFF_H = 4h;LED0_OFF_L = CCh(此计数到1228后LED开始关闭)
整个周期为4095, LED_ON 和 LED_OFF 2个的设定值确定脉宽,在后面的代码里,LED_ON 设为0, LED_OFF就是脉宽了。 这里都用2位字节来表示。
相关地址表
这里只截图了需要的地址,分别是:
#define PCA\_Addr 0x80 //IIC地址
#define PCA\_Model 0x00
#define LED0\_ON\_L 0x06
#define LED0\_ON\_H 0x07
#define LED0\_OFF\_L 0x08
#define LED0\_OFF\_H 0x09
#define PCA\_Pre 0xFE //配置频率地址
2
3
4
5
6
7
移植过程
引脚选择
移植至工程
我们的目标是将例程移植至ESP32-S3开发板上。已经为大家提供了完整的驱动代码,按照以下步骤,即可完成移植。
具体新建文件夹和新建c和h文件在【DHT11温湿度传感器】章节中有详细的教学,这里就不再多说了。
只不过这里我们将文件名 bsp_dht11.c 和 bsp_dht11.h 换成 bsp_pca9685.c 和 bsp_pca9685.h,文件夹名字改为PCA9685。
代码写入
在文件bsp_pca9685.c中,编写如下代码。
/*
* 立创开发板软硬件资料与相关扩展板软硬件资料官网全部开源
* 开发板官网:www.lckfb.com
* 技术支持常驻论坛,任何技术问题欢迎随时交流学习
* 立创论坛:club.szlcsc.com
* 关注bilibili账号:【立创开发板】,掌握我们的最新动态!
* 不靠卖板赚钱,以培养中国工程师为己任
* Change Logs:
* Date Author Notes
* 2024-01-16 LCKFB-lp first version
*/
#include "bsp_pca9685.h"
#include "stdio.h"
#include <math.h>
void delay_ms(unsigned int ms)
{
vTaskDelay(ms / portTICK_PERIOD_MS);
}
void delay_us(unsigned int us)
{
ets_delay_us(us);
}
void delay_1ms(unsigned int ms)
{
vTaskDelay(ms / portTICK_PERIOD_MS);
}
void delay_1us(unsigned int us)
{
ets_delay_us(us);
}
/******************************************************************
* 函 数 名 称:PCA9685_GPIO_Init
* 函 数 说 明:PCA9685的引脚初始化
* 函 数 形 参:无
* 函 数 返 回:无
* 作 者:LC
* 备 注:无
******************************************************************/
void PCA9685_GPIO_Init(void)
{
gpio_config_t OUT_config = {
.pin_bit_mask = (1ULL<<GPIO_SDA)|(1ULL<<GPIO_SCL), //配置引脚
.mode =GPIO_MODE_OUTPUT, //输出模式
.pull_up_en = GPIO_PULLUP_DISABLE, //不使能上拉
.pull_down_en = GPIO_PULLDOWN_DISABLE, //不使能下拉
.intr_type = GPIO_INTR_DISABLE //不使能引脚中断
};
gpio_config(&OUT_config);
}
/******************************************************************
* 函 数 名 称:IIC_Start
* 函 数 说 明:IIC起始时序
* 函 数 形 参:无
* 函 数 返 回:无
* 作 者:LC
* 备 注:无
******************************************************************/
void IIC_Start(void)
{
SDA_OUT();
SDA(1);
delay_us(5);
SCL(1);
delay_us(5);
SDA(0);
delay_us(5);
SCL(0);
delay_us(5);
}
/******************************************************************
* 函 数 名 称:IIC_Stop
* 函 数 说 明:IIC停止信号
* 函 数 形 参:无
* 函 数 返 回:无
* 作 者:LC
* 备 注:无
******************************************************************/
void IIC_Stop(void)
{
SDA_OUT();
SCL(0);
SDA(0);
SCL(1);
delay_us(5);
SDA(1);
delay_us(5);
}
/******************************************************************
* 函 数 名 称:IIC_Send_Ack
* 函 数 说 明:主机发送应答或者非应答信号
* 函 数 形 参:0发送应答 1发送非应答
* 函 数 返 回:无
* 作 者:LC
* 备 注:无
******************************************************************/
void IIC_Send_Ack(unsigned char ack)
{
SDA_OUT();
SCL(0);
SDA(0);
delay_us(5);
if(!ack) SDA(0);
else SDA(1);
SCL(1);
delay_us(5);
SCL(0);
SDA(1);
}
/******************************************************************
* 函 数 名 称:I2C_WaitAck
* 函 数 说 明:等待从机应答
* 函 数 形 参:无
* 函 数 返 回:0有应答 1超时无应答
* 作 者:LC
* 备 注:无
******************************************************************/
unsigned char I2C_WaitAck(void)
{
char ack = 0;
unsigned char ack_flag = 10;
SCL(0);
SDA(1);
SDA_IN();
delay_us(5);
SCL(1);
delay_us(5);
while( (SDA_GET()==1) && ( ack_flag ) )
{
ack_flag--;
delay_us(5);
}
if( ack_flag <= 0 )
{
IIC_Stop();
return 1;
}
else
{
SCL(0);
SDA_OUT();
}
return ack;
}
/******************************************************************
* 函 数 名 称:Send_Byte
* 函 数 说 明:写入一个字节
* 函 数 形 参:dat要写人的数据
* 函 数 返 回:无
* 作 者:LC
* 备 注:无
******************************************************************/
void Send_Byte(uint8_t dat)
{
int i = 0;
SDA_OUT();
SCL(0);//拉低时钟开始数据传输
for( i = 0; i < 8; i++ )
{
SDA( (dat & 0x80) >> 7 );
delay_us(1);
SCL(1);
delay_us(5);
SCL(0);
delay_us(5);
dat<<=1;
}
}
/******************************************************************
* 函 数 名 称:Read_Byte
* 函 数 说 明:IIC读时序
* 函 数 形 参:无
* 函 数 返 回:读到的数据
* 作 者:LC
* 备 注:无
******************************************************************/
unsigned char Read_Byte(void)
{
unsigned char i,receive=0;
SDA_IN();//SDA设置为输入
for(i=0;i<8;i++ )
{
SCL(0);
delay_us(5);
SCL(1);
delay_us(5);
receive<<=1;
if( SDA_GET() )
{
receive|=1;
}
delay_us(5);
}
SCL(0);
return receive;
}
/******************************************************************
* 函 数 名 称:PCA9685_Write
* 函 数 说 明:向PCA9685写命令或数据
* 函 数 形 参:addr写入的寄存器地址 data写入的命令或数据
* 函 数 返 回:无
* 作 者:LC
* 备 注:无
******************************************************************/
void PCA9685_Write(uint8_t addr,uint8_t data)
{
IIC_Start();
Send_Byte(PCA_Addr);
I2C_WaitAck();
Send_Byte(addr);
I2C_WaitAck();
Send_Byte(data);
I2C_WaitAck();
IIC_Stop();
}
/******************************************************************
* 函 数 名 称:PCA9685_Read
* 函 数 说 明:读取PCA9685数据
* 函 数 形 参:addr读取的寄存器地址
* 函 数 返 回:读取的数据
* 作 者:LC
* 备 注:无
******************************************************************/
uint8_t PCA9685_Read(uint8_t addr)
{
uint8_t data;
IIC_Start();
Send_Byte(PCA_Addr);
I2C_WaitAck();
Send_Byte(addr);
I2C_WaitAck();
IIC_Stop();
delay_us(10);
IIC_Start();
Send_Byte(PCA_Addr|0x01);
I2C_WaitAck();
data = Read_Byte();
IIC_Send_Ack(1);
IIC_Stop();
return data;
}
/******************************************************************
* 函 数 名 称:PCA9685_setPWM
* 函 数 说 明:设置第num个PWM引脚,on默认为0,控制舵机旋转off角度
* 函 数 形 参:num:设置第几个引脚输出,范围0~15
* on :默认为0
* off:舵机旋转角度,范围:0~180
* 函 数 返 回:无
* 作 者:LC
* 备 注:无
******************************************************************/
void PCA9685_setPWM(uint8_t num,uint32_t on,uint32_t off)
{
IIC_Start();
Send_Byte(PCA_Addr);
I2C_WaitAck();
Send_Byte(LED0_ON_L+4*num);
I2C_WaitAck();
Send_Byte(on&0xFF);
I2C_WaitAck();
Send_Byte(on>>8);
I2C_WaitAck();
Send_Byte(off&0xFF);
I2C_WaitAck();
Send_Byte(off>>8);
I2C_WaitAck();
IIC_Stop();
}
/******************************************************************
* 函 数 名 称:PCA9685_setFreq
* 函 数 说 明:设置PCA9685的输出频率
* 函 数 形 参:freq
* 函 数 返 回:无
* 作 者:LC
* 备 注:
floor语法:
FLOOR(number, significance)
Number必需。要舍入的数值。
Significance必需。要舍入到的倍数。
说明
将参数 number 向下舍入(沿绝对值减小的方向)为最接近的 significance 的倍数。
如果任一参数为非数值型,则 FLOOR 将返回错误值 #VALUE!。
如果 number 的符号为正,且 significance 的符号为负,则 FLOOR 将返回错误值 #NUM!
示例
公式 说明 结果
FLOOR(3.7,2) 将 3.7 沿绝对值减小的方向向下舍入,使其等于最接近的 2 的倍数 2
FLOOR(-2.5, -2) 将 -2.5 沿绝对值减小的方向向下舍入,使其等于最接近的 -2 的倍数 -2
******************************************************************/
void PCA9685_setFreq(float freq)
{
uint8_t prescale,oldmode,newmode;
double prescaleval;
// freq *= 0.9; // Correct for overshoot in the frequency setting (see issue #11).
// PCA9685的内部时钟频率是25Mhz
// 公式: presale_Volue = round( 25000000/(4096 * update_rate) ) - 1
// round = floor(); floor是数学函数,需要导入 math.h 文件
// update_rate = freq;
prescaleval = 25000000;
prescaleval /= 4096;
prescaleval /= freq;
prescaleval -= 1;
prescale = floor(prescaleval+0.5f);
//返回MODE1地址上的内容(保护其他内容)
oldmode = PCA9685_Read(PCA_Model);
//在MODE1中设置SLEEP位
newmode = (oldmode&0x7F)|0x10;
//将更改的MODE1的值写入MODE1地址,使芯片睡眠
PCA9685_Write(PCA_Model,newmode);
//写入我们计算的设置频率的值
//PCA_Pre = presale 地址是0xFE,可以数据手册里查找到
PCA9685_Write(PCA_Pre,prescale);
//重新复位
PCA9685_Write(PCA_Model,oldmode);
//等待复位完成
delay_1ms(5);
//设置MODE1寄存器开启自动递增
PCA9685_Write(PCA_Model,oldmode|0xa1);
}
/******************************************************************
* 函 数 名 称:setAngle
* 函 数 说 明:设置角度
* 函 数 形 参:num要设置的PWM引脚 angle设置的角度
* 函 数 返 回:无
* 作 者:LC
* 备 注:无
******************************************************************/
void setAngle(uint8_t num,uint8_t angle)
{
uint32_t off = 0;
off = (uint32_t)(158+angle*2.2);
PCA9685_setPWM(num,0,off);
}
/******************************************************************
* 函 数 名 称:PCA9685_Init
* 函 数 说 明:PCA9685初始化,所有PWM输出频率配置与所有PWM引脚输出的舵机角度
* 函 数 形 参:hz设置的初始频率 angle设置的初始角度
* 函 数 返 回:无
* 作 者:LC
* 备 注:无
******************************************************************/
void PCA9685_Init(float hz,uint8_t angle)
{
uint32_t off = 0;
PCA9685_GPIO_Init();
//在MODE1地址上写0x00
PCA9685_Write(PCA_Model,0x00); //这一步很关键,如果没有这一步PCA9685就不会正常工作。
// pwm.setPWMFreq(SERVO_FREQ)函数主要是设置PCA9685的输出频率,
// PCA9685的16路PWM输出频率是一致的,所以是不能实现不同引脚不同频率的。
// 下面是setPWMFreq函数的内容,主要是根据频率计算PRE_SCALE的值。
PCA9685_setFreq(hz);
//计算角度
off = (uint32_t)(145+angle*2.4);
//控制16个舵机输出off角度
PCA9685_setPWM(0,0,off);
PCA9685_setPWM(1,0,off);
PCA9685_setPWM(2,0,off);
PCA9685_setPWM(3,0,off);
PCA9685_setPWM(4,0,off);
PCA9685_setPWM(5,0,off);
PCA9685_setPWM(6,0,off);
PCA9685_setPWM(7,0,off);
PCA9685_setPWM(8,0,off);
PCA9685_setPWM(9,0,off);
PCA9685_setPWM(10,0,off);
PCA9685_setPWM(11,0,off);
PCA9685_setPWM(12,0,off);
PCA9685_setPWM(13,0,off);
PCA9685_setPWM(14,0,off);
PCA9685_setPWM(15,0,off);
delay_1ms(100);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
在文件bsp_pca9685.h中,编写如下代码。
/*
* 立创开发板软硬件资料与相关扩展板软硬件资料官网全部开源
* 开发板官网:www.lckfb.com
* 技术支持常驻论坛,任何技术问题欢迎随时交流学习
* 立创论坛:club.szlcsc.com
* 关注bilibili账号:【立创开发板】,掌握我们的最新动态!
* 不靠卖板赚钱,以培养中国工程师为己任
* Change Logs:
* Date Author Notes
* 2024-01-16 LCKFB-lp first version
*/
#ifndef _BSP_PCA9685_H_
#define _BSP_PCA9685_H_
#include <stdio.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/uart.h"
#include "driver/gpio.h"
#include "driver/i2c.h"
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "freertos/queue.h"
#include <inttypes.h>
#include "sdkconfig.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include "rom/ets_sys.h"
#include "esp_system.h"
#include "driver/gpio.h"
#include "driver/spi_master.h"
#include "driver/spi_common.h"
#include "hal/gpio_types.h"
#include "driver/ledc.h"
#include "driver/mcpwm.h"
#include "esp_adc/adc_cali_scheme.h"
#include "esp_adc/adc_cali.h"
#include "driver/adc.h"
#include "esp_adc_cal.h"
#include "string.h"
//端口移植
#define GPIO_SDA 2
#define GPIO_SCL 1
//设置SDA输出模式
#define SDA_OUT() gpio_set_direction(GPIO_SDA,GPIO_MODE_OUTPUT)
//设置SDA输入模式
#define SDA_IN() gpio_set_direction(GPIO_SDA,GPIO_MODE_INPUT)
//获取SDA引脚的电平变化
#define SDA_GET() gpio_get_level(GPIO_SDA)
//SDA与SCL输出
#define SDA(x) gpio_set_level(GPIO_SDA, (x?1:0))
#define SCL(x) gpio_set_level(GPIO_SCL, (x?1:0))
#define PCA_Addr 0x80 //IIC地址
#define PCA_Model 0x00
#define LED0_ON_L 0x06
#define LED0_ON_H 0x07
#define LED0_OFF_L 0x08
#define LED0_OFF_H 0x09
#define PCA_Pre 0xFE //配置频率地址
void PCA9685_Init(float hz,uint8_t angle);
void setAngle(uint8_t num,uint8_t angle);
void PCA9685_setFreq(float freq);
void PCA9685_setPWM(uint8_t num,uint32_t on,uint32_t off);
#endif
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
移植验证
在自己工程中的main主函数中,编写如下。
/*
* 立创开发板软硬件资料与相关扩展板软硬件资料官网全部开源
* 开发板官网:www.lckfb.com
* 技术支持常驻论坛,任何技术问题欢迎随时交流学习
* 立创论坛:club.szlcsc.com
* 关注bilibili账号:【立创开发板】,掌握我们的最新动态!
* 不靠卖板赚钱,以培养中国工程师为己任
* Change Logs:
* Date Author Notes
* 2024-01-16 LCKFB-lp first version
*/
#include <stdio.h>
#include "bsp_pca9685.h"
#include "string.h"
#include "esp_private/esp_task_wdt.h"
#include "esp_private/esp_task_wdt_impl.h"
int app_main(void)
{
uint8_t i = 0;
esp_task_wdt_deinit();
PCA9685_Init(60 , 0);
vTaskDelay(1000 / portTICK_PERIOD_MS);
printf("Demo Start......\r\n");
while(1)
{
i = ( i + 1 ) % 180;
setAngle(0,i);
vTaskDelay(50 / portTICK_PERIOD_MS);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
上电效果:
驱动文件: