侧边栏壁纸
博主头像
xiaoming 博主等级

累死自己,卷死别人,为了小刘而努力!!!

  • 累计撰写 34 篇文章
  • 累计创建 7 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

IMX6ULL Linux开发板学习记录

Administrator
2024-02-08 / 0 评论 / 0 点赞 / 7 阅读 / 0 字 / 正在检测是否收录...

1、nxp uboot 编译下载

  • 命令 解释
    bdinfo 查询板子运行地址,网卡等信息
    printenv 查看当前板子的环境变量
    setenv 设置环境变量
    saveenv 保存环境变量
  • 下载编译好的 uboot 到 sd 卡

    • 复制正点原子 imxdownload 软件到 uboot 目录下,并赋予可执行的权限
    • sudo fdisk -l  #查看u盘挂载到哪里
      
    • # 将uboot文件下载到sd卡中,不需要配置sd卡,格式化之后下载
      ./imxdownload u-boot.bin /dev/sdb
      
  • 配置网口连接网络

    • 设置网口网络

      setenv ipaddr 192.168.0.50         # 开发板ip地址
      setenv ethaddr 00:04:9f:04:d2:25   # 开发板网卡mac地址
      setenv gatewayip 192.168.0.1       # 开发板默认网关
      setenv netmask 255.255.255.0       # 开发板子网掩码
      setenv serverip 192.168.0.196      # 服务器地址,也就是ubuntu的地址
      saveenv
      
    • 设置开发板从 tftp 下载 zimage 和 dtb,nfs 挂载根文件系统

      setenv bootargs 'console=tty0 console=ttymxc0,115200 rw nfsroot=192.168.0.196:/home/linux/nfs/rootfs,proto=tcp ip=192.168.0.50:192.168.0.196:192.168.1.1:255.255.255.0::eth0:off'   #ttymxc0,115200 为板子串口,tty0 为显示屏
      setenv bootcmd 'tftp 80800000 zImage;tftp 83000000 imx6ull-alientek-emmc.dtb;bootz 80800000 - 83000000'
      saveenv
      

2、nxp linux 内核编译下载

  • linux 内核编译步骤

    • make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean         # 配置清理工程
      make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_v7_defconfig  # 配置默认配置文件
      make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig        # 启动图形化配置界面
      make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j16              # 编译内核 一般选择电脑逻辑处理器的三分之二作为虚拟机的处理器
      
  • linux 内核下载到sd卡

    将 zImage 和 .dts 文件复制到 boot 分区

3、编译下载 rootfs 文件

4、加载测试设备树

  • 查看设备树节点 cd /proc/device-tree/

image-20230706235451613

  • 加载驱动模块 depmod modprobe rmmod lsmod

image-20230706235658746

  • 查看设备是否注册成功

     ls /dev   # 列出所有已注册的设备
    
  • 应用程序测试驱动模块功能

    • 编译应用程序

      arm-linux-gnueabihf-gcc ledApp.c -o ledApp  # 编译应用程序
      sudo cp ledAPP  /home/linux/nfs/rootfs/lib/modules/4.1.15/ -f # 拷贝应用程序
      
    • 测试应用程序

      image-20230707145847246

  • GPIO 功能配置

    1. 配置设备树

      key{
      compatible = "alientek,key";
      pinctrl-names = "default";
      pinctrl-0 = <&pinctrl_key>;
      key-gpios = <&gpio1 18 GPIO_ACTIVE_LOW>;
      status = "okay";
      interrupt-parent = <&gpio1>;
      interrupts = <18 IRQ_TYPE_EDGE_BOTH>;
      };
      
      
      pinctrl_key: keygrp{
          fsl,pins = <
          MX6UL_PAD_UART1_CTS_B__GPIO1_IO18   0xf080
          >;
      };
      
    2. 获取设备树节点

      of_find_node_by_path("/key"); 
      
    3. 获取设备树节点信息

    of_get_named_gpio(dev->nd, "key-gpios", 0);
    
    1. 请求 GPIO 使用

      gpio_request(dev->irqkey[i].gpio, dev->irqkey[i].name); 
      

      添加释放 GPIO

      gpio_free(dev->irqkey[i].gpio); 
      
    2. 设置 GPIO 的输入

      gpio_direction_input(dev->irqkey[i].gpio);  
      

      设置 GPIO 输出

      gpio_direction_output(gpioled.led_gpio, 1); 
      
    3. 中断结构体

      /* 中断IO描述结构体 */
      struct irq_keydesc {
      	int gpio;								/* gpio */
      	int irqnum;								/* 中断号     */
      	unsigned char value;					/* 按键对应的键值 */
      	char name[10];							/* 名字 */
      	irqreturn_t (*handler)(int, void *);	/* 中断服务函数 */
      };
      
    4. 设置 GPIO 中断功能

      dev->irqkey[i].irqnum = gpio_to_irq(dev->irqkey[i].gpio); 
      
    5. 请求 GPIO 中断

      ret = request_irq(dev->irqkey[i].irqnum, dev->irqkey[i].handler,IRQF_TRIGGER_RISING|IRQF_TRIGGER_FALLING, dev->irqkey[i].name, &imx6uirq); 
      

      释放请求的 GPIO 中断

      free_irq(imx6uirq.irqkey[i].irqnum, &imx6uirq); 
      

5、错误处理

  • 编译驱动报错

    image-20230706220718602

    解决方法:重新编译使用到的 linux 内核源码

  • 应用程序使用 printf 没有将信息输出出来

    由于 printf 缓冲区机制导致信息被暂时保存到缓冲区中,只有遇到刷新标志或者缓冲区满了才会显示出来

    解决方法:在要输出的字符串后面加上 '\n'

  • 编译内核出错

    /usr/bin/ld: scripts/dtc/dtc-parser.tab.o:(.bss+0x50): multiple definition of `yylloc'; scripts/dtc/dtc-lexer.lex.o:(.bss+0x0): first defined here
    collect2: error: ld returned 1 exit status
    make[2]: *** [scripts/Makefile.host:100:scripts/dtc/dtc] 错误 1
    make[1]: *** [scripts/Makefile.build:403:scripts/dtc] 错误 2
    

    ubuntu 版本过高

    解决方法:

    在 ./scripts/dtc/dtc-lexer.lex.c:640:YYLTYPE yylloc;
    
    前面加上 extern
    
  • 加载驱动模块报错

    image-20230822155123990

    解决方法:

    将内核源码里的这些文件复制到模块目录下

    image-20230822155239317

6、并发与竞争

  • 原子操作定义和使用

    atomic_t a;  //定义a变量
    atomic_t b=ATOMIC_INT(0);  //定义原子变量b并赋值初值为0
    
  • 原子操作 API

    函数 描述
    ATOMIC_INIT(int i) 定义原子变量的时候对其初始化
    int atomic_read(atomic_t *v) 读取v的值,并且返回
    void atomic_set(atomic_t *v, int i) 向v写入i值
    void atomic_add(int i, atomic_t *v) 给v加上i值
    void atomic_sub(int i, atomip_t *v) 从v减去i值
    void atomic_inc(atomic_t *v) 给v加1,也就是自增
    void atomic_dec(atomic_t*v) 从v减1,也就是自减
    int atomic_dec_return(atomic_t *v) 从v减1,并且返回v的值
    int atomic_inc_return(atomic_t *v) 给v加1,并且返回v的值
    int atomic_sub_and_test(int i, atomic_t *v) 从v减i,如果结果为0就返回真,否则返回假
    int atomic_dec_and_test(atomic_t*v) 从v减1,如果结果为0就返回真,否则返回假
    int atomic_inc_and_test(atomic_t *v) 给v加1,如果结果为0就返回真,否则返回假
    int atomic_add_negative(int i, atomic_t *v) 给v加i,如果结果为负就返回真,否则返回假
  • 原子位操作 API

    函数 描述
    void set_bit(int nr, void*p) 将p地址的第nr位置1
    void clear_bit(int nr,void *p) 将p地址的第nr位清零
    void change_bit(int nr, void*p) 将p地址的第nr位进行翻转
    int test_bit(int nr, void *p) 获取p地址的第nr位的值
    int test_and_set_bit(int nr, void *p) 将p地址的第nr位置1,并且返回nr位原来的值
    int test_and_clear_bit(int nr, void *p) 将p地址的第nr位清零,并且返回nr位原来的值
    int test_and_change_bit(int nr, void *p) 将p地址的第nr位翻转,并且返回nr位原来的值
  • 自旋锁

    • 注意事项

      • 自旋锁的持有时间不能太长
      • 自旋锁适用于短时间的轻量加锁
      • 线程与线程:被自旋锁保护的临界区一定不能调用任何睡眠和阻塞的 API 函数否则会造成死锁
      • 线程与中断:中断可以打断被自旋锁保护的临界区,如果中断也需要使用共享资源会造成死锁,使用关闭本地中断的 API 函数获取锁
    • 自旋锁 API

      函数 描述
      DEFINE_SPINLOCK(spinlock_t lock) 定义并初始化一个自选变量
      int spin_lock_init(spinlock_t*lock) 初始化自旋锁。
      void spin_lock(spinlock_t*lock) 获取指定的自旋锁,也叫做加锁。
      void spin_unlock(spinlock_t *lock) 释放指定的自旋锁。
      int spin_trylock(spinlock_t *lock) 尝试获取指定的自旋锁,如果没有获取到就返回0
      int spin_is_locked(spinlock_t*lock) 检查指定的自旋锁是否被获取,如果没有被获取就返回非0,否则返回0。
      void spin_lock_irq(spinlock_t *lock) 禁止本地中断,并获取自旋锁。
      void spin_unlock_irq(spinlock_t *lock) 激活本地中断,并释放自旋锁。
      void spin_lock_irqsave(spinlock_t*lock,unsigned long flags) 保存中断状态,禁止本地中断,并获取自旋锁。
      void spin_unlock_irqrestore(spinlock_t*lock, unsigned long flags) 将中断状态恢复到以前的状态,并且激活本地中断,释放自旋锁
  • 信号量(二值信号量)

    • 注意事项

      • 信号量可以使等待的资源线程进入休眠的状态,适用于那些占用资源比较久的场合
      • 信号量不能应用于中断中,因为信号量会引起休眠,中断不能休眠
      • 如果共享资源的持有时间比较短,不适合使用信号量,频繁的休眠、切换线程的开销要远大于信号量带来的优势
    • 信号量 API 函数

      函数 描述
      DEFINE_SEAMPHORE(name) 定义一个信号量,并且设置信号量的值为1
      void sema_init(struct semaphore *sem, int val) 初始化信号量sem,设置信号量值为val
      void down(struct semaphore *sem) 获取信号量,因为会导致休眠,因此不能在中l断中使用
      int down_trylock(struct semaphore *sem); 尝试获取信号量,如果能获取到信号就获取,并且返回0。如果不能就迟回非0,并且不会进入休眠
      int down_interruptible(struct semaphore *sem) 获取信号量,和 down类似,只是使down进入休眠状态的线程不能被信号打断。而使用此函数进入休眠以后是可以被信号打断的
      void up(struct semaphore *sem) 释放信号量
  • 互斥体

    • 注意事项

      • 互斥体可以导致休眠,不能在中断中使用
      • 和信号量一样,互斥体保护的临界区可以调用引起阻塞的 API 函数
      • 互斥体一次只能一个线程持有,不能递归上锁和解锁
    • 互斥体 API 函数

      函数 描述
      DEFINE_MUTEX(name) 定义并初始化一个mutex变量。
      void mutex_init(mutex*lock) 初始化mutex。
      void mutex_lock(struct mutex *lock) 获取mutex,也就是给mutex 上锁。如果获取不到就进休眠。
      void mutex_unlock(struct mutex*lock) 释放mutex,也就给mutex解锁。
      int mutex_trylock(struct mutex *lock) 尝试获取mutex,如果成功就返回1,如果失败就返回0。
      int mutex_is_locked(struct mutex *lock) 判断mutex是否被获取,如果是的话就返回1,否则返回0。
      int mutex_lock_interruptible(struct mutex *lock) 使用此函数获取信号量失败进入休眠以后可以被信号打断。

7、内核定时器

  • 判断是否超时

    函数 描述
    time_after(unkown, known) unkown通常为jiffies,known通常是需要对比的值。
    time_before(unkown, known)
    time_after_eq(unkown, known)
    time_before_eq(unkown, known)

    如果 unkown 超过 known 的话,time_after 函数返回真,否则返回假。如果 unkown 没有超过 known 的话 time_before 函数返回真,否则返回假。time_after_eq 函数和 time_after 函数类似,只是多了判断等于这个条件。同理,time_before_eq 函数和 time_before 函数也类似。比如我们要判断某段代码执行时间有没有超时,此时就可以使用如下所示代码:

    unsigned long timeout;
    timeout = jiffies + (2*HZ) ;/*超时的时间点*/
    
    /*判断有没有超时*/
    if(time_before(jiffies, timeout) ){
    /*超时未发生*/
    }else {
    /*超时发生*/
    )
    
  • 时间类型转换

    函数 描述
    int jiffies_to_msecs(const unsigned long j) 将jiffies类型的参数j分别转换为对应的毫秒、微秒、纳秒。
    int jiffies_to_usecs(const unsigned long j)
    u64 jiffies_to_nsecs(const unsigned long j)
    long msecs_to_jiffies(const unsigned int m) 将毫秒、微秒、纳秒转换为jiffies类型。
    long usecs_to_jiffies(const unsigned int u)
    unsigned long nsecs_to_jiffies(u64 n)
  • 定时器API函数

    • init_timer 函数

      init_timer 函数负责初始化 timer_list 类型变量,当我们定义一个 timer_list 变量以后一定要先用 init_timer 初始化一下。init_timer 函数原型如下:

      void init_timer(struct timer_list *timer)
      

      timer:要初始化定时器

      返回值:无

    • add_timer 函数

      add_timer 函数用于向 Linux 内核注册定时器,使用 add_timer 函数向内核注册定时器以后,定时器就会开始运行,函数的原型如下:

      void add_timer(struct timer_list *timer)
      

      timer:要要注册定时器

      返回值:无

    • del_timer 函数

      del_timer 函数用于删除一个定时器,不管定时器有没有被激活,都可以使用此函数删除。在多处理器系统上,定时器可能会在其他的处理器上运行,因此在调用 del_timer 函数删除定时器之前要先等待其他处理器的定时处理器函数退出。del_timer 函数原型如下:

      int del_timer(struct timer_list * timer)
      

      timer:要删除的定时器

      返回值:0,定时器还没被激活;1,定时器已经激活

    • del_timer_sync 函数

      del_timer_sync 函数是 del_timer 函数的同步版,会等待其他处理器使用完定时器再删除,del_timer_sync 不能使用在中断上下文中。del_timer_sync 函数原型如下所示:

      int del_timer_sync(struct timer_list *timer)
      

      timer:要删除的定时器

      返回值:0,定时器还没被激活;1,定时器已经激活

    • mod_timer 函数

      mod_timer 函数用于修改定时值,如果定时器还没有激活的话,mod_timer 函数会激活定时器!函数原型如下:

      int mod_timer(struct timer_list *timer, unsigned long expires)
      

      timer:要修改超时时间(定时值)的定时器

      expires:修改后的超时时间

      返回值:0,调用 mod_timer 函数前定时器未被激活;1,调用 mod_timer 函数前定时器已被激活。

    • 使用方式

      /* 定时器中断函数 */
      static void timer_func(unsigned long arg)
      {
          struct gpioled_dev *dev = (struct gpioled_dev*)arg;
      }
      /* 7. 初始化定时器
      init_timer(&timerdev.timer)
      
      timerdev.timer.function = timer_func;
      timerdev.timer.expires = jiffies + msecs_to_jiffies(500); //!!!
      timerdev.timer.data = (unsigned long)&gpioled;
      add_timer(&timerdev.timer);/*添加到系统*/
      
  • 内核短延时函数

    • 非阻塞延时

      schedule_timeout 函数

    • 阻塞延时

      函数 描述
      void ndelay(unsigned long nsecs) 纳秒延时函数
      void udelay(unsigned long nsecs) 微秒延时函数
      void mdelay(unsigned long nsecs) 毫秒延时函数

8、内核中断

  • 设备树配置 GPIO 中断 (interrupt.h) 文件中

    *zozhongkai key */
    key{
    	compatible = "alientek , key";
        pinctrl-names = "default";
    	pinctrl-0 = <&pinctrl_key>;
    	key-gpios = <&gpio1 18 GPIO_ACTIVE_HIGH>;
    	status = "okay " ;
    	interrupt-parent = <&gpio1>;         // 设置GPIO引脚中断
    	interrupts = <18 IRQ_TYPE_EDGE_BOTH>;
    };
    
  • 驱动获取中断编号

    编写驱动的时候需要用到中断号,我们用到中断号,中断信息已经写到了设备树里面,因此可以通过 irq_of_parse_and_map 函数从 interupts 属性中提取到对应的设备号,函数原型如下:

    unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
    

    dev:设备节点

    index:索引号,interrupts 属性可能包含多条中断信息,通过 index 指定要获取的信息

    返回值:中断号

    如果使用 GPIO 的话,可以使用 gpio_to_irq 函数来获取 gpio 对应的中断号,函数原型如下:

    int gpio_to_irq(unsigned int gpio)
    

    gpio:要获取的 GPIO 编号

    返回值:GPIO 对应的中断号

  • 驱动中断请求与配置(中断触发方式文件定义 (include/linux/irq.h)

    • request_irq 函数

      在 Linux 内核中要想使用某个中断是需要申请的,request_irq 函数用于申请中断,request_irq 函数可能会导致睡眠,因此不能在中断上下文或者其他禁止睡眠的代码段中使用 request_irq 函数。request_irq 函数会激活(使能)中断,所以不需要我们手动去使能中断,request_irq 函数原型如下:

      int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev)
      

      irq:要申请中断的中断号
      handler:中断处理函数,当中断发生以后就会执行此中断处理函数
      flags:中断标志,可以在文件 include/linux/interrupt.h 里面查看所有的中断标志

      几个常用的中断标志

      标志 描述
      IRQF_SHARED 多个设备共享一个中断线,共享的所有中断都必须指定此标志。如果使用共享中断的话,request_irq函数的dev参数就是唯一区分他们的标志
      IRQF_ONESHOT 单次中断,中断执行一次就结束
      IRQF_TRIGGER NONE 无触发
      IRQF_TRIGGER_RISING 上升沿触发
      IRQF_TRIGGER_FALLING 下降沿触发
      IROF_TRIGGER HIGH 高电平触发
      IRQF_TRIGGER LOW 低电平触发

      name:中断名字,设置以后可以在 /proc/interrupts 文件中看到对应的中断名字。
      dev:如果将 flags 设置为 IRQF_SHARED 的话,dev 用来区分不同的中断,一般情况下将 dev 设置为设备结构体,dev 会传递给中断处理函数 irq_handler_t 的第二个参数。
      返回值:0 中断申请成功,其他负值中断申请失败,如果返回 -EBUSY 的话表示中断已经被申请了。

    • free_irq 函数

      使用中断的时候需要通过 request_irq 函数申请,使用完成以后就要通过 free_irq 函数释放掉相应的中断。如果中断不是共享的,那么 free_irq 会删除中断处理函数并且禁止中断。free_irq 函数原型如下所示:

      void free_irq(unsigned int irq, void *dev)
      

      irq:要释放的中断
      dev:如果中断设置为共享 (IRQF_SHARED) 的话,此参数用来区分具体的中断。共享中断只有在释放最后中断处理函数的时候才会被禁止掉
      返回值:无

    • 中断处理函数

      使用 request_irq 函数申请中断的时候需要设置中断处理函数,中断处理函数格式如下所示:

      static irqreturn_t key0_handler(int irq, void *dev_id)
      {
      	struct imx6uirq_dev *dev = dev_id;
      	imx6uirq.timer.data = (volatile unsigned long)dev_id;
      	mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10));
      
      	return IRQ_HANDLED;
      }
      
      irqreturn_t (*irq_handler_t) (int, void*)
      

      第一个参数是要中断处理函数要相应的中断号。第二个参数是一个指同 void 的指针,也就是个通用指针,需要与 request_irq 函数的 dev 参数保持一致。用于区分共享中断的不同设备,dev 也可以指向设备数据结构。中断处理函数的返回值为 irqreturn_t 类型,irqreturn_t 类型定义如下所示:

      enum irqreturn {
      	IRO_NONE		= (0 << 0),
      	IRQ_HANDLED 	= (1 << 0),
      	IRO_WAKE_THREAD	= (l < 1),
      };
      
      typedef enum irqreturn irqreturn_t;
      

      可以看出 irqreturn_t 是个枚举类型,一共有三种返回值。一般中断服务函数返回值使用如下形式:

      return IRQ_RETVAL(IRQ_HANDLED)
      
    • 中断使能与禁止函数
      常用的中断使用和禁止函数如下所示:

      void enable_irq(unsigned int irq)
      void disable_irq(unsigned int irq)
      

      enable_irq 和 disable_irq 用于使能和禁止指定的中断,irq 就是要禁止的中断号。

  • 查看中断申请是否成功

    image-20230715181354017

  • 中断下半部处理

    中断还是由上半部分触发,但是中断处理函数中的内容由定义的中断下半部分去处理,这样可以快速的从上半部分中断中结束中断函数处理,本来应该在上半部分中断函数里的需要处理的内容现在由下半部中断函数在之后合适的时间处理。即类似在上半部分中断处理函数中加入了任务调度。

    tasklet 软中断

    • 特点

      1. 软中断上下文:
        • Tasklet 在软中断上下文中执行。这意味着它们不能睡眠,因为软中断上下文不能进行阻塞操作。
      2. 快速执行:
        • Tasklet 设计用于短小的任务,应尽可能快速执行。适合处理短暂的后续中断处理。
      3. 串行执:
        • 同一 tasklet 类型的所有实例在同一时刻只能有一个在运行,保证不会同时运行多个相同类型的 tasklet 实例。
        • 但不同 tasklet 类型可以并行执行。

      适用场景

      1. 后续中断处理:
        • 适合从中断处理程序中剥离出来的短小任务。例如,网络驱动中的后续数据处理。
      2. 高优先级任务:
        • 需要比工作队列有更高的优先级,但又不能在中断上下文中完成的任务。

    工作队列

    • 特点

      1. 进程上下文:
        • 工作队列在进程上下文中执行,可以进行阻塞操作,比如睡眠、等待锁等。
      2. 复杂任务:
        • 适合执行更复杂、更长时间的任务。
      3. 并行执行:
        • 工作队列中的多个工作项可以并行执行,前提是它们被分配到不同的工作线程上。

      适用场景

      1. 需要睡眠的任务:
        • 任何可能需要睡眠或等待的任务都应该使用工作队列。
      2. 长时间执行的任务:
        • 任务可能执行时间较长,不适合在中断上下文或软中断上下文中执行的情况。

    如果要使用 tasklet ,必须先定义一个 tasklet ,然后使用 tasklet_init 函数初始化 tasklet,taskled_init 函数原型如下:

    void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned longdata);
    

    t:要初始化的 tasklet

    func:tasklet 的处理函数

    data:要传递给 func 函数的参数

    返回值:没有返回值
    也可以使用宏 DECLARE_TASKLET 来一次性完成tasklet的定义和初始化,DECLARE_TASKLET 定义在 include/linux/interrupt.h 文件中,定义如下:

    DECLARE_TASKLET(name, func, data)
    

    其中 name 为要定义的 tasklet 名字,这个名字就是一个 tasklet_struct 类型的时候变量,func 就是 tasklet 的处理函数,data 是传递给 func 函数的参数。

    在上半部,也就是中断处理函数中调用 tasklet_schedule 函数就能使 tasklet 在合适的时间运行,tasklet_schedule 函数原型如下:

    void tasklet_schedule(struct tasklet_struct *t)
    

    t:要调度的 tasklet,也就是 DECLARE_TASKLET 宏里面的 name
    返回值:没有返回值

  • tasklet 使用方式

    /* 定义 taselet */
    struct tasklet_struct testtasklet;
    
    /* tasklet 处理函数 */
    void testtasklet_func(unsigned long data)
    {
    /* tasklet 具体处理内容 */
    }
    
    /* 中断处理函数 */
    irqreturn_t test_handler(int irq, void *dev_id)
    {
        ......
        /* 调度 tasklet */
        tasklet_schedule(&testtasklet);
        ......
    }
    
    /* 驱动入口函数 */
    static int __init xxxx_init(void)
    {
        ......
        /* 初始化 tasklet */
        tasklet_init(&testtasklet, testtasklet_func, data);
        /* 注册中断处理函数 */
        request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
        ......
    }
    
  • 工作队列

9、阻塞、非阻塞与异步通知

阻塞、非阻塞、异步通知,这三种是针对不同的场合提出来的不同的解决方法,没有优劣之分,在实际的工作和学习中,根据自己的实际需求选择合适的处理方法即可。

阻塞:当资源不可用的时候,应用程序就会挂起。当资源可用的时候,唤醒任务。应用程序使用open打开驱动文件,默认是阻塞方式打开。
非阻塞:当资源不可用的时候,应用程序轮询查看,或放弃。会有超时处理机制。应用程序在使用open打开驱动文件的时候,使用O_NONBLOCK。

异步通知:用于在资源状态发生变化时通知应用程序,而不需要应用程序不断地轮询资源状态。

  • 应用程序的阻塞与非阻塞的访问

    • int fd;
      int data = 0;
      
      fd = open("/dev/xxx_dev ", O_RDWR);			/*阻塞方式打开*/
      ret = read(fd, &data, sizeof(data));		/*读取数据*/
      
    • int fd;
      int data = 0;
      
      fd = open("/dev/xxx_dev ", O_RDWR | O_NONBLOCK);			/*非阻塞方式打开*/
      ret = read(fd, &data, sizeof(data));						/*读取数据*/
      
  • 阻塞 IO 方式--等待队列

    • 等待队列头

      阻塞访问最大的好处就是当设备文件不可操作的时候进程可以进入休眠态,这样可以将 CPU 资源让出来。但是,当设备文件可以

      操作的时候就必须唤醒进程,一般在中断函数里面完成唤醒工作。Linux 内核提供了等待队列 (wait queue) 来实现阻塞进程的唤醒

      工作,如果我们要在驱动中使用等待队列,必须创建并初始化一个等待队列头,等待队列头使用结构体 wait_queue_head_t 表

      示,wait_queue_head_t 结构体定义在文件 include/linux/wait.h 中,结构体内容如下所示:

      struct __wait__queue_head {
      	spinlock_t lock;
      	struct list_head task_list;
      };
      typedef struct _wait_queue_head wait_queue_head_t;
      

      定义好等待队列头以后需要初始化,使用 init_waitqueue_head 函数初始化等待队列头,函数原型如下:

      void init_waitqueue_head(wait_queue_head_t*q)
      

      参数 q 就是要初始化的等待队列头

      也可以使用宏 DECLARE_WAIT_QUEUE_HEAD 来一次性完成等待队列头的定义的初始化

    • 等待队列项

      等待队列头就是一个等待队列的头部,每个访问设备的进程都是一个队列项,当设备不可用的时候就要将这些进程对应的等待队列项添加到等待队列里面。结构体 wait _queue_t 表示等待队列项,结构体内容如下:

      struct __wait_queue
      {
          unsigned int flags;
          void *private;
          wait_queue_func_t func;
          struct list_head task_list;
      };
      typedef struct __wait_queue wait_queue_t;
      

      使用宏 DECLARE_WAITQUEUE 定义并初始化一个等待队列项,宏的内容如下:

      DECLARE_WAITQUEUE(name, tsk)
      

      name 就是等待队列项的名字,tsk表示这个等待队列项属于哪个任务(进程),一般设置为 current,在 Linux 内核中 current 相当于一个全局变量,表示当前进程。因此宏 DECLARE_WAITQUEUE 就是给当前正在运行的进程创建并初始化了一个等待队列项。

    • 将队列项添加或移除等待队列头

      当设备不可访问的时候就需要将进程对应的等待队列项添加到前面创建的等待队列头中,只有添加到等待队列头中以后进程才能进入休眠态。当设备可以访问以后再将进程对应的等待队列项从等待队列头中移除即可,等待队列项添加API函数如下

      void add_wait_queue(wait_queue_head t *q, wait_queue_t *wait)
      

      函数参数和返回值含义如下:

      q:等待队列项要加入的等待队列头。

      wait:要加入的等待队列项。

      返回值:无。

      等待队列项移除 API 函数如下:

      void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
      

      函数参数和返回值含义如下:

      q:要删除的等待队列项所处的等待队列头。

      wait:要删除的等待队列项。
      返回值:无。

    • 等待唤醒

      当设备可以使用的时候就要唤醒进入休眠态的进程,唤醒可以使用如下两个函数:

      void wake_up(wait_queue_head_t *q)
      void wake_up_interruptible(wait_queue_head_t *q)
      

      参数 q 就是要唤醒的等待队列头,这两个函数会将这个等待队列头中的所有进程都唤醒。wake_up 函数可以唤醒处于TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 状态的进程,而 wake_up_interruptible 函数只能唤醒处于TASK_INTERRUPTIBLE 状态的进程。

    • 等待事件

      除了主动唤醒以外,也可以设置等待队列等待某个事件,当这个事件满足以后就唤醒等待队列中的进程,和等待事件有关的 API函数:

      函数 描述
      wait_event(wq, condition) 等待以 wq 为等待队列头的等待队列被唤醒,前提是 condition 条件必须满足(为真),否则一直阻塞。此函数会将进程设置为TASK_UNINTERRUPTIBLE 状态
      wait_event_timeout(wq, condition, timeout) 功能和 wait_event 类似,但是此函数可以添加超时时间,以 jiffies 为单位。此函数有返回值,如果返回0的话表示超时时间到,而且condition 为假。为1的话表示 condition 为真,也就是条件满足了。
      wait_event_interruptible(wq, condition) 与 wait_event 函数类似,但是此函数将进程设置为 TASK_INTERRUPTIBLE,就是可以被信号打断。
      wait_event_interruptible_timeout(wq,condition, timeout) 与 wait_event_timeout 函数类似,此函数也将进程设置为TASK_INTERRUPTIBLE ,可以被信号打断。
  • 使用方式

    • 使用等待事件方式实现

      • 1.1定义等待队列头

        wait_queue_head_t r_wait;   /* 等待队列头 */
        
      • 1.2初始化等待队列头

        /*等待队列头*/
        init_waitqueue_head(&imx6uirq->r_wait);
        
      • 1.3等待事件

        /*等待事件*/
        wait_event_interruptible(dev->r_wait,atomic_read(&dev->releasekey));   /*等待按键有效*/
        
      • 1.4中断中唤醒进程

        成功地唤醒一个被 wait_event_interruptible() 的进程,需要满足:

        1. condition 为真的前提下,即 dev->releasekey 为真
        
        2. 调用 wake_up()
        
        /*唤醒进程*/
        if(atomic_read (&dev->releasekey)) 
        {
            wake_up(&dev->r_wait);
        }
        
    • 使用添加或移除队列项到等待队列头的方式实现

      DECLARE_ WAITQUEUE(wait,current);         /*定义一个等待队列项*/
      add_wait_queue(&dev->r_wait,&wait);      /* 将队列项添加到等待队列头*/
      _set_current_state(TASK_INTERRUPTIBLE);   /*当前进程设置为可被打断的状态*/
      schedule();                  /*切换*/
      /*唤醒以后从这里运行*/
      if (signal_pending(current))   /* 如果是被信号唤醒的 */
      {
          ret = -ERESTARTSYS;
          goto data_error;
      }
      keyvalue = atomic_read(&dev->keyvalue);
      releasekey = atomic_read(&dev->releasekey);
      if(releasekey)       /*有效按键*/
      {
          if(keyvalue & 0×80)
          {
              keyvalue &= ~0x80;
              ret = copy_to_user(buf, &keyvalue,sizeof(keyvalue));
          }
          else
              goto data_error;
          atomic_set(&dev->releasekey,0);    /*按下标志清零*/
      }
      else
          goto data_error;
      
      data error:
          _set_current_state(TASK_RUNNING);  /*将当前任务设置为运行状态*/
          remove_wait_queue(&dev->r_wait,&wait);/*将对应的队列项从等待队列头删除*/
      	return ret;
      
  • 非阻塞 IO 方式--轮询

待补充。。。
  • 异步通知--软中断

    • 信号种类

      #define SIGHUP    1     /* 终端挂起或控制进程终止 */
      #define SIGINT    2     /* 终端中断(Ctrl+C 组合键) */
      #define SIGQUIT   3     /* 终端退出(Ctrl+\组合键) */
      #define SIGILL    4     /* 非法指令 */
      #define SIGTRAP   5     /* debug 使用,有断点指令产生 */
      #define SIGABRT   6     /* 由 abort(3)发出的退出指令 */
      #define SIGIOT    6     /* IOT 指令 */
      #define SIGBUS    7     /* 总线错误 */
      #define SIGFPE    8     /* 浮点运算错误 */
      #define SIGKILL   9     /* 杀死、终止进程 */
      #define SIGUSR1   10    /* 用户自定义信号 1 */
      #define SIGSEGV   11    /* 段违例(无效的内存段) */
      #define SIGUSR2   12    /* 用户自定义信号 2 */
      #define SIGPIPE   13    /* 向非读管道写入数据 */
      #define SIGALRM   14    /* 闹钟 */
      #define SIGTERM   15    /* 软件终止 */
      #define SIGSTKFLT 16    /* 栈异常 */
      #define SIGCHLD   17    /* 子进程结束 */
      #define SIGCONT   18    /* 进程继续 */
      #define SIGSTOP   19    /* 停止进程的执行,只是暂停 */
      #define SIGTSTP   20    /* 停止进程的运行(Ctrl+Z 组合键) */
      #define SIGTTIN   21    /* 后台进程需要从终端读取数据 */
      #define SIGTTOU   22    /* 后台进程需要向终端写数据 */
      #define SIGURG    23    /* 有"紧急"数据 */
      #define SIGXCPU   24    /* 超过 CPU 资源限制 */
      #define SIGXFSZ   25    /* 文件大小超额 */
      #define SIGVTALRM 26    /* 虚拟时钟信号 */
      #define SIGPROF   27    /* 时钟信号描述 */
      #define SIGWINCH  28    /* 窗口大小改变 */
      #define SIGIO     29    /* 可以进行输入/输出操作 */
      #define SIGPOLL SIGIO
      /* #define SIGLOS 29 */
      #define SIGPWR    30    /* 断点重启 */
      #define SIGSYS    31    /* 非法的系统调用 */
      #define SIGUNUSED 31    /* 未使用信号 */
      
    • 异步通知定义与 API 函数

      1、fasync 函数
      如果要使用异步通知,需要在设备驱动中实现 file_operations 操作集中的 fasync 函数,此函数格式如下所示:

      int (*fasync) (int fd, struct file*filp, int on)
      

      fasync 函数里面一般通过调用 fasync_helper 函数来初始化前面定义的 fasync_struct 结构体指针,fasync_helper 函数原型如下:

      int fasync_helper(int fd, struct file* filp, int on, struct fasync_struct**fapp)
      

      fasync_helper 函数的前三个参数就是 fasync 函数的那三个参数,第四个参数就是要初始化的 fasync_struct 结构体指针变量。当应用程序通过 “fcntl(fd, F_SETFL, flags | FASYNC)” 改变 fasync 标记的时候,驱动程序 file_operations 操作集中的 fasync 函数就会执行。

      2、kill_fasync 函数
      当设备可以访问的时候,驱动程序需要向应用程序发出信号,相当于产生“中断”。kill_fasync 函数负责发送指定的信号,kill_fasync 函数原型如下所示:

      void kill_fasync(struct fasync_struct**fp, int sig, int band)
      

      函数参数和返回值含义如下:

      fp:要操作的 fasync_struct。

      sig:要发送的信号。

      band:可读时设置为 POLL_IN,可写时设置为 POLL_OUT。

      返回值:无。

    • 应用程序

      fcntl 函数功能依据 cmd 的值的不同而不同。参数对应功能如下:

      命令名 描述
      F_DUPFD 复制文件描述符
      F_GETFD 获取文件描述符标志
      F_SETFD 设置文件描述符标志
      F_GETFL 获取文件状态标志
      F_SETFL 设置文件状态标志
      F_GETLK 获取文件锁
      F_SETLK 设置文件锁
      F_SETLKW 类似F_SETLK,但等待返回
      F_GETOWN 获取当前接收SIGI0和SIGURG信号的进程ID和进程组ID
      F_SETQWN 设置当前接收SIGIO和SIGURG信号的进程和D进程组ID
    • 使用方式

      • 驱动程序

        1. 使用 fasync_struct 定义一个指针结构体变量

          struct fasync_struct *fasync_que;
          
        2. 实现 file_operations 里面的 fasync 函数,函数原型:

          int (*fasync)(int, struct file*, int)
          

          fasync 还需要借助 fasync_helper 函数

          static int imx6uirq_fasync(int fd, struct file *filp, int on)
          {
              struct imx6uirq_dev *dev = filp->private_data;
              return fasync_helper(fd,filp, on,&dev->fasync_queue);
          }
          
          /*操作集*/
          static const struct file_operations imx6uirq_fops = {
          .owner   =  THIS_MODULE,
          .open    =  imx6uirq_open,
          .read    =  imx6uirq read,
          .fasync  =  imx6uirq fasync,
          .release =  imx6uirq release,
          };
          
        3. 驱动里面调用 fasync 向应用发送信号,函数原型

          void kill_fasync(struct fasync_struct **fp, int sig, int band)
          
          if(atomic_read (&dev->releasekey))   /*有效的按键过程*/
          {
              if (dev->fasync_ queue)
              kill_fasync(&dev->fasync_queue,SIGIO,POLL_IN);   /* 向应用程序发送信号 */
          }
          
        4. 关闭驱动程序的时候删除信号

          int imx6uirq_release(struct inode *inode, struct file *filp)
          {
          	imx6uirq_fasync(-1,filp,0);
          }
          
      • 应用程序

        1. 注册信号处理函数

          static void sigio_signal_func(int num){
              int err;
              unsigned int keyvalue = 0;
              err = read(fd,&keyvalue, sizeof(keyvalue));
              if(err <0){
              }else {
              	printf("sigio signal! key value = %d",keyvalue);
              }
          }
          
          /*设置信号处理函数*/
          signal(SIGI0,sigio_signal_func);
          
        2. 将本应用程序的进程号告诉内核

          fcntl(fd,F_SETOWN, getpid());    /*设置当前进程接收SIGIO信号*/
          
        3. 开启异步通知

          fcntl(fd, F_SETOWN, getpid());       /*设置当前进程接收SIGIO信号*/
          flags = fcntl(fd, F_GETFL);
          fcntl(fd,F_SETFL, flags | FASYNC);  /*异步通知*/
          

10、platform设备驱动

在有设备树的时候设备由设备树描述的,因此不需要向总线注册设备,而是直接修改设备树。只需要修改设备树,然后编写驱动。

  • platform 设备驱动

    • 注册 platform 驱动

      int platform_driver_register (struct platform_driver *driver)
      

      函数参数和返回值含义如下:

      driver:要注册的 platform 驱动。

      返回值:负数,失败;0,成功。

    • 注销 platform 驱动

      void platform_driver_unregister(struct platform_driver *drv)
      

      函数参数和返回值含义如下:

      drv:要卸载的 platform 驱动。

      返回值: 无。

  • 使用方式

    • 头文件

      #include <linux/platform_device.h>
      
    • 修改设备树

      gpioled{
          compatible = "alientek,gpioled";    /* 非常重要用于和驱动进行匹配 */
          pinctrl-names = "default";
          pinctrl-0 = <&pinctrl_gpioled>;
          led-gpios = <&gpio1 3 GPIO_ACTIVE_LOw>;
          status = "okay";
      };
      
    • asds

      /* 设备结构体 */
      struct xxx_dev{
          struct cdev cdev;
          /* 设备结构体其他具体内容 */
      };
      struct xxx_dev xxxdev; /* 定义个设备结构体变量 */
      
      static int xxx_open(struct inode *inode, struct file *filp)
      {
          /* 函数具体内容 */
          return 0;
      }
      
      static ssize_t xxx_write(struct file *filp, const char __user *buf,size_t cnt, loff_t *offt)
      {
          /* 函数具体内容 */
          return 0;
      }
      
      /*
      * 字符设备驱动操作集
      */
      static struct file_operations xxx_fops = {
          .owner = THIS_MODULE,
          .open = xxx_open,
          .write = xxx_write,
      };
      
      /*
      * platform 驱动的 probe 函数
      * 驱动与设备匹配成功以后此函数就会执行
      */
      static int xxx_probe(struct platform_device *dev)
      {
          ......
          cdev_init(&xxxdev.cdev, &xxx_fops); /* 注册字符设备驱动 */
          /* 函数具体内容 */
          return 0;
      }
      
      static int xxx_remove(struct platform_device *dev)
      {
          ......
          cdev_del(&xxxdev.cdev);/* 删除 cdev */
          /* 函数具体内容 */
          return 0;
      }
      
      /* 匹配列表 */
      static const struct of_device_id xxx_of_match[] = {
          { .compatible = "xxx-gpio" },
          { /* Sentinel */ }
      };
      
      /*
      * platform 平台驱动结构体
      */
      static struct platform_driver xxx_driver = {
          .driver = {
              .name = "xxx",    /* 名字如果和设备树中相应节点的名字一样,会直接匹配成功 */
              .of_match_table = xxx_of_match,
          },
          .probe = xxx_probe,
          .remove = xxx_remove,
      };
      
      
      /* 驱动模块的自动加载和卸载 */
      module_platform_driver(&xxx_driver);
      /* 二选一 */
      #if 0
      /* 驱动模块加载 */
      static int __init xxxdriver_init(void)
      {
          return platform_driver_register(&xxx_driver);
      }
      
      /* 驱动模块卸载 */
      static void __exit xxxdriver_exit(void)
      {
          platform_driver_unregister(&xxx_driver);
      }
      
      module_init(xxxdriver_init);
      module_exit(xxxdriver_exit);
      #endif
      
      
      MODULE_LICENSE("GPL");
      MODULE_AUTHOR("zuozhongkai");
      

11、MISC 驱动框架

所有的 MISC 设备驱动的主设备号都为 10,不同的设备使用不同的从设备号。随着 Linux 字符设备驱动的不断增加,设备号变得越来越紧张,尤其是主设备号, MISC 设备驱动就用于解决此问题。 MISC 设备会自动创建 cdev,不需要像我们以前那样手动创建,因此采用 MISC 设备驱动可以简化字符设备驱动的编写。

  • 修改设备树

  • 编写驱动程序

    • 头文件

      #include <linux/miscdevice.h>
      
    • 定义

      /* MISC 设备结构体 */
      static struct miscdevice beep_miscdev = {
          .minor = MISCBEEP_MINOR,   /* 子设备号 范围0-255*/
          .name = MISCBEEP_NAME,     /* 设备名字 */
          .fops = &miscbeep_fops,    /* 设备操作集 */
      };
      
    • 注册MISC设备

      int misc_register(struct miscdevice * misc)
      

      函数参数和返回值含义如下:
      misc:要注册的 MISC 设备。
      返回值:负数,失败;0,成功。

    • 注销 MISC 设备

      int misc_deregister(struct miscdevice *misc)
      

      函数参数和返回值含义如下:
      misc:要注销的 MISC 设备。
      返回值:负数,失败;0,成功。

12、设备驱动框架选择

  • 原始驱动框架: 这是最基本的框架,你需要手动编写所有的初始化、清理、文件操作函数等。这可以帮助你深入理解字符设备驱动的工作原理,但需要更多的代码编写和调试工作。优点是操作简单直接,缺点是编写比较麻烦,需要的函数比较多。
  • Misc 设备框架: miscdevice 是一种简化的字符设备驱动框架,适用于那些不需要复杂文件操作的设备。它通过定义一个 struct miscdevice 结构体来简化驱动代码,适用于一些小型的设备。
  • Platform 驱动框架: 如果你的设备与特定的硬件平台相关,你可以使用 platform_driver 框架。这种框架允许你在设备树中描述设备,并将驱动与硬件平台关联起来。方便管理各种设备和驱动资源

13、Linux 自带的 Led 驱动

  • 内核开启 led 驱动

    image-20230728182104470

  • 配置设备树开启 Led 灯

    backlight:LED 灯作为背光。
    default-on:LED 灯打开。
    heartbeat:LED 灯作为心跳指示灯,可以作为系统运行提示灯。
    ide-disk:LED 灯作为硬盘活动指示灯。
    timer:LED 灯周期性闪烁,由定时器驱动,闪烁频率可以修改 。

    dtsleds {
        compatible = "gpio-leds";
        led0 {
            label = "red";
            pinctrl-names = "default";
            pinctrl-0 = <&pinctrl_gpioled>;
            gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
            linux,default-trigger = "heartbeat";
            default-state = "on";
        };
    };
    

14、INPUT子系统

  • 先申请一个 input_dev 结构体变量,使用 input_allocate_device 函数来申请一个 input_dev,此函数原型如下所示:

    struct input_dev *input_allocate_device(void)
    

    参数:无

    返回值:申请到的 input_dev

  • 如果要注销的 input 设备的话需要使用 input_free_device 函数来释放掉前面申请到的 input_dev,input_free_device 函数原型下:

    void input_free_device(struct input_dev *dev)
    

    dev:需要释放的 input_dev。

    返回值:无

  • 申请好一个 input_dev 以后就需要初始化这个 input_dev,需要初始化的内容主要为事件类 型 (evbit) 和事件值 (keybit) 这两种

    input_dev 初始化完成以后就需要向 Linux 内核注册 input_dev 了,需要用到 input_register_device 函数,此函数原型如下:

    int input_register_device(struct input_dev *dev)
    

    dev:要注册的 input_dev

    返回值:0,input_dev 注册成功;负值,input_dev 注册失败

  • 注销 input 驱动的时候也需要使用 input_unregister_device 函数来注销掉前面注册 的 input_dev,input_unregister_device 函数原型如下:

    void input_unregister_device(struct input_dev *dev)
    

    dev:要注销的 input_dev

    返回值:无

  • 上报输入事件

    input_event 函数,此函数用于上报指定的事件以及对应的值,函数原型如下:

    void input_event(struct input_dev *dev,  unsigned int type,  unsigned int code,  int value)
    

    函数参数和返回值含义如下:

    dev:需要上报的 input_dev。

    type:上报的事件类型,比如 EV_KEY。

    code:事件码,也就是我们注册的按键值,比如 KEY_0、KEY_1 等等。

    value:事件值,比如 1 表示按键按下,0 表示按键松开。

    返回值:无。

    input_event 函数可以上报所有的事件类型和事件值,Linux 内核也提供了其他的针对具体 事件的上报函数,这些函数其实都用到了 input_event 函数。

    比如上报按键所使用的 input_report_key 函数,此函数内容如下:

    static inline void input_report_key(struct input_dev *dev, unsigned int code, int value)
    { 
    	input_event(dev, EV_KEY, code, !!value); 
    } 
    

    input_report_key 函数的本质就是 input_event 函数,如果 要上报按键事件的话建议使用 input_report_key 函数。

    同样的还有一些其他的事件上报函数,这些函数如下所示:

    void input_report_rel(struct input_dev *dev, unsigned int code, int value)
    void input_report_abs(struct input_dev *dev, unsigned int code, int value)
    void input_report_ff_status(struct input_dev *dev, unsigned int code, int value)
    void input_report_switch(struct input_dev *dev, unsigned int code, int value)
    void input_mt_sync(struct input_dev *dev)
    

    当我们上报事件以后还需要使用 input_sync 函数来告诉 Linux 内核 input 子系统上报结束, input_sync 函数本质是上报一个同步事件,此函数原型如下所示:

    void input_sync(struct input_dev *dev) 
    

    函数参数和返回值含义如下:

    dev:需要上报同步事件的 input_dev。

    返回值:无。

    按键的上报事件的参考代码如下所示:

    /* 用于按键消抖的定时器服务函数 */
    void timer_function(unsigned long arg)
    {
        unsigned char value;
    
        value = gpio_get_value(keydesc->gpio); /* 读取 IO 值 */
        if(value == 0){ /* 按下按键 */
            /* 上报按键值 */
            input_report_key(inputdev, KEY_0, 1); /* 最后一个参数 1,按下 */
            input_sync(inputdev); /* 同步事件 */
        } else { 					/* 按键松开 */
            input_report_key(inputdev, KEY_0, 0); /* 最后一个参数 0,松开 */
            input_sync(inputdev); /* 同步事件 */
        }
    } 
    
  • 综上所述,input_dev 注册过程如下:

    • 使用 input_allocate_device 函数申请一个 input_dev。

    • 初始化 input_dev 的事件类型以及事件值。

      • 事件类型

        unsigned long evbit[BITS_TO_LONGS(EV_CNT)];        /* 事件类型的位图 */
        unsigned long keybit [BITS_TO_LONGS(KEY_CNT)];     /* 按键值的位图 */
        unsigned long relbit[BITS_TO_LONGS (REL_CNT)];     /* 相对坐标的位图 */
        unsigned long absbit [BITS_TO_LONGS (ABs_CNT)];    /* 绝对坐标的位图 */
        unsigned long mscbit [BITS_TO_LONGS (Msc_CNT)];    /* 杂项事件的位图* /
        unsigned long ledbit [BITS_TO_LONGS(LED_CNT)];     /* LED相关的位图 */
        unsigned long sndbit [BITS_TO_LONGS(SND_CNT)];     /* sound有关的位图 */
        unsigned long ffbit[BITS_TO_LONGS (FF_CNT)];       /* 压力反馈的位图 */
        unsigned long swbit [BITS_ TO_LONGS(Sw_CNT)];      /* 开关状态的位图 */
        
        #define EV_SYN          0x00                       /*同步事件*/
        #define EV KEY          0x01                       /*按键事件*/
        #define EV_REL          0x02                       /*相对坐标事件*/
        #define EV ABS          0x03                       /*绝对坐标事件*/
        #define EV MSC          0x04                       /*杂项(其他)事件*/
        #define EV_Sw           0x05                       /*开关事件*/
        #define EV_LED          0x11                       /*LED*/
        #define EV_SND          0x12                       /*sound(声音)*/
        #define EVREP           0x14                       /*重复事件*/
        #define EV FF           0x15                       /*压力事件*/
        #define EV PWR          0x16                       /*电源事件*/
        #define EV FF STATUS    0x17                       /*压力状态事件*/
        
        
      • 按键事件值

        Linux 内核定义了很多按键值,这些按键值定义在 include/uapi/linux/input.h 文件中,按键值如下:

        #define KEY_RESERVED         0
        #define KEY_ESC              1
        #define KEY_1                2
        #define KEY_2                3
        #define KEY_3                4
        #define KEY_4                5
        #define KEY_5                6
        #define KEY_6                7
        #define KEY_7                8
        #define KEY_8                9
        #define KEY_9                10
        #define KEY_0                11
        #define BTN_TRIGGER_HAPPY39  0x2e6
        
    • 使用 input_register_device 函数向 Linux 系统注册前面初始化好的 input_dev。

    • 卸载 input 驱动的时候需要先使用 input_unregister_device 函数注销掉注册的 input_dev, 然后使用 input_free_device 函数释放掉前面申请的 input_dev。

    • 使用 input_event 上报按键值

  • input_dev 注册过程示例代码如下 所示:

    struct input_dev *inputdev; /* input 结构体变量 */
    
    /* 驱动入口函数 */
    static int __init xxx_init(void)
    {
        ......
        inputdev = input_allocate_device(); /* 申请 input_dev */
        inputdev->name = "test_inputdev"; /* 设置 input_dev 名字 */
    
        /*********第一种设置事件和事件值的方法***********/
        __set_bit(EV_KEY, inputdev->evbit); /* 设置产生按键事件 */
        __set_bit(EV_REP, inputdev->evbit); /* 重复事件 */
        __set_bit(KEY_0, inputdev->keybit); /*设置产生哪些按键值 */
        /************************************************/
    
        /*********第二种设置事件和事件值的方法***********/
        keyinputdev.inputdev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REP);
        keyinputdev.inputdev->keybit[BIT_WORD(KEY_0)] |= BIT_MASK(KEY_0);
        /********************************************/
    
        /*********第三种设置事件和事件值的方法***********/
        keyinputdev.inputdev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REP);
        input_set_capability(keyinputdev.inputdev, EV_KEY, KEY_0);
        /********************************************/
    
        /* 注册 input_dev */
        input_register_device(inputdev);
        ......
        return 0;
    }
    
    /* 驱动出口函数 */
    static void __exit xxx_exit(void)
    {
        input_unregister_device(inputdev); /* 注销 input_dev */
        input_free_device(inputdev); /* 删除 input_dev */
    } 
    
  • 使用方式

    • 头文件

      #include <linux/input.h>
      
    • 定义

      struct input_dev *inputdev;     /*输入设备*/
      
    • 申请 input_dev

      /* 2、注册input_dev*/
      keyinputdev.inputdev = input_allocate_device();
      if(keyinputdev.inputdev ==NULL) {
          ret = -EINVAL;
          goto fail_keyinit;
      }
      
    • 初始化 input_dev 的事件类型以及事件值。

      keyinputdev.inputdev->name = KEYINPUT_NAME;
      set_bit(EV_KEY, keyinputdev.inputdev->evbit);    /*按键事件*/
      _set_bit(EV_REP, keyinputdev.inputdev->evbit);  /*重复事件 */
      set_bit(KEY_0, keyinputdev.inputdev->keybit);    /*按键值*/
      
    • 注册按键事件

          ret = input_register_device(keyinputdev.inputdev);
          if (ret) {
              goto fail_input_register;
          }
          return 0;
      fail_input_register:
      	input_free_device(keyinputdev.inputdev);
      
    • 注销按键事件

      /* 4、注销input_dev */
      input_unregister_device( keyinputdev.inputdev);
      input_free_device( keyinputdev.inputdeV);
      
    • 上报按键事件

      value = gpio_get_value(dev->irqkey[0].gpio);
      if(value == 0){   /*按下*/
          /*上报按键值*/
          input_event(dev->inputdev, EV_KEY, KEY_0, 1);  /*1代表按下*/
          input_sync(dev->inputdev);
      }else if(value == 1){  /*释放*/
          /*上报按键值*/
          input_event(dev->inputdev,EV_KEY,KEY_0, 0);  /*0代表按下*/
          input_sync(dev->inputdev);
      }
      
    • 测试方式

      image-20240524183734469

  • input_event 结构体

    Linux 内核使用 input_event 这个结构体来表示所有的输入事件,input_envent 结构体定义在 include/uapi/linux/input.h 文件中,结构体内容如下:

    struct input_event {
        struct timeval time;
        __u16 type;
        __u16 code;
        __s32 value;
    };
    

    input_event 结构体中的各个成员变量:

    time:时间,也就是此事件发生的时间,为 timeval 结构体类型,timeval 结构体定义如下:

    typedef long __kernel_long_t;
    typedef __kernel_long_t __kernel_time_t;
    typedef __kernel_long_t __kernel_suseconds_t;
    
    struct timeval {
        __kernel_time_t tv_sec; /* 秒 */
        __kernel_suseconds_t tv_usec; /* 微秒 */
    }; 
    

    type:事件类型,比如 EV_KEY,表示此次事件为按键事件,此成员变量为 16 位。

    code:事件码,比如在 EV_KEY 事件中 code 就表示具体的按键码,如:KEY_0、KEY_1 等等这些按键。此成员变量为 16 位。

    value:值,比如 EV_KEY 事件中 value 就是按键值,表示按键有没有被按下,如果为 1 的 话说明按键按下,如果为 0 的话说明按键没有被按下或者按键松开了。

    input_envent 这个结构体非常重要,因为所有的输入设备最终都是按照 input_event 结构体呈现给用户的,用户应用程序可以通过input_event 来获取到具体的输入事件或相关的值,比如 按键值等。

  • 具体数据分析

    image-20240524184202244

  • Linux自带的Key驱动

    -> Device Drivers
        -> Input device support
            -> Generic input layer (needed for keyboard, mouse,...) (INPUT [=y])
                -> Keyboards (INPUT_KEYBOARD[=y]
                    ->GPIO Buttons
    
  • 配置设备树开启按键

    gpio-keys {
        compatible = "gpio-keys";
        #address-cells = <1>;
        #size-cells = <0>;
        autorepeat;
        key0 {
            label = "GPIO Key Enter";
            linux,code = <KEY_ENTER>;
            gpios = <&gpio1 18 GPIO_ACTIVE_LOW>;
        };
    };
    

15、LCD RGB 屏幕驱动

13.1 修改 LCD 屏幕 IO 配置

这个其实 NXP 都已经写好了,不需要修改,打开 imx6ull-alientek-emmc.dts 文件,在 iomuxc 节点中找到如下内容:

pinctrl_lcdif_dat: lcdifdatgrp {  /* LCD屏幕数据接口 */
    fsl,pins = <
        MX6UL_PAD_LCD_DATA00__LCDIF_DATA00 0x79
        MX6UL_PAD_LCD_DATA01__LCDIF_DATA01 0x79
        MX6UL_PAD_LCD_DATA02__LCDIF_DATA02 0x79
        MX6UL_PAD_LCD_DATA03__LCDIF_DATA03 0x79
        MX6UL_PAD_LCD_DATA04__LCDIF_DATA04 0x79
        MX6UL_PAD_LCD_DATA05__LCDIF_DATA05 0x79
        MX6UL_PAD_LCD_DATA06__LCDIF_DATA06 0x79
        MX6UL_PAD_LCD_DATA07__LCDIF_DATA07 0x79
        MX6UL_PAD_LCD_DATA08__LCDIF_DATA08 0x79
        MX6UL_PAD_LCD_DATA09__LCDIF_DATA09 0x79
        MX6UL_PAD_LCD_DATA10__LCDIF_DATA10 0x79
        MX6UL_PAD_LCD_DATA11__LCDIF_DATA11 0x79
        MX6UL_PAD_LCD_DATA12__LCDIF_DATA12 0x79
        MX6UL_PAD_LCD_DATA13__LCDIF_DATA13 0x79
        MX6UL_PAD_LCD_DATA14__LCDIF_DATA14 0x79
        MX6UL_PAD_LCD_DATA15__LCDIF_DATA15 0x79
        MX6UL_PAD_LCD_DATA16__LCDIF_DATA16 0x79
        MX6UL_PAD_LCD_DATA17__LCDIF_DATA17 0x79
        MX6UL_PAD_LCD_DATA18__LCDIF_DATA18 0x79
        MX6UL_PAD_LCD_DATA19__LCDIF_DATA19 0x79
        MX6UL_PAD_LCD_DATA20__LCDIF_DATA20 0x79
        MX6UL_PAD_LCD_DATA21__LCDIF_DATA21 0x79
        MX6UL_PAD_LCD_DATA22__LCDIF_DATA22 0x79
        MX6UL_PAD_LCD_DATA23__LCDIF_DATA23 0x79
    >;
};
pinctrl_lcdif_ctrl: lcdifctrlgrp {     /* LCD屏幕控制接口 */
    fsl,pins = <
        MX6UL_PAD_LCD_CLK__LCDIF_CLK 0x79
        MX6UL_PAD_LCD_ENABLE__LCDIF_ENABLE 0x79
        MX6UL_PAD_LCD_HSYNC__LCDIF_HSYNC 0x79
        MX6UL_PAD_LCD_VSYNC__LCDIF_VSYNC 0x79
    >;
    pinctrl_pwm1: pwm1grp {           /* LCD屏幕PWM背光控制接口 */
    fsl,pins = <
        MX6UL_PAD_GPIO1_IO08__PWM1_OUT 0x110b0
    >;
};

13.2 LCD 屏幕参数节点信息修改

&lcdif {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_lcdif_dat         /* 使用到的 IO */
    &pinctrl_lcdif_ctrl
    &pinctrl_lcdif_reset>;
    display = <&display0>;
    status = "okay";

    display0: display {                     /* LCD 属性信息 */
        bits-per-pixel = <16>;              /* 一个像素占用几个 bit */
        bus-width = <24>;                   /* 总线宽度 */

        display-timings {
            native-mode = <&timing0>;         /* 时序信息 */
            timing0: timing0 {
                clock-frequency = <9200000>;  /* LCD 像素时钟,单位 Hz */
                hactive = <480>;              /* LCD X 轴像素个数 */
                vactive = <272>;              /* LCD Y 轴像素个数 */
                hfront-porch = <8>;           /* LCD hfp 参数 */
                hback-porch = <4>;            /* LCD hbp 参数 */
                hsync-len = <41>;             /* LCD hspw 参数 */
                vback-porch = <2>;            /* LCD vbp 参数 */
                vfront-porch = <4>;           /* LCD vfp 参数 */
                vsync-len = <10>;             /* LCD vspw 参数 */
                hsync-active = <0>;           /* hsync 数据线极性 */
                vsync-active = <0>;           /* vsync 数据线极性 */
                de-active = <1>;              /* de 数据线极性 */
                pixelclk-active = <0>;        /* clk 数据线先极性 */
            };
        };
    };
};

13.3 LCD 屏幕背光节点信息

13.4 使能 Linux logo 显示

-> Device Drivers
    -> Graphics support
        -> Bootup logo(LOGO[=y))
            -> Standard black and white Linux logo
            -> Standard 16-color Linux logo
            -> Standard 224-color Linux logo

image-20240601174537328

13.5 设置 LCD 作为终端控制台

打开开发板根文件系统中的 /etc/inittab 文件,在里面加入下面这一行:

tty1:askfirst:-/bin/sh

添加完成以后的 /etc/inittab 文件内容

image-20240601175338736

修改完成以后保存 /etc/inittab 并退出,然后重启开发板

16、RTC 驱动

17、IIC 驱动

  • 驱动 API 函数

    Linux 内核注册这个 i2c_driver ,此函数原型如下:

    int i2c_add_driver(struct i2c_driver *driver)
    

    函数参数和返回值含义如下:

    driver:要注册的 i2e_driver。

    返回值:0,成功;负值,失败。

    注销 I2C 设备驱动,此函数原型如下:

    void i2c_del_driver(struct i2c_driver *driver)
    

    函数参数和返回值含义如下:

    driver:要注销的 i2c_driver。

    返回值:无。

    使用 i2c 发送数据,i2c_transfer 函数原型如下:

    int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
    

    函数参数和返回值含义如下:

    adap:所使用的 I2C 适配器,i2c_client 会保存其对应的 i2c_adapter。

    msgs:I2C 要发送的一个或多个消息。

    num:消息数量,也就是 msgs 的数量。

    返回值:负值,失败,其他非负值,发送的 msgs 数量。

    msgs 这个参数,这是一个 i2c_msg 类型的指针参数, I2C 进行数据收发说白了就是消息的传递,在通过 i2c 进行收发数据时需要提前构建好 i2c_msg 的参数,使用 i2c_transfer 进行 I2C 数据收发

    struct i2c_msg {
        __u16 addr; /* 从机地址 */
        __u16 flags; /* 标志 */
        #define I2C_M_TEN 0x0010
        #define I2C_M_RD 0x0001
        #define I2C_M_STOP 0x8000
        #define I2C_M_NOSTART 0x4000
        #define I2C_M_REV_DIR_ADDR 0x2000
        #define I2C_M_IGNORE_NAK 0x1000
        #define I2C_M_NO_RD_ACK 0x0800
        #define I2C_M_RECV_LEN 0x0400
        __u16 len; /* 消息(本 msg)长度 */
        __u8 *buf; /* 消息数据 */
    };
    
  • 使用方式

    • 头文件

      #include <linux/ i2c.h>
      
    • 修改设备树

      pinctrl_i2c1: i2c1grp {
          fsl,pins = <
              MX6UL_PAD_UART4_TX_DATA__I2C1_SCL 0x4001b8b0
              MX6UL_PAD_UART4_RX_DATA__I2C1_SDA 0x4001b8b0
          >;
      };
      
      &i2c1 {
          clock-frequency = <100000>;
          pinctrl-names = "default";
          pinctrl-0 =<&pinctrl_i2c1>;
          status = "okay" ;
          ap3216c@1e {
              compatible = "alientek,ap3216c";
              reg = <0x1e>;
          };
      };
      

      /sys/bus/i2c/devices 目录下存放着所有 I2C 设备,如果设备树修改正确的话,会在 /sys/bus/i2c/devices 目录下看到一个名为 “0-001e” 的子目录

    • 驱动框架

      /* 设备结构体 */
      struct xxx_dev {
          ......
          void *private_data; /* 私有数据,一般会设置为 i2c_client */
      };
      
      /*
      * @description : 读取 I2C 设备多个寄存器数据
      * @param – dev : I2C 设备
      * @param – reg : 要读取的寄存器首地址
      * @param – val : 读取到的数据
      * @param – len : 要读取的数据长度
      * @return : 操作结果
      */
      static int xxx_read_regs(struct xxx_dev *dev, u8 reg, void *val, int len)
      {
          int ret;
          struct i2c_msg msg[2];
          struct i2c_client *client = (struct i2c_client *)dev->private_data;
      
          /* msg[0],第一条写消息,发送要读取的寄存器首地址 */
          msg[0].addr = client->addr; /* I2C 器件地址 */
          msg[0].flags = 0; /* 标记为发送数据 */
          msg[0].buf = ® /* 读取的首地址 */
          msg[0].len = 1; /* reg 长度 */
      
          /* msg[1],第二条读消息,读取寄存器数据 */
          msg[1].addr = client->addr; /* I2C 器件地址 */
          msg[1].flags = I2C_M_RD; /* 标记为读取数据 */
          msg[1].buf = val; /* 读取数据缓冲区 */
          msg[1].len = len; /* 要读取的数据长度 */
      
          ret = i2c_transfer(client->adapter, msg, 2);
          if(ret == 2) {
              ret = 0;
          } else {
              ret = -EREMOTEIO;
          }
          return ret;
      }
      
      /*
      * @description : 向 I2C 设备多个寄存器写入数据
      * @param – dev : 要写入的设备结构体
      * @param – reg : 要写入的寄存器首地址
      * @param – buf : 要写入的数据缓冲区
      * @param – len : 要写入的数据长度
      * @return : 操作结果
      */
      static s32 xxx_write_regs(struct xxx_dev *dev, u8 reg, u8 *buf, u8 len)
      {
          u8 b[256];
          struct i2c_msg msg;
          struct i2c_client *client = (struct i2c_client *)dev->private_data;
      
          b[0] = reg; /* 寄存器首地址 */
          memcpy(&b[1],buf,len); /* 将要发送的数据拷贝到数组 b 里面 */
          msg.addr = client->addr; /* I2C 器件地址 */
          msg.flags = 0; /* 标记为写数据 */
          msg.buf = b; /* 要发送的数据缓冲区 */
          msg.len = len + 1; /* 要发送的数据长度 */
      
          return i2c_transfer(client->adapter, &msg, 1);
      }
      
      static int ap3216c_probe(struct i2c_client *client, const struct i2c_device_id *id)
      {
          return 0;
      }
      static int ap3216c_remove(struct i2c_client *client)
      {
          return 0;
      }
      
      /*设备树匹配表*/
      static struct of_device_id ap3216c_of_match[] = {
          {.compatible = "alientek,ap3216c"},
          {},
      };
      
      /* i2c_driver */
      static struct i2c_driver ap3216c_driver = {
          .probe = ap3216c_probe,
          .remove = ap3216c_remove,
          .driver = {
              .name = "ap3216c",
              .owner = THIS_MODULE,
              .of_match_table = of_match_ptr(ap3216c_of_match),
          },
      };
      
      /*驱动入口函数*/
      static int ___init ap3216c_init(void)
      {
          int ret = 0;
          ret = i2c_add_driver(&ap3216c_driver);
          return ret;
      }
      
      /*驱动出口函数*/
      static void _exit ap3216c_exit(void)
      {
          i2c_del_driver(&ap3216c_driver);
      }
      
    • 查看 I2C 设备

      /sys/bus/i2c/devices 目录下存放着所有 I2C 设备,如果设备树修改正确的话,会在 /sys/bus/i2c/devices 目录下看到一个名为 “0-001e” 的子目录

      image-20240601180516919

18、SPI 驱动

16.1 驱动 api 函数

  • spi 设备注册

    /* 传统匹配方式 ID 列表 */
    static const struct spi_device_id icm20608_id[] = {
        {"alientek,icm20608", 0},
        {}
    };
    
    /* 设备树匹配列表 */
    static const struct of_device_id icm20608_of_match[] = {
        { .compatible = "alientek,icm20608" },
        { /* Sentinel */ }
    };
    
    /* SPI 驱动结构体 */
    static struct spi_driver icm20608_driver = {
            .probe = icm20608_probe,
            .remove = icm20608_remove,
            .driver = {
                .owner = THIS_MODULE,
                .name = "icm20608",
                .of_match_table = icm20608_of_match,
        },
        .id_table = icm20608_id,
    }; 
    

    spi_driver 初始化完成以后需要向 Linux 内核注册, spi_driver 注册函数为spi_register_driver,函数原型如下:

    int spi_register_driver(struct spi_driver *sdrv)
    

    函数参数和返回值含义如下:

    sdrv:要注册的 spi_driver。

    返回值:0,注册成功;赋值,注册失败。

  • spi 设备注销

    注销 SPI 设备驱动以后也需要注销掉前面注册的 spi_driver,使用 spi_unregister_driver 函数完成 spi_driver 的注销,函数原型如下:

    void spi_unregister_driver(struct spi_driver *sdrv)
    

    函数参数和返回值含义如下:

    sdrv*:要注销的 spi_driver。

    返回值:无。

  • spi_message 初始化

    struct spi_message {
        struct list_head transfers;
        struct spi_device *spi;
        unsigned is_dma_mapped:1;
        ......
        /* completion is reported through a callback */
        void (*complete)(void *context);
        void *context;
        unsigned frame_length;
        unsigned actual_length;
        int status;
    
        /* for optional use by whatever driver currently owns the
        * spi_message ... between calls to spi_async and then later
        * complete(), that's the spi_master controller driver.
        */
        struct list_head queue;
        void *state;
    };
    

    在使用 spi_message 之前需要对其进行初始化, spi_message 初始化函数为 spi_message_init,函数原型如下:

    void spi_message_init(struct spi_message *m)
    

    函数参数和返回值含义如下:

    m:要初始化的 spi_message。

    返回值:无。

  • spi_transfer 添加到 spi_message 队列

    struct spi_transfer {
        /* it's ok if tx_buf == rx_buf (right?)
        * for MicroWire, one buffer must be null
        * buffers must work with dma_*map_single() calls, unless
        * spi_message.is_dma_mapped reports a pre-existing mapping
        */
        const void *tx_buf;
        void *rx_buf;
        unsigned len;
    
        dma_addr_t tx_dma;
        dma_addr_t rx_dma;
        struct sg_table tx_sg;
        struct sg_table rx_sg;
    
        unsigned cs_change:1;
        unsigned tx_nbits:3;
        unsigned rx_nbits:3;
        #define SPI_NBITS_SINGLE 0x01 /* 1bit transfer */
        #define SPI_NBITS_DUAL 0x02 /* 2bits transfer */
        #define SPI_NBITS_QUAD 0x04 /* 4bits transfer */
        u8 bits_per_word;
        u16 delay_usecs;
        u32 speed_hz;
    
        struct list_head transfer_list;
    };
    

    spi_message 初始化完成以后需要将 spi_transfer 添加到 spi_message 队列中,这里我们要用到 spi_message_add_tail 函数,此函数原型如下:

    void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m)
    

    函数参数和返回值含义如下:

    t:要添加到队列中的 spi_transfer。

    m:spi_transfer 要加入的 spi_message。

    返回值:无。

  • spi 同步数据传输

    spi_message 准备好以后既可以进行数据传输了,数据传输分为同步传输和异步传输,同步传输会阻塞的等待 SPI 数据传输完成,同步传输函数为 spi_sync,函数原型如下:

    int spi_sync(struct spi_device *spi, struct spi_message *message)
    

    函数参数和返回值含义如下:

    spi:要进行数据传输的 spi_device。

    message:要传输的 spi_message。

    返回值

  • spi 异步数据传输

    异步传输不会阻塞的等到 SPI 数据传输完成,异步传输需要设置 spi_message 中的 complete 成员变量, complete 是一个回调函数,当 SPI 异步传输完成以后此函数就会被调用。 SPI 异步传输函数为 spi_async,函数原型如下:

    int spi_async(struct spi_device *spi, struct spi_message *message)
    

    函数参数和返回值含义如下:

    spi:要进行数据传输的 spi_device。

    message:要传输的 spi_message。

    返回值

  • spi 同步传输读数据函数

    spi_read 是对上面的传输方式的一个封装,是一种同步的传输方式,函数原型如下:

    static int spi_read(struct spi_device *spi, void *buf, size_t len)
    

    函数参数和返回值含义如下:

    spi:要进行数据传输的 spi_device。

    buf:要传输的数据 buf。

    返回值

  • spi 同步传输写数据函数

    spi_read 是对上面的传输方式的一个封装,是一种同步的传输方式,函数原型如下:

    static int spi_read(struct spi_device *spi, const void *buf, size_t len)
    

    函数参数和返回值含义如下:

    spi:要进行数据传输的 spi_device。

    buf:要传输的数据 buf。

    返回值

  • spi 同步传输读写数据函数

    能自动在一个片选信号下完成读写操作,函数原型如下:

    int spi_write_then_read(struct spi_device *spi, const void *txbuf, unsigned n_tx, const void *rxbuf, unsigned n_rx)
    

    函数参数和返回值含义如下:

    spi:要进行数据传输的 spi_device。

    txbuf:要发送的数据。

    n_tx:要发送的数据长度。

    rxbuf:要接收的数据。

    n_rx:要接收的数据长度。

    返回值

16.2 使用方式

  • 头文件

  • 设备树修改

    • IO 的 pinctrl 子节点创建与修改

      不能把片选引脚配置为 spi 硬件片选引脚,需要配置为不同的 io 引脚

      pinctrl_ecspi3: ecspi3grp{
          fsl, pins = <
              MX6UL_PAD_UART2_TX_DATA_GPIO1I020     0x10b0
              MX6UL_PAD_UART2_RX_DATA_ECSPI3_SCLK   0x10b1
              MX6UL_PAD_UART2_CTS_B_ECSPI3_MOS1     0x10b1
              MX6UL_PAD_UART2_RTS_B_ECSPI3_MISO     0x10b1
          >;
      };
      
    • SPI 设备节点的创建与修改

      &ecspi3 {
          fsl,spi-num-chipselects = <1>;           /*1个片选*/
          cs-gpio = <&gpio1 20 GPIO_ACTIVE_LOW>;   /*片选引脚,软件片选!*/
          pinctrl-names = "default";
          pinctrl-0 =<&pinctrl_ecspi3>;
          status = "okay";
      
          /*对应的SPI芯片子节点*/
          spidev0: imc20608@0 {                 /* @后面的0表示次SPI芯片接到哪个硬件片选上*/
              reg = <0>;
              compatible = "alientek,icm20608";
              spi-max-frequency = <8000000>;    /* SPI时钟频率8MHz*/
          };
      };
      
    • 查看设备挂载

      image-20240601191600184

  • 编写设备驱动

    • spi 初始化设置

      /*获取片选引脚*/
      icm20608dev.nd = of get parent(spi->dev.of_node);   /* 通过子节点查找父节点 */
      icm20608dev.cs_gpi0 = of_get_named_gpio(icm20608dev,nd,"cs-gpio",0);
      if(icm20608dev.cs_gpio < 0){
          printk ( "can't get cs-gpio\r\n ");
          goto fail_gpio;
      }
      ret = gpio_request(icm20608dev.cs_gpio, "cS");
      if(ret< 0)
      	printk("cs_gpio request failed!\r\n" );
      ret = gpic_direction_output(icm2060Bdev.cs_gpio,1);/*默认高电平“/
      
      /*初始化spi_device*/
      spi->mode = SPI_MODE_0;    /* 设置spi的模式 在内核中spi.h文件的spi_device结构体中查看用法 */
      spi_setup(spi);            /* 设置模式 */
      
    • spi 读写寄存器

      • 第一种方式

        /*
        * @description : 从 icm20608 读取多个寄存器数据
        * @param – dev : icm20608 设备
        * @param – reg : 要读取的寄存器首地址
        * @param – val : 读取到的数据
        * @param – len : 要读取的数据长度
        * @return : 操作结果
        */
        static int icm20608_read_regs(struct icm20608_dev *dev, u8 reg, void *buf, int len)
        {
            int ret = -1;
            unsigned char txdata[1];
            unsigned char * rxdata;
            struct spi_message m;
            struct spi_transfer *t;
            struct spi_device *spi = (struct spi_device *)dev->private_data;
        
            t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);
            if(!t) {
                return -ENOMEM;
            }
        
            rxdata = kzalloc(sizeof(char) * len, GFP_KERNEL); /* 申请内存 */
            if(!rxdata) {
                goto out1;
            }
        
            /* 一共发送 len+1 个字节的数据,第一个字节为寄存器首地址,一共要读取 len 个字节长度的数据, */
            txdata[0] = reg | 0x80;       /* 写数据的时候首寄存器地址 bit8 要置 1 */
            t->tx_buf = txdata;           /* 要发送的数据 */
            t->rx_buf = rxdata;           /* 要读取的数据 */
            t->len = len+1;               /* t->len=发送的长度+读取的长度 */
            spi_message_init(&m);         /* 初始化 spi_message */
            spi_message_add_tail(t, &m);
            ret = spi_sync(spi, &m);      /* 同步发送 */
            if(ret) {
                goto out2;
            }
            memcpy(buf , rxdata+1, len); /* 只需要读取的数据 */
        
        out2:
            kfree(rxdata);               /* 释放内存 */
        out1:
            kfree(t);                    /* 释放内存 */
            return ret;
        }
        
        /*
        * @description : 向 icm20608 多个寄存器写入数据
        * @param – dev : icm20608 设备
        * @param – reg : 要写入的寄存器首地址
        * @param – val : 要写入的数据缓冲区
        * @param – len : 要写入的数据长度
        * @return : 操作结果
        */
        static s32 icm20608_write_regs(struct icm20608_dev *dev, u8 reg, void *buf, u8 len)
        {
            int ret = -1;
            unsigned char *txdata;
            struct spi_message m;
            struct spi_transfer *t;
            struct spi_device *spi = (struct spi_device *)dev->private_data;
        
            t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);
            if(!t) {
                return -ENOMEM;
            }
        
            txdata = kzalloc(sizeof(char)+len, GFP_KERNEL);
            if(!txdata) {
                goto out1;
            }
        
            /* 一共发送 len+1 个字节的数据,第一个字节为寄存器首地址, len 为要写入的寄存器的集合, */
            *txdata = reg & ~0x80;           /* 写数据的时候首寄存器地址 bit8 要清零 */
            memcpy(txdata+1, buf, len);      /* 把 len 个寄存器拷贝到 txdata 里 */
            t->tx_buf = txdata;              /* 要发送的数据 */
            t->len = len+1;                  /* t->len=发送的长度+读取的长度 */
            spi_message_init(&m);            /* 初始化 spi_message */
            spi_message_add_tail(t, &m);
            ret = spi_sync(spi, &m);         /* 同步发送 */
            if(ret) {
                goto out2;
            }
        
        out2:
            kfree(txdata);                   /* 释放内存 */
        out1:
            kfree(t);                        /* 释放内存 */
            return ret;
        }
        
        /*
        * @description : 读取 icm20608 指定寄存器值,读取一个寄存器
        * @param – dev : icm20608 设备
        * @param – reg : 要读取的寄存器
        * @return : 读取到的寄存器值
        */
        static unsigned char icm20608_read_onereg(struct icm20608_dev *dev, u8 reg)
        {
            u8 data = 0;
            icm20608_read_regs(dev, reg, &data, 1);
            return data;
        }
        
        /*
        * @description : 向 icm20608 指定寄存器写入指定的值,写一个寄存器
        * @param – dev : icm20608 设备
        * @param – reg : 要写的寄存器
        * @param – data : 要写入的值
        * @return : 无
        */
        static void icm20608_write_onereg(struct icm20608_dev *dev, u8 reg, u8 value)
        {
            u8 buf = value;
            icm20608_write_regs(dev, reg, &buf, 1);
        }
        
      • 第二种方式(软件片选信号使能)

        /* SPI写寄存器*/
        static int icm20608_write_regs(struct icm20608_dev *dev, u8 reg, u8 *buf, int len)
        {
            u8 data = 0;
            struct spi_device *spi = (struct spi_device *)dev->private_data;
        
            gpio_set_value(dev->cs_gpio, 0);    /*片选拉低*/
            data = reg & ~0x80;
            spi_write(spi,&data, 1);           /*发送要写的寄存器地址*/
            spi_write(spi, buf, len);           /*发送要写的寄存器地址*/
            gpio_set_value(dev->cs_gpio, 1);    /*片选拉高*/
        }
        
        /* SPI读寄存器*/
        static int icm20608_read_regs(struct icm20608_dev *dev, u8 reg, void *buf, int len)
        {
            u8 data = 0;
            struct spi_device *spi = (struct spi_device *)dev->private_data;
        
            gpio_set_value(dev->cs_gpio, 0);    /*片选拉低*/
            data = reg | 0x80;
            spi_write(spi, &data, 1);           /*发送要读取的寄存器地址*/
            spi_read(spi, buf, len);            /*读取数据*/
            gpio_set_value(dev->cs_gpio, 1);    /*片选拉高*/
        }
        
      • 第三种方式(硬件片选信号使能,本质也是软件,使用设备树中配置的片选引脚自动使能)

        /* SPI写寄存器*/
        static int icm20608_write_regs(struct icm20608_dev *dev, u8 reg, u8 *buf, int len)
        {
            u8 data = 0;
            u8 *txdata;
            struct spi_device *spi = (struct spi_device *)dev->private_data;
        	txdata = kzalloc(len + 1, GFP_KERNEL);
        
            txdata[0] = reg & ~0x80;
            memcpy(&txdata[1], buf, len);
            spi_write(spi,txdata, len + 1);           /*发送要写的寄存器地址*/
            kfree(txdata);
        }
        
        /* SPI读寄存器*/
        static int icm20608_read_regs(struct icm20608_dev *dev, u8 reg, void *buf, int len)
        {
            u8 data = 0;
            struct spi_device *spi = (struct spi_device *)dev->private_data;
        
            data = reg | 0x80;
            spi_write_then_read(spi, &data, 1, buf, len);
        }
        

19、串口驱动

修改设备树,添加 pinctrl 信息

pinctrl_uart3: uart3grp {
    fsl,pins = <
        MX6UL_PAD_UART3_TX_DATA_UART3_DCE_TX     0x1b0b1
        MX6UL_PAD_UART3_RX_DATA__ _UART3_DCE_RX  0x1b0b1
    >;
};

添加节点信息

&uart3 {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_uart3>;
    status = "okay";
};

20、多点触摸屏驱动

21、WiFi 驱动

19.1 删除 Linux 内核自带的 RTL8192CU 驱动,打开 drivers/net/wireless/rtlwifi/Kconfig,找到下面所示内容然后删除掉:

config RTL8192CU
    tristate "Realtek RTL8192CU/RTL8188CU USB Wireless Network Adapter"
    depends on USB
    select RTLWIFI
    select RTLWIFI_USB
    select RTL8192C_COMMON
    ---help---
    This is the driver for Realtek RTL8192CU/RTL8188CU 802.11n USB
    wireless network adapters.

    If you choose to build it as a module, it will be called rtl8192cu

继续打开 drivers/net/wireless/rtlwifi/Makefile, 找到下面这样然后删除掉:

obj-$(CONFIG_RTL8192CU) += rtl8192cu/

至此,linux 内核自带的 RTL8192CU/8188CU 驱动就屏蔽掉了。

19.2 将 rtl81xx 驱动添加到 Linux 内核中

将 realtek 整个目录拷贝到 ubuntu 下 Linux 内核源码中的 drivers/net/wireless 目录下,此目录下存放着所有 WIFI 驱动文件。

image-20240602170813925

19.3 修改 drivers/net/wireless/Kconfig

打开 drivers/net/wireless/Kconfig,在里面加入下面这一行内容:

source "drivers/net/wireless/realtek/Kconfig"

添加完以后的 Kconfig 文件内容如下所示:

#
# Wireless LAN device configuration
#

menuconfig WLAN
......
source "drivers/net/wireless/rsi/Kconfig"
source "drivers/net/wireless/realtek/Kconfig"    //添加这个

endif # WLAN

这样 WIFI 驱动的配置界面才会出现在 Linux 内核配置界面上。

19.4 修改 drivers/net/wireless/Makefile

打开 drivers/net/wireless/Makefile,在里面加入下面一行内容:

obj-y += realtek/

修改完以后的 Makefile 文件内容如下所示:

#
# Makefile for the Linux Wireless network device drivers.
#

obj-$(CONFIG_IPW2100) += ipw2x00/
......
obj-$(CONFIG_CW1200) += cw1200/
obj-$(CONFIG_RSI_91X) += rsi/

obj-y += realtek/     //添加这个

19.5 配置 Linux 内核

  • 配置 USB 支持设备

    -> Device Drivers
        -> <*> USB support
            -> <*> Support for Host-side USB
                -> <*> EHCI HCD (USB 2.0) support
                -> <*> OHCI HCD (USB 1.1) support
                -> <*> ChipIdea Highspeed Dual Role Controller
                    -> [*] ChipIdea device controller
                    -> [*] ChipIdea host controller
    
  • 配置支持 WIFI 设备

    -> Device Drivers
        -> [*] Network device support
            -> [*] Wireless LAN
                -> <*> IEEE 802.11 for Host AP (Prism2/2.5/3 and WEP/TKIP/CCMP)
                    -> [*] Support downloading firmware images with Host AP driver
                    -> [*] Support for non-volatile firmware download
    
  • 配置支持 IEEE 802.11

    -> Networking support
        -> -*- Wireless
            -> [*] cfg80211 wireless extensions compatibility
            -> <*> Generic IEEE 802.11 Networking Stack (mac80211)
    
  • 配置添加编译 WIFI 驱动

    -> Device Drivers
        -> Network device support (NETDEVICES [=y])
            -> Wireless LAN (WLAN [=y])
                -> Realtek wifi (REALTEK_WIFI [=m])
                    -> rtl8189ftv sdio wifi
                    -> rtl8188eus usb wifi
                    -> Realtek 8192C USB WiFi
    

    选中 “rtl8189fs/ftv sdio wifi”、“rtl8188eus usb wifi” 和 “Realtek 8192C USB WiFi”,将其编译为模块。

  • 拷贝到开发板测试

    需要的 RTL8188EUS、RTL8189FS 和 RTL8188CUS/8192CU 的 驱 动 模 块 文 件 , 将 这 三 个 文 件 拷 贝 到 rootfs/lib/modules/4.1.15 目录中,执行加载驱动模块命令测试

    depmod               //第一次加载驱动的时候需要运行此命令
    modprobe 8188eu.ko   //RTL8188EUS 模块加载 8188eu.ko 模块
    

    如果加载成功后的话

    image-20240602171751173

    输入ifconfig -a 命令查看。一般是 wlan0 网卡是否存在

19.6 wireless tools 工具移植与测试

用于扫描当前环境下的所有 WiFi 热点信息

  • wireless tools 功能

    wireless tools 是操作 WIFI 的工具集合,包括一下工具:

    ① iwconfig:设置无线网络相关参数。

    ② iwlist:扫描当前无线网络信息,获取 WIFI 热点。

    ③ iwspy:获取每个节点链接的质量。

    ④ iwpriv:操作 WirelessExtensions 特定驱动。

    ⑤ ifrename:基于各种静态标准命名接口。

  • wireless tools 移植

    将 iwlist_for_visteon-master.tar.bz2 拷贝到 Ubuntu 中前面创建的 tool 目录下,拷贝完成以后将其解压,生成 iwlist_for_visteon-master 文件夹。进入到 iwlist_for_visteon-master 文件夹里面,打开 Makefile 文件,修改 Makefile 中的 CC、 AR 和 RANLIB 这三个变量

    image-20240602172244239

    修改完成以后就可以使用如下命令编译:

    make clean  //先清理一下工程
    make        //编译
    

    编译完成以后就会在当前目录下生成 iwlist、 iwconfig、 iwspy、 iwpriv、 ifrename 这 5 个工具,另外还有很重要的 libiw.so.29 这个库文件。将这 5 个工具拷贝到开发板根文件系统下的/usr/bin 目录中,将 libiw.so.29 这个库文件拷贝到开发板根文件系统下的 /usr/lib 目录中,命令如下:

    sudo cp iwlist iwconfig iwspy iwpriv ifrename /home/linux/nfs/rootfs/usr/bin/ -f
    sudo cp libiw.so.29 /home/linux/nfs/rootfs/usr/lib/ -f
    

    拷贝完成以后可以测试 iwlist 是否工作正常,前提是如需要开启加载并打开 wlan0 网卡,使用如下命令

    modprobe 8188eu.ko   //加载 RTL8188 驱动模块
    ifconfig wlan0 up    //打开 wlan0 网卡
    

    使用如下命令扫描当前环境下的 WiFi 热点

    iwlist wlan0 scan 
    

19.7 wpa_supplicant 移植

用于连接WiFi热点配置WiFi热点信息,wpa_supplicant 依赖于 openssl 和 libnl

19.7.1 openssl 移植

将 openssl-1.1.1d.tar.gz 源码压缩包拷贝到 Ubuntu 中前面创建的 tool 目录下,然后使用如下命令将其解压:

tar -vxzf openssl-1.1.1d.tar.gz

解压完成以后就会生成一个名为 openssl-1.1.1d 的目录,然后在新建一个名为 “openssl” 的文件夹,用于存放 openssl 的编译结果。进入到解压出来的 openssl-1.1.1d 目录中,然后执行如下命令进行配置:

./Configure linux-armv4 shared no-asm --prefix=/home/linux/tool/openssl CROSS_COMPILE=arm-linux-gnueabihf-

上述配置中 “ linux-armv4” 表示 32 位 ARM 凭条,并没有 “ linux-armv7” 这个选项。CROSS_COMPILE 用于指定交叉编译器。配置成功以后会生成 Makefile,输入如下命令进行编译:

make
make install

编译安装完成以后的 openssl 目录内容所示:

image-20240602173118365

将 lib 目录下的 libcrypto 和 libssl 库拷贝到开发板根文件系统中的 /usr/lib 目录下,命令如下:

sudo cp libcrypto.so* /home/linux/nfs/rootfs/lib/ -af
sudo cp libssl.so* /home/linux/nfs/rootfs/lib/ -af

19.7.2 libnl 库移植

在编译 libnl 之前先安装 biosn 和 flex,命令如下:

sudo apt-get install bison
sudo apt-get install flex

将 libnl-3.2.23.tar.gz 源码压缩包拷贝到 Ubuntu 中前面创建的 tool 目录下,然后使用如下命令将其解压:

tar -vxzf libnl-3.2.23.tar.gz

得到解压完成以后会得到 libnl-3.2.23 文件夹,然后在新建一个名为“libnl”的文件夹,用于存放 libnl 的编译结果。进入到 libnl-3.2.23 文件夹中,然后执行如下命令进行配置:

./configure --host=arm-linux-gnueabihf --prefix=/home/zuozhongkai/linux/IMX6ULL/tool/libnl/

--host 用于指定交叉编译器的前缀,这里设置为 “arm-linux-gnueabihf”, --prefix 用于指定编译结果存放目录,这里肯定要设置为我们刚刚创建的 libnl 文件夹。配置完成以后就可以执行如下命令对 libnl 库进行编译、安装:

make -j12      //编译
make install   //安装

编译安装完成以后的 libnl 目录如图所示:

image-20240602173411260

将 lib 目录下的所有文件拷贝到开发板根文件系统的 /usr/lib 目录下,命令如下所示:

sudo cp lib/* /home/zuozhongkai/linux/nfs/rootfs/usr/lib/ -rf  

19.7.3 wpa_supplicant 移植

将 wpa_supplicant-2.7.tar.gz 拷贝到 Ubuntu 中,输入如下命令进行解压:

tar -vxzf wpa_supplicant-2.7.tar.gz

解压完成以后会得到 wpa_supplicant-2.7 文件夹,进入到此文件夹中

进入到 wpa_supplicant 目录下,然后进行配置, wpa_supplicant 的配置比较特殊,需要将 wpa_supplicant 下的 defconfig 文件拷贝一份并重命名为 .config,命令如下:

cd wpa_supplicant/
cp defconfig .config

完成以后打开 .config 文件,在里面指定交叉编译器、openssl、libnl 库和头文件路径,设置如下:
.config 文件需要添加的内容

CC = arm-linux-gnueabihf-gcc

#openssl 库和头文件路径*
4 CFLAGS += -I/home/linux/tool/openssl/include
5 LIBS += -L/home/linux/tool/openssl/lib -lssl -lcrypto

#libnl 库和头文件路径
CFLAGS += -I/home/linux/tool/libnl/include/libnl3
LIBS += -L/home/linux/tool/libnl/lib

CC 变量用于指定交叉编译器,这里就是 arm-linux-gnueabihf-gcc,CFLAGS 指定需要使用的库头文件路径,LIBS 指定需要用到的库路径。编译 wap_supplicant 的时候需要用到 openssl 和libnl 库。

.config 文件配置好以后就可以编译 wpa_supplicant 了,使用如下命令编译:

export PKG_CONFIG_PATH=/home/linux/tool/libnl/lib/pkgconfig:$PKG_CONFIG_PATH   //指定 libnl 库 pkgconfig 包位置
make -j12    //编译

首先我们使用 export 指定了 libnl 库的 pkgconfig 路径,环境变量 PKG_CONFIG_PATH 保存着 pkgconfig 包路径,在 tool/libnl/lib/ 下有个名为 “pkgconfig” 的目录。

编译完成之后,将 wpa_cli 和 wpa_supplicant 这两个文件拷贝到开发板根文件系统的 /usr/bin 目录中,命令如下:

sudo cp wpa_cli wpa_supplicant /home/linux/nfs/rootfs/usr/bin/ -f

拷贝完成以后重启开发板!输入 “wpa_supplicant -v” 命令查看一下 wpa_supplicant 版本号,如果 wpa_supplicant 工作正常的话就会打印出版本号。

19.8 WiFi 联网测试

要连接的 WIFI 热点扫描到以后就可以连接了,先在开发板根文件系统的 /etc 目录下创建一个名为 “wpa_supplicant.conf” 的配置文件,此文件用于配置要连接的 WIFI 热点以及 WIFI 秘密,比如我要连接到 “ZZK” 这个热点上,因此 wpa_supplicant.conf 文件内容如下所示:

ctrl_interface=/var/run/wpa_supplicant
ap_scan=1
network={
 ssid="ZZK"
 psk="xxxxxxxx"
}

注意, wpa_supplicant.conf 文件对于格式要求比较严格,“=” 前后一定不能有空格,也不要用 TAB 键来缩进,比如第 4 行和 5 行的缩进应该采用空格,否则的话会出现 wpa_supplicant.conf 文件解析错误!最重要的一点! wpa_supplicant.conf 文件内容要自己手动输入,不要偷懒复制粘贴!!!
wpa_supplicant.conf 文 件 编 写 好 以 后 再 在 开 发 板 根 文 件 系 统 下 创 建 一 个 “/var/run/wpa_supplicant” 目录, wpa_supplicant 工具要用到此目录!命令如下:

mkdir /var/run/wpa_supplicant -p

一切准备好以后就可以使用 wpa_supplicant 工具让 RTL8188 USB WIFI 连接到热点上,输入如下命令:

wpa_supplicant -D wext -c /etc/wpa_supplicant.conf -i wlan0 &     //USB WiFi RTL8188eus
wpa_supplicant -Dnl80211 -c /etc/wpa_supplicant.conf -i wlan0 &   //RTL8189 SDIO WIFI

当 RTL8188 连接上 WIFI 热点以后会输出如图所示的信息:

image-20240602174302971

当 RTL8188 连接到 WIFI 热点上以后会输出 “wlan0: CTRL-EVENTCONNECTED” 字样。接下来就是最后一步了,设置 wlan0 的 IP 地址,这里使用 udhcpc 命令
从路由器申请 IP 地址,输入如下命令:

udhcpc -i wlan0 //从路由器获取 IP 地址

IP 地址获取成功以后会输出所示信息:

image-20240602174352204

22、PWM驱动

23、Regmap API 驱动

24、IIO驱动

0

评论区