LED PWM控制芯片PCA9685的Linux 驱动

本文介绍PCA9685 LED驱动芯片在Linux下的设备驱动调试过程,包括开发环境搭建、DeviceTree配置及LED控制实现,并解决了LED在特定亮度设置下无法正常工作的bug。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文目的

在调试PCA9685的Linux设备驱动过程中, 发现有值得记录和备忘的事项 . 特此记录,方便自己查阅.

PCA9685简介

PCA9685是NXP生产的一款LED驱动芯片, 其主要特性:
1. 16通道, 即能够提供16个GPIO控制管脚,相应能够控制16个LED;
2. PWM控制. 通过PWM机制来控制LED的亮度; 亮度的控制寄存器12bit,即亮度的取值范围为: 0~4095;

其他特性参考手册.

开发环境

Ti am335x SDK: ti-processor-sdk-Linux-rt-am335x-evm-03.01.00.06
Linux版本: 4.4.19
pca9685的驱动位于:

(Linux source root)/drivers/pwm/pwm-pca9685.c

源代码可以直接查看
pwm-pca9685.c

DeviceTree

第一步在dts文件中添加对pca9685芯片的支持.
在 Documentation/devicetree/bindings/pwm/nxp,pca9685-pwm.txt中已经有对其的基本描述, 在此拷贝如下:

NXP PCA9685 16-channel 12-bit PWM LED controller
================================================

Required properties:
  - compatible: "nxp,pca9685-pwm"
  - #pwm-cells: Should be 2. See pwm.txt in this directory for a description of
    the cells format.
    The index 16 is the ALLCALL channel, that sets all PWM channels at the same
    time.

Optional properties:
  - invert (bool): boolean to enable inverted logic
  - open-drain (bool): boolean to configure outputs with open-drain structure;
               if omitted use totem-pole structure

Example:

For LEDs that are directly connected to the PCA, the following setting is
applicable:

pca: pca@41 {
    compatible = "nxp,pca9685-pwm";
    #pwm-cells = <2>;
    reg = <0x41>;
    invert;
    open-drain;
};

直接拷贝例子到自己的dts文件中,并修改I2C的从设备地址.
I2C的从设备地址的低6bit由芯片的A5~A0决定.
在本人硬件设计中, A5~A0都接地,所以地址直接是0x40

&I2C0 {
....
    pca9685pw0: pca9685pw0@40 {
        compatible = "nxp,pca9685-pwm";
        reg = <0x40>; 
        #pwm-cells = <2>;
        open-drain;
        invert;
    };
...
}

注意, 实际的pca9865设备都是挂在某一个具体的I2C总线下, 因此上面的设备树子节点也应该挂在某I2C节点下.在上面的例子中, pca9685是挂在i2c0总线下。

完成上述步骤之后, 本人遇到了第一个问题:
在menuconfig中钩选上pca9685的配置, 重新编译内核和dtb文件后,发现在sys文件系统中, 不存在对pca9685对应的LEDs的操作。pca9685的驱动至少要对用户提供三种操作:
GPIO置低;
GPIO置高;
pwm占空比的调整;

而pca9685相关的sys入口只有/sys/class/pwm/pwmchip2/, 其目录内容只有:

root@am335x-evm:/sys/class/pwm/pwmchip2# ls
device     export     npwm       power      subsystem  uevent     unexport
root@am335x-evm:/sys/class/pwm/pwmchip2#

回想曾经调试过LCD背光模块, 它一样是是使用PWM来控制亮度的, 其dts文件中包含2部分:

...
/* LCD Backlight */
    backlight {
        compatible = "pwm-backlight";
        PWM_POLARITY_INVERTED;
        pwms = <&ehrpwm1 0 50000 0>;//ehrpwm1 是下面定义的pwm控制器;
        brightness-levels = <0 51 53 56 62 75 101 152 255>;
        default-brightness-level = <6>;
    };
...

/* 这是PWM控制器 */
&epwmss1 {
    status = "okay";

    ehrpwm1: pwm@48302200 {
        status = "okay";
        pinctrl-names = "default";
        pinctrl-0 = <&epwmss1_pins_default>;
    };
};
...

其中第一部分是对背光的描述, 第二部分是对PWM控制器的描述。
而回到PCA9685的驱动dts文件,pca9685pw0只是相当于定义了PWM控制器。
因此还需要一个定义LED驱动的节点信息。

查看Linux源代码, 在drivers/leds下发现有文件leds-pwm.c。到此基本查找到问题所在。
查看Documentation/devicetree/bindings/leds/leds-pwm.txt后,添加自己的led节点 部分dts:

...
pwm-bicolor-leds{
    compatible = "pwm-leds";
        ambled0 {
            label = "ambled0";
            pwms = <&pca9685pw0 0 5000000>; //pca9685pw0是上述定义的PWM控制器, 0表示第一个gpio控制的LED。
            max-brightness = <4095>;
        };
        ambled1 {
            label = "ambled1";
            pwms = <&pca9685pw0 1 5000000>;
            max-brightness = <4095>;
        };
...

重新配置menuconfig,使能leds-pwm驱动,编译dtb文件, 在sys文件夹下出现了如下 内容:


root@am335x-evm:/sys/class/leds# ls
Funled1      Rungreenled  ambled2      ambled8      redled2      redled8
Funled2      Runredled    ambled3      ambled9      redled3      redled9
Funled3      **ambled0**      ambled4      redled0      redled4
Funled4      **ambled1**      ambled5      redled1      redled5
LRgreenled   ambled10     ambled6      redled10     redled6
LRredled     ambled11     ambled7      redled11     redled7
root@am335x-evm:/sys/class/leds#

至此, pca9685的dts文件移植成功。

驱动测试

当在sys/class/leds下显示出了dts设定的LED时,可以使用下列命令测试LED灯:

cd /sys/class/leds;
echo 0 > ambled0/brightness  //关闭LED
echo 4095 > ambled0/brightness  //最大亮度打开LED
echo 400 > ambled0/brightness  //按照400亮度打开LED

在测试过程中,遇到调试过程中的第二个问题:
当执行echo 0 > ambled0/brightness, LED能够正常关闭。
而再执行echo 4095 > ambled0/brightness,led不能正常打开,而是LED灯闪烁一下,然后继续维持OFF的状态。
通过添加打印语句, 跟踪代码执行情况,发现最终操作pca9685的操作函数是 pca9685_pwm_config();

static int pca9685_pwm_config(struct pwm_chip *chip, struct pwm_device *pwm,
                  int duty_ns, int period_ns)
{
    struct pca9685 *pca = to_pca(chip);
    unsigned long long duty;
    unsigned int reg;
    int prescale;

    if (period_ns != pca->period_ns) {
        prescale = DIV_ROUND_CLOSEST(PCA9685_OSC_CLOCK_MHZ * period_ns,
                         PCA9685_COUNTER_RANGE * 1000) - 1;

        if (prescale >= PCA9685_PRESCALE_MIN &&
            prescale <= PCA9685_PRESCALE_MAX) {
            /* Put chip into sleep mode */
            regmap_update_bits(pca->regmap, PCA9685_MODE1,
                       MODE1_SLEEP, MODE1_SLEEP);

            /* Change the chip-wide output frequency */
            regmap_write(pca->regmap, PCA9685_PRESCALE, prescale);

            /* Wake the chip up */
            regmap_update_bits(pca->regmap, PCA9685_MODE1,
                       MODE1_SLEEP, 0x0);

            /* Wait 500us for the oscillator to be back up */
            udelay(500);

            pca->period_ns = period_ns;

            /*
             * If the duty cycle did not change, restart PWM with
             * the same duty cycle to period ratio and return.
             */
            if (duty_ns == pca->duty_ns) {
                regmap_update_bits(pca->regmap, PCA9685_MODE1,
                           MODE1_RESTART, 0x1);
                return 0;
            }
        } else {
            dev_err(chip->dev,
                "prescaler not set: period out of bounds!\n");
            return -EINVAL;
        }
    }

    pca->duty_ns = duty_ns;

    if (duty_ns < 1) {
    //当执行echo 0 > ambled0/brightness, 程序会进入该if语句, 直接设置寄存器LED_N_OFF_H,使得LED状态为OFF。
        if (pwm->hwpwm >= PCA9685_MAXCHAN)
            reg = PCA9685_ALL_LED_OFF_H;
        else
            reg = LED_N_OFF_H(pwm->hwpwm);

        regmap_write(pca->regmap, reg, LED_FULL);

        return 0;
    }

    if (duty_ns == period_ns) {
    //当执行echo 4095 > ambled0/brightness, 程序会进入该if语句,首先关闭寄存器LED_N_OFF_H和LED_N_OFF_L,再设置LED_N_ON_H,使得LED状态为ON。
        /* Clear both OFF registers */
        if (pwm->hwpwm >= PCA9685_MAXCHAN)
            reg = PCA9685_ALL_LED_OFF_L;
        else
            reg = LED_N_OFF_L(pwm->hwpwm);

        regmap_write(pca->regmap, reg, 0x0);

        if (pwm->hwpwm >= PCA9685_MAXCHAN)
            reg = PCA9685_ALL_LED_OFF_H;
        else
            reg = LED_N_OFF_H(pwm->hwpwm);

        regmap_write(pca->regmap, reg, 0x0);

        /* Set the full ON bit */
        if (pwm->hwpwm >= PCA9685_MAXCHAN)
            reg = PCA9685_ALL_LED_ON_H;
        else
            reg = LED_N_ON_H(pwm->hwpwm);

        regmap_write(pca->regmap, reg, LED_FULL);
        //在此设置延时5S, 查看LED是否被点亮。
        usleep(5000);
        return 0;
    }

    duty = PCA9685_COUNTER_RANGE * (unsigned long long)duty_ns;
    duty = DIV_ROUND_UP_ULL(duty, period_ns);

    if (pwm->hwpwm >= PCA9685_MAXCHAN)
        reg = PCA9685_ALL_LED_OFF_L;
    else
        reg = LED_N_OFF_L(pwm->hwpwm);

    regmap_write(pca->regmap, reg, (int)duty & 0xff);

    if (pwm->hwpwm >= PCA9685_MAXCHAN)
        reg = PCA9685_ALL_LED_OFF_H;
    else
        reg = LED_N_OFF_H(pwm->hwpwm);

    regmap_write(pca->regmap, reg, ((int)duty >> 8) & 0xf);

    /* Clear the full ON bit, otherwise the set OFF time has no effect */
    if (pwm->hwpwm >= PCA9685_MAXCHAN)
        reg = PCA9685_ALL_LED_ON_H;
    else
        reg = LED_N_ON_H(pwm->hwpwm);

    regmap_write(pca->regmap, reg, 0);

    return 0;
}

通过添加延时函数(见上述代码的注释), 确定执行echo 4095 > ambled0/brightness时LED能够维持5s为亮,然后又转为OFF状态。
初步判定在驱动的其他地方又操作了寄存器LED_N_OFF_H。
很容易就发现在驱动中还有两函数操作了该寄存器, 分别是pca9685_pwm_disable()和pca9685_pwm_enable().
分别在这两个函数中添加打印输出语句,测试后发现确实在执行echo 4095 > ambled0/brightness时,执行pca9685_pwm_config()后再次调用了pca9685_pwm_enable(), 而在echo 0 > ambled0/brightness时则最后调用了pca9685_pwm_disable()函数。
经过一番代码搜索和测试,最终发现调用的地方是在文件leds-pwm.c中的函数:__led_pwm_set()

static void __led_pwm_set(struct led_pwm_data *led_dat) {   
    int new_duty = led_dat->duty;
     pwm_config(led_dat->pwm, new_duty, led_dat->period); 
     if (new_duty == 0)
         //亮度为0时执行
        pwm_disable(led_dat->pwm); 
    else 
        ////亮度为非0时执行,pwm_enable()函数会重新将芯片的寄存器LED_N_ON_H bit12 清0。
        pwm_enable(led_dat->pwm); 
}

pca9685_pwm_config()函数会在亮度最大时设置寄存器LED_N_ON_H的bit12, 而pwm_enable()函数又会将之清0。
找到问题所在,修改代码很简单:

static void __led_pwm_set(struct led_pwm_data *led_dat) {   
    int new_duty = led_dat->duty;
     pwm_config(led_dat->pwm, new_duty, led_dat->period); 
     if (new_duty == 0)
        pwm_disable(led_dat->pwm); 
    else  if(new_duty !=led_dat->period)//当最大亮度, 不执行pwm_enable
        pwm_enable(led_dat->pwm); 
}

即当设置亮度最大值时,不执行pwm_enable()函数。

总结

pca9685的驱动已经包含在Linux的源代码中,因此不用重新写。只需要拷贝和修改dts文件的相关节点。
在本人调试的时候, 网上对该芯片的dts节点配置没有提供完整的例子。只有Linux的文档提供了PWM控制器的节点例子。需要个人添加上LED的节点,才能完整控制LED灯。
在4.4.19版本中,源代码存在一个bug,当设定LED的亮度为0后,再设定最大亮度4095,LED不能正常点亮。通过修改文件leds-pwm.c的__led_pwm_set()函数中的条件,可以很简单的解决这个问题。

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值