linux驱动开发(三)

1.设备树

1.1设备树功能简介

在过去的arm-linux内核源码树中arch/arm/plat-xxx和arch/arm/mach-xxx 等目录下边充斥着大量的垃圾代码,相当多数的代码只是在描述板级细节信息,而这些板级细节信息对于内核来说都是垃圾代码,比如板上的 platform_device、 resource、 i2c_board_info、 spi_board_info 以及各种硬件的 platform_data.

为了改变这种局面,ARM 社区开始在3.10内核后使用 PowerPC 等其他体系架构下已经使用的Flattened Device Tree(FDT). Device Tree 是一种描述硬件的数据结构,它起源于OpenFirmware(OF). 采用 Device Tree后,许多板级细节信息可以直接通过Device Tree传递给 linux内核,而不需要在arch/arm/plat-xxx和arch/arm/mach-xxx中进行大量的冗余编码,内核启动时会展开Device Tree并创建和注册相关的设备(比如platform_device,i2c_client,spi_device),同时驱动程序也会以新的方式和Devie Tree中定义的设备结点进行匹配. Device Tree的主要优势:对于相同SOC的不同板卡,只需更换设备树文件.dtb即可实现不同板卡的无差异支持

image-20230807094829915

system runtime参数(主要设置bootargs)
cpu的数量和类别
memory基地址和大小(设置DDR内存)
总线外设
内核基础设施使用情况,比如:
 中断控制器和中断使用情况
 DMA控制器和DMA使用情况
 GPIO控制器和GPIO使用情况
 CLOCK控制器和ClOCK使用情况
 PINCTRL控制器和PINCTRL使用情况

1.2设备树的文件类型及编译方式

stm32mp157a-fsmp1a.dts --------------类似于--------->xxx.c

stm32mp15xx-fsmp1x.dtsi --------------类似于--------->xxx.h

​ |

​ DTC (make dtbs)

​ |

stm32mp157a-fsmp1a.dtb 内核能够使用的二进制文件

1.3设备树如何被内核驱动使用

setenv bootcmd tftp 0xc2000040 uImage\; 

tftp 0xc4000000 stm32mp157a-fsmp1a.dtb\; 

bootm 0xc2000040 - 0xc4000000

在Linux内核启动的时候会解析设备树,会以树状的形式将设备信息存放在内存上。

驱动如果想要使用设备信息,只需要获取到对应的设备树节点,从内部获取设备信息即可。

1.4设备树的语法格式

Device Tree Usage - eLinux.org

1.4.1设备树的组成

设备树是有节点和属性组成的树状结构,属性就是键值对,节点可以包含属性和子节点。

1.4.2设备树的节点的组成

<name>[@<unit-address>]

name:节点的类型名,最多不超过31个字符
unit_address:这个是节点内部描述的地址,如果节点内有reg属性就要写地址,如果没有就不写地址

节点特性:

  1. 节点可以取别名

    demo:mynode@0x12345678{};

  2. 节点可以合并

    设备树中同名的节点会被合并成一个节点

  3. 节点可以被引用

    &demo{};

  4. 节点中的属性可以被删除

    /delete-property/uint;

1.4.3设备树中属性的组成

属性是简单的键值对,其中的值可以为空,也可以包含任意字节流。虽然数据类型没有编码到数据结构中,但是有一些基本的数据表示可以在设备树源文件中表示。

  • 文本字符串(以空结尾)用双引号表示:

    string-property = "a string";

  • 'Cells'是由尖括号分隔的32位无符号整数:

    cell-property = <0xbeef 123 0xabcd1234>;

  • 二进制数据(16进制表示的单字节的数)用方括号表示:

    binary-property = [00 01 23 45 67];

  • 不同表示形式的数据可以用逗号连接在一起:

    mixed-property = "a string", [01 23 45 67], <0x12345678>;

  • 逗号也用于创建字符串列表:

    string-list = "red fish", "blue fish";

默认含义的键值对:

  1. compatible = "厂商,设备名";

  2. reg属性

    reg是用来描述节点的地址信息的,需要配合#address-cells和#size-cells才能够使用

    #address-cells修饰reg一个元素组成中地址成员个数
    #size-cells修饰reg一个元素组成中长度个数

1.4.4设备树语法实例

/dts-v1/;   //设备树的版本号

/ {       // /{}; 根节点
    node1 {  // node1{}; 根节点的子节点
        a-string-property = "A string";  //属性
        a-string-list-property = "first string", "second string";
        a-byte-data-property = [01 23 34 56];
        child-node1 { //node1节点的子节点
            first-child-property; //空属性,只起到标识作用
            second-child-property = <1>;
            a-string-property = "Hello, world";
        };
        child-node2 { //node1节点的子节点
        };
    };
    node2 { // node2{}; 根节点的子节点
        an-empty-property; //空属性,只起到标识作用
        a-cell-property = <1 2 3 4>; /* each number (cell) is a uint32 */
        child-node1 { //node2节点的子节点
        };
    };
};

1.4.5设备节点添加

  1. 在stm32mp157a-fsmp1a.dts添加节点

    linux-5.10.61/arch/arm/boot/dts/stm32mp157a-fsmp1a.dts

  2. 添加的节点如下

    	mynode@0x12345678{
      a-string = "hello DC23041 everyone!";
      uint = <0x11223344 0xaabbccdd>;
      binary-data = [00 0c 29 7b f9 be];
      mixed-data = "list",<0x12345678>,[11 22 33];
     };
    
  3. 编译设备树

    make dtbs

  4. 将设备树文件拷贝到tftpboot目录下

    cp arch/arm/boot/dts/stm32mp157a-fsmp1a.dtb ~/tftpboot/

  5. 重启开发板,让内核解析设备树

  6. 在/proc/device-tree目录下可以看到添加的设备树节点

    image-20230807104639448

1.5驱动如何使用设备信息

1.5.1设备树节点结构体及相关函数

1.节点结构体device_node
    struct device_node {
        const char *name;      //节点名   "mynode"
        const char *full_name; //节点全名 "mynode@0x12345678"
        struct property *properties; //属性,在一个节点内核所有的属性构成一条单链表
        struct device_node *parent;  //父节点
        struct device_node *child;   //子节点
        struct device_node *sibling; //兄弟节点
    };
2.属性结构体property
    struct property {
        char *name; //键
        int length;     //值的字节数
        void *value; //值
        struct property *next; //指向下一个属性
    };

3.获取节点的函数
 struct device_node *of_find_node_by_path(const char *path)
    功能:通过节点的路径获取节点
    参数:
        @path:节点的路径 "/mynode@0x12345678"
 返回值:成功返回节点的首地址,失败返回NULL

 struct device_node *of_find_node_by_name(struct device_node *from,
  const char *name)
    功能:通过节点的名字获取节点
    参数:
        @from:填写NULL,表示从根节点开始解析
  @name:节点名
 返回值:成功返回节点的首地址,失败返回NULL
 
    struct device_node *of_find_compatible_node(struct device_node *from,
  const char *type, const char *compatible)
 功能:通过compatible获取节点
 参数:
        @from:NULL,表示从根节点开始解析
  @type:NULL
        @compatible:厂商和设备名
 返回值:成功返回节点的首地址,失败返回NULL
4.获取属性的函数
 struct property *of_find_property(const struct device_node *np,
      const char *name,
      int *lenp)
 功能:根据属性名获取属性结构体
 参数:
        @np:节点指针
        @name:键
        @lenp:获取到的值的长度
 返回值:成功返回property结构体指针,失败返回NULL

1.5.2设备树节点获取实例

通过路径名字匹配到节点

#include <linux/init.h>
#include <linux/module.h>
#include <linux/of.h>
// mynode@0x12345678{
// 	a-string = "hello DC23041 everyone!";
// 	uint = <0x11223344 0xaabbccdd>;
// 	binary-data = [00 0c 29 7b f9 be];
// 	mixed-data = "list",<0x12345678>,[11 22 33];
// };
struct device_node* node;

static int __init mynode_init(void)
{
    // node = of_find_node_by_path("/mynode@0x12345678");
    node = of_find_node_by_name(NULL, "mynode");
    if (node == NULL) {
        printk("find node failed!\n");
        return -ENODATA;
    }

    printk("key=%s,value=%s\n", node->properties->name, (char*)node->properties->value);
    printk("key=%s,value=%#x,%#x\n", node->properties->next->name,
        __be32_to_cpup(node->properties->next->value),
        __be32_to_cpup(node->properties->next->value+4));
    printk("len = %d\n",node->properties->next->length);
    return 0;
}
static void __exit mynode_exit(void)
{
}
module_init(mynode_init);
module_exit(mynode_exit);
MODULE_LICENSE("GPL"); 

通过compatible获取节点

#include <linux/init.h>
#include <linux/module.h>
#include <linux/of.h>
// mynode@0x12345678{
//  compatible = "hqyj,mynode";
// 	a-string = "hello DC23041 everyone!";
// 	uint = <0x11223344 0xaabbccdd>;
// 	binary-data = [00 0c 29 7b f9 be];
// 	mixed-data = "list",<0x12345678>,[11 22 33];
// };
struct device_node* node;

static int __init mynode_init(void)
{
    node = of_find_compatible_node(NULL, NULL, "hqyj,mynode");
    if (node == NULL) {
        printk("find node failed!\n");
        return -ENODATA;
    }

    printk("key=%s,value=%s\n", node->properties->name, (char*)node->properties->value);
    printk("key=%s,value=%#x,%#x\n", node->properties->next->next->name,
        __be32_to_cpup(node->properties->next->next->value),
        __be32_to_cpup(node->properties->next->next->value + 4));

    return 0;
}
static void __exit mynode_exit(void)
{
}
module_init(mynode_init);
module_exit(mynode_exit);
MODULE_LICENSE("GPL");

通过of_find_property获取属性结构体

#include <linux/init.h>
#include <linux/module.h>
#include <linux/of.h>
// mynode@0x12345678{
//  compatible = "hqyj,mynode";
// 	a-string = "hello DC23041 everyone!";
// 	uint = <0x11223344 0xaabbccdd>;
// 	binary-data = [00 0c 29 7b f9 be];
// 	mixed-data = "list",<0x12345678>,[11 22 33];
// };
struct device_node* node;
struct property *prop;
static int __init mynode_init(void)
{
    node = of_find_compatible_node(NULL, NULL, "hqyj,mynode");
    if (node == NULL) {
        printk("find node failed!\n");
        return -ENODATA;
    }

    prop = of_find_property(node, "uint", NULL);
    if (prop == NULL) {
        printk("find property failed!\n");
        return -ENODATA;
    }
    printk("key: %s, value: %#x\n", prop->name,__be32_to_cpup(prop->value));

    return 0;
}
static void __exit mynode_exit(void)
{
}
module_init(mynode_init);
module_exit(mynode_exit);
MODULE_LICENSE("GPL");

2.gpio子系统

2.1gpio子系统框架

image-20220720134334068

2.2gpio子系统的API

int of_get_named_gpio(struct device_node *np,
                     const char *propname, int index)
功能:解析设备树上的属性对应的gpio号
参数:
    @np:节点的指针
 @propname:键
    @index:索引号
返回值:成功返回gpio号,失败返回错误码
    
int gpio_request(unsigned gpio, const char *label)
功能:申请要使用的gpio
参数:
    @gpio:gpio编号(设备树)
    @label:名字(NULL)
返回值:成功返回0,失败返回错误码
        
int gpio_direction_input(unsigned gpio)
功能:设置管脚的方向为输入
参数:
    @gpio:gpio编号
返回值:成功返回0,失败返回错误码
        
int gpio_direction_output(unsigned gpio, int value) 
功能:设置管脚的方向为输出
参数:
    @gpio:gpio编号
    @value:1高  0低电平
返回值:成功返回0,失败返回错误码
        
void gpio_set_value(unsigned gpio, int value)
功能:设置电平的状态   
参数:
    @gpio:gpio编号
    @value:1高  0低电平
返回值:无
           
int gpio_get_value(unsigned gpio)
功能:获取电平的状态
参数:
    @gpio:gpio编号
返回值:1高电平,0低电平

void gpio_free(unsigned gpio)
功能:释放gpio
参数:
    @gpio:gpio编号
返回值:无

2.3gpio子系统的设备树

2.3.1画出硬件连接原理图

image-20230807144438498

2.3.2找出控制器的设备树

arch/arm/boot/dts/stm32mp151.dtsi
    
    gpioe: gpio@50006000 {
        gpio-controller;  //空属性,标识当前的节点是gpio控制器的节点
        #gpio-cells = <2>; //修饰子节点使用当前节点成员个数是2
        reg = <0x50006000 0x400>; //控制器地址及长度
        clocks = <&rcc GPIOE>;    //时钟
        st,bank-name = "GPIOE";   //GPIOE节点
        status = "disabled";      //控制器状态   "disabled"没有使能  "okay"使能
    };                            //控制器已经使能了

2.3.3参考内核帮助文档编写自己的设备树

/home/linux/linux-5.10.61/Documentation/devicetree/bindings/gpio/$ vi gpio.txt

image-20230807150426463

myleds{
 led1 = <&gpioe 10 0>;
 led2 = <&gpiof 10 0>;
    led3 = <&gpioe 8 0>;
};

2.4gpio子系统点灯实例

思路:

        1.在~/linux-5.10.61/arch/arm/boot/dts/stm32mp157a-fsmp1a.dts下添加myleds节点

        2.编写驱动文件

             init:

                i.获取节点

                        node=of_fine_node_path()

                ii.解析获取gpio号

                        gpiono=of_get_named_qpio()

                iii.申请gpio

                        ret=gpio_request()

                iv.设置gpio输出(可以直接在这一步设置高低电平)

                        ret=gpio_direction_output()

                v.设置高电平

                        gpio_set_value()

                exit:

                        关灯

                        释放gpio资源

注意别写错设备树文件,不然报错

myleds{
		led1=<&gpioe,10,0>;
		led2=<&gpiof,10,0>;
		led3=<&gpioe,8,0>;
	};

myled.c

#include <linux/init.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
//1.添加设备节点
// myleds{
// 		led1=<&gpioe,10,0>;
// 		led2=<&gpiof,10,0>;
// 		led3=<&gpioe,8,0>;
// 	}
//2.1创建节点结构体
struct device_node *node;
//3.1
int gpiono1,gpiono2,gpiono3,ret;
static int __init myled_init(void){
    //2.获取节点
    if(NULL==(node=of_find_node_by_path("/myleds"))){
        printk("of_find_node_by_path error\n");
        return -ENODATA;
    }
    //3.解析gpio号
    //led1
    if(0>(gpiono1=of_get_named_gpio(node,"led1",0))){
        printk("of_get_named_gpio error\n");
        return gpiono1;
    }
    //led2
    if(0>(gpiono2=of_get_named_gpio(node,"led2",0))){
        printk("of_get_named_gpio error\n");
        return gpiono2;
    }
    //led3
    if(0>(gpiono3=of_get_named_gpio(node,"led3",0))){
        printk("of_get_named_gpio error\n");
        return gpiono3;
    }
    //3.申请gpio
    ret=gpio_request(gpiono1,NULL);
    if(ret){
        printk("gpio_request error\n");
        return -ret;
    }
    ret=gpio_request(gpiono2,NULL);
    if(ret){
        printk("gpio_request error\n");
        return -ret;
    }
    ret=gpio_request(gpiono3,NULL);
    if(ret){
        printk("gpio_request error\n");
        return -ret;
    }
    //4.设置gpio输出模式
    //点亮三个灯
    ret=gpio_direction_output(gpiono1,1);
    if(ret){
        printk("gpio_direction_output error\n");
        return ret;
    }
    ret=gpio_direction_output(gpiono2,1);
    if(ret){
        printk("gpio_direction_output error\n");
        return ret;
    }
    ret=gpio_direction_output(gpiono3,1);
    if(ret){
        printk("gpio_direction_output error\n");
        return ret;
    }

    return 0;
}
static void __exit myled_exit(void){
    //关灯
    gpio_set_value(gpiono1,0);//led1
    gpio_set_value(gpiono2,0);//led2
    gpio_set_value(gpiono3,0);//led3
    //释放gpio
    gpio_free(gpiono1);
    gpio_free(gpiono2);
    gpio_free(gpiono3);
}
module_init(myled_init);
module_exit(myled_exit);
MODULE_LICENSE("GPL");

常用方法

myleds.h

#ifndef __MYLEDS_H__
#define __MYLEDS_H__

#define LED_ON _IO('o',0)
#define LED_OFF _IO('o',1)
#endif

myleds.c

#include <linux/fs.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include "myleds.h"
// myleds{
//     led1 = <&gpioe 10 0>;
//     led2 = <&gpiof 10 0>;
//     led3 = <&gpioe 8 0>;
// };
#define CNAME "myleds"
int major = 0;
struct class* cls;
struct device* dev;
struct device_node* node;
int gpiono;
// open
int myleds_open(struct inode* inode, struct file* filp)
{
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
    return 0;
}
// ioctl
long myleds_ioctl(struct file* filp, unsigned int cmd, unsigned long arg)
{
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
    switch(cmd){
        case LED_ON:
            gpio_set_value(gpiono, 1);
            break;
        case LED_OFF:
            gpio_set_value(gpiono, 0);
            break;
    }
    return 0;
}
// close
int myleds_close(struct inode* inode, struct file* filp)
{
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
    return 0;
}
struct file_operations fops = {
    .open = myleds_open,
    .unlocked_ioctl = myleds_ioctl,
    .release = myleds_close,
};
static int __init myleds_init(void)
{
    int ret;
    // 1.获取节点
    node = of_find_node_by_path("/myleds");
    if (node == NULL) {
        printk("find node failed!\n");
        ret = -ENODATA;
        goto err1;
    }
    // 2.解析得到gpio号
    gpiono = of_get_named_gpio(node, name[i], 0);
    if (gpiono < 0) {
        printk("get gpio failed!\n");
        ret = gpiono;
        goto err1;
    }
    // 3.申请gpio
    ret = gpio_request(gpiono, NULL);
    if (ret) {
        printk("request gpio failed!\n");
        goto err1;
    }
    // 4.设置gpio为输出
    ret = gpio_direction_output(gpiono, 0);
    if (ret) {
        printk("set gpio direction failed!\n");
        goto err2;
    }

    // 5.注册字符设备驱动
    major = register_chrdev(0, CNAME, &fops);
    if (major < 0) {
        printk("register_chrdev failed!\n");
        goto err2;
    }
    // 6.创建设备节点
    cls = class_create(THIS_MODULE, CNAME);
    if (IS_ERR(cls)) {
        printk("class_create failed!\n");
        ret = PTR_ERR(cls);
        goto err3;
    }
    dev = device_create(cls, NULL, MKDEV(major, 0), NULL, CNAME);
    if (IS_ERR(dev)) {
        printk("device_create failed!\n");
        ret = PTR_ERR(dev);
        goto err4;
    }

    return 0;
err4:
    class_destroy(cls);
err3:
    unregister_chrdev(major, CNAME);
err2:
    gpio_free(gpiono);
err1:
    return ret;
}
static void __exit myleds_exit(void)
{
    device_destroy(cls, MKDEV(major, 0));
    class_destroy(cls);
    unregister_chrdev(major, CNAME);
    gpio_set_value(gpiono, 0);
    gpio_free(gpiono);
}
module_init(myleds_init);
module_exit(myleds_exit);
MODULE_LICENSE("GPL");

test.c

#include "myleds.h"
#include <head.h>
#include <sys/ioctl.h>
int main(int argc, const char* argv[])
{
    int fd;
    if ((fd = open("/dev/myleds", O_RDWR)) == -1)
        PRINT_ERR("open error");

    while (1) {
        if (ioctl(fd, LED_ON))
            PRINT_ERR("ioctl error");
        sleep(1);

        if (ioctl(fd, LED_OFF))
            PRINT_ERR("ioctl error");

        sleep(1);
    }
    close(fd);
    return 0;
}

2.5gpio子系统的练习

  • 练习:使用gpio子系统将开发板上的六盏灯点亮

    myleds{
     core_leds{
         led1 = <&gpioz 5 0>;
            led2 = <&gpioz 6 0>;
            led3 = <&gpioz 7 0>;
        };
        extend_leds{
            led1 = <&gpioe 10 0>;
            led2 = <&gpiof 10 0>;
            led3 = <&gpioe 8 0>;
        };
    };
    

3.modprobe安装命令使用方法

3.1在内核目录下执行如下命令

make modules_install INSTALL_MOD_PATH=/home/linux/rootfs

3.2修改Makefile

arch?=arm
modname?=demo
ifeq ($(arch),arm)
 KERNELDIR:= /home/linux/linux-5.10.61
else
 KERNELDIR := /lib/modules/$(shell uname -r)/build/
endif
PWD := $(shell pwd)

all:
 make -C $(KERNELDIR) M=$(PWD) modules
install:
 make -C $(KERNELDIR) M=$(PWD) modules_install  INSTALL_MOD_PATH=/home/linux/rootfs
clean: 
 make -C $(KERNELDIR) M=$(PWD) clean
obj-m:= $(modname).o #编译编译的模块是demo

make install

模块会被拷贝到开发板/lib/modules/5.10.61/extra/目录下

3.3安装卸载模块的命令

安装命令:modprobe 模块名

卸载命令:modprobe -r 模块名

modprobe和insmod的区别?

  1. insmod安装模块的时候不会安装依赖模块,modprobe会安装依赖模块
  2. insmod安装的时候可以指定模块路径安装,modprobe安装模块的时候都是/lib/modules/5.10.61/kernel extra

4.Linux内核中的中断子系统

4.1异常处理流程

中断是基于硬件实现的,不管有没有Linux内核中断的执行都是一样的,中断

是异常的一种,遵从4大步3小步的执行流程。

  1. 将cpsr保存spsr中
  2. 修改cpsr的值
    • 设置为ARM模型
    • 设置为对应的异常模式
    • 如果有必要的话就进制中断
  3. 将LR的值保存起来(返回的地址)
  4. 设置PC跳转到对应的位置执行

4.2Linux内核中断处理流程

image-20220721105626462

4.3Linux内核中断子系统API

#include <linux/interrupt.h>
#include <linux/of_irq.h>
unsigned int irq_of_parse_and_map(struct device_node *np, int index)
功能:解析得到软中断号
参数:
    @np:节点指针
 @index:设备树节点中interrupts键对应值的下标
返回值:成功返回软中断号,失败返回0
    
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
     const char *name, void *dev)
功能:注册中断
参数:
    @irq:软中断号(设备树)
    @handler:中断处理函数指针
        //typedef irqreturn_t (*irq_handler_t)(int, void *);
        irqreturn_t key_irq_handle(int irqno,void *dev)
     {
      //中断的处理
         return IRQ_NONE;    //失败
         return IRQ_HANDLED; //成功
     }
 @flags:中断触发方式
         IRQF_TRIGGER_RISING  //上升沿
         IRQF_TRIGGER_FALLING //下降沿 
         IRQF_TRIGGER_HIGH  //高电平
            IRQF_TRIGGER_LOW  //低电平
         IRQF_SHARED          //共享中断
 @name:中断的名字
         cat /proc/interrupts
 @dev:给中断处理函数传递的参数
返回值:成功返回0,失败返回错误码

const void *free_irq(unsigned int irq, void *dev_id)
功能:释放中断
参数:
    @irq:软中断号
 @dev_id:给中断处理函数传递的参数
返回值:返回设备的名字

4.4Linux内核中断子系统设备树填写

4.4.1画出硬件连接原理图

image-20230808102736617

4.4.2找出控制器的设备树

stm32mp151.dtsi
    intc: interrupt-controller@a0021000 {
  compatible = "arm,cortex-a7-gic";
  #interrupt-cells = <3>;  //修饰子节点使用当前节点成员个数(中断)
  interrupt-controller;
  reg = <0xa0021000 0x1000>,
        <0xa0022000 0x2000>;
 }; 
 soc {
  compatible = "simple-bus";
  #address-cells = <1>;
  #size-cells = <1>;
  interrupt-parent = <&intc>; //父节点
        exti: interrupt-controller@5000d000 {
            compatible = "st,stm32mp1-exti", "syscon";
            interrupt-controller;
            #interrupt-cells = <2>;
            reg = <0x5000d000 0x400>;
        };
    };
    pinctrl: pin-controller@50002000 {
        #address-cells = <1>;
        #size-cells = <1>;
        interrupt-parent = <&exti>; //父节点        
        gpiof: gpio@50007000 {
            interrupt-controller;
            #interrupt-cells = <2>;  //修饰子节点使用当前节点成员个数(中断)
            reg = <0x50007000 0x400>;
            clocks = <&rcc GPIOF>;
            st,bank-name = "GPIOF";
            status = "disabled"; // 控制器已经使能了
        };
    };

4.4.3参考内核帮助文档编写自己的设备树

/home/linux/linux-5.10.61/Documentation/devicetree/bindings/interrupt-controller/$

vi interrupts.txt

image-20230808104101968

myirqs{
 interrupt-parent = <&gpiof>; //父节点
 interrupts = <9 0>,<7 0>,<8 0>; //对应中断管脚及触发方式0代表默认状态
};

1.5中断子系统实例(一个按键)

#include <linux/init.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
// myirqs{
// 	interrupt-parent = <&gpiof>; //父节点
// 	interrupts = <9 0>,<7 0>,<8 0>; //对应中断管脚及触发方式0代表默认状态
// };
struct device_node *node;
unsigned int irqno;
irqreturn_t key_irq_handle(int irq, void *dev)
{
    printk("key1 down...\n");
    return IRQ_HANDLED;
}
static int __init myirqs_init(void)
{
    int ret;
    // 1.获取节点
    node = of_find_node_by_path("/myirqs");
    if (node == NULL)
    {
        printk("of_find_node_by_path error\n");
        return -EINVAL;
    }
    // 2.获取软中断号
    irqno = irq_of_parse_and_map(node, 0);
    if(irqno == 0)
    {
        printk("irq_of_parse_and_map error\n");
        return -EINVAL;
    }
    // 3.注册中断
    ret = request_irq(irqno, key_irq_handle, IRQF_TRIGGER_FALLING, "myirqs", NULL);
    if(ret){
        printk("request_irq error\n");
        return ret;
    }
    return 0;
}
static void __exit myirqs_exit(void)
{
    free_irq(irqno, NULL);
}
module_init(myirqs_init);
module_exit(myirqs_exit);
MODULE_LICENSE("GPL");

4.6中断子系统实例(多个按键)

#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_irq.h>
// myirqs{
// 	interrupt-parent = <&gpiof>; //父节点
// 	interrupts = <9 0>,<7 0>,<8 0>; //对应中断管脚及触发方式0代表默认状态
// };
struct device_node* node;
unsigned int irqno[3];
irqreturn_t key_irq_handle(int irq, void* dev)
{
    switch ((int)dev) {
    case 0:
        printk("key1 down...\n");
        break;
    case 1:
        printk("key2 down...\n");
        break;
    case 2:
        printk("key3 down...\n");
        break;
    }
    return IRQ_HANDLED;
}
static int __init myirqs_init(void)
{
    int ret, i;
    // 1.获取节点
    node = of_find_node_by_path("/myirqs");
    if (node == NULL) {
        printk("of_find_node_by_path error\n");
        return -EINVAL;
    }
    for (i = 0; i < ARRAY_SIZE(irqno); i++) {
        // 2.获取软中断号
        irqno[i] = irq_of_parse_and_map(node, i);
        if (irqno[i] == 0) {
            printk("irq_of_parse_and_map error\n");
            return -EINVAL;
        }
        // 3.注册中断
        ret = request_irq(irqno[i], key_irq_handle,
            IRQF_TRIGGER_FALLING, "myirqs", (void*)i);
        if (ret) {
            printk("request_irq error\n");
            return ret;
        }
    }
    return 0;
}
static void __exit myirqs_exit(void)
{
    int i;
    for (i = 0; i < ARRAY_SIZE(irqno); i++) {
        free_irq(irqno[i], (void*)i);
    }
}
module_init(myirqs_init);
module_exit(myirqs_exit);
MODULE_LICENSE("GPL");

5.内核定时器

5.1内核定时器定时原理

  1. 内核当前时间如何获取?

    jiffies:内核时钟节拍数,从系统启动这一刻起这个jiffies(64bit)值就一直在增加。jiffies可以直接使用

    不需要包含头文件。

  2. 定时器每增加1走的时间?

    定时器的频率可以通过make menuconfig选配,选配后的选项在.config文件中保存着。

    开发板:CONFIG_HZ=100就是频率,定时器每增加1代表走10ms。

    Ubuntu:CONFIG_HZ=250就是频率,定时器每增加1代表走4ms。

5.2内核定时器的API

1.分配对象
    struct timer_list {
        struct hlist_node entry;   //构成内核链表成员
        unsigned long  expires; //定时器时间
        void   (*function)(struct timer_list *); //定时器处理函数
        u32			flags;      //标志位,一般写0
    };
        
    struct timer_list mytimer;
2.对象的初始化
    mytimer.expires = jiffies + HZ; //定时1s  #define HZ CONFIG_HZ
    timer_setup(&mytimer, 定时器处理函数, 0);
3.启动定时器
 void add_timer(struct timer_list *timer);
 //启动定时器,定时器启动之后只会执行一次
 
 mod_timer(struct timer_list *timer, unsigned long expires);
 //再次启动定时器
4.删除定时器
    int del_timer(struct timer_list * timer);
 //移除定时器

5.3内核定时器的实例

#include <linux/init.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/timer.h>
// myleds
// {
//     led1 = <&gpioe 10 0>;
//     led2 = <&gpiof 10 0>;
//     led3 = <&gpioe 8 0>;
// };
struct timer_list mytimer;
struct device_node* node;
int gpiono[3];
char* name[] = { "led1", "led2", "led3" };
// 定时器处理函数
void timer_led_handle(struct timer_list* timer)
{
    int i;
    for (i = 0; i < ARRAY_SIZE(name); i++) {
        gpio_set_value(gpiono[i],!gpio_get_value(gpiono[i]));
    }
    mod_timer(&mytimer, jiffies + HZ);
}
static int __init mytimer_init(void)
{
    int ret, i;
    // 1.获取节点
    node = of_find_node_by_name(NULL, "myleds");
    if (node == NULL) {
        printk("of_find_node_by_name error\n");
        return -EINVAL;
    }
    for (i = 0; i < ARRAY_SIZE(name); i++) {
        // 2.解析gpio号
        gpiono[i] = of_get_named_gpio(node, name[i], 0);
        if (gpiono[i] < 0) {
            printk("of_get_named_gpio error\n");
            return -EINVAL;
        }
        // 3.初始化LED
        ret = gpio_request(gpiono[i], name[i]);
        if (ret) {
            printk("gpio_request error\n");
            return ret;
        }
        ret = gpio_direction_output(gpiono[i], 0);
        if (ret) {
            printk("gpio_direction_output error\n");
            return ret;
        }
    }
    // 4.初始化定时器
    mytimer.expires = jiffies + HZ;
    timer_setup(&mytimer, timer_led_handle, 0);
    // 5.启动定时器
    add_timer(&mytimer);
    return 0;
}
static void __exit mytimer_exit(void)
{
    int i;
    del_timer(&mytimer);
    for (i = 0; i < ARRAY_SIZE(name); i++) {
        gpio_free(gpiono[i]);
    }
}
module_init(mytimer_init);
module_exit(mytimer_exit);
MODULE_LICENSE("GPL");

5.4内核定时器的练习

  1. 使用内核定时器让LED闪烁

  2. 使用内核定时器完成按键中断消抖工作

    image-20230808143423567

#include <linux/init.h>
#include <linux/module.h>
#include <linux/interrupt.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/timer.h>
#include <linux/of_irq.h>

// 三个按键中断并消抖:中断  定时器
struct device_node *node;
struct timer_list mytimer;
unsigned int irq[3];
int gpiono[3];
void timer_handle(struct timer_list *tim)
{
    // 按键按下后执行对应操作
    int i;
    for (i = 0; i < 3; i++)
    {
        if (!gpio_get_value(gpiono[i]))
        {
            printk("key%d down...\n", gpiono[i]);
        }
    }
}
irqreturn_t key_irq_handle(int irq, void *dev)
{
    // 执行定时器函数
    mod_timer(&mytimer, jiffies + 1); // 再次启动定时器
    return IRQ_HANDLED;
}
static int __init myirqs_init(void)
{
    int ret, i;
    // 1.获取节点
    if (NULL == (node = of_find_node_by_path("/myirqs")))
    {
        printk("of_find_get_node error\n");
        return -EINVAL;
    }
    // printk("111111111111111\n");
    // 4.初始化定时器
    mytimer.expires = jiffies + 1; // 10ms
    timer_setup(&mytimer, timer_handle, 0);
    add_timer(&mytimer);
    for (i = 0; i < 3; i++)
    {
        // 2.获取中断号
        if (0 == (irq[i] = irq_of_parse_and_map(node, i)))
        {
            printk("of_find_get_node error\n");
            return ret;
        }

        // 3.获取gpio号
        if (0 > (gpiono[i] = of_get_named_gpio(node, "keys", i)))
        {
            printk("of_get_named_gpio error\n");
            return gpiono[i];
        }
        // printk("22222222222222222\n");

        // 5.注册中断
        if (0 != (ret = request_irq(irq[i], key_irq_handle, IRQF_TRIGGER_FALLING, "myirqs", NULL)))
        {
            printk("request_irq error\n");
            return ret;
        }
    }

    return 0;
}
static void __exit myirqs_exit(void)
{
    int i;
    for (i = 0; i < 3; i++)
    {
        free_irq(irq[i], NULL);
    }
    del_timer(&mytimer);
}

module_init(myirqs_init);
module_exit(myirqs_exit);
MODULE_LICENSE("GPL");

 错误:有的时候会遇到这个问题,有的时候就能运行成功,原因是将第四步初始化定时器写在了for循环内,现在将定时器拿出来就没有问题了,写的时候需注意

6.输入子系统驱动

6.1输入子系统驱动简介

输入类设备都是通过输入子系统进行数据上报的。比如鼠标,键盘,触摸屏等驱动

都是需要通过输入子系统驱动上报数据的,上报的数据是input_event结构体,这个

input_event结构体被上报/dev/input/event*节点中了。

6.2输入子系统驱动框架结构

image-20230808163115379

6.3输入子系统驱动API

~/linux-5.10.61/Documentation$ vi input/input-programming.rst

1.分配对象
    struct input_dev *key_dev;  
 key_dev = input_allocate_device();
2.对象初始化
    set_bit(EV_KEY, key_ev->evbit); // 指定上报的是键盘类事件
    set_bit(KEY_L, key_ev->keybit); // 指定上报l键
    set_bit(KEY_S, key_ev->keybit); // 指定上报s键
    set_bit(KEY_ENTER, key_ev->keybit); // 指定上报enter键
3.注册
    int input_register_device(struct input_dev *dev)
4.注销
    void input_unregister_device(struct input_dev *dev)
5.上报数据
    void input_event(struct input_dev *dev,
   unsigned int type, unsigned int code, int value) //上报数据
 void input_sync(struct input_dev *dev) //同步

6.4键盘驱动的实例

keybroad.c

#include <linux/init.h>
#include <linux/input.h>
#include <linux/interrupt.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_irq.h>
// myirqs{
// 	interrupt-parent = <&gpiof>; //父节点
// 	interrupts = <9 0>,<7 0>,<8 0>; //对应中断管脚及触发方式0代表默认状态
// };
struct device_node* node;
unsigned int irqno[3];
struct input_dev* key_ev;
irqreturn_t key_irq_handle(int irq, void* dev)
{
    switch ((int)dev) {
    case 0:
        input_event(key_ev, EV_KEY, KEY_L, 1);
        input_sync(key_ev);
        input_event(key_ev, EV_KEY, KEY_L, 0);
        input_sync(key_ev);
        break;
    case 1:
        input_event(key_ev, EV_KEY, KEY_S, 1);
        input_sync(key_ev);
        input_event(key_ev, EV_KEY, KEY_S, 0);
        input_sync(key_ev);
        break;
    case 2:
        input_event(key_ev, EV_KEY, KEY_ENTER, 1);
        input_sync(key_ev);
        input_event(key_ev, EV_KEY, KEY_ENTER, 0);
        input_sync(key_ev);
        break;
    }
    return IRQ_HANDLED;
}
static int __init myirqs_init(void)
{
    int ret, i;
    // 1.获取节点
    node = of_find_node_by_path("/myirqs");
    if (node == NULL) {
        printk("of_find_node_by_path error\n");
        return -EINVAL;
    }
    for (i = 0; i < ARRAY_SIZE(irqno); i++) {
        // 2.获取软中断号
        irqno[i] = irq_of_parse_and_map(node, i);
        if (irqno[i] == 0) {
            printk("irq_of_parse_and_map error\n");
            return -EINVAL;
        }
        // 3.注册中断
        ret = request_irq(irqno[i], key_irq_handle,
            IRQF_TRIGGER_FALLING, "myirqs", (void*)i);
        if (ret) {
            printk("request_irq error\n");
            return ret;
        }
    }
    // 4.注册输入子系统
    key_ev = input_allocate_device();
    if (key_ev == NULL) {
        printk("input_allocate_device error\n");
        return -ENOMEM;
    }
    set_bit(EV_KEY, key_ev->evbit); // 指定上报的是键盘类事件
    set_bit(KEY_L, key_ev->keybit); // 指定上报l键
    set_bit(KEY_S, key_ev->keybit); // 指定上报s键
    set_bit(KEY_ENTER, key_ev->keybit); // 指定上报s键

    ret = input_register_device(key_ev);
    if (ret) {
        printk("input_register_device error\n");
        return ret;
    }
    return 0;
}
static void __exit myirqs_exit(void)
{
    int i;
    input_unregister_device(key_ev);
    for (i = 0; i < ARRAY_SIZE(irqno); i++) {
        free_irq(irqno[i], (void*)i);
    }
}
module_init(myirqs_init);
module_exit(myirqs_exit);
MODULE_LICENSE("GPL");

test.c

可以使用如下程序测试键盘上报的数据,也可以通过exec 0</dev/tty1

#include <head.h>
#include <linux/input.h>
#include <sys/select.h>
int main(int argc, const char* argv[])
{
    int fd1, fd2, ret;
    fd_set rfds;
    struct input_event key_ev;
    char buf[128] = { 0 };
    if ((fd2 = open("/dev/input/event0", O_RDWR)) == -1)
        PRINT_ERR("open keybroad error");
    while (1) {
        FD_ZERO(&rfds);
        FD_SET(fd2, &rfds);

        if ((ret = select(fd2 + 1, &rfds, NULL, NULL, NULL)) == -1)
            PRINT_ERR("select error");

        if (FD_ISSET(fd2, &rfds)) {
            memset(&key_ev, 0, sizeof(key_ev));
            read(fd2, &key_ev, sizeof(key_ev));
            printf("type = %d,code = %d,value=%d\n",
                key_ev.type,key_ev.code,key_ev.value);
        }
    }

    close(fd2);
    return 0;
}

7.中断底半部

7.1中断底半部的简介

什么中断底半部?为什么要设计中断底半部?

中断顶半部不能做延时,耗时,甚至休眠的操作,也就是说在中断顶半部中只能够

做简短的、紧急的、不耗时的操作。但是有的时候又希望在中断到来的时候做尽可能

多的操作,所以两者就产生了矛盾,内核为了解决这一矛盾引入了中断底半部的机制。

也就是在中断底半部中,可以做不紧急的,耗时的操作。中断底半部的机制分为软中

断、tasklet、工作队列。

eg:

在网卡中断到来的时候,想要在中断处理函数中接收网络数据包,这个过程就是耗时

的操作,所以内核将这一操作放在中中断底半部机制中(软中断)。

7.2中断底半部之tasklet

7.2.1tasklet的工作原理

tasklet是基于软中断实现的,特点和软中断一样,但是tasklet没有个数限制,而软中断

是有个数限制的,因为tasklet是通过链表来实现的。tasklet这种中断底半部的机制工作

在中断上下文,它是中断的一个部分,不能够脱离中断单独执行,在tasklet的底半部中

不能做休眠的操作,只能做相对耗时或延时的操作。tasklet这种底半部的机制在中断

顶半部执行即将结束的时候被调用执行的。

7.2.2tasklet的API

1.分配对象
    struct tasklet_struct
    {
        struct tasklet_struct *next; //构成tasklet的链表
        unsigned long state;         //是否被触发的标志位
        atomic_t count;              //触发的次数
        bool use_callback;           //如果为真使用callback,如果为假使用func
        union {
            void (*func)(unsigned long data); //旧版本接口
            void (*callback)(struct tasklet_struct *t); //新版本接口
        };
        unsigned long data; //向底半部处理函数传递的参数
    };
 struct tasklet_struct tasklet;
2.初始化对象
    void tasklet_init(struct tasklet_struct *t,
    void (*func)(unsigned long), unsigned long data) //旧版本的初始化

    void tasklet_setup(struct tasklet_struct *t,
     void (*callback)(struct tasklet_struct *)) //新版本的初始化
3.开启底半部
    void tasklet_schedule(struct tasklet_struct *t)

7.2.3tasklet的代码实例

#include <linux/init.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/delay.h>
struct device_node *node;
struct tasklet_struct tasklet;
int irqno;
void key_irq_tasklet(struct tasklet_struct *task){
    int i=10;
    while(i--){
        printk("key_irq_tasklet i = %d\n",i);
        mdelay(500); //延时500ms
    }
}
irqreturn_t key_irq_handle(int irq, void *dev){
    // 5.开启底半部
    tasklet_schedule(&tasklet);
    return IRQ_HANDLED;
}
static int __init myirqs_init(void){
    int ret;
    //1.获取节点
    if (NULL == (node = of_find_node_by_path("/myirqs")))
    {
        printk("of_find_get_node error\n");
        return -EINVAL;
    }
    // 2.获取中断号
    if (0 == (irqno= irq_of_parse_and_map(node, 0)))
    {
        printk("of_find_get_node error\n");
        return ret;
    }
    //3.初始化myirqs
    tasklet_setup(&tasklet,key_irq_tasklet);
    // 4.注册中断
    if (0 != (ret = request_irq(irqno, key_irq_handle, IRQF_TRIGGER_FALLING, "myirqs", NULL)))
    {
        printk("request_irq error\n");
        return ret;
    }
    return 0;

}
static void __exit myirqs_exit(void){
    free_irq(irqno, NULL);
}
module_init(myirqs_init);
module_exit(myirqs_exit);
MODULE_LICENSE("GPL");

7.3中断底半部之工作队列

7.3.1工作队列的工作原理

在内核启动的时候默认会启动一个events的线程,这个线程默认处于休眠的状态,在这个

线程中维护了一个队列,如果你想让这个线程执行你的代码,你只需要向这个线程工作队列

中提交一个队列项,然后唤醒这个休眠的线程执行你的代码即可。工作队列工作在进程上下

。工作队列没有个数限制可以脱离中断单独执行。在工作队列中可以执行延时,耗时,

甚至休眠的操作

7.3.2工作队列的API

1.分配对象
    struct work_struct {
        atomic_long_t data; //向底半部处理函数传递的的数据
        struct list_head entry; //队列项
        work_func_t func;  //底半部处理函数
        //typedef void (*work_func_t)(struct work_struct *work);
    }
 struct work_struct work;
2.对象的初始化
    INIT_WORK(&work, 工作队列底半部处理函数);  
3.调用执行
    schedule_work(struct work_struct *work)
4.等待工作队列执行完在卸载驱动
    cancel_work_sync(struct work_struct *work)

7.3.3工作队列的代码实例

#include <linux/init.h>
#include <linux/module.h>
#include <linux/of.h>

8.platform总线驱动

8.1device、bus、driver模型

在Linux内核中所有总线驱动都遵从设备驱动的模型,总线驱动的模型如下图:

设备模型

内核在设计这些总线驱动模型的时候将一个驱动分为了三个部分device、bus、driver。device是用来描述硬件设备的,bus是总线用来链接device和driver,driver是用来描述驱动的对象。在内核中所有的device放在内核的klist_devices的链表中管理,而内核中所有的driver放在klist_driver中管理。内核中的device和driver通过bus完成关联。当device和driver匹配成功之后执行驱动的probe函数,在probe函数中就可以完成操作硬件了。当卸载任何一方驱动的时候都会执行驱动中的remove函数

8.2platform总线驱动的原理

platform总线驱动遵从设备模型,platform是Linux内核抽象出来的软件代码,并没有真实的总线协议与之对应。platform总线驱动的思想就是要将设备信息和设备驱动进行分离。platform_device和platform_driver通过总线匹配成功之后会执行驱动中probe函数,在probe函数中驱动就能够拿到设备信息。

image-20220518102931467

8.3platform总线驱动的API

1.platform_device端
    1.1分配对象初始化
    struct platform_device {
        const char *name;   //用于匹配的名字
        int  id;          //总线号 PLATFORM_DEVID_AUTO
        struct device dev; //父类
        u32		num_resources; //设备信息成员个数
        struct resource *resource; //设备信息结构体
    };
    struct device{ //父类
     void (*release)(struct device *dev);
    };
    struct resource {
        resource_size_t start; //起始资源 0x50006000     0x12345678        71
        resource_size_t end;   //结束资源 0x50006000+3   0x12345678 +49    71
        unsigned long flags;   //资源类型 IORESOURCE_IO  IORESOURCE_MEM  IORESOURCE_IRQ
    };
    1.2注册
         int platform_device_register(struct platform_device *);
    1.3注销
         void platform_device_unregister(struct platform_device *);
2.platform_driver端
    1.分配并初始化对象
    struct platform_driver {
        int (*probe)(struct platform_device *); //匹配成功执行的函数
        int (*remove)(struct platform_device *);//分离的时候执行的函数
        struct device_driver driver; //父类
        const struct platform_device_id *id_table; //2idtable匹配
    };
 struct device_driver {//父类
  const char *name;  //1按照名字匹配
        const struct of_device_id *of_match_table; //3.设备树匹配
    }
    2.注册
      platform_driver_register(drv)   
    3.注销
      void platform_driver_unregister(struct platform_driver *);
 4.一键注册注销的宏
      module_platform_driver(变量名);

8.4platform总线驱动的实例(name)

pdev.c

#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
struct resource res[] = {
    [0] = {
        .start = 0x12345678,
        .end = 0x12345678+49,
        .flags = IORESOURCE_MEM,
    },
    [1] = {
        .start = 71,
        .end = 71,
        .flags = IORESOURCE_IRQ,
    },
};
void pdev_release(struct device *dev)
{
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
}
struct platform_device pdev = {
    .name = "duang duang duang",
    .id = PLATFORM_DEVID_AUTO,
    .dev = {
        .release = pdev_release,
    },
    .resource = res,
    .num_resources = ARRAY_SIZE(res),
};
static int __init pdev_init(void)
{
    return platform_device_register(&pdev);
}
static void __exit pdev_exit(void)
{
    platform_device_unregister(&pdev);
}
module_init(pdev_init);
module_exit(pdev_exit);
MODULE_LICENSE("GPL");

pdrv.c

#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
int pdrv_probe(struct platform_device *pdev)
{
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
    
    return 0;
}
int pdrv_remove(struct platform_device *pdev)
{
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
    return 0;
}
struct platform_driver pdrv = {
    .probe = pdrv_probe,
    .remove = pdrv_remove,
    .driver = {
        .name = "duang duang duang",
    },
}; 
static int __init pdrv_init(void)
{
    return platform_driver_register(&pdrv);
}
static void __exit pdrv_exit(void)
{
    platform_driver_unregister(&pdrv);
}
module_init(pdrv_init);
module_exit(pdrv_exit);
MODULE_LICENSE("GPL");

8.5platform驱动获取设备信息的过程

struct resource *platform_get_resource(struct platform_device *pdev,
           unsigned int type, unsigned int index)
功能:在platform驱动中获取设备信息
参数:
    @pdev:platform_device的结构体指针
    @type:类型 IORESOURCE_IO   IORESOURCE_MEM   IORESOURCE_IRQ
 @index:同类型资源的下标
返回值:成功返回结构体指针,失败返回NULL

int platform_get_irq(struct platform_device *pdev, unsigned int index)
功能:获取中断类型的资源
参数:
 @pdev:platform_device的结构体指针
    @index:中断类型资源的下标
返回值:成功返回中断号,失败返回错误码

pdev.c

#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
struct resource res[] = {
    [0] = {
        .start = 0x12345678,
        .end = 0x12345678 + 49,
        .flags = IORESOURCE_MEM,
    },
    [1] = {
        .start = 71,
        .end = 71,
        .flags = IORESOURCE_IRQ,
    },
};
void pdev_release(struct device* dev)
{
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
}
struct platform_device pdev = {
    .name = "duang duang duang",
    .id = PLATFORM_DEVID_AUTO,
    .dev = {
        .release = pdev_release,
    },
    .resource = res,
    .num_resources = ARRAY_SIZE(res),
};
static int __init pdev_init(void)
{
    return platform_device_register(&pdev);
}
static void __exit pdev_exit(void)
{
    platform_device_unregister(&pdev);
}
module_init(pdev_init);
module_exit(pdev_exit);
MODULE_LICENSE("GPL");

pdrv.c

#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
struct resource *res;
int irqno;
int pdrv_probe(struct platform_device *pdev)
{
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
    res = platform_get_resource(pdev,IORESOURCE_MEM,0);
    if(res == NULL){
        printk("platform_get_resource error\n");
        return -EINVAL;
    }
    irqno = platform_get_irq(pdev,0);
    if(irqno < 0){
        printk("platform_get_irq error\n");
        return -EINVAL;
    }
    printk("addr = %#llx,irqno = %d\n",res->start,irqno);
    return 0;
}
int pdrv_remove(struct platform_device *pdev)
{
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
    return 0;
}
struct platform_driver pdrv = {
    .probe = pdrv_probe,
    .remove = pdrv_remove,
    .driver = {
        .name = "duang duang duang",
    },
}; 
static int __init pdrv_init(void)
{
    return platform_driver_register(&pdrv);
}
static void __exit pdrv_exit(void)
{
    platform_driver_unregister(&pdrv);
}
module_init(pdrv_init);
module_exit(pdrv_exit);
MODULE_LICENSE("GPL");

8.6platform驱动idtable匹配方式

8.6.1为什么需要idtable匹配方式

如果按照name匹配,driver只能对应一个device,但是有的时候某些厂商

生成的一系列设备为这一系列的设备写一个驱动,所以此时驱动中就需要把

它兼容的设备名字全部写上去,保存这些名字的结构体就叫做idtable。

8.6.2idtable匹配方式的实例

#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/mod_devicetable.h>
struct resource* res;
int irqno;
int pdrv_probe(struct platform_device* pdev)
{
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
    printk("name = %s,driver_data = %ld\n",
    pdev->id_entry->name,pdev->id_entry->driver_data);
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (res == NULL) {
        printk("platform_get_resource error\n");
        return -EINVAL;
    }
    irqno = platform_get_irq(pdev, 0);
    if (irqno < 0) {
        printk("platform_get_irq error\n");
        return -EINVAL;
    }
    printk("addr = %#llx,irqno = %d\n", res->start, irqno);
    return 0;
}
int pdrv_remove(struct platform_device* pdev)
{
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
    return 0;
}

// idtable匹配方式
const struct platform_device_id idtable[] = {
    {"hello0",111},
    {"hello1",222},
    {"hello2",333},
    {},
};

struct platform_driver pdrv = {
    .probe = pdrv_probe,
    .remove = pdrv_remove,
    .driver = {
        .name = "duang duang duang",
    },
    .id_table = idtable,
};
static int __init pdrv_init(void)
{
    return platform_driver_register(&pdrv);
}
static void __exit pdrv_exit(void)
{
    platform_driver_unregister(&pdrv);
}
module_init(pdrv_init);
module_exit(pdrv_exit);
MODULE_LICENSE("GPL");

8.7platform驱动设备树匹配方式

在有设备树的内核版本中会将platform_device里面描述的设备信息放到设备树中。

也就说不需要编写platform_device这部分代码了,内核启动的时候会根据设备树节

点自动创建platform_device,并完成注册过程。

8.7.1platform设备树匹配设备树节点填写

linux-5.10.61/arch/arm/boot/dts/stm32mp157a-fsmp1a.dts

	myplatform{
        compatible = "hqyj,myplatform";
        reg = <0x12345678 49>;
        interrupt-parent = <&gpiof>;
        interrupts = <9 0>;
    };

8.7.2platform设备树匹配驱动实例

#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/mod_devicetable.h>
int irqno;
struct resource *p;
int pdrv_probe(struct platform_device *pdev){//获取device的设备信息
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);

    if(NULL==(p=platform_get_resource(pdev,IORESOURCE_MEM,0))){
        printk("platform_get_resource error\n");
        return EINVAL;
    }
    if(0>(irqno=platform_get_irq(pdev,0))){
        printk("platform_get_irq error\n");
        return irqno;
    }
    printk("%#x\n",p->start);
    printk("%d\n",irqno);
    return 0;
}
int pdrv_remove(struct platform_device *pdev){
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
    return 0;
}
const struct of_device_id oftable[]={
    {.compatible="nyz,myplatform"},
    {},//防止match在匹配的过程中越界访问
};
//1.分配对象并初始化
struct platform_driver pdrv={
    .probe=pdrv_probe,
    .remove=pdrv_remove,
    .driver={.name="hi hi hi",
        .of_match_table=oftable,},
};
module_platform_driver(pdrv);
MODULE_LICENSE("GPL");

8.8platform总线驱动练习

1.使用驱动代码实现如下要求
a.应用程序通过阻塞的IO模型来读取status的值
b.status是内核驱动中的一个变量(status代表LED1的状态)
c.status的值随着按键按下而改变(按键中断)
 例如status=0 按下按键status=1 ,再次按下按键status=0
d.在按下按键的时候需要同时将LED1的状态取反
e.驱动中需要编写字符设备驱动
f.驱动中需要自动创建设备节点
g.这个驱动需要的所有设备信息放在设备树的同一个节点中
h.要求使用platform实现上述驱动

test.c

#include <stdio.h>
#include <head.h>
int main(int argc, char const *argv[])
{
    int fd,status;
    if(-1==(fd=open("/dev/pdrv",O_RDWR))){
        ERRLOG("open error\n");
    }
    while(1){
        if(-1==(read(fd,&status,4))){
            ERRLOG("read error\n");
        }
        printf("status=%d\n",status);
    }
    
    close(fd);
    return 0;
}

pdrv.c

#include <linux/init.h>
#include <linux/mod_devicetable.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/timer.h>
#include <linux/fs.h>
#include <linux/of.h>
// test{
//     compatible="nyz,test";
//     interrupt-parent=<&gpiof>;
//     interrupts=<9 0>;
//     led1 = <&gpioe 10 0>;
// };
#define CNAME "pdrv"
int irqno,keygpio,ledgpio;
int status=0;
int major=0;
int condition=0;
struct timer_list mytimer;
struct class *cls;
struct device *dev;
wait_queue_head_t wq_head;
//按下按键->消抖,status=1->亮灯
void timer_handle(struct timer_list *tim){
    if(!gpio_get_value(keygpio)){
        status=!status;
        gpio_set_value(ledgpio,status);
        condition=1;
    }
}
irqreturn_t key_led_irq_handle(int irq,void *dev){
    //消抖
    mod_timer(&mytimer, jiffies + 1);
    return IRQ_HANDLED;
}
int pdrv_open(struct inode *inode, struct file *filp){
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
    return 0;
}
ssize_t pdrv_read(struct file *filp, char __user *ubuf, size_t size, loff_t *offs){
    int ret;
    //1.判断是否是非阻塞
    if(filp->f_flags &O_NONBLOCK){
        return EINVAL;
    }else{
        //2.放入等待队列
        ret = wait_event_interruptible(wq_head, condition);
        if (ret) {
            printk("wait_event_interruptible by signal...\n");
            return -ERESTARTSYS;
        }
        //3..将status拷贝到用户空间
        if (size > sizeof(status)){
            size = sizeof(status);
        }   
        ret = copy_to_user(ubuf, &status, size);
        if (ret) {
            printk("copy_to_user failed\n");
            return -EIO;
        }
    }
    condition=0;
    return 0;
}
int pdrv_close(struct inode *inode, struct file *filp){
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
    return 0;
}
struct file_operations fops = {
    .open = pdrv_open,
    .read = pdrv_read,
    .release = pdrv_close,
};
//1.分配platform_driver并初始化
int pdrv_probe(struct platform_device *pdev){
    int ret;
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
    //驱动函数在probe里
    //2.初始化定时器
    mytimer.expires = jiffies + 1; // 10ms
    timer_setup(&mytimer, timer_handle, 0);
    add_timer(&mytimer);
    //3.获取中断号
    if(0>(irqno=platform_get_irq(pdev,0))){
        printk("platform_get_irq error\n");
        ret=irqno;
        goto ERR1;
    }
    //4.获取gpio号 需要node pdev->dev.of_node
    if(0>(ledgpio=of_get_named_gpio(pdev->dev.of_node,"leds",0))){
        printk("of_get_named_gpio error\n");
        ret=ledgpio;
        goto ERR1;
    }
    if(0>(keygpio=of_get_named_gpio(pdev->dev.of_node,"keys",0))){
        printk("of_get_named_gpio error\n");
        ret=keygpio;
        goto ERR1;
    }
    //5.申请gpio号
    ret=gpio_request(ledgpio,NULL);
    if(ret){
        printk("gpio_request error\n");
        goto ERR1;
    }
    //6.设置gpio输出
    ret=gpio_direction_output(ledgpio,status);
    if(ret){
        printk("gpio_direction_output error\n");
        goto ERR2;
    }
    //7.注册中断
    if(0!=(ret=request_irq(irqno,key_led_irq_handle,IRQF_TRIGGER_FALLING,"test",NULL))){
        printk("request_irq error\n");
        goto ERR2;
    }
    //8.注册字符设备驱动
    if(0>(major=register_chrdev(0,CNAME,&fops))){
        printk("register_chrdev error\n");
        goto ERR3;
    }
    //9.创建节点 cls dev
    cls=class_create(THIS_MODULE,CNAME);
    if(IS_ERR(cls)){
        printk("class_create error\n");
        ret = PTR_ERR(cls);
        goto ERR4;
    }
    dev = device_create(cls, NULL, MKDEV(major, 0), NULL, CNAME);
    if (IS_ERR(dev)) {
        printk("device_create failed!\n");
        ret = PTR_ERR(dev);
        goto ERR5;
    }    
    //10.初始化等待队列头
    init_waitqueue_head(&wq_head);
    return 0;
ERR5:
    class_destroy(cls);
ERR4:
    unregister_chrdev(major,CNAME);
ERR3:
    free_irq(irqno,NULL);
ERR2:
    gpio_free(ledgpio);
ERR1:
    return ret;
}
int pdrv_remove(struct platform_device *pdev){
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
    device_destroy(cls,MKDEV(major,0));
    class_destroy(cls);
    unregister_chrdev(major,CNAME);
    free_irq(irqno,NULL);
    gpio_free(ledgpio);
    return 0;
}
//根据设备树获取
const struct of_device_id oftable[]={
    { .compatible = "nyz,test" },
    {},
};
struct platform_driver pdrv={
    .probe=pdrv_probe,
    .remove=pdrv_remove,
    .driver={
        .name="hi",
        .of_match_table=oftable,
    },
};
module_platform_driver(pdrv);
MODULE_LICENSE("GPL");

9.i2c总线协议回顾

9.1i2c有几根线

scl:时钟线

sda:数据线

9.1.2i2c有几种信号

起始信号(start):当scl为高电平的时候,sda从高到低跳变 (主机发出)

停止信号(stop):当scl为高电平的时候,sda从低到高跳变 (主机发出)

应答信号(Ack):在第九个时钟周期的时候,sda管脚是低电平(从机可以给主机应答,主机也可以给从机应答)

非应答信号(No Ack):在第九个时钟周期的时候,sda管脚是高电平(一般是主机不给从机应答)

9.1.3i2c的读写协议

  1. 写协议

    start+(7bit从机地址+1bit(写0))+ack+(8bit/16bit寄存器地址)+ack+(8bit/16bit数据)+ack+stop

    image-20230810101156255

  2. 读协议

    start+(7bit从机地址+1bit(写0))+ack+(8bit/16bit寄存器地址)+ack+

    start+(7bit从机地址+1bit(读1))+ack+(8bit/16bit从机给主机回复数据)+No Ack+stop

    image-20230810101557998

9.1.4i2c的7位/8位/10位从机地址寻址

7-bit 8-bit and 10-bit I2C Slave Addressing - Total Phase

image-20230810102237425

image-20230810102304516

image-20230810102422450

9.1.5i2c总线的特点

i2c总线特点:i2c是同步的,半双工的,串行的,具备应答机制的总线协议

9.1.6i2c总线速率问题

100KHz 400KHz 1MHz(其他平台3.4MHz)

9.2i2c总线驱动

9.2.1i2c总线驱动的框架结构

image-20220723101129370

注:在编写i2c设备驱动前需要先将i2c的核心层和总线驱动层选配到内核中,否则设备驱动无法工作。

控制器驱动配置到内核中:

│ -> Device Drivers

│ -> I2C support

│ -> I2C Hardware Bus support

│ <*> STMicroelectronics STM32F7 I2C support

核心层选配到内核中:

│ -> Device Drivers

│ I2C support

| -*- I2C support

9.2.2i2c设备驱动层API

1.分配并初始化对象
    struct i2c_driver {
        int (*probe)(struct i2c_client *client, const struct i2c_device_id *id);
        //匹配成功执行的函数
        int (*remove)(struct i2c_client *client);
        //分离的时候执行的函数
        struct device_driver driver;
        //父类
        const struct i2c_device_id *id_table;
        //1.idtable匹配方式
    };
 struct device_driver {
        const char  *name; //i2c中name不用于匹配,但是不必须填充
        const struct of_device_id *of_match_table; //2.设备树匹配
    }
2.注册
    #define i2c_add_driver(driver) \
          i2c_register_driver(THIS_MODULE, driver)
3.注销
    void i2c_del_driver(struct i2c_driver *driver);
4.一键注册,注销的宏
    module_i2c_driver(变量名);

9.2.3i2c设备驱动代码实例

#include <linux/init.h>
#include <linux/module.h>
#include <linux/i2c.h>
// probe函数
static int si7006_probe(struct i2c_client *client, 
    const struct i2c_device_id *id)
{
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
    return 0;
}
// remove函数
static int si7006_remove(struct i2c_client *client)
{
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
    return 0;
}
// oftable
static const struct of_device_id oftable[] = {
    {.compatible = "hqyj,si7006"},
    {},
};
struct i2c_driver si7006 = {
    .probe = si7006_probe,
    .remove = si7006_remove,
    .driver = {
        .name = "si7006",
        .of_match_table = oftable,
    },
};
module_i2c_driver(si7006);
MODULE_LICENSE("GPL");

 安装成功,节点可以识别到

9.2.4si7006的设备树编写

9.2.4.1画出硬件连接图

image-20230706102614242

9.2.4.2找出控制器的设备树

arch/arm/boot/dts/stm32mp151.dtsi
    i2c1: i2c@40012000 {
        compatible = "st,stm32mp15-i2c";
        reg = <0x40012000 0x400>;
        interrupt-names = "event", "error";
        interrupts-extended = <&exti 21 IRQ_TYPE_LEVEL_HIGH>,
        <&intc GIC_SPI 32 IRQ_TYPE_LEVEL_HIGH>;
        clocks = <&rcc I2C1_K>;
        resets = <&rcc I2C1_R>;
        #address-cells = <1>;
        #size-cells = <0>;
        dmas = <&dmamux1 33 0x400 0x80000001>,
        <&dmamux1 34 0x400 0x80000001>;
        dma-names = "rx", "tx";
        power-domains = <&pd_core>;
        st,syscfg-fmp = <&syscfg 0x4 0x1>;
        wakeup-source;
        i2c-analog-filter;
        status = "disabled";
    };

9.2.4.3参考内核帮助文档编写自己的设备树

/home/linux/linux-5.10.61/Documentation/devicetree/bindings/i2c

参考st,stm32-i2c.yaml文件

 参考i2c-st.txt

pinctrl不会写可以参考pinctrl的文件 在上一级目录里有

&i2c1{
    pinctrl-names   = "default","sleep";
    pinctrl-0   = <&i2c1_pins_b>;  //工作状态管脚设置
    pinctrl-1   = <&i2c1_sleep_pins_b>; //休眠状态管脚设置
    clock-frequency = <400000>;  //控制器的频率
    i2c-scl-rising-time-ns = <185>;                                         
    i2c-scl-falling-time-ns = <20>;
 status = "okay";  //将控制器使能

 si7006@40{        //si7006节点
     compatible = "hqyj,si7006";
        reg = <0x40>;
    };
};

 检查节点,忘记加引用了

9.2.5i2c驱动数据封装和发送

9.2.5.1i2c数据封装和发送需要用到的结构体

当i2c驱动进入到probe函数的时候会收到内核传递的i2c_client结构体,这个结构体就是

设备驱动和总线驱动匹配成功时候内核创建的,这个i2c_client结构体是用来携带信息的

结构体(从机地址,标志位,控制器驱动对象等等)。

struct i2c_client {
 unsigned short flags; //i2c的标志位
  //I2C_CLIENT_TEN	10位从机地址
  //I2C_CLIENT_SLAVE	标识它是从机
  //I2C_CLIENT_SCCB	摄像头特有的总线协议SCCB
 unsigned short addr;    //从机地址
 char name[I2C_NAME_SIZE]; //名字
 struct i2c_adapter *adapter; //控制器驱动对象
 struct device dev;   //dev.of_node 节点结构体指针
 int irq;        //软中断号
};

设备驱动和总线驱动数据交互的时候使用的就是i2c_msg结构体

struct i2c_msg {
 __u16 addr;      //从机地址  client->addr
 __u16 flags;     //  1读   0写
 __u16 len;   //消息的长度
 __u8 *buf;   //指向消息的首地址
};

9.2.5.2i2c消息结构体的封装

有多少个起始信号就有多少个消息,消息的长度是以字节来表示的

  1. 写消息结构体封装

    start+(7bit从机地址+1bit(写0))+ack+(8bit/16bit寄存器地址)+ack+(8bit/16bit数据)+ack+stop

    (7bit从机地址+1bit(写0))+(8bit寄存器地址)+(8bit数据)

    char w_buf[] = {reg,data};
    struct i2c_msg w_msg = {
     .addr = client->addr,
        .flags = 0,
        .len = 2,
        .buf = w_buf,
    };
    
  2. 读消息结构体封装

    start+(7bit从机地址+1bit(写0))+ack+(8bit/16bit寄存器地址)+ack+

    start+(7bit从机地址+1bit(读1))+ack+(8bit/16bit从机给主机回复数据)+No Ack+stop

    (7bit从机地址+1bit(写0))+(8bit寄存器地址)+

    (7bit从机地址+1bit(读1))+(8bit从机给主机回复数据)

    char r_buf[] = {reg};
    char data;
    struct i2c_msg r_msg[] = {
     [0] = {
         .addr = client->addr,
            .flags=0,
         .len = 1,
            .buf = r_buf,
        },
     [1] = {
         .addr = client->addr,
            .flags = 1,
         .len = 1,
            .buf = &data,
        },
    };
    

9.2.5.3i2c消息结构体的发送

int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
功能:消息结构体的发送
参数:
    @adap:总线驱动对象client->adapter(发给谁)
    @msgs:消息结构体的首地址(发什么)
    @num:消息的个数(发多少)
返回值:成功返回num,否则就是失败

9.2.6si7006驱动读取温湿度实例

si7006.h

#ifndef __SI7006_H__
#define __SI7006_H__

10.spi总线驱动

10.1spi总线协议回顾

  1. spi总线特点

    SPI 是串行外设接口(Serial Peripheral Interface)的缩写。它

    是 Motorola 公司推出的一种同步串行接口技术,是一种高

    速的,全双工,同步的通信总线。

    SPI优点:

    支持全双工通信,通信简单,数据传输速率快

    1):高速、同步、全双工、非差分、总线式

    2):主从机通信模式

    缺点:

    没有指定的流控制,没有应答机制确认是否接收到数据,

    所以跟IIC总线协议比较在数据的可靠性上有一定的缺陷。

  2. spi管脚及模式

    可以一主机多从机,具体和那个从机通讯通过cs片选决定。

    MISO :主机输入,从机输出

    MOSI :主机输出,从机输入

    SCK :时钟线(只能主机控制)

    CS :片选线

    数据传输的四种方式:

    CPOL(时钟极性) : 0:时钟起始位低电平,1:时钟起始为高电平

    CPHA(时钟相位) :0:第一个时钟周期采样,1:第二个时钟周期采样

  3. spi协议解析

    image-20220520092309567

    CPOL=0,CPHA=0:此时空闲态时,SCLK处于低电平,数据

    采样是在第1个边沿,也就是 SCLK由低电平到高电平的跳变,

    所以数据采样是在上升沿,数据发送是在下降沿。

    CPOL=0,CPHA=1:此时空闲态时,SCLK处于低电平,数据

    发送是在第1个边沿,也就是 SCLK由低电平到高电平的跳变,

    所以数据采样是在下降沿,数据发送是在上升沿。

    CPOL=1,CPHA=0:此时空闲态时,SCLK处于高电平,数据

    采集是在第1个边沿,也就是 SCLK由高电平到低电平的跳变,

    所以数据采集是在下降沿,数据发送是在上升沿。

    CPOL=1,CPHA=1:此时空闲态时,SCLK处于高电平,数据

    发送是在第1个边沿,也就是 SCLK由高电平到低电平的跳变,

    所以数据采集是在上升沿,数据发送是在下降沿。

10.2spi驱动框架结构

image-20220621145745804

将stm32mp157a中spi控制器设备树和核心层选配到内核中

在linux_5.10.61/drivers/spi下    vi Makefile中找对应厂商

然后在顶层目录下make menuconfig搜

找到对应路径选中

spi控制器驱动配置:
 Device Drivers --->
    [*] SPI support  ---> 
   <*>   STMicroelectronics STM32 SPI controller      
spi核心层配置:
 Device Drivers --->
    [*] SPI support  ---> 
重新编译内核
 make uImage LOADADDR=0xc2000000
将编译好的内核拷贝到tftpboot目录下
   cp arch/arm/boot/uImage ~/tftpboot/

10.3spi设备驱动的API

1.分配并初始化对象
    struct spi_driver {
        int (*probe)(struct spi_device *spi);
        //匹配成功执行probe
        int (*remove)(struct spi_device *spi);
        //分离执行remove
        struct device_driver driver;
        //父类
    };
 struct device_driver{
        const char  *name; //名字
        const struct of_device_id *of_match_table; //设备树匹配
    };
2.注册
    #define spi_register_driver(driver) \
 __spi_register_driver(THIS_MODULE, driver)
3.注销
    void spi_unregister_driver(struct spi_driver *sdrv)
4.一键注册,注销
    module_spi_driver(spi_driver变量名);

10.4spi总线驱动代码实例1

#include <linux/init.h>
#include <linux/module.h>
#include <linux/spi/spi.h>
int m74hc595_probe(struct spi_device* spi)
{
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
    return 0;
}
int m74hc595_remove(struct spi_device* spi)
{
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
    return 0;
}
const struct of_device_id oftable[] = {
    { .compatible = "hqyj,m74hc595", },
    {},
};
struct spi_driver m74hc595 = {
    .probe = m74hc595_probe,
    .remove = m74hc595_remove,
    .driver = {
        .name = "m74ch595",
        .of_match_table = oftable,
    },
};
module_spi_driver(m74hc595);
MODULE_LICENSE("GPL");

可以运行,具体能不能用还要添加设备树节点之后才能验证 

10.5编写spi相关的设备树

10.5.1画出硬件连接关系图

image-20220922151830863

10.5.2找出控制器的设备树

arch/arm/boot/dts/stm32mp151.dtsi  文件内找spi4

	spi4: spi@44005000 {
   #address-cells = <1>; //修饰子节点reg地址个数
   #size-cells = <0>;    //修饰子节点reg长度个数
   compatible = "st,stm32h7-spi";
   reg = <0x44005000 0x400>;
   interrupts = <GIC_SPI 84 IRQ_TYPE_LEVEL_HIGH>;
   clocks = <&rcc SPI4_K>;
   resets = <&rcc SPI4_R>;
   dmas = <&dmamux1 83 0x400 0x01>,
          <&dmamux1 84 0x400 0x01>;
   dma-names = "rx", "tx";
   power-domains = <&pd_core>;
   status = "disabled";  //控制器默认没有使能
  };

10.5.3参考内核帮助文档编写自己的设备树

 Documentation/devicetree/bindings/spi 里找对应厂商的文档,看里面的例子

发现需要增加片选和自己的spi设备子节点,还需将status改成okay,

查看控制器是否使能 /proc/device-tree/soc/spi 下查看

当前写的节点

除了gpioe11还有三个引脚没写,参考pinctrl的文件stm32mp15-pinctrl.dtsi

在里面找spi4

发现两个合适的节点,完善自己的节点

编译下载运行

 设备树可以识别到

&spi4{
    pinctrl-names   = "default","sleep";
    pinctrl-0   = <&spi4_pins_b>;  //工作状态管脚设置
    pinctrl-1   = <&spi4_sleep_pins_b>; //休眠状态管脚设置
    cs-gpios = <&gpioe 11 0>;  //片选管脚
 status = "okay";           //控制器使能
    
 m74hc595@0{
     compatible = "hqyj,m74hc595";
        reg = <0>;  //片选下标编号
        spi-max-frequency = <10000000>; //工作频率
        //spi-cpol; //如果写了就代表cpol=1,否则cpol=0
  //spi-cpha; //如果写了就代表cpha=1,否则cpha=0
    };
};

10.6spi数据收发接口

int spi_write(struct spi_device *spi, const void *buf, size_t len) //发数据
int spi_read(struct spi_device *spi, void *buf, size_t len)       //接收数据
int spi_write_then_read(struct spi_device *spi,                   //同时收发
  const void *txbuf, unsigned n_tx,
  void *rxbuf, unsigned n_rx);

10.7使用spi让4个数码管显示5

#include <linux/init.h>
#include <linux/module.h>
#include <linux/spi/spi.h>
int m74hc595_probe(struct spi_device* spi)
{
    char w_buf[] = {0xf,0x6d}; //4个数码管显示5
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
    spi_write(spi,w_buf,sizeof(w_buf));
    return 0;
}
int m74hc595_remove(struct spi_device* spi)
{
    char w_buf[] = {0xf,0}; //4个数码管不显示
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
    spi_write(spi,w_buf,sizeof(w_buf));
    return 0;
}
const struct of_device_id oftable[] = {
    { .compatible = "hqyj,m74hc595", },
    {},
};
struct spi_driver m74hc595 = {
    .probe = m74hc595_probe,
    .remove = m74hc595_remove,
    .driver = {
        .name = "m74ch595",
        .of_match_table = oftable,
    },
};
module_spi_driver(m74hc595);
MODULE_LICENSE("GPL");

10.8通过应用程序控制数码管显示数据

m74hc595.h

#ifndef __M74HC595_H__
#define __M74HC595_H__

#define SEG_WHICH _IOW('k',0,int)
#define SEG_DAT  _IOW('k',1,int)
#endif

m74hc595.c

#include <linux/init.h>
#include <linux/module.h>
#include <linux/spi/spi.h>
#include <linux/fs.h>
#include "m74hc595.h"
/*
 &spi4 { 
    pinctrl-names = "default", "sleep"; 
    pinctrl-0 = <&spi4_pins_b>; 
    pinctrl-1 = <&spi4_sleep_pins_b>; 
    cs-gpios = <&gpioe 11 GPIO_ACTIVE_HIGH>;
    status = "okay";
    m74hc595@0{
     compatible = "hqyj,m74hc595";
     reg = <0>;
        spi-max-frequency = <40000000>;
    };
};
*/
#define NAME "m74hc595"
int major = 0;
struct class *cls;
struct device *dev;
struct spi_device *gspi;
u8 code[] = {
 0x3f, //0
 0x06, //1
 0x5b, //2
 0x4f, //3
 0x66, //4
 0x6d, //5
 0x7d, //6
 0x07, //7
 0x7f, //8
 0x6f, //9
 0x77, //A
 0x7c, //b
 0x39, //c
 0x5e, //d
 0x7b, //e
 0x71, //f
};

u8 which[] = {
 0x1, //sg0
 0x2, //sg1
 0x4, //sg2
 0x8, //sg3
};

int m74hc595_open(struct inode *inode, struct file *file)
{
 printk("%s:%d\n",__func__,__LINE__);
 return 0;
}
long m74hc595_ioctl(struct file *file, unsigned int cmd, unsigned long args)
{ 
 switch(cmd){
  case SEG_WHICH:
   spi_write(gspi,&which[args],1);
   break;
  case SEG_DAT:
   spi_write(gspi,&code[args],1);
   break;
  default: printk("ioctl error\n");break;
 }
 
 return 0;
}

int m74hc595_close(struct inode *inode, struct file *file)
{
 printk("%s:%d\n",__func__,__LINE__);
 return 0;
}
struct file_operations fops = {
 .open = m74hc595_open,
 .unlocked_ioctl = m74hc595_ioctl,
 .release = m74hc595_close,
};

int m74hc595_probe(struct spi_device *spi)
{
 u8 buf[2] = {0xf,0x0};
 printk("%s:%d\n",__func__,__LINE__);
 gspi = spi;
 spi_write(gspi,buf,ARRAY_SIZE(buf));
 major = register_chrdev(0,NAME,&fops);
 if(major < 0){
  printk("register chrdev error\n");
  return major;
 }

 cls = class_create(THIS_MODULE,NAME);
 if(IS_ERR(cls)){
  printk("class create  error\n");
  return PTR_ERR(cls);
 }
 dev = device_create(cls,NULL,MKDEV(major,0),NULL,NAME);
 if(IS_ERR(dev)){
  printk("device create  error\n");
  return PTR_ERR(dev);
 }
 return 0;
}

int m74hc595_remove(struct spi_device *spi)
{
 printk("%s:%d\n",__func__,__LINE__);
 device_destroy(cls,MKDEV(major,0));
 class_destroy(cls);
 unregister_chrdev(major,NAME);
 return 0;
}

const struct of_device_id of_match[] = {
 {.compatible = "hqyj,m74hc595",},
 {},
};
struct spi_driver m74hc595 = {
 .probe = m74hc595_probe,
 .remove = m74hc595_remove,
 .driver = {
  .name = "hello_m74hc595",
  .of_match_table = of_match,
 }, 
};
module_spi_driver(m74hc595);
MODULE_LICENSE("GPL");

test.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include "m74hc595.h"

int main(int argc, const char *argv[])
{
 int which=0;
 int data=0;
 int fd;
 fd = open("/dev/m74hc595",O_RDWR);
 if(fd < 0){
  perror("open error");
  return -1;
 }
 while(1){
  ioctl(fd,SEG_WHICH,which++);
  ioctl(fd,SEG_DAT,data++);
  if(which >= 4)which=0;
  if(data >= 16)data = 0;
  sleep(1);
 }
 close(fd);
 return 0;
}

11.Linux内核中内存分配

11.1地址的概念

物理地址:在datasheet中能够查到的地址称之为物理地址,实际设备的操作地址;

虚拟地址、线性地址:在操作系统程序员能够操作的地址称之为虚拟地址;

逻辑地址:将程序进行反汇编之后,其中能够看到的地址称之为逻辑地址;

11.2内存管理

段式管理:x86架构,分段管理

段式管理

页式管理:arm架构,页式管理

页式管理

11.3内存映射关系

linux内核地址空间

image-20220322142329287

11.4linux内核空间内存分配的函数

void *kmalloc(size_t s, gfp_t gfp)
功能:分配对应的虚拟内存
参数:size:分配内存区的大小
  flags:内存分配标志
  GFP_KERNEL:内核可能被休眠,用于进程上下文中
  GFP_ATOMIC:处理紧急的事务,用在中断上下文
返回值:对应虚拟地址
特点:最大128k , 分配虚拟地址,其虚拟地址空间连续,
      物理地址空间也是连续,分配的内存必须是2的次幂的形式
类似函数:kzalloc = kmalloc+memset(,0,):分配虚拟内存区并清零

void kfree(const void *x)
功能:释放对应的虚拟内存
参数:x:虚拟内存的起始地址
返回值:无

void *vmalloc(unsigned long size)
功能:分配对应的虚拟内存
参数:size:分配内存区的大小
返回值:对应虚拟地址
特点:分配虚拟地址,其虚拟地址空间连续,
但是物理地址空间不一定连续

void vfree(const void *addr)
功能:释放对应的虚拟内存
参数:addr:虚拟内存区的首地址
返回值:无
    
unsigned long __get_free_page(gfp_t gfp)
功能:分配一个页的内存 4K
void free_page(unsigned long addr)
释放一个页的内存

unsigned long __get_free_pages(gfp_t gfp_mask, get_order(57600))
功能:分配多个页的内存
57600-->2^n :第二个参数填写的是n
n = get_order(57600)

void free_pages(unsigned long addr, unsigned long order)
释放多个页的内存

11.5实例

#include <linux/init.h>
#include <linux/module.h>
#include <linux/slab.h>
char *buf;
static int __init mycdev_init(void)
{
    buf = kzalloc(128,GFP_KERNEL);
    if(buf == NULL){
        printk("kzalloc memory error\n");
        return -ENOMEM;
    }

    memcpy(buf,"i am test kzalloc func....\n",
        strlen("i am test kzalloc func....\n"));

    printk("buf = %s\n",buf);

    return 0;
}
static void __exit mycdev_exit(void)
{
    kfree(buf);
}
module_init(mycdev_init);
module_exit(mycdev_exit);
MODULE_LICENSE("GPL");

12.块设备驱动

块设备驱动是用来操作硬盘类的存储设备的

image-20220520113053720

12.1什么是块设备驱动

  1. 块设备驱动的概念:系统中能够随机访问固定大小(1block 512byte)数据片的设备被称之为块设备。块设备文件一般都是以安装文件系统的方式使用,这也是块设备通常的访问方式。块设备的方式访问方式是随机的。
  2. 块设备中最小的可寻址单位是扇区扇区大小一般是2的整数倍。最常见的大小是512字节。块是文件系统的一种抽象,只能基于块来访问文件系统。物理磁盘寻址是按照扇区的级别进行的,内核访问的所有磁盘操作又都是按照块进行的。扇区是设备的最小可寻址单位,所以块不能比扇区还小,只能数倍与扇区大小。
  3. 内核对块大小的要求是:必须是扇区大小的整数倍,并且小于页面的大小,所以块的大小通常是512字节、1K或者4K。

1.2块设备驱动和字符设备驱动的对比

  1. 块设备接口相对复杂,不如字符设备明晰易用
  2. 块设备驱动程序对整个系统的性能影响较大,速度和效率是设计块设备驱动程要重点考虑的问题
  3. 系统中使用缓冲区与访问请求的优化管理(合并与重新排序)来提高系统性能

image-20220520115038053

1.3块设备驱动的相关知识简介

磁头:一个磁盘有多少个面就有多少个磁头

磁道:在一个磁头上可以有很多环,这些环就叫做磁道

扇区:磁道上访问数据的最小的单位就是扇区,一个扇区的大小就是512字节

1block = 512字节 1024字节 2048字节 4096字节

1扇区 = 512字节

块设备的能存储的数据=磁头*磁道*扇区*512

image-20220520113853510

1.4块设备驱动的框架图

image-20220520120038401

  1. 虚拟文件系统(VFS):隐藏了各种硬件的具体细节,为用户操作不同的硬件提供了一个统一的接口。其基于不同的文件系统格式,比如EXT,FAT等。用户程序对设备的操作都通过VFS来完成,在VFS上面就是诸如open、close、write和read的函数API。
  2. Disk Cache:硬盘的高速缓存,用户缓存最近访问的文件数据,如果能在高速缓存中找到,就不必去访问硬盘,毕竟硬盘的访问速度慢很多。
  3. 映射层(mapping layer):这一层主要用于确定文件系统的block size,然后计算所请求的数据包含多少个block。同时调用具体文件系统函数来访问文件的inode,确定所请求的数据在磁盘上面的逻辑地址
  4. Generic Block Layer:通用块层,Linux内核把块设备看做是由若干个扇区组成的数据空间,上层的读写请求在通用块层被构造成一个或多个bio结构
  5. I/O Scheduler Layer :I/O调度层,负责将通用块层的块I/O操作进行(电梯调度算法)调度、插入、暂存、排序、合并、分发等操作,对磁盘的操作更为高效。负责将通用块层的块I/O操作进行
  6. 块设备驱动层:在块系统架构的最底层,由块设备驱动根据排序好的请求,对硬件进行数据访问。
user:
 open     read    write    close
-------------------(io请求)-----------------------------------
kernel	|中间层: (block_device)
     |	将用户的io请求转化成BIO(block,input ,output),
     |	在物理内存上连续的bio会被合成request,这个request
     |	会被放到内核的一个队列上。
     |---------------------------------------------------------
     |driver:gendisk
     | 1.分配对象
     | 2.对象初始化
     | 3.初始化一个队列  head----request(read)----request(write)---...
     | //4.硬盘设备的初始化
     | 5.注册、注销
------------------------------------------------------------------  
haredware :   分配的内存(模拟真实的设备)(1M)

1.5块设备驱动的API

1.gendisk的结构体对象
    struct gendisk {   
        int major;   //块设备的主设备号
        int first_minor; //起始的次设备号
        int minors; //设备的个数,分区的个数
        char disk_name[DISK_NAME_LEN]; //磁盘的名字
        struct disk_part_tbl  *part_tbl;
        //磁盘的分区表的首地址
        struct hd_struct part0;
        //part0分区的描述

        const struct block_device_operations *fops;
        //块设备的操作方法结构体

        struct request_queue *queue;
        //队列(重要)

        void *private_data;
        //私有数据
    };

    分区的结构体
    struct hd_struct {
        sector_t start_sect; //起始的扇区号
        sector_t nr_sects;   //扇区的个数                                                                                             
        int  partno;        //分区号
    };

    //块设备的操作方法结构体
    struct block_device_operations {
        int (*open) (struct block_device *, fmode_t);
        int (*release) (struct gendisk *, fmode_t);
        int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
        int (*getgeo)(struct block_device *, struct hd_geometry *); 
        //设置磁盘的磁头,磁道,扇区的个数的。hd_geometry
    } 

image-20220520135311721

2.结构体对象的初始化
    struct gendisk *mydisk;

    struct gendisk *alloc_disk(int minors)
    //void put_disk(struct gendisk *disk)
    //归还引用计数
    功能:分配gendisk的内存,然后完成必要的初始化
    参数:
        @minors:分区的个数
    返回值:成功返回分配到的内存的首地址,失败返回NULL


    int register_blkdev(unsigned int major, const char *name)
    //void unregister_blkdev(unsigned int major, const char *name)
    功能:申请设备设备驱动的主设备号
    参数:
        @major : 0:自动申请
                  >0 :静态指定
        @name  :名字  cat /proc/devices
    返回值: 
            major=0 ;成功返回主设备号,失败返回错误码
            major>0 :成功返回0 ,失败返回错误码

    void set_capacity(struct gendisk *disk, sector_t size)
    功能:设置磁盘的容量

    struct request_queue *blk_mq_init_sq_queue(struct blk_mq_tag_set *set,
     const struct blk_mq_ops *ops,unsigned int queue_depth,unsigned int set_flags)
    //void blk_cleanup_queue(struct request_queue *q)
    功能:用于在给定队列深度的情况下使用mq ops设置队列的助手,以及通过mq ops标志传递的助手
    参数:
     @被初始化的tag对象,tag被上层使用,里面包含硬件队列的个数,队列的操作方法结构体,标志位等
        @放入到tag中的操作方法结构体
     @tag中指定支持的队列深度
     @将tag中队列的处理标志位,例如BLK_MQ_F_SHOULD_MERGE, BLK_MQ_F_BLOCKING等
    返回值:成功返回队列指针,失败返回错误码指针 

3.注册、注销
    void add_disk(struct gendisk *disk)
    //注册
    void del_gendisk(struct gendisk *disk)
    //注销

1.6块设备驱动实例1

#include <linux/blkdev.h>
#include <linux/genhd.h>
#include <linux/hdreg.h>
#include <linux/init.h>
#include <linux/module.h>
#define BLKSIZE (1 * 1024 * 1024) // 1M
#define BLKNAME "mydisk"
struct gendisk* mydisk;
int major;
char *dev_addr; //1M内存首地址,当成磁盘使用的
int mydisk_open(struct block_device* dev, fmode_t mode)
{
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
    return 0;
}
int mydisk_getgeo(struct block_device* dev, struct hd_geometry* geo)
{
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
    geo->heads = 4;   //磁头
    geo->cylinders = 16; //磁道
    geo->sectors = BLKSIZE / geo->heads / geo->cylinders / 512; //扇区
    return 0;
}
void mydisk_release(struct gendisk* disk, fmode_t mode)
{
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
}
const struct block_device_operations fops = {
    .open = mydisk_open,
    .release = mydisk_release,
    .getgeo = mydisk_getgeo,
};
static int __init mydisk_init(void)
{
    // 1.分配对象
    mydisk = alloc_disk(4);
    if (mydisk == NULL) {
        printk("alloc_disk error\n");
        return -ENOMEM;
    }
    // 2.对象初始化
    // 2.1申请主设备号
    major = register_blkdev(0, BLKNAME);
    if (major < 0) {
        printk("register_blkdev error\n");
        return -EBUSY;
    }
    // 2.2设置磁盘容量
    set_capacity(mydisk, BLKSIZE >> 9);
    // 2.3队列的申请

    // 2.4mydisk成员赋值
    mydisk->major = major;
    mydisk->first_minor = 0;
    strcpy(mydisk->disk_name, BLKNAME);
    mydisk->fops = &fops;
    // mydisk->queue = ?

    // 3.申请1M内存当成磁盘使用
    dev_addr = vmalloc(BLKSIZE);
    if (dev_addr == NULL) {
        printk("vmalloc error\n");
        return -ENOMEM;
    }
    // 4.注册
    add_disk(mydisk);
    return 0;
}
static void __exit mydisk_exit(void)
{
    del_gendisk(mydisk);
    vfree(dev_addr);
    unregister_blkdev(major, BLKNAME);
    put_disk(mydisk);
}
module_init(mydisk_init);
module_exit(mydisk_exit);
MODULE_LICENSE("GPL");

1.7队列处理相关的结构体、关系、相关函数

struct  request_queue 
{
    /*双向链表数据结构,将所有加入到队列的IO请求组建成一个双向链表*/
    struct  list_head  queue_head; 
    struct list_head    requeue_list; //request队列
    spinlock_t      requeue_lock;     //队列自旋锁
    unsigned long     nr_requests;     /* 最大的请求数量 */
    unsigned long     queue_flags;/*当前请求队列的状QUEUE_FLAG_STOPPED*/ 
    …
};
struct  request
{
    struct list_head queuelist;/* 请求对象中的链表元素*/
    struct request_queue *q; /* 指向存放当前请求的请求队列*/
    unsigned int __data_len; /* 当前请求要求数据传输的总的数据量 */
    sector_t __sector;         /* 当前请求要求数据传输的块设备的起始扇区 */
    struct bio *bio;  /* bio对象所携带的信息转存至请求对象中*/
    struct bio *biotail; /* bio链表*/
    …
};
通常一个request请求可以包含多个bio,一个bio对应一个I/O请求  
 struct bio {  
    struct bio *bi_next;  /* 指向当前bio的下一个对象*/ 
    unsigned long  bi_flags;   /* 状态、命令等 */ 
    unsigned long bi_rw;   /* 表示READ/WRITE*/ 
    struct block_device *bi_bdev;    /* 与请求相关联的块设备对象指针*/ 
    unsigned short bi_vcnt;  /* bi_io_vec数组中元素个数 */ 
    unsigned short bi_idx;  /* 当前处理的bi_io_vec数组元素索引 */
    unsigned int bi_size;  /* 本次传输需要传输的数据总量,byte(扇区大小整数倍) */ 
    struct bio_vec *bi_io_vec;/* 指向一个IO向量的数组,数组中的内各元素对应一个物理页的page对象 */
  };
struct bio_vec {  
    struct page  *bv_page; //指向用于数据传输的页面所对应的struct page对象
    unsigned int bv_len;   //表示当前要传输的数据大小  
    unsigned int bv_offset;//表示数据在页面内的偏移量 
};

reque_queue和request及bio的关系

image-20220520154603655

块设备驱动

1.8虚拟块设备驱动

#include <linux/init.h>
#include <linux/module.h>
#include <linux/blk-mq.h>
#include <linux/blkdev.h>
#include <linux/genhd.h>
#include <linux/hdreg.h>
#include <linux/vmalloc.h>
#define BLKNAME "mydisk"
//1.分配对象
struct gendisk *mydisk;
int major;
#define BLKSIZE 1*1024*1024  //
char* dev_addr; // 1M内存首地址,当成磁盘使用的
//2.6.1分配队列对象
struct request_queue *queue;
int mydisk_open(struct block_device *dev, fmode_t mode){
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
    return 0;
}
void mydisk_release(struct gendisk * disk, fmode_t mode){
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
}   
int mydisk_getgeo(struct block_device *dev , struct hd_geometry * geo){
    printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
    //2.5.2 磁头磁道扇区
    geo->heads=4;
    geo->cylinders=16;
    geo->sectors=BLKSIZE/geo->heads/geo->cylinders;
    return 0;
}
//2.5.1构造操作方法结构体
const struct block_device_operations fops={
    .open=mydisk_open,
    .release=mydisk_release,
    .getgeo=mydisk_getgeo,
}; 
//2.6.2创建初始化对象   给上层留的接口
struct blk_mq_tag_set tags;
//2.6.4操作方法结构体访问操作队列的函数
//操作队列的函数 相当于probe release remove
blk_status_t mydisk_queue_rq(struct blk_mq_hw_ctx* ctx,
    const struct blk_mq_queue_data* bd){
        //在这个函数里要完成数据的读写入磁盘
        struct request* rq=bd->rq;//找到request
        struct bio_vec bvec;//找到bio_vec:request-->struct bio-->struct bio_vec
        //ii.i需先定义迭代器
        struct req_iterator iter;
        //找磁盘的地址rq_for_each_segment

        //i.告诉上层开始处理队列了
        blk_mq_start_request(rq);
        unsigned long pos = rq->__sector * 512;
        //ii.获取到内存片段的线性地址
        rq_for_each_segment(bvec,rq, iter){
            //将磁盘地址转换为内存片段的线性地址
            void* b_buf = page_address(bvec.bv_page) + bvec.bv_offset;
            unsigned long len = bvec.bv_len;
            // 2.2根据读写完成拷贝
            if (rq_data_dir(rq)) { // 1 Write
                // 目的:磁盘地址 dev_addr+pos
                // 源: b_buf
                // 大小:len
                memcpy(dev_addr + pos, b_buf, len);
            } else { // 0 Read
                // 目的:b_buf
                // 源: 磁盘地址dev_addr+pos
                // 大小:len
                memcpy(b_buf, dev_addr + pos, len);
            }
            pos += len;
        }
        //iii.告诉上层队列处理结束
        blk_mq_end_request(rq, BLK_STS_OK);
        return BLK_STS_OK;
    }
//2.6.3填充操作方法结构体
const struct blk_mq_ops qops={
    .queue_rq=mydisk_queue_rq,
};
static int __init mydisk_init(void)
{
    //2.对象初始化
    if(NULL==(mydisk=alloc_disk(4))){//部分初始化
        printk("alloc_disk error\n");
        return -ENOMEM;
    }
    //剩下的初始化需要自己完成
    //2.1申请主设备号
    if(0>(major=register_blkdev(0,BLKNAME))){
        printk("register_blkdev error\n");
        return -EBUSY;
    }
    mydisk->major = major;
    //2.2起始次设备号
    mydisk->first_minor=0;
    //2.3磁盘名
    strcpy(mydisk->disk_name,BLKNAME);
    //2.4part0分区
    set_capacity(mydisk,BLKSIZE/512);
    //2.5操作方法结构体
    mydisk->fops=&fops;
    //2.6申请队列
    queue=blk_mq_init_sq_queue(&tags,&qops,16,BLK_MQ_F_SHOULD_MERGE);
    if (IS_ERR(queue)) {
        printk("blk_mq_init_sq_queue error\n");
        return -ENOMEM;
    }
    mydisk->queue = queue;
    //4.申请1M内存当成磁盘使用
    dev_addr = vmalloc(BLKSIZE);
    if (dev_addr == NULL) {
        printk("vmalloc error\n");
        return -ENOMEM;
    }
    //5.注册
    add_disk(mydisk);
    return 0;
}
static void __exit mydisk_exit(void)
{
    //5.注销
    del_gendisk(mydisk);
    vfree(dev_addr);
    blk_cleanup_queue(queue);
    unregister_blkdev(major, BLKNAME);
    put_disk(mydisk);
}
module_init(mydisk_init);
module_exit(mydisk_exit);
MODULE_LICENSE("GPL");

1.9块设备驱动的测试方法

1.安装块设备驱动
    sudo insmod mydisk.ko
2.查看设备节点
    ls -l /dev/mydisk
    brw-rw---- 1 root disk 252, 0 6月  22 14:14 /dev/mydisk
3.查看磁盘相关的命令
    sudo  fdisk -l
    Disk /dev/mydisk:1 MiB,1048576 字节,2048 个扇区
    单元:扇区 / 1 * 512 = 512 字节
    扇区大小(逻辑/物理):512 字节 / 512 字节
    I/O 大小(最小/最佳):512 字节 / 512 字节  
4.分区
    sudo fdisk /dev/mydisk
    m 获取帮助
    d   删除分区
    p   打印分区表
    n   添加新分区
    w   将分区表写入磁盘并退出
    q   退出而不保存更改

    在dev下产生/dev/mydisk1节点
5.分区格式化
    sudo mkfs.ext2 /dev/mydisk1
        
6.挂载硬盘到某个目录下
     sudo mount -t ext2 /dev/mydisk1  ~/udisk
        
7.向磁盘中存放文件
     sudo cp ~/work/day11/01mydisk/mydisk.c .
8.取消挂载
     cd
     sudo umount udisk
9.将块设备驱动的数据读取到ram.bin文件中
   sudo cat /dev/mydisk1 > ram.bin
        
10.重新挂载查看
    sudo mount -o loop ram.bin ~/udisk
    去udisk目录下查看是否有刚才拷贝的文件,如果有就说明成功了

2.摄像头驱动硬件介绍

2.1ov5640简介

一、OV5640简介
OV5640是OV(OmniVision)公司推出的一款CMOS图像传感器,实际感光阵列为:2592 x 1944(即500w像素),该传感器内部集成了图像处理的电路,包括自动曝光控制(AEC)、自动白平
衡( AWB) 等。同时该传感器支持LED补光、 MIPI(移动产业处理器接口,多用于手机等)输出接口和DVP(数字视频并行,在设计HDMI显示时,就用的这个)输出接口选择、 ISP(图像信号处理)以及自动聚焦控制(VCM)等功能。支持输出RAW RGB, RGB565/555/444, CCIR656, YUV422/420, YCbCr422, 和图像格式压缩JPEG。

camera1

camera2

2.2OV5640内部构造图

image-20210323202038383

可以看到,时序发生器和系统控制逻辑(timing generator and system control logic)控制着感光阵列(image array)、放大器(AMP)、AD转换(10bit)以及输出外部时序信号(PCLK和行场同步信号等)。感光阵列输出模拟信号,经过AMP增强信号强度,进入到AD转换器,转换成数字信号并经过ISP,进行相关图像处理,最终输出10位DVP数据流或者MIPI数据流。AMP和ISP等都是由控制寄存器进行控制,而配置寄存器的接口时序就是使用的SCCB。由于OV5640寄存器较多,OV5640寄存器的地址为16位,所以SCCB协议中的寄存器地址为16位。

2.3OV5640采集的图像分辨率

image-20210323205751552

 

image-20210323202544658

2.4OV5640接口简介

image-20210322192601676

 

image-20210323204729744

2.5摄像头图像输出接口

MIPI

MIPI是差分串口传输,速度快,抗干扰。主流手机模组现在都是用MIPI传输,传输时使用4对差分信号传输图像数据和一对差分时钟信号;最初是为了减少LCD屏和主控芯片之间连线的数量而设计的,后来发展到高速了,支持高分辨率的显示屏,现在基本上都是MIPI接口了。 MIPI的camera接口叫 CSI,MIPI的display接口叫DSI。

DVP

DVP是并口传输,速度较慢,传输的带宽低,使用需要PCLK\sensor输出时钟、MCLK(XCLK):外部时钟输入、VSYNC:场同步、HSYNC:行同步、D[0:11]:并口数据—可以是8/10/12bit数据位数大小。

image-20220523100313972

2.6摄像头外设管脚工作原理

image-20220523101745408

2.7SCCB总线

SCCB(SeriaI Camera ControlBus)是简化的I2C协议,SIO-l是串行时钟输入线,SIO-O是串行双向数据线,分别相当于I2C协议的SCL和SDA。SCCB的总线时序与I2C基本相同,它的响应信号ACK被称为一个传输单元的第9位,分为Don’t care和NA。Don’t care位由从机产生;NA位由主机产生,由于SCCB不支持多字节的读写,NA位必须为高电平。另外,SCCB没有重复起始的概念,因此在SCCB的读周期中,当主机发送完片内寄存器地址后,必须发送总线停止条件。不然在发送读命令时,从机将不能产生Don’t care响应信号。

image-20210323222443043

 

image-20210323222529664

3.摄像头V4L2驱动框架

3.1v4l2架构的功能(video for linux 2)

V4l2可以支持多种设备,它可以有以下几种接口:
1.视频采集接口(Video Capture Interface):V4L2最初设计用于支持视频
    采集设备,例如高频头或摄像头。视频采集接口允许应用程序访问和控
    制这些设备,以获取从摄像头或其他视频源采集到的视频数据。
2.视频输出接口(Video Output Interface):V4L2还可以驱动计算机外
    围的视频图像设备,如能够输出电视信号格式的设备(例如电视机、显
    示器)。通过视频输出接口,V4L2允许将计算机中生成的视频图像传输
    到外部显示设备进行展示。
3.直接传输视频接口(Video Overlay Interface):该接口的主要功能是直
    接将从视频采集设备采集到的信号输出到输出设备上,而不需要经过系统
    的中央处理器(CPU)。这种传输方式可以提高视频播放的效率和质量,
    并减轻CPU的负载。
4.视频间隔消隐信号接口(VBI Interface):VBI接口允许应用程序访问传输
    中的视频间隔消隐(VBI)信号。VBI信号是一种特殊的视频信号,用于传
    输附加的信息,例如字幕、广告和时间码。通过VBI接口,应用程序可以
    提取和处理这些附加信息。
5.收音机接口(Radio Interface):该接口用于处理从AM或FM高频头设备接
    收到的音频流。V4L2中的收音机接口允许计算机与收音机硬件进行交互,
    接收和处理收音机信号,以实现收音

3.2v4l2驱动框架图

 

image-20220523104232196

3.3V4L2驱动编写流程

1.分配对象 
 struct video_device *vfd;
 vfd = video_device_alloc();      
2.对象的初始化                             
 vfd->name       = "ov5640",
 vfd->ops           = &ov5640_fops, //v4l2核心类的操作方法结构体v4l2_file_operations
 vfd->ioctl_ops  = &ov5640_ioctl_ops, //v4l2的ioctl操作方法 v4l2_ioctl_ops
 vfd->release    = video_device_release,
 
     snprintf(dev->v4l2_dev.name, sizeof(dev->v4l2_dev.name),
                "%s-%03d", VIVI_MODULE_NAME, inst);
     v4l2_device_register(NULL, &dev->v4l2_dev);
     vfd->v4l2_dev = &dev->v4l2_dev;

3.注册
 video_register_device(vfd, VFL_TYPE_GRABBER, video_nr);   
/*
 __video_register_device(vdev, type, nr, 1, vdev->fops->owner);

   switch (type) {  ===>指定本次分配的设备的类型
  case VFL_TYPE_GRABBER:
   name_base = "video";
   break;
  case VFL_TYPE_VBI:
   name_base = "vbi";                                                                         
   break;
  case VFL_TYPE_RADIO:
   name_base = "radio";
   break;
   }
 switch (type) {
  case VFL_TYPE_GRABBER:
   minor_offset = 0; ===>指定次设备号的范围
   minor_cnt = 64;
   break;
  case VFL_TYPE_RADIO:
   minor_offset = 64;
   minor_cnt = 64;
   break;                                                                                     
  case VFL_TYPE_VBI:
   minor_offset = 224;
   minor_cnt = 32;
   break;
  default:
   minor_offset = 128;
   minor_cnt = 64;
   break;
  }
  
     vdev->cdev = cdev_alloc();  ====>注册了字符设备驱动
  vdev->cdev->ops = &cdev_fops; //这是字符设备驱动的操作方法结构体file_operations
  vdev->cdev->owner = owner;
  ret = cdev_add(vdev->cdev, MKDEV(VIDEO_MAJOR, vdev->minor), 1);

  class_create
  device_reate   ===>创建设备节点 video0
*/
4.注销
    video_unregister_device 
user:
open("/dev/video0",) read(fd,buf,sizeof(buf)); ioctl(fd,,) close(fd); 
---------------------------------------------------------
cdev:file_opreations:fops :open  read  write ioctl close
==>字符设备驱动的操作方法结构体
---------------------------------------------------------
v4l2_file_operations:fops  :open  read  write ioctl close 
==>上面字符设备驱动的open,read,write,close等函数就会转到
---------------------------------------------------------
v4l2_ioctl_ops :ioctl_ops; //必备的11个ioctl
 /*表示它是一个摄像头设备*/
 .vidioc_querycap = vidioc_querycap,
 /* 用于列举、获得、测试、设置摄像头的数据的格式 */
 .vidioc_enum_fmt_vid_cap = vidioc_enum_fmt_vid_cap,
 .vidioc_g_fmt_vid_cap  = vidioc_g_fmt_vid_cap,
 .vidioc_try_fmt_vid_cap  = vidioc_try_fmt_vid_cap,
 .vidioc_s_fmt_vid_cap  = vidioc_s_fmt_vid_cap,
 /* 缓冲区操作: 申请/查询/放入队列/取出队列 */
 .vidioc_reqbufs  = vidioc_reqbufs,
 .vidioc_querybuf = vidioc_querybuf,
 .vidioc_qbuf = vidioc_qbuf,
 .vidioc_dqbuf  = vidioc_dqbuf,
 /*启动/停止*/
 .vidioc_streamon = vidioc_streamon,
 .vidioc_streamoff  = vidioc_streamoff,

3.4配置ov5640

3.4.1配置ov5640相关驱动

需要配置的驱动的代码DCMI(摄像头控制器),V4L2的核心,ov5640到内核中

配置V4l2的核心

image-20220523113451301

配置ov5640

image-20220523112954698

配置DCMI驱动

image-20230411103612852

将上述的代码配置到内核之后,重新编译内核 make uImage LOADADDR=0xc2000000 -j4

4.4.2配置设备树

参看内核中的摄像头设备树的参考文件配置即可

修改arch/arm/boot/dts/stm32mp15xx-fsmp1x.dtsi

#include "stm32mp15-m4-srm.dtsi"
#include "stm32mp15-m4-srm-pinctrl.dtsi"
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/mfd/st,stpmic1.h>
#include <dt-bindings/rtc/rtc-stm32.h>

/ {
    memory@c0000000 {
        device_type = "memory";
        reg = <0xc0000000 0x20000000>;
    };
 
 v1v8_audio: regulator-v1v8_audio {
  compatible = "regulator-fixed";
  regulator-name = "v1v8_audio";
  regulator-min-microvolt = <1800000>;
  regulator-max-microvolt = <1800000>;
  regulator-always-on;
  regulator-boot-on;
 };
 sound {
  compatible = "audio-graph-card";
  label = "STM32MP1-FSMP1A";
  routing =
   "Playback" , "MCLK",
   "Capture" , "MCLK",
   "MICL" , "Mic Bias";
  dais = <&sai2a_port &sai2b_port>;
  status = "okay";
 };

    reserved-memory {
        #address-cells = <1>;
        #size-cells = <1>;
        ranges;

        mcuram2: mcuram2@10000000 {
            compatible = "shared-dma-pool";
            reg = <0x10000000 0x40000>;
            no-map;
        };

        vdev0vring0: vdev0vring0@10040000 {
            compatible = "shared-dma-pool";
            reg = <0x10040000 0x1000>;
            no-map;
        };

        vdev0vring1: vdev0vring1@10041000 {
            compatible = "shared-dma-pool";
            reg = <0x10041000 0x1000>;
            no-map;
        };

        vdev0buffer: vdev0buffer@10042000 {
            compatible = "shared-dma-pool";
            reg = <0x10042000 0x4000>;
            no-map;
        };

        mcuram: mcuram@30000000 {
            compatible = "shared-dma-pool";
            reg = <0x30000000 0x40000>;
            no-map;
        };

        retram: retram@38000000 {
            compatible = "shared-dma-pool";
            reg = <0x38000000 0x10000>;
            no-map;
        };
    };

    vin: vin {
        compatible = "regulator-fixed";
        regulator-name = "vin";
        regulator-min-microvolt = <5000000>;
        regulator-max-microvolt = <5000000>;
        regulator-always-on;
    };

    v3v3: regulator-3p3v {
        compatible = "regulator-fixed";
        regulator-name = "v3v3";
        regulator-min-microvolt = <3300000>;
        regulator-max-microvolt = <3300000>;
        regulator-always-on;
        regulator-boot-on;
    };

    vdd: regulator-vdd {
         compatible = "regulator-fixed";
         regulator-name = "vdd";
         regulator-min-microvolt = <3300000>;
         regulator-max-microvolt = <3300000>;
         regulator-always-on;
         regulator-boot-on;
     };

 v3v3_hdmi: regulator-v3v3-hdmi {
  compatible = "regulator-fixed";
  regulator-name = "v3v3_hdmi ";
  regulator-min-microvolt = <3300000>;
  regulator-max-microvolt = <3300000>;
  regulator-always-on;
  regulator-boot-on;
 };

 v1v2_hdmi: regulator-v1v2-hdmi {
  compatible = "regulator-fixed";
  regulator-name = "v1v2_hdmi";
  regulator-min-microvolt = <1200000>;
  regulator-max-microvolt = <1200000>;
  regulator-always-on;
  regulator-boot-on;
 };


 panel_backlight: panel-backlight {
  compatible = "pwm-backlight";
  pwms = <&pwm2 0 5000000>;
 
  brightness-levels = <0 4 8 16 32 64 128 255>;
  default-brightness-level = <6>;
  status = "okay";
 };
 v2v8: regulator-2p8v {
  compatible = "regulator-fixed";
  regulator-name = "v2v8";
  regulator-min-microvolt = <2800000>;
  regulator-max-microvolt = <2800000>;
  regulator-always-on;
  regulator-boot-on;
 };
 clocks {
  clk_ext_camera: clk-ext-camera {
   #clock-cells = <0>;
   compatible = "fixed-clock";
   clock-frequency = <24000000>;
  };
 };
 
 wifi_pwrseq: wifi-pwrseq {
    compatible = "mmc-pwrseq-simple";
    reset-gpios = <&gpiod 4 GPIO_ACTIVE_LOW>;
 
 };


};

/*HDMI CEC控制器*/
&cec {
    pinctrl-names = "default", "sleep";
    pinctrl-0 = <&cec_pins_b>;
    pinctrl-1 = <&cec_sleep_pins_b>;
    status = "okay";
};

/*循环冗余校验计算单元*/
&crc1 {
    status = "okay";
};

&dma1 {
    sram = <&dma_pool>;
};

&dma2 {
    sram = <&dma_pool>;
};
/*数字钟温度传感器*/
&dts {
    status = "okay";
};
/*图像处理单元*/
&gpu {
    contiguous-area = <&gpu_reserved>;
    status = "okay";
};
/*哈希处理器*/
&hash1 {
    status = "okay";
};

/*处理器间通信控制器*/
&ipcc {
    status = "okay";
};

/*看门狗*/
&iwdg2 {
    timeout-sec = <32>;
    status = "okay";
};

/*随机数发生器*/
&rng1 {
    status = "okay";
};

/*实时时钟*/
&rtc {
 st,lsco = <RTC_OUT2_RMP>;
  pinctrl-0 = <&rtc_out2_rmp_pins_a>;
  pinctrl-names = "default";

 status = "okay";

};

/*sdmmc1 TF卡*/
&sdmmc1 {
    pinctrl-names = "default", "opendrain", "sleep";
    pinctrl-0 = <&sdmmc1_b4_pins_a>;
    pinctrl-1 = <&sdmmc1_b4_od_pins_a>;
    pinctrl-2 = <&sdmmc1_b4_sleep_pins_a>;
    cd-gpios = <&gpioh 3 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
    disable-wp;
    st,neg-edge;
    bus-width = <4>;
    vmmc-supply = <&v3v3>;
    status = "okay";
};

&sdmmc2 {
 pinctrl-names = "default", "opendrain", "sleep";
 pinctrl-0 = <&sdmmc2_b4_pins_a &sdmmc2_d47_pins_a>;
 pinctrl-1 = <&sdmmc2_b4_od_pins_a &sdmmc2_d47_pins_a>;
 pinctrl-2 = <&sdmmc2_b4_sleep_pins_a &sdmmc2_d47_sleep_pins_a>;
 non-removable;
 no-sd;
 no-sdio;
 st,neg-edge;
 bus-width = <8>;
 vmmc-supply = <&v3v3>;
 vqmmc-supply = <&vdd>;
 mmc-ddr-3_3v;
 status = "okay";
};

&sdmmc3 {
    arm,primecell-periphid = <0x10153180>;
    pinctrl-names = "default", "opendrain", "sleep";
    pinctrl-0 = <&sdmmc3_b4_wifi_pins_a>;
    pinctrl-1 = <&sdmmc3_b4_od_wifi_pins_a>;
    pinctrl-2 = <&sdmmc3_b4_sleep_wifi_pins_a>;
    non-removable;
    st,neg-edge;
    bus-width = <4>;
    vmmc-supply = <&v3v3>;
    mmc-pwrseq = <&wifi_pwrseq>;
    #address-cells = <1>;
    #size-cells = <0>;
    keep-power-in-suspend;
    status = "okay";

    brcmf: bcrmf@1 {
        reg = <1>;
        compatible = "brcm,bcm4329-fmac";
    };
};


&sram {
    dma_pool: dma_pool@0 {
        reg = <0x50000 0x10000>;
        pool;
    };
};

/*命令行终端*/
&uart4 {
    pinctrl-names = "default", "sleep", "idle";
    pinctrl-0 = <&uart4_pins_a>;
    pinctrl-1 = <&uart4_sleep_pins_a>;
    pinctrl-2 = <&uart4_idle_pins_a>;
    pinctrl-3 = <&uart4_pins_a>;
    /delete-property/dmas;
    /delete-property/dma-names;
    status = "okay";
};

/*电源基准缓冲器*/
&vrefbuf {
    regulator-min-microvolt = <2500000>;
    regulator-max-microvolt = <2500000>;
    vdda-supply = <&vdd>;
    status = "okay";
};

&timers2 {
 /* spare dmas for other usage */
 /delete-property/dmas;
 /delete-property/dma-names;
 status = "okay";
 pwm2: pwm {
  pinctrl-0 = <&pwm2_pins_b>;
  pinctrl-1 = <&pwm2_sleep_pins_b>;
  pinctrl-names = "default", "sleep";
  #pwm-cells = <2>;
  status = "okay";
 };
 timer@2 {
  status = "disabled";
 };
};

&pinctrl {
 sdmmc3_b4_wifi_pins_a: sdmmc3-b4-wifi-0 {
     pins1 {
         pinmux = <STM32_PINMUX('F', 0, AF9)>, /* SDMMC3_D0 */
              <STM32_PINMUX('F', 4, AF9)>, /* SDMMC3_D1 */
              <STM32_PINMUX('D', 5, AF10)>, /* SDMMC3_D2 */
             <STM32_PINMUX('D', 7, AF10)>, /* SDMMC3_D3 */
              <STM32_PINMUX('D', 0, AF10)>; /* SDMMC3_CMD */
         slew-rate = <1>;
         drive-push-pull;
         bias-pull-up;
     };
     pins2 {
         pinmux = <STM32_PINMUX('G', 15, AF10)>; /* SDMMC3_CK */
         slew-rate = <2>;
         drive-push-pull;
         bias-pull-up;
     };
 };

 sdmmc3_b4_od_wifi_pins_a: sdmmc3-b4-od-wifi-0 {
     pins1 {
         pinmux = <STM32_PINMUX('F', 0, AF9)>, /* SDMMC3_D0 */
             <STM32_PINMUX('F', 4, AF9)>, /* SDMMC3_D1 */
              <STM32_PINMUX('D', 5, AF10)>, /* SDMMC3_D2 */
              <STM32_PINMUX('D', 7, AF10)>; /* SDMMC3_D3 */
         slew-rate = <1>;
         drive-push-pull;
         bias-pull-up;
     };

     pins2 {
         pinmux = <STM32_PINMUX('G', 15, AF10)>; /* SDMMC3_CK */
         slew-rate = <2>;
         drive-push-pull;
         bias-pull-up;
     };

     pins3 {
         pinmux = <STM32_PINMUX('D', 0, AF10)>; /* SDMMC2_CMD */
         slew-rate = <1>;
         drive-open-drain;
         bias-pull-up;
     };
 };

 sdmmc3_b4_sleep_wifi_pins_a: sdmmc3-b4-sleep-wifi-0 {
     pins {
         pinmux = <STM32_PINMUX('F', 0, ANALOG)>, /* SDMMC3_D0 */
              <STM32_PINMUX('F', 4, ANALOG)>, /* SDMMC3_D1 */
              <STM32_PINMUX('D', 5, ANALOG)>, /* SDMMC3_D2 */
              <STM32_PINMUX('D', 7, ANALOG)>, /* SDMMC3_D3 */
              <STM32_PINMUX('G', 15, ANALOG)>, /* SDMMC3_CK */
              <STM32_PINMUX('D', 0, ANALOG)>; /* SDMMC3_CMD */
     };
 };

 pwm2_pins_b: pwm2-0 {
  pins {
   pinmux = <STM32_PINMUX('A', 5, AF1)>; /* TIM2_CH1 */
   bias-pull-down;
   drive-push-pull;
   slew-rate = <0>;
  };
 };

 pwm2_sleep_pins_b: pwm1-sleep-0 {
  pins {
   pinmux = <STM32_PINMUX('A', 5, ANALOG)>; /* TIM2_CH1 */
  };
 };

 usart3_pins_bt: usart3-bt-0 {
     pins1 {
         pinmux = <STM32_PINMUX('D', 8, AF7)>, /* USART3_TX */
              <STM32_PINMUX('D', 12, AF7)>; /* USART3_RTS */
         bias-disable;
         drive-push-pull;
         slew-rate = <0>;
     };
     pins2 {
         pinmux = <STM32_PINMUX('D', 9, AF7)>, /* USART3_RX */
              <STM32_PINMUX('D', 11, AF7)>; /* USART3_CTS_NSS */
         bias-disable;
     }; 
 };

 usart3_idle_pins_bt: usart3-idle-bt-0 {
     pins1 {
         pinmux = <STM32_PINMUX('D', 8, ANALOG)>, /* USART3_TX */
               <STM32_PINMUX('D', 12, ANALOG)>, /* USART3_RTS */
               <STM32_PINMUX('D', 11, ANALOG)>; /* USART3_CTS_NSS */
     };
     pins2 {
         pinmux = <STM32_PINMUX('D', 9, AF7)>; /* USART3_RX */
         bias-disable;
     };
 };

 usart3_sleep_pins_bt: usart3-sleep-bt-0 {
     pins {
         pinmux = <STM32_PINMUX('D', 8, ANALOG)>, /* USART3_TX */
                <STM32_PINMUX('D', 12, ANALOG)>, /* USART3_RTS */
                <STM32_PINMUX('D', 11, ANALOG)>, /* USART3_CTS_NSS */
                <STM32_PINMUX('D', 9, ANALOG)>; /* USART3_RX */
     };
 };

 btreg0: bt_reg_on-0 {
     pins {
         pinmux = <STM32_PINMUX('D', 13, GPIO)>;
         drive-push-pull;
         bias-pull-up;
         output-high;
         slew-rate = <0>;
     };
 };
};

&i2c5 {
 pinctrl-names = "default", "sleep";
 pinctrl-0 = <&i2c5_pins_a>;
 pinctrl-1 = <&i2c5_sleep_pins_a>;
 i2c-scl-rising-time-ns = <100>;
 i2c-scl-falling-time-ns = <7>;
 clock-frequency = <100000>;
 /* spare dmas for other usage */
 /delete-property/dmas;
 /delete-property/dma-names;
 status = "okay";

 hdmi-transmitter@39 {
  compatible = "sil,sii9022";
  reg = <0x39>;
  iovcc-supply = <&v3v3_hdmi>;
  cvcc12-supply = <&v1v2_hdmi>;
  reset-gpios = <&gpioa 13 GPIO_ACTIVE_LOW>;
  interrupts = <14 IRQ_TYPE_EDGE_FALLING>;
  interrupt-parent = <&gpioa>;
  #sound-dai-cells = <0>;
  status = "okay";

  ports {
   #address-cells = <1>;
   #size-cells = <0>;

   port@0 {
    reg = <0>;
    sii9022_in: endpoint {
     remote-endpoint = <&ltdc_ep0_out>;
    };
   };
  };
 };

 cs42l51: cs42l51@4a {
 compatible = "cirrus,cs42l51";
 reg = <0x4a>;
 #sound-dai-cells = <0>;
 VL-supply = <&v3v3>;
 VD-supply = <&v1v8_audio>;
 VA-supply = <&v1v8_audio>;
 VAHP-supply = <&v1v8_audio>;
 reset-gpios = <&gpioc 0 GPIO_ACTIVE_LOW>;
 clocks = <&sai2a>;
 clock-names = "MCLK";
 status = "okay";

 cs42l51_port: port {
  #address-cells = <1>;
  #size-cells = <0>;

  cs42l51_tx_endpoint: endpoint@0 {
   reg = <0>;
   remote-endpoint = <&sai2a_endpoint>;
   frame-master;
   bitclock-master;
  };

  cs42l51_rx_endpoint: endpoint@1 {
   reg = <1>;
   remote-endpoint = <&sai2b_endpoint>;
   frame-master;
   bitclock-master;
  };
 };
};

 ov5640: camera@3c {
  compatible = "ovti,ov5640";
  reg = <0x3c>;
  clocks = <&clk_ext_camera>;
  clock-names = "xclk";
  DOVDD-supply = <&v2v8>;
  powerdown-gpios = <&gpioa 4 (GPIO_ACTIVE_HIGH | GPIO_PUSH_PULL)>;
  reset-gpios = <&gpioa 3 (GPIO_ACTIVE_LOW | GPIO_PUSH_PULL)>;
  rotation = <180>;
  status = "okay";

  port {
   ov5640_0: endpoint {
    remote-endpoint = <&dcmi_0>;
    bus-width = <8>;
    data-shift = <2>; /* lines 9:2 are used */
    hsync-active = <0>;
    vsync-active = <0>;
    pclk-sample = <1>;
    pclk-max-frequency = <77000000>;
     };
  };
 };

};
&ltdc {
 pinctrl-names = "default", "sleep";
 pinctrl-0 = <&ltdc_pins_b>;
 pinctrl-1 = <&ltdc_sleep_pins_b>;
 status = "okay";

 port {
  #address-cells = <1>;
  #size-cells = <0>;

  ltdc_ep0_out: endpoint@0 {
   reg = <0>;
   remote-endpoint = <&sii9022_in>;
  };
 };
};

&sai2 {
 clocks = <&rcc SAI2>, <&rcc PLL3_Q>, <&rcc PLL3_R>;
 clock-names = "pclk", "x8k", "x11k";
 pinctrl-names = "default", "sleep";
 pinctrl-0 = <&sai2a_pins_a>, <&sai2b_pins_b>;
 pinctrl-1 = <&sai2a_sleep_pins_a>, <&sai2b_sleep_pins_b>;
 status = "okay";

 sai2a: audio-controller@4400b004 {
  #clock-cells = <0>;
  dma-names = "tx";
  clocks = <&rcc SAI2_K>;
  clock-names = "sai_ck";
  status = "okay";

  sai2a_port: port {
   sai2a_endpoint: endpoint {
    remote-endpoint = <&cs42l51_tx_endpoint>;
    format = "i2s";
    mclk-fs = <256>;
    dai-tdm-slot-num = <2>;
    dai-tdm-slot-width = <32>;
   };
  };
 };

 sai2b: audio-controller@4400b024 {
  dma-names = "rx";
  st,sync = <&sai2a 2>;
  clocks = <&rcc SAI2_K>, <&sai2a>;
  clock-names = "sai_ck", "MCLK";
  status = "okay";

  sai2b_port: port {
   sai2b_endpoint: endpoint {
    remote-endpoint = <&cs42l51_rx_endpoint>;
    format = "i2s";
    mclk-fs = <256>;
    dai-tdm-slot-num = <2>;
    dai-tdm-slot-width = <32>;
   };
  };
 };
};


&dcmi {
 status = "okay";
 pinctrl-names = "default", "sleep";
 pinctrl-0 = <&dcmi_pins_a>;
 pinctrl-1 = <&dcmi_sleep_pins_a>;

 port {
  dcmi_0: endpoint {
   remote-endpoint = <&ov5640_0>;
   bus-width = <8>;
   hsync-active = <0>;
   vsync-active = <0>;
   pclk-sample = <1>;
   pclk-max-frequency = <77000000>;
  };
 };
};

&usart3 {
    pinctrl-names = "default", "sleep", "idle";
    pinctrl-0 = <&usart3_pins_bt>;
    pinctrl-1 = <&usart3_idle_pins_bt>;
    pinctrl-2 = <&usart3_sleep_pins_bt>;
    uart-has-rtscts; 
    status = "okay";
        
    bluetooth {
        pinctrl-names = "default";
        pinctrl-0 = <&btreg0>;
        compatible = "brcm,bcm43438-bt";
        max-speed = <3000000>;
        vbat-supply = <&v3v3>;
        vddio-supply = <&v3v3>;
    };
};

&sdmmc3 {
    arm,primecell-periphid = <0x10153180>;
    pinctrl-names = "default", "opendrain", "sleep";
    pinctrl-0 = <&sdmmc3_b4_wifi_pins_a>;
    pinctrl-1 = <&sdmmc3_b4_od_wifi_pins_a>;
    pinctrl-2 = <&sdmmc3_b4_sleep_wifi_pins_a>;
    non-removable;
    st,neg-edge;
    bus-width = <4>;
    vmmc-supply = <&v3v3>;
    mmc-pwrseq = <&wifi_pwrseq>;
    #address-cells = <1>;
    #size-cells = <0>;
    keep-power-in-suspend;
    status = "okay";

    brcmf: bcrmf@1 {
        reg = <1>;
        compatible = "brcm,bcm4329-fmac";
    };
};

&ethernet0 {
 status = "okay";
 pinctrl-0 = <&ethernet0_rgmii_pins_a>;
 pinctrl-1 = <&ethernet0_rgmii_sleep_pins_a>;
 pinctrl-names = "default", "sleep";
 phy-mode = "rgmii-id";
 max-speed = <1000>;
 phy-handle = <&phy0>;

 mdio0 {
  #address-cells = <1>;
  #size-cells = <0>;
  compatible = "snps,dwmac-mdio";
  phy0: ethernet-phy@0 {
   reg = <0>;
  };
 };
};

重新编译设备树make dtbs,此时设备树即可工作

将设备树和内核拷贝到tftpboot目录下

3.4.3查看节点

当上述的内核和设备树编译好之后,在保证摄像头接入的情况下,启动开发板之后就能够看到/dev/video0的节点

3.4.4执行应用程序拍照

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <linux/videodev2.h>
#include <sys/ioctl.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>
#include <string.h>

typedef struct VideoBuffer {
 void   *start;
 size_t  length;
} VideoBuffer;
VideoBuffer *buffers;

int camera_device_open(const char * dev)
{
 int fd;
 //用阻塞模式打开摄像头设备
 fd = open(dev,O_RDWR,0);
 if(fd < 0){
  printf("open %s is fail.\n",dev);
  exit(EXIT_FAILURE);
 }
 return fd;
}

int init_camera_attribute(int fd)
{
 int numBufs;
 v4l2_std_id id;
 struct v4l2_format fmt;
 struct v4l2_requestbuffers  req;
 struct v4l2_buffer    buf;

 //检查当前视频设备支持的标准
 ioctl(fd,VIDIOC_QUERYSTD,&id);
 
 //设置视频捕获格式
 memset(&fmt,0,sizeof(fmt));
 fmt.type  = V4L2_BUF_TYPE_VIDEO_CAPTURE;
 fmt.fmt.pix.width = 640;
 fmt.fmt.pix.height = 480;
 fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
//	fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;

 if(ioctl(fd,VIDIOC_S_FMT,&fmt) == -1){
  perror("set VIDIOC_S_FMT is fail");
  exit(EXIT_FAILURE);
 }
 
 //分配内存
 memset(&req,0,sizeof(req));
 req.count = 4;
 req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
 req.memory = V4L2_MEMORY_MMAP;
 
 if(ioctl(fd,VIDIOC_REQBUFS,&req) == -1){
  perror("set VIDIOC_REQBUFS is fail");
  exit(EXIT_FAILURE);
 }

 //获取并记录缓存的物理空间
 buffers = calloc(req.count,sizeof(*buffers));
 for(numBufs = 0; numBufs < req.count; numBufs ++){
  memset(&buf,0,sizeof(buf));
  buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
  buf.memory = V4L2_MEMORY_MMAP;
  buf.index = numBufs;

  //读取缓存
  if(ioctl(fd,VIDIOC_QUERYBUF,&buf) == -1){
   perror("set VIDIOC_REQBUFS is fail");
   exit(EXIT_FAILURE);
  }
   // 转换成相对地址
  buffers[numBufs].length = buf.length;
  buffers[numBufs].start  = mmap(NULL,buf.length,PROT_READ|PROT_WRITE,
    MAP_SHARED,fd,buf.m.offset);
  if(buffers[numBufs].start == MAP_FAILED){
   perror("mmap is fail");
   exit(EXIT_FAILURE); 
  }

  // 放入缓存队列
  if(ioctl(fd,VIDIOC_QBUF,&buf) == -1){
   perror("set VIDIOC_QBUF is fail");
   exit(EXIT_FAILURE);
  }
 }
 
 return 0;
}

int start_capturing(int fd)
{
 enum v4l2_buf_type type;

 //开始采集数据
 type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
 if(ioctl(fd,VIDIOC_STREAMON,&type) == -1){
  perror("start capturing is fail");
  exit(EXIT_FAILURE);
 }

 return 0;
}

int build_picture(void *addr,int length)
{
 FILE *fp;
 static int num=0;
 char picture_name[20];
 sprintf(picture_name,"picture%d.yuv",num++);
 
 fp = fopen(picture_name,"w");
 if(fp == NULL){
  perror("fail to open ");
  exit(EXIT_FAILURE);
 }

 fwrite(addr,1,length,fp);

 fclose(fp);

 return 0;
}

int read_image(int fd)
{
 struct v4l2_buffer buf;
 memset(&buf,0,sizeof(buf));
 buf.type=V4L2_BUF_TYPE_VIDEO_CAPTURE;
 buf.memory=V4L2_MEMORY_MMAP;
 buf.index=0;

 //读取缓存
 if(ioctl(fd,VIDIOC_DQBUF,&buf) == -1){
  perror("set VIDIOC_DQBUF is fail");
  exit(EXIT_FAILURE);
 }//将数据存为图片
 build_picture(buffers[buf.index].start,buffers[buf.index].length);

 //重新放入缓存队列
 if(ioctl(fd,VIDIOC_QBUF,&buf) == -1){
  perror("reset VIDIOC_QBUF is fail");
  exit(EXIT_FAILURE);
 }

 return 0;

}

int when_to_read(int fd)
{
 int i=0;
 for(i=0;i<6;i++)
 {
  fd_set rfds;
  struct timeval tv;
  int retval;

  FD_ZERO(&rfds);
  FD_SET(fd, &rfds);

  tv.tv_sec = 2;
  tv.tv_usec = 0;

  retval = select(fd+1, &rfds, NULL, NULL, &tv);
  if(retval == -1){
    perror("select()");
    exit(EXIT_FAILURE);
  }else if(retval == 0){
   printf("select is timeout\n");
  }else{
   read_image(fd);
  }
 }

 return 0;
}

int stop_capturing(int fd)
{
 enum v4l2_buf_type type;

 //开始采集数据
 type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
 if(ioctl(fd,VIDIOC_STREAMOFF,&type) == -1){
  perror("stop capturing is fail");
  exit(EXIT_FAILURE);
 }

 return 0;
}

int uninit_camera(int fd)
{
 int i;
 for(i=0;i<4;i++){
  if(-1 == munmap(buffers[i].start,buffers[i].length))
  {
   perror("munmap is fail");
   exit(EXIT_FAILURE);
  }
 }
 free(buffers);
 
 close(fd);
 return 0;
}

int main(int argc, const char *argv[])
{
 int fd;
 fd = camera_device_open(argv[1]); 
 
 init_camera_attribute(fd);
 
 start_capturing(fd);
 
 when_to_read(fd);

 stop_capturing(fd);
 
 uninit_camera(fd);

 return 0;
}

编译之后arm-linux-gnueabihf-gcc ov5640_camera_app.c -o ov5640_camera_app,

执行./ov5640_camera_app /dev/video0

就能够拍摄出来照片了 picture0.yuv - picture5.yuv

3.4.5查看图片

上述的命令在开发板的终端执行完之后,得到了picturex.yuv但是这个图片不能直接打开,

因为没有按照图片的格式来存储,这个文件中存储的是图像的原始数据,如果想查看需

要在Ubuntu上安装如下的工具:

sudo apt-get install ffmpeg

当你安装完这个工具有在Ubuntu上执行

ffplay -pixel_format yuyv422 -video_size 640*480 picture5.yuv 
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值