Linux驱动第十一章、PWM子系统

第一节、PWM简述:

1.PWM与占空比:

PWM(脉冲宽度调制)是一种用于产生模拟信号的技术,通常用于控制电子设备中的电机、灯光、声音等。PWM通过调整脉冲的宽度,以实现对输出信号的调节。

PWM是一种通过高低电平产生的梯度高低的方波信号。

频率:(Hz)单位,即1秒内产生的方波的个数。

周期:(s)秒,即1Hz所占用的时间。

占空间比:即高电压在整个周期的比值

基本上,PWM是通过在一个周期内改变信号的高电平时间(占空比)来控制输出的。占空比是高电平时间与一个完整周期的比例。通过调整占空比,可以改变输出信号的平均电压,从而实现对设备的功率的控制。

在嵌入式系统和电子控制领域,PWM广泛应用于各种场景,包括但不限于:

电机控制: 通过PWM控制电机的速度和方向。

LED亮度控制: 调整LED的亮度。

音频控制: 生成模拟音频信号。

电源管理: 在电源管理中,PWM可用于调整电压。

PWM接口通常由硬件电路实现,而在软件层面,操作系统或驱动程序提供相应的接口来配置和控制PWM。在嵌入式系统中,微控制器、FPGA或其他嵌入式处理器通常集成了PWM功能。

2.PWM实现原理与定时器内部逻辑:

高级定时器框图:

1. PSC分频器(Prescaler):

作用:psc分频器用于将定时器的输入时钟降低到合适的频率,分频器的值决定了定时器的计数的步进速度:

工作原理:假设输入的时钟频率为200MHZ,分频值为200,那么分频后的时钟频率就是1MHZ

设置PSC寄存器的值,可以控制定时器计数的速度。

2.自动重载寄器(ARR,Auto-Reload Register)

作用:ARR决定了定时器计数的最大值。每当计数器达到这个值时,计数器(CNT)会自动重置为0,并重新开始计数。

工作原理:定时器从0计数到ARR的值,然后重新开始计数,这样形成一个周期性计数过程。ARR的值决定了PWM信号的周期。

设置ARR寄存器的值,可以控制PWM信号的周期。

3.计数器(CNT,Counter)

作用:计数器是定时器的核心器件,它按设定的时钟频率计数。

工作原理:计数器的值从0开始,每经过一个定时器的时钟周期(PSC分频器决定),计数器的值加+1.当计数器达到ARR的值时,计数据重置为0,并触发中断或一个更新事件。

计数过程:计数器不断地从0计数到ARR值,再从0开始重新计数,这个循环决定了PWM信号的周期性。

4.捕获/比较寄存器(CCR,Capture/Compare Register)

作用:CCR用于设置PWM信号的点空比,它存储一个值,当计数器的值与CCR的值匹配时,定时器输出信号的电平会发生变化。

工作原理:在PWM模式下,当计数器的值小于CCR时,输出为高电平; 当计数器的值等于或大于CCR的值时,输出信号变低电平。

设置CCR寄存器的值,可以控制PWM信号的高电平持续的时间,从而调速占空比。如图所示:

第二节、硬件PWM子系统框架:

Linux PWM子系统提供了一套标准的类型和接口函数,用于管理和控制PWM设备。通过实现PWM控制器驱动程序,并使用这些接口函数,开发者可以方便地在内核中配置和控制PWM信号,同时提供用户空间接口,便于用户空间程序访问和操作PWM设备(也可以通过sys/class/xxx来控制pwm信号)。

所以在使用硬件PWM子系统时,一定要去make menuconfig看一下内核中的没有选配BSP原厂支持的定时器与支持所PWM制制器的驱动,如果没有选配一定切记选配:

以STM32MP157A为例,就没有选配,所以一定要先选配这些BSP原厂的支持的驱动才可以:如图所示:

第三节、硬件PWM常用类型与接口:

1.PWM设备结构体:pwm_device:

struct pwm_device {
    const char              *label;          // 设备标签,用于标识该PWM设备
    unsigned long           flags;           // 设备标志,存储与该设备相关的状态或配置信息
    unsigned int            hwpwm;           // 硬件PWM编号,表示该设备在硬件中的通道编号
    unsigned int            pwm;             // 全局PWM编号,在系统中的唯一标识

    struct pwm_chip         *chip;           // 指向所属的PWM芯片结构体的指针
    void                    *chip_data;      // 与PWM芯片相关的私有数据

    unsigned int            period;          // ① PWM周期,单位为纳秒
    unsigned int            duty_cycle;      // ② PWM占空比,单位为纳秒
    enum pwm_polarity       polarity;        // ③ PWM信号的极性,表示信号输出的高低电平状态
};
enum pwm_polarity {
    PWM_POLARITY_NORMAL,//正常极性,
    PWM_POLARITY_INVERSED,//反向极性,
};
PWM_POLARITY_NORMAL//正常极性:在一个 PWM 周期内,PWM 信号首先保持高电平,持续占空比指定的时间,然后变为低电平,持续剩余的时间。
示例:假设周期为 10 ms,占空比为 40%(4 ms),则 PWM 信号的前 4 ms 为高电平,后 6 ms 为低电平。
PWM_POLARITY_INVERSED,//反向极性:在一个 PWM 周期内,PWM 信号首先保持低电平,持续占空比指定的时间,然后变为高电平,持续剩余的时间。
示例:假设周期为 10 ms,占空比为 40%(4 ms),则 PWM 信号的前 4 ms 为低电平,后 6 ms 为高电平。

2.获取pwm_device: dev_of_pwm_get:

struct pwm_device *devm_of_pwm_get(struct device *dev, struct device_node *np,
                               const char *con_id)
作用:从指定的设备树节点获取PWM。
参数:dev是指向调用此函数的设备,设备卸载时会自动回收内存。推荐使用此函数
参数np:指定从哪个设备节点获取PWM。
参数con_id: PWM 设备的连接 ID,一般用于指定特定的 PWM 设备,可以为 NULL
返回值:成功返回pwm_device对象指针,失败返回错误码指针。

2.设置占空比:pwm_config

//1.配置PWM周期,占空比
int pwm_config(struct pwm_device *pwm, int duty_ns, int period_ns)

6.开启PWM / 停止PWM功能:

int pwm_enable(struct pwm_device *pwm);//开启PWM功能
void pwm_disable(struct pwm_device *pwm)//停止PWM功能
void pwm_free(struct pwm_device *pwm)//回收pwm对象内存。//如果获取pwm资源加了前缀devm时,则无需回收。

第四节、PWM应用:控制电机转速控制、音量控制等

甲方需求:根据应用层输入的数值,来控制电机的转速。

风扇 ---TIM1_CH1------PE9端口------PWM功能(AF1,

TIM1内存映射地址:0x44000000

1. 查看原理图,画出连接关系:

2.PWM复用引用的功能,设备树节点的描述方式:

  /{
      ... 
     //描述风扇设备节点信息:
        pwm-fan{
            compatible = "WX, pwm-fan";
            status = "okay";
            //描述连接pwm引脚信息:
            pwms = <&pwm1 0 1000 0>;
        };
};

&timers1{
    /delete-property/dmas;
    /delete-property/dma-names;
    status = "okay";

    pwm1:pwm {
        compatible = "st,stm32-pwm";
        #pwm-cells = <3>;
        status = "okay";
        //添加pwm引脚复用:PE9复用pwm功能:
        pinctrl-names = "default","sleep";
        pinctrl-0 = <&pwm1_pins_a>;
        pinctrl-1 = <&pwm1_sleep_pins_a>;
    };
}
&pinctrl{
    pwm1_pins_a: pwm1-0 {
        pins {
            pinmux = <STM32_PINMUX('E', 9, AF1)>; /* TIM1_CH1 */
        };
    };

    pwm1_sleep_pins_a: pwm1-sleep-0 {
        pins {
            pinmux = <STM32_PINMUX('E', 9, ANALOG)>; /* TIM1_CH1 */
        };
    };
};

3.驱动示例编写:

甲方需求:从应用层输入一个值,控制电机的转速:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/platform_device.h>
#include <linux/of_platform.h>
#include <linux/of.h>
#include <linux/pwm.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/string.h>

// 封装pwm_fan结构体:
struct Myfan
{
    struct pwm_device *pwm;
    u32 duty_ns;
    u32 priod;
    // 字符设备相关的属性:
    struct cdev *c_dev;
    struct class *class;
    struct device *dev;
};
struct Myfan pwm_fan = {
    .duty_ns = 0,    // 0占空比
    .priod = 1000000 // 1ms周期
};

int pwm_fan_open(struct inode *inode, struct file *file)
{
    printk("pwm_fan_open执行了\n");
    return 0;
}

// 在驱动的write中响应用户的输入,配置pwm占空比,启动pwm功能:
ssize_t pwm_fan_write(struct file *file, const char *usrbuf, size_t size, loff_t *offset)
{
    char k_buf[32] = {0};
    int recv_val = 0;
    printk("pwm_fan_write执行了\n");
    // pwm功能逻辑的构建:...
    if (size > sizeof k_buf)
    {
        size = sizeof k_buf;
    }
    copy_from_user_nofault(k_buf, usrbuf, size);
    // 把字符解析为相应数字整形:
    kstrtoint(k_buf, 10, &recv_val);
    pwm_fan.duty_ns = recv_val * 10000;
    // 配置占空比,并启用pwm功能:
    pwm_config(pwm_fan.pwm, pwm_fan.duty_ns, pwm_fan.priod);
    //启用pwm功能:
    pwm_enable(pwm_fan.pwm);
    return size;
}

int pwm_fan_release(struct inode *inode, struct file *file)
{
    printk("pwm_fan_release执行了\n");
    pwm_disable(pwm_fan.pwm);
    return 0;
}

struct file_operations fops = {
    .open = pwm_fan_open,
    .write = pwm_fan_write,
    .release = pwm_fan_release,
};

int pwm_fan_driver_probe(struct platform_device *pdev)
{
    printk("pwm_fan_driver_probe执行了\n");
    pwm_fan.pwm = devm_of_pwm_get(&pdev->dev, pdev->dev.of_node, NULL);
    if (IS_ERR(pwm_fan.pwm))
    {
        printk("获取pwm资源失败\n");
        return -ENODEV;
    }

    // 配置占空比

    // 启动pwm

    // 向上提供应用层接口及创建设备节点:
    pwm_fan.c_dev = cdev_alloc();
    cdev_init(pwm_fan.c_dev, &fops);

    alloc_chrdev_region(&pwm_fan.c_dev->dev, 0, 1, "FAN");

    cdev_add(pwm_fan.c_dev, pwm_fan.c_dev->dev, 1);

    // 自动创建设备节点:
    pwm_fan.class = class_create(THIS_MODULE, "pwm_fan");

    pwm_fan.dev = device_create(pwm_fan.class, NULL, pwm_fan.c_dev->dev, NULL, "pwm_fan");

    return 0;
}

int pwm_fan_driver_remove(struct platform_device *pdev)
{
    printk("pwm_fan_driver_remove执行了\n");
    device_destroy(pwm_fan.class, pwm_fan.c_dev->dev);
    class_destroy(pwm_fan.class);
    cdev_del(pwm_fan.c_dev);
    unregister_chrdev_region(pwm_fan.c_dev->dev, 1);
    kfree(pwm_fan.c_dev);
    return 0;
}

struct of_device_id of_pwm_fan_table[] = {
    [0] = {.compatible = "WX, pwm-fan"},
    [1] = {/*代表结束*/},
};

struct platform_driver pwm_fan_driver = {
    .probe = pwm_fan_driver_probe,
    .remove = pwm_fan_driver_remove,
    .driver = {
        .name = "pwm_fan_driver",
        .of_match_table = of_pwm_fan_table,
    },
};

// 入口函数:
int __init my_test_module_init(void)
{
    printk("A模块的入口函数执行了");
    platform_driver_register(&pwm_fan_driver);
    return 0;
}

// 出口函数:
void __exit my_test_module_exit(void)
{
    printk("出口函数执行了\n");
    platform_driver_unregister(&pwm_fan_driver);
}
// 指定许可:
MODULE_LICENSE("GPL");
MODULE_AUTHOR("gaowanxi, email:gaonetcom@163.com");
// 指定入口及出口函数:
module_init(my_test_module_init);
module_exit(my_test_module_exit);

4.应用层测试代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdbool.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char const *argv[])
{
    int fd = open("/dev/pwm_fan",O_WRONLY);

    char buf[32] = {0};

    while (true)
    {
        fgets(buf, sizeof(buf), stdin);
        int nbytes = write(fd, buf, strlen(buf));
        if(nbytes == -1)
        {
            perror("write err:");
            return -1;
        }
    }
    close(fd);
    return 0;
}

第五节、模拟PWM之高级定时器:

假设:你的设备没有连接PWM引脚,但也想通过PWM功能来控制设备的功率,哪怎么办呢?

亦可以通过GPIO口使用高级定时器hrtimer来模拟输出高低电压,从而模拟一个PWM输出,从而间接控制设备的功率:

1. Linux高级定时器hrtimer介绍:

与timer_list类似:但精度更高:

struct hrtimer {
	struct timerqueue_node		node;//定时器节点,添加定时器时由内核管理。
	ktime_t				_softexpires;//软超时
	enum hrtimer_restart		(*function)(struct hrtimer *);//超时回调函数
	struct hrtimer_clock_base	*base;
	u8				state;
	u8				is_rel;
	u8				is_soft;
	u8				is_hard;
};

2.高级定时器接口函数:

1. 高精度定时器初始化辅助函数:hrtimer_init:

void hrtimer_init(struct hrtimer *timer, clockid_t clock_id,
enum hrtimer_mode mode);
参数:
struct hrtimer *timer:指向需要初始化的高精度定时器结构体的指针。
clockid_t clock_id:指定定时器使用的时钟源。常见的时钟源有:
    CLOCK_REALTIME:系统实时时钟。
    CLOCK_MONOTONIC:系统单调时钟。
    CLOCK_BOOTTIME:包含系统挂起时间的单调时钟。
    CLOCK_TAI:国际原子时钟。
enum hrtimer_mode mode:指定定时器的模式,可以是以下两种之一:
    HRTIMER_MODE_REL:相对时间模式,定时器的超时是相对于当前时间的一个偏移量。
    HRTIMER_MODE_ABS:绝对时间模式,定时器的超时是一个绝对时间点。

2.设置定时时间ktime_set:

ktime_t ktime_set(const s64 secs, const unsigned long nsecs);
作用:设置超时时间,精度为纳秒。
参数secs:秒
参数nsecs:纳秒
返回值为:超时精度计量为:秒 + 纳秒

3.启动定时器hrtimer_start:

void hrtimer_start(struct hrtimer *timer, ktime_t tim,
const enum hrtimer_mode mode);
作用:启动定时器
参数time:高精度定时器对象指针。
参数tim:纳秒级精度的超时时间。
参数mod:使用的模式。与初始化时使用的模式保持统一。

4.重新设置定时器hrtimer_forward:循环超时使用类似于timer_list:这个函数如果超时很短很简易出现阻塞,可以直接使用hrtimer_start再次启动从而实现循环。

u64 hrtimer_forward(struct hrtimer *timer, ktime_t now, ktime_t interval);
作用:重新设置定时器
参数timer:定时器对象指针
参数now:当前时间:
参数interval:重新设定的时间。
u64 hrtimer_forward_now(struct hrtimer *timer,  ktime_t interval);//推荐使用下面的这个新的API接口,更方便使用。

5.关闭定时器函数:hrtimer_cancel函数:

int hrtimer_cancel(struct hrtimer* timer);
作用:关闭使用的定时器
参数:要关掉的定时器。

第六节、模拟PWM实验:呼吸灯

甲方需求:通过应用层数据发来的数据,实现LED呼吸灯效果:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/platform_device.h>
#include <linux/of_platform.h>
#include <linux/of.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/hrtimer.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/of.h>
//封装产生pwm的虚拟设备:
struct virtual_pwm_device
{
    // 1.产生超时的时钟:高级运行时钟:
    struct hrtimer timer;
    // 2.添加控制gpio控制的gpio号:
    int gpio_id;
    // 3.占空比:
    u32 duty_ns;
    // 4.周期:
    u32 period_ns;
    // 5.超时时间:
    ktime_t jifies_ns;
    // 6.添加可以延时处理阻塞操作任务的工作队列:
    struct work_struct work;
};

struct virtual_pwm_device pwm = {
    .duty_ns = 0,
    .period_ns = 1000,
};
// 处理阻塞操作任务的工作队列:因为对高低电平的设置是一种阻塞操作:
void pwm_work_func(struct work_struct *work)
{
    //构建逻辑:实现计数,及比较计数的功能:
    //...
    static int counter = 0;
    counter++;
    if(counter == pwm.period_ns)
    {
        counter = 0;
    }

    if(counter < pwm.duty_ns)
    {
        //输出高电平:
        gpio_set_value(pwm.gpio_id, 1);
    }
    else{
        //输出低电平:
        gpio_set_value(pwm.gpio_id, 0);
    }


    //1.再次配置定时器:这个函数可以删除,如果超时时间短会出现阻塞
    //hrtimer_forward_now(&pwm.timer, pwm.jifies_ns);
    //2.重新再一次启动定时器:
    hrtimer_start(&pwm.timer, pwm.jifies_ns,HRTIMER_MODE_REL);
}

// 高级定时器超时发生时的回调函数:
enum hrtimer_restart hrtimer_function(struct hrtimer *timer)
{
    enum hrtimer_restart status = HRTIMER_NORESTART;
    // 高级定时器的回调函数是在软中断中执行的,所以不可能有出现阻塞的操作:
    schedule_work(&pwm.work);
    return status;
}

// 1.初始化接口:
int virtual_pwm_device_init(struct virtual_pwm_device *pwm, struct platform_device *pdev)
{
    pwm->gpio_id = of_get_named_gpio(pdev->dev.of_node, "breadthing_led_gpios", 0);
    if (pwm->gpio_id < 0)
    {
        printk("获取gpio资源失败\n");
        return -ENODEV;
    }
    // 申请gpio资源:
    devm_gpio_request(&pdev->dev, pwm->gpio_id, "breathing-led-gpios");
    //初始化gpio口的输出方向:
    pinctrl_gpio_direction_output(pwm->gpio_id);
    //初始化gpio口输出的电平:
    gpio_set_value(pwm->gpio_id,0);

    // 初始化高级定时器:
    hrtimer_init(&pwm->timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);

    // 设置定时器的超时时间:
    pwm->jifies_ns = ktime_set(0, 10);

    // 设置高级定时器超时发生时的回调函数:
    pwm->timer.function = hrtimer_function;

    // 初始化工作:
    INIT_WORK(&pwm->work, pwm_work_func);

    printk("pwm_device初始化完成\n");
    return 0;
}

// 2.配置pwm占空比的接口:
void virtual_pwm_config(struct virtual_pwm_device *pwm, u32 duty_ns, u32 period_ns)
{
    pwm->period_ns = period_ns;
    pwm->duty_ns = duty_ns > period_ns ? period_ns : duty_ns;
    //printk("配置了占空比\n");
}

// 3.启动pwm的函数:
void virtual_pwm_enable(struct virtual_pwm_device *pwm)
{
    //开启高级定时器:
    hrtimer_start(&pwm->timer,pwm->jifies_ns,HRTIMER_MODE_REL);
    //printk("pwm启动\n");
}

// 4.停止pwm的接口:
void virtual_pwm_disable(struct virtual_pwm_device *pwm)
{
    //停止高级定时器:
    hrtimer_cancel(&pwm->timer);
    gpio_set_value(pwm->gpio_id, 0);
    cancel_work_sync(&pwm->work);
    printk("pwm停止\n");
}
//-----------------------字符设备--------------------------//

//封装字符设备类型:
struct BreathingLed 
{
    struct cdev* c_dev;
    struct class* class;
    struct device* dev;
};
struct BreathingLed myled = {0};
int myled_open(struct inode *inod, struct file *file)
{
    printk("myled_open执行了\n");
    return 0;
}


ssize_t myled_write(struct file *file, const char *usrbuf, size_t size, loff_t *offset)
{
    char k_buf[32] = {0};
    int recv_val = 0;
    //printk("myled_write执行了\n");
    //响应用户发来的数据,通过虚拟pwm的占空比,进行控制led的明暗度:
    //拷贝用户空间的数据到内核空间:
    if(size > sizeof(k_buf))
    {
        size = sizeof(k_buf);
    }
    copy_from_kernel_nofault(&recv_val, usrbuf, size);

    //字符串转成数字整形:如果直接传的是整形可以将下面的kstrtoint函数注释掉:
    kstrtoint(k_buf, 10, &recv_val);
    pwm.duty_ns = recv_val * 10;

    //配置占空比:
    virtual_pwm_config(&pwm,pwm.duty_ns, pwm.period_ns);

    //启用:
    virtual_pwm_enable(&pwm);

    return size;
}

int myled_release(struct inode *inode, struct file *file)
{
    printk("myled_close执行了\n");
    virtual_pwm_disable(&pwm);
    return 0;
}



struct file_operations fops = {
    .open = myled_open,
    .write = myled_write,
    .release = myled_release,
};

int breathing_led_driver_probe(struct platform_device *pdev)
{
    printk("breathing_led_driver_probe执行了\n");
    //对虚拟pwm设备初始化:
    virtual_pwm_device_init(&pwm, pdev);
    //字符设备进行初始化:
    myled.c_dev = cdev_alloc();
    cdev_init(myled.c_dev, &fops);
    alloc_chrdev_region(&myled.c_dev->dev, 0, 1, "breathingled");

    cdev_add(myled.c_dev, myled.c_dev->dev, 1);

    //自动创建设备节点:
    myled.class = class_create(THIS_MODULE,"BREATHINGLED");
    myled.dev = device_create(myled.class, NULL, myled.c_dev->dev, NULL, "breathingled");


    return 0;
}

int breathing_led_driver_remove(struct platform_device *pdev)
{
    printk("breathing_led_driver_remove执行了\n");
    device_destroy(myled.class, myled.c_dev->dev);
    class_destroy(myled.class);
    cdev_del(myled.c_dev);
    unregister_chrdev_region(myled.c_dev->dev,1);
    kfree(myled.c_dev);

    return 0;
}

struct of_device_id of_led_table[] = {
    [0] = {.compatible = "WX,breadthing-led-001"},
    [1] = {/*代表结束*/},
};
struct platform_driver breathing_led_driver = {
    .probe = breathing_led_driver_probe,
    .remove = breathing_led_driver_remove,
    .driver = {
        .name = "breadthing-led-driver",
        .of_match_table = of_led_table,
    },
};


// 入口函数:
int __init my_test_module_init(void)
{
    printk("A模块的入口函数执行了");
    platform_driver_register(&breathing_led_driver);

    return 0;
}

// 出口函数:
void __exit my_test_module_exit(void)
{
    printk("出口函数执行了\n");
    platform_driver_unregister(&breathing_led_driver);
}
// 指定许可:
MODULE_LICENSE("GPL");
MODULE_AUTHOR("gaowanxi, email:gaonetcom@163.com");
// 指定入口及出口函数:
module_init(my_test_module_init);
module_exit(my_test_module_exit);

gpio模拟pwm设备树:

/{
    ...
    //呼吸灯breathing_led设备节点的描述:
    breathing_led{
        compatible = "WX,breadthing-led-001";
        breadthing_led_gpios = <&gpioe 10 0>;
        status = "okay";
    };
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值