一、概述
很多时候由于节省硬件资源,降低成本,会把PWM控制芯片去掉或者是改做它用,导致当我们想用PWM方式控制背光时只能使用普通的GPIO口。本篇文档就来讲解下如何使用GPIO模拟PWM功能进行背光的控制。本文以IMX6为例。
二、确认硬件接口
硬件连接如下:
查看IMX6手册,发现引脚为GPIO3_IO18
三、软件配置
1、添加设备树节点
2、驱动编写
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/platform_device.h>
#include <linux/err.h>
#include <linux/pwm.h>
#include <linux/slab.h>
#include <linux/clk.h>
#include <linux/iio/iio.h>
#include <linux/iio/machine.h>
#include <linux/iio/driver.h>
#include <linux/iio/consumer.h>
#include <linux/delay.h>
#include <linux/time.h>
#include <linux/timer.h>
#include <linux/input.h>
#include <linux/interrupt.h>
#include <linux/of_platform.h>
#include <linux/of_device.h>
#include <linux/types.h>
#include <linux/ioport.h>
#include <asm/irq.h>
#include <linux/of_irq.h>
#include <linux/of_address.h>
#include <linux/reset.h>
struct gpio_blacklight_dev {
int value;
unsigned gpio;
struct gpio_desc * gpiod;
struct mutex mtx;
const char* name;
int light_level;
struct device *dev;
int high_time;
int low_time;
unsigned long flags;
unsigned active_low;
struct timer_list pwm_timer;
void __iomem *reg_base;
};
static int gpio_sim = 0;
static int tcount = 0, flag = 0;
static int high_flag;
static int low_flag;
static int frequency = 1000; // 初始频率,单位:Hz
static int high_cnt = 50;//调节该值就能操作占空比
static int period_cnt = 100;//100个定时周期认为是一个pwm周期,所以定时器的时间应该是1s/frequency/period_cnt
static struct hrtimer pwm_timer1;
static ktime_t pwm_period;
// 背光最大值
static int max_high_cnt = 100; // 定义最大值
static ssize_t max_light_level_show(struct device *dev, struct device_attribute *attr, char *buf)
{
return sprintf(buf, "%d\n", max_high_cnt);
}
static ssize_t max_light_level_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
long new_max_high_cnt;
kstrtoul(buf, 10, &new_max_high_cnt);
if (new_max_high_cnt >= 0) {
max_high_cnt = new_max_high_cnt;
}
return count;
}
static ssize_t light_level_show(struct device *dev, struct device_attribute *attr, char *buf)
{
struct gpio_blacklight_dev *g_dev = (struct gpio_blacklight_dev *)dev_get_drvdata(dev);
return sprintf(buf, "%d\n", high_cnt);
}
static ssize_t light_level_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
long light_level;
struct gpio_blacklight_dev *g_dev = (struct gpio_blacklight_dev *)dev_get_drvdata(dev);
kstrtoul(buf, 10, &light_level);
high_cnt = light_level;
return count;
}
static DEVICE_ATTR(light_level, S_IRUGO | S_IWUSR, light_level_show, light_level_store);
static DEVICE_ATTR(max_light_level, 0664, max_light_level_show, max_light_level_store);
static struct device_attribute light_level = {
.attr = {
.name = "light_level",
.mode = S_IRUGO | S_IWUGO | S_IXUGO ,
},
.show = light_level_show,
.store = light_level_store,
};
static const struct of_device_id of_gpio_blacklights_match[] = {
{ .compatible = "gpio-backlight" },
{},
};
MODULE_DEVICE_TABLE(of, of_gpio_blacklights_match);
/*************************************************************************/
static enum hrtimer_restart pwm_timer_callback(struct hrtimer *timer) {
static int timer_cnt = 0;
if(++timer_cnt == high_cnt){
gpio_set_value(gpio_sim, 0);
}
if(timer_cnt == period_cnt){
gpio_set_value(gpio_sim, 1);
timer_cnt = 0;
}
hrtimer_forward(timer, timer->_softexpires, pwm_period);
printk("pwm_timer_callback\n");
return HRTIMER_RESTART;
}
static int io_pwm_init(void) {
// 计算 PWM 周期为pwm_period*period_cnt=pwm_period*100=1*10^(-6)
pwm_period = ktime_set(0, 1000000000 / frequency/ period_cnt);
// 初始化定时器
hrtimer_init(&pwm_timer1, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
pwm_timer1.function = pwm_timer_callback;//回调函数操作GPIO
// 启动高精度定时器
hrtimer_start(&pwm_timer1, pwm_period, HRTIMER_MODE_REL);
printk(KERN_INFO "PWM Timer Module Loaded\n");
printk("io_pwm_init\n");
return 0;
}
/*************************************************************************/
static int gpio_blacklight_probe(struct platform_device *pdev)
{
int count = 0, ret = 0;
struct gpio_blacklight_platform_data *pdata = dev_get_platdata(&pdev->dev);
struct device *dev = &pdev->dev;
struct gpio_blacklight_dev *gblacklight_dev;
printk("enter gpio_blacklight_probe init\n");
gblacklight_dev = devm_kzalloc(dev, sizeof(struct gpio_blacklight_dev), GFP_KERNEL);
memset(gblacklight_dev, 0, sizeof(*gblacklight_dev));
if (!gblacklight_dev)
{
printk("devm_kzalloc gblacklight_dev fail \n");
return -1;
}
gpio_sim = of_get_named_gpio_flags(pdev->dev.of_node, "gpio-sim", 0, NULL);
if (gpio_sim < 0)
{
printk("%s() Can not read property gpio_sim\n", __FUNCTION__);
gpio_sim = 0;
}
ret = gpio_request(gpio_sim, "gpio-sim");
if (ret < 0)
{
printk("gpio request gpio_sim error!\n");
}
else
{
printk("==============================gpio-sim\n");
gpio_direction_output(gpio_sim, 1);
}
//放在这里初始化
io_pwm_init();
ret = device_create_file(&pdev->dev, &dev_attr_light_level);
if (ret) {
dev_err(&pdev->dev, "Failed to create sysfs entry\n");
return ret;
}
device_create_file(&pdev->dev, &dev_attr_max_light_level);
printk("gpio_pwm probe ok\n");
err:
return ERR_PTR(ret);
}
static int gpio_blacklight_remove(struct platform_device *pdev)
{
struct gpio_blacklight_dev *gblacklight_dev = (struct gpio_blacklight_dev *)dev_get_drvdata(&pdev->dev);
del_timer(&gblacklight_dev->pwm_timer);
device_remove_file(gblacklight_dev->dev, &light_level);
dev_set_drvdata(gblacklight_dev->dev, NULL);
hrtimer_cancel(&pwm_timer1);
return 0;
}
static struct platform_driver gpio_blacklight_driver = {
.probe = gpio_blacklight_probe,
.remove = gpio_blacklight_remove,
.driver = {
.name = "blacklight-gpio",
.of_match_table = of_match_ptr(of_gpio_blacklights_match),
.owner = THIS_MODULE,
},
};
module_platform_driver(gpio_blacklight_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("xth");
四、测试
1、sys节点
路径:/sys/devices/soc0/blacklight/light_level
问题1:背光无变化
之前调试AIM915X/916X时,将背光脚设置为了916端本地输出,修改为GPIO透传模式,如果使用的是直连的话是没有问题的
PWM GPIO2
915端为输入 i2cset -f -y 2 0x0c 0x0b 0x30
916端远程输出 i2cset -f -y 2 0x2c 0x1d 0x50
修改aim915916驱动如下
再次测试
查看寄存器是否成功写入
调节背光有明显变化,成功:
echo 10 > /sys/devices/soc0/blacklight/light_level
echo 80 > /sys/devices/soc0/blacklight/light_level
问题2:背光上电过快导致屏幕显示出撕扯的画面,或者上电一瞬间闪屏的状态
如何解决开发板背光上电过快导致的闪屏和画面撕裂?
发送每个命令之间添加一个延迟,达到延迟启动背光的效果。
addr = 0x0B;
value = 0x30;
if((ret = i2c_smbus_write_byte_data(aim955_client, addr, value)) < 0){
printk("I2C_AIM955 error: 0x%x 0x%x\n", addr, value);
return ret;
}
msleep(100); // 100毫秒延迟
addr = 0x1D;
value = 0x50;
if((ret = i2c_smbus_write_byte_data(aim956_client, addr, value)) < 0){
printk("I2C_AIM955 error: 0x%x 0x%x\n", addr, value);
return ret;
}
测试无效果
查看日志log,发现PWM驱动是在屏之前加载的
PWM驱动在屏驱动加载之后?
将PWM驱动编译为模块在aim915915加载后再进行加载
make modules -j44
在启动脚本中加载PWM模块
闪屏没了,但会存在一个问题,上电到点亮背光要很长的时间
尝试修改内核中驱动的加载顺序
修改如下:
GPIOPWM驱动是在AIM915916后加载,但是还在存在闪屏
重点看下GPIOPWM驱动的问题,尝试修改占空比以及频率
修改了占空比之后问题解决,驱动在内核中进行加载,优化了系统启动时间
调试到此完成,后续会结合光感模块,实现背光自动调节功能,如感兴趣,可持续关注后续文章内容。