本文目的
在调试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"
-
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";
reg = <0x41>;
invert;
open-drain;
};
直接拷贝例子到自己的dts文件中,并修改I2C的从设备地址.
I2C的从设备地址的低6bit由芯片的A5~A0决定.
在本人硬件设计中, A5~A0都接地,所以地址直接是0x40
&I2C0 {
....
pca9685pw0: pca9685pw0@40 {
compatible = "nxp,pca9685-pwm";
reg = <0x40>;
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
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
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) {
regmap_update_bits(pca->regmap, PCA9685_MODE1,
MODE1_SLEEP, MODE1_SLEEP);
regmap_write(pca->regmap, PCA9685_PRESCALE, prescale);
regmap_update_bits(pca->regmap, PCA9685_MODE1,
MODE1_SLEEP, 0x0);
udelay(500);
pca->period_ns = period_ns;
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) {
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) {
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);
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);
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);
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)
pwm_disable(led_dat->pwm);
else
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(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()函数中的条件,可以很简单的解决这个问题。