Linux驱动之利用STM32、设备树、pwm子系统实现风扇的分级调控
系 统:Linux 5.10.61
开发板:STM32mp157a
硬 件:风扇
一、首先我们需要对PWM和定时器(TIM)的联系简单的做一下了解,具体详细的PWM原理可见PWM原理 PWM频率与占空比详解。
PWM(脉宽调制)和TIM(定时器)在嵌入式系统中有着紧密的联系。以下是它们之间的主要关系和作用:
(一)、PWM(Pulse Width Modulation)脉宽调制
PWM 是一种用于控制模拟电路的数字信号技术。通过调节脉冲的宽度(即高电平持续时间),可以控制平均输出电压,从而实现对设备如电机、LED亮度的调节。PWM 信号的频率和占空比是两个关键参数。
(二)、TIM(Timer)定时器
TIM 是微控制器中用于计时和产生精确时间间隔的硬件模块。它们通常用于以下几个方面:
- 产生定时中断
- 计数事件(如外部脉冲)
- 产生PWM信号
(三)、PWM 和 TIM 的联系
- PWM的实现依赖于定时器:
- 定时器可以用于产生周期性的中断,进而改变PWM信号的高电平和低电平持续时间。
- 在STM32等微控制器中,定时器模块通常具有专门的PWM模式,可以直接生成PWM信号。通过设置定时器的相关寄存器(如自动重装载寄存器ARR和比较寄存器CCR),可以调节PWM信号的频率和占空比。
- 定时器配置:
- 频率设置:定时器的频率决定了PWM信号的频率。通过设置定时器的预分频器(Prescaler)和自动重装载寄存器(ARR),可以配置定时器的计数频率。
- 占空比设置:PWM信号的占空比由定时器的比较寄存器(CCR)决定。通过改变CCR的值,可以调节PWM信号的高电平时间。
二、编写设备树
1、查看设备原理图,查看风扇的连接方式
硬件连接:
拓展版:
开发板:
上图我们可以看到,我们的风扇模块连接是这样的:风扇—TIM1—PE9—PWM(AF1)功能 到了PE9这个引脚上
内存地址映射:0x44000000
引脚复用:
上图可以看到想要用到pwm(这里是tim1控制)需要将PE9复用为AF1。
下面我们看着原理图以及芯片手册简单的画一下我们的连接图:
2、查看厂商设备树,分析填写内容
先看厂商提供的帮助文档,这是为了让我们知道,我们想建立一个pwm节点都需要得到哪些信息:
帮助文档路径:xxx@xxx:xxxx/linux-5.10.61/Documentation/devicetree/bindings/pwm
读帮助文档得知,我们的pwm节点应该像这样:
pwm1: pwm@fe510000 { // 定义名为pwm1的PWM控制器节点,地址为0xfe510000
compatible = "st,pwm"; // 指定该设备与"st,pwm"驱动程序兼容
reg = <0xfe510000 0x68>; // 定义寄存器基地址0xfe510000和大小0x68
#pwm-cells = <2>; // 指定pwm-cells的数量为2,即每个PWM描述符包含两个参数
pinctrl-names = "default"; // 定义引脚控制状态名为"default"
pinctrl-0 = <&pinctrl_pwm1_chan0_default // 引脚控制组0,包含四个默认状态的引脚配置
&pinctrl_pwm1_chan1_default
&pinctrl_pwm1_chan2_default
&pinctrl_pwm1_chan3_default>;
clocks = <&clk_sysin>; // 指定该PWM控制器使用的时钟源为clk_sysin
clock-names = "pwm"; // 定义时钟的名称为"pwm"
st,pwm-num-chan = <4>; // 定义该PWM控制器拥有4个PWM通道
st,capture-num-chan = <2>; // 定义该PWM控制器拥有2个捕获通道
};
我们去Linux源码目录下的设备树文件中找到我们所需要的信息:
找与我们板子匹配的设备树,我这里用到的是STM32MP157A的板子,那么我就去找其对应的设备树:
由于我们这此设备树中没有找到我们所想要得到的信息,那么我们就去该文件的头文件中去找我们想要的信息:
上图我们找到了tim1的设备树节点位置,下面我们接着找到PE9复用为PWM的配置信息:
我们进入到这个xxxxx-pinctrl.dtsi中:
仿照其原有格式去寻找我们的PE9复用AF1:(‘E’, 9, AF1)我们去ctrl+F去查找我们想要的:
3、接下来修改我们的设备树,将上面的内容复制到我们的设备树中进行修改:
在根节点下添加:
/{
...
//描述风扇信息:
pwm_fan{
compatible = "wyc , pwm_fan";
status = "okay";
//描述引脚
pwms = <&pwm1 0 100 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 */
};
};
}
下面在我们源码目录下执行:make dtbs命令 如下:
这样我们就得到了添加好设备节树点的设备树
我们将设备树移动到我们的开发板对应目录下
重启开发板
我们的设备树就写好了。
三、写驱动代码
(一)、头文件 :fan_dev.h
#ifndef __FAN_DEV_H__
#define __FAN_DEV_H__
#include <linux/init.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_device.h>
#include <linux/uaccess.h>
#include <linux/fs.h>
#include <linux/pwm.h>
#include <linux/err.h>
#include <linux/kernel.h>
#include <linux/device.h>
#include <asm/io.h>
#include <linux/types.h>
#define NO_ONE _IO('f', 1) //低速命令
#define NO_TWO _IO('f', 2) //高速命令
#define NO_THREE _IO('f', 3) //极速命令
#endif
(二)、驱动函数 : fan_dev.c
#include "../include/fan_dev.h"
#define NAME "pwm_fan_mod" // 定义设备名称常量
// 全局变量定义
struct class *cls = NULL; // 类指针,用于设备类创建
struct device *dev = NULL; // 设备指针,用于设备创建
struct device_node *node = NULL; // 设备树节点指针
struct pwm_device *pwm_dev; // PWM设备指针
int major; // 主设备号
// 打开设备的函数定义
int fan_open(struct inode *inode, struct file *file)
{
printk("%s,%s,%d\n", __FILE__, __func__, __LINE__); // 打印文件名、函数名和代码行号
return 0;
}
// 关闭设备的函数定义
int fan_close(struct inode *inode, struct file *file)
{
printk("%s,%s,%d\n", __FILE__, __func__, __LINE__);
return 0;
}
// 设备IO控制函数
long fan_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
int ret;
printk("%s,%s,%d\n", __FILE__, __func__, __LINE__);
// 根据cmd执行相应的操作
switch (cmd)
{
case NO_ONE: // 低速
// 设置PWM占空比和周期
ret = pwm_config(pwm_dev, 350000, 1000000);
if (ret < 0)
{
printk("pwm_config error\n");
return ret;
}
// 使能PWM输出
ret = pwm_enable(pwm_dev);
if (ret < 0)
{
printk("pwm_enable error\n");
return ret;
}
break;
case NO_TWO: // 高速
// 设置PWM占空比和周期
ret = pwm_config(pwm_dev, 600000, 1000000);
if (ret < 0)
{
printk("pwm_config error\n");
return ret;
}
// 使能PWM输出
ret = pwm_enable(pwm_dev);
if (ret < 0)
{
printk("pwm_enable error\n");
return ret;
}
break;
case NO_THREE: // 极速
// 设置PWM占空比和周期
ret = pwm_config(pwm_dev, 900000, 1000000);
if (ret < 0)
{
printk("pwm_config error\n");
return ret;
}
// 使能PWM输出
ret = pwm_enable(pwm_dev);
if (ret < 0)
{
printk("pwm_enable error\n");
return ret;
}
break;
default:
break;
}
return 0;
}
// 文件操作函数集合
struct file_operations fops =
{
.open = fan_open,
.release = fan_close,
.unlocked_ioctl = fan_ioctl,
};
// 驱动模块初始化函数
int __init fan_pwm_init(void)
{
int ret = 0;
// 从设备树中获取节点
node = of_find_node_by_name(NULL, "pwm_fan_mod");
if (NULL == node)
{
printk("of_find_node_by_name error\n");
ret = -ENODATA;
goto err;
}
// 注册字符设备驱动
major = register_chrdev(0, NAME, &fops);
if (major < 0)
{
printk("register_chrdev error\n");
ret = major;
goto err;
}
// 创建设备类
cls = class_create(THIS_MODULE, NAME);
if (IS_ERR(cls))
{
printk("class_create error\n");
ret = PTR_ERR(cls);
goto err_class;
}
// 创建设备
dev = device_create(cls, NULL, MKDEV(major, 0), NULL, "pwm_fan");
if (IS_ERR(dev))
{
printk("device_create error\n");
ret = PTR_ERR(dev);
goto err_device;
}
// 获取PWM设备资源
pwm_dev = devm_of_pwm_get(dev, node, NULL);
if (IS_ERR(pwm_dev))
{
printk("pwm_get error\n");
ret = PTR_ERR(pwm_dev);
goto err_pwm;
}
printk("Fan PWM driver ok\n");
return 0;
err_pwm:
device_destroy(cls, MKDEV(major, 0));
err_device:
class_destroy(cls);
err_class:
unregister_chrdev(major, NAME);
err:
return ret;
}
// 驱动模块卸载函数
void __exit fan_pwm_exit(void)
{
pwm_disable(pwm_dev); // 禁用PWM
device_destroy(cls, MKDEV(major, 0)); // 销毁设备
class_destroy(cls); // 销毁设备类
unregister_chrdev(major, NAME); // 注销字符设备
printk("Fan PWM driver exited\n");
}
module_init(fan_pwm_init);
module_exit(fan_pwm_exit);
MODULE_LICENSE("GPL"); // 指明驱动许可证类型
(三)、应用程序 :fan_main.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdbool.h>
#include <unistd.h>
#include <string.h>
#include <sys/ioctl.h>
#define NO_ONE _IO('f', 1) //低速命令
#define NO_TWO _IO('f', 2) //高速命令
#define NO_THREE _IO('f', 3) //极速命令
int main(int argc, char const *argv[])
{
int fd = open("/dev/pwm_fan", O_WRONLY);
int nbytes = 0;
int flag = 0;
while (true)
{
printf("请选择档位1.低速,2.高速,3.极速:\n");
scanf("%d", &flag);
switch (flag)
{
case 1:
printf("1\n");
nbytes = ioctl(fd, NO_ONE);
if (nbytes == -1)
{
perror("write err");
return -1;
}
break;
case 2:
nbytes = ioctl(fd, NO_TWO);
if (nbytes == -1)
{
perror("write err");
return -1;
}
break;
case 3:
nbytes = ioctl(fd, NO_THREE);
if (nbytes == -1)
{
perror("write err");
return -1;
}
break;
default:
break;
}
}
close(fd);
return 0;
}
四、可能出现的错误:
1、在加载驱动时可能出现错误,驱动加载不上
原因:
这大概率是编译系统源码时没有选配板子的PWM功能
解决办法:
进入我们开发板的源码目录下,执行命令:make menuconfig
然后参考自己板子的手册,开启PWM,TIM对应的功能
2、在PWM子系统在注册资源时使用pwm_get可能会在加载驱动时出现pwm_get错误
原因:
这是因为pwm_get是手动注册管理资源,可能需要更精确的错误处理和资源清理策略,如果调用pwm_get后没有正确配置或启用PWM设备(或者没有正确处理错误),可能导致后续资源申请失败。
解决办法:
在PWM子系统中,注册资源函数有devm_of_pwm_get和pwm_get,我们选用devm_of_pwm_get来注册资源,devm_of_pwm_get通过设备管理器自动处理PWM资源的生命周期,减少了因为手动管理资源而导致的错误。