参考视频地址
【北京迅为】嵌入式学习之Linux驱动(第十八期PWM全新升级)基于RK3568开发板哔哩哔哩_bilibili
1.基础知识
pwm基础知识
PWM(Pulse Width Modulation) 是一种通过调节脉冲信号的 宽度(占空比) 来模拟不同电压或功率输出的技术。其核心是通过 快速切换高低电平 的开关状态,控制平均能量输出,从而实现对设备(如电机、LED等)的精准控制。
占空比(Duty Cycle):高电平时间占整个周期的百分比,公式:
占空比越大,平均电压越高(如占空比50%时,平均电压为峰值的一半)。
频率(Frequency):每秒内脉冲信号的周期数,单位:Hz。高频减少噪声(如电机驱动),低频简化滤波电路设计。
典型应用
应用场景 | 实现原理 | 案例 |
---|---|---|
LED调光 | 调节占空比控制LED的平均亮度(人眼感知为连续变化)。 | Arduino的analogWrite() 函数输出PWM。 |
电机调速 | 改变占空比等效调节电机驱动电压,控制转速。 | 无人机电调(ESC)控制无刷电机转速。 |
电源转换(DC-DC) | 通过PWM控制开关管通断时间,实现升降压转换(如Buck/Boost电路)。 | 手机充电器中的高效电压调节。 |
音频合成 | 通过高频PWM滤波生成模拟音频信号(需低通滤波器)。 | 数字功放(Class D放大器)。 |
ITOP-RK3568开发板pwm接口介绍
16路PWM硬件控制器。
rk3568 PWM特性
-
16 个片上 PWM(PWM0 ~ PWM15):支持基于中断的操作模式。
-
可编程预分频总线时钟:可对总线时钟进行预分频,并进一步分频调整。
-
内置 32 位定时器/计数器:提供高精度计时功能。
-
支持捕获模式:用于捕捉外部信号的时间或频率。
-
支持连续模式或单次模式:
-
连续模式:持续生成 PWM 信号。
-
单次模式:单次触发后停止。
-
-
提供参考模式:可输出多种占空比的波形。
-
针对红外应用的优化:PWM3、PWM7、PWM11 和 PWM15 专门优化,适用于红外信号控制场景。
迅为rk3568开发板,PWM使用情况
ITOP-RK3588 开发板pwm接口介绍
PWM
-
支持16个片上PWM模块(PWM0~PWM15):采用基于中断的操作模式。
-
可编程预分频总线时钟:支持对总线时钟进行预分频,并可进一步调整分频参数。
-
内置32位定时器/计数器模块:提供高精度计时与计数功能。
-
支持捕获模式:用于精确捕捉外部信号的时间或频率。
-
支持连续模式或单次触发模式:
-
连续模式:持续生成PWM信号。
-
单次模式:单次触发后自动停止。
-
-
提供参考模式:可输出多种占空比的波形(如方波、脉冲等)。
-
针对红外应用的优化:PWM3、PWM7、PWM11和PWM15通道专门优化,适用于红外信号控制(如遥控器、传感器等场景),支持输入捕获功能。
2.pwm子系统框架
pwm子系统框架
实验外设:SG90舵机介绍
舵机是一种位置(角度)伺服的驱动器。适用于那些需要角度步段变化的情景中,所以常用于舵面操纵。
rk3568硬件连接
SG90舵机模块引脚 | DH11模块引脚名称 | 连接到开发板的引脚编号 | 连接到开发板的应交名称 |
---|---|---|---|
黄线 | PWM | 6 | UART9_TX_M1(GPIO4_C5) |
红线 | VCC | 10 | VCC_5V |
棕线 | GND | 20 | GND |
SG90使用方法
SG90 舵机的控制一般需要一个 20ms 周期的 pwm_信号。高电平持续时间一般为 0.5ms 到 2.5ms 范围内对角度进行控制。以 180 度角度 SG90 为例。对应关系如下:
0.5ms->0 度
1.0ms->45 度
1.5ms->90 度
2.0ms->135 度
2.5ms->180 度
使用sysfs接口操作pwm
配置管脚支持PWM
迅为rk3588开发板,默认设备树有蜂鸣器的pwm配置
kernel\arch\arm64\boot\dts\rockchip\topeet_rk3588_config.dtsi
//蜂鸣器
&pwm15 {
status = "okay";
pinctrl-0 = <&pwm15m2_pins>;
};
kernel\arch\arm64\boot\dts\rockchip\rk3588s.dtsi
pwm15: pwm@febf0030 {
compatible = "rockchip,rk3588-pwm", "rockchip,rk3328-pwm";
reg = <0x0 0xfebf0030 0x0 0x10>;
interrupts = <GIC_SPI 350 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 351 IRQ_TYPE_LEVEL_HIGH>;
#pwm-cells = <3>;
pinctrl-names = "active";
pinctrl-0 = <&pwm15m0_pins>;
clocks = <&cru CLK_PWM3>, <&cru PCLK_PWM3>;
clock-names = "pwm", "pclk";
status = "disabled";
};
查看3588板子是否有pwm15:
root@topeet:~# cat /sys/kernel/debug/pwm
platform/febf0030.pwm, 1 PWM device
pwm-0 ((null) ): period: 0 ns duty: 0 ns polarity: normal
platform/fd8b0030.pwm, 1 PWM device
pwm-0 ((null) ): period: 0 ns duty: 0 ns polarity: normal
platform/fd8b0010.pwm, 1 PWM device
pwm-0 (backlight ): requested enabled period: 25000 ns duty: 19607 ns polarity: normal
我们看到第2行,febf0030 对应的地址就为pwm15,设备树reg的地址。
pwm15对应哪个pwmchipX呢?对应关系如下所示:
视频老师使用的是rk3568开发板子,对应设备树的修改如下所示:
打开arch/arm64/boot/dts/rockchip/rk3568-evb1-ddr4-v10-linux.dtsi 设备树文件。因为pwm12和串口9复用,请先注释掉串口9的节点。
打开pwm12,并且使用pinctrl设置服用。因为我们使用的是pwm12的m1,所以要是m1这组。
&pwm12 {
status = "okay"
pinctrl-names = "active";
pinctrl-0 = <&pwm12m1_pins>;
};
重新编译内核镜像,烧写到开发板。
如果配置成功,cat /sys/kernel/debuf/pwm, 可以看到截图圈出来的信息:
操作pwm设备
以rk3588的板子为例:
cd /sys/devices/platform/febf0030.pwm/pwm/pwmchip2/
echo 0 > export //导出
ls
device export npwm power pwm0 subsystem uevent unexport //执行导出后,增加了一个pwm0
cd pwm0
echo 20000000 > period //PWM周期设置20ms
echo 2000000 > duty_cycle //设置高电平的时间
echo normal > polarity // 设置极性,有normal或inversed两个参数选中
echo 1 > enable //使能pwm,使能后开发板的蜂鸣器开始响声
echo 0 > enable //关闭pwm,关闭后蜂鸣器发声停止
pwm控制器注册流程
pwm控制器程序程序代码路径:kernel/drivers/pwm/pwm-rockchip.c
pwm子系统API函数
1. pwm_request() – 申请PWM设备
struct pwm_device *pwm_request(int pwm_id, const char *label);
参数:
pwm_id:PWM通道号(如0表示第一个PWM通道)。
label:字符串标识,用于区分不同使用者(通常为驱动名称)。
返回值:
成功:返回struct pwm_device指针。 失败:返回ERR_PTR()包装的错误码(如-EBUSY表示设备已被占用)。
作用:
从系统中申请指定PWM通道的使用权,需在驱动初始化时调用。
2. pwm_config() – 配置PWM周期与占空比
int pwm_config(struct pwm_device *pwm, int duty_ns, int period_ns);
参数:
pwm:pwm_request()返回的设备指针。
duty_ns:高电平时间(单位:纳秒,必须小于period_ns)。
period_ns:信号周期(单位:纳秒,决定PWM频率:频率 = 1 / (period_ns * 1e-9))。
返回值:
成功:返回0。 失败:返回负数错误码(如-EINVAL表示参数无效)。
作用:
设置PWM的周期和占空比,需在pwm_enable()前调用。
3. pwm_enable() / pwm_disable() – 启用/禁用PWM输出
int pwm_enable(struct pwm_device *pwm);
void pwm_disable(struct pwm_device *pwm);
参数:
pwm:已配置好的PWM设备指针。
返回值:
pwm_enable():成功返回0,失败返回负数错误码。 pwm_disable():无返回值。
作用:
pwm_enable():启动PWM信号输出。 pwm_disable():停止PWM输出(占空比清零)。
4. pwm_set_polarity() – 设置PWM极性
int pwm_set_polarity(struct pwm_device *pwm, enum pwm_polarity polarity);
参数:
pwm:PWM设备指针。
polarity:极性枚举值: PWM_POLARITY_NORMAL:默认极性(高电平有效)。 PWM_POLARITY_INVERSED:反转极性(低电平有效)。
返回值:
成功:0。 失败:负数错误码(如硬件不支持极性反转时返回-ENOTSUPP)。
作用:
调整PWM信号的有效电平方向,需在pwm_enable()前调用。
5. pwm_free() – 释放PWM设备
void pwm_free(struct pwm_device *pwm);
参数:
pwm:要释放的PWM设备指针。
返回值:
无。
作用:
释放PWM通道资源,通常在驱动卸载或设备移除时调用。
6. pwm_get() – 通过设备树获取PWM设备(推荐方式)
struct pwm_device *pwm_get(struct device *dev, const char *con_id);
参数:
dev:关联的设备结构体指针(通常为&pdev->dev)。
con_id:设备树中PWM标识符(如"pwm"对应设备树的pwms属性)。
返回值:
与pwm_request()相同。
作用:
从设备树自动解析并申请PWM资源,替代手动调用pwm_request()。
7.devm_of_pwm_get 函数
struct pwm_device *devm_of_pwm_get(struct device *dev, struct device_node *np,const char *con_id);
参数:
dev: device
np: 设备节点
con_id: 名字
函数作用:获取pwm_device
3.蜂鸣器驱动编写
编写驱动程序(一)
教学视频中编写的是S90舵机驱动,暂时没有这个模块,暂时选择RK3588板子的蜂鸣器,添加device部分代码,修改kernel\arch\arm64\boot\dts\rockchip\rk3588-evb7-lp4.dtsi,代码修改如下所示:
PWM_BZ {
compatible = "PWM_BZ";
//选择pwm15,占空比暂时设置为0,周期20ms,正极性
pwms = <&pwm15 0 20000000 1>;
};
接下来编写driver代码, touch PWM_BZ.c:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/platform_device.h>
#include <linux/of.h>
static dev_t dev_num;
static struct cdev cdev_test;
struct class *class;
struct device *device;
static int cdev_test_open(struct inode *inode, struct file *file)
{
printk("This is cdev_test_open.\n");
return 0;
}
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size_t, loff_t *off)
{
printk("This is cdev_test_read.\n");
return 0;
}
ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
printk("This is cdev_test_write.\n");
return 0;
}
int cdev_test_release(struct inode *inode, struct file *file)
{
printk("This is cdev_test_release.\n");
return 0;
}
static struct file_operations cdev_test_ops = {
.owner = THIS_MODULE,
.open = cdev_test_open,
.read = cdev_test_read,
.write = cdev_test_write,
.release = cdev_test_release
};
int BZ_probe(struct platform_device *dev)
{
int ret;
ret = alloc_chrdev_region(&dev_num,0,1,"chrdev_name");
if (ret < 0){
printk("alloc_chrdev_region is error\n");
}
printk("alloc_register_region is ok\n");
cdev_init(&cdev_test,&cdev_test_ops);
cdev_test.owner = THIS_MODULE;
ret = cdev_add(&cdev_test,dev_num,1);
if(ret < 0 )
{
printk("cdev_add is error\n");
}
printk("cdev_add is ok\n");
class = class_create(THIS_MODULE, "test");
device = device_create(class, NULL, dev_num, NULL, "BZ");
return 0;
}
int BZ_remove(struct platform_device *dev)
{
return 0;
}
const struct of_device_id BZ_of_device_id[] = {
{.compatible = "PWM_BZ"},
{}
};
struct platform_driver BZ_platform_driver = {
.driver = {
.name = "BZ",
.of_match_table = BZ_of_device_id,
},
.probe = BZ_probe,
.remove = BZ_remove,
};
static int __init module_cdev_init(void)
{
platform_driver_register(&BZ_platform_driver);
return 0;
}
static void __exit module_cdev_exit(void)
{
unregister_chrdev_region(dev_num, 1);
cdev_del(&cdev_test);
device_destroy(class, dev_num);
class_destroy(class);
platform_driver_unregister(&BZ_platform_driver);
printk("module exit \n");
}
module_init(module_cdev_init);
module_exit(module_cdev_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");
Makefile:
export ARCH=arm64#设置平台架构
export CROSS_COMPILE=aarch64-none-linux-gnu-#交叉编译器前缀
obj-m += PWM_BZ.o #此处要和你的驱动源文件同名
KDIR :=/home/topeet/Linux/rk3588-linux/kernel #这里是你的内核目录
PWD ?= $(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules #make操作
clean:
make -C $(KDIR) M=$(PWD) clean #make clean操作
编译内核代码并烧写到板子,编译PWM_BZ.c,将KO放到板子上,操作命令:
root@topeet:/run# insmod PWM_BZ.ko
root@topeet:/run# ls /dev/BZ
/dev/BZ
内核打印,说明设备和驱动匹配上了,执行了函数BZ_probe:
[ 71.208444] alloc_register_region is ok
[ 71.208468] cdev_add is ok
编写驱动程序(二)
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/pwm.h>
#include <linux/uacce.h>
static dev_t dev_num;
static struct cdev cdev_test;
struct class *class;
struct device *device;
struct pwm_device *bz_pwm_device;
static struct pwm_state state = {
.period = 20000000, // 周期(纳秒)
.duty_cycle = 500000, // 占空比(纳秒)
.polarity = PWM_POLARITY_NORMAL,
.enabled = false, // 先禁用再配置
};
static int cdev_test_open(struct inode *inode, struct file *file)
{
printk("This is cdev_test_open.\n");
//占空比:0.5ms, 周期:20ms,极性设置为默认极性(高电平有效)
//pwm_config(bz_pwm_device, 500000, 20000000);
//pwm_set_polarity(bz_pwm_device, PWM_POLARITY_NORMAL); //奇怪RK3588的内核没有函数pwm_set_polarity
pwm_apply_state(bz_pwm_device, &state);
pwm_enable(bz_pwm_device);
return 0;
}
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size_t, loff_t *off)
{
printk("This is cdev_test_read.\n");
return 0;
}
ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
int ret;
unsigned char data[1];
printk("This is cdev_test_write.\n");
ret = copy_from_user(data, buf, size);
//通过用户传入的参数,改变占空比
pwm_config(bz_pwm_device, 500000 + data[0] * (500000 / 45), 20000000);
return 0;
}
int cdev_test_release(struct inode *inode, struct file *file)
{
printk("This is cdev_test_release.\n");
pwm_config(bz_pwm_device, 500000, 20000000);
pwm_disable(bz_pwm_device);
return 0;
}
static struct file_operations cdev_test_ops = {
.owner = THIS_MODULE,
.open = cdev_test_open,
.read = cdev_test_read,
.write = cdev_test_write,
.release = cdev_test_release
};
int BZ_probe(struct platform_device *dev)
{
int ret;
ret = alloc_chrdev_region(&dev_num,0,1,"chrdev_name");
if (ret < 0){
printk("alloc_chrdev_region is error\n");
}
printk("alloc_register_region is ok\n");
cdev_init(&cdev_test,&cdev_test_ops);
cdev_test.owner = THIS_MODULE;
ret = cdev_add(&cdev_test,dev_num,1);
if(ret < 0 )
{
printk("cdev_add is error\n");
}
printk("cdev_add is ok\n");
class = class_create(THIS_MODULE, "test");
device = device_create(class, NULL, dev_num, NULL, "BZ");
bz_pwm_device = devm_of_pwm_get(&dev->dev, dev->dev.of_node, NULL);
if( IS_ERR(bz_pwm_device) )
{
printk("devm_of_pwm_get is ERROR\n");
return -1;
}
return 0;
}
int BZ_remove(struct platform_device *dev)
{
return 0;
}
const struct of_device_id BZ_of_device_id[] = {
{.compatible = "PWM_BZ"},
{}
};
struct platform_driver BZ_platform_driver = {
.driver = {
.name = "BZ",
.of_match_table = BZ_of_device_id,
},
.probe = BZ_probe,
.remove = BZ_remove,
};
static int __init module_cdev_init(void)
{
platform_driver_register(&BZ_platform_driver);
return 0;
}
static void __exit module_cdev_exit(void)
{
unregister_chrdev_region(dev_num, 1);
cdev_del(&cdev_test);
device_destroy(class, dev_num);
class_destroy(class);
pwm_free(bz_pwm_device);
platform_driver_unregister(&BZ_platform_driver);
printk("module exit \n");
}
module_init(module_cdev_init);
module_exit(module_cdev_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");
注意代码第31行,注释掉了函数pwm_set_polarity(),是由于RK3588内核代码,PWM不支持这个函数,所以改用函数pwm_apply_state()。
编写测试APP
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
int fd; // 文件描述符
unsigned char buf[1]; // 缓冲区,存放两个整数值
// 检查命令行参数个数是否正确
if (argc != 2)
{
fprintf(stderr, "Usage: %s <count>\n", argv[0]);
return -1;
}
// 打开 PWM GPIO 设备文件
fd = open("/dev/BZ", O_WRONLY);
if (fd < 0)
{
perror("open error");
return -1;
}
// 将命令行参数转换为整数,存入缓冲区
buf[0] = atoi(argv[1]);
// 将缓冲区的数据写入 PWM GPIO 设备
write(fd, buf, sizeof(buf));
//蜂鸣器响30秒
sleep(30);
// 关闭设备文件
close(fd);
return 0;
}
编译驱动和APP程序,将两者拷贝到RK3588板子上,加载驱动,接着执行app程序,可以听到蜂鸣器响30s后停止。
4.模拟pwm驱动编写
模拟PWM使用GPIO口,通过软件的方式,模拟PWM波形。
普通定时器的时钟频率可以设置在100Hz到1000Hz之间,所以精度只能限制在毫秒级别,无法满足高精度的场景,为此Linux提供了高精度定时器,可以提供纳秒级别的精度。
在Linux普通定时器用struct time_list来表示(include/linux/timer.h)。
struct timer_list {
...;
struct hlist_node entry; //定时器到期时间
void (*function)(struct timer_list *); //超时服务函数
u32 flags;
...;
};
高精度定时器用struct hrtimer结构体来表示(include/linux/hrtimer.h)
struct hrtimer {
...;
ktime_t _softexpires; //定时时间
enum hrtimer_restart (*function)(struct hrtimer *);//超时服务时间
struct hrtimer_clock_base *base;
...;
};
高精度定时器API函数
1.初始化定时器
void hrtimer_init(struct hrtimer *timer, clockid_t which_clock, enum hrtimer_mode mode);
参数:
timer:要初始化的 hrtimer 结构体。
which_clock:时钟类型,常用值: CLOCK_MONOTONIC:单调递增时钟(不受系统时间调整影响)。 CLOCK_REALTIME:系统实时时钟(受时间调整影响)。
mode:定时器模式: HRTIMER_MODE_ABS:绝对时间模式。 HRTIMER_MODE_REL:相对时间模式。
注意:
如果which_clock 设置为CLOCK_MONOTONIC,那么mode要设置为HARTIMER_MODE_REL;
如果which_clock 设置为CLOCK_REALTIME,那么mode要设置为HRTIMER_MODE_ABS。
2.启动定时器
int hrtimer_start(struct hrtimer *timer, ktime_t tim, const enum hrtimer_mode mode);
参数:
tim:到期时间(根据 mode 为绝对或相对时间)。
返回 0 表示成功,负数表示错误。
3.重新调整定时器时间
u64 hrtimer_forward(struct hrtimer *timer, ktime_t now, ktime_t interval);
-
将定时器的到期时间向前推移一个间隔 interval。
-
返回推移的次数(适用于周期性定时器)。
4.取消定时器
int hrtimer_cancel(struct hrtimer *timer);
-
停止定时器,若定时器正在执行回调,会等待其完成。
-
返回 1 表示定时器正在运行,0 表示未运行。
5.设置定时时间
static inline ktime_t ktime_set(const s64 secs, const unsigned long nsecs)
参数:const s64 secs: 秒; const unsigned long nsecs: 纳秒;
编写驱动程序
目标使用模拟PWM控制LED小灯的亮暗。
由于我使用的迅为RK3588开发板,硬件电路上并GPIO并没有连接LED等,所以需要外接一个LED模块。
这里我选择GPIO2_C4_d 这个GPIO引脚。修改设备树:kernel/arch/arm64/boot/dts/rockchip/rk3588-evb7-lp4.dtsi,添加代码
leds {
compatible = "pwm-leds";
led-gpios = <&gpio2 RK_PC4 GPIO_ACTIVE_HIGH>;
};
接着编译内核代码,重新烧写到板子上。
PWM_gpio.c驱动代码如下所示:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/pwm.h>
#include <linux/uacce.h>
#include <linux/slab.h>
#include <linux/gpio/consumer.h>
static dev_t dev_num;
static struct cdev cdev_test;
struct class *class;
struct device *device;
struct pwm_led_data {
int sum_count; //PWM 周期总脉冲数
int high_count; //PWM 高电平持续脉冲数
struct gpio_desc *gpiod; //GPIO 描述符
struct hrtimer pwm_timer; //高分辨率定时器
ktime_t time; //定时器时间间隔
};
//声明 PWM LED 数据结构体指针
struct pwm_led_data *data;
// PWM 定时器回调函数
enum hrtimer_restart pwm_timer_func(struct hrtimer *timer)
{
static int timer_count = 0; // 定时器计数器
struct pwm_led_data *mydata = container_of(timer, struct pwm_led_data, pwm_timer);
// 如果计数器达到总脉冲数, 将 GPIO 设为高电平
if( timer_count == mydata->sum_count )
{
gpiod_set_value(mydata->gpiod, 1);
timer_count = 0;
}
// 如果计数器达到高电平持续脉冲数, 将 GPIO 设为低电平
if( timer_count == mydata->high_count )
{
gpiod_set_value(mydata->gpiod, 0);
}
timer_count++;
// 如果高电平持续脉冲数为 0, 则计数器重置为 0
hrtimer_forward(timer, hrtimer_cb_get_time(timer), mydata->time);
return HRTIMER_RESTART;
}
static int cdev_test_open(struct inode *inode, struct file *file)
{
printk("This is cdev_test_open.\n");
return 0;
}
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size_t, loff_t *off)
{
printk("This is cdev_test_read.\n");
return 0;
}
ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
int ret;
int kbuf[2];
printk("This is cdev_test_write.\n");
ret = copy_from_user(kbuf, buf, size);
if( ret != 0 )
{
printk("copy_from_user failed\n");
return -EFAULT;
}
// 更新 PWM LED 数据结构体
data->sum_count = kbuf[0];
data->high_count = kbuf[1];
return size;
}
int cdev_test_release(struct inode *inode, struct file *file)
{
printk("This is cdev_test_release.\n");
return 0;
}
static struct file_operations cdev_test_ops = {
.owner = THIS_MODULE,
.open = cdev_test_open,
.read = cdev_test_read,
.write = cdev_test_write,
.release = cdev_test_release
};
int led_probe(struct platform_device *dev)
{
int ret;
// 分配 PWM LED 数据结构体内存
data = kmalloc(sizeof(struct pwm_led_data), GFP_KERNEL);
if( !data )
{
printk("kmalloc failed\n");
return -ENOMEM;
}
// 初始化 PWM LED 数据结构体
data->sum_count = 20;
data->high_count = 10;
ret = alloc_chrdev_region(&dev_num,0,1,"alloc_name");
if (ret < 0){
printk("alloc_chrdev_region is error\n");
kfree(data);
return ret;
}
printk("alloc_register_region is ok\n");
cdev_init(&cdev_test,&cdev_test_ops);
cdev_test.owner = THIS_MODULE;
ret = cdev_add(&cdev_test,dev_num,1);
if(ret < 0 )
{
printk("cdev_add is error\n");
unregister_chrdev_region(dev_num, 1);
kfree(data);
return ret;
}
printk("cdev_add is ok\n");
class = class_create(THIS_MODULE, "test");
if( IS_ERR(class) )
{
printk("class_create is error\n");
cdev_del(&cdev_test);
unregister_chrdev_region(dev_num, 1);
kfree(data);
return PTR_ERR(class);
}
device = device_create(class, NULL, dev_num, NULL, "pwm-gpio"); // 创建设备节点
if( IS_ERR(device) )
{
printk("device_create is error\n");
class_destroy(class);
cdev_del(&cdev_test);
unregister_chrdev_region(dev_num, 1);
kfree(data);
return PTR_ERR(device);
}
//第2个参数"led",为设备树"led-gpios"的前缀,默认输出高电平
data->gpiod = gpiod_get(&dev->dev, "led", GPIOD_OUT_HIGH);
if (IS_ERR(data->gpiod))
{
printk("gpiod_get is error\n");
device_destroy(class, dev_num);
class_destroy(class);
cdev_del(&cdev_test);
unregister_chrdev_region(dev_num, 1);
kfree(data);
return PTR_ERR(data->gpiod);
}
gpiod_set_value(data->gpiod, 1); //将 GPIO 设为高电平
// 初始化高分辨率定时器
data->time = ktime_set(0, 1000000); // 1 ms
hrtimer_init(&data->pwm_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
data->pwm_timer.function = pwm_timer_func;
hrtimer_start(&data->pwm_timer, data->time, HRTIMER_MODE_REL); // 启动高分辨率定时器
printk("led_probe successful\n");
return 0;
}
int led_remove(struct platform_device *dev)
{
hrtimer_cancel(&data->pwm_timer); // 停止高分辨率定时器
gpiod_put(data->gpiod); // 释放 GPIO 描述符
device_destroy(class, dev_num); // 删除设备节点
class_destroy(class); // 删除设备类
cdev_del(&cdev_test); // 从内核注销字符设备
unregister_chrdev_region(dev_num, 1); // 释放设备号
kfree(data); // 释放 PWM LED 数据结构体内存
printk("led_remove successful\n");
return 0;
}
// 设备树匹配表
const struct of_device_id led_of_device_id[] = {
{.compatible = "pwm-leds"},
{}
};
//MODULE_DEVICE_TABLE 是一个关键宏,用于将设备驱动支持的设备标识符(如 PCI、USB、ACPI 等设备的 ID 表)导出到模块的元数据中。
MODULE_DEVICE_TABLE(of, led_of_device_id);
struct platform_driver led_platform_driver = {
.driver = {
.name = "pwm-leds",
.of_match_table = led_of_device_id,
},
.probe = led_probe,
.remove = led_remove,
};
static int __init module_cdev_init(void)
{
int ret;
ret = platform_driver_register(&led_platform_driver);
if( ret )
{
printk("platform_driver_register is error\n");
return ret;
}
return 0;
}
static void __exit module_cdev_exit(void)
{
// 注销平台设备驱动
platform_driver_unregister(&led_platform_driver);
printk("bye bye\n");
}
module_init(module_cdev_init);
module_exit(module_cdev_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");
Makefile:
export ARCH=arm64#设置平台架构
export CROSS_COMPILE=aarch64-none-linux-gnu-#交叉编译器前缀
obj-m += PWM_gpio.o #此处要和你的驱动源文件同名
KDIR :=/home/topeet/Linux/rk3588-linux/kernel #这里是你的内核目录
PWD ?= $(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules #make操作
clean:
make -C $(KDIR) M=$(PWD) clean #make clean操作
编写测试APP
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
int fd; // 文件描述符
int buf[2]; // 缓冲区,存放两个整数值
// 检查命令行参数个数是否正确
if (argc != 3)
{
fprintf(stderr, "Usage: %s <sum_count> <high_count>\n", argv[0]);
return -1;
}
// 打开 PWM GPIO 设备文件
fd = open("/dev/pwm-gpio", O_WRONLY);
if (fd < 0)
{
perror("open error");
return -1;
}
// 将命令行参数转换为整数,存入缓冲区
buf[0] = atoi(argv[1]);
buf[1] = atoi(argv[1]);
// 将缓冲区的数据写入 PWM GPIO 设备
if (write(fd, buf, sizeof(buf)) != sizeof(buf))
{
perror("write error");
close(fd);
return -1;
}
// 关闭设备文件
close(fd);
return 0;
}
课后作业--呼吸灯
int led_probe(struct platform_device *dev)
{
...;
data->sum_count = 100;
data->high_count = 10;
...;
// 初始化高分辨率定时器
data->time = ktime_set(0, 200000); // 1 ms
}
应用代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
int fd; // 文件描述符
int buf[2]; // 缓冲区,存放两个整数值
int i;
// 检查命令行参数个数是否正确
if (argc != 3)
{
fprintf(stderr, "Usage: %s <sum_count> <high_count>\n", argv[0]);
return -1;
}
// 打开 PWM GPIO 设备文件
fd = open("/dev/pwm-gpio", O_WRONLY);
if (fd < 0)
{
perror("open error");
return -1;
}
// 将命令行参数转换为整数,存入缓冲区
buf[0] = atoi(argv[1]);
buf[1] = atoi(argv[1]);
// 进入死循环
while (1)
{
// 递增占空比
for (i = 0; i <= buf[0]; i++)
{
buf[1] = i;
write(fd, buf, sizeof(buf));
usleep(30000); // 延迟 30 毫秒
}
// 递减占空比
for (i = buf[0]; i > 0; i--)
{
buf[1] = i;
write(fd, buf, sizeof(buf));
usleep(30000); // 延迟 30 毫秒
}
}
// 关闭设备文件
close(fd);
return 0;
}
5.扩展
为什么可以使用sysfs接口操作pwm?
在函数pwmchip_add_width_polarity() 函数最后调用了pwmchip_sys_export(),pwmchip_sys_export()函数向sys文件系统注册了一个pwm控制器设备。pwmchip_sys_export()调用的时候传入全局变量pwm_class,这个全局变量在pwm_sysfs_init()中进行初始化(上面截图大红色框框圈出来那部分)。pwm_group保存了对sysfs操作的一部分文件操作函数,通过这些文件操作函数对PWM设备进行操作:显示,周期显示,占空比显示,设置收起,设置占空比等。
体验一把原厂工程师的工作 --- 支持PWM输入捕获功能
由于瑞芯微只实现了状态获取和申请两个函数,并没有实现输入捕获相关的函数,而输入捕获也是 PWM 的常用功能之一,要想在RK3588 上实现 PWM 的输入捕获,就需要我们自己来实现 PWM 输入捕获的驱动了。这个章节介绍,如何实现输入捕获功能。
在章节《ITOP-RK3588 开发板pwm接口介绍》我们知道,只有只有 PWM3、PWM7、PWM11 和 PWM15这 4 路 PWM 通道可以进行输入捕获,在本章节将使用 PWM15 进行输入捕获演示,默认情况下 PWM15 用到了蜂鸣器,具体硬件原理图如下所示:
所以需要将默认设备树中有关蜂鸣器的进行修改,首先使用以下命令打开 topeet_rk3588_config.dtsi 设备树,修改以下内容,如下图所示:
vi kernel\arch\arm64\boot\dts\rockchip\topeet_rk3588_config.dtsi
&pwm15 {
status = "okay";
compatible = "pwm-capture";
pinctrl-names = "default";
pinctrl-0 = <&pwm15m2_pins>;
pwm-channel = <15>;
};
查看pwm15 默认结点配置如下所示
vi kernel\arch\arm64\boot\dts\rockchip\rk3588s.dtsi
pwm15: pwm@febf0030 {
compatible = "rockchip,rk3588-pwm", "rockchip,rk3328-pwm";
reg = <0x0 0xfebf0030 0x0 0x10>;
interrupts = <GIC_SPI 350 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 351 IRQ_TYPE_LEVEL_HIGH>;
#pwm-cells = <3>;
pinctrl-names = "active";
pinctrl-0 = <&pwm15m0_pins>;
clocks = <&cru CLK_PWM3>, <&cru PCLK_PWM3>;
clock-names = "pwm", "pclk";
status = "disabled";
};
输入PWM设备树节点配置好后,还需要一个输出PWM,这里选择PWM4,操作步骤如下所示:
从上面这张图中,我们可以知道PWM4_M1这个GPIO没有被使用,那么设备树修改, 并添加修改:
vim kernel/arch/arm64/boot/dts/rockchip/rk3588s-pinctrl.dtsi
pwm4 {
...;
/omit-if-no-ref/
pwm4m1_pins: pwm4m1-pins {
rockchip,pins =
/* pwm4_m1 */
<4 RK_PC3 11 &pcfg_pull_none>;
};
};
使用以下命令打开 topeet-rk3588-linux.dts 进行修改,进入该设备树之后, 添加如下结点:
vi kernel\arch\arm64\boot\dts\rockchip\topeet_rk3588_config.dtsi
&pwm4 {
status = "okay";
pinctrl-0 = <&pwm4m1_pins>;
};
编译内核,然后将编译好的内核文件烧写到开发板。
cat /sys/kernel/debug/pwm
根据 rk3588 的数据手册或者设备树可以得到 pwm4 的设备地址为 febd0000,与上图的第三个设备相匹配,就可以证明 PWM 配置成功了。
编写pwm输入捕获驱动程序
编写输入捕获驱动程序
capture_probe
// 驱动探测函数
int capture_probe(struct platform_device *pdev)
{
int ret;
struct rkxx_capture_drvdata *ddata;
struct resource *r;
struct clk *clk;
struct clk *p_clk;
struct device_node *np = pdev->dev.of_node;
int pwm_channel;
int irq;
struct pwm_capture_cdev *pcdev;
int freq;
// 分配驱动数据结构
ddata = devm_kzalloc(&pdev->dev, sizeof(struct rkxx_capture_drvdata), GFP_KERNEL);
if (!ddata) {
dev_err(&pdev->dev, "Failed to allocate memory for driver data\n");
return -ENOMEM;
}
ddata->state = RMC_IDLE;
// 获取资源
r = platform_get_resource(pdev, IORESOURCE_MEM, 0);
ddata->base = devm_ioremap_resource(&pdev->dev, r);
if (IS_ERR(ddata->base)) {
dev_err(&pdev->dev, "Failed to map memory resource\n");
return PTR_ERR(ddata->base);
}
ddata->dev = pdev->dev;
// 获取时钟
clk = devm_clk_get(&pdev->dev, "pwm");
if (IS_ERR(clk)) {
dev_err(&pdev->dev, "Failed to get PWM clock\n");
return PTR_ERR(clk);
}
ddata->clk = clk;
p_clk = devm_clk_get(&pdev->dev, "pclk");
if (IS_ERR(p_clk)) {
dev_err(&pdev->dev, "Failed to get peripheral clock\n");
return PTR_ERR(p_clk);
}
ddata->p_clk = p_clk;
// 从设备树中读取 PWM 通道
ret = of_property_read_u32(np, "pwm-channel", &pwm_channel);
if (ret) {
dev_err(&pdev->dev, "Failed to get PWM channel from device tree\n");
return ret;
}
pwm_channel %= 4;
ddata->pwm_channel = pwm_channel;
// 获取中断号
irq = platform_get_irq(pdev, 0);
if (irq < 0) {
dev_err(&pdev->dev, "Failed to get IRQ\n");
return irq;
}
ddata->irq = irq;
// 设置驱动数据
platform_set_drvdata(pdev, ddata);
// 请求中断; IRQF_NO_SUSPEND 不要在挂起的时候进入中断
ret = devm_request_irq(&pdev->dev, irq, rk_pwm_capture, IRQF_NO_SUSPEND, "rk_pwm_capture_irq", ddata);
if (ret) {
dev_err(&pdev->dev, "Failed to request IRQ\n");
return ret;
}
// 启用时钟
ret = clk_prepare_enable(ddata->clk);
if (ret) {
dev_err(&pdev->dev, "Failed to enable PWM clock\n");
return ret;
}
ret = clk_prepare_enable(ddata->p_clk);
if (ret) {
clk_disable_unprepare(ddata->clk);
dev_err(&pdev->dev, "Failed to enable peripheral clock\n");
return ret;
}
// 计算 PWM 频率
freq = clk_get_rate(ddata->clk) / 64;
ddata->pwm_freq_nstime = 1000000000 / freq;
// 注册字符设备
pcdev = &ddata->pwm_cdev;
ret = alloc_chrdev_region(&pcdev->dev_num, 0, 1, "alloc_name");
if (ret < 0) {
dev_err(&pdev->dev, "alloc_chrdev_region error\n");
goto err_alloc_chrdev;
}
printk("alloc_chrdev_region success\n");
pcdev->cdev_test.owner = THIS_MODULE;
cdev_init(&pcdev->cdev_test, &cdev_test_ops);
ret = cdev_add(&pcdev->cdev_test, pcdev->dev_num, 1);
if (ret) {
dev_err(&pdev->dev, "Failed to add cdev\n");
goto err_cdev_add;
}
// 创建设备类
pcdev->class = class_create(THIS_MODULE, "test");
if (IS_ERR(pcdev->class)) {
ret = PTR_ERR(pcdev->class);
dev_err(&pdev->dev, "Failed to create class\n");
goto err_class_create;
}
// 创建设备
pcdev->device = device_create(pcdev->class, NULL, pcdev->dev_num, NULL, "capture");
if (IS_ERR(pcdev->device)) {
ret = PTR_ERR(pcdev->device);
dev_err(&pdev->dev, "Failed to create device\n");
goto err_device_create;
}
rk_pwm_capture_init(ddata->base, ddata->pwm_channel);
return 0;
err_device_create:
class_destroy(pcdev->class);
err_class_create:
cdev_del(&pcdev->cdev_test);
err_cdev_add:
unregister_chrdev_region(pcdev->dev_num, 1);
err_alloc_chrdev:
clk_disable_unprepare(ddata->p_clk);
clk_disable_unprepare(ddata->clk);
return ret;
}
rk_pwm_capture_init
这个函数将PWM设置为输入捕获模式,代码如下所示:
/* 初始化 PWM 捕获 */
static void rk_pwm_capture_init(void __iomem *pwm_base, uint pwm_id)
{
int val;
/* 禁用 PWM */
val = readl_relaxed(pwm_base + PWM_REG_CTRL);
val = (val & 0xFFFFFFFE) | PWM_DISABLE;
writel_relaxed(val, pwm_base + PWM_REG_CTRL);
/* 设置为捕获模式 */
val = readl_relaxed(pwm_base + PWM_REG_CTRL);
val = (val & 0xFFFFFFF9) | PWM_MODE_CAPTURE;
writel_relaxed(val, pwm_base + PWM_REG_CTRL);
/* 设置分频值 */
val = readl_relaxed(pwm_base + PWM_REG_CTRL);
val = (val & 0xFF0001FF) | PWM_DIV64;
writel_relaxed(val, pwm_base + PWM_REG_CTRL);
/* 启用中断 */
rk_pwm_int_ctrl(pwm_base, pwm_id, PWM_INT_ENABLE);
/* 这里可以启用 PWM 捕获(注释掉的代码) */
/*
val = readl_relaxed(pwm_base + PWM_REG_CTRL);
val = (val & 0xFFFFFFFE) | PWM_ENABLE;
writel_relaxed(val, pwm_base + PWM_REG_CTRL);
*/
}
第7行代码读取PWM控制寄存器的地址。我们使用的是PWM3,对应控制寄存器偏移地址要为0x003C,但是代码中为啥宏PWM_REG_CTRL为0x0C呢?
这个是由于设备树中对应PWM3配置的基地址为0xfebf0030,所以宏PWM_REG_CTRL要为0x0C,两者相加刚好为PWM3控制寄存器地址0xfebf003C。
代码第11~第14行,将PWM设置为捕获模式。
第16~19行代码,设置分频。代码中将prescale=2^6=64,scale设置为0,即默认512。
代码第22行,启动中断。
中断初始化函数
/* PWM 中断控制 */
static void rk_pwm_int_ctrl(void __iomem *pwm_base, uint pwm_id, int ctrl)
{
int val;
if (pwm_id > 3)
return; /* 如果 PWM ID 超过 3,直接返回 */
val = readl_relaxed(pwm_base + PWM_REG_INT_EN(pwm_id)); /* 读取当前中断使能状态 */
if (ctrl) {
val |= PWM_CH_INT_ENABLE(pwm_id); /* 设置中断使能 */
writel_relaxed(val, pwm_base + PWM_REG_INT_EN(pwm_id)); /* 写入中断使能寄存器 */
} else {
val &= ~PWM_CH_INT_ENABLE(pwm_id); /* 清除中断使能 */
writel_relaxed(val, pwm_base + PWM_REG_INT_EN(pwm_id)); /* 写入中断使能寄存器 */
}
}
中断服务函数
// PWM 捕获中断处理函数
irqreturn_t rk_pwm_capture(int irq, void *dev_id) {
struct rkxx_capture_drvdata *ddata = dev_id;
unsigned int channel = ddata->pwm_channel;
int val, lpr, hpr;
val = readl_relaxed(ddata->base + PWM_REG_INTSTS(channel));
if ((val & PWM_CH_INT(channel)) == 0) {
return IRQ_NONE;
}
// 根据极性读取 lpr 或 hpr
if ((val & PWM_CH_POL(channel)) == 0) {
if (ddata->state != RMC_DONE) {
lpr = readl_relaxed(ddata->base + PWM_REG_LPR);
ddata->lpr = lpr;
}
} else {
if (ddata->state != RMC_DONE) {
hpr = readl_relaxed(ddata->base + PWM_REG_HPR);
ddata->hpr = hpr;
}
}
// 清除中断状态
writel_relaxed(PWM_CH_INT(channel), ddata->base + PWM_REG_INTSTS(channel));
// 状态机处理
switch (ddata->state) {
case RMC_IDLE1:
ddata->hpr = 0;
ddata->lpr = 0;
ddata->state = RMC_IDLE2;
break;
case RMC_IDLE2:
ddata->hpr = 0;
ddata->lpr = 0;
ddata->state = RMC_GETDATA;
break;
case RMC_GETDATA:
printk("ddata->hpr is %d, ddata->lpr is %d\n", ddata->hpr, ddata->lpr);
if (ddata->hpr && ddata->lpr) {
ddata->state = RMC_DONE;
}
break;
default:
break;
}
return IRQ_HANDLED;
}
第3行代码,由于在capture_probe()函数第65行,使用platform_set_drvdata()设置了driver_data,所以可以直接通过参数dev_id,获取到struct rkxx_capture_drvdata类型的地址ddata;
第7行代码是读取中断寄存器的值,如何获取到中断寄存器的地址? 打开RK3588芯片的数据手册《Rockchip RK3588 TRM V1.0-Part1-20220309.pdf》,第1411页:
我们看到PWM_INSTS对应的偏移地址为0x0040,又由于通过设备树<reg>结点,我们知道基地址为:0xfebf0030,所以宏PWM_REG_INTSTS(n)
#define PWM_REG_INTSTS(n) ((3 - (n)) * 0x10 + 0x10)
比如说我们在《体验一把原厂工程师的工作 --- 支持PWM输入捕获功能》章节中,将pwm-channel配置为15:
15 % 4 = 3
(3 - 3) * 0x10 + 0x10 = 0x10
那么PWM_INSTS的地址就为0xfebf0030+0x10 = 0xfebf0040。
代码第8~第10行是判断pwm通道3是否产生中断, 打开芯片手册第1422页,我们可以看到PWM_INSTS的第三通道对应的bit取值,0表示没有产生中断,1表示产生中断:
接下来关注芯片手册1422页,我们看到CH3_Pol,CH2_Pol, CH1_Pol,CH0_Pol。其中一个CH3_Pol bit翻译如下所示:
CH3_Pol
该位用于捕获模式中,以识别产生中断时输入波形的跳变方向。
当该位为 1 时,请参考 PWM3_PERIOD_HPR 寄存器获取通道3输入波形的有效高电平周期值;
否则,请参考 PWM3_PERIOD_LPR 寄存器获取通道3输入波形的有效低电平周期值。
向 CH3_IntSts 寄存器写入 1 可清除该位状态。
也就是说,通过CH3_Pol的值:
如果其值为高电平,那么通过PWM3_PERIOD_HPR寄存器,获取到通道3输入波形的有效高电平周期值;
如果其值为低电平,那么通过PWM3_PERIOD_LPR寄存器,获取到通道3输入波形的有效低电平周期值;
代码第13~23行,就是在做如上的操作,获取高电平的值,保存到ddata->hpr中,获取低电平的值保存到ddata->lpr中。
代码低26行,将对应通道中断位写1,表示中断已经发生。
驱动open()函数
static int cdev_test_open(struct inode *inode, struct file *file) {
struct rkxx_capture_drvdata *ddata;
struct pwm_capture_cdev *pcdev;
printk("This is cdev_test_open\n");
// 从 inode 中获取设备数据
pcdev = container_of(inode->i_cdev, struct pwm_capture_cdev, cdev_test);
ddata = container_of(pcdev, struct rkxx_capture_drvdata, pwm_cdev);
if (!ddata) {
printk(KERN_ERR "Failed to get device data\n");
return -ENODEV;
}
// 将设备数据保存到文件私有数据中
file->private_data = ddata;
return 0;
}
第8~第9行代码目的是获取struct rkxx_capture_drvdata *ddata,并将其保存到file->private_data中。
container_of
container_of` 是 Linux 内核中一个重要的宏,用于**通过结构体成员的指针反向获取外层结构体的指针**。它在内核链表、设备驱动等场景中被广泛使用,是内核开发中的核心工具之一。
#define container_of(ptr, type, member) ({ \
const typeof(((type *)0)->member) *__mptr = (ptr); \
(type *)((char *)__mptr - offsetof(type, member)); \
})
参数说明:
-
ptr
:结构体内部某成员的指针(已知地址) -
type
:外层结构体的类型(需显式指定) -
member
:成员在结构体中的名称(需与ptr
对应)
工作流程
-
类型安全检查:
typeof(((type *)0)->member)
通过 GCC 扩展获取成员的类型,并定义临时指针__mptr
,确保ptr
的类型与member
的类型匹配。 -
计算偏移量:
offsetof(type, member)
计算成员member
在结构体type
中的字节偏移量(通过编译器宏实现)。 -
反向定位结构体: 将成员指针
ptr
转换为char*
(确保按字节计算),减去偏移量后得到外层结构体的起始地址,最后强制转换为type*
类型。
以驱动open()函数中第9行代码为例子:
ddata = container_of(pcdev, struct rkxx_capture_drvdata, pwm_cdev);
struct rkxx_capture_drvdata {
void __iomem *base;
int irq;
struct device dev;
int pwm_freq_nstime;
int pwm_channel;
int hpr;
int lpr;
eRMC_STATE state;
struct clk *clk;
struct clk *p_clk;
struct pwm_capture_cdev pwm_cdev;
struct pwm_data data;
};
const typeof(((type *)0)->pwm_cdev) *__mptr = (pcdev); //确保pcdev与typeof(((type *)0)->pwm_cdev)类型要匹配
//计算成员pwm_cdev在结构体struct rkxx_capture_drvdata内的地址偏移,接着使用__mptr地址,减去offsetof(type, member)得到的地址,即可获取外层结构体的地址。
(type *)((char *)__mptr - offsetof(type, member));
在举一个Linux 内核链表的例子:
// 自定义结构体,内嵌链表节点
struct my_data {
int value;
struct list_head list; // 链表成员
};
// 已知链表节点指针,反向获取外层结构体指针
struct list_head *node_ptr = ...; // 某个链表节点
struct my_data *data = container_of(node_ptr, struct my_data, list);
驱动remove函数
// 驱动移除函数
int capture_remove(struct platform_device *pdev)
{
struct rkxx_capture_drvdata *ddata = platform_get_drvdata(pdev);
struct pwm_capture_cdev *pcdev = &ddata->pwm_cdev;
device_destroy(pcdev->class, pcdev->dev_num);
class_destroy(pcdev->class);
cdev_del(&pcdev->cdev_test);
unregister_chrdev_region(pcdev->dev_num, 1);
clk_disable_unprepare(ddata->p_clk);
clk_disable_unprepare(ddata->clk);
return 0;
}
驱动read函数
// 读取字符设备的回调函数
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off) {
struct rkxx_capture_drvdata *ddata;
int val, i, ret;
ddata = file->private_data;
if (!ddata) {
printk(KERN_ERR "Device data is NULL\n");
return -EINVAL;
}
// 初始化捕获数据
ddata->lpr = 0;
ddata->hpr = 0;
ddata->state = RMC_IDLE1;
// 启用 PWM
val = readl_relaxed(ddata->base + PWM_REG_CTRL);
val = (val & 0xFFFFFFFE) | PWM_ENABLE;
writel_relaxed(val, ddata->base + PWM_REG_CTRL);
// 等待数据捕获完成
for (i = 0; i < 100; i++) {
msleep(1);
if (ddata->state == RMC_DONE && ddata->hpr && ddata->lpr) {
printk("capture ok!\n");
break;
}
}
// 禁用 PWM
val = readl_relaxed(ddata->base + PWM_REG_CTRL);
val = (val & 0xFFFFFFFE) | PWM_DISABLE;
writel_relaxed(val, ddata->base + PWM_REG_CTRL);
if (ddata->hpr == 0 || ddata->lpr == 0) {
printk(KERN_ERR "Failed to capture PWM data\n");
return -EIO;
}
// 计算周期和占空比
ddata->data.period_ns = (ddata->lpr + ddata->hpr) * ddata->pwm_freq_nstime;
ddata->data.duty_ns = ddata->hpr * ddata->pwm_freq_nstime;
// 将数据拷贝到用户空间
ret = copy_to_user(buf, &ddata->data, size);
if (ret) {
printk(KERN_ERR "Failed to copy data to user space\n");
return -EFAULT;
}
printk("This is cdev_test_read\n");
ddata->state = RMC_IDLE;
return size;
}
完整驱动代码
PWM_capture.c
#include <linux/module.h>
#include <linux/init.h>
#include <linux/moduleparam.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/pwm.h>
#include <linux/uaccess.h>
#include <linux/clk.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
#include <linux/delay.h>
#include "rockchip_pwm_capture.h"
// 打开字符设备的回调函数
static int cdev_test_open(struct inode *inode, struct file *file) {
struct rkxx_capture_drvdata *ddata;
struct pwm_capture_cdev *pcdev;
printk("This is cdev_test_open\n");
// 从 inode 中获取设备数据
pcdev = container_of(inode->i_cdev, struct pwm_capture_cdev, cdev_test);
ddata = container_of(pcdev, struct rkxx_capture_drvdata, pwm_cdev);
if (!ddata) {
printk(KERN_ERR "Failed to get device data\n");
return -ENODEV;
}
// 将设备数据保存到文件私有数据中
file->private_data = ddata;
return 0;
}
// 读取字符设备的回调函数
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off) {
struct rkxx_capture_drvdata *ddata;
int val, i, ret;
ddata = file->private_data;
if (!ddata) {
printk(KERN_ERR "Device data is NULL\n");
return -EINVAL;
}
// 初始化捕获数据
ddata->lpr = 0;
ddata->hpr = 0;
ddata->state = RMC_IDLE1;
// 启用 PWM
val = readl_relaxed(ddata->base + PWM_REG_CTRL);
val = (val & 0xFFFFFFFE) | PWM_ENABLE;
writel_relaxed(val, ddata->base + PWM_REG_CTRL);
// 等待数据捕获完成
for (i = 0; i < 100; i++) {
msleep(1);
if (ddata->state == RMC_DONE && ddata->hpr && ddata->lpr) {
printk("capture ok!\n");
break;
}
}
// 禁用 PWM
val = readl_relaxed(ddata->base + PWM_REG_CTRL);
val = (val & 0xFFFFFFFE) | PWM_DISABLE;
writel_relaxed(val, ddata->base + PWM_REG_CTRL);
if (ddata->hpr == 0 || ddata->lpr == 0) {
printk(KERN_ERR "Failed to capture PWM data\n");
return -EIO;
}
// 计算周期和占空比
ddata->data.period_ns = (ddata->lpr + ddata->hpr) * ddata->pwm_freq_nstime;
ddata->data.duty_ns = ddata->hpr * ddata->pwm_freq_nstime;
// 将数据拷贝到用户空间
ret = copy_to_user(buf, &ddata->data, size);
if (ret) {
printk(KERN_ERR "Failed to copy data to user space\n");
return -EFAULT;
}
printk("This is cdev_test_read\n");
ddata->state = RMC_IDLE;
return size;
}
// 释放字符设备的回调函数
static int cdev_test_release(struct inode *inode, struct file *file) {
printk("This is cdev_test_release\n");
return 0;
}
// 字符设备操作函数结构体
static struct file_operations cdev_test_ops = {
.owner = THIS_MODULE,
.open = cdev_test_open,
.read = cdev_test_read,
.release = cdev_test_release
};
// PWM 捕获中断处理函数
irqreturn_t rk_pwm_capture(int irq, void *dev_id) {
struct rkxx_capture_drvdata *ddata = dev_id;
unsigned int channel = ddata->pwm_channel;
int val, lpr, hpr;
val = readl_relaxed(ddata->base + PWM_REG_INTSTS(channel));
if ((val & PWM_CH_INT(channel)) == 0) {
return IRQ_NONE;
}
// 根据极性读取 lpr 或 hpr
if ((val & PWM_CH_POL(channel)) == 0) {
if (ddata->state != RMC_DONE) {
lpr = readl_relaxed(ddata->base + PWM_REG_LPR);
ddata->lpr = lpr;
}
} else {
if (ddata->state != RMC_DONE) {
hpr = readl_relaxed(ddata->base + PWM_REG_HPR);
ddata->hpr = hpr;
}
}
// 清除中断状态
writel_relaxed(PWM_CH_INT(channel), ddata->base + PWM_REG_INTSTS(channel));
// 状态机处理
switch (ddata->state) {
case RMC_IDLE1:
ddata->hpr = 0;
ddata->lpr = 0;
ddata->state = RMC_IDLE2;
break;
case RMC_IDLE2:
ddata->hpr = 0;
ddata->lpr = 0;
ddata->state = RMC_GETDATA;
break;
case RMC_GETDATA:
printk("ddata->hpr is %d, ddata->lpr is %d\n", ddata->hpr, ddata->lpr);
if (ddata->hpr && ddata->lpr) {
ddata->state = RMC_DONE;
}
break;
default:
break;
}
return IRQ_HANDLED;
}
// 驱动探测函数
int capture_probe(struct platform_device *pdev)
{
int ret;
struct rkxx_capture_drvdata *ddata;
struct resource *r;
struct clk *clk;
struct clk *p_clk;
struct device_node *np = pdev->dev.of_node;
int pwm_channel;
int irq;
struct pwm_capture_cdev *pcdev;
int freq;
// 分配驱动数据结构
ddata = devm_kzalloc(&pdev->dev, sizeof(struct rkxx_capture_drvdata), GFP_KERNEL);
if (!ddata) {
dev_err(&pdev->dev, "Failed to allocate memory for driver data\n");
return -ENOMEM;
}
ddata->state = RMC_IDLE;
// 获取资源
r = platform_get_resource(pdev, IORESOURCE_MEM, 0);
ddata->base = devm_ioremap_resource(&pdev->dev, r);
if (IS_ERR(ddata->base)) {
dev_err(&pdev->dev, "Failed to map memory resource\n");
return PTR_ERR(ddata->base);
}
ddata->dev = pdev->dev;
// 获取时钟
clk = devm_clk_get(&pdev->dev, "pwm");
if (IS_ERR(clk)) {
dev_err(&pdev->dev, "Failed to get PWM clock\n");
return PTR_ERR(clk);
}
ddata->clk = clk;
p_clk = devm_clk_get(&pdev->dev, "pclk");
if (IS_ERR(p_clk)) {
dev_err(&pdev->dev, "Failed to get peripheral clock\n");
return PTR_ERR(p_clk);
}
ddata->p_clk = p_clk;
// 从设备树中读取 PWM 通道
ret = of_property_read_u32(np, "pwm-channel", &pwm_channel);
if (ret) {
dev_err(&pdev->dev, "Failed to get PWM channel from device tree\n");
return ret;
}
pwm_channel %= 4;
ddata->pwm_channel = pwm_channel;
// 获取中断号
irq = platform_get_irq(pdev, 0);
if (irq < 0) {
dev_err(&pdev->dev, "Failed to get IRQ\n");
return irq;
}
ddata->irq = irq;
// 设置驱动数据
platform_set_drvdata(pdev, ddata);
// 请求中断
ret = devm_request_irq(&pdev->dev, irq, rk_pwm_capture, IRQF_NO_SUSPEND, "rk_pwm_capture_irq", ddata);
if (ret) {
dev_err(&pdev->dev, "Failed to request IRQ\n");
return ret;
}
// 启用时钟
ret = clk_prepare_enable(ddata->clk);
if (ret) {
dev_err(&pdev->dev, "Failed to enable PWM clock\n");
return ret;
}
ret = clk_prepare_enable(ddata->p_clk);
if (ret) {
clk_disable_unprepare(ddata->clk);
dev_err(&pdev->dev, "Failed to enable peripheral clock\n");
return ret;
}
// 计算 PWM 频率
freq = clk_get_rate(ddata->clk) / 64;
ddata->pwm_freq_nstime = 1000000000 / freq;
// 注册字符设备
pcdev = &ddata->pwm_cdev;
ret = alloc_chrdev_region(&pcdev->dev_num, 0, 1, "alloc_name");
if (ret < 0) {
dev_err(&pdev->dev, "alloc_chrdev_region error\n");
goto err_alloc_chrdev;
}
printk("alloc_chrdev_region success\n");
pcdev->cdev_test.owner = THIS_MODULE;
cdev_init(&pcdev->cdev_test, &cdev_test_ops);
ret = cdev_add(&pcdev->cdev_test, pcdev->dev_num, 1);
if (ret) {
dev_err(&pdev->dev, "Failed to add cdev\n");
goto err_cdev_add;
}
// 创建设备类
pcdev->class = class_create(THIS_MODULE, "test");
if (IS_ERR(pcdev->class)) {
ret = PTR_ERR(pcdev->class);
dev_err(&pdev->dev, "Failed to create class\n");
goto err_class_create;
}
// 创建设备
pcdev->device = device_create(pcdev->class, NULL, pcdev->dev_num, NULL, "capture");
if (IS_ERR(pcdev->device)) {
ret = PTR_ERR(pcdev->device);
dev_err(&pdev->dev, "Failed to create device\n");
goto err_device_create;
}
rk_pwm_capture_init(ddata->base, ddata->pwm_channel);
return 0;
err_device_create:
class_destroy(pcdev->class);
err_class_create:
cdev_del(&pcdev->cdev_test);
err_cdev_add:
unregister_chrdev_region(pcdev->dev_num, 1);
err_alloc_chrdev:
clk_disable_unprepare(ddata->p_clk);
clk_disable_unprepare(ddata->clk);
return ret;
}
// 驱动移除函数
int capture_remove(struct platform_device *pdev)
{
struct rkxx_capture_drvdata *ddata = platform_get_drvdata(pdev);
struct pwm_capture_cdev *pcdev = &ddata->pwm_cdev;
device_destroy(pcdev->class, pcdev->dev_num);
class_destroy(pcdev->class);
cdev_del(&pcdev->cdev_test);
unregister_chrdev_region(pcdev->dev_num, 1);
clk_disable_unprepare(ddata->p_clk);
clk_disable_unprepare(ddata->clk);
return 0;
}
// 设备树匹配表
const struct of_device_id capture_of_device_id[] = {
{.compatible = "pwm-capture"},
{}
};
// 平台驱动结构体
struct platform_driver capture_platform_driver = {
.driver = {
.name = "pwm-capture",
.of_match_table = capture_of_device_id,
},
.probe = capture_probe,
.remove = capture_remove,
};
// 模块初始化函数
static int __init modulecdev_init(void)
{
return platform_driver_register(&capture_platform_driver);
}
// 模块退出函数
static void __exit modulecdev_exit(void)
{
platform_driver_unregister(&capture_platform_driver);
}
module_init(modulecdev_init);
module_exit(modulecdev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");
rockchip_pwm_capture.h
/* SPDX-License-Identifier: GPL-2.0 */
#ifndef __RKXX_PWM_REMOTECTL_H__
#define __RKXX_PWM_REMOTECTL_H__
#include <linux/input.h>
#include <linux/pwm.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/slab.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/platform_device.h>
/* 最大按键数 */
#define MAX_NUM_KEYS 60
/* 最大PWM捕获数 */
#define PWM_PWR_KEY_CAPURURE_MAX 10
/* PWM寄存器定义 */
#define PWM_REG_CNTR 0x00 /* 计数器寄存器 */
#define PWM_REG_HPR 0x04 /* 周期寄存器 */
#define PWM_REG_LPR 0x08 /* 占空比寄存器 */
#define PWM_REG_CTRL 0x0c /* 控制寄存器 */
#define PWM3_REG_INTSTS 0x10 /* PWM3中断状态寄存器 */
#define PWM2_REG_INTSTS 0x20 /* PWM2中断状态寄存器 */
#define PWM1_REG_INTSTS 0x30 /* PWM1中断状态寄存器 */
#define PWM0_REG_INTSTS 0x40 /* PWM0中断状态寄存器 */
#define PWM3_REG_INT_EN 0x14 /* PWM3中断使能寄存器 */
#define PWM2_REG_INT_EN 0x24 /* PWM2中断使能寄存器 */
#define PWM1_REG_INT_EN 0x34 /* PWM1中断使能寄存器 */
#define PWM0_REG_INT_EN 0x44 /* PWM0中断使能寄存器 */
/* 控制寄存器位定义 */
#define PWM_ENABLE (1 << 0) /* PWM使能 */
#define PWM_DISABLE (0 << 0) /* PWM禁用 */
/* 操作模式 */
#define PWM_MODE_ONESHOT (0x00 << 1) /* 单次模式 */
#define PWM_MODE_CONTINUMOUS (0x01 << 1) /* 连续模式 */
#define PWM_MODE_CAPTURE (0x02 << 1) /* 捕获模式 */
/* 占空比输出极性 */
#define PWM_DUTY_POSTIVE (0x01 << 3) /* 正极性 */
#define PWM_DUTY_NEGATIVE (0x00 << 3) /* 负极性 */
/* 非活动状态输出极性 */
#define PWM_INACTIVE_POSTIVE (0x01 << 4) /* 正极性 */
#define PWM_INACTIVE_NEGATIVE (0x00 << 4) /* 负极性 */
/* 时钟源选择 */
#define PWM_CLK_SCALE (1 << 9) /* 时钟分频 */
#define PWM_CLK_NON_SCALE (0 << 9) /* 无时钟分频 */
#define PWM_CH0_INT (1 << 0)
#define PWM_CH1_INT (1 << 1)
#define PWM_CH2_INT (1 << 2)
#define PWM_CH3_INT (1 << 3)
#define PWM_PWR_KEY_INT (1 << 7)
#define PWM_CH0_POL (1 << 8)
#define PWM_CH1_POL (1 << 9)
#define PWM_CH2_POL (1 << 10)
#define PWM_CH3_POL (1 << 11)
#define PWM_CH0_INT_ENABLE (1 << 0)
#define PWM_CH0_INT_DISABLE (0 << 0)
#define PWM_CH1_INT_ENABLE (1 << 1)
#define PWM_CH1_INT_DISABLE (0 << 1)
#define PWM_CH2_INT_ENABLE (1 << 2)
#define PWM_CH2_INT_DISABLE (0 << 2)
#define PWM_CH3_INT_ENABLE (1 << 3)
#define PWM_CH3_INT_DISABLE (0 << 3)
#define PWM_INT_ENABLE 1
#define PWM_INT_DISABLE 0
/* 预分频因子 */
#define PWMCR_MIN_PRESCALE 0x00
#define PWMCR_MAX_PRESCALE 0x07
#define PWMDCR_MIN_DUTY 0x0001
#define PWMDCR_MAX_DUTY 0xFFFF
#define PWMPCR_MIN_PERIOD 0x0001
#define PWMPCR_MAX_PERIOD 0xFFFF
enum pwm_div {
PWM_DIV1 = (0x0 << 12),
PWM_DIV2 = (0x1 << 12),
PWM_DIV4 = (0x2 << 12),
PWM_DIV8 = (0x3 << 12),
PWM_DIV16 = (0x4 << 12),
PWM_DIV32 = (0x5 << 12),
PWM_DIV64 = (0x6 << 12),
PWM_DIV128 = (0x7 << 12),
};
/* NEC 协议 */
#define RK_PWM_TIME_PRE_MIN 4000
#define RK_PWM_TIME_PRE_MAX 5000
#define RK_PWM_TIME_PRE_MIN_LOW 8000
#define RK_PWM_TIME_PRE_MAX_LOW 10000
#define RK_PWM_TIME_BIT0_MIN 390
#define RK_PWM_TIME_BIT0_MAX 730
#define RK_PWM_TIME_BIT1_MIN 1300
#define RK_PWM_TIME_BIT1_MAX 2000
#define RK_PWM_TIME_BIT_MIN_LOW 390
#define RK_PWM_TIME_BIT_MAX_LOW 730
#define RK_PWM_TIME_RPT_MIN 2000
#define RK_PWM_TIME_RPT_MAX 2500
#define RK_PWM_TIME_SEQ1_MIN 95000
#define RK_PWM_TIME_SEQ1_MAX 98000
#define RK_PWM_TIME_SEQ2_MIN 30000
#define RK_PWM_TIME_SEQ2_MAX 55000
#define PWM_REG_INTSTS(n) ((3 - (n)) * 0x10 + 0x10)
#define PWM_REG_INT_EN(n) ((3 - (n)) * 0x10 + 0x14)
#define RK_PWM_VERSION_ID(n) ((3 - (n)) * 0x10 + 0x2c)
#define PWM_REG_PWRMATCH_CTRL(n) ((3 - (n)) * 0x10 + 0x50)
#define PWM_REG_PWRMATCH_LPRE(n) ((3 - (n)) * 0x10 + 0x54)
#define PWM_REG_PWRMATCH_HPRE(n) ((3 - (n)) * 0x10 + 0x58)
#define PWM_REG_PWRMATCH_LD(n) ((3 - (n)) * 0x10 + 0x5C)
#define PWM_REG_PWRMATCH_HD_ZERO(n) ((3 - (n)) * 0x10 + 0x60)
#define PWM_REG_PWRMATCH_HD_ONE(n) ((3 - (n)) * 0x10 + 0x64)
#define PWM_PWRMATCH_VALUE(n) ((3 - (n)) * 0x10 + 0x68)
#define PWM_PWRCAPTURE_VALUE(n) ((3 - (n)) * 0x10 + 0x9c)
#define PWM_CH_INT(n) BIT(n)
#define PWM_CH_POL(n) BIT(n + 8)
#define PWM_CH_INT_ENABLE(n) BIT(n)
#define PWM_PWR_INT_ENABLE BIT(7)
#define CH3_PWRKEY_ENABLE BIT(3)
/* PWM 数据结构 */
struct pwm_data {
int period_ns; /* 周期(纳秒) */
int duty_ns; /* 占空比(纳秒) */
};
/* PWM 状态枚举 */
typedef enum _RMC_STATE {
RMC_IDLE, /* 空闲状态 */
RMC_IDLE1, /* 空闲状态1 */
RMC_IDLE2, /* 空闲状态2 */
RMC_GETDATA,/* 获取数据状态 */
RMC_DONE, /* 完成状态 */
} eRMC_STATE;
/* PWM 捕获平台数据结构 */
struct RKxx_remotectl_platform_data {
int nbuttons; /* 按钮数 */
int rep; /* 重复 */
int timer; /* 计时器 */
int wakeup; /* 唤醒 */
};
/* PWM 捕获字符设备数据结构 */
struct pwm_capture_cdev {
dev_t dev_num; /* 设备号 */
struct cdev cdev_test; /* 字符设备结构体 */
struct class *class; /* 设备类 */
struct device *device; /* 设备结构体 */
struct rkxx_capture_drvdata *ddata; /* 驱动数据 */
};
/* PWM 捕获驱动数据结构 */
struct rkxx_capture_drvdata {
void __iomem *base; /* 基地址 */
int irq; /* 中断号 */
struct device dev; /* 设备结构体 */
int pwm_freq_nstime; /* PWM 频率(纳秒) */
int pwm_channel; /* PWM 通道 */
int hpr; /* 高电平周期 */
int lpr; /* 低电平周期 */
eRMC_STATE state; /* PWM 状态 */
struct clk *clk; /* 时钟 */
struct clk *p_clk; /* 父时钟 */
struct pwm_capture_cdev pwm_cdev; /* PWM 捕获字符设备 */
struct pwm_data data; /* PWM 数据结构 */
};
/* PWM 中断控制 */
static void rk_pwm_int_ctrl(void __iomem *pwm_base, uint pwm_id, int ctrl)
{
int val;
if (pwm_id > 3)
return; /* 如果 PWM ID 超过 3,直接返回 */
val = readl_relaxed(pwm_base + PWM_REG_INT_EN(pwm_id)); /* 读取当前中断使能状态 */
if (ctrl) {
val |= PWM_CH_INT_ENABLE(pwm_id); /* 设置中断使能 */
writel_relaxed(val, pwm_base + PWM_REG_INT_EN(pwm_id)); /* 写入中断使能寄存器 */
} else {
val &= ~PWM_CH_INT_ENABLE(pwm_id); /* 清除中断使能 */
writel_relaxed(val, pwm_base + PWM_REG_INT_EN(pwm_id)); /* 写入中断使能寄存器 */
}
}
/* 初始化 PWM 捕获 */
static void rk_pwm_capture_init(void __iomem *pwm_base, uint pwm_id)
{
int val;
/* 禁用 PWM */
val = readl_relaxed(pwm_base + PWM_REG_CTRL);
val = (val & 0xFFFFFFFE) | PWM_DISABLE;
writel_relaxed(val, pwm_base + PWM_REG_CTRL);
/* 设置为捕获模式 */
val = readl_relaxed(pwm_base + PWM_REG_CTRL);
val = (val & 0xFFFFFFF9) | PWM_MODE_CAPTURE;
writel_relaxed(val, pwm_base + PWM_REG_CTRL);
/* 设置分频值 */
val = readl_relaxed(pwm_base + PWM_REG_CTRL);
val = (val & 0xFF0001FF) | PWM_DIV64;
writel_relaxed(val, pwm_base + PWM_REG_CTRL);
/* 启用中断 */
rk_pwm_int_ctrl(pwm_base, pwm_id, PWM_INT_ENABLE);
/* 这里可以启用 PWM 捕获(注释掉的代码) */
/*
val = readl_relaxed(pwm_base + PWM_REG_CTRL);
val = (val & 0xFFFFFFFE) | PWM_ENABLE;
writel_relaxed(val, pwm_base + PWM_REG_CTRL);
*/
}
#endif
Makefile:
export ARCH=arm64#设置平台架构
export CROSS_COMPILE=aarch64-none-linux-gnu-#交叉编译器前缀
obj-m += PWM_capture.o #此处要和你的驱动源文件同名
KDIR :=/home/topeet/Linux/rk3588-linux/kernel #这里是你的内核目录
PWD ?= $(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules #make操作
clean:
make -C $(KDIR) M=$(PWD) clean #make clean操作
编写测试APP
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
// 定义 PWM 数据结构体
struct pwm_data {
int period_ns; // PWM 周期,单位纳秒
int duty_ns; // PWM 占空比,单位纳秒
};
int main(int argc, char *argv[]) {
int fd;
struct pwm_data data;
// 打开 PWM 捕获设备文件
fd = open("/dev/capture", O_RDONLY);
if (fd < 0) {
printf("打开设备文件失败\n");
return -1;
}
// 读取 PWM 捕获数据
if (read(fd, &data, sizeof(data)) != sizeof(data)) {
printf("读取数据失败\n");
close(fd);
return -1;
}
// 打印 PWM 捕获数据
printf("period_ns is %d, duty_ns is %d\n", data.period_ns, data.duty_ns);
// 关闭设备文件
close(fd);
return 0;
}