Linux下的驱动学习笔记(1)

笔记目录

重要结构体和宏定义:

//板子上与虚拟机用网卡进行通信
ping 192.168.44.194
if pxe get; then pxe boot; fi
//加入编译工具
3> 导入sdk
linux@ubuntu:~/farsight/sdk$ source /opt/stm32_sdk/environment-setup-cortexa7t2hf-neon-vfpv4-ostl-linux-gnueabi
//设备层
struct platform_device {//pedv
	const char	* name;  // 资源(结构体)数组"名称"---用来匹配
	int		id;          // 不同寄存器组的编号
	struct device	dev; // 父类
	u32		num_resources; // 资源的个数一般=ARRAY_SIZE(led_main)
	struct resource	* resource; //资源(直接给资源结构体数组名字[])
};
extern int platform_device_register(struct platform_device *);//在设备层led_dev_init里注册
extern void platform_device_unregister(struct platform_device *);//在设备层led_dev_exit里注销
----------------------------------------------
//资源结构体	
struct resource 
{
	resource_size_t start; //起始
	resource_size_t end; //结束
	const char *name; //名字,自定义
	unsigned long flags; //地址资源还是中断资源
};
----------------------------------------------
//驱动层操作	
struct platform_driver {
	int (*probe)(struct platform_device *);
	int (*remove)(struct platform_device *);
	void (*shutdown)(struct platform_device *);
	int (*suspend)(struct platform_device *, pm_message_t state);
	int (*resume)(struct platform_device *);
	struct device_driver driver; //父类
	const struct platform_device_id *id_table; //名字列表
};
extern int platform_driver_register(struct platform_driver *);
extern void platform_driver_unregister(struct platform_driver *);
----------------------------------------------
//stm32mp157GPIO结构体
typedef struct {
	volatile unsigned int MODER;   // 0x00
	volatile unsigned int OTYPER;  // 0x04
	volatile unsigned int OSPEEDR; // 0x08
	volatile unsigned int PUPDR;   // 0x0C
	volatile unsigned int IDR;     // 0x10
	volatile unsigned int ODR;     // 0x14
	volatile unsigned int BSRR;    // 0x18
	volatile unsigned int LCKR;    // 0x1C
	volatile unsigned int AFRL;    // 0x20
	volatile unsigned int AFRH;    // 0x24
	volatile unsigned int BRR;     // 0x28
	volatile unsigned int res;
	volatile unsigned int SECCFGR; // 0x30
}gpio_t;
#define  GPIOA   0x50002000
#define  GPIOB   0x50003000
#define  GPIOC   0x50004000
#define  GPIOD   0x50005000
#define  GPIOE   0x50006000
#define  GPIOF   0x50007000
#define  GPIOG   0x50008000
#define  GPIOH   0x50009000
#define  GPIOI   0x5000A000
#define  GPIOJ   0x5000B000
#define  GPIOK   0x5000C000
#define  GPIOZ   0x54004000
#define RCC   0x50000000
//平台总线
const struct platform_device_id led_table[] = {//参数1:设备名字,参数2:占内核大小
	{"led_main", 0x1234},
	{},
};

一、调用设备树API方法

1、初始化外设结构体以及获取次设备号和开时钟方法

struct stm32mp157{
	int major;//主设备号
	struct class * clazz;//类
	struct device * dev;//数据节点
	gpio_t * gpioz;
	gpio_t * gpioe;
	rcc_t *rcc;
};
struct stm32mp157 * stm32_led;
--------------------------------------------------
//获取次设备号的方法,在ssize_t led_write(struct file * filp, const char __user * buff, size_t count, loff_t * pos)
	int minor;
	struct inode * node;
	node = filp->f_path.dentry->d_inode;
	minor = iminor(node);//获得次设备号
--------------------------------------------------
//打开锁相环
  	key->rcc->PLL4CR|= (1<<0);
//等待锁相环进入ready状态
	while((key->rcc->PLL4CR & (1<<1)) == 0);
	key->rcc->MP_AHB4ENSETR |= (1 << 5);
	return 0; 

2、进入probe函数

2.1、创建平台总线(外设)资源节点(资源内容在app内:时钟,GPIO)

struct resource * res1;
struct resource * res2;

2.2、创建设备树资源

zhangsan:stm32mp157_led@54004000 {
        compatible = "stm32mp157,led_test";
        reg = <0x50005400 0x400 0x50000000 0x1000>;
        shen_name = "daoge","farsight";
        age = <33>;
        ages = <33 44>, <55 66>;
        test = [06 06 06];
        special{
           spec = "linux";
       };

在这里插入图片描述

2.3、获取平台总线资源

ps:在应用程序(app)操作之前,内核里的驱动层和设备层就已经匹配完毕。应用层只是调用匹配完后的内核里的接口。进程只是应用层里的概念,而这些进程公用驱动层的一套程序而已。

在这里插入图片描述

struct resource led_main[] = {
	[0] = {
		.start = RCC_S,
		.end = RCC_P,
		.name = "led_main_rcc",
		.flags = IORESOURCE_MEM,
	},
	[1] = {
		.start = GPIOZ_S,
		.end = GPIOZ_P,	
		.name = "led_main_gpio",
		.flags = IORESOURCE_MEM,
	},
	//========以下为测试=========
	[2] = {
		.start = 11,
		.end = 11,	
		.name = "led_main_irq",
		.flags = IORESOURCE_IRQ,
	},
};
struct platform_device pdev_main=
{
	.name = "led_main",
	.id = -1,
	.dev = {
		.release = led_dev_release,
	},
	.num_resources = ARRAY_SIZE(led_main),//资源的个数,ARRAY_SIZE(数组)计算数组长度
	.resource = led_main,//资源(地址,中断)
};

res1 = platform_get_resource(pdev, IORESOURCE_MEM, 0);
//参数2为资源类别,参数3为以该资源类的下标(从0开始)

2.4、设备树获取函数族

//根据给的属性名去找属性并返回对应的值
shen_name = (char *)of_get_property(pdev->dev.of_node, "shen_name", &lenp);
//带参宏,定义如下
/*#define of_property_for_each_string(np, propname, prop, s)	\
	for (prop = of_find_property(np, propname, NULL),	\
		s = of_prop_next_string(prop, NULL);		\
		s;						\
		s = of_prop_next_string(prop, s))
*/
//该函数会遍历属性数组"shen_name",把每个读到的内容存到s
of_property_for_each_string(pdev->dev.of_node, "shen_name", prop, s)
{
	printk("s=%s\n",s);
}
//计算该属性有几个元素
lenp = of_property_count_u32_elems(pdev->dev.of_node, "ages");
//从属性读取一个32位数组的值,返回值放out_value数组里,参数4为读取的个数,其他类似
of_property_read_u32_array(pdev->dev.of_node, "ages", out_value,4);
of_property_read_string_helper(pdev->dev.of_node,"shen_name",&shen_name, 2, 1);
of_property_read_u32(pdev->dev.of_node, "age", &value);
of_property_read_u8_array(pdev->dev.of_node, "test", data, 3);
node = of_find_node_by_name(pdev->dev.of_node, "special");
of_property_read_string(node,"spec",&special);

2.5、进入remove函数做反操作

二、调用GPIO的设备树API

1、初始化外设结构体和设备树

struct stm32mp157{
        int major;
        struct class * clazz;
        struct device * dev;
        int gpioz_5;
        int gpioz_6;
        int gpioz_7;
};
stm32mp157_led_test{//设置引脚有效电平
          compatible = "stm32mp157,led_test2";
          led-gpios = <&gpioz 5 GPIO_ACTIVE_HIGH>,
                  <&gpioz 6 GPIO_ACTIVE_HIGH>,
                  <&gpioz 7 GPIO_ACTIVE_HIGH>;
      };

2、进入probe函数

2.1、GPIO的设备API

方法1:
//物理地址转化成虚拟地址
stm32_led->gpioz = ioremap(res3->start, resource_size(res3));
//获得GPIO号
stm32_led->gpioz_5 = of_get_gpio(pdev->dev.of_node, 0);
	if(stm32_led->gpioz_5 < 0)
	{
		ret = stm32_led->gpioz_5;
		printk("of_get_gpio error1\n");
		goto device_create_err;
	}
//请求操作GPIO权限
    ret = gpio_request(stm32_led->gpioz_5, "led1");
	if(ret < 0)
	{
		printk("gpio_request error1\n");
		goto device_create_err;
	}
//设置GPIO输出,低电平
	gpio_direction_output(stm32_led->gpioz_5, 0);
//释放GPIO
	gpio_free(stm32_led->gpioz_5);
方法2:
//物理地址转化成虚拟地址
stm32_led->gpioz = ioremap(res3->start, resource_size(res3));
//获得GPIO号(使用devm前缀不用自己释放)
stm32_led->led1 = devm_gpiod_get_index(&pdev->dev,"led",0,GPIOD_OUT_HIGH);
//设置引脚为输出模式,且初始值value为0
gpiod_direction_output(stm32_led->led1,0);
//改变电平的值
gpiod_set_value(stm32_led->led1,1);

三、杂项驱动

1、初始化杂项结构体

/*struct miscdevice  {//杂项驱动结构体的major默认是10
	int minor;
	const char *name;
	const struct file_operations *fops;
	struct list_head list;
	struct device *parent;
	struct device *this_device;
	const struct attribute_group **groups;
	const char *nodename;
	umode_t mode;
};*/
struct miscdevice misc = {
	.minor = 199,
	.name = "key_drv",
	.fops = &key_fops,
};

2、 注册杂项驱动(注册、申请类、创造数据节点直接做完)

ret = misc_register(&misc);
	if(ret < 0)
	{
		printk("misc_register error\n");
		goto kzalloc_err2;
	}

在这里插入图片描述

3、硬件初始化

//外设结构体
struct key_info{
    //按键地名称
    char name[10];
	//按键的ID
	int id;
	//按键中断号
	int irq_no;
	//按键的值
	int value;
	//触发方式
	int flag;
};
//自动获取外设结构体并赋值和注册中断
for(i = 0; i < pdev->num_resources; i++)
	{
	    sprintf(key->info[i].name, "KEY_%d", i+1);
		key->info[i].id = i+1;
		key->info[i].flag = IRQF_TRIGGER_RISING|IRQF_TRIGGER_FALLING;//中断判定方式
		key->info[i].irq_no = platform_get_irq(pdev, i);//从设备树获取设备号
		ret = request_irq(key->info[i].irq_no, key_drv_handler, key->info[i].flag, key->info[i].name, &key->info[i]);
		if(ret < 0)
		{
			printk("request_irq error\n");
			goto misc_register_err;
		}
	}
request_irq	中断注册函数
//参数1:虚拟中断号(设备树种获取),参数2:中断处理函数,参数3:触发方式,参数4:描述,参数5:传递给中断处理函数的参数

四、中断

中断过程:

  1. 保存现场
  2. 进入对应的异常向量表
  3. 进入异常处理函数
  4. 读控制器的位(标志宏)
  5. 进入中断处理函数
  6. 回复现场

1、两种中断流程

在这里插入图片描述

2、tasklet编程流程

  1. 创造结构体变量:

    struct tasklet_struct tasklet;
    
  2. 初始化结构体变量(probe):

    //参数1:中断结构体赋值,参数2:下半部处理函数的函数指针,参数3:下半部处理函数的参数
    	tasklet_init(&key->tasklet, key_drv_func, 0);
    
  3. 在上半部分处理函数(key_drv_handler)进行调度

    //调度,启动下半部分函数
    	tasklet_schedule(&key->tasklet);
    

3、workqueue编程流程(第一个进程跑到下半部分调度后,中断唤醒进程再继续跑未完成的下半部分函数(绿实线))

  1. 创建work结构体变量:

    struct work_struct work;
    
  2. 初始化结构体变量(probe):

    //参数1:work结构体指针,参数2:下半部处理函数的函数指针
    	INIT_WORK(&key->work, key_drv_work);
    
  3. 在上半部分处理函数(key_drv_handler)进行调度

    schedule_work(&key-> work);
    

4、thread_handler编程流程

  1. 写后半段处理函数作为申请线程中断处理的参数

    irqreturn_t key_drv_irq_thread(int irq_no,void * data)
    {
    	printk("-----------start---%s---------\n",__FUNCTION__);
    	return IRQ_HANDLED;
    }
    
  2. 申请线程中断处理

    //可以再带一个线程处理中断函数
    	ret = request_threaded_irq(key->info[i].irq_no, key_drv_handler,key_drv_irq_thread, key>info[i].flag, key->info[i].name, &key->info[i]);
    
  3. 在上半部分处理函数(key_drv_handler)返回唤醒进程的宏

    return IRQ_WAKE_THREAD;//唤醒线程,启动线程中断处理函数5、
    

5、thread阻塞唤醒(可大大增加CPU利用率)

  1. 创建等待队列头以及资源标志位

    struct wait_queue_head wq_head;//等待队列头
    int have_data;//是否有资源标志位
    
  2. 初始化等待队列头以及资源标志位

    key->have_data = 0;//初始化标志位
    init_waitqueue_head(&key->wq_head);//初始化等待队列头
    
  3. 在key_drv_read中设置一个可以被打断的休眠(阻塞)

    //一个可以被打断的休眠(阻塞),即进程被挂在了休眠队列上。参数1:等待队列头,参数2:休眠条件
    wait_event_interruptible(key->wq_head, key->have_data);
    
  4. 在中断函数中拿到资源并唤醒休眠

    key->have_data = 1;
    wake_up_interruptible(&(key->wq_head));//中断唤醒休眠
    

6、多路IO复用(read函数要求阻塞)

  1. 创建监听事件结构体

    //struct pollfd
    //{
    //	int fd;
    //	short events;//输入值:被监视的的事件
    //	short revents;//输出值:返回真正发生的事件
    //};
    struct pollfd fds[2];
    
  2. 初始化监听事件结构体

    	fds[0].fd = fd;
    	fds[0].events = POLLIN;
    
    	fds[1].fd = 0;
    	fds[1].events = POLLIN;
    
  3. 开启轮询监听

    while(1)
    	{	 //参数1:监听事件结构体pollfd,参数2:被监听的事件总数,参数3:设置-1等于没有超时时间
    		//返回值>0表示监听到事件
    		ret = poll(fds, sizeof(fds)/sizeof(fds[0]), -1);
    
  4. 判定监听事件以及动作

    if(ret > 0)
    		{
    			if(fds[0].revents & POLLIN) //有按键数据了
    			{				
    				read(fd, &info, sizeof(info));
    				//
    				if(info.value)
    					printf("KEY--->%s, Pressed...\n",info.name);
    				else
    					printf("KEY--->%s, Release...\n",info.name);
    			}
    			
    			if(fds[1].revents & POLLIN)//有标准输出了
    			{
    				int len;
    				printf("---------start3----------\n");
    				len = read(0, buff, sizeof(buff));
    				buff[len] = '\0';
    				printf("buff=%s\n",buff);
    			}			
    		}
    

7、异步通知(信号量)

  1. (app)注册信号函数key_signandler()

    void key_signandler(int signal)
    {
    	ret = read(fd,&info,sizeof(info));
    	if(ret < 0)
    	{
    		perror("read");
    		exit(1);
    	}
    	if(info.value)
    	printf("KEY--->%s,Pressed...\n",info.name);
    	else
    	printf("KEY--->%s,Pressed...\n",info.name);
    }
    
    //注册信号函数。收到驱动来的信号后执行key_sighandler函数。
    	signal(SIGIO, key_signandler);
    
  2. (app)fcntl设置文件属性[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ia2xxC4w-1656996812744)(.\Markdown图库\20160721111922800.png)]

    //fcntl设置文件属性,把进程ID告诉驱动
    	fcntl(fd,F_SETOWN,getpid());
    	//获取文件状态标志
    	flags = fcntl(fd,F_GETFL);
    	//设置当前文件标志,加入异步通信功能。会直接执行key_drv_fasync函数。
    	fcntl(fd,F_SETFL,flags | FASYNC);
    
  3. (app)空跑

    while(1)//空跑
    	{
    		printf("-------zhi neng shuo hen kun-------\n");
    		sleep(2);
    	}
    
  4. (drv)创建异步通知结构体

    struct fasync_struct * fasync;
    
  5. (drv)初始化异步通信结构体

    //初始化fasync结构体,应用程序进程ID会保存在fasync结构体里
    int key_drv_fasync(int fd,struct file * filp,int on)
    {
    	fasync_helper(fd, filp, on, &key->fasync);
    }
    
  6. (drv)中断服务函数里给应用进程(app)发信号

    //根据进程ID发信号给当前应用进程
    kill_fasync(&key->fasync, SIGIO, POLL_IN);
    
  7. (app)被捕捉信号后,执行信号函数

8、中断延时

  1. 创建定时器结构体

    struct timer_list timer;
    
  2. (probe)定时器初始化

    //定时器初始化,参数1:定时器,参数2:超时执行的函数,参数3:标志
    	timer_setup(&key->timer,key_drv_timer,0);
    
    //超时执行函数
    void key_drv_timer(struct timer_list * timer)
    {
    	printk("----------%s-----------\n",__FUNCTION__);
    	if()
    	switch(key->current_info.id)
    	{
    		case 1:
    			if(gpiod_get_value(key->current_info.desc))//抬起
    			{
    				key->current_info.value = 0;
    				printk("KEY--->%s, Release...\n",key->current_info.name);
    			}
    				
    			else//按下
    			{
    				key->current_info.value = 1;
    				printk("KEY--->%s, Pressed...\n",key->current_info.name);
    			}
    			break;
    		case 2:
    			if(gpiod_get_value(key->current_info.desc))//抬起
    			{
    				key->current_info.value = 0;
    				printk("KEY--->%s, Release...\n",key->current_info.name);
    			}
    			else//按下
    			{
    				key->current_info.value = 1;
    				printk("KEY--->%s, Pressed...\n",key->current_info.name);
    			}
    			break;
    		case 3:
    			if(gpiod_get_value(key->current_info.desc))//抬起
    			{
    				key->current_info.value = 0;
    				printk("KEY--->%s, Release...\n",key->current_info.name);
    			}
    			else//按下
    			{
    				key->current_info.value = 1;
    				printk("KEY--->%s, Pressed...\n",key->current_info.name);
    			}
    			break;
    		default:
    			break;
    	}
    	//根据进程ID发信号给当前应用进程
    	kill_fasync(&key->fasync, SIGIO, POLL_IN);
    }
    
  3. 启动定时器

    //启动一个定时器,把定时器添加到内核
    	add_timer(&key->timer);
    
  4. 中断服务函数确定中断延时

    irqreturn_t key_drv_handler(int irq_no, void * data)
    {
        struct key_info *pdata = (struct key_info *)data;
    	key->current_info = *pdata;
    	
    	//修改定时器的超时值
    	mod_timer(&key->timer,jiffies + 3*HZ);//3s一次中断
    	return IRQ_HANDLED;
    }
    
  5. (probe)若出错删除定时器

    misc_register_err:
    	{
    		for(; i > 0; i--)
    		{
    			free_irq(key->info[i - 1].irq_no, &key->info[i - 1]);
    		}
    		//从内核中删除定时器
    		del_timer(&key->timer);
    		misc_deregister(&misc);
    	}
    
  6. (remove)反操作

    del_timer(&key->timer);
    

五、mmap(内存映射操作,比read、write性能更高)

  1. (驱动层)为虚拟地址创建泛型指针,注册杂项设备,用kzalloc开辟连续物理地址

    void * vm_addr;//虚拟地址
    static int __init mmap_drv_init(void)
    {
    	int ret;
    	//注册杂项设备
    	ret = misc_register(&misc);
    	if(ret < 0)
    	{
    		printk("misc_register error\n");
    		return ret;
    	}
    	
    	//开辟的物理地址,返回值是虚拟地址
    	vm_addr = kzalloc(PAGE_SIZE,GFP_KERNEL);
    	
    	if(vm_addr == NULL)
    	{
    		misc_deregister(&misc);
    		return -ENOMEM;
    	}
    	return 0;
    }
    
  2. 对上一条进行反操作

    static void __exit mmap_drv_exit(void)
    {
    	kfree(vm_addr);
    	misc_deregister(&misc);
    }
    
  3. 从内核读内容到用户空间

    ssize_t drv_read(struct file *filp, char __user * buff, size_t count, loff_t * pos)
    {
    	int ret;
    	ret = copy_to_user(buff, vm_addr, count);
    	if(ret > 0)
    	{
    		printk("copy_to_user error\n");
    		return -EAGAIN;
    	}
    	return count;
    }
    
  4. 将kzalloc返回的虚拟地址转化为物理地址,再将内核内存映射到用户空间

    int drv_mmap (struct file *filp, struct vm_area_struct *vma)
    {
    	int ret;
    	//再把返回的虚拟地址映射出物理地址
    	unsigned long phy_addr = virt_to_phys(vm_addr);
    	//remap_pfn_range:将内核内存重映射到用户空间
    	//参数1:用户空间传递过来的虚拟内存的信息
    	//参数2:用户空间虚拟地址的起始
    	//参数3:页帧号(即物理地址在哪个页上)
    	//参数4:用户空间虚拟地址大小
    	//参数5:用户空间虚拟地址权限
    	ret = remap_pfn_range(vma, vma->vm_start, phy_addr >> PAGE_SHIFT, vma->vm_end-vma->vm_start, vma->vm_page_prot);
    	if(ret < 0)
    	{
    		printk("remap_pfn_range error\n");
    		return -EFAULT;
    	}
    	return 0;
    }
    
  5. (应用层)实现映射

    //实现映射
    	//参数1:指定文件被映射到进程空间的起始地址,给NULL则内核自己创建空间。
    	//参数2:进程地址空间的字节数,从开头offset个字节开始算起。
    	//参数3:指定共享内存的访问权限。
    	//参数4:选共享内存。
    	//参数5:fd。
    	//参数6:offset。
    	addr = mmap(NULL, PAGE_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    
  6. 将应用层内容拷贝到内核

    memset(addr,'\0',PAGE_SIZE);
    	//拷贝到内核空间
    	memcpy(addr,string, strlen(string));
    
  7. 读取内核内容

    	len = read(fd,buff,sizeof(buff));
    	//以字符串打印要最后要加'\0'
    	buff[len] = '\0';
    	printf("buff=%s\n",buff);
    
  8. 释放地址

    munmap(addr, PAGE_SIZE);
    

六、原子操作

  1. 原子初始化

    //原子操作底层表现为一条汇编指令,所以他们再执行过程中不会被别的代码路径中断,而且执行速度很快。
    //---使用原子操作可不让多个进程同时进入内核---
    //原子操作初始化
    static atomic_t flag = ATOMIC_INIT(1);
    
  2. 在open里做自减测试

    static int open_atomic(struct inode * node, struct file *filp)
    {
    	printk("-----------%s----------\n",__FUNCTION__);
    	//atomic_dec_and_test:先自减(i--),然后测试结果是否为0,如果为0,整个表达式的值为真,否则为假
    	if(!atomic_dec_and_test(&flag))
    	{
    		atomic_inc(&flag);
    		return -EBUSY;
    	}
    	return 0;
    }
    
  3. 在release里自加测试

    static int release_atomic (struct inode * node, struct file *filp)
    {
    	printk("-----------%s----------\n",__FUNCTION__);
    	atomic_inc(&flag);
    	return 0;
    }
    

七、信号灯(限制一定数量线程运行)

  1. 初始化信号量

    struct semaphore sema;//初始化信号灯结构体
    //初始化信号量,如果有5个资源就写5
    //模块开始加载(mmap_drv_init)时,初始化count = 1
    	sema_init(&sema,1);
    
  2. 获得信号量(获得信号量的线程就可以跑)

    static int open_atomic(struct inode * node, struct file *filp)
    {
    	//获得信号量,如果不允许更多任务获取信号量,调用此函数将使任务处于休眠状态,直到信号量释放。
    	//count-1,count = 0时下一个进程休眠
    	down(&sema);
    	printk("-----------%s----------\n",__FUNCTION__);
    	return 0;
    }
    
  3. 释放信号量(释放了线程就结束)

    static int release_atomic (struct inode * node, struct file *filp)
    {
    	//释放信号量
    	//count++
    	up(&sema);
    	printk("-----------%s----------\n",__FUNCTION__);
    	return 0;
    }
    

八、锁

  1. 创建并初始化一个锁

    //创造一个锁的结构体
    struct mutex lock;
    //初始化一个锁
    mutex_init(&lock);
    
  2. 上锁

    //上锁(同信号灯,拿到锁的进程跑,拿不到的休眠。一次只能跑一个进程,拿不到锁的进程要休眠排队)
    	mutex_lock(&lock);
    
  3. 解锁

    //释放锁
    	mutex_unlock(&lock);
    

九、自旋锁(当一个进程和中断一起访问时,谁拿到锁谁跑,没拿到锁的休眠)

  1. 上锁

    spin_lock();
    
  2. 解锁

    spin_unlock();
    
    (线程)
    .....
    spin_lock();
    a++;
    spin_unlock();
    .....
    --------------------------------	
    (中断)
    .....
    a--;
    ....
    //这里线程先拿到锁所以a++先跑完才到a--
    

十、input子系统

1、输入子系统框架及重要路径

1.1 input子系统框架

//查找设备节点名
cd /sys/class/input/event0
cat name
//查看设备号
cat /proc/devices
//查找设备节点
ls /dev/input/*

在这里插入图片描述

2、重要结构体

struct input_dev {
	const char *name;
	const char *phys;
	const char *uniq;
	struct input_id id;
	unsigned long propbit[BITS_TO_LONGS(INPUT_PROP_CNT)];
	unsigned long evbit[BITS_TO_LONGS(EV_CNT)];  //能够产生哪种类型的数据
	unsigned long keybit[BITS_TO_LONGS(KEY_CNT)]; //能够产生哪些按键数据
	unsigned long relbit[BITS_TO_LONGS(REL_CNT)]; //能够产生哪些相对坐标类型数据
	unsigned long absbit[BITS_TO_LONGS(ABS_CNT)]; //能够产生哪些绝对坐标类型数据
	unsigned long mscbit[BITS_TO_LONGS(MSC_CNT)];
	unsigned long ledbit[BITS_TO_LONGS(LED_CNT)];
	unsigned long sndbit[BITS_TO_LONGS(SND_CNT)];
	unsigned long ffbit[BITS_TO_LONGS(FF_CNT)];
	unsigned long swbit[BITS_TO_LONGS(SW_CNT)];
	unsigned int hint_events_per_packet;
	unsigned int keycodemax;
	unsigned int keycodesize;
	void *keycode;
}

//内核封装的结构体,可以用来装任何输入设备的数据
struct input_event {
	struct timeval time;
	__u16 type; //输入数据的类型
	__u16 code; //输入数据的键
	__s32 value;//输入数据的值
};

3、编程思路

struct input_dev *dev;
int probe(struct platform_device * pdev)
{
	// 1.创建input_dev对象
	key->dev = devm_input_allocate_device(&pdev->dev);
	if(IS_ERR(key->dev))
	{
		printk("input_allocate_device error\n");
		ret = PTR_ERR(key->dev);
		goto kzalloc_err2;
	}
	
	//2.初始化input_dev对象
	set_bit(EV_KEY,key->dev->evbit);
	set_bit(KEY_A,key->dev->keybit);
	set_bit(KEY_B,key->dev->keybit);
	set_bit(KEY_C,key->dev->keybit);
	//一定要命名,要不然识别不了外设
	key->dev->name = "stm32mp157_key"; //设置名称,可以在/sys/class/input/device/name中看,驱动设备节点到底是谁
	
	// 3.注册input_dev对象
	ret = input_register_device(key->dev);
	if(ret < 0)
	{
		printk("input_register_device error\n");
		goto kzalloc_err2;
	}
}

int remove(struct platform_device * pdev)
{
	input_unregister_device(key->dev);
}

//中断处理函数
irqreturn_t drv_handler(int irq_no, void * data)
{
	//上报数据
    input_event(dev, EV_KEY, KEY_A, 0);
    //handler层会产生堵塞,唤醒中断
    input_sync(dev);
}

十一、用户态uinput(未看)

1. 概念

参考Linux 5.x内核:
Documentation\input\uinput.rst
drivers\input\misc\uinput.c
uinput是一个内核模块(驱动),它允许应用程序模拟输入设备(input_dev)。
应用程序通过访问`/dev/uinput``/dev/input/uinput`:
* 创建一个虚拟的输入设备
* 设置它的属性
* APP发送数据给它,让它产生输入事件
* uinput就会把这些输入事件分发给其他使用者(APP或内核里其他模块)

如图:

在这里插入图片描述

2. 实现过程

1> 配置内核

make menuconfig arch=arm
-> Device Drivers
  -> Input device support
      -> Miscellaneous devices
         <M> User level driver support

2> 编译模块

drivers/input/misc/uinput.ko

3> 编译应用程序

参考:《input_key2》

4> 运行应用程序

./uinput_test &

5> 查看现象

[root@fsmp1a drv_module]# cat /dev/input/event0|hexdump
0000000 500a 3875 0b92 0001 0003 0000 037a 0000
0000010 500a 3875 0b92 0001 0003 0001 037a 0000
0000020 500a 3875 0b92 0001 0000 0000 0000 0000
0000030 500f 3875 0c3c 0001 0003 0000 0384 0000
0000040 500f 3875 0c3c 0001 0003 0001 0384 0000
0000050 500f 3875 0c3c 0001 0000 0000 0000 0000
0000060 5014 3875 0cd8 0001 0003 0000 038e 0000
0000070 5014 3875 0cd8 0001 0003 0001 038e 0000
0000080 5014 3875 0cd8 0001 0000 0000 0000 0000
0000090 5019 3875 0d6f 0001 0003 0000 0398 0000
00000a0 5019 3875 0d6f 0001 0003 0001 0398 0000
00000b0 5019 3875 0d6f 0001 0000 0000 0000 0000

十二、I2C和smbus总线

一. I2C协议

1. 硬件连接

I2C在硬件上的接法如下所示,主控芯片引出两条线SCL,SDA线,在一条I2C总线上可以接很多I2C设备,我们还会放一个上拉电阻

在这里插入图片描述

2. I2C传输数据的格式

1> 写操作

流程如下:
* 主芯片要发出一个start信号
* 然后发出一个设备地址(用来确定是往哪一个芯片写数据),方向(读/写,0表示写,1表示读)
* 从设备回应(用来确定这个设备是否存在),然后就可以传输数据
* 主设备发送一个字节数据给从设备,并等待回应
* 每传输一字节数据,接收方要有一个回应信号(确定数据是否接受完成),然后再传输下一个数据。
* 数据发送完之后,主芯片就会发送一个停止信号。
下图:白色背景表示"主→从",灰色背景表示"从→主"

在这里插入图片描述

2> 读操作

流程如下:
* 主芯片要发出一个start信号
* 然后发出一个设备地址(用来确定是往哪一个芯片写数据),方向(读/写,0表示写,1表示读)
* 从设备回应(用来确定这个设备是否存在),然后就可以传输数据
* 从设备发送一个字节数据给主设备,并等待回应
* 每传输一字节数据,接收方要有一个回应信号(确定数据是否接受完成),然后再传输下一个数据。
* 数据发送完之后,主芯片就会发送一个停止信号。
下图:白色背景表示"主→从",灰色背景表示"从→主"

在这里插入图片描述

3. I2C信号

I2C协议中数据传输的单位是字节,也就是8位。但是要用到9个时钟:前面8个时钟用来传输8数据,第9个时钟用来传输回应信号。传输时,先传输最高位(MSB)。
* 开始信号(S):SCL为高电平时,SDA山高电平向低电平跳变,开始传送数据。
* 结束信号(P):SCL为高电平时,SDA由低电平向高电平跳变,结束传送数据。
* 响应信号(ACK):接收器在接收到8位数据后,在第9个时钟周期,拉低SDA
* SDA上传输的数据必须在SCL为高电平期间保持稳定,SDA上的数据只能在SCL为低电平期间变化
I2C协议信号如下:

在这里插入图片描述

4. 协议细节

* 如何在SDA上实现双向传输?
  主芯片通过一根SDA线既可以把数据发给从设备,也可以从SDA上读取数据,连接SDA线的引脚里面必然有两个引脚(发送引脚/接受引脚)。
* 主、从设备都可以通过SDA发送数据,肯定不能同时发送数据,怎么错开时间?
  在9个时钟里,
  前8个时钟由主设备发送数据的话,第9个时钟就由从设备发送数据;
  前8个时钟由从设备发送数据的话,第9个时钟就由主设备发送数据。
* 双方设备中,某个设备发送数据时,另一方怎样才能不影响SDA上的数据?
  设备的SDA中有一个三极管,使用开极/开漏电路(三极管是开极,CMOS管是开漏,作用一样)
  如下图:

在这里插入图片描述

在这里插入图片描述

从真值表和电路图我们可以知道:

* 当某一个芯片不想影响SDA线时,那就不驱动这个三极管
* 想让SDA输出高电平,双方都不驱动三极管(SDA通过上拉电阻变为高电平)
* 想让SDA输出低电平,就驱动三极管(即基极给电)

从下面的例子可以看看数据是怎么传的(实现双向传输)。
举例:主设备发送(8bit)给从设备

* 前8个clk
  * 从设备不要影响SDA,从设备不驱动三极管
  * 主设备决定数据,主设备要发送1时不驱动三极管,要发送0时驱动三极管
  
* 第9个clk,由从设备决定数据
  * 主设备不驱动三极管
  * 从设备决定数据,要发出回应信号的话,就驱动三极管让SDA变为0
  * 从这里也可以知道ACK信号是低电平
从上面的例子,就可以知道怎样在一条线上实现双向传输,这就是SDA上要使用上拉电阻的原因。

为何SCL也要使用上拉电阻?
在第9个时钟之后,如果有某一方需要更多的时间来处理数据,它可以一直驱动三极管把SCL拉低。
当SCL为低电平时候,大家都不应该使用IIC总线,只有当SCL从低电平变为高电平的时候,IIC总线才能被使用。
当它就绪后,就可以不再驱动三极管,这是上拉电阻把SCL变为高电平,其他设备就可以继续使用I2C总线了。

对于IIC协议它只能规定怎么传输数据,数据是什么含义由从设备决定。

二. SMBus协议(未看)

1. SMBus协议简介

* Linux内核文档:Documentation\i2c\smbus-protocol.rst
* SMBus协议:http://www.smbus.org/specs/
SMBus是I2C协议的一个子集

在这里插入图片描述

SMBus和一般的I2C协议的差别:

* VDD的极限值不一样
  * I2C协议:范围很广,甚至讨论了高达12V的情况
  * SMBus:1.8V~5V

* 最小时钟频率、最大的`Clock Stretching `
  * Clock Stretching含义:某个设备需要更多时间进行内部的处理时,它可以把SCL拉低占住I2C总线
  * I2C协议:时钟频率最小值无限制,Clock Stretching时长也没有限制
  * SMBus:时钟频率最小值是10KHz,Clock Stretching的最大时间值也有限制

* 地址回应(Address Acknowledge)
  * 一个I2C设备接收到它的设备地址后,是否必须发出回应信号?
  * I2C协议:没有强制要求必须发出回应信号
  * SMBus:强制要求必须发出回应信号,这样对方才知道该设备的状态:busy,failed,或是被移除了

* SMBus协议明确了数据的传输格式
  * I2C协议:它只定义了怎么传输数据,但是并没有定义数据的格式,这完全由设备来定义
  * SMBus:定义了几种数据格式

* REPEATED START Condition(重复发出S信号)
  * 比如读EEPROM时,涉及2个操作:
    * 把存储地址发给设备
    * 读数据
  * 在写、读之间,可以不发出P信号,而是直接发出S信号:这个S信号就是`REPEATED START`
  * 如下图所示

在这里插入图片描述

因为很多设备都实现了SMBus,而不是更宽泛的I2C协议,所以优先使用SMBus。
即使I2C控制器没有实现SMBus,软件方面也是可以使用I2C协议来模拟SMBus。
所以:Linux建议优先使用SMBus。

2. SMBus协议详细(还没看)

1> SMBus Quick Command

只是用来发送一位数据:R/W#本意是用来表示读或写,但是在SMBus里可以用来表示其他含义。
比如某些开关设备,可以根据这一位来决定是打开还是关闭。

在这里插入图片描述

2> SMBus Receive Byte

I2C-tools中的函数:i2c_smbus_read_byte()。
读取一个字节,Host adapter接收到一个字节后不需要发出回应信号(上图中N表示不回应)。
Functionality flag: I2C_FUNC_SMBUS_READ_BYTE

在这里插入图片描述

3> SMBus Send Byte

I2C-tools中的函数:i2c_smbus_write_byte()。
发送一个字节。
Functionality flag: I2C_FUNC_SMBUS_WRITE_BYTE

在这里插入图片描述

4> SMBus Read Byte

I2C-tools中的函数:i2c_smbus_read_byte_data()。
先发出`Command Code`(它一般表示芯片内部的寄存器地址),再读取一个字节的数据。
上面介绍的`SMBus Receive Byte`是不发送Comand,直接读取数据
Functionality flag: I2C_FUNC_SMBUS_READ_BYTE_DATA

在这里插入图片描述

5> SMBus Read Word

I2C-tools中的函数:i2c_smbus_read_word_data()。
先发出`Command Code`(它一般表示芯片内部的寄存器地址),再读取2个字节的数据
Functionality flag: I2C_FUNC_SMBUS_READ_WORD_DATA

在这里插入图片描述

6> SMBus Write Byte

I2C-tools中的函数:i2c_smbus_write_byte_data()。
先发出`Command Code`(它一般表示芯片内部的寄存器地址),再发出1个字节的数据
Functionality flag: I2C_FUNC_SMBUS_WRITE_BYTE_DATA

在这里插入图片描述

7> SMBus Write Word

I2C-tools中的函数:i2c_smbus_write_word_data()。
先发出`Command Code`(它一般表示芯片内部的寄存器地址),再发出1个字节的数据。
Functionality flag: I2C_FUNC_SMBUS_WRITE_WORD_DATA

在这里插入图片描述

8> SMBus Block Read

I2C-tools中的函数:i2c_smbus_read_block_data()。
先发出`Command Code`(它一般表示芯片内部的寄存器地址),再发起度操作:
* 先读到一个字节(Block Count),表示后续要读的字节数
* 然后读取全部数据
Functionality flag: I2C_FUNC_SMBUS_READ_BLOCK_DATA

在这里插入图片描述

9> SMBus Block Write

I2C-tools中的函数:i2c_smbus_write_block_data()。
先发出`Command Code`(它一般表示芯片内部的寄存器地址),再发出1个字节的`Byte Conut`(表示后续要发出的数据字节数),最后发出全部数据。
Functionality flag: I2C_FUNC_SMBUS_WRITE_BLOCK_DATA

在这里插入图片描述

10> I2C Block Read

在一般的I2C协议中,也可以连续读出多个字节。
它跟`SMBus Block Read`的差别在于设备发出的第1个数据不是长度N
I2C-tools中的函数:i2c_smbus_read_i2c_block_data()。
先发出`Command Code`(它一般表示芯片内部的寄存器地址),再发出1个字节的`Byte Conut`(表示后续要发出的数据字节数),最后发出全部数据。
Functionality flag: I2C_FUNC_SMBUS_READ_I2C_BLOCK

在这里插入图片描述

11> I2C Block Write

在一般的I2C协议中,也可以连续发出多个字节。
它跟`SMBus Block Write`的差别在于发出的第1个数据不是长度N
I2C-tools中的函数:i2c_smbus_write_i2c_block_data()。
先发出`Command Code`(它一般表示芯片内部的寄存器地址),再发出1个字节的`Byte Conut`(表示后续要发出的数据字节数),最后发出全部数据
Functionality flag: I2C_FUNC_SMBUS_WRITE_I2C_BLOCK

在这里插入图片描述

12> SMBus Block Write - Block Read Process Call

先写一块数据,再读一块数据
Functionality flag: I2C_FUNC_SMBUS_BLOCK_PROC_CALL

在这里插入图片描述

13> Packet Error Checking (PEC)

PEC是一种错误校验码,如果使用PEC,那么在P信号之前,数据发送方要发送一个字节的PEC码(它是CRC-8码)。
以`SMBus Send Byte`为例,下图中,一个未使用PEC,另一个使用PEC:

在这里插入图片描述

三. Linux内核中I2C框架

应用层:APP
------------------------------------------------------
驱动层:
    i2c_driver层:自己写代码
    //完成从设备的初始化
    //实现和用户的交互,file_operation
    --------------------------------------------------
    i2c_core层:linux-5.4.31/drivers/i2c/i2c-core-base.c
    //维护i2c的框架
    --------------------------------------------------
    i2c_adapter层:linux-5.4.31/drivers/i2c/busses/i2c-stm32f7.c
    //初始化i2c控制器
    //实现i2c发送和接收数据的方法

在这里插入图片描述

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值