2019-01-19 19:14:38 m0_37542524 阅读数 165

1, 驱动和应用程序的设计思想

1.1,应用程序和驱动扮演的是什么角色

用户态:应用程序
	    玩策略: 怎么去做
				1, 一闪一闪
				2,10s闪一次,也可以1s闪一次
				3,一直亮
				4,跑马灯
		控制权是在应用程序(程序员)
--------------------------------------
内核态:驱动
		玩机制: 能做什么 
				led:亮 和 灭

2,编写字符设备驱动的步骤和规范

2.1,步骤:

	1,实现模块加载和卸载入口函数
			module_init(chr_dev_init);
			module_exit(chr_dev_exit);
	
	2,在模块加载入口函数中
		a,申请主设备号  (内核中用于区分和管理不同字符设备)
				 register_chrdev(dev_major, "chr_dev_test", &my_fops);

		b,创建设备节点文件 (为用户提供一个可操作到文件接口--open())
				struct  class *class_create(THIS_MODULE, "chr_cls");
				struct  device *device_create(devcls, NULL, MKDEV(dev_major, 0), NULL, "chr2");

		c, 硬件的初始化
			   1,地址的映射
					gpx2conf = ioremap(GPX2_CON, GPX2_SIZE);
			   2,中断到申请
			   3,实现硬件的寄存器的初始化
					// 需要配置gpio功能为输出
					*gpx2conf &= ~(0xf<<28);
					*gpx2conf |= (0x1<<28);
		e,实现file_operations
				const struct file_operations my_fops = {
						.open = chr_drv_open,
						.read = chr_drv_read,
						.write = chr_drv_write,
						.release = chr_drv_close,
				};

2.2,规范:

	1,面向对象编程思想
		用一个结构体来表示一个对象

		//设计一个类型,描述一个设备的信息
		struct led_desc{
			unsigned int dev_major; //设备号
			struct class *cls;
			struct device *dev; //创建设备文件
			void *reg_virt_base;
		};

		struct led_desc *led_dev;//表示一个全局的设备对象

		
		// 0(在init中第一步做), 实例化全局的设备对象--分配空间
		//  GFP_KERNEL 如果当前内存不够用到时候,该函数会一直阻塞(休眠)
		//  #include <linux/slab.h>
		led_dev = kmalloc(sizeof(struct led_desc), GFP_KERNEL);
		if(led_dev == NULL)
		{
			printk(KERN_ERR "malloc error\n");
			return -ENOMEM;
		}

		led_dev->dev_major = 250;

	2,做出错处理
			在某个位置出错了,要将之前申请到资源进行释放
	
		led_dev = kmalloc(sizeof(struct led_desc), GFP_KERNEL);	
	
		led_dev->dev_major = register_chrdev(0, "led_dev_test", &my_fops);
		if(led_dev->dev_major < 0)
		{
			printk(KERN_ERR "register_chrdev error\n");
			ret = -ENODEV;
			goto err_0;
		}


		err_0:
			kfree(led_dev);
			return ret;

3,操作寄存器地址的方式 readl/writel():

3.1,传统的方式

	volatile unsigned long *gpxcon;
	*gpxcon &= ~(0xf<<28);

3.2,内核提供的方式

readl/writel();
	u32 readl(const volatile void __iomem *addr) 	//从地址中读取地址空间的值

	void writel(unsigned long value , const volatile void __iomem *addr)		// 将value的值写入到addr地址

	例子1:
		// gpio的输出功能的配置
		u32 value = readl(led_dev->reg_virt_base);
		value &= ~(0xf<<28);
		value |= (0x1<<28);
		writel(value, led_dev->reg_virt_bas);	

	例子2:
			*gpx2dat |= (1<<7);
		替换成:
			writel( readl(led_dev->reg_virt_base + 4) | (1<<7),   led_dev->reg_virt_base + 4 );

4,例—LED灯闪烁

4.1,驱动代码 led_drv.c

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/slab.h>

#include <uapi/asm-generic/errno-base.h>

//设计一个类型,描述一个设备的信息
struct led_desc{
	unsigned int dev_major; 	//主设备号
	struct class *cls;
	struct device *dev; 	//创建设备文件
	void *reg_virte_base; 	//存放虚拟地址的首地址(寄存器地址的基准值)
};

//物理地址
//#define GPX2_CON 	(*(volatile unsigned int *)0x11000C40)  //此处是裸机驱动开发时的用法,此处不能用
#define GPX2_CON 0x11000C40
//GPX2_DATA就是GPX2_CON + 4
#define GPX2_SIZE 8

struct led_desc *led_dev; 	//申明设备对象

static int kernel_val = 555;

ssize_t led_drv_read(struct file *filep, char __user *buf, size_t count, loff_t *fpos)
{
	int ret;
	
	printk("---------%s-------------\n",__FUNCTION__);

	//从内核空间拷贝数据到用户空间
	ret = copy_to_user(buf, &kernel_val, count);
	if(ret > 0)
	{
		printk("copy_to_user error\n");
		return -EFAULT;			
	}
	
	return 0;
}
ssize_t led_drv_write(struct file *filep, const char __user *buf, size_t count, loff_t *fops)
{
	int ret;
	int value;
	
	printk("---------%s-------------\n",__FUNCTION__);

	//从用户空间拷贝数据到内核空间
	ret = copy_from_user(&value, buf, count);
	if(ret > 0)
	{
		printk("copy_from_user error\n");
		return -EFAULT;			
	}

	//控制GPX2_7 I/O口电平变化
	if(value)
	{
		writel((readl(led_dev->reg_virte_base + 4) | (0x1 << 7)), led_dev->reg_virte_base + 4);
	}
	else
	{
		writel((readl(led_dev->reg_virte_base + 4) & ~(0x1 << 7)), led_dev->reg_virte_base + 4);
	}
	
	return 0;
}
int led_drv_open(struct inode *inode, struct file *filep)
{
	printk("---------%s-------------\n",__FUNCTION__);
	
	return 0;
}
int led_drv_close(struct inode *inode, struct file *filep)
{
	printk("---------%s-------------\n",__FUNCTION__);
	
	return 0;
}

const struct file_operations my_fops = {
	.open = led_drv_open,
	.read = led_drv_read,
	.write = led_drv_write,
	.release = led_drv_close,
};

static int __init led_dev_init(void)
{
	int ret;
	
	printk("---------%s-------------\n",__FUNCTION__);

	//实例化全局的设备对象---分配空间
	led_dev = kmalloc(sizeof(struct led_desc), GFP_KERNEL);
	if(led_dev == NULL)
	{
		printk(KERN_ERR "malloc error\n");
		return -ENOMEM;
	}

	//装载一般都是申请设备号资源
	//申请主设备号

#if 0	//静态申请主设备号
	led_dev->dev_major = 250;
	ret = register_chrdev(led_dev->dev_major, "led_dev_test", &my_fops);
	if(ret == 0){
		printk("register ok\n");
	}
	else{
		printk("register failed\n");
		ret = -EINVAL;
		goto err_0;
	}
#else 	//动态态申请主设备号
	led_dev->dev_major = register_chrdev(0, "led_dev_test", &my_fops);
	if(led_dev->dev_major < 0){
		printk(KERN_ERR "register_chrdev error\n");
		ret = -ENODEV;
		goto err_0;
	}
	else{
		printk("register ok\n");
	}
#endif

	//自动创建设备节点
	led_dev->cls = class_create(THIS_MODULE, "led_cls");
	if(IS_ERR(led_dev->cls))
	{
		printk(KERN_ERR "class_create error\n");
		ret = PTR_ERR(led_dev->cls); 	//将指针出错的具体原因转换成一个出错码
		goto err_1;
	}
	led_dev->dev = device_create(led_dev->cls,NULL,MKDEV(led_dev->dev_major, 0),NULL,"led%d",0); 	// 	/dev/led0
	if(IS_ERR(led_dev->dev))
	{
		printk(KERN_ERR "device_create error\n");
		ret = PTR_ERR(led_dev->dev); 	//将指针出错的具体原因转换成一个出错码
		goto err_2;
	}

	//地址映射
	led_dev->reg_virte_base = ioremap(GPX2_CON, GPX2_SIZE);
	if(led_dev->reg_virte_base == NULL)
	{
		printk(KERN_ERR "ioremap error\n");
		ret = -ENOMEM;
		goto err_3;
	}

	//配置GPIO的功能为输出
#if 0
	u32 value = readl(led_dev->reg_virte_base);
	value = (value & ~(0xf << 28)) | (0x1 << 28);
	writel(value, led_dev->reg_virte_base);
#else
	writel(((readl(led_dev->reg_virte_base) & ~(0xf << 28)) | (0x1 << 28)), led_dev->reg_virte_base);
#endif
	return 0;
	
err_3:
	device_destroy(led_dev->cls,MKDEV(led_dev->dev_major, 0));
err_2:
	class_destroy(led_dev->cls);
err_1:
	unregister_chrdev(led_dev->dev_major,"led_dev_test");	
err_0:
	kfree(led_dev);	
	return ret;
	
}

static void __exit led_dev_exit(void)
{
	printk("---------%s-------------\n",__FUNCTION__);
	
 	//卸载一般都是释放资源

	//去映射
	iounmap(led_dev->reg_virte_base);

	//销毁节点和类
	device_destroy(led_dev->cls,MKDEV(led_dev->dev_major, 0));
	class_destroy(led_dev->cls);

	//释放主设备号
 	unregister_chrdev(led_dev->dev_major,"led_dev_test");	

	//释放动态内存
 	kfree(led_dev);	
}

module_init
(led_dev_init);
module_exit(led_dev_exit);
MODULE_LICENSE("GPL");

4.2,应用程序 led_test.c

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
	//调用驱动
	int fd;
	int value = 0;

	fd = open("/dev/led0", O_RDWR);
	if(fd < 0)
	{
		perror("led_test open");
		exit(1);
	}
	
	read(fd,&value,4);
	printf("__USER__ : value = %d\n",value);

	while(1)
	{
		value = 1;
		write(fd, &value, 4);
		sleep(1);

		
		value = 0;
		write(fd, &value, 4);
		sleep(1);
	}

	close(fd);

	return 0;
}

4.3,Makefile

ROOTFS_DIR = /nfs/rootfs#根文件系统路径

APP_NAME = led_test
MODULE_NAME = led_drv
CROSS_COMPILE = arm-none-linux-gnueabi-
CC = $(CROSS_COMPILE)gcc

ifeq ($(KERNELRELEASE),)

KERNEL_DIR = /home/linux/linux-3.14.79  		#编译过的内核源码的路径
CPU_DIR = $(shell pwd) 	#当前路径

all:
	make -C $(KERNEL_DIR) M=$(CPU_DIR) modules  #把当前路径编成modules
	@#make -C 进入到内核路径
	@#M 指定当前路径
	$(CC) $(APP_NAME).c -o $(APP_NAME)

clean:
	make -C $(KERNEL_DIR) M=$(CPU_DIR) clean
	rm $(APP_NAME)

install:
	sudo cp -raf *.ko $(APP_NAME) $(ROOTFS_DIR)/drv_module 	#把当前的所有.ko文件考到根文件系统的drv_module目录

else

obj-m += $(MODULE_NAME).o  #指定内核要将哪个文件编译成ko

endif

4.4,串口终端信息

在这里插入图片描述

2020-03-03 21:32:16 weixin_43072093 阅读数 10

一、创建工程

1、知识前提

​ 一般编写驱动很少直接操作寄存器来初始化外设,但是不排除要调试时要读取或置位寄存器的某些位。
​ 在linux系统下操作寄存器不能直接按照芯片手册的寄存器物理地址来操作,因为linux内核启动的时候会初始化MMU,MMU的作用是将物理内存和虚拟内存映射起来,也就是地址映射:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0BQGUbev-1583242316335)(C:\Users\10309\AppData\Roaming\Typora\typora-user-images\image-20200303124904617.png)]

​ 对于32位处理器来说,虚拟地址范围是232 = 4GB,而开发板上的物理内存不一定达到4GB,因此物理地址和虚拟地址不是逐一对应的,不同的虚拟地址可能映射到同一个物理地址中。而linux内核启动后CPU访问的都是虚拟地址,因此直接访问寄存器的物理地址是操作不了寄存器的。必须得到寄存器物理地址对应的虚拟地址,使用 ioremapiounmap 两个函数可以确定虚拟地址和物理地址之间关系。

ioremap函数

​ ioremap函数用于获取指定物理地址空间对应的虚拟地址空间,定义在linux内核源码arch/arm/include/asm/io.h文件中,调用前要添加头文件。函数定义如下:

#define ioremap(cookie,size) __arm_ioremap((cookie), (size), MT_DEVICE) //函数宏定义
void __iomem * __arm_ioremap(phys_addr_t phys_addr, size_t size, unsigned int mtype) //函数定义
{
    return arch_ioremap_caller(phys_addr, size, mtype, __builtin_return_address(0));
}

​ ioremap是个宏,有两个参数:
​ cookie:要映射的物理起始地址
​ size:要映射的内存空间大小(单位:字节),可对指定内存块进行操作。
​ 返回值:__iomem类型的指针,指向映射后的虚拟空间首地址

iounmap函数

​ 卸载驱动时需要使用iounmap函数来释放ioremap函数所做的映射。函数原型如下:

void iounmap (volatile void __iomem *addr)

​ addr:取消映射的虚拟地址空间首地址。

io内存访问函数

​ linux内核不建议直接通过地址指针来访问内存,提供一些读写函数来访问内存。
读操作函数如下:

u8 readb(const volatile void __iomem *addr)		//读8位数据
u16 readw(const volatile void __iomem *addr) 	//读16位数据
u32 readl(const volatile void __iomem *addr)	//读32位数据

写操作函数如下:

void writeb(u8 value, volatile void __iomem *addr)		//写8位数据
void writew(u16 value, volatile void __iomem *addr)		//写16位数据
void writel(u32 value, volatile void __iomem *addr)		//写32位数据

2、添加头文件路径

​ 因为是编写 linux 驱动,因此会用到 linux 源码中的函数。我们需要在 VSCode 中添加 linux 源码中的头文件路径。按下 “Ctrl + Shift + P” 打开控制台,然后输入 “C/C++: Edit configurations(JSON)”,打开C/C++编辑配置文件,打开以后会自动在 .vscode 文件下生成一个名为 c_cpp_properties.json 的文件,然后添加 linux 源码的头文件路径如下:

{
    "configurations": [
        {
            "name": "Linux",
            "includePath": [
                "${workspaceFolder}/**",
                "/home/liuzhikai/linux/kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga/include",
                "/home/liuzhikai/linux/kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga/arch/arm/include",
                "/home/liuzhikai/linux/kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga/arch/arm/include/generated/"
            ],
            "defines": [],
            "compilerPath": "/usr/bin/gcc",
            "cStandard": "c11",
            "cppStandard": "c++17",
            "intelliSenseMode": "clang-x64"
        }
    ],
    "version": 4
}

3、编写加载和卸载注册函数

​ linux 驱动有两种运行方式,一是编译进 linux 内核中,内核启动时自动运行驱动程序;二是将驱动编译成模块(模块扩展名为 xx.ko),然后内核启动后才用命令加载驱动模块。模块有加载卸载两种操作,加载和卸载注册函数如下:

module_init(led_init);   //注册模块加载函数
module_exit(led_exit);   //注册模块卸载函数

​ 参数 led_init 和 led_exit 是需要注册的具体函数,加载驱动的时候就会调用 led_init ,卸载驱动的时候会调用 led_exit 函数。两个函数的模板如下:

#define DEVICE_NAME "led"
#define LED_MAJOR 200

/* 驱动入口函数,加载驱动时调用 */
static int __init led_init(void)
{
	int retvalue = 0; //注册字符设备函数返回值
	printk("led init\r\n"); //打印进入驱动注册信息
	retvalue = register_chrdev(LED_MAJOR, DEVICE_NAME, &led_fops); //实际的字符设备注册函数
	if(retvalue < 0) {//如果字符注册失败
		printk("led register failed\r\n");
		return -1;
	}
	return 0;
}
  • 第 1 行定义了驱动名:led

  • 第 2 行定义了主设备号

  • 第 7 行 retvalue 用于存储注册函数的返回值,小于 0 则注册失败。

  • 第 8 行打印调试信息,在终端提示成功进入驱动加载函数,linux 下使用的是 printk 函数,使用 man printk 命令可以查看该函数需要包含哪些头文件。

  • 第 9 行是实际的注册函数,该函数的原型为:

    static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
    

    major:驱动的主设备号,可以在 linux 系统下用 cat /proc/devices 命令来查看哪些主设备号可用。

    name:驱动设备名称,指向一串字符串。

    fops:结构体 file_operations 类型指针,指向设备的操作函数集合。

/* 驱动出口函数,卸载驱动时调用 */
static void __exit led_exit(void)
{
	printk("chardevbase_exit\r\n"); //打印进入驱动卸载信息
	unregister_chrdev(LED_MAJOR, DEVICE_NAME); //实际的字符设备注销函数
}
  • 第 5 行是实际的注销函数,该函数原型为:

    static inline void unregister_chrdev(unsigned int major, const char *name)
    

    major:驱动的主设备号,同上注册时使用的主设备号。

    name:驱动设备名称,同上注册时使用的名称。

3、编写设备的具体操作函数

​ 注册函数中 file_operations 类型的结构体定义了实际注册的操作函数,file_operations 结构体有很多成员函数,这些函数不一定全都需要注册,挑选用到的注册即可,各成员函数的模板可以参考 linux 内核的驱动。在注册函数前定义 led_fops 结构体和操作函数:

/* 
写操作函数,应用程序向内核空间写数据时调用 
buf:用户空间传递进来的数据
count:一次写入的字节数
*/
static ssize_t led_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos)
{
	int ret = 0; //拷贝函数返回值
    /*
    拷贝函数,从用户空间拷贝数据到内核空间
    函数原型:unsigned long copy_from_user(void *to, const void *from, unsigned long n);
    to:内核空间的数据缓冲区
    from:用户空间的 buf
    n:一次写入的字节数 count
    返回值:0 成功,非零失败
    */
	ret = copy_from_user(writebuf, buf, count);
	if(ret < 0){ //拷贝出错
		printk("recieve cmd wrong\r\n");
		return -1;
	}
	return 0;
}

/*
open 函数,驱动打开后调用
*/
static int led_open(struct inode *inode, struct file *filp)
{
	return 0;
}

/*
close 函数,驱动结束后调用
*/
static int led_close(struct inode *inode, struct file *filp)
{
	return 0;
}

/*
操作函数注册结构体
*/
static const struct file_operations led_fops = {
	.owner	= THIS_MODULE,
	.write	= led_write,
	.open	= led_open,
	.release = led_close,
};

4、添加头文件及创建物理地址指针和虚拟地址指针

​ 参考 linux 内核的驱动代码时,找到可能用到的头文件,添加进工程。在调用系统调用函数库函数时,在终端使用 man 命令可查看调用的函数需要包含哪些头文件。
​ man 命令数字含义:1:标准命令 2:系统调用 3:库函数
​ 添加以下头文件:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/uaccess.h>		//两个拷贝函数所需头文件
#include <asm/io.h>			    //ioremap iounmap 函数所需头文件

/* 寄存器物理地址 */
#define CCM_CCGR1_BASE 			    (0X020C406C)
#define SW_MUX_GPIO1_IO03_BASE		(0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE		(0X020E02F4)
#define GPIO1_GDIR_BASE			    (0X0209C004)
#define GPIO1_DR_BASE			    (0X0209C000)

/* 寄存器映射虚拟地址 */
static void __iomem *CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_GDIR;
static void __iomem *GPIO1_DR;

5、添加 License 和作者信息

​ 驱动的 License 是必须的,缺少的话会报错,在文件最末端添加以下代码:

MODULE_LICENSE("GPL");
MODULE_AUTHOR("lzk");

6、添加led初始化代码

1)在 led_init 函数建立地址映射,添加led灯的IO初始化代码:

static int __init led_init(void)
{
	int retvalue = 0; //注册函数返回值
	u32 register_data = 0;

	/* 获取映射虚拟地址 */
	CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
	SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
	SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
	GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);
	GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);

	/* 初始化GPIO1时钟 */
	register_data = readl(CCM_CCGR1); //读出CCM_CCGR1寄存器数据
	register_data |= (3 << 26);		 //打开GPIO1时钟
	writel(register_data, CCM_CCGR1); 

	/* 设置IO复用 */
	writel(0x5, SW_MUX_GPIO1_IO03);

	/* 设置电气属性 */
	writel(0x10B0, SW_PAD_GPIO1_IO03);

	/* 初始化GPIO */
	register_data = readl(GPIO1_GDIR);
	register_data |= (1 << 3);
	writel(register_data, GPIO1_GDIR);	//GPIO1_IO03输出
	led_switch(LED_ON);  //点亮led灯
	...
	return 0;
}

2)在 led_exit 添加取消映射代码:

static void __exit led_exit(void)
{
	led_switch(LED_OFF); //关闭led灯

	/* 取消映射 */
	iounmap(CCM_CCGR1);
	iounmap(SW_MUX_GPIO1_IO03);
	iounmap(SW_PAD_GPIO1_IO03);
	iounmap(GPIO1_GDIR);
	iounmap(GPIO1_DR);

	printk("led exit\r\n");
	unregister_chrdev(LED_MAJOR, DEVICE_NAME);
}

3)编写led状态切换函数:

#define LED_ON   1
#define LED_OFF	 0

void led_switch(u8 led_state)
{
	u32 register_data = 0;
	if(led_state == LED_ON){  //打开led灯
		register_data = readl(GPIO1_DR);
		register_data &= ~(1 << 3);
		writel(register_data, GPIO1_DR);
	}
	else{	//关闭led灯
		register_data = readl(GPIO1_DR);
		register_data |= (1 << 3);
		writel(register_data, GPIO1_DR);
	}
}

4)在 write 函数添加led控制代码:

static ssize_t led_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos)
{
	int ret = 0;
	u8 led_state[1];

	ret = copy_from_user(led_state, buf, count); //读取应用程序指令
	if(ret < 0){
		printk("recieve cmd wrong\r\n");
		return -1;
	}
	if(led_state[0] == LED_ON){ //开灯
		led_switch(LED_ON);
	}
	else{
		led_switch(LED_OFF);  //关灯
	}
	return 0;
}

二、编写测试应用程序

​ 在驱动文件夹下创建 ledAPP.c 文件:

#include <sys/types.h>		// open 函数所需头文件
#include <sys/stat.h>		//open 函数所需头文件
#include <fcntl.h>			//open 函数所需头文件
#include <stdio.h>           //printf 函数所需头文件
#include <unistd.h>			//read write close 函数所需头文件
#include <string.h>			//memcpy 函数所需头文件
#include <stdlib.h>			//atoi 函数所需头文件

#define LED_ON   1
#define LED_OFF	 0
/*
应用程序使用 arm-linux-gnueabihf-gcc ledAPP.c -o ledAPP来编译,生成可执行文件
执行应用程序时使用命令行:./ledAPP /dev/ledAPP 1
命令信息保存在 main 函数的两个入口参数中
argc:为命令参数个数,为 3
argv:为命令各个参数的具体内容:
argv[0]:打开应用程序
argv[1]:open读取的文件,驱动文件都在/dev目录下
argv[2]:定义读写功能,1 为开灯 ,0为关灯
命令所带参数可自行定义
*/
int main(int argc, char *argv[])
{
    int fd = 0; //文件描述符,读取文件之前要用open函数打开文件,打开成功后得到文件描述符
    int ret = 0; //read write函数返回值
    unsigned char led_state[1]; //保存led状态
    char *filename; //open函数读取的文件,为argv[2]

    if(argc != 3) //如果命令参数不等于3,表明输入命令格式不对
    {
        printf("missing parameter!\r\n");
        return -1;
    }

    filename = argv[1]; //获取驱动文件名
    fd = open(filename , O_RDWR); //打开驱动文件,O_RDWR表明读写模式(man 2 open 查看具体)
    if(fd < 0) //如果文件描述符小于0,则表明打开文件失败
    {
        printf("open file %s failed\r\n", filename);
        return -1;
    }
    
    /*
    写操作
    命令行传递的参数均为字符,atoi函数把字符数字转化成整型
    */
    if(atoi(argv[2]) == 1){
        led_state[0] = LED_ON; //开灯
        ret = write(fd, led_state, sizeof(led_state)); //写入led状态
        if(ret < 0){
            printf("write file %s failed\r\n", filename);
            return -1;
        }
    }
    else{
        led_state[0] = LED_OFF; //关灯
        ret = write(fd, led_state, sizeof(led_state)); //写入led状态
        if(ret < 0){
            printf("write file %s failed\r\n", filename);
            return -1;
        }
    }

    ret = close(fd); //关闭文件
    if(ret < 0) //返回值小于0关闭文件失败
    {
        printf("close file %s failed\r\n", filename);
        return -1;
    }
	return 0;
}

三、编译和测试

1、编写Makefile,编译驱动程序

​ 驱动程序源码需要编译成.ko模块,创建Makefile:

KERNELDIR := /home/liuzhikai/linux/kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga
CURRENT_PATH := $(shell pwd)
obj-m := led.o

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
  • 第1行,KERNELDIR 表示开发板所使用的 Linux 内核源码目录,使用绝对路径。

  • 第2行,CURRENT_PATH 表示当前路径。

  • 第3行,obj-m 表示将 led.c 这个文件编译为模块。

  • 第5行,默认目标为 kernel_modules。

  • 第8行,具体的编译命令,后面的 modules 表示编译模块,-C表示将当前的工作目录切换到指定目录中。M表示模块源码目录。“make modules”命令中加入 M=dir 以后程序会自动到指定的 dir 目录中读取模块源码并将其编译出 .ko文件。

    编译成功以后会生成一个 led.ko 文件,这个文件就是驱动模块。

2、编译驱动程序

​ 测试 APP 要在 ARM 开发板上运行,所以使用交叉编译器编译:

arm-linux-gnueabihf-gcc ledAPP.c -o ledAPP

​ 编译完成生成 ledAPP 的可执行程序。

3、运行测试

​ linux 系统选择网络启动,并且用 nfs 挂载根文件系统,U-Boot 设置如下:
​ bootcmd 的值:

tftp 80800000 zImage;tftp 83000000 imx6ull-alientek-emmc.dtb;bootz 80800000 - 83000000

​ bootargs 的值:

console=ttymxc0,115200 root=/dev/nfs rw nfsroot=192.168.1.138:/home/liuzhikai/linux/nfs/rootfs ip=192.168.1.123:192.168.1.138:192.168.1.2:255.255.255.0::eth0:off

​ 将 led.koledAPP 拷贝到以下目录(不存在的目录创建):

sudo cp led.ko ledApp /home/liuzhikai/linux/nfs/rootfs/lib/modules/4.1.15/ -f

​ 使用如下命令加载驱动模块:

depmod
modprobe led.ko  //加载驱动模块
lsmod  //查看当前系统中存在的模块
cat /proc/devices  //查看系统中有没有led这个设备
mknod /dev/led c 200 0 //创建设备节点,c表示设备是字符设备,200表示主设备号,0表示次设备号
ls /dev/led  //查看是否创建节点成功
./ledAPP /dev/led 1  //执行应用程序,打开led灯
./ledAPP /dev/led 0  //执行应用程序,关闭led灯
rmmod led.ko  //卸载驱动模块

.168.1.2:255.255.255.0::eth0:off


​		将 ==led.ko== 和 ==ledAPP== 拷贝到以下目录(不存在的目录创建):

```c
sudo cp led.ko ledApp /home/liuzhikai/linux/nfs/rootfs/lib/modules/4.1.15/ -f

​ 使用如下命令加载驱动模块:

depmod
modprobe led.ko  //加载驱动模块
lsmod  //查看当前系统中存在的模块
cat /proc/devices  //查看系统中有没有led这个设备
mknod /dev/led c 200 0 //创建设备节点,c表示设备是字符设备,200表示主设备号,0表示次设备号
ls /dev/led  //查看是否创建节点成功
./ledAPP /dev/led 1  //执行应用程序,打开led灯
./ledAPP /dev/led 0  //执行应用程序,关闭led灯
rmmod led.ko  //卸载驱动模块
2017-01-24 13:34:36 TongxinV 阅读数 703

动态映射之结构体方式操作寄存器

仿效真实驱动中,用结构体封装的方式来进行单次多个寄存器的地址映射。即代替文章《随笔–Linux字符设备驱动开发基础》中动态映射操作LED一节的基础动态映射方式

#include <linux/io.h>
#include <linux/ioport.h>
......
typedef struct GPJ0REG
{
    volatile unsigned int gpj0con;
    volatile unsigned int gpj0dat; 
}gpj0_reg_t;

//#define GPJ0CON_PA    0xe0200240
//#define GPJ0DAT_PA    0xe0200244
#define GPJ0_REGBASE    0xe0200240

//unsigned int *pGPJ0CON;       //指针类型才能接受ioremap返回的地址
//unsigned int *pGPJ0DAT;
gpj0_reg_t *pGPJ0REG;     

...
// 模块安装函数
static int __init chrdev_init(void)
{
        //注册字符设备驱动
    ...
#if 0
    // 使用动态映射的方式来操作寄存器
    if (!request_mem_region(GPJ0CON_PA, 4, "GPJ0CON"))
        return -EINVAL;
    if (!request_mem_region(GPJ0DAT_PA, 4, "GPJ0DAT"))
        return -EINVAL;
    pGPJ0CON = ioremap(GPJ0CON_PA, 4);
    pGPJ0DAT = ioremap(GPJ0DAT_PA, 4);

    *pGPJ0CON = 0x11111111;
    *pGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));    // 亮
#endif
    // 2步完成映射
    if (!request_mem_region(GPJ0_REGBASE, sizeof(gpj0_reg_t), "GPJ0REG"))
        return -EINVAL;
    pGPJ0REG = ioremap(GPJ0_REGBASE, sizeof(gpj0_reg_t));
    // 映射之后用指向结构体的指针来进行操作
    // 指针使用->结构体内元素 的方式来操作各个寄存器
    pGPJ0REG->gpj0con = 0x11111111;
    pGPJ0REG->gpj0dat = ((0<<3) | (0<<4) | (0<<5));    // 亮
        ...
}
// 模块卸载函数
static void __exit chrdev_exit(void)
{
        ...
    // 解除映射
#if 0
    iounmap(pGPJ0CON);
    iounmap(pGPJ0DAT);
    release_mem_region(GPJ0CON_PA, 4);
    release_mem_region(GPJ0DAT_PA, 4);
#endif
    iounmap(pGPJ0REG);
    release_mem_region(GPJ0_REGBASE, sizeof(gpj0_reg_t));
        ...
        //注销字符设备驱动
        ...
}

动态映射结构体方式操作寄存器:在结构体中添加相应成员就可以,不用像之前的那么麻烦

2012-03-29 14:43:15 yby19870627 阅读数 5369

LED灯驱动编写--寄存器操作


(转载请写明出处: http://blog.csdn.net/yby19870627/article/details/7407130 )


这里没有用的内存映射的方法,而是直接对寄存器进行操作,我建议在开发驱动的时候,用NFS挂载的方式进行开发,这样可以节省很多时间,NFS挂载方法可以从我以前的文章中找到。

平台:Fedora14

内核:linux-2.632.2

 

一、首先要编写最基本的模块,因为编程就是要一步一步调试的,这样才能发现问题,如果一开始不管三七二十一,先把代码写完再说,那当你make完看到那些错误的时候,那个时候,我估计你连死的心都有了。

 

1.        添加最基本的头文件:#include <linux/module.h>

                                                   #include <linux/init.h>

2.        编写模块初始化函数和模块推出函数

static      int   __init   Led_init(void)

{

                  printk("<0>module--->Led_init\n");

}
//--------------------------------------------------------------------------------

static    void   __exit   Led_exit(void)

{

             printk("<0>module--->exitok!\n");

}

//---------------------------------------------------------------------------------

module_init(Led_init);

module_exit(Led_exit);

 

3.        编译程序,然后再insmod led.ko/rmmod led

看看是否输出了代码里面的两条打印语句。

 

二、第一步通过之后,那么就要开始添加新的代码了,我们是要注册设备的,所以需要定义一个设备号变量,同时要注册到内核中还需要一个struct cdev结构的变量,所以要定义     

             struct  cdev  led_cdev;   

             dev_t   led_devno;

定义完之后还需要考虑到要有一个变量来存储主设备号,所以还要定义一个主设备号变量。而且还要定义主设备号和次设备号两个宏。

             #define   MAINLEDNO   108

            #define   MINORLEDNO   0 

            static   int   led_major;

 

 

定义struct  file_operations结构,一开始里面可以什么都不写。

 

struct  file_operations  led_ops={

         

 };

修改模块初始化函数。

static   int   __init   Led_init(void)

{

         printk("<0>module--->Led_init\n");

         int  result;

         //申请设备号

         led_devno =MKDEV(MAINLEDNO,MINORLEDNO);

         result =register_chrdev_region(led_devno,1,"myled");

        

          if(result<0)

         {

                   result =alloc_chrdev_region(&led_devno,0,1,"myled");

                   led_major = MAJOR(led_devno);

         }

         //注册设备

         cdev_init(&led_cdev,&led_ops);

          led_cdev.owner = THIS_MODULE;

          result =cdev_add(&led_cdev,led_devno,1);

         if(result<0)

         {

                   printk("<0>module--->adderror!\n");

                   return result;

         }

         printk("<0>module--->cdev_addok!\n");

         return 0;

}

 

修改模块退出函数。

         static void __exit Led_exit(void)

         {       

                  cdev_del(&led_cdev);

                  unregister_chrdev_region(led_devno,1);

                  printk("<0>module--->exitok!\n");

         }

编译,通过后安装和卸载模块,看看打印信息是否正确。

 

 

三、设备注册完之后,就要开始编写struct  file_operations结构变量里面的对应的函数了,这个结构里面的函数在这就不花时间讲述了,只要记住里面是系统调用函数和自己编写函数对应关系就OK了。

struct   file_operations  led_ops={

.open     = led_open,

.release   =led_close,

};

 

里面把自己的led_open函数与系统的open函数联系起来,说白了,就是app程序用open函数打开这个设备的时候,就会执行led_open函数里面的代码,所以,我们要实现led_open函数和led_close函数。

 

首先要知道我们是对ARM的寄存器进行操作,所以首先添加头文件

#include <linux/types.h>

#include<mach/regs-gpio.h> //寄存器的地址头文件

#include<asm/io.h>

 

我们的目的是控制LED等,所以我们要看开发板上LED灯是怎么连接的。


通过电路图,我们可以知道,连接的是GPB5、GPB6、GPB7、 GPB8这四个引脚,当引脚电平为低电平时,LED就会亮,当引脚电平为高电平时,LED就熄灭。

 

staticint led_open(struct inode *inode,struct file *file)

{

         int   value = 0;

         value =(1<<10)|(1<<12)|(1<<14)|(1<<16);

         iowrite32(value,S3C2410_GPBCON);

         value = 0x0;

         iowrite32(value,S3C2410_GPBUP);

         value = 0xffffffff;

         iowrite32(value,S3C2410_GPBDAT);

         printk("<0>module--->Ledrun!\n");

         return 0;

}

 

static    int   led_close(struct inode *inode,struct file *file)

{

         return 0;

 }

当中的iowrite32函数是2.6内核的写函数,老版本的是writel,我们可以看到,用iowrite32函数对寄存器进行赋值,来完成相应的功能。

像S3C2410_GPBCON这些函数都是#include <mach/regs-gpio.h>头文件定义好的,所以不需要自己定义。

 

编写完之后,我们可以编译一下,但是安装完之后,没有反应,是因为没有应用程序来对这个驱动进行操作,所以我们现在来写应用程序。

 #include<stdio.h>

int  main(int argc,char* argv[])

{

         int   fd = 0;

         int   cmd= 0;

         char  arg[10];

        

         fd = open("/dev/myled0",0);

         getchar();

         close(fd);

         return 0;

         }

运行这个应用程序,看看LED灯是否熄灭,注意在insmod模块之后,别忘了mknod设备哦:mknod /dev/myled0 c 108 0.

 

四、如果做到上面那一步了,那我们就离成功不远了,现在只需要在内核中定义命令就可以实现对LED灯的控制了。

 

首先在struct  file_operations 结构变量中添加ioctl的对象。

structfile_operations led_ops={

         .open            = led_open,

         .release          = led_close,

         .unlocked_ioctl    = led_ioctl,

};

 

 

 

然后再定义ioctl的相关命令,这里要提到命令中的宏定义。具体的ioctl函数的命令这里就不做过多的解释。

 

#defineLED_MAGIC 'y'                   //定义幻数

 

#defineLED1_ON   _IO(LED_MAGIC,1)   //定义相关命令

#defineLED1_OFF  _IO(LED_MAGIC,2)

 

#defineLED2_ON   _IO(LED_MAGIC,3)

#defineLED2_OFF  _IO(LED_MAGIC,4)

 

#defineLED3_ON   _IO(LED_MAGIC,5)

#defineLED3_OFF  _IO(LED_MAGIC,6)

 

#defineLED4_ON   _IO(LED_MAGIC,7)

#defineLED4_OFF  _IO(LED_MAGIC,8)

 

宏定义好了之后,那么就来实现ioctl函数。

static   long led_ioctl(struct   file *file,unsigned   int   cmd,unsigned   long   arg)

{

         printk("<0>module--->Led_ioctlin!\n");

        

         int   gpbdate;

         gpbdate = ioread32(S3C2410_GPBDAT);

         printk("<0>module--->gpbdate:%d\n",gpbdate);

        

         switch(cmd)

         {

                   case LED1_ON:

                            printk("<0>module--->CMDLED1_ON!\n");

                            iowrite32((~(0x01<<5)& gpbdate),S3C2410_GPBDAT);

                            break;

                   case LED1_OFF:

                            printk("<0>module--->CMDLED1_OFF!\n");

                            iowrite32(((0x01<<5)| gpbdate),S3C2410_GPBDAT);

                            break;

                           

                   case LED2_ON:

                            printk("<0>module--->CMDLED2_ON!\n");

                            iowrite32((~(0x01<<6)& gpbdate),S3C2410_GPBDAT);

                            break;

                   case LED2_OFF:

                            printk("<0>module--->CMDLED2_OFF!\n");

                            iowrite32(((0x01<<6)| gpbdate),S3C2410_GPBDAT);

                            break;

                           

                   case LED3_ON:

                            printk("<0>module--->CMD LED3_ON!\n");

                            iowrite32((~(0x01<<7)& gpbdate),S3C2410_GPBDAT);

                            break;

                   case LED3_OFF:

                            printk("<0>module--->CMDLED3_OFF!\n");

                            iowrite32(((0x01<<7)| gpbdate),S3C2410_GPBDAT);

                            break;

                           

                   case LED4_ON:

                            printk("<0>module--->CMDLED4_ON!\n");

                            iowrite32((~(0x01<<8)& gpbdate),S3C2410_GPBDAT);

                            break;

                   case LED4_OFF:

                            printk("<0>module--->CMDLED4_OFF!\n");

                            iowrite32(((0x01<<8)| gpbdate),S3C2410_GPBDAT);

                            break;

         }

 }

代码都是上面讲过的,所以不做过多的解释。

 

五、驱动代码写好之后,那么要怎么用应用代码来验证呢,我们在内核中定义的ioctl定义的命令,要怎么让应用层知道呢,我开始也迷茫了很久,并且也在网上搜索答案,一直没有找到解决办法,后面还是自己一步一步试出来的。

 

只需要在应用代码中重新定义,当然我们也可以全部定义在一个头文件里。

#define   LED_MAGIC 'y'                   //定义幻数

 

#define    LED1_ON   _IO(LED_MAGIC,1)   //定义相关命令

#define    LED1_OFF  _IO(LED_MAGIC,2)

 

#define   LED2_ON   _IO(LED_MAGIC,3)

#define   LED2_OFF  _IO(LED_MAGIC,4)

 

#define   LED3_ON   _IO(LED_MAGIC,5)

#define   LED3_OFF  _IO(LED_MAGIC,6)

 

#define   LED4_ON   _IO(LED_MAGIC,7)

#defineLED4_OFF  _IO(LED_MAGIC,8)

 

并且添加头文件#include <linux/fs.h>就OK了,记住这是应用代码中。

命令定义好之后,我们就可以直接调用ioctl了。

int main(intargc,char* argv[])

{

         int fd = 0;

         int cmd= 0;

         char arg[10];

        

         fd = open("/dev/myled0",0);

         memset(arg,'\0',sizeof(arg));

        

         while(1)

         {

                   printf("Please inputon/off led number!\n");

                   scanf("%s",arg);

                  

                   if(strcmp(arg,"on1")== 0)

                   {

                            cmd = LED1_ON;

                   }

 

                   if(strcmp(arg,"off1")== 0)

                   {

                            cmd = LED1_OFF;

                   }

                   if(strcmp(arg,"on2")== 0)

                   {

                            cmd = LED2_ON;

                   }

                   if(strcmp(arg,"off2")== 0)

                   {

                            cmd = LED2_OFF;

                   }

                   if(strcmp(arg,"on3")== 0)

                   {

                            cmd = LED3_ON;

                   }

                   if(strcmp(arg,"off3")== 0)

                   {

                            cmd = LED3_OFF;

                   }

                   if(strcmp(arg,"on4")== 0)

                   {

                            cmd = LED4_ON;

                   }

                   if(strcmp(arg,"off4")== 0)

                   {

                            cmd = LED4_OFF;

                   }

                   printf("cmd =%d\n",cmd);

                   memset(arg,'\0',sizeof(arg));

                   ioctl(fd,cmd,0);

                  

         }

         getchar();

         close(fd);

         return 0;

}

 

这样一个LED的驱动程序就写好了。

2018-09-04 16:52:00 baidu_37973494 阅读数 3451

一句话概括关系:固件库就是函数的集合,固件库函数的作用就是:向下负责与寄存器直接打交道,向上提供用户函数调用的接口(API)。对于STM32这种级别的MCU,数百个寄存器记起来何谈容易,于是推出了官方固件库,固件库将这些寄存器底层操作都封装起来,提供一套API供开发者使用。大多数情况下,你不需要知道操作的是哪个寄存器,你只需要知道调用哪些函数即可。

对比一下:入下面只需要知道GPIO_SetBit这个函数的使用即可。

寄存器操作:

库函数操作:

任何的处理器,不管多么高级,归根结底都是对寄存器的操作。固件库不是万能的,如果想要把STM32学透,知其然知其所以然,光读STM32固件库是远远不够的。但是如果知识为了完成一个简单项目,我们只把库函数读明白,知道怎么去使用库函数,也足以完成任务。

 

浅谈驱动开发

阅读数 44

linux 驱动开发

阅读数 2331

没有更多推荐了,返回首页