【正点原子】Alpha-I.MX开发板操作系统移植流程

以下内容是自己在学习正点原子Alpha-I.MX开发板基于linux操作系统移植时做的笔记,
包括环境搭建,外接传感器实验等内容。
记于2022.11.24  疫情期间

文章目录

1. I.MX开发板移植流程

整个过程总结如下:

  • uboot移植。根据官方uboot,对你自己的开发板做适配,更改uboot代码,编译 为 .bin文件,然后利用imxDownload 烧写 .imx文件
  • **linux 内核移植。**在linux内核系统中添加自己的开发板 imx6ull 文件夹,编译成 zImage 镜像文件
  • **根文件系统构建。**制造根文件系统,生成.dtb文件

整个过程可以注意以下几点:

  1. 将整个系统烧写进SD卡或者EMMC,然后通过SD卡或者EMMC方式启动内核测试。
  2. 开发过程中,为防止多次插拔SD卡烧写内核和设备树,可以先烧写uboot,然后通过uboot的tftp和nfs功能,启动在Ubuntu环境下的zImage 和 .dtb测试
  3. 可以使用韦东山老师提供的 100ask_imx6ull烧写工具v4 烧写
  4. uboot 启动 启动在Ubuntu环境下的zImage 和 .dtb 是将2者加载到内存中运行,可以不烧写进去

1.1 开发板资料

  • 手头的开发板:I.MX6U-ALPHA 开发板,以NXP的 IMX6UL/ULL为核心的Cortex-A7开发平台(对应CPU架构是ArmV7

    参考:https://zhuanlan.zhihu.com/p/95501267

  • 韦东山教程中的开发板:100ASK_IMX6ULL_PRO 开发板,基于 NXP Cortex-A7 IMX6ULL处理器

    参考:https://zhuanlan.zhihu.com/p/302851297

1.2 代码的编译和烧写

利用 imxdownload 软件将编译好的 .bin文件,烧写到SD卡中

1、将 imxdownload 拷贝到工程根目录下

2、给予 imxdownload 可执行权限 (chmod 777 imxdownload ,ls命令之后,有权限的文件是绿色,无权限的文件是白色)

3、往SD卡中烧写文件(注意:要准备一张无数据的SD卡,烧写过程中会格式化SD卡)

./imxdownload led.bin /dev/sdd /dev/sdd 是你SD卡在Ubuntu中的挂载目录,这条指令是像SD卡中烧写 led.bin 文件,烧写完成后会在当前目录下生成一个load.imx文件,这就是根据NXP官方启动方式 在 led.bin 文件前面添加了一些数据头以后生成的,最终烧写到SD卡中的就是 load.imx 文件而非 led.bin。

知识点注意:

Ubuntu 下所有的设备文件都在目录“/dev”里面 ,其中存储设备都是以“/dev/sd”开头的 。插上SD卡后,也会挂载到/dev目录下。

首先,查看不插SD卡的存储设备:输入指令: ls /dev/sd* 回复: /dev/sda /dev/sda1 注意:这两个是系统磁盘

然后,插入SD卡以后,再查看存储设备,回复: /dev/sda /dev/sda1 /dev/sdb /dev/sdc /dev/sdc1 /dev/sdc2

哪个分区是自己的SD卡呢?将读卡器连接到Windows下,显示有三个分区,分别是Boot U盘G U盘 J ,对应在虚拟机下,/dev/sdc 是我的SD卡。

image-20220815090358252

1.3 Ubuntu环境的搭建和配置

  1. 下载 VMware和 Ubuntu 的镜像文件,安装虚拟机。

严格按照参考文档,(注意:虚拟机的磁盘空间至少大于50G,最好预留100G的空间),这是官方已经做好的开发环境

image-20220811153037317
  1. 配置网络环境,能用起 TFTPNFS
  • Ubuntu 开发板 Windows网络环境配置
  • TFTP环境配置
  • NFS环境配置

我用的是电脑wifi上网,开发板用网线和电脑直连:

image-20220811215454453

  1. VMnet0 装虚拟机时,配置的网络是桥接模式,这个需要手动分配IP,用于开发板和Ubuntu通信(适配器1)

  2. VMnet8 给虚拟机添加一个网络适配器,配置为NAT模式,供虚拟机上网,这个已经自动分配好IP了(适配器2)

  3. VMnet1 是虚拟机的仅主机模式

要求:在虚拟机中设置 VMnet0 和 Windows下的以太网口在同一个网段下,在开发板中设置开发板的ip和VMnet0在同一网段下。因此,开发板、虚拟机的VMnet0 和 Windows的以太网口 三者都在同一网段下,实现两两互ping

配置完之后的各自的IP地址如下:其中开发板、虚拟机以及Windows的以太网口的IP为手动设置,让他们三个在同一个网段上。

开发板系统IP(手动设置)虚拟机Ubuntu的IP(手动设置)windows网口的IP(手动设置)
192.168.10.50192.168.10.100192.168.10.200

其中,在Ubuntu中

VMnet0:192.168.10.100 ——Windows中的 以太网口,手动设置为 :192.168.10.200

VMnet1:192.168.204.0 ——Windows中的 VMnet1:192.168.204.0

VMnet8:192.168.74.0 ——Windows中的 VMnet8:192.168.74.1

测试结果:

image-20220812143528224

以下TFTP和NFS在从SD卡启动和从EMMC启动的情况下均测试成功。

TFTP,简单文件传输协议, 客户机与服务器之间进行简单文件传输的协议 ,TFTP 搭建之后测试:

Ubuntu中的路径:/home/alientek/linux/tftp ,开发板可以将Ubuntu中的文件下载下来

image-20220812144325603

NFS,网络文件传输,实现用户访问网络上别处的文件,像访问自己的计算机文件一样 ,NFS搭建好测试结果如下:

Ubuntu中的路径: /home/alientek/linux/nfs

image-20220812150256748

1.4 系统的烧写

参考资料:【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.6 第三十九章————系统烧写

1.4.1 OTG 模式烧录

1.4.1.1 USB OTG方式

USB OTG 方式固化到 Emmc

工具:MfgTool

连线:USB OTG (板载上边那个口是OTG,下边是TTL),拨码到 0100_0000 ,烧写时需要用电源线供电,注意,使用OTG烧写时,iMX6ULL开发板不能插入SD卡

烧写原理:MfgTool 其实是先通过 USB OTG 先将 uboot、 kernel 和.dtb(设备树)这是三个文件下载到开发板的 DDR 中, 注意不需要下载 rootfs,相当于直接在开发板的 DDR上启动 Linux 系统,等 Linux 系统启动以后再向 EMMC 中烧写完整的系统,包括 uboot、 linux kernel、 .dtb(设备树)和 rootfs,

启动方式:EMMC 启动设置: 1010_0110

测试:烧写正点原子以及NXP官方的都失败,换胡胡的电脑烧写也失败

测试烧写失败,卡在这里,参考 论坛官方的 方法也未解决 关于Linux使用 mfgtool 上位机固化系统(OTG 方式)的问题-OpenEdv-开源电子网

image-20220811165102831

1.4.1.2 采用脚本固化的方式

脚本方式固化到 Emmc

脚本方式固化到 SD卡然后从SD卡启动

参考手册,【用户快速体验】,使用脚本固化系统部分,完成SD卡的固化。

image-20220812134500299

测试从SD卡可启动,从EMMC中野可启动,注意串口连接上不打印消息,注意开发板底板上串口跳线帽的选择

1.5 Imax自带内核移植

U-BootLinux kernelrootfs 这三者一起构成了一个可以正常使用、功能完善的 Linux 系统。

1.5.1 交叉编译工具安装

用户快速体验手册: 4.1 安装通用ARM交叉编译工具链 4.2 安装Poky交叉编译工具链

安装后的版本:arm-linux-gnueabihf-gcc --version 4.9.4 arm-poky-linux-gnueabi-gcc --version 5.3.0

1.5.2 U-boot和 linux内核编译

  • U-Boot编译

编译uboot之后,生成相关文件。.imx文件 是已经添加头部信息的U-boot 镜像,可直接使用 dd 指令烧写到 TF 卡和开发板上的 eMMC 储存设备。.bin文件 是未添加状信息的 U-boot镜像,需要使用【正点原子】 I.MX6U 嵌入式 Linux 驱动开发指南里所说的 imxdownload 工具烧写

image-20220813141625215

  • linux内核编译

下载某个版本的linux内核 → 将下载的内核移植到自己的CPU架构上 → 再移植到自己所用的开发板上

编译后,在 arch/arm/boot 目录下生成 Linux 内核 zImage镜像文件,在 arch/arm/boot/dts 下生成很多.dtb 设备树文件 ,还有 modules.tar.bz2(内核模块)

image-20220813145124586

1.5.3 文件系统附录

目录目录功能或放置的内容
/bin系统放置了许多执行文件,如 mv、 cp、 date 等指令,这些指令一般是单用户维护 模式使用,还可以被 root 用户使用
/bootboot 目录一般放开机启动文件,但是 I.MX6U 已经有单独一个分区 boot 存储启动 文件(zImage 和设备树 dtb 文件)
/devLinux 系统使用任何设备与接口都是以文件的形式存放在这个目录下,在 Linux 下一切皆文件
/etc系统配置文件几乎都放在这个目录,例如启动脚本及所有的服务文件等
/home默认用户主文件夹,默认存在 root 用户。新建的用户都会在此生成用户主文件夹
/lib系统函数库,及内核模块 modules(驱动程序)
/mnt一般用于临时挂载目录
/opt第三方软件放置的目录,所以出厂 Qt 应用程序及所用到歌曲歌词等都放在这个目 录下
/proc此目录是一个虚拟的文件系统,它的数据在内存中,如内核、进程、外部设备和 网络状态等,所以不占磁盘空间
/run放置系统运行时所需要文件, 以前防止在/var/run 中, 后来拆分成独立的/run 目录。 重启后重新生成对应的目录数据
/sbin放置 sbin 目录下的指令是系统指令,只有 root 用户可以执行
/sys与/proc 目录一样,是虚拟的文件系统,记录核心系统的硬件信息
/tmp存放临时文件目录
/usrLinux 核心目录,目录下包括非常多的文件,如共享文件,一些可执行文件等等
/var存放系统执行过程经常改变的文件

1.5.4 通过uboot从网络启动linux系统

uboot编译烧写完之后,使用 tftp 从 Ubuntu 中下载 zImage 和设备树文件 。

将RT-Thread 内核移植到其他芯片上去,就要在TH-Thread内核中根据要移植的芯片做硬件适配修改

(1)开发板重启,进入uboot环境

开发板重启,3s之内快速按回车键,便可以进入uboot;若不按键,则直接会启动内核。image-20220825110907873

(2)给uboot设置环境变量

有两种方式,需要哪种就用哪种:ip地址需要根据实际环境更改(以下从种方法设置一次环境变量即可)

  • 使用TFTP 挂载内核和设备树

确保Ubuntu和开发板网络环境互通的前提下,通过下载tftp文件夹下的 设备树系统镜像文件,启动内核

setenv ipaddr 192.168.10.50
setenv ethaddr 00:04:9f:04:d2:35
setenv gatewayip 192.168.10.1
setenv netmask 255.255.255.0
setenv serverip 192.168.10.100
saveenv


setenv bootargs 'console=ttymxc0,115200 root=/dev/mmcblk1p2 rootwait rw'
setenv bootcmd 'tftp 80800000 zImage; tftp 83000000 imx6ull-alientek-emmc.dtb; bootz 80800000 - 83000000'
saveenv
  • 使用NFS挂载文件系统
setenv bootcmd 'tftp 80800000 zImage; tftp 83000000 imx6ull-alientek-emmc.dtb; bootz 80800000 - 83000000'
setenv bootargs 'console=ttymxc0,115200 root=/dev/nfs \
nfsroot=192.168.10.100:/home/alientek/linux/nfs/rootfs,proto=tcp rw \
ip=192.168.10.50:192.168.10.100:192.168.10.1:255.255.255.0::eth0:off'
saveenv
boot

格式为:

image-20220825151414435

只烧写uboot———将正点原子官方配置好的uboot烧写到SD卡中

首先确定 /dev/sdc 是我的SD卡设备:ls /dev/sd* /dev/sda /dev/sda1 /dev/sdb /dev/sdc /dev/sdc1 /dev/sdc2

然后将上边小节编译好的uboot烧写到SD卡中(一张格式化了的空SD卡),然后设置SD卡启动

image-20220815102349672

烧写成功后从串口看uboot启动内核的信息,提示没有镜像文件。并且uboot启动 的过程中还报了一些错误,(这些错误是没有设置uboot的相关环境变量参数)

我们**检测在uboot环境下TFTP和NFS服务没有问题的前提下,利用Uboot 启动虚拟机中的linux内核**,烧写好u-boot后,先配置u-boot的参数:

  • 配置网络参数:

确保Ubuntu 主机和开发板的 IP地址在同一个网段内, 开发板IP设置为 192.168.10.50,Ubuntu的ip为192.168.10.100,都在192.168.10.1这个网关下,设置后确保开发板的uboot可以ping通虚拟机,注意:开发板uboot可以ping虚拟机,但是虚拟机不能ping开发板的Uboot,因为uboot中没有处理外部ping的功能

setenv ipaddr 192.168.10.50
setenv ethaddr b8:ae:1d:01:00:00
setenv gatewayip 192.168.10.1
setenv netmask 255.255.255.0
setenv serverip 192.168.10.100
saveenv

设置好网络参数之后,首先确保开发板能ping通Ubuntu,然后用nfs指令访问Ubuntu中的zImage镜像文件。

nfs 808000000 192.168.10.100:home/alientek/linux/nfs/zImage

表示:uboot通过nfs找到Ubuntu的ip地址和路径,下载zImage 文件保存到地址为0x808000000 处

image-20220815101215682

先在Ubuntu环境下,编译好linux内核

编译过程参考上边小节,最终生成zImage文件

设置从SD卡中启动uboot,通过tftp和nfs,启动虚拟机中的内核

将zImage下载到DRAM的0X80800000地址处,然后将设备树下载到 DRAM 中的 0X83000000 地址处,最后之后命令 bootz 启动,测试结果内核启动成功

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

image-20220815103131532

2. uboot 部分

以下内容是基于正点原子的 Alpha I.MX 开发板的一些关于uboot内容的学习笔记。

2.1 uboot 简介

uboot是BootLoader的一种,作用是开机时先初始化DDR等外设,然后将Linux内核从flash(NAND,NOR FLASH, SD, MMC 等)拷贝到 DDR 中,最后启动 Linux 内核。 现在的 uboot 已经支持液晶屏、网络、 USB 等高级功能。

关系:uboot源码——芯片厂商(如NXP维护一个源码版本,支持自产的芯片的uboot启动)——–开发板uboot(如正点原子,修改自芯片厂商维护的uboot,支持自产的开发板启动,因为半导体厂商的uboot针对的是芯片,对应某个开发板,开发板的某些外设驱动可能不支持)

种类描述
uboot 官方的 uboot 代码由 uboot 官方维护开发的 uboot 版本,版本更新快,基本包含所 有常用的芯片。
半导体厂商的 uboot 代码半导体厂商维护的一个 uboot,专门针对自家的芯片,在对自家 芯片支持上要比 uboot 官方的好。
开发板厂商的 uboot 代码开发板厂商在半导体厂商提供的 uboot 基础上加入了对自家开发 板的支持

2.2 uboot 编译

2.2.2 编译命令行解释

解释:每条指令中间的两句中,arm 表示CPU架构,arm-linux-gnueabihf- 表示指令使用的交叉编译器,其中 make distclean表示第一次编译时清除一下工程,make mx6ull_14x14_ddr512_emmc_defconfig 表示用于配置uboot的配置文件名称,make -j4表示用4个核编译uboot。V=1 用于设置编译过程的信息输出级别;

#!/bin/sh    //新建脚本时使用
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-  distclean
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-  mx6ull_14x14_ddr512_emmc_defconfig
make V=1 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-  -j4

为防止每次编译 uboot 都要输入一长串命令 ,可以将以上命令写到一个脚本中,如 mx6ull_alientek_emmc.sh ,内容为以上命令,为规范 .sh脚本,并在首行添加 #!/bin/sh ,执行脚本命令 ./mx6ull_alientek_emmc.sh 编译后,生成 u-boot.bin 文件

说明:uboot 是 bootloader 的一种,可以用来引导Linux,但是 uboot 除了引导 Linux 以外还可以引导其它的系统 。而且 uboot 还支持其它的架构和外设, 比如 USB、 网络、 SD 卡等。这些都是可以配置的,需要什么功能就使能什么功能。 所以在编译 uboot 之前,一定要根据自己的需求配置 uboot。 如上边 mx6ull_14x14_ddr512_emmc_defconfig 就是正点原子为 Alpha I.MX 的 EMMC存储 512MB ddr内存 开发板写的配置文件。

说明:对于正点原子的 alpha i.mx 这款开发板,上电uboot启动后,倒计时提示,默认倒计时 3 秒,倒计时结束之前按下回车键就会进入 Linux 命令行模式。如果在倒计时结束以后没有按下回车键,那么 Linux 内核就会启动, Linux 内核一旦启动, uboot 就会寿终正寝。

2.2.3 编译生成文件解释

编译后生成 alientek_uboot.tar.bz2 压缩包,其中包括 u-boot.bin 、 u-boot.imx 文件等等 ,压缩包中的文件解释如下。 u-boot.imx 文件是NXP 的 CPU 专用文件

  • …/arch/arm/cpu 目录下的 uboot.lds 文件,是ARM 芯片所使用的 u-boot 链接脚本文件 。
  • …/board/freescale/mx6ullevk 目录下是和具体的板子有关的 内容。
  • …/config 目录为uboot的配置文件,编译uboot之前首先 make xxx_defconfig 来配置uboot
  • .u-boot.imx.cmd 文件: 将 .bin 文件转化为 .imx 文件。
cmd_u-boot.imx := ./tools/mkimage -n
board/freescale/mx6ull_alientek_emmc/imximage.cfg.cfgtmp -T imximage -
e 0x87800000 -d u-boot.bin u-boot.imx

解析:用到了工具 tools/mkimage,而 IVT、 DCD 等数据保存在了文board/freescale/mx6ullevk/imximage-ddr512.cfg.cfgtmp 中 ,工具 mkimage 就是读取文件xxx.cfg.cfgtmp 里面的信息,然后将其添加到文件 u-boot.bin 的头部,最终生成 u-boot.imx。

  • 根目录下 u-boot.lds 文件:程序的链接脚本文件,展示了代码的入口点,在arch/arm/lib/vectors.S 文件中
  • arch/arm/lib/vectors.S :

2.3 uboot 常用命令

信息查询

命令描述
printenv输出环境变量信息
bdinfo查看板子信息
version查看 uboot 的版本号

环境变量操作

MMC(0) — SD 卡 MMC(1) —-EMMC

一般环境变量是存放在外部 flash 中的,uboot 启动的时候会将环境变量从 flash 读取到 DRAM 中

命令描述
setenv设置或者修改DRAM 中环境变量的值
saveenv保存修改后的环境变量, 保存到 flash 中

如:修改 bootargs 环境变量的值并保存

setenv bootargs 'console=ttymxc0,115200 root=/dev/mmcblk1p2 rootwait rw'` `saveenv

内存操作

网络操作

EMMC和SD卡操作

FAT好EXT格式文件系统操作

BOOT操作

剩下的参考手册,在此不给出

2.4uboot 启动流程

链接文件 arch/arm/cpu/ u-boot.lds → arch/arm/lib/vectors.S 路径下 代码入口点: _start → 在u-boot.map 文件中找到 __image_copy_start ,地址为 0X87800000 ,.text 的起始地址也是 0X87800000说明vectors 段的起始地址也是 0X87800000。因此,整个 uboot 的起始地址就是 0X87800000, 裸机程序链接起始地址选择0X87800000就是为了和uboot保持一样。向量表的起始地址也是 0X87800000。

linux内核在DRAM中的加载地址为 0x8080_0000

关于汇编部分。。。

2.5 uboot 移植

直接将NXP官方的uboot烧写到正点原子开发板中发现:

  • ① uboot 启动正常, DRAM 识别正确, SD 卡和 EMMC 驱动正常。
  • ② uboot 里面的 LCD 驱动默认是给 4.3 寸 480x272 分辨率的,如果使用的其他分辨率的屏幕需要修改驱动。
  • ③ 网络不能工作,识别不出来网络信息,需要修改驱动。

在uboot中添加自己的开发板的流程如下:

  • 添加开发板默认配置文件 xxx_defconfig

​ 在 configs目录下复制一份 mx6ull_14x14_evk_emmc_defconfig (表示芯片14*14大小) ,重命名为 mx6ull_alientek_emmc_defconfig ,然后修改内容。

  • 添加开发板对应的头文件 include xxxx.h

​ 在目录 include/configs 下添加 I.MX6ULL-ALPHA 开 发 板 对 应 的 头 文 件 , 复 制 include/configs/mx6ullevk.h,并重命名为 mx6ull_alientek_emmc.h ,然后修改头文件内容,头文件中的宏定义用于配置uboot和配置项目。mx6ull_alientek_emmc.h 文件的主要功能就是配置或者裁剪 uboot。如果需要某个功能的话就在里面添加这个功能对应的 CONFIG_XXX 宏即可,如果不需要某个功能的
话就删除掉对应的宏即可 。文件中以 CONFIG_CMD 开头的宏都是用于使能相应命令的,其他的以 CONFIG 开头的宏都是完成一些配置功能的 。

  • 添加开发板对应的板级文件夹

NXP 的 I.MX 系列芯片的所有板级文件夹都存放在 board/freescale 目录下,在这个目录下有个名为 mx6ullevk 的文件夹,这个文件夹就是 NXP 官方 I.MX6ULL EVK 开发板的板级文件夹。

3. linux内核部分

3.1内核启动流程

arch/arm/kernel/vmlinux.lds 中指明了了 Linux 内核入口,入口为 stext ,该文件在 arch/arm/kernel/head.S 中

arch/arm/kernel/head.S stext 是 Linux 内核的入口地址

init/main.c 启动内核

3.2 根文件系统构建–buildroot

​ buildroot 和 uboot LinuxKernal 类似,需要先下载源码,然后配置相关参数,如设置交叉编译器、设置目标CPU参数等,最重要的是选择需要的第三方软件或库,配置好以后进行编译,编译成功后在一个文件夹存放编译好的结果,也就是根文件系统。

下载源码

官网地址为 https://buildroot.org/ ,下载相应的版本如,buildroot-2019.02.6.tar.bz2

利用 menuconfig配置 buildroot

  • Target options :项目名称、架构等
  • Toolchain :配置本机交叉编译工具链的路径
  • System configuration:系统配置,比如开发板名字、欢迎语、用户名、密码等
  • Filesystem images :根文件系统的格式
  • uboot 和 linuxkernal :禁止编译uboot 和linux内核
  • Target packages :配置第三方软件库 (这一点比较重要)

编译 buildroot

sudo make ,编译完成以后就会在 buildroot-2019.02.6/output/images 下生成根文件系统

4. 简单的字符设备驱动开发

4.1概念和原理

​ 解释含义:字符设备驱动就是一个一个字节,按照字节流读写操作的设备,读写数据是分先后顺序的。比如,点灯、按键、IIC、SPI、LCD等都是字符设备,设备的驱动叫做字符设备驱动。

image-20220819093820544

​ 操作过程:如LED驱动加载好以后,会生成 /dev/led的驱动文件,(/dev是设备目录,/led是具体的驱动的名称)应用程序使用 open() 函数打开文件 dev/led,使用完成后用 close()函数关闭这个文件。open()和close()就是打开和关闭LED驱动的函数。若要点灯,则使用 write()函数操作向驱动写入数据,这个数据就是要关闭还是打开LED的控制参数。若要获取LED灯的状态,则用read()函数从驱动中读取相应的状态。用户空间不能直接对内核进行操作 ,必须通过调用库的方法实现对内核的调用以及底层驱动的访问 。每一个系统调用,在驱动中都有与之对应的一个驱动函数,在 Linux 内核文件 include/linux/fs.h 中 file_operations 的结构体是 Linux 内核驱动操作函数集合。

4.2 字符设备驱动开发步骤

四步:驱动模块的加载与卸载、字符设备的注册于注销、实现字符设备的具体操作函数、添加 license 和作者信息

驱动的运行方式:

(1) 将驱动编译进Linux内核中。Linux内核启动时自动运行驱动程序(一般是调试好了之后再编译进去)

(2)将驱动编译成模块( .ko文件),内核启动后使用 modprobe加载驱动模块(调试用,修改后重新编译加载即可,不需要重启内核)

模块的加载和卸载:

加载 : insmod drv.ko 或者 modprobe drv.ko 卸载: modprobe drv.ko 或者 rmmod drv.ko

驱动模块加载成功 → 注册字符设备 卸载驱动模块 → 注销字符设备

Linux设备号: include/linux/kdev_t.h

注册字符设备的时候需要给设备指定一个设备号,并保证它没有被使用过,可以用 alloc_chrdev_region ()函数动态申请设备号

主设备号:表示一个具体的驱动 。数据类型32位,高12

次设备号:表示使用这个驱动的各个设备。数据类型32位,低20

chrdevbase 字符设备驱动开发实验(虚拟一个设备,测试Linux加载改设备驱动的读写操作,步骤如下:)

  1. 编写chrdevbase.c 和 Makefile 文件,编译生成 chrdevbase.ko 文件
  2. 编写chrdevbaseApp.c ,编译生成 chrdevbaseApp 这个在ARM架构下的可执行程序
  3. 加载驱动模块。开发板通过tftp和nfs将Ubuntu的文件下载到开发板,modprobe chrdevbase.ko 加载驱动文件,用cat /proc/devices 查看当前系统中所有设备
  4. 创建设备节点文件。mknod /dev/chrdevbase c 200 0 c 表示是字符设备 200是主设备号 0是次设备号
  5. chrdevbase 设备操作测试 ,./chrdevbaseApp /dev/chrdevbase 1 读取 ./chrdevbaseApp /dev/chrdevbase 2 写入
  6. 卸载驱动模块。rmmod chrdevbase.ko

4.3 LED 灯驱动开发过程

Linux 下的任何外设驱动,最终都是要配置相应的硬件寄存器。

地址映射:对于 32 位的处理器来说,虚拟地址范围是 2^32=4GB,但是开发板的DDR只有512MB (物理地址),经过MMU可以将 512MB的物理内存映射到整个4GB的虚拟内存空间。这个时候肯定存在多个虚拟地址映射到同一个物理地址的问题,这个暂不深究。Linux 内核启动的时候会初始化 MMU,设置好内存映射,设置好以后 CPU 访问的都是虚拟地址

**I/O内存:**当外部寄存器或内存映射到内存空间时,称为I/O内存。将寄存器的物理地址映射到虚拟地址后,就可以通过指针直接访问地址

代码的调用关系如下图所示:

编写的APP就是编写linux应用。

image-20220819152527824

makefile 文件代码解释:

image-20220819155816991

4.4 LED驱动–新字符设备驱动开发

旧:使用设备时用 register_chrdev 函数注册字符设备 , 不使用设备时用 unregister_chrdev 函数注销字符设备 ,驱动模块加载成功后需要用 mknod 命令手动创建设备节点。

新:编写新字符设备驱动,并且**在驱动模块加载的时候自动创建设备节点文件**

image-20220820104638543

4.5 设备树相关知识

笔记:.dts文件的路径:arch\arm\boot\dts\imx6ull-alientek-emmc.dts

修改设备树文件时,设备节点最好添加到好区分的地方,一级节点是最好的地方,修改后,DTC 工具源码在 Linux 内核的 scripts/dtc 目录下:编译 .dts 文件的命令是,在Linux源码根目录下,命令make dtbs

image-20220825103632325

4.5.1设备树概述

设备树是用来**描述板子上的设备信息**的,不同的设备其信息不同,反映到设备树中就是属性不同。

Linux 内核会根据 compatbile 属性值来查找对应的驱动文件

(一)DTS 、DTB 和DTC

.dts文件是设备树的源码文件, .dtb是将源码文件编译好的二进制文件,将.c文件编译为.o文件需要gcc编译器,将.dts文件编译为.dtb文件需要 DTC工具。

.dtsi文件里面是一款芯片所支持的所有板子共同存在的设备信息.dts文件中会引入头文件.dtsi文件,剩下的具体链接什么外设啊,这种信息都存在于.dts文件中,比如说I2C接了某传感器,前面的信息由.dtsi 提供,后面的信息由.dts提供。

如果我们使用 I.MX6ULL 新做了一个板子,只需要新建一个此板子对应的.dts 文件,然后将对应的.dtb 文件名添加到dtb-$(CONFIG_SOC_IMX6ULL) 下,这样在编译设备树的时候就会将对应的.dts 编译为二进制的.dtb文件。

(二)设备树在系统中体现

设备树文件中“/”是根节点,剩下的是子节点,子节点下面可能还会包含子节点,子节点下会有子节点包含的设备信息(也就是属性信息)

在移植好的系统中的, /proc/device-tree 目录下

两个特殊节点:aliases 和 chosen: aliases 子节点用于给节点定义别名,用于方便访问节点。chosen 自己诶单功能是 uboot向Linux内核传递数据用,重点是传递 bootargs 参数。(uboot 在启动Linux内核时会将bootargs的值传递给Linux内核,bootargs回作为Linux内核的命令行参数)

Linux 在 start kernal 后,会解析 DTB文件中的各个节点

(三)设备树常用的 OF 操作函数

设备都是以节点的形式“挂”到设备树上的,因此要想获取这个设备的其他属性信息,必须先获取到这个设备的节点。

这些函数用于获取设备树中某些属性的信息,如设备树使用reg属性描述了某个外设寄存器地址为0X02005482,长度为 0X400 ,在驱动开发时需要获取reg属性值,然后初始化外设。就可以用 include/linux/of.h 文件中的一些列 OF 函数。

  • 查找节点的函数:

of_find_node_by_name () of_find_node_by_type () of_find_compatible_node()

of_find_matching_node_and_match() of_find_node_by_path()

4.5.2 设备树中的重要属性

1. compatible 属性

compatible 属性也叫做“兼容性”属性,属性值是一串字符串列表,用于选择设备要使用的驱动程序,将设备和驱动绑定起来

格式:“manufacturer,model” 如: 在sound节点下,compatible = "fsl,imx6ul-evk-wm8960","fsl,imx-audio-wm8960"; 表示有2个属性,第一个属性是第一个引号,厂商是 fsl (飞思卡尔),驱动模块为“imx6ul-evk-wm8960”和“imx-audio-wm8960”表示驱动模块名字 。

sound这个设备会在Linux内核中查找是否可以找到与引号中的两个驱动文件相匹配的值,一般驱动程序文件会有一个OP匹配表,表中保存着一些 compatible 值,若设备节点的 compatible 属性和OF匹配表中的任何一个值相等,就表示可以使用这个驱动。

2. model 属性

用于描述设备模块信息 ,如 model = "wm8960-audio";

3. statue 属性

描述
“okay”表明设备是可操作的。
“disabled”表明设备当前是不可操作的,但是在未来可以变为可操作的,比如热插拔设备 插入以后。至于 disabled 的具体含义还要看设备的绑定文档。
“fail”表明设备不可操作,设备检测到了一系列的错误,而且设备也不大可能变得可 操作。
“fail-sss”含义和“fail”相同,后面的 sss 部分是检测到的错误内容。

4. #address-cells 和#size-cells 属性

无符号32位整形,可以用在任何拥有西街店的设备中,用于描述子节点的地址信息,分别决定了子节点reg属性中地址和长度信息的子长为32位。

如下表示:aips3: aips-bus@02200000 节点起始地址长度所占用的字长为 1,地址长度所占用的字长也为 1。 子节点 dcp: dcp@02280000 的 reg 属性值为<0x02280000 0x4000> 相当于设置了**起始地址为 0x02280000地址长度为 0x40000**

aips3: aips-bus@02200000 {
	compatible = "fsl,aips-bus", "simple-bus";
 	#address-cells = <1>;
​	 #size-cells = <1>;

	dcp: dcp@02280000 {
		compatible = "fsl,imx6sl-dcp";
		reg = <0x02280000 0x4000>;
	};
};

5. reg 属性

reg 属性一般用于描述设备地址空间资源信息,一般都是**某个外设的寄存器地址范围信息**。reg 属性的值一般是(address, length)对。

4.5.3 向节点追加内容

一旦硬件发生变化,则需要同步修改设备树文件,因为设备树是描述板子硬件信息的文件。比如 有两个芯片 fxls8471, fxls8471接到了板子的 IIC 接口上,那么就需要在 IIC 这个节点上再追加2个子节点。

imx6ull.dtsi :所有 使用 i.mx6ull 这个SOC 的开发板使用的文件(不能直接修改这个文件,否则会影响使用这个SOC的其他开发板)imx6ull-alientek-emmc.dts :正点原子alpha开发板使用的设备树文件,在这个文件中修改,只影响自己的开发板。追加方法如下:

image-20220824171020993

4.5.4 根节点和子节点

根节点 ”/“:在 /proc/device-tree 目录下,根节点属性表现为一个个的文件,“compatible”、“model”和“name” 等文件,其内容就是

子节点: 根节点下的子节点在同上目录下,表现为一个个文件夹,“compatible”、“model”和“name” 等,内容就是.dts文件中的信息

根节点:在同上目录下,,表现为一个个文件

4.5.5 特殊节点

  • aliases 子节点

aliases 节点的主要功能就是定义别名 ,目的就是为了方便访问节点。在节点定义命名时加上label,然后设备树文件中一般使用 &label 的形式访问节点,类似与下边的形式。

aliases {

​ spi0 = &ecspi1;
​ spi1 = &ecspi2;

… }

  • chosen 子节点

chosen 并不是一个真实的设备, chosen 节点主要是为了 uboot 向 Linux 内核传递数据,重点是 bootargs 参数。

uboot 在启动 Linux 内核的时候会将 bootargs 的值传递给 Linux内核, bootargs 会作为 Linux 内核的命令行参数。 uboot 自己在 chosen 节点里面添加了 bootargs 属性!并且设置 bootargs 属性的值为 bootargs环境变量的值。

4.6 设备树下的LED驱动

之前过程:在驱动文件中 xxx.c 定义寄存器的物理地址 → io_remap 内存映射,得到虚拟地址 → 操作寄存器对应的虚拟地址初始化GPIO

使用设备树之后的过程:

  • ①、在 imx6ull-alientek-emmc.dts 文件中创建 alphaled 设备节点,添加寄存器地址、长度信息,make dtbs,编译新的.dtb文件
  • ②、编写驱动程序,获取设备树中的相关属性值。
  • ③、使用获取到的有关属性值来初始化 LED 所使用的 GPIO。

第一步、使用新编译好的imx6ull-alientek-emmc.dtb 启动Linux 内核,启动后在/proc/device-tree/目录中查看“alphaled”节点 。

第二步、在代码中,初始化时获取设备树节点属性,

第三步、利用获取到的reg值,进行寄存器映射,初始化GPIO

结果验证:

(1)修改设备树,添加节点,编译;并将编译成的dtb文件发送到 home/alientek/linux/tftp 目录下。启动开发板,看是否成功添加成功节点(因为我将 alphaled 这个节点添加到了 / 一级子节点下面,所以在 /proc/device-tree/ 目录下可以直接看到这个节点

image-20220825104844132

(2)测试驱动模块加载无误

将编译生成的dtsled_zyh.ko 文件复制到我的Ubuntu目录下,然后利用 nfs 挂载到Ubuntu的目录下,参考自己写的【通过uboot 从网络启动Linux系统 】利用 nfs 挂载文件系统部分。

sudo cp dtsled_zhangyh.ko /home/alientek/linux/nfs/rootfs/lib/modules/4.1.15 -f

image-20220825153131932

测试加载驱动过程

depmod   								//第一次加载设备使用
modprobe chrdevbase.ko     				//加载驱动
cat /proc/devices              	  		//查看当前系统中有没有 **chrdevbase** 这个设备  
lsmod     								//查看当前系统中存在的模块  
rmmod  chrdevbase.ko   	 				//卸载驱动

image-20220825162618794

image-20220825174423984

4.7 pinctrl 和 gpio 子系统

pinctrl 子系统:针对 PIN 引脚的配置 drivers/pinctrl gpio 子系统 :针对 GPIO 的配置

面向对象,驱动分离和分层的思想。一般的开发流程是:首先,修改设备树,添加相应的节点,重点设置 reg 属性。然后,在驱动文件中利用 of 函数获取 reg 属性,进行内存映射并初始化 GPIO (初始化GPIO过程:设备 pin 脚的复用、上下拉、速度等功能,然后设置 GPIO 为输入/输出等模式,即先设置pin脚的功能,然后设置pin引脚对应的GPIO

pinctrl 子系统主要工作内容如下:

①、获取设备树中 pin 信息。
②、根据获取到的 pin 信息来设置 pin 的复用功能
③、根据获取到的 pin 信息来设置 pin 的电气特性,比如上/下拉、速度、驱动能力等。

这样就可以直接使用这两个子系统来配置GPIO ,简化开发流程

4.7.1pinctrl 子系统

不同的外设使用不同的pin,也具有不同的配置,“一个萝卜一个坑”,将某个外设使用的所有pin都组织到一个子节点里边。

一个 PIN 的配置主要包括两方面,一个是设置 PIN 的 引脚复用功能,另一个就是设置 PIN 的电气特性

创建节点时格式以及解释:

同一个外设的 PIN 都放到一个节点里面,在 imx6ull-alientek-emmc.dts 中,在 iomuxc 节点中的“imx6ul-evk”子节点下添加“pinctrl_test”节点,注意,节点前缀一定要为“pinctrl_”

/* 设备所使用的 PIN 配置信息 */
/*config 是具体设置值*/
pinctrl_test: testgrp {
	fsl,pins = <MX6UL_PAD_GPIO1_IO00__GPIO1_IO00 config>; 
}

注意,属性名一定是 fsl ,pins ,因为对于I.MX系列的SOC 来说,pinctrl 驱动是通过这个属性值读取设置的pin的信息的 。

解释:

#define MX6UL_PAD_UART1_RTS_B__GPIO1_IO19                         0x0090 0x031C 0x0000 0x5 0x0

fsl,pins<MX6UL_PAD_UART1_RTS_B__GPIO1_IO19  0x17059>;  

参数分别表示为:mux_reg conf_reg input_reg mux_mode input_val

<mux_reg conf_reg input_reg mux_mode input_val> = <0x0090 0x031C 0x0000 0x5 0x0>

注意: IO配置寄存器整体上分为两种, MUX 寄存器用于配置IO引脚的复用功能 PAD寄存器用于配置IO的电气特性。

  • mux_reg: mux_reg 寄存器(引脚复用寄存器)的偏移地址 IOMUXC_SW_ MUX_CTL_PAD_UART1_RTS_B (见下图1)
  • conf_reg : conf_reg 寄存器(引脚配置寄存器)的偏移地址 IOMUXC_SW_ PAD_CTL_PAD_UART1_RTS_B (见下图2)
  • input_reg : input_reg 寄存器的偏移地址 ,没有的话设置0
  • mux_mode:mux_reg寄存器的值,相当于设置 IOMUXC_SW_MUX_CTL_PAD_UART1_RTS_B 寄存器的值为 5,即设置 UART1_RTS_B 这
    个 PIN 复用为 GPIO1_IO19
  • input_val:input_reg 寄存器值,在这里无效。
  • 0x17059就是conf_reg寄存器的值

image-20220831180743626

image-20220831181517616

4.7.2 GPIO子系统

用于初始化GPIO并且提供相应的API函数。设备树中添加 gpio 节点模板 格式如下:

接着上一小节,向 pinctrl_test 这个节点中添加 test 设备,(pinctl_test 节点中描述了 test 设备所使用的 GPIO1_IO00这个pin脚信息)

test{
	pincrtl-names = "default";  /*属性值*/
	pinctrl-0=<&pinctrl_test>;  /*添加一个节点,此节点用 pinctrl_test 节点中的pin脚信息*/
	gpio =<&gpio1 3 GPIO_ACTIVE_LOW>;   /*添加GPIO 属性信息,表名test设备所用的 GPIO 使用gpio1的io03,低电平有效*/
};

4.7.3 驱动编写流程

设置好设备树以后,使用 GPIO 子系统提供的 API函数操作指定的 GPIO

  • 在iomux节点下的imx6ul-evk子节点下创建 “pinctrl_led”的子节点,将 GPIO1_IO03 这个引脚复用为 GPIO1_IO03,电气属性值为 0X10B0。

    pinctrl_led:ledgrp{
    	fsl,pins<MX6UL_PAD_GPIO1_IO03__GPIO1_IO03  0x10b0>;
    };
    // #define MX6UL_PAD_GPIO1_IO03__GPIO1_IO03         0x0068 0x02F4 0x0000 0x5 0x0
    
![image-20220831182808020](H:/typora_picture/image-20220831182808020.png)



- 添加LED设备节点,在 根节点下 “/” 添加 LED灯节点,命名为 gpioled 

  ```c
  gpioled {
  	#address-cells=<1>;
  	#size-cells=<1>;
  	compatible = "alientek-gpioled";
  	status = "okey";
  	pincrtl-names = "default";
  	pinctrl-0=<&pinctrl_led>;
  	led-gpio =<&gpio1 3 GPIO_ACTIVE_LOW>; /*指定LED灯使用的gpio为  gpio1 的 io03*/
  };
  • 检查该pin脚是否已经被其他外设使用。

  • 编写驱动代码


5 . Linux 并发与竞争

注意:ARM架构不支持直接对寄存器进行读写操作。C 语言中的 int a = 8; 转化为汇编语言可能为:

ldr r0,=0X30000000 /变量 a 地址 /
ldr r1, = 3 /要写入的值 */
str r1, [r0] / 将 3 写入到 a 变量中 /

描述:Linux是一个多任务操作系统,肯定会存在**多个任务共同操作同一段内存或者设备**的情况,当多个任务同时访问一片内存区域时,这些任务可能会相互颠覆这段内存中的数据,造成内存数据混乱。多个任务甚至中断都能访问的资源叫做共享资源, 在驱动开发中要注意对共享资源的保护,也就是要处理对共享资源的并发访问。

并发:多个“用户”同时访问同一个共享的资源,并发带来的问题就是竞争。 产生并发的原因主要有以下:

① 多线程并发访问, Linux 是多任务(线程)的系统,所以多线程访问是最基本的原因。
② 抢占式并发访问,Linux2.6版本开始内核支持抢占,也就是说调度程序可以在任意时刻抢占正在运行的线程,从而运行其他的线程。
③ 中断程序并发访问,学过 STM32 的同学应该知道,硬件中断的权利可是很大的。
④ SMP(多核)核间并发访问,现在 ARM 架构的多核 SOC 很常见,多核 CPU 存在核间并发访问

保护的内容:保护的是数据不是代码,保护的是多个线程都会访问的共享数据

并发与竞争的例子:线程AB都操作 0x3000_0000这个内存,线程A给这个内存赋值10,线程B赋值20,理想的情况下,A执行完在执行B,但是若不管理AB两个线程,则可能造成右侧情况,A执行的同时,B线程也在竞争这个内存,结果就是,A执行完了这个内存里的值不是目标值10,而是线程B的执行结果20.

image-20220823164910973

5.1 自旋锁的概念

​ 当一个线程要访问某个共享资源的时候首先要先获取相应的锁, 锁只能被一个线程持有,只要此线程不释放持有的锁,那么其他的线程就不能获取此锁。 对于**自旋锁**而言,如果自旋锁正在被线程 A 持有,线程 B 想要获取自旋锁,那么线程 B 就会处于忙循环-旋转-等待状态,线程 B 不会进入休眠状态或者说去做其他的处理,而是会一直傻傻的在那里“转圈圈”的等待锁可用。

​ 通俗的讲,自旋锁的“自旋”也就是“原地打转”的意思,“原地打转”的目的是为了等待自旋锁可以用,可以访问共享资源。 比如现在有个公用电话亭,一次肯定只能进去一个人打电话,现在电话亭里面有人正在打电话,相当于获得了自旋锁。此时你到了电话亭门口,因为里面有人,所以你不能进去打电话,相当于没有获取自旋锁,这个时候你肯定是站在原地等待,你可能因为无聊的等待而转圈圈消遣时光,反正就是哪里也不能去,要一直等到里面的人打完电话出来。终于,里面的人打完电话出来了,相当于释放了自旋锁,这个时候你就可以使用电话亭打电话了,相当于获取到了自旋锁 。

​ 自旋锁只适用于短时期的轻量级加锁,原因就是等待自旋锁的线程会一直处于自旋状态,会浪费处理器时间,降低系统性能,所以自旋锁的持有时间不能太长 。

**死锁现象:**自旋锁会自动禁止抢占,也就说当线程 A得到锁以后会暂时禁止内核抢占 。线程A在持有锁期间若调用了引起休眠或者阻塞的函数,那么线程A自动放弃CPU使用权,但是线程A休眠没有执行完,就不会解锁,线程B 一直等待A解锁,并且这个时候还禁止内核抢占。一直无法释放锁,那么死锁就发生了。

自锁使用注意:

①、因为在等待自旋锁的时候处于“自旋”状态,因此锁的持有时间不能太长,一定要短,否则的话会降低系统性能。如果临界区比较大,运行时间比较长的话要选择其他的并发处理方式,比如稍后要讲的信号量和互斥体。
②、自旋锁保护的临界区内不能调用任何可能导致线程休眠的 API 函数,否则的话可能导致死锁。
③、不能递归申请自旋锁,因为一旦通过递归的方式申请一个你正在持有的锁,那么你就必须“自旋”,等待锁被释放,然而你正处于“自旋”状态,根本没法释放锁。结果就是自己把自己锁死了!
④、在编写驱动程序的时候我们必须考虑到驱动的可移植性,因此不管你用的是单核的还是多核的 SOC,都将其当做多核 SOC 来编写驱动程序

5.2 信号量

信号量常常用于**控制对共享资源的访问** 。

​ 通俗的解释:比如 A 与 B、 C 合租了一套房子,这个房子只有一个厕所,一次只能一个人使用。某一天早上 A 去上厕所了,过了一会 B 也想用厕所,因为 A 在厕所里面,所以 B 只能等到 A 用来了才能进去。B 要么就一直在厕所门口等着,等 A 出来,这个时候就相当于**自旋锁。 B 也可以告诉 A,让 A 出来以后通知他一下,然后 B 继续回房间睡觉,这个时候相当于信号量。可以看出,使用信号量会提高处理器的使用效率,毕竟不用一直傻乎乎的在那里“自旋”等待。但是,信号量的开销要比自旋锁大,因为信号量使线程进入休眠状态以后会切换线程,切换线程就会有开销。总结一下信号量的特点:
①、因为信号量可以使等待资源线程进入休眠状态,因此
适用于占用资源比较久的场合。
②、因此信号量
不能用于中断**中,因为信号量会引起休眠,中断不能休眠。
③、如果共享资源的持有时间比较短,那就不适合使用信号量了,因为频繁的休眠、切换线程引起的开销要远大于信号量带来的那点优势 。

​ 信号量有一个信号量值,相当于一个房子有 10 把钥匙,这 10 把钥匙就相当于信号量值为10。因此,可以通过信号量来控制访问共享资源的访问数量,如果要想进房间,那就要先获取一把钥匙,信号量值减 1,直到 10 把钥匙都被拿走,信号量值为 0,这个时候就不允许任何人进入房间了,因为没钥匙了。如果有人从房间出来,那他要归还他所持有的那把钥匙,信号量值加 1,此时有 1 把钥匙了,那么可以允许进去一个人。相当于通过信号量控制访问资源的线程数,在初始化的时候将信号量值设置的大于 1,那么这个信号量就是计数型信号量,计数型信号量不能用于互斥访问,因为它允许多个线程同时访问共享资源。

5.3 互斥体—mutex

互斥访问表示一次只有一个线程可以访问共享资源 。

①、mutex 可以导致休眠,因此不能在中断中使用 mutex,中断中只能使用自旋锁。
②、和信号量一样, mutex 保护的临界区可以调用引起阻塞的 API 函数。
③、因为一次只有一个线程可以持有 mutex,因此,必须由 mutex 的持有者释放 mutex。并且 mutex 不能递归上锁和解锁。

5.4 解决并发与竞争

实验目的:实现一次只允许一个 app 来访问 LED 灯,在这个APP访问led期间不允许其他操作,直到这个APP释放设备,才会允许对LED的其他操作。

  • 原子操作实验: 在open 函数中申请原子变量,在release 函数中释放原子变量。

  • 自旋锁实验自旋锁保护的临界区尽可能要短。因此不能和原子实验中一样在open中申请release中释放。思路:可以使用一个变量如 dev_status 表示设备的使用情况,设备被使用变量+1,设备被释放后变量-1,只需要用自旋锁保护这个变量,而不是保护整个是被使用的过程。真正实现设备的互斥访问的是 dev_status 变量,使用自旋锁对这个变量做保护。

  • **信号量实验:**信号量导致休眠,因此 信号量保护的临界区没有运行时间限制。在驱动open函数申请信号量,在release中释放信号量,但是一定要注意 信号量不可以在中断中使用。信号量的头文件为 <linux/semaphore.h>

    image-20220826141216561

6. Linux内核定时器 、中断

6.1 定时器部分

内核定时器基本介绍

​ Linux 内核中有大量的函数需要时间管理,比如周期性的调度程序、延时程序等,最常用的就是定时器。硬件定时器提供时钟源,通过设置时钟源的频率,系统周期性产生定时中断,系统使用定时中断来记时。为 中断周期性产生频率就是系统频率,比如1000Hz。在Linux源码下,利用make menuconfig 在界面下设置,->kernel Features ->Timer frequency([=y]),默认100Hz。设置完在根目录下的 .config 文件找到 CONFIG_HZ = 100

高低时钟频率的优缺点:

​ 关于系统时钟频率:高节拍率会提高系统时间精度 ,比如1000Hz的精度就是 1ms,但是100Hz精度只有10ms;但是高节拍率会导致处理器频繁的处理中断,中断服务函数占用处理器的时间增加。

注意的点:

**内核定时器并不是周期性运行的,超时以后就会自动关闭, **因此如果想实现周期性定时,就需要在定时处理函数中重新开启定时器。

6.2 中断部分

Linux 内核提供了完善的中断框架 ,只需要申请中断然后编写中断服务函数即可,不需要一系列复杂的寄存器配置。

正常的中断流程:
①、使能中断,初始化相应的寄存器。
②、注册中断服务函数,也就是向 irqTable 数组的指定标号处写入中断服务函数
②、中断发生以后进入 IRQ 中断服务函数,在 IRQ 中断服务函数在数组 irqTable 里面查找具体的中断处理函数,找到以后执行相应的中断处理函数

7. 字符驱动开发部分

7.1 复杂驱动的分离与分层

platform 框架下的设备驱动 :为了实现驱动的可重用性,Linux包含了驱动的分离与分层的思路。

image-20220826174056985

7.2 I2C接口驱动

7.2.1 原理部分

一个 I2C 控制器下可以挂多个 I2C 从设备 , 不同的I2C从设备有不同的器件地址,主控制器通过 I2C 设备的器件地址访问指定的 I2C设备

image-20220829152118931

读写数据时序:

image-20220829152313702

7.2.2 I.MX SOC IIC Register

寄存器功能
I2Cx_IADRbit[7:1] 有效,保存I2C从设备地址数据,访问某个从设备时,将设备地址写到ADR中
I2Cx_IFDRbit5:0] ,设置 IC2 的波特率
I2Cx_I2CR控制寄存器,
I2Cx_I2DRbit[7:0]有效,数据寄存器。发送数据时,将数据写入到寄存器,接收数据时,直接读取寄存器中的内容
I2C1_I2SR表征I2C总线的工作状态

实验过程:通过I.MX6U的 I2C1 读取 AP3216C内部的上传感器的值,并在LCD 上显示。

分别 初始化:IO(引脚复用、中断) I2C1(波特率设置) AP3216C

7.2.3 iic-Linux驱动框架

设备驱动的分离与分层

image-20220830090906404

整体流程:

1、在设备树中描述好IIC设备信息

2、构建IIC Driver并注册到IIC总线

3、实现 probe() 函数(申请设备号,实现file_operations、创建设备节点文件、通过IIC的接口去初始化IIC从设备)

  • 修改设备树文件

AP3216C 使用的是 I2C1,其中 I2C1_SCL 使用的 UART4_TXD 这个 IO, I2C1_SDA 使用的是 UART4_R XD 这个 IO。

(1)在 .dtb 文件中添加节点 pinctrl_i2c1 ,将 UART4_TXD 引脚复用为 I2C1_SCL ,电气属性设置为 0x4001b8b0

(2)在 i2c节点中追加 ap3216c 子节点 注意:1e 表示i2c设备的地址,这个地址需要查找芯片手册

image-20220829140433647

修改完之后,在 /sys/bus/i2c/devices 目录下看到 0-001e 的子目录,0-001e 就是 ap3216c 器件的地址,其中有name 文件值ap3216c

器件驱动编写

初始化 i2c 驱动并且向Linux内核注册,当设备和驱动匹配后,i2c_driver 中的的probe 函数就会执行 。

(1)iic总线驱动

Linux 内核将 SOC 的 I2C 适配器(控制器)抽象成 i2c_adapter, i2c_adapter 结构体定义在 include/linux/i2c.h 文件中,完成 iic 与设备之间的通信。这一部分有半导体厂商编写好了,我们中需要专注于iic设备驱动的开发即可。

(2)设备驱动

I2C 设备驱动重点关注两个数据结构: i2c_clienti2c_driver, 作用分别是描述设备信息、描述驱动内容。

i2c_client :一个设备对应与一个i2c_client ,每检测到一个iic设备就会给这个设备分配一个 i2c_client 。

i2c_driver:当 I2C 设备和驱动匹配成功以后 probe 函数就会执行 。对于 I2C 设备驱动编写人来说,重点工作就是构建 i2c_driver,构建完成以后需要向Linux 内核注册这个 i2c_driver。

设备和驱动的匹配过程:drivers/i2c/i2c-core.c 比较 I2C 设备节点的 compatible 属性和 of_device_id 中的 compatible 属性是否相等,如果相当的话就表示 I2C设备和驱动匹配。

(3)数据收发

ap3216c_read_regs() : iic 从ap3216c读取多个寄存器数据,过程:连续发送2条消息,第一条为要读取的寄存器的首地址,第二条为读取的数据以及读取的长度

ap3216c_write_regs():iic向ap3216c多个寄存器写入数据

重点函数分析

  • i2c_transfer() :进行i2c数据的传输。 读写 I2C 设备中的寄存器 (从机地址、传输方向、寄存器地址和长度、数据缓冲区地址和长度)
  • i2c_client: 表示i2c设备,不需要我们自己创建i2c_client,我们一般在设备树里边添加具体的i2c芯片,比如fxls8471,系统在解析设备树时就会知道有这个i2c设备,并创建相应的i2c_client。
  • i2c_driver: 定义 ap3216c_driver
/* i2c驱动结构体 */	
static struct i2c_driver ap3216c_driver = {
    /* 当设备和驱动匹配时,执行此函数。  完成构建设备号、注册设备、 创建类和设备等(字符驱动开发框架)等功能*/
	.probe = ap3216c_probe,  
	.remove = ap3216c_remove,  /*移除设备时执行*/
	.driver = {
                .owner = THIS_MODULE,
                .name = "ap3216c",
                .of_match_table = ap3216c_of_match,  /*使用设备树时,用 of函数查找 匹配*/
		      },
	.id_table = ap3216c_id,  	/*不使用设备树时,传统的 ID 匹配*/
};

7.3 SPI接口驱动

7.3.1 基本原理

驱动开发作用:基于Linux系统内核的基础上,开发 alpha 开发板的驱动,可以实现在应用程序中读取SPI接口传感器的数据。

全称:Serial Perripheral Interface,串行外围设备接口 速度方面:I2C最多400MHz ,而 SPI 可以到达几十 MHz 。

SPI信号线:

image-20220830150003132

工作时序图实例:

image-20220830145530188

image-20220830181035629

7.3.2 驱动框架

  • 主机驱动:SOC 的 SPI 控制器驱动,用 spi_master 结构体表示。

spi_alloc_master()函数申请 spi_master, spi_master_put() 释放spi_master。

spi_register_master () 函数向Linux内核注册spi_master, spi_unregister_master ()函数注销 spi_master。

  • 设备驱动:spi_driver 结构体表示一个设备。

spi_register_driver() 向 Linux 内核注册 注册 spi_driver ,初始化完成以后就要注册

spi_unregister_driver () 注销spi_driver , 注销 SPI 设备驱动以后也需要注销掉前面注册的 spi_driver,

  • 设备和驱动匹配

SPI 设备和驱动的匹配过程是由 SPI 总线来完成的。spi_bus_type 这个结构体中的match函数用来匹配驱动和设备。

7.4 串行通信驱动

7.4.1 基本概念

  • 串行通信:数字信号通过一根数据线一个Bit一个Bit的发送出去
  • Uart通用异步收发器,是一堆电路。电路中有位移寄存器、波特率发生器、缓存器等。 UART 只采用了数据线。数据传输开始的时候transmitter在信号线上有电平跳变,receiveer 感知到电平跳变后开始采样,采样频率一般是波特率的4或16倍。transmitter发送数据的频率称为波特率
  • RS232 / RS485:当串行通信应用在设备与设备之间的时候,由于大多数芯片都是输出TTL电平的,因为TTL的抗噪能力较差,因此采用增加逻辑1和逻辑0之间电压差的方法,如RS232电平:(逻辑1:-5 至 -15V,逻辑0:+5 至 15V)。RS485电平:采用差分信号的形式。所以 RS232或485只是一种接口,定义了接口的外观、各个引脚的功能、信号0和1对应的电压等,他们都属于串行通信的一种。

三条线: TXD RXD GND

image-20220831170940827

电平标准:

  • TTL :一般开发板上都有 TXD 和 RXD 引脚 ,低电平表示逻辑0,高电平表示逻辑1;
  • RS-232:采用差分线, -3~ -15V 表示逻辑 1,+3~ +15V 表示逻辑 0

7.5 驱动框架

要做的工作:在设备树中添加所要使用的串口节点信
息。当系统启动以后串口驱动和设备匹配成功,相应的串口就会被驱动起来,生成
/dev/ttymxcX (X=0….n)文件

uart_driver 结构体:表示一个串口驱动, 在 include/linux/serial_core.h 路径中,用 uart_register_driver() 注册,uart_unregister_driver () 注销

uart_port :表示一个具体的 port ,在 include/linux/serial_core.h 路径下,通过 uart_add_one_port() 将端口和驱动结合起来。uart_remove_one_port ()卸载 。

uart_ops 结构体:是最底层的 uart 驱动接口函数,直接操作 uart 寄存器。

板载硬件:RS-232 / 485 :都连接I.MX6ULL的 uart3 接口上


7.6 USB 驱动

USB 只能主机与设备之间进行数据通信,主机与主机、设备与设备之间是不能通信的。在
一个 USB 系统中,仅有一个 USB 主机,但是可以有多个 USB 设备。

USB OTG :增加一根 ID 线,通过 ID 线电平的高低判断,USB 作为主机(host)还是从机(device),高电平时,OTG设备从机模式,低电平时,主机模式

USB 描述符 :描述 USB 信息(设备描述符、配置描述符、字符串描述符、接口描述符、端口描述符)

8. 编译命令

8.1 GCC 编译器

格式: gcc [选项] [文件名字] arm-linux-gnueabihf-gcc ledApp.c -o ledApp

解释:将 led.c 文件编译为 led 文件,这个 led 类似于 Windows下的exe可执行文件,led文件是在ARM下的可执行文件,可以用./led的命令来执行。

选项解释
-c只编译不链接为可执行文件,编译器将输入的.c 文件编译为.o 的目标文件。
-o<输出文件名>用来指定编译结束以后的输出文件名 ,如果不使用该选项, GCC 默认编译出来的可执行文件名字为 a.out
-g添加调试信息,如果要使用调试工具(如 GDB)的话就必须加入此选项,此选项指示编译的时候生成调试所需的符号信息

9. 字符驱动移植过程

SPI 通讯协议是没有设备地址的,它是通过片信号来寻址的。主机通过不同的片选信号选择需要通信的从机设备。

IIC 通讯协议是有设备地址的(一般是 8位) ,其中最高位是读写标志位,剩下的7位是根据硬件设计来的。

9.1 AHT10 温湿度传感器

输入电压范围:2.3V至3.3V

基于iic 总线模式,温度范围 -40~85℃,精度±0.5℃;湿度范围:0% - 100% 精度为±3%RH

image-20220901084142152

I.MX开发板有4路I2C接口,板载已经使用I2C1接口连接了一个距离传感器AP3216C。自己准备用I2C2接口驱动AHT10温湿度传感器。

修改设备树文件

首先,我想使用I2C2_SCL 和 I2C2_SDA 这两个引脚,需要将 UART5_RX_DATA 复用为I2C2_SDA,UART5_TX_DATA 复用为I2C2_SCL,因此修改引脚的复用和电气属性值为以下:

#define MX6UL_PAD_UART5_TX_DATA__I2C2_SCL                          0x00BC 0x0348 0x05AC 0x2 0x2
#define MX6UL_PAD_UART5_RX_DATA__I2C2_SDA                          0x00C0 0x034C 0x05B0 0x2 0x2
mux_reg    conf_reg      input_reg      mux_mode      input_val

其中,input_reg的值如下:

image-20220901174552838

pinctrl_i2c2: i2c2grp {
			fsl,pins = <
				MX6UL_PAD_UART5_TX_DATA__I2C2_SCL 0x4001b8b0
				MX6UL_PAD_UART5_RX_DATA__I2C2_SDA 0x4001b8b0
			>;
		};

在 i2c2 节点追加 aht10 子节点

查找 aht10 的技术手册,ATH10的器件地址为0x38,然后它的读写指令格式就是:设备地址(7bit)+ SDA方向位(1bit),其中方向位读R:1,写W:0

&i2c2 {
	clock-frequency = <100000>;
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_i2c2>;
	status = "okay";

	aht10@38 {
		compatible = "alientek,aht10";
		reg = <0x38>;
	};
};

验证设备树:

/sys/bus/i2c/devices 目录下有 1-0038 的子目录,38就是设备地址,检查目录下的name文件,确实是自己修改的 ath10

image-20220901142549300

初始化以及读写流程

先发送设备地址的 0x38 的 00111000 低7bit 和 一个写标志位 0,因此发送的8个bit为:0111_0000 = 0x70

分别发送 0x70 0xe1 0x08 0x00 0xac

读数据时,先发送 0x70(写命令)、在发送0xac 0x33 0x00 延时一会,在发送0x71(读命令) ,然后开始读取数据

写数据时,先发送0x70

  • 初始化过程:

先发送 0x71,读取一个byte的状态,通过bit[3]判断,是否已经校准,若未,

发送 E1 08 00,初始化传感器

  • 数据收发

读取数据时,先发送AC 33 00,触发测量,然后MCU等待80ms时间,传感器要采集处理数据,延时后读取6个字节的数据,从后5个字节中解析出温湿度数据来

注意:以上自己都是用的i2c1接口,用i2c接口测试的时候,读写数据都是失败的。

9.2 MS5611传感器

9.2.1 SPI接口

MS5611支持SPI和I2C通信,可以通过上拉PS引脚( Protocol Select)选择I2C协议,下拉PS引脚则选择SPI协议

  • iic模式时,接线如下:

    PS接高电平,复用为iic模式,CSB接地,设备地址为0xee

  • SPI模式下:

    MS5611cpu引脚 (以下是开发板的 SPI3接口)
    VCC3.3v
    GNDGND
    CSB (片选信号,低电平有效)UART2_TX_DATA
    PSGND (下拉ps引脚选择SPI模式)
    SCLK (时钟信号)UART2_RX_DATA
    SDA(SDI)UART2_CTS_B
    SDOUART2_RTS_B

允许工作在SPI的模式0和3,CSB片选引脚用来控制芯片的使能/禁用

执行ADC Read指令后会返回一个24-bit结果,只信你个 PROM read返回一个16-bit结果。

初始化:

重启芯片,然后从PROM读取出厂校准值

启动温度AD转换,读取AD值

启动气压AD转换,读取AD值

计算真实气压和温度值

计算海拔值

参考博客:https://blog.csdn.net/xhj1021/article/details/123863255

https://blog.csdn.net/qq_34430371/article/details/103870968?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-103870968-blog-123863255.pc_relevant_multi_platform_whitelistv4eslandingctr2&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-103870968-blog-123863255.pc_relevant_multi_platform_whitelistv4eslandingctr2&utm_relevant_index=1

9.2.2 IIC接口

接线:

VCC3.3v
GNDGND
CSB (片选信号,低电平有效)GND
PS3.3 V
SCLK (时钟信号)UART4_TX_DATA
SDAUART4_RX_DATA

设备地址:0xee 因为将csb引脚 拉低了。

读取 prom中 的值:

image-20220906172624186

10. 设备驱动

与字符设备的区别:

  • 块设备是以块为单位进行读写访问的,需要数据缓冲(在缓冲区暂存数据,时机成熟后再将缓冲区的数据一次性写入块设备中);
  • 字符设备是以字节为单位进行数据传输的,不需要缓冲;

驱动框架:

  • 用 block_device 结构体表示块设备,定义在include/linux/fs.h 文件中
  • 用 gendisk 结构体来描述一个磁盘设备,定义在 include/linux/genhd.h
    中,
  • 用 block_device_operations 结构体表示块设备的操作集,定义在 include/linux/blkdev.h 中
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

积跬步、至千里

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值