树莓派用WiringPi和Bcm2835库和PCA9685输出PWM



1.前言

PWM(脉宽调制)被常用于控制直流电机转速、舵机、电调等。树莓派可以通过WiringPi,Bcm2835或通过16路PWM芯片PCA9685产生。
测试环境见我的这篇博客

2.Bca2835发生PWM

BCM2835 在指定的GPIO 引脚上支持硬件 PWM。此 bcm2835 库提供配置和控制这些引脚上的 PWM 输出的功能。(官方文档又说当前版本只能用BCM的18号引脚,即RPi pin 1-12,有待测试,但有文档说40-pin models物理引脚 编号12, 32, 33, 35支持硬件PWM,即BCM编号18,12,13,19。)
BCM2835 包含 2 个独立的 PWM 通道 (0 和 1),每个通道都只能连接到指定的 GPIO 引脚的。以下 GPIO 引脚可连接到以下 PWM 通道:

GPIO PIN    RPi pin  PWM Channel    ALT FUN
12                    0            0
13                    1            0
18         1-12       0            5
19                    1            5
40                    0            0
41                    1            0
45                    1            0
52                    0            1
53                    1            1

为了让 GPIO 引脚从其 PWM 通道发出输出,必须将其设置为上述指出的 Alt Fun。注意:树莓派的当前版本仅暴露其中一个引脚(GPIO 18 = RPi Pin 1-12),因此这是 RPi 上唯一可用于 PWM 的 IO 引脚。此外,它必须设置为ALT FUN 5,以获得PWM输出(可能物理上支持四个引脚,但是当前版本的Bcm库只能用其中的一个)。

结合官方例子说明用法:

#include <bcm2835.h>
#include <stdio.h>
 
// PWM output on RPi Plug P1 pin 12 (which is GPIO pin 18) 即BCM编号为18的引脚
// in alt fun 5.
// Note that this is the _only_ PWM pin available on the RPi IO headers
#define PIN RPI_GPIO_P1_12   //RPI_GPIO_P1_12 的宏定义为18
// and it is controlled by PWM channel 0
#define PWM_CHANNEL 0
// This controls the max range of the PWM signal
#define RANGE 1024
 
int main(int argc, char **argv)
{
    if (!bcm2835_init())
        return 1;
 
    // Set the output pin to Alt Fun 5, to allow PWM channel 0 to be output there
    bcm2835_gpio_fsel(PIN, BCM2835_GPIO_FSEL_ALT5);
 
    // Clock divider is set to 16.
    // With a divider of 16 and a RANGE of 1024, in MARKSPACE mode,
    // the pulse repetition frequency will be
    // 1.2MHz/1024 = 1171.875Hz, suitable for driving a DC motor with PWM
    bcm2835_pwm_set_clock(BCM2835_PWM_CLOCK_DIVIDER_16);
    bcm2835_pwm_set_mode(PWM_CHANNEL, 1, 1);
    bcm2835_pwm_set_range(PWM_CHANNEL, RANGE);
 
    // Vary the PWM m/s ratio between 1/RANGE and (RANGE-1)/RANGE
    // over the course of a a few seconds
    int direction = 1; // 1 is increase, -1 is decrease
    int data = 1;
    while (1)
    {
        if (data == 1)
            direction = 1;   // Switch to increasing
        else if (data == RANGE-1)
            direction = -1;  // Switch to decreasing
        data += direction;
        bcm2835_pwm_set_data(PWM_CHANNEL, data);
        bcm2835_delay(1);
    }
 
    bcm2835_close();
    return 0;
}

函数:void bcm2835_gpio_fsel (uint8_t pin, uint8_t mode)
描述:设置给定管脚的功能选择寄存器,将管脚配置为输入、输出或6个备用功能之一。
形参 pin:引脚,需要使用Bcm编号。
形参 mode:从下图中选择,若要18引脚配置成PWM输出,则需要选择BCM2835_GPIO_FSEL_ALT5
在这里插入图片描述
函数:void bcm2835_pwm_set_clock(uint32_t divisor)
描述:设置PWM时钟除数,PWM时钟由19.2MHz时钟导出,如果divisor=16,那么PWM的时钟频率为19.2/16=1.2MHz。您可以设置任何分频器,但一些常见的分频器由从bcm2835PWMClockDivider中选择BCM2835_PWM_CLOCK_DIVIDER_*。
形参 divisor:

函数:void bcm2835_pwm_set_mode(uint8_t channel, uint8_t markspace,uint8_t enabled
)

描述:设置给定PWM通道的模式,允许您控制PWM模式并启用/禁用该通道
形参 markspace:1 - Mark-Space 模式(每RANGE个时钟中,先出现DATA个高电平,再出现RANGE-DATA个低电平);0 - Balanced mode(硬件发送时钟脉冲的组合,最终每个RANGE时钟中出现DATA个高电平,RANGE-DATA个低电平)。两种模式的结果最终都呈现,每个PWM周期内的占空比都为DATA/RANGE.
形参 enabled:1-开始;0-禁用

函数:void bcm2835_pwm_set_range(uint8_t channel, uint32_t range)
描述:设置PWM输出的最大范围。DATA可以在0和该范围之间变化,以控制PWM输出。
形参 channel:0或1
形参 range:在bcm2835_pwm_set_clock函数中,可以设定PWM的脉冲时钟,对于1.2MHz的脉冲时钟,即一秒产生1.2MHz的脉冲。通过设置range,再把RANGE个脉冲划分为一组(一个周期,其一个周期有高电平数量为DATA,低电平数量为RANGE-DATA)。以range=1024为例,那么我们通常说的PWM的频率为1.2/1024 = 1171.875Hz。所以通过BCM库输出的PWM的频率的计算公式为19.2MHz/divisor/range。看见很多博客说即使按照这个公式设置,最终输出的频率也会与设计的有所偏差,有条件的可以用示波器看波形再手动微调divisor。

函数:void bcm2835_pwm_set_data (uint8_t channel, uint32_t data)
描述:设置data控制PWM占空比比为DATA/RANGE
形参 channel:0或1
形参 data:通过改变data的值,改变占空比。data的取值范围为0-RANGE。

3.WiringPi发生PWM

3.1 硬件PWM

函数:void pinMode (int pin, int mode)
描述:将引脚的模式设置为 INPUT, OUTPUT, PWM_OUTPUT or GPIO_CLOCK. PWM_OUTPUT 输出模式只可以用于wiringPi编号1引脚。

函数:pwmSetMode (int mode)
描述:PWM发生器可以在两种模式下运行–“balanced”和mark:space”. 树莓派中的默认模式是“balanced”。可以通过参数PWM_MODE_BAL和PWM_MODE_MS来进行切换。两种模式的理解见上一节

函数:pwmSetClock (int divisor)
描述:设置时钟分频器

函数:pwmSetRange (unsigned int range)
描述:设置range寄存器,默认是1024.

函数:void pwmWrite (int pin, int value)
描述:将value写入给定引脚的PWM寄存器。Raspberry Pi有一个板载PWM引脚,引脚1(BMC_GPIO 18,Phys 12),范围为0-1024。其他PWM设备可能有其他PWM范围。在Sys模式下,此功能无法控制Pi的板载PWM。

本用法也存在divisor,range和value三个参数来控制PWM的频率和占空比。详细见上一节。

// wiringPi 硬件PWM 运行需要权限 sudo
#include <stdio.h>
#include <wiringPi.h>

// 设置GPIO1为LED灯的控制引脚
#define PWM_PIN     1
int main(void)
{
    int bright ;
    printf("wiringPi-C PWM test program\n") ;
    // 配置gpio
    wiringPiSetup();
    // 配置GPIO1为PWM模式
    pinMode(PWM_PIN, PWM_OUTPUT);
    // 配置PWM输出为mark:space模式
    pwmSetMode(PWM_MODE_MS);
    // 配置50HZ的PWM,50Hz = 19.2*1000000/divisor/range ,最终输出的PWM频率可能不准,可以对着示波器调整分频divisor
    int divisor = 375int range = 1024pwmSetClock(divisor);
    pwmSetRange(range);
    while(1) {
        for(bright = 0 ; bright < range; bright++) {
            // bright为 0 ~ range,
            pwmWrite(PWM_PIN, bright);
            delay(1) ;
        }
        for(bright = range; bright >= 0; bright--) {
            pwmWrite(PWM_PIN, bright);
            delay(1) ;
        }
    }
    return 0 ;
}

3.2 软件PWM-Software PWM Library

上面写到硬件PWM可能只能产生一路PWM,使用WiringPi的Software PWM Library可以产生多个软PWM。引脚也可由任意定义。
由于是软PWM,所以使用PWM会占用CPU的使用率。为了保持低CPU使用率,最小脉冲宽度是100μs。range的默认的取值为100,这样可以得到100Hz的PWM。可以降低range以获得更高的频率,但是会牺牲分辨率;也可以增加range以获得更高的分辨率,但这样会降低频率。如果更改驱动程序代码中的脉冲宽度,需要注意,wiringPi在软件循环中的延迟小于100μs 时,将导致CPU的使用率急剧上升,控制多个pin输出PWM几乎是不可能的。还要注意的是,虽然例程以更高的实时优先级运行,但Linux仍然会影响生成信号的准确性。然而,在这些限制条件下,控制灯/LED或电机是非常可行的。
使用软PWM库,头文件除了包含#include <wiringPi.h>,还需要包含#include <softPwm.h>。在编译时,除了添加 -l wiringPi 还得添加 -l pthread来链接pthread库。

#include <wiringPi.h>
#include <softPwm.h>

调用软PWM函数前,首先得初始化wiringPi,必须使用wiringPiSetup()、wiringPiSetupGpio()或wiringPiSetupPhys()函数之一初始化wiringPi,推荐使用第一个。

函数:int softPwmCreate (int pin, int initialValue, int pwmRange)
这将创建一个软件控制的PWM引脚。您可以使用任何GPIO引脚,引脚编号将是您使用的wiringPiSetup()函数的编号。pwmRange使用100,那么给定管脚的初值initialValue可以是0(关闭)到100(完全打开)之间的任何值。
返回值为0表示成功。否则可以检查全局errno变量,看看哪里出了问题。
PWM频率计算:PWMfreq = 1 x 10^6 / (100 x pwmRange)

函数:void softPwmWrite (int pin, int value)
这将更新给定引脚上的PWM值。该值被检查为在范围内,以前未通过softPwmCreate初始化的管脚将被忽略。

注意:

  1. 当range取默认值100时,PWM输出的每个“周期”需要10毫秒,因此尝试每秒更改PWM值超过100次将是徒劳的。其他频率依次类推。
  2. 在软PWM模式下激活的每个引脚使用大约0.5%的CPU。
  3. 目前没有办法在程序运行时禁用引脚上的软脉宽调制。即在程序时通过softPwmCreate函数设置的引脚将一直输出PWM,没有函数停止它。
  4. 你需要保持你的程序运行,以保持PWM输出!
//软件PWM,运行可以不要权限
#include <stdio.h>
#include <wiringPi.h>
#include <softPwm.h>
// 定义控制LED灯的GPIO引脚,使用GPIO0
#define PWM_PIN     0
int main(void)
{
    int bright ;
    printf("wiringPi-C Software PWM test program\n") ;
    // 初始化
    wiringPiSetup();
    pinMode(PWM_PIN, OUTPUT);
	//配置50Hz的PWM,50Hz = 1*1000000/(100*range)
	int range = 200;
    softPwmCreate(PWM_PIN, 0, range );
    while(1) {
        for(bright = 0; bright < range ; bright++) {
            softPwmWrite(PWM_PIN, bright);
            delay(10);
        }
        for(bright = range ; bright >= 0; bright--) {
            softPwmWrite(PWM_PIN, bright);
            delay(10);
        }
    }
    return 0 ;
}

4.外接PCA9685输出PWM

可以看出通过Bcm2835和wiringPi都可以产生PWM,但都有局限性,输出端口少,精度不高。树莓派可以外接PCA9685来产生多路PWM波。
可以买到的最常见的PCA9685模块都差不多长这样:
在这里插入图片描述
在这里插入图片描述
树莓派通过2线制IIC和PCA9685通信,通过配置向其发送指令或配置寄存器的值来改变通道的PWM频率和占空比。树莓派控制PCA,首先需要大致了解一下IIC通信机制,在树莓派上实现IIC通信;然后需要学习PCA9685用户手册,进而通过IIC控制PCA。前者,可以借助wiringPi或者Bcm2835库快速使用IIC通信(本文使用Bca9685库的IIC)。后者,可以借鉴Arduino生态中关于PCA9685优秀的库进行移植(本文借鉴Adafruit_PWM_Servo_Driver_Library)。移植时,只需将Arduino中的IIC通信的函数(其使用Wire库来实现IIC)修改为Bcm中对应功能的函数即可。

4.1树莓派IIC配置

首先需要打开树莓派的IIC,使用

sudo raspi-config

然后Interface Options》I2C,然后打开,然后Finish,可能需要重启一下。
一般使用的SDA.1和SCL.1引脚来连接一般的IIC从设备(i2c1是为了控制大部分i2c设备的,i2c0是为了控制摄像头或者显示屏等外设用的,深究可以查看这篇博客)。
检验线路是否连接好或者设备是否正常可以使用指令 i2cdetect -y 1 来扫描i2c1总线上的设备,如果i2c1总线上至挂载了PCA9685,可以扫描出0x40和0x70两个地址。
其中,有6个地址控制脚,通过这些引脚可以控制设备的i2c地址。7位的I2C地址为:0x40 + A5:A0,A5到A0如果不做任何处理的话是0,想要把哪一位置1就把那个引脚焊到一起。
另外用i2cdetect检测出还有一个0x70地址一直存在,这是一个通用地址,可以给所有从机下达指令。

4.2移植

Bcm中的IIC基础可以看这篇
首先对Adafruit_PWMServoDriver.h中的代码进行修改,最后形成PCA9685.h文件,删除一些和Arduino相关的头文件和声明,然后加上了#include <bcm2835.h>等。

#ifndef PCA9685_H
#define PCA9685_H

#include <bcm2835.h>
#include <cstdio>
#include <cstdint>
#include <cmath>

#define PCA9685_MODE1 0x0
#define PCA9685_PRESCALE 0xFE

#define LED0_ON_L 0x6
#define LED0_ON_H 0x7
#define LED0_OFF_L 0x8
#define LED0_OFF_H 0x9

/**
 * @brief  Class that stores state and functions for interacting with PCA9685 PWM chip
 */
class PCA9685{
	public:
		PCA9685(uint8_t addr = 0x40);
		~PCA9685();
		int init();
		void setPWMFreq(float freq = 50);
		void setPWM(uint8_t channel, uint32_t pulseWidth);
		void setPWM(uint8_t channel, uint16_t on, uint16_t off);
		
	private:
		uint8_t _addr;
		uint32_t _T;  //PWM Period[us]
		char sendBuf[5];
		uint8_t errCode;
		
		uint8_t read8(uint8_t addr);
		void write8(uint8_t addr, uint8_t d);
};
#endif

可以发现调用的Arduino的Wire库进行IIC读写的相关函数在read8、write8和setPWM(uint8_t channel, uint16_t on, uint16_t off)中,所以在PCA9685.cpp中最大的工作就是使用Bcm的方式重写IIC读写。最后稍做整理和精简就可以使用了。

//Adafruit_PWMServoDriver.cpp中的read8、write8、setPWM函数
uint8_t Adafruit_PWMServoDriver::read8(uint8_t addr) {
  _i2c->beginTransmission(_i2caddr);
  _i2c->write(addr);
  _i2c->endTransmission();

  _i2c->requestFrom((uint8_t)_i2caddr, (uint8_t)1);
  return _i2c->read();
}

void Adafruit_PWMServoDriver::write8(uint8_t addr, uint8_t d) {
  _i2c->beginTransmission(_i2caddr);
  _i2c->write(addr);
  _i2c->write(d);
  _i2c->endTransmission();
}
void Adafruit_PWMServoDriver::setPWM(uint8_t num, uint16_t on, uint16_t off) {
#ifdef ENABLE_DEBUG_OUTPUT
  Serial.print("Setting PWM "); Serial.print(num); Serial.print(": "); Serial.print(on); Serial.print("->"); Serial.println(off);
#endif

  _i2c->beginTransmission(_i2caddr);
  _i2c->write(LED0_ON_L+4*num);
  _i2c->write(on);
  _i2c->write(on>>8);
  _i2c->write(off);
  _i2c->write(off>>8);
  _i2c->endTransmission();
}
//在树莓派中使用Bcm移植的read8、write8、setPWM函数
uint8_t PCA9685::read8(uint8_t addr)
{
	bcm2835_i2c_setSlaveAddress(_addr);
	sendBuf[0] = addr;
	char readByte;
	if((errCode = bcm2835_i2c_read_register_rs(sendBuf,&readByte,1)))
	{
		printf("bcm2835_i2c_read_register_rs failed at %s%d, errCode = 0x%x\n",__FILE__,__LINE__,errCode);
		return 0;
	}else
		return static_cast<uint8_t>(readByte);
}

void PCA9685::write8(uint8_t addr, uint8_t d)
{
	bcm2835_i2c_setSlaveAddress(_addr);
	sendBuf[0] = addr;
	sendBuf[1] = d;
	if((errCode = bcm2835_i2c_write(sendBuf,2)))
		printf("bcm2835_i2c_write failed, errCode = 0x%x\n", errCode);
}
void PCA9685::setPWM(uint8_t channel, uint16_t on, uint16_t off)
{
	bcm2835_i2c_setSlaveAddress(_addr);
	sendBuf[0] = LED0_ON_L+4*channel;
	sendBuf[1] = on & 0x00FF;
	sendBuf[2] = on >> 8;
	sendBuf[3] = off & 0x00FF;
	sendBuf[4] = off >> 8;
	if((errCode = bcm2835_i2c_write(sendBuf,5)))
		printf("bcm2835_i2c_write failed at %s%d, errCode = 0x%x\n",__FILE__,__LINE__, errCode);
}

在树莓派上基于Bcm2835移植的PCA9685驱动库放在我的github上,欢迎使用。

5.参考

感谢各位大佬的博客对我的帮助!

  1. Bcm官网的PWM文档
  2. WiringPi的Software PWM Library官方文档
  3. PCA9685 16路12位pwm信号发生器
  4. 树莓派4B硬件pwm驱动舵机
  5. [树莓派系列] 入门WiringPi库的PWM接口(C和Python)
  • 5
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
要在STM32上使用PCA9685进行PWM输出,你需要使用I2C总线与PCA9685通信,并发送适当的控制命令。以下是一些步骤供参考: 1. 首先,确保你的STM32上已经正确配置了I2C总线,并且正确连接了PCA9685。 2. 使用STM32的I2C函数初始化I2C总线,并设置适当的时钟速率和其他参数。 3. 在STM32上编写一个函数,用于设置PCA9685PWM输出。该函数将接受通道号、占空比等参数,并发送适当的数据序列来设置PCA9685PWM输出。 4. 在主程序中调用该函数,以设置PCA9685PWM输出。你可以根据需要设置通道号和占空比值。 以下是一个示例代码片段,用于设置PCA9685PWM输出: ```c #include "stm32fxxx.h" // STM32 #define PCA9685_ADDRESS 0x40 // PCA9685的I2C地址 void PCA9685_SetPWM(uint8_t channel, uint16_t on_time, uint16_t off_time) { // 向PCA9685写入PWM占空比值 I2C_StartTransmission(I2C1, I2C_Direction_Transmitter, PCA9685_ADDRESS); I2C_WriteData(I2C1, 0x06 + (4 * channel)); // 写入PWM寄存器起始地址 I2C_WriteData(I2C1, on_time & 0xFF); // 写入低字节 I2C_WriteData(I2C1, on_time >> 8); // 写入高字节 I2C_WriteData(I2C1, off_time & 0xFF); // 写入低字节 I2C_WriteData(I2C1, off_time >> 8); // 写入高字节 I2C_StopTransmission(I2C1); } int main(void) { // 初始化I2C总线 // 设置PCA9685PWM输出 PCA9685_SetPWM(0, 0, 2048); // 设置通道0的占空比为50% while (1) { // 主程序代码 } } ``` 请注意,以上代码只是一个示例,你需要根据你的具体硬件和函数进行适当的修改。此外,还应该检查PCA9685的数据手册以获取更多详细信息。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值