编辑
2024-03-26
学习记录
0
请注意,本文编写于 442 天前,最后修改于 418 天前,其中某些信息可能已经过时。

目录

imx6ull应用开发基础
1.shell脚本
2.GCC
3.Makefile文件及编写规则
4.文件IO
5.Framebuffer
6.各种编码方式
7.读取输入设备信息
8.网络编程(类似文件传输有源\目的\长度)
9.多线程编程
10.TTY体系中设备节点
11.串口应用程序编程
12.IIC应用编程
13.IIC-Tools访问iic设备:
imx6ull驱动开发基础
1.怎么写一个驱动程序
2.普适GPIO引脚操作方法
2.1 AM335x和AM437x的GPIO操作方法
2.2 RK3288和RK3399的GPIO操作方法
2.3 imx6ull的GPIO操作方法
2.4 STM32MP157的GPIO操作方法
3.最简单的LED驱动程序
4.LED驱动程序框架
5.驱动设计的思想
6.驱动设计的进化,总线设备驱动模型
7.使用总线设备驱动模型改造LED模板驱动程序
8.驱动进化:设备树的引入
9.使用设备树改造LED模板驱动程序
10.按键驱动程序开发
11.Pinctrl子系统
12.GPIO子系统
12.Linux 系统对中断的处理
13.编写使用中断的按键中断程序
14.驱动程序的基石
1.休眠与唤醒
2.POLL机制
3.异步通知
4.阻塞与非阻塞
5.定时器(属于软件中断)
6.中断下半部tasklet
7.工作队列
8.中断的线程化处理
9.mmap内存映射
imx6ull驱动大全

imx6ull应用开发基础

1.shell脚本

  1. .s格式的文件,第一行必须是#!/bin/bash;
  2. echo打印命令;
  3. read命令:读取命令行的输入;
  4. &&和||:命令1 && 命令2,表示命令1执行并且正确后命令2执行;命令1 || 命令2,表示命令1执行并且错误后命令2执行;
  5. test命令:查看文件是否存在、两个字符串是否相等、两个值比大小等;
  6. 中括号[]判断符,整体输出真或假;
  7. 默认变量:0表示命令行第一个参数./.sh0表示命令行第一个参数./**.sh;1-n表示第一个到底n个参数;n表示第一个到底n个参数;#表示参数的数量;$@表示所有的参数;
  8. 判断:{if [表达式]:then (执行语句) fi};或者{if [表达式]:then (执行语句) else (执行语句) fi};或者{if [表达式]:then (执行语句) elif [表达式]:then (执行语句) else (执行语句) fi};
  9. 判断:case:{case $val in "值a") (执行语句) ;; "值b") (执行语句) ;; '*') (执行语句) ;; esac}
  10. 函数
    函数名(){函数体};
  11. 循环:{while [表达式] do (执行语句) done};{for val in con1 con2 ... do (执行语句) done};{for((初始值; 限制值; 执行步长)) do (执行语句) done};

2.GCC

  1. 预处理:.c中的头文件找到、宏展开、编译开关展开,得到.i文件;
  2. 编译:得到汇编文件.s(语法错误在该过程中发现);
  3. 汇编:得到机器码文件.o;
  4. 链接:链接为一个应用程序;

3.Makefile文件及编写规则

4.文件IO

  1. 用到的函数:open、creat、write、read;
  2. 文件IO系统调用内部机制
    调用glibc写的系统接口访问Linux内核提供的服务(触发异常 )(文件句柄和文件install绑定的时候回产生一个task_struct结构体(每个进程都有一个),其里面有一个files成员,其里面有个fdt成员,其里面有个fd数组,对应进程的各个句柄对应的文件值);
  3. 两次读一个文件,各读各的,f_pos互相不影响,使用fd2=dup(fd1)是在fd数组中新建的句柄指针fd2指向被复制的数组fd1所以f_pos是一样的。(dup也可指定别的句柄(dup2(fd1,*)));

5.Framebuffer

  1. 控制LCD;
  2. freetype显示单个文字、显示旋转的文字、显示一行文字;

6.各种编码方式

ASCII编码、ANSI编码、字符集、unicode编码(UTF16-LE编码、UTF16-BE编码、UTF8编码)

7.读取输入设备信息

  1. 查询方式;
  2. 休眠唤醒方式;
  3. POLL/SELECT方式(定个闹钟,到时间了看一下);
  4. 异步通知方式(驱动程序主动告知app);

8.网络编程(类似文件传输有源\目的\长度)

  1. TCP(可靠、重传、有链接);
    1. 服务端
      1. fd=socket()
      2. bind(自己的ip,端口)
      3. listen(开始监测数据)
      4. accept(建立一条连接)
      5. send发送数据
      6. recv接收数据
    2. 客户端
      1. fd=socket()
      2. connect(目的)
      3. send发送数据
      4. recv接收数据
  2. UDP(不可靠、无连接);
    1. 服务端
      1. fd=socket()
      2. bind(自己的ip,端口)
      3. recvfrom/send_to
    2. 客户端
      1. fd=socket()
      2. connect(实际没有使用)
      3. send发送数据
      4. recv接收数据

9.多线程编程

进程的话是两个main函数,内部函数需要通信。线程的话是一个main函数,内部线程可以通过全景变量传递信息;

10.TTY体系中设备节点

  1. TTY/Terminal/Console/UART区别;
  2. TTY驱动程序框架(RXT TXT--UART--uart_driver--line_displine行规程);

11.串口应用程序编程

  1. ARM从高电平->低电平开始计时,保持1bit时间(开始位);
  2. ARM根据数据驱动TXD电平,bit[0]、bit[1]...bit7;
  3. (奇/偶校验位)数据位中为1的个数;
  4. (奇/偶校验位)数据位中为1的个数;
  5. ARM->高电平(停止位);

12.IIC应用编程

  1. 协议价绍(看裸机开发);
  2. SMBus协议(IIC的拓展);
  3. IIC系统重要的结构体:
    1. 控制器:I2C_Adapter(num第几个iic总线,xfer传输函数);
    2. 设备:I2C_Client(addr设备地址,adapter表示挂在哪个iic控制器上);
    3. 数据传输:I2C_msg(addr从设备地址,flag读还是写,buffer数据,len长度);

Linux内核中一般有了iic的驱动程序Adapter-DRV,提供了iic总线的读写能力,应用程序可以通过i2c-dev.c访问到Adapter-DRV进而控制iic总线;

13.IIC-Tools访问iic设备:

  1. 指定IIC控制器:i2c-dev.c为每个I2C控制器都生成了一个设备节点/dev/i2c-0、/dev/i2c-1,使用open某个设备节点/dev/i2c-x,就是访问该I2C控制器下的设备;
  2. 指定IIC设备:通过ioctl指定iic设备的地址,具体使用ioctl(file,I2C_SLAVE,addreess)或者ioctl(file,I2C_SLAVE_FORCE,addreess)。
  3. 传输数据:使用SMBus协议方式:ioctl(file,I2C_SMBUS,&args)或者iic协议
    (file,I2C_RDWR,&rdwr);

imx6ull驱动开发基础

1.怎么写一个驱动程序

  1. /* 1.确定主设备号 */
  2. /* 2.定义自己的file_operation结构体 */
  3. /* 3.实现对应的open/read/write等函数,填入file_operation结构体 */
  4. /* 4.把file_operation结构体告诉内核:注册驱动程序 */
  5. /* 5.谁来注册驱动程序,需要有一个入口函数,安装驱动程序时,就回去调用这个入口函数 */
  6. /* 6.有入口函数就有出口函数:卸载驱动程序时就去调用这个出口函数 */
  7. /* 7.有入口函数就有出口函数:卸载驱动程序时需要将注册的驱动程序去除 */

2.普适GPIO引脚操作方法

  1. 时钟使能;
  2. 模式选择;
  3. 输入/输出;
  4. 值;

注:读写寄存器的时候不要影响到其他位;(读、置位、写)

2.1 AM335x和AM437x的GPIO操作方法

  1. AM335X有gpio0-gpio3;AM437有gpio0-gpio5;gpio0常使能、其他的通过PRCM模块来控制使能状态;
  2. PRCM:Power,Reset,and Clock Managenment
  3. 拿到一个开发板配置GPIO要看他的芯片手册中GPIO章节、PRCM章节和Control Module章节;
  4. 配置某个引脚工作于GPIO模式:
    1. 在原理图上找到pin number;
    2. 根据pin number确定芯片手册中的pin name;
    3. 根据pin name在芯片手册的Control Module章节确定该引脚的寄存器(基地址+偏移地址);

2.2 RK3288和RK3399的GPIO操作方法

  1. RK3288有gpio0-gpio8;RK3399有gpio0-gpio4;每组GPIO分abcd四个小组gpio(0-7);
  2. 每组GPIO都有Data RegisterI(输出用)、Data Direction Register、EXT Data Register(输入用);
  3. 使能GPIO时钟寄存器:CRU(Clock & Reset Unit)时钟和复位单元;
  4. 设置GPIO模式寄存器:对于gpio1-8:PMU(Power Managerment)电源管理单元、对于gpio0:GRF(General Register Files)通用寄存器文件;

2.3 imx6ull的GPIO操作方法

  1. 有5组GPIO,每组最多有32个引脚;
  2. 使能某一组gpio时钟:CCM(Clock Controller Module);
  3. 设置gpio引脚模式:IOMUXC(包括MUX_MODE和SW_PAD_CTL两个寄存器,分别用来选择模式和是否上拉、回环等等);
  4. 设置gpio寄存器:输入还是输出GPIO_GDIR(还有中断寄存器等)、值多少GPIO_DR、状态GPIO_PSR;

2.4 STM32MP157的GPIO操作方法

  1. 内部有两个Cortex-A7核,一个Cortex-M4核;
  2. 要先使能PLL(PLL4)时钟本身,才能使能GPIO时钟;
  3. 使能GPIO时钟:RCC_MP寄存器或者RCC_MC寄存器;
  4. 设置GPIOX_MODER寄存器配置工作为GPIO模式、输入/输出;

3.最简单的LED驱动程序

  1. app只能open/read/write/ioctl;
  2. app——file_operation结构体——访问硬件;
  3. file_operation结构体是驱动程序的核心;
  4. app和驱动程序通过copy_to_user和copy_from_user传输数据;
  5. 驱动程序不可以直接访问寄存器的物理地址,需要通过ioremap把物理地址映射为虚拟地址进行访问;
  6. 编写led驱动程序,其实就是file_operation结构体中的读写、出入口函数等;
  7. 编写led测试程序(作为app);
  8. 编写Makefile后make编译得到.ko文件和生成的app,复制到开发板,使用insmod *.ko导入驱动程序(rmmod name 是卸载驱动程序);
  9. ls /dev/(驱动文件里写的名字) -l 查看有无对应的设备节点;
  10. 运行测试app;

4.LED驱动程序框架

额外知识:

  1. dmesg可以查看内核打印的信息;
  2. MMU(Memory Manager Unit)内存管理单元两大功能:
  3. 地址映射:CPU发出同样的地址(虚拟地址),执行不同的APP时,访问的是物理地址,由MMU执行中间的转换;
  4. 权限保护:CPU发出的地址,要经过MMU审核之后才可以访问具体硬件;

LED驱动程序框架:

  1. 找到原理图LED的引脚;
  2. 看芯片手册找到GPIO的时钟寄存器使能;
  3. 看芯片手册找到GPIO的IO_MUX寄存器复用为GPIO;
  4. 看芯片手册找到GPIO内部方向寄存器设置输入/输出;
  5. 看芯片手册找到GPIO内部数据寄存器 设置输出值/读输入值;
  6. 在每个开发板的驱动程序board.c中写对应开发板的led控制逻辑,修改寄存器值;抽象出来一个led_operation结构体;
  7. 使用中间层驱动leddrv.c(类似字符驱动框架)根据每个开发板的led_operation结构体进行LED控制;

5.驱动设计的思想

面向对象、分层、分离

  1. file_operation调get_board_led_opr调get_led_resouce;
  2. file_operation实现从app到led驱动的第一层驱动;
  3. get_board_led_opr实现所有关于led的io的初始化和控制关于led的io输出高低电平;
  4. get_led_resouce实现初始化芯片资源的哪个引脚(LED的话就选择led对应的引脚);

6.驱动设计的进化,总线设备驱动模型

image.png

  1. 分配/设置/注册platform_device结构体:定义所用资源,指定设备名字;
  2. 分配/设置/注册platform_driver结构体:在其中的probe函数里,分配/设置/注册file_operations结构体,并从platform_device中确定所用硬件资源,指定platform_driver的名字;

7.使用总线设备驱动模型改造LED模板驱动程序

  1. board_A.c:在入口函数中注册platform_device结构体,在 platform_device结构体中指定使用哪个GPIO引脚。
  2. chip_demo_gpio.c:注册platform_driver结构体使用Bus/Dev/Drv模型,当有匹配的platform_device时,它的 probe函数就会被调用,从匹配的platform_device中得到获取资源、确定引脚并创建设备。

8.驱动进化:设备树的引入

  1. 设备树文件dts、语法(格式、属性、常用的节点);
  2. 编译dts文件为dtb文件,在服务器make dtbs编译或者是手工编译,也可以反编译;
  3. 板子上/sys/firmware/fdt文件,就是 dtb 格式的设备树文件;

image.png

9.使用设备树改造LED模板驱动程序

  1. 修改服务器内核中的设备树文件添加led节点(包括宏、compatible属性、哪个引脚);
  2. 在chip_gpio.c在platform_driver结构体中添加of_match_table表示支持设备树,其中compatible的值需要和设备树文件对应;
  3. 在probe函数中,dev.of_node取出设备节点,并读出pin属性值;
  4. 创建设备节点的/dev/*,后面实现init和ctl;

10.按键驱动程序开发

相关概念:

  1. APP读取按键的方式:查询方式(循环查询)、休眠-唤醒方式(中断判断或rtos)、poll方式(闹钟+中断)、异步通知方式(按键自动通知);
  2. 驱动的基本技能:中断、休眠、唤醒、poll等机制;
  3. APP开发的基本技能:阻塞、非阻塞、休眠、poll、异步通知;

查询方式写按键驱动:

  1. button_drv.c实现file_operation结构体;
  2. board_xxx.c实现button_operations结构体;
  3. app调用button_drv调用board_xxx,不同单板修改board_xxx.c;

11.Pinctrl子系统

相关概念:

  1. 之前驱动思路:资源和驱动在同一个文件(一层驱动)->资源使用platform_devive指定,驱动在platform_driver实现->资源使用设备树指定,驱动在platform_driver实现;
  2. 每次操作都要查看寄存器引脚,很繁琐,利用Pinctrl子系统简化这部分操作(由bsp工程师完成),

12.GPIO子系统

相关概念:

  1. 实现在设备树里指定GPIO引脚;
  2. 在驱动代码中:使用 GPIO 子系统的标准函数获得 GPIO、设置 GPIO 方向、读取/设置 GPIO 值;
  3. 这样的驱动代码,将是单板无关的;

实现demo:

  1. 在命令行设置某个引脚(也提供了两个版本的代码供程序里使用):
  2. 在开发板目录:/sys/class/gpio下找到gpiochipxxx目录,进入后cat label查看该组gpio的基地址;
  3. 找到对应的gpiochipxxx,基地址xxx(可通过cat base查看)+io号就对应改组gpio的号码;
  4. 假设引脚号码为N,则在命令行可以配置方向和值等:
    1. echo N > /sys/class/gpio/export
    2. echo out > /sys/class/gpio/gpioN/direction
    3. echo 1 > /sys/class/gpio/gpioN/value
    4. echo N > /sys/class/gpio/unexport

12.Linux 系统对中断的处理

相关概念

  1. 对于耗时中断解决:1.分成上半部(硬件中断,无法处理其他中断)和下半部(软件中断,可以被中断打断,通过软件中断来实现);2.用内核线程(中断下半部作为一个线程,轮流执行);
  2. 硬件中断不能嵌套,软件中断可以;
  3. 软件中断是处理完硬件中断后顺便处理的;
  4. 为了避免中断下半部执行时间过长,内核有个worker线程,内部有个work queue会调度执行queue内的函数,所以当中断下半部需要执行的时间很长时,要先构造work结构体内部填充执行函数,在中断上半部执行完后把work放入work queue。
  5. 线程化中断:每个中断都创建一个内核线程,分配到多个cpu核上运行;
  6. 设备树里指定中断,irq_domain中有个xlate函数解析设备树中指定的irq_parent和interrapt,还有个map函数将硬件中断号映射为如软件中断号,会保存在palatform_device中,就可以使用request_irq注册这个中断处理函数了。
  7. 读取GPIO寄存器得到hwirq,根据hwirq得到之前映射的软件中断,既可以实现一个中断的硬件中断和软件中断的绑定。

13.编写使用中断的按键中断程序

对于GPIO按键,使用内核自带的驱动程序drivers/input/keyboard/gpio_key.c,只需要修改设备树指定的引脚及键值;

image.png

14.驱动程序的基石

1.休眠与唤醒

drv_read驱动程序中,读按键有值就返回,没有值就休眠,按键中断程序会唤醒app并把按键值传给app

2.POLL机制

中断+定时,检查有无数据没有数据则进入睡眠状态,如果还没有数据,休眠到时间后再去判断,超时退出; 内核帮忙实现了sys_poll,app调用sys_poll,内核函数调用驱动程序drv_poll(把线程放入work queue中,但不休眠,返回event状态,休眠在上层内核函数sys_poll中进行)。

3.异步通知

驱动程序发信号,app把自己的pid号给驱动程序; 使用kill_fasync(PID, SIGIO)发信号; app事先注册一个信号处理函数signal(SIGIO, func),收到信号后执行func函数;

image.png

4.阻塞与非阻塞

驱动程序能获得应用程序open文件传入的O_RDWR或O_NONBLOCK;

5.定时器(属于软件中断)

按键抖动会频繁发生中断,每次按键中断我们都忘后延迟20ms,这20ms是超时时间,按键稳定后超时再执行按键中断函数;

image.png

6.中断下半部tasklet

软件中断中有个tasklet_action队列,里面存放tasklet任务结构体,硬件中断执行完会将该中断的下半部的tasklet放入队列中,内核线程会去执行;

7.工作队列

在内核线程中执行; 驱动程序构造work(含有执行函数func),然后把work放入队列,最后唤醒;

8.中断的线程化处理

使用request_threaded_irq替换原来的中断注册函数request_irq image.png

9.mmap内存映射

基础知识:

  1. 用户态buffer和内核态buffer传数据要使用专门的函数,但当数据量大时就容易出现问题;
  2. 解决:mmap是把内核的buffer映射到用户态,让app在用户态直接读写内存;
  3. cache和buffer;
  4. cpu发出虚拟地址通过mmu内存管理单元找到对应的虚拟地址,具体的过程是通过页表映射实现;

代码编写:

应用程序编写函数格式可以在命令行执行“man 函数名”查看; image.png

imx6ull驱动大全

本文作者:zzw

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 License 许可协议。转载请注明出处!