NXP

LED PWM控制芯片PCA9685的Linux 驱动

2019-07-12 13:57发布

本文目的

在调试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! "); 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()函数中的条件,可以很简单的解决这个问题。