目录
1.前言
之前使用过STM32的开发板,对嵌入式的裸机开发有一定的了解,在此基础上,又认真学习了嵌入式linux开发的过程。本文以字符设备驱动为例,从设备树和内核这一层面介绍简单led驱动开发过程。特备感谢韦东山老师《嵌入式Linux应用开发完全手册》。
2.驱动开发流程
(1)确定主设备号,也可以让内核分配;
(2) 定义file_operations结构体,用于绑定内核来操作你的硬件;
(3)把驱动程序中的drv_open/drv_read/drv_write等函数,填入file_operations结构体;
(4) 把file_operations结构体注册进内核;
(5)实现入口函数:安装驱动程序时,就会去调用这个入口函数;
(6)实现出口函数:卸载驱动程序时,出口函数调用unregister_chrdev;
(7)其他完善:提供设备信息,自动创建设备节点。
3.设备树
对于板级资源,需要加载硬件资源的头文件,每当更换一次GPIO,就必须修改代码,重新编译和加载驱动,这会在Linux内核当中留下大量残余文件。于是,设备树的出现就解决了这一问题。设备树,即给内核当中的驱动指定硬件资源。
下面是我编写的qemu的设备树文件
首先找到设备树文件存放位置
book@100ask:~/100ask_imx6ull-qemu/linux-4.9.88/arch/arm/boot/dts$
打开设备树文件, vi 100ask_imx6ull_qemu.dts,这里我之前已经指定好了qemu中所有的led资源,注意设备树文件编写的格式。
100ask_led@0 {
compatible = "100as,leddrv";
pin = <GROUP_PIN(5, 3)>;
};
100ask_led@1 {
compatible = "100as,leddrv";
pin = <GROUP_PIN(1, 3)>;
};
100ask_led@2 {
compatible = "100as,leddrv";
pin = <GROUP_PIN(1, 5)>;
};
100ask_led@3 {
compatible = "100as,leddrv";
pin = <GROUP_PIN(1, 6)>;
};
这里编写的时候也可以用到GPIO和Pinctrl子系统概念,由于是简单led,不再赘述。
之后就需要重新编译设备树文件,并把它放在qemu目录下。
随后,可以在~/ubuntu-18.04_imx6ul_qemu_system/ imx6ull-system-image下看到该设备树文件
4.驱动程序编写
1.确定主设备号
一般初始化为0,也可以让内核来分配。
/* 1. 确定主设备号 */
static int major = 0;
2.定义file_operations结构体
static struct file_operations led_drv = {
.owner = THIS_MODULE,
.open = qemu_led_drv_open,
.read = qemu_led_drv_read,
.write = qemu_led_drv_write,
.release = qemu_led_drv_close,
};
3.完善file_operations结构体
因为是点亮led灯,需要从用户态往内核写数据,主要完善write函数。
static ssize_t qemu_led_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
int err;
char status;
struct inode *inode = file_inode(file); //转换设备节点
int minor = iminor(inode); //得到次设备号
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
err = copy_from_user(&status, buf, 1);
p_led_opr->ctl(minor,status); //led控制函数
return 0;
}
根据用户态的指令,还需要初始化硬件
static ssize_t led_drv_open(struct inode * node, struct file * file)
{
int minor = iminor(node);
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
p_led_opr->init(minor); //根据次设备号,初始化该组GPIO
return 0;
}
以下代码为指定硬件资源的初始化和控制函数,放在chip_led.c文件。
static int board_led_init(int which)
{
printk("init gpio: group %d, pin %d\n", GROUP(g_ledpins[which]), PIN(g_ledpins[which]));
if (!CCM_CCGR1)
{
CCM_CCGR1 = ioremap(0x20C406C, 4);
}
switch(GROUP(g_ledpins[which]))
{
case 5:{
/* 1. enable GPIO5
* CG15, b[31:30] = 0b11
*/
*CCM_CCGR1 |= (3<<30);
/* GPIO5 IOMUXC*/
IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = ioremap(0x2290014, 4);
gpio5 = ioremap(0x20AC000, sizeof(struct imx6ull_gpio));
switch(PIN(g_ledpins[which]))
{
case 3:
{
*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = 5;
gpio5->gdir |= (1<<3);
break;
}
default : break;
}
break;
}
case 1:{
/* 1. enable GPIO1
* CG13, b[27:266] = 0b11
*/
*CCM_CCGR1 |= (3<<26);
iomux = ioremap(0x20e0000, sizeof(struct iomux)); //gpio1 iomux baseaddr
gpio1 = ioremap(0x209C000, sizeof(struct imx6ull_gpio));
switch(PIN(g_ledpins[which]))
{
case 3:
{
iomux->IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 = 5;
gpio1->gdir |= (1<<3);
break;
}
case 5:
{
iomux->IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO05 = 5;
gpio1->gdir |= (1<<5);
break;
}
case 6:
{
iomux->IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO06 = 5;
gpio1->gdir |= (1<<6);
break;
}
default : break;
}
break;
}
default : break;
}
return 0;
}
static int board_led_ctl(int which,char status)
{
printk("%s %s line %d, led %d, %s\n", __FILE__, __FUNCTION__, __LINE__, which, status ? "on" : "off");
switch(GROUP(g_ledpins[which]))
{
case 1:
{
switch(PIN(g_ledpins[which]))
{
case 3:
{
if (status) /* on : output 0 */
gpio1->dr &= ~(1<<3);
else /* on : output 1 */
gpio1->dr |= (1<<3);
break;
}
case 5:
{
if (status) /* on : output 0 */
gpio1->dr &= ~(1<<5);
else /* on : output 1 */
gpio1->dr |= (1<<5);
break;
}
case 6:
{
if (status) /* on : output 0 */
gpio1->dr &= ~(1<<6);
else /* on : output 1 */
gpio1->dr |= (1<<6);
break;
}
default : break;
}
break;
}
case 5:
{
switch(PIN(g_ledpins[which]))
{
case 3:
{
if (status) /* on : output 0 */
gpio5->dr &= ~(1<<3);
else /* on : output 1 */
gpio5->dr |= (1<<3);
break;
}
}
break;
}
default : break;
}
return 0;
}
接下来,就是重要的设备树解析:内核解析dtb文件,把每一个节点都转换为device_node结构体;对于某些device_node结构体,会被转换为platform_device结构体。这样就可以得到硬件资源。
static int chip_demo_gpio_probe(struct platform_device *pdev)
{
struct device_node *np;
int err = 0;
int led_pin;
np = pdev->dev.of_node;
if (!np)
return -1;
err = of_property_read_u32(np, "pin", &led_pin);
g_ledpins[g_ledcnt] = led_pin;
led_class_create_device(g_ledcnt);
g_ledcnt++;
return 0;
}
4.注册file_operations结构体
major = register_chrdev(0,"100ask_led", &led_drv);
5.实现入口函数
static int __init qemu_led_init(void)
{
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
major = register_chrdev(0,"qemu_100ask_led", &led_drv);
led_class = class_create(THIS_MODULE, "100ask_led_class");
err = PTR_ERR(led_class);
if(IS_ERR(led_class))
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "qemu_100ask_led");
return -1;
}
return 0;
}
6.实现出口函数
static void __exit led_exit(void)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
class_destroy(led_class);
unregister_chrdev(major, "qemu_100ask_led");
}
7.完善其他信息
创建设备节点和用户操作函数
void led_class_create_device(int minor)
{
device_create(led_class, NULL, MKDEV(major,minor), NULL, "qemu_100ask_led%d",minor);
}
void led_class_destroy_device(int minor)
{
device_destroy(led_class, MKDEV(major,minor));
}
void register_led_operations(struct led_operations *opr)
{
p_led_opr = opr;
}
EXPORT_SYMBOL(led_class_create_device);
EXPORT_SYMBOL(led_class_destroy_device);
EXPORT_SYMBOL(register_led_operations);
5.实验
(1)在此之前,需要编写Makefile文件。我的理解是,make可以把驱动编译链接为linux内核可以识别的模块。
KERN_DIR = /home/book/100ask_imx6ull-qemu/linux-4.9.88
#配置内核
all:
make -C $(KERN_DIR) M=`pwd` modules
$(CROSS_COMPILE)gcc -o ledtest ledtest.c
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
rm -f ledtest
bj-m += leddrv.o chip_led.o
编辑好之后make一下
把chip_led.ko、leddrv.ko、ledtest放入nfs文件系统。
book@100ask:~/winFile/qemu_bus_led_devicetree$ cp ledtest chip_led.ko leddrv.ko ~/nfs_rootfs/
(2)完成以上工作之后,启动qemu,挂载ubantu目录。
mount -t nfs -o nolock,vers=3 10.0.2.2:/home/book/nfs_rootfs /mnt
#####################
加载驱动时一定要注意依赖关系,不然会报错。
#####################
(3)加载驱动,如下所示:
加载chip_led.ko时显示忙,可能是我电脑原因,但实际上加载成功了。
(4)查看设备树指定的硬件资源已经转换为设备节点,用于验证驱动是否正确。红框里已经标注,确实有四个设备节点。
(5)点亮led灯
注意四个led灯全是熄灭状态。
开始点亮第一个led灯 ./ledtest /dev/100ask_led0 on
6. 结束语
本文以一个简单的led为实验对象,描述了基本的驱动开发流程,并最终验证了驱动程序编写的正确性。
文中只给出了一部分核心的代码用于介绍驱动开发的步骤,且这部分代码写于我研一初始嵌入式Linux,有不足之处还请多多体谅。