基本概念
前后调试过两次高通平台pwm,个人觉得有必要来这样一篇帖子记录一下,有备无患。
首先明确基本概念,pwm全称为pulse width modulation,即脉冲宽度调制。脉冲即周期脉冲方波,宽度调制说的就是脉冲方波高低电平占比,就是我们常说的占空比,那调这玩意有啥用呢?
既然存在pwm就肯定有需要他的地方,到目前为止,我所接触到的显示屏都是TFT-LCD屏,也就是我们比较常用的薄膜晶体管液晶显示屏,特点是显示屏本身不能发光,需要依赖背光板才能显示,有关屏幕显示技术可自行查阅资料,此处不做过多展开。
所以要想点亮LED背光板,就需要用到背光芯片为背光板供电并调整其亮度;而我们调试的pwm就是给背光芯片使用,并通过占空比来调整背光亮度。
调试PWM功能总共分两块,PWM控制器 & 端子配置
PWM子系统框图
Linux PWM子系统框图如上
- 最底层PWM硬件,一般soc跟mcu均具备输出pwm的能力,高通平台由pmic输出PWM,与soc之间通过spmi通信;
- PWM HW driver,硬件驱动层,一般由芯片厂商提供,该驱动用于直接读写寄存器来控制PWM硬件;
- PWM subsystem,HW driver备好之后,会向PWM子系统注册PWM设备,子系统这部分为Linux源码,目的是为了吸收不同芯片厂商带来的差异,并向上提供统一的接口用于控制PWM;
- PWM consumer driver,PWM设备向系统注册注册完毕之后,PWM的使用者会通过PWM子系统提供的接口来获取PWM设备,再通过PWM ops来控制跟调整PWM参数;这个consumer driver推荐使用led driver,注册led设备后,led class会在sysfs里提供brightness/enable等接口,方便使用;
- Application,有了led提供的接口,应用程序就可以通过接口来控制PWM了。
PWM HW driver
HW driver加载后,初始化PWM控制器并向系统注册PWM设备,查阅了Linux 4下面两个版本,发现其中并没有包含高通PWM driver,此处只能贴上节选,想要查看全部源码需要自行查阅资料。
高通私有qpnp pwm chip结构 & Linux标准pwm chip结构,二者属于嵌套结构,在Linux结构基础上做了再次封装,用于保存高通寄存器及其他私有变量;
/* Public facing structure */
struct qpnp_pwm_chip {
struct spmi_device *spmi_dev;
struct pwm_chip chip;
bool enabled;
struct _qpnp_pwm_config pwm_config;
struct qpnp_lpg_config lpg_config;
spinlock_t lpg_lock;
enum qpnp_lpg_revision revision;
u8 sub_type;
u32 flags;
u8 qpnp_lpg_registers[QPNP_TOTAL_LPG_SPMI_REGISTERS];
int channel_id;
const char *channel_owner;
u32 dtest_line;
u32 dtest_output;
bool in_test_mode;
};
/**
* struct pwm_chip - abstract a PWM controller
* @dev: device providing the PWMs
* @list: list node for internal use
* @ops: callbacks for this PWM controller
* @base: number of first PWM controlled by this chip
* @npwm: number of PWMs controlled by this chip
* @pwms: array of PWM devices allocated by the framework
* @can_sleep: must be true if the .config(), .enable() or .disable()
* operations may sleep
*/
struct pwm_chip {
struct device *dev;
struct list_head list;
const struct pwm_ops *ops;
int base;
unsigned int npwm;
struct pwm_device *pwms;
struct pwm_device * (*of_xlate)(struct pwm_chip *pc,
const struct of_phandle_args *args);
unsigned int of_pwm_n_cells;
bool can_sleep;
};
static struct pwm_ops qpnp_pwm_ops = {
.enable = qpnp_pwm_enable,
.disable = qpnp_pwm_disable,
.config = qpnp_pwm_config,
.free = qpnp_pwm_free,
.owner = THIS_MODULE,
};
static int qpnp_pwm_probe(struct spmi_device *spmi)
{
struct qpnp_pwm_chip *pwm_chip;
int rc;
pwm_chip = kzalloc(sizeof(*pwm_chip), GFP_KERNEL);
if (pwm_chip == NULL) {
pr_err("kzalloc() failed.\n");
return -ENOMEM;
}
spin_lock_init(&pwm_chip->lpg_lock);
pwm_chip->spmi_dev = spmi;
dev_set_drvdata(&spmi->dev, pwm_chip);
rc = qpnp_parse_dt_config(spmi, pwm_chip);
if (rc) {
pr_err("Failed parsing DT parameters, rc=%d\n", rc);
goto failed_config;
}
pwm_chip->chip.dev = &spmi->dev;
pwm_chip->chip.ops = &qpnp_pwm_ops;
pwm_chip->chip.base = -1;
pwm_chip->chip.npwm = 1;
rc = pwmchip_add(&pwm_chip->chip);
if (rc < 0) {
pr_err("pwmchip_add() failed: %d\n", rc);
goto failed_insert;
}
...
return rc;
}
probe函数中分析dts参数,创建一个新的qpnp pwm chip设备,填充成员变量及ops,并通过函数pwmchip_add()向系统注册PWM资源(pwm chip),共consumer driver申请使用。
PWM subsystem
完成HW driver注册后,系统有了可申请的PWM设备(pwm chip),现在看PWM子系统,这部分源码隶属于Linux,/linux-4.4/drivers/pwm/core.c
/**
* pwm_request() - request a PWM device
* @pwm: global PWM device index
* @label: PWM device label
*
* This function is deprecated, use pwm_get() instead.
*
* Returns: A pointer to a PWM device or an ERR_PTR()-encoded error code on
* failure.
*/
struct pwm_device *pwm_request(int pwm, const char *label)
{
struct pwm_device *dev;
int err;
if (pwm < 0 || pwm >= MAX_PWMS)
return ERR_PTR(-EINVAL);
mutex_lock(&pwm_lock);
dev = pwm_to_device(pwm);
if (!dev) {
dev = ERR_PTR(-EPROBE_DEFER);
goto out;
}
err = pwm_device_request(dev, label);
if (err < 0)
dev = ERR_PTR(err);
out:
mutex_unlock(&pwm_lock);
return dev;
}
EXPORT_SYMBOL_GPL(pwm_request);
此处以pwm_request为例,函数已导出,供consumer driver使用,通过该接口可获取pwm设备;
此外还有enable/disable/of_pwm_get/pwm_config等接口,用来操作pwm设备,内部基本使用了HW driver中提供的ops,也有调用HW driver中的全局函数。
PWM consumer driver
consumer driver作为PWM的使用者,以led driver为例,一方面负责调用接口调试PWM设备,另一方面通过sysfs提供接口,供应用程序使用,显而易见在这个过程中led driver起到了承上启下的作用;
源码中随便找了一个注册led的实例,/linux-4.4/drivers/leds/leds-lm3530.c
static int lm3530_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
...
drvdata->led_dev.name = LM3530_LED_DEV;
drvdata->led_dev.brightness_set = lm3530_brightness_set;
drvdata->led_dev.max_brightness = MAX_BRIGHTNESS;
drvdata->led_dev.groups = lm3530_groups;
...
err = led_classdev_register(&client->dev, &drvdata->led_dev);
if (err < 0) {
dev_err(&client->dev, "Register led class failed: %d\n", err);
return err;
}
return 0;
}
led_dev原型为,struct led_classdev,源码路径/linux-4.4/include/linux/leds.h;
struct led_classdev的成员里面我们比较关心brightness_set()/max_brightness/name;
- brightness_set()用来调整亮度,此处我们填充PWM subsystem提供的接口pwm_config(),通过配置PWM来起到调整亮度的作用;
- max_brightness最大亮度值,一般255;
- name是注册的led设备名,这个名字最终会体现在/sys/class/leds/下;
将led_dev成员变量及方法填充完毕之后,通过led_classdev_register()向系统注册led设备,完成注册即可通过终端查看/sys/class/leds/下是否有你刚注册的设备。
Application
应用程序通过sysfs中led class提供的属性文件brightness/max_brightness,对其进行读写来调整亮度值,也有追加brightness_state的,用来控制背光使能,这个需要自行封装,一般都是通过背光使能端子来控制其使能;
读写brightness节点,进入内核后led调用brightness_set()->pwm_config()->HW driver,完成对PWM控制器的控制,进而达到调整亮度的目的。
端子配置
到目前位置,我们仅仅完成了对PWM控制器的配置,并向应用层提供接口;想要完成PWM输出,还有一个重要的部分就是端子配置,否则PWM控制器配置的再漂亮,没有输出端口也是没有意义的;
由于我们平台端子配置都是现成的,手里也没有高通PMIC原厂手册,所以此处只做定性分析;
端子配置的目的是将配置好的PWM通过某一端子输出,端子配置项包括function/strength/pull/mode等等,Documentation中有包括这部分dts的配置方法及实例,以下配置仅供参考。
qpnp: qcom,spmi@fc4c0000 {
#address-cells = <1>;
#size-cells = <0>;
interrupt-controller;
#interrupt-cells = <3>;
qcom,pm8941@0 {
spmi-slave-container;
reg = <0x0>;
#address-cells = <1>;
#size-cells = <1>;
pm8941_gpios: gpios {
spmi-dev-container;
compatible = "qcom,qpnp-pin";
gpio-controller;
#gpio-cells = <2>;
#address-cells = <1>;
#size-cells = <1>;
gpio@c000 {
reg = <0xc000 0x100>;
qcom,pin-num = <62>;
};
gpio@c100 {
reg = <0xc100 0x100>;
qcom,pin-num = <20>;
qcom,source_sel = <2>;
qcom,pull = <5>;
};
};
qcom,testgpio@1000 {
compatible = "qcom,qpnp-testgpio";
reg = <0x1000 0x1000>;
gpios = <&pm8941_gpios 62 0x0 &pm8941_gpios 20 0x1>;
};
};
};