嵌入式Linux驱动笔记(八)------依赖Linux kernel驱动的pwm编写

你好!这里是风筝的博客,

欢迎和我一起交流。


之前我们写Linux驱动,都是自己写,从platform driver到platform device,都是自己一手包办,其实,在kernel里,很多驱动都已经写好了的,只需我们会用就好了,省时省力。

现在以pwm驱动为例,芯片是2440的芯片:
查阅芯片手册我们可以知道,S3C2440上有4 通道 16 位具有 PWM 功能的定时器,1 通道 16 位基于 DMA或基于中断运行的内部定时器,其中,pwm定时器0的输出引脚在GPIOB0,pwm定时器挂载PCLK时钟上。

好了,现在我们看下kernel源码:
以kernel4.8.17为例,在pwm-samusng.c(drivers/pwm目录)文件里,有:

static struct platform_driver pwm_samsung_driver = {
	.driver		= {
		.name	= "samsung-pwm",
		.pm	= &pwm_samsung_pm_ops,
		.of_match_table = of_match_ptr(samsung_pwm_matches),
	},
	.probe		= pwm_samsung_probe,
	.remove		= pwm_samsung_remove,
};
module_platform_driver(pwm_samsung_driver);

我们可以非常简单的看出,这是一个platform driver,

.name = “samsung-pwm”,

这就是platform用来匹配的关键,记住他。
ok,现在知道了driver,现在我们来写device文件:
其实kernel里也有关于pwm的demo的,在kernel里搜索:“samsung-pwm”,就会在devs.c(arch/arm/plat-samsung目录)文件中找到蛛丝马迹:

static struct resource samsung_pwm_resource[] = {
	DEFINE_RES_MEM(SAMSUNG_PA_TIMER, SZ_4K),
};

struct platform_device samsung_device_pwm = {
	.name		= "samsung-pwm",
	.id		= -1,
	.num_resources	= ARRAY_SIZE(samsung_pwm_resource),
	.resource	= samsung_pwm_resource,
};

好了,我们直接照抄just ok!
新建一个pwm_dev.c文件:

#include 
#include 

#include 

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include 
#include 
#include 

static struct resource s3c_pwm_resource[] = {
	DEFINE_RES_MEM(SAMSUNG_PA_TIMER, SZ_4K),
};

static struct platform_device s3c_device_pwm = {/*platform里注册设备文件*/
	.name			= "samsung-pwm",
	.id				= -1,
	.num_resources	= ARRAY_SIZE(s3c_pwm_resource),
	.resource	= s3c_pwm_resource,
};

static int S3C_PWM_init(void)
{
	/* 2. 注册 */
	int result = platform_device_register(&s3c_device_pwm);
	if (result != 0) /*注册失败*/ 
		printk("register false \n");
	return 0;
}

static void S3C_PWM_exit(void)
{
	platform_device_unregister(&s3c_device_pwm);
}

module_init(S3C_PWM_init);
module_exit(S3C_PWM_exit);

MODULE_LICENSE("GPL");

写好后就行了吗?如果你编译好拿去测试,sorry,不行,insmod时会发现:no platform data specified.
这是为什么呢?
还记得driver文件吗?在他的probe函数里(名字匹配成功就会调用probe函数),pwm_samsung_probe函数,有:

if (IS_ENABLED(CONFIG_OF) && pdev->dev.of_node) {
		ret = pwm_samsung_parse_dt(chip);
		if (ret)
			return ret;

		chip->chip.of_xlate = of_pwm_xlate_with_flags;
		chip->chip.of_pwm_n_cells = 3;
	} else {
		if (!pdev->dev.platform_data) {
			dev_err(&pdev->dev, "no platform data specified\n");
			return -EINVAL;
		}

这里,因为我们没有使用dts设备树,所以会执行else分支。
里面

if (!pdev->dev.platform_data)

因为我们pwm_dev.c这个device文件里,没有为.platform_data赋值,所以到这里就会error了!
那问题来了,.platform_data到底改写什么呢???
其实,kernel就是最好的demo,搜索一下就会看到了:
在pwm-samsung.h里有个结构体:

struct samsung_pwm_variant {
	u8 bits;
	u8 div_base;
	u8 tclk_mask;
	u8 output_mask;
	bool has_tint_cstat;
};

再搜索下哪里使用有samsung_pwm_variant结构体就可以参考下了,结果发现:

static const struct samsung_pwm_variant s3c24xx_variant = {
	.bits		= 16,
	.div_base	= 1,
	.has_tint_cstat	= false,
	.tclk_mask	= (1 << 4),
};

这四个结构体成员,我也不知道具体的是什么,bits应该哦是16位定时器的意思,div_base应该是分频吧,其他两个我就不知道是什么了,看了下pwm-samsung.txt这个文档也没见说有…
这四个结构体成员就够了吗?
还不够!!!
还要再加一个:.output_mask = 1,
至于为什么?待会说。
所以,pwm_dev.c修改为:

static struct samsung_pwm_variant s3c_pwm_pdata = {
	.bits		= 16,
	.div_base	= 1,
	.has_tint_cstat	= false,
	.tclk_mask	= (1 << 4),
	.output_mask	= 1,
};

static struct platform_device s3c_device_pwm = {/*platform里注册设备文件*/
	.name			= "samsung-pwm",
	.id				= -1,
	.num_resources	= ARRAY_SIZE(s3c_pwm_resource),
	.resource	= s3c_pwm_resource,
	.dev				= {
		.platform_data	= &s3c_pwm_pdata,
	},
};

这样,编译成功后,insmod后,会在/sys/class/pwm/目录下产生pwmchip0目录
执行命令:
cd /sys/class/pwm/pwmchip0/
会发现里面这七个文件:
device export npwm power subsystem uevent unexport
执行命令:
echo 0 > export
就是向export文件写入0,就是打开pwm定时器0,会产生一个pwn0目录。
这是为什么呢?
我们看下driver文件里的probe函数,即pwm_samsung_probe函数,有:

static int pwm_samsung_probe(struct platform_device *pdev)
{
	struct device *dev = &pdev->dev;
	struct samsung_pwm_chip *chip;
	struct resource *res;
	unsigned int chan;
	int ret;

	//printk("the probe name is %s \n",pdev->name);//2017.8.6
	chip = devm_kzalloc(&pdev->dev, sizeof(*chip), GFP_KERNEL);
	if (chip == NULL)
		return -ENOMEM;

	chip->chip.dev = &pdev->dev;
	chip->chip.ops = &pwm_samsung_ops;
	chip->chip.base = -1;
	chip->chip.npwm = SAMSUNG_PWM_NUM;
	chip->inverter_mask = BIT(SAMSUNG_PWM_NUM) - 1;
	/*以下代码省略......*/
}

在里面有一句:

chip->chip.ops = &pwm_samsung_ops;

看一下pwm_samsung_ops这个结构体:

static const struct pwm_ops pwm_samsung_ops = {
	.request	= pwm_samsung_request,
	.free		= pwm_samsung_free,
	.enable		= pwm_samsung_enable,
	.disable	= pwm_samsung_disable,
	.config		= pwm_samsung_config,
	.set_polarity	= pwm_samsung_set_polarity,
	.owner		= THIS_MODULE,
};

没错,刚刚我们向export文件写入0时,就会调用这里的pwm_samsung_request函数了:

static int pwm_samsung_request(struct pwm_chip *chip, struct pwm_device *pwm)
{
	struct samsung_pwm_chip *our_chip = to_samsung_pwm_chip(chip);
	struct samsung_pwm_channel *our_chan;

	if (!(our_chip->variant.output_mask & BIT(pwm->hwpwm))) {
		dev_warn(chip->dev,
			"tried to request PWM channel %d without output\n",
			pwm->hwpwm);
		return -EINVAL;
	}

	our_chan = devm_kzalloc(chip->dev, sizeof(*our_chan), GFP_KERNEL);
	if (!our_chan)
		return -ENOMEM;

	pwm_set_chip_data(pwm, our_chan);

	return 0;
}

看到第一个if判断了吗?就是这里,如果之前我们没有设置output_mask这个结构体成员,这里就一定会error掉了!

接着上面的操作,继续执行命令:
cd pwm0
里面有七个文件:
capture enable polarity uevent duty_cycle period power
其中,
enable:写入1使能pwm,写入0关闭pwm;
polarity:有normal或inversed两个参数选择,表示TOUT_n输出引脚电平翻转;
duty_cycle:在normal模式下,表示一个周期内高电平持续的时间(单位:纳秒),在reversed模式下,表示一个周期中低电平持续的时间(单位:纳秒);
period:表示pwm波的周期(单位:纳秒);
往里面写入,就是会调用到pwm_samsung_ops结构体里的成员函数。

所以可以这样使用pwm:

echo 1000000000 > period
echo 500000000 > duty_cycle
echo normal > polarity
echo 1 > enable

这样就是占空比为500000000/1000000000的pwm出现。
但是!!你会发现,GPIOB0引脚没有反应…
这是为什么?这个小问题困扰了我两天,终于发现!!!
原来是GPIOB0没有配置,而且要配置成复用引脚!!!都是泪啊…
添加上配置引脚部分就好了,这样,
往duty_cycle写入不同的值,就会出现不同占空比的pwm了。
最后,往unexport写入0就会关闭pwm定时器了,同时pwm0目录会被删除
即:
cd …
echo 0 > unexport

最后附上完整代码:

#include 
#include 

#include 

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include 
#include 
#include 

static volatile unsigned long *gpbcon;
static volatile unsigned long *gpbdat;

static struct samsung_pwm_variant s3c_pwm_pdata = {
	.bits		= 16,
	.div_base	= 1,
	.has_tint_cstat	= false,
	.tclk_mask	= (1 << 4),
	.output_mask	= 1,
};

static struct resource s3c_pwm_resource[] = {
	DEFINE_RES_MEM(SAMSUNG_PA_TIMER, SZ_4K),
};

static struct platform_device s3c_device_pwm = {/*platform里注册设备文件*/
	.name			= "samsung-pwm",
	.id				= -1,
	.num_resources	= ARRAY_SIZE(s3c_pwm_resource),
	.resource	= s3c_pwm_resource,
	.dev				= {
		.platform_data	= &s3c_pwm_pdata,
	},
};

static int S3C_PWM_init(void)
{
	/* 2. 注册 */
	int result = platform_device_register(&s3c_device_pwm);
	if (result != 0) /*注册失败*/ 
		printk("register false \n");
	gpbcon 	= ioremap(0x56000010, 8);
	gpbdat 	= gpbcon+1;

	*gpbcon &= ~(0x3<<(0*2));
	*gpbcon |= (0x2<<(0*2));					/*复用模式*/
	return 0;
}

static void S3C_PWM_exit(void)
{
	platform_device_unregister(&s3c_device_pwm);
	iounmap(gpbcon);
}

module_init(S3C_PWM_init);
module_exit(S3C_PWM_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("kite");
MODULE_DESCRIPTION("A pwm Module for testing module ");
MODULE_VERSION("V2.0");

后记:
其实我感觉就
platform_device_register(&samsung_device_pwm);
就好了,因为在devs.c这个文件里就把platform_device这个结构体实现了,同时在smdk2440_map_io这个函数里,调用了两个 函数:
s3c24xx_init_io函数
samsung_set_timer_source函数
这两个函数里就会把samsung_device_pwm.dev.platform_data和output_mask成员填充好了。
当然,我没试过这样,有兴趣的小伙伴可以试试。

你可能感兴趣的:(Linux驱动)