20.2 设备树中的 platform 驱动编写
一、设备树下的 platform 驱动
platform 驱动框架分为总线、设备和驱动,总线不需要我们去管理,这个是 Linux 内核提供。在有了设备树的前提下,我们只需要实现 platform_driver 即可。
1. 修改 pinctrl-stm32.c 文件
先复习一下 pinctrl 子系统和 gpio子系统,pinctrl 子系统是在设备树中去配置 pin 的信息和电气属性(复用、上/下拉,速度等),gpio 子系统是初始化 gpio,比如设置 gpio 的输入输出,读取 gpio 的值等。
这ST 针对 STM32MP1 提供的 Linux 系统中,其 pinctrl 的前提是在 platform 平台下引用,一切以使用的芯片为准。在使用 pinctrl 的时候需要先修改 pinctrl-stm32.c
文件,否则当某个引脚作为 GPIO 的时候是无法申请到的。
首先进入 /linux/atk-mpl/linux/my_linux/linux-5.4.31/drivers/pinctrl/stm32 文件夹下,打开 pinctrl-stm32.c
,修改文件:
/* 865行开始 */
static const struct pinmux_ops stm32_pmx_ops = {
.get_functions_count = stm32_pmx_get_funcs_cnt,
.get_function_name = stm32_pmx_get_func_name,
.get_function_groups = stm32_pmx_get_func_groups,
.set_mux = stm32_pmx_set_mux,
.gpio_set_direction = stm32_pmx_gpio_set_direction,
.strict = false, // 这里原本是 true
// 这里设置false就是不采用严格模式
};
修改完成重新编译 Linux 内核:
make uImage LOADADDR=0XC2000040 -j16
/*
uImage代表了要编译出来的内核镜像的格式
LOADADDR=0XC2000040是指定内核加载到的起始地址
*/
把 uImage 复制:
cd arch/arm/boot/
sudo cp uImage /home/alientek/linux/tftpboot/ -f
测试一下看是否成功打开 uImage:
2. 创建设备的 pinctrl 节点
进入 /linux/atk-mpl/linux/my_linux/linux-5.4.31/arch/arm/boot/dts 文件夹下,打开 stm32mp15-pinctrl.dtsi
文件,之后 STM32MP1 的所有引脚都是在 pinctrl 文件里配置完成。pinctrl 节点加入以下内容:
3. 在设备树中创建设备节点
这里面重点是配置 compatible 属性值,因为 platform 总线需要通过设备节点中的 compatible 属性值来匹配驱动。打开 /linux/atk-mpl/linux/my_linux/linux-5.4.31/arch/arm/boot/dts 文件夹下的 stm32mp157d-atk.dts:
4. 编写 platform 驱动的时候注意兼容属性
使用设备树的时候,platform 驱动会通过 of_match_table 来保存兼容性值,表明此驱动兼容哪些设备。下面是 platform_driver 例程:
static const struct of_device_id led_of_match[] = {
{ .compatible = "alientek,led" }, /* 兼容属性 */ // 这里面的每个元素都是兼容属性,表示兼容的设备
{ /* Sentinel */ } // 注意最后一个元素为空!!!
};
MODULE_DEVICE_TABLE(of, led_of_match); // 声明led_of_match设备匹配表
// 内核模块可以将 led_of_match 数组注册到内核中
static struct platform_driver led_platform_driver = {
.driver = {
.name = "stm32mp1-led",
.of_match_table = led_of_match,
},
.probe = led_probe,
.remove = led_remove,
};
二、检查引脚复用配置
1. 检查引脚 pinctrl 配置
在嵌入式 Linux 下,严格按照一个引脚对应一个功能设计硬件。比如 PIO 现在用作 GPIO 来驱动 LED,那么就不能把 PI0 作为其他的功能。开发版上是将 PI0 连接到了 LED0,但是 ST 官方是这样:
这里 ST 官方是把 PI0 设置为 LCD_G5,模式为端口复用,现在只能一个 IO 复用为一个功能,所以这里需要屏蔽掉。
继续往下看:
这里 ST 官方是把 PI0 设置为 LCD_G5,模式为模拟输入模式,所以也要屏蔽掉。
2. 检查 gpio 占用
上一节检查 PI0 引脚有没有被复用多个设备。当我们将一个引脚作为 GPIO 的时候,一定要检查当前设备树里面是否有其他设备也用到了这个 GPIO,保证设备树中只有一个设备树在使用这个 GPIO。
三、实验程序编写
首先就要修改设备树文件,设备树里面加上我们需要的设备信息。需要创建 LED0 引脚的 pinctrl 节点,另外需要创建的 LED0 的设备节点。虽然之前完成了,但是没有编译:
cd linux/atk-mpl/linux/my_linux/linux-5.4.31
make dtbs -j32
# 将编译好的stm32mp157d-atk.dtb文件复制到
sudo cp stm32mp157d-atk.dtb /home/alientek/linux/tftpboot/ -f
1. platform 驱动程序编写
在 cd linux/atk-mpl/Drivers/ 文件夹下,新建 18_dtsplatform,在里面新建 Vscode 工作区,并新建 leddriver.c 文件,并输入:
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/irq.h>
#include <linux/wait.h>
#include <linux/poll.h>
#include <linux/fs.h>
#include <linux/fcntl.h>
#include <linux/platform_device.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#define LEDDEV_CNT 1 /* 设备号长度 */
#define LEDDEV_NAME "dtsplatled" /* 设备名字 */
#define LEDOFF 0
#define LEDON 1
/* leddev设备结构体 */
struct leddev_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
struct device_node *node; /* LED设备节点 */
int gpio_led; /* LED灯GPIO标号 */
};
struct leddev_dev leddev; /* led设备 */
/*
* @description : LED打开/关闭
* @param - sta : LEDON(0) 打开LED,LEDOFF(1) 关闭LED
* @return : 无
*/
void led_switch(u8 sta)
{
if (sta == LEDON)
gpio_set_value(leddev.gpio_led, 0);
else if (sta == LEDOFF)
gpio_set_value(leddev.gpio_led, 1);
}
static int led_gpio_init(struct device_node *nd)
{
int ret;
/* 从设备树中获取GPIO */
leddev.gpio_led = of_get_named_gpio(nd, "led-gpio", 0);
if(!gpio_is_valid(leddev.gpio_led)) {
printk(KERN_ERR "leddev: Failed to get led-gpio\n");
return -EINVAL;
}
/* 申请使用GPIO */
ret = gpio_request(leddev.gpio_led, "LED0");
if (ret) {
printk(KERN_ERR "led: Failed to request led-gpio\n");
return ret;
}
/* 将GPIO设置为输出模式并设置GPIO初始电平状态 */
gpio_direction_output(leddev.gpio_led,1);
return 0;
}
/*
* @description : 打开设备
* @param - inode : 传递给驱动的inode
* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
* 一般在open的时候将private_data指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int led_open(struct inode *inode, struct file *filp)
{
return 0;
}
/*
* @description : 向设备写数据
* @param - filp : 设备文件,表示打开的文件描述符
* @param - buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
int retvalue;
unsigned char databuf[1];
unsigned char ledstat;
retvalue = copy_from_user(databuf, buf, cnt);
if(retvalue < 0) {
printk("kernel write failed!\r\n");
return -EFAULT;
}
ledstat = databuf[0];
if (ledstat == LEDON) {
led_switch(LEDON);
} else if (ledstat == LEDOFF) {
led_switch(LEDOFF);
}
return 0;
}
/* 设备操作函数 */
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.write = led_write,
};
/*
* @description : flatform驱动的probe函数,当驱动与
* 设备匹配以后此函数就会执行
* @param - dev : platform设备
* @return : 0,成功;其他负值,失败
*/
static int led_probe(struct platform_device *pdev) // 如果设备和驱动匹配成功,先去初始化pinctrl配置的IO,再去执行probe函数
{
int ret;
printk("led driver and device was matched!\r\n");
/* 初始化 LED */
ret = led_gpio_init(pdev->dev.of_node); // of_node连接着设备树的设备节点
if(ret < 0)
return ret;
/* 1、申请设备号 */
ret = alloc_chrdev_region(&leddev.devid, 0, LEDDEV_CNT, LEDDEV_NAME);
if(ret < 0) {
pr_err("%s Couldn't alloc_chrdev_region, ret=%d\r\n", LEDDEV_NAME, ret);
goto free_gpio;
}
/* 2、初始化cdev */
leddev.cdev.owner = THIS_MODULE;
cdev_init(&leddev.cdev, &led_fops);
/* 3、添加一个cdev */
ret = cdev_add(&leddev.cdev, leddev.devid, LEDDEV_CNT);
if(ret < 0)
goto del_unregister;
/* 4、创建类 */
leddev.class = class_create(THIS_MODULE, LEDDEV_NAME);
if (IS_ERR(leddev.class)) {
goto del_cdev;
}
/* 5、创建设备 */
leddev.device = device_create(leddev.class, NULL, leddev.devid, NULL, LEDDEV_NAME);
if (IS_ERR(leddev.device)) {
goto destroy_class;
}
return 0;
destroy_class:
class_destroy(leddev.class);
del_cdev:
cdev_del(&leddev.cdev);
del_unregister:
unregister_chrdev_region(leddev.devid, LEDDEV_CNT);
free_gpio:
gpio_free(leddev.gpio_led);
return -EIO;
}
/*
* @description : platform驱动的remove函数,移除platform驱动的时候此函数会执行
* @param - dev : platform设备
* @return : 0,成功;其他负值,失败
*/
static int led_remove(struct platform_device *dev)
{
gpio_set_value(leddev.gpio_led, 1); /* 卸载驱动的时候关闭LED */
gpio_free(leddev.gpio_led); /* 注销GPIO */
cdev_del(&leddev.cdev); /* 删除cdev */
unregister_chrdev_region(leddev.devid, LEDDEV_CNT); /* 注销设备号 */
device_destroy(leddev.class, leddev.devid); /* 注销设备 */
class_destroy(leddev.class); /* 注销类 */
return 0;
}
/* 匹配列表(设备树才有的)*/ //
// 这里的省略了获取compatible属性值,因为这里的匹配列表直接和设备匹配
static const struct of_device_id led_of_match[] = {
{ .compatible = "alientek,led" },
{ /* Sentinel */ }
};
MODULE_DEVICE_TABLE(of, led_of_match); // 声明 led_of_match
/* platform驱动结构体 */
static struct platform_driver led_driver = {
.driver = {
/* 设置这个 platform 驱动的名字为“stm32mp1-led”后,当驱动加载成功以后就会在
/sys/bus/platform/drivers/目录下存在一个名为“stm32mp1-led”的文件 */
.name = "stm32mp1-led", /* 驱动名字,用于和设备匹配 */
.of_match_table = led_of_match, /* 设备树匹配表 */
},
.probe = led_probe,
.remove = led_remove,
};
/*
* @description : 驱动模块加载函数
* @param : 无
* @return : 无
*/
static int __init leddriver_init(void)
{
return platform_driver_register(&led_driver); // 注册platform平台
}
/*
* @description : 驱动模块卸载函数
* @param : 无
* @return : 无
*/
static void __exit leddriver_exit(void)
{
platform_driver_unregister(&led_driver); // 卸载platform平台
}
module_init(leddriver_init);
module_exit(leddriver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
MODULE_INFO(intree, "Y");
2. 测试 APP 编写
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#define LEDOFF 0
#define LEDON 1
/*
* @description : main主程序
* @param - argc : argv数组元素个数
* @param - argv : 具体参数
* @return : 0 成功;其他 失败
*/
int main(int argc, char *argv[])
{
int fd, retvalue;
char *filename;
unsigned char databuf[1];
if(argc != 3){
printf("Error Usage!\r\n");
return -1;
}
filename = argv[1];
/* 打开led驱动 */
fd = open(filename, O_RDWR);
if(fd < 0){
printf("file %s open failed!\r\n", argv[1]);
return -1;
}
databuf[0] = atoi(argv[2]); /* 要执行的操作:打开或关闭 */
retvalue = write(fd, databuf, sizeof(databuf));
if(retvalue < 0){
printf("LED Control Failed!\r\n");
close(fd);
return -1;
}
retvalue = close(fd); /* 关闭文件 */
if(retvalue < 0){
printf("file %s close failed!\r\n", argv[1]);
return -1;
}
return 0;
}
四、运行测试
编写 Makefile 文件:
KERNELDIR := /home/alientek/linux/atk-mpl/linux/my_linux/linux-5.4.31
CURRENT_PATH := $(shell pwd)
obj-m := leddriver.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
编译 leddriver.c 和 ledApp.c 文件:
make -j32
arm-none-linux-gnueabihf-gcc ledApp.c -o ledApp
将编译好的 ledApp 和 leddriver.ko 文件复制:
sudo cp ledApp leddriver.ko /home/alientek/linux/nfs/rootfs/lib/modules/5.4.31/ -f
开启开发板,输入一下命令:
cd lib/modules/5.4.31/
depmod
modprobe leddriver.ko
之后就会看到这样的消息:
在设备节点中有 gpioled,在驱动文件中有 stm32mp1-led 驱动文件,设备和驱动匹配成功后才会出现上图中的消息。
测试 App:
./ledApp /dev/dtsplatled 1 // 打开LED
./ledApp /dev/dtsplatled 0 // 关闭LED
/*
有一点忘记 dtsplatled 怎么来的,复习了一下是在最开始创建设备名字,
在创建类,创建设备用到了,所以名字为 dtsplatled
*/
卸载驱动:rmmod leddriver.ko
总结
这一节是在设备树下使用platform,先是修改了 pinctrl-stm32.c 文件,这个是根据具体的芯片来修改这个的,这里修改了严格模式。之后创建设备的 pinctrl 节点,这里是配置io口和电气设置的。然后在设备树中创建设备节点,这里面需要添加 pinctrl-names 和 pinctrl-0。最后要检查 引脚 pinctrl 的配置和 gpio 占用情况,因为 Linux 下必须严格按照一个引脚对应一个功能设计硬件。
程序中:
① 把之前的驱动入口函数(驱动模块加载)转移到了 probe 函数中,将驱动出口函数(驱动模块卸载)转移到了 remove 函数;
② 因为有设备树的前提下,新增匹配列表,这里面最重要的就是匹配设备的 compatible,注意最后一行需要留空,类似下图:
③ 上一节新增的 platform 驱动结构体,.driver里面成员变量 .name = XXX 和 .of_match_table,.name 在驱动和设备匹配成功后会在 /sys/bus/platform/drivers/目录下新建一个名为"XXX" 的文件夹,.of_match_table 是让之前读取 status 属性等获取设备树代码没有的关键。