Linux驱动开发(十七)---树莓派PWM驱动

前文回顾

《Linux驱动开发(一)—环境搭建与hello world》
《Linux驱动开发(二)—驱动与设备的分离设计》
《Linux驱动开发(三)—设备树》
《Linux驱动开发(四)—树莓派内核编译》
《Linux驱动开发(五)—树莓派设备树配合驱动开发》
《Linux驱动开发(六)—树莓派配合硬件进行字符驱动开发》
《Linux驱动开发(七)—树莓派按键驱动开发》
《Linux驱动开发(八)—树莓派SR04驱动开发》
《Linux驱动开发(九)—树莓派I2C设备驱动开发(BME280)》
《Linux驱动开发(十)—树莓派输入子系统学习(红外接收)》
《Linux驱动开发(十一)—树莓派SPI驱动学习(OLED)》
《Linux驱动开发(十二)—树莓派framebuffer学习(改造OLED)》
《Linux驱动开发(十三)—USB驱动HID开发学习(鼠标)》
《Linux驱动开发(十四)—USB驱动开发学习(键盘+鼠标)》
《Linux驱动开发(十五)—如何使用内核现有驱动(显示屏)》
《Linux驱动开发(十六)—块设备驱动》
今天来学习一下PWM,这个东西在控制电动机方面还是挺有作用,万一以后用到的上呢。
Linux驱动开发(十七)---树莓派PWM驱动_第1张图片

PWM

脉宽调制(PWM,Pulse Width Modulation)是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术。
PWM值是在一个周期内,开关管导通时间长短相加的平均值。导通时间越长,则直流输出的平均值越大;
PWM占空比是一个周期内,导通时间与周期时间的一个比值。
Linux驱动开发(十七)---树莓派PWM驱动_第2张图片
根据面积等效法则,可以通过对改变脉冲的时间宽度,来等效的获得所需要合成的相应幅值和频率的波形;
例如模拟正弦波,就是不停改变占空比,让每一时刻的等效电压按照正弦波的波形变化。
Linux驱动开发(十七)---树莓派PWM驱动_第3张图片
Linux驱动开发(十七)---树莓派PWM驱动_第4张图片

  • pwm的频率:
    是指1秒钟内信号从高电平到低电平再回到高电平的次数(一个周期);
    也就是说一秒钟PWM有多少个周期
    单位: Hz
    表示方式: 50Hz 100Hz

  • pwm的周期:
    T=1/f
    周期=1/频率
    50Hz = 20ms 一个周期
    如果频率为50Hz ,也就是说一个周期是20ms 那么一秒钟就有 50次PWM周期

  • 占空比:
    是一个脉冲周期内,高电平的时间与整个周期时间的比例
    单位: % (0%-100%)
    表示方式:20%

摘自《PWM原理 PWM频率与占空比详解》。
里面还介绍了如何控制LED产生呼吸效果和舵机的转动角度。后续我们要深入学习。
Linux驱动开发(十七)---树莓派PWM驱动_第5张图片

设备树修改

树莓派是支持PWM的,看了一下补充文档中的内容,给了例子。

BCM2835 PWM controller (Raspberry Pi controller)

Required properties:
- compatible: should be "brcm,bcm2835-pwm"
- reg: physical base address and length of the controller's registers
- clocks: This clock defines the base clock frequency of the PWM hardware
  system, the period and the duty_cycle of the PWM signal is a multiple of
  the base period.
- #pwm-cells: Should be 3. See pwm.yaml in this directory for a description of
  the cells format.

Examples:

pwm@2020c000 {
	compatible = "brcm,bcm2835-pwm";
	reg = <0x2020c000 0x28>;
	clocks = <&clk_pwm>;
	#pwm-cells = <3>;
};

clocks {
	....
		clk_pwm: pwm {
			compatible = "fixed-clock";
			reg = <3>;
			#clock-cells = <0>;
			clock-frequency = <9200000>;
		};
	....
};

其实这个配置,树莓派已经给配置好了,只需要打开就可以。
Linux驱动开发(十七)---树莓派PWM驱动_第6张图片

并且开发板上已经带了驱动了

root@raspberrypi:/sys/class/pwm# ls /lib/modules/5.15.55-v7+/kernel/drivers/pwm/
pwm-bcm2835.ko  pwm-pca9685.ko  pwm-raspberrypi-poe.ko

修改一下设备树,看看能否加载驱动。
这个设备树如何修改,其实很简单,只需要加上下面

&pwm {
	status = "okay";
};

至于这个调用关系,我也是找了很久,根据bcm2837的数据手册,找到这个地址
在这里插入图片描述
然后我们看一下bcm283x.dtsi被谁包含
在这里插入图片描述
再看bcm2837.dtsi被谁包含
在这里插入图片描述
正好这个bcm2710.dtsi就在我们使用的设备树中包含。
所以直接打开pwm就可以了。
Linux驱动开发(十七)---树莓派PWM驱动_第7张图片

其实在原始定义部分是这样的
Linux驱动开发(十七)---树莓派PWM驱动_第8张图片
然后更新dts,重启设备

root@raspberrypi:/home/pgg# cd /sys/class/pwm/
root@raspberrypi:/sys/class/pwm# ls
pwmchip0

Linux驱动开发(十七)---树莓派PWM驱动_第9张图片

PWM配置

操作方法,参考《linux操作PWM命令》
生成目录

root@raspberrypi:/sys/class/pwm/pwmchip0# echo 1 > /sys/class/pwm/pwmchip0/export

会生成
下面有几个文件可以配置

  • enable :写入1使能pwm,写入0关闭pwm
  • polarity :有normal和inversed两个参数选择,表示输出引脚电平翻转。
  • duty_cycle :单位纳秒,
    在normal模式下,表示高电平持续的时间
    在inversed模式下,表示低电平持续时间。
  • period :单位纳秒,表示pwm波持续周期

使用例子:配置一个50%占空比pwm。

设置PWM一个周期的时间,单位为ns

root@raspberrypi:/sys/class/pwm/pwmchip0# echo 1000000 > /sys/class/pwm/pwmchip0/pwm1/period

设置PWM一个周期中“ON”的时间,单位为ns,占空比为50%

root@raspberrypi:/sys/class/pwm/pwmchip0# echo 500000 > /sys/class/pwm/pwmchip0/pwm1/duty_cycle

使能 PWM

root@raspberrypi:/sys/class/pwm/pwmchip0# echo 1 > /sys/class/pwm/pwmchip0/pwm1/enable

关闭 PWM

root@raspberrypi:/sys/class/pwm/pwmchip0# echo 0 > /sys/class/pwm/pwmchip0/pwm1/enable 

Linux驱动开发(十七)---树莓派PWM驱动_第10张图片

重点!重点!

驱动中的驱动

之前就一直在思考,如果两个驱动怎么传递数据,难道还需要经过用户空间吗?那感觉好傻啊。事实证明,我们可以在驱动中调用其他的驱动,简单的例子就是,用PWM控制led灯。
部分内容参考自《Linux驱动 | LED驱动(使用PWM子系统)》

Linux驱动开发(十七)---树莓派PWM驱动_第11张图片

led是我要驱动的设备,这个设备中用到了pwm,而pwm是用默认的驱动。这里需要先修改设备树,如何定义自己的LED设备,包含pwm设备。
阅读Documentation/devicetree/bindings/pwm/pwm.txt
Linux驱动开发(十七)---树莓派PWM驱动_第12张图片
我们就可以修改一个我们的PWM驱动的LED设备
Linux驱动开发(十七)---树莓派PWM驱动_第13张图片
我们自己写一个驱动。内部在使用PWM子系统。形成了包含驱动的驱动。
Linux驱动开发(十七)---树莓派PWM驱动_第14张图片

驱动代码

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
//#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define LED_PWM_CMD_SET_DUTY         0x01
#define LED_PWM_CMD_SET_PERIOD       0x02
#define LED_PWM_CMD_SET_BOTH         0x03
#define LED_PWM_CMD_ENABLE           0x04
#define LED_PWM_CMD_DISABLE          0x05

struct my_pwm_param_struct 
{
    int duty_ns;
    int period_ns;
};

struct my_pwm_led_dev_struct 
{
    dev_t dev_no;                    
    struct cdev chrdev;            
    struct class *led_class;
    struct device_node *dev_node;
    struct pwm_device *led_pwm;
};

static struct my_pwm_param_struct 	 my_pwm_para;
static struct my_pwm_led_dev_struct   my_pwm_led_dev;

static int red_led_drv_open (struct inode *node, struct file *file)
{
    int ret = 0;
    
	//pwm_set_polarity(my_pwm_led_dev.led_pwm, PWM_POLARITY_INVERSED);
	pwm_enable(my_pwm_led_dev.led_pwm);

    printk("led_pwm open\r\n");
    return ret;
}

static ssize_t red_led_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
    int err;

    if (size != sizeof(my_pwm_para)) return -EINVAL;

	err = copy_from_user(&my_pwm_para, buf, size);
    if (err > 0) return -EFAULT;

	pwm_config(my_pwm_led_dev.led_pwm, my_pwm_para.duty_ns, my_pwm_para.period_ns);

	return 1;
}

static long _drv_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    int ret = 0;
    void __user *my_user_space = (void __user *)arg;
    
    switch (cmd)
    {
        case LED_PWM_CMD_SET_DUTY:
            ret = copy_from_user(&my_pwm_para.duty_ns, my_user_space, sizeof(my_pwm_para.duty_ns));
            if (ret > 0) 
				return -EFAULT;
            pwm_config(my_pwm_led_dev.led_pwm, my_pwm_para.duty_ns, my_pwm_para.period_ns);
            break;
        case LED_PWM_CMD_SET_PERIOD:
            ret = copy_from_user(&my_pwm_para.period_ns, my_user_space, sizeof(my_pwm_para.period_ns));
            if (ret > 0) 
				return -EFAULT;
            pwm_config(my_pwm_led_dev.led_pwm, my_pwm_para.duty_ns, my_pwm_para.period_ns);
            break;
        case LED_PWM_CMD_SET_BOTH: 
            ret = copy_from_user(&my_pwm_para, my_user_space, sizeof(my_pwm_para));
            if (ret > 0) 
				return -EFAULT;
            pwm_config(my_pwm_led_dev.led_pwm, my_pwm_para.duty_ns, my_pwm_para.period_ns);
            break;
        case LED_PWM_CMD_ENABLE:
            pwm_enable(my_pwm_led_dev.led_pwm);
            break;
        case LED_PWM_CMD_DISABLE:
            pwm_disable(my_pwm_led_dev.led_pwm);
            break;
    }
	return 0;
}

static int red_led_drv_release(struct inode *node, struct file *filp)
{
    int ret = 0;

    pwm_config(my_pwm_led_dev.led_pwm, 0, 5000);
    printk("led pwm dev close\r\n");
//    pwm_disable(my_pwm_led_dev.led_pwm);
    return ret;
}

static struct file_operations red_led_drv = {
	.owner	 = THIS_MODULE,
	.open    = red_led_drv_open,
	.write   = red_led_drv_write,
    .unlocked_ioctl = _drv_ioctl,
    .release  = red_led_drv_release,
};



static int led_red_driver_probe(struct platform_device *pdev)
{
    int err;
    int ret;
    struct device *tdev;
    struct device_node *child;

	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	
    tdev = &pdev->dev;
    child = of_get_next_child(tdev->of_node, NULL);      /* 获取设备树子节点 */
	if (!child) 
	{
        return -EINVAL;
    }

    my_pwm_led_dev.led_pwm = devm_of_pwm_get(tdev, child, NULL);     /* 从子节点中获取PWM设备 */
    if (IS_ERR(my_pwm_led_dev.led_pwm)) 
	{
        printk(KERN_ERR"can't get led_pwm!!\n");
        return -EFAULT;
    }

    ret = alloc_chrdev_region(&my_pwm_led_dev.dev_no, 0, 1, "led_pwm");
	if (ret < 0) 
	{
		pr_err("Error: failed to register mbochs_dev, err: %d\n", ret);
		return ret;
	}

	cdev_init(&my_pwm_led_dev.chrdev, &red_led_drv);
	cdev_add(&my_pwm_led_dev.chrdev, my_pwm_led_dev.dev_no, 1);

    my_pwm_led_dev.led_class = class_create(THIS_MODULE, "led_pwm");
	
	err = PTR_ERR(my_pwm_led_dev.led_class);
	if (IS_ERR(my_pwm_led_dev.led_class)) 
	{
        goto failed1;
	}

    tdev = device_create(my_pwm_led_dev.led_class , NULL, my_pwm_led_dev.dev_no, NULL, "my_pwm_led"); 
    if (IS_ERR(tdev))
	{
        ret = -EINVAL;
		goto failed2;
	}

   	printk(KERN_INFO"%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    
    return 0;
failed2:
    device_destroy(my_pwm_led_dev.led_class, my_pwm_led_dev.dev_no);
    class_destroy(my_pwm_led_dev.led_class);
failed1:
    cdev_del(&my_pwm_led_dev.chrdev);
	unregister_chrdev_region(my_pwm_led_dev.dev_no, 1);
    return ret;
}

int led_red_driver_remove(struct platform_device *dev)
{
    // pwm_disable(my_pwm_led_dev.led_pwm);
    // pwm_free(my_pwm_led_dev.led_pwm);
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	
    device_destroy(my_pwm_led_dev.led_class, my_pwm_led_dev.dev_no);
	class_destroy(my_pwm_led_dev.led_class);
	unregister_chrdev_region(my_pwm_led_dev.dev_no, 1);
    cdev_del(&my_pwm_led_dev.chrdev);
     
    return 0;
}

static struct of_device_id dts_match_table[] = {
    {.compatible = "pgg,my_pwm_led", },  
    {},                  
};

static struct platform_driver my_pwm_led_platform_driver = 
{
      .probe = led_red_driver_probe,
      .remove = led_red_driver_remove,
      .driver = 
      {
        .name = "pgg,my_pwm_led",
        .owner = THIS_MODULE,
        .of_match_table = dts_match_table,//通过设备树匹配
      },
};

module_platform_driver(my_pwm_led_platform_driver);

MODULE_AUTHOR("PGG");
MODULE_LICENSE("GPL");

这里要注意的就是,我们不需要注册pwm设备,而是从设备树获取这个设备就可以

my_pwm_led_dev.led_pwm = devm_of_pwm_get(tdev, child, NULL);     /* 从子节点中获取PWM设备 */

用户侧程序

#include "stdio.h"
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define DEV_NAME   "/dev/my_pwm_led"

#define LED_PWM_CMD_SET_DUTY         0x01
#define LED_PWM_CMD_SET_PERIOD       0x02
#define LED_PWM_CMD_SET_BOTH         0x03
#define LED_PWM_CMD_ENABLE           0x04
#define LED_PWM_CMD_DISABLE          0x05

struct led_pwm_param {
    int duty_ns;
    int period_ns;
};

void sleep_ms(unsigned int ms)
{
    struct timeval delay;
	delay.tv_sec = 0;
	delay.tv_usec = ms * 1000; 
	select(0, NULL, NULL, NULL, &delay);
}

int main(int argc, char **argv)
{
    int fd;
    int ret;
  
	/* 2. 打开文件 */
	fd = open(DEV_NAME, O_RDWR | O_NONBLOCK);   // | O_NONBLOCK

	if (fd < 0)
	{
		printf("can not open file %s, %d\n", DEV_NAME, fd);
		return -1;
	}
     
    int buf = 3;
	struct led_pwm_param led_pwm;
	
	led_pwm.duty_ns = 500;
	led_pwm.period_ns = 5000;
    write(fd, &led_pwm, sizeof(led_pwm));
    sleep_ms(3000);

	for (int i = 0; i < 5; i++)
	{
		led_pwm.duty_ns += 300;
		if (led_pwm.duty_ns < led_pwm.period_ns)
			ioctl(fd, LED_PWM_CMD_SET_DUTY, &led_pwm.duty_ns);
		sleep_ms(1000);
	}

	// led_pwm.duty_ns = 0;
	// ioctl(fd, LED_PWM_CMD_SET_DUTY, &led_pwm.duty_ns);

	sleep_ms(3000);

	close(fd);
    
    return 0;
}

别以为到这里就结束了,其实上面的代码都有问题。
Linux驱动开发(十七)---树莓派PWM驱动_第15张图片

调试过程

第一步调用shell的方式,就发现和PWM相关的那些引脚,都没有反应。翻看了一遍设备树,也没有找到怎么配置PWM输出到哪个引脚上,这感觉像是缺少点配置。于是经过搜索,发现了一个类似的博客
《LINUX驱动开发11:【设备树】NANOPI的PWM驱动》,在配置设备树的时候,似乎要配置使用的引脚。
Linux驱动开发(十七)---树莓派PWM驱动_第16张图片
所以我这里也需要增加一个引脚,但这个引脚怎么写呢
于是我反编译了生成的dtb,查看到了树莓派中pwm引脚的定义
Linux驱动开发(十七)---树莓派PWM驱动_第17张图片
那么灵感来了,设备树修改成这样,感觉八九不离十了
Linux驱动开发(十七)---树莓派PWM驱动_第18张图片
继续上机调试,shell命令行的方式,成功了!!
Linux驱动开发(十七)---树莓派PWM驱动_第19张图片

然后就是调试led驱动,结果发现灯一直是灭的。突然又来灵感了,配置pwm,必须要先关停pwm,所以,修改了驱动中的代码,在配置前后都加上了禁用和启用
在这里插入图片描述

然后用户侧的程序,进行了优化,连续让灯变亮变暗

	while(1)
	{
		if(led_pwm.duty_ns<=500)
		{
			while(led_pwm.duty_ns<led_pwm.period_ns)
			{
				ioctl(fd, LED_PWM_CMD_SET_DUTY, &led_pwm.duty_ns);
				sleep_ms(50);
				led_pwm.duty_ns += 300;
			}
		}
		else
		{
			while(led_pwm.duty_ns > 500)
			{
				ioctl(fd, LED_PWM_CMD_SET_DUTY, &led_pwm.duty_ns);
				sleep_ms(50);
				led_pwm.duty_ns -= 300;
			}
		}
		
	}

测试起来!
Linux驱动开发(十七)---树莓派PWM驱动_第20张图片

小技巧

在搜索dts文件的时候,常用下面命令,用来找到包含关键内容的文件,及在文件第几行。

grep "内容" * -nr

前面搜索pwm配置的时候,就用到了这种命令。

Linux驱动开发(十七)---树莓派PWM驱动_第21张图片

结束语

昨天看到夫人给发的孩子照片,突然感觉像一个大孩子了。结果下午出去玩,上厕所出来走丢了,也知道告诉热心人父母的电话,帮忙联系父母。
我很欣慰啊,就算被人拐卖走了,应该也能找回来了。
Linux驱动开发(十七)---树莓派PWM驱动_第22张图片
夫人说得克制一下脾气了,孩子前天晚上说:我只有一个烦恼,就是妈妈爱发火。害,人哪能没有烦恼呢,没有烦恼,那不就光剩快乐了吗,这不符合我们社会主义初级阶段的国情。
Linux驱动开发(十七)---树莓派PWM驱动_第23张图片
今天上午公司有试用期员工转正大会,赶紧收工,一会去看热闹。
Linux驱动开发(十七)---树莓派PWM驱动_第24张图片

你可能感兴趣的:(驱动开发,操作系统,linux知识,驱动开发,linux,树莓派,PWM)