以下内容是自己在学习正点原子Alpha-I.MX开发板基于linux操作系统移植时做的笔记,
包括环境搭建,外接传感器实验等内容。
记于2022.11.24 疫情期间
整个过程总结如下:
整个过程可以注意以下几点:
手头的开发板: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
利用 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卡。
- 下载 VMware和 Ubuntu 的镜像文件,安装虚拟机。
严格按照参考文档,(注意:虚拟机的磁盘空间至少大于50G,最好预留100G的空间),这是官方已经做好的开发环境
- 配置网络环境,能用起 TFTP 和 NFS。
我用的是电脑wifi上网,开发板用网线和电脑直连:
VMnet0 装虚拟机时,配置的网络是桥接模式,这个需要手动分配IP,用于开发板和Ubuntu通信(适配器1)
VMnet8 给虚拟机添加一个网络适配器,配置为NAT模式,供虚拟机上网,这个已经自动分配好IP了(适配器2)
VMnet1 是虚拟机的仅主机模式
要求:在虚拟机中设置 VMnet0 和 Windows下的以太网口在同一个网段下,在开发板中设置开发板的ip和VMnet0在同一网段下。因此,开发板、虚拟机的VMnet0 和 Windows的以太网口 三者都在同一网段下,实现两两互ping
配置完之后的各自的IP地址如下:其中开发板、虚拟机以及Windows的以太网口的IP为手动设置,让他们三个在同一个网段上。
开发板系统IP(手动设置) | 虚拟机Ubuntu的IP(手动设置) | windows网口的IP(手动设置) |
---|---|---|
192.168.10.50 | 192.168.10.100 | 192.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
测试结果:
以下TFTP和NFS在从SD卡启动和从EMMC启动的情况下均测试成功。
TFTP,简单文件传输协议, 客户机与服务器之间进行简单文件传输的协议 ,TFTP 搭建之后测试:
Ubuntu中的路径:/home/alientek/linux/tftp ,开发板可以将Ubuntu中的文件下载下来
NFS,网络文件传输,实现用户访问网络上别处的文件,像访问自己的计算机文件一样 ,NFS搭建好测试结果如下:
Ubuntu中的路径: /home/alientek/linux/nfs
参考资料:【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.6 第三十九章————系统烧写
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-开源电子网
脚本方式固化到 Emmc
脚本方式固化到 SD卡 ,然后从SD卡启动
参考手册,【用户快速体验】,使用脚本固化系统部分,完成SD卡的固化。
测试从SD卡可启动,从EMMC中野可启动,注意串口连接上不打印消息,注意开发板底板上串口跳线帽的选择
U-Boot
、Linux kernel
和 rootfs
这三者一起构成了一个可以正常使用、功能完善的 Linux 系统。
用户快速体验手册: 4.1 安装通用ARM交叉编译工具链 4.2 安装Poky交叉编译工具链
安装后的版本:arm-linux-gnueabihf-gcc --version
4.9.4 arm-poky-linux-gnueabi-gcc --version
5.3.0
- U-Boot编译
编译uboot之后,生成相关文件。.imx文件 是已经添加头部信息的U-boot 镜像,可直接使用 dd 指令烧写到 TF 卡和开发板上的 eMMC 储存设备。.bin文件 是未添加状信息的 U-boot镜像,需要使用【正点原子】 I.MX6U 嵌入式 Linux 驱动开发指南里所说的 imxdownload 工具烧写
- linux内核编译
下载某个版本的linux内核 → 将下载的内核移植到自己的CPU架构上 → 再移植到自己所用的开发板上
编译后,在 arch/arm/boot 目录下生成 Linux 内核 zImage镜像文件,在 arch/arm/boot/dts 下生成很多.dtb 设备树文件 ,还有 modules.tar.bz2(内核模块)
目录 | 目录功能或放置的内容 |
---|---|
/bin | 系统放置了许多执行文件,如 mv、 cp、 date 等指令,这些指令一般是单用户维护 模式使用,还可以被 root 用户使用 |
/boot | boot 目录一般放开机启动文件,但是 I.MX6U 已经有单独一个分区 boot 存储启动 文件(zImage 和设备树 dtb 文件) |
/dev | Linux 系统使用任何设备与接口都是以文件的形式存放在这个目录下,在 Linux 下一切皆文件 |
/etc | 系统配置文件几乎都放在这个目录,例如启动脚本及所有的服务文件等 |
/home | 默认用户主文件夹,默认存在 root 用户。新建的用户都会在此生成用户主文件夹 |
/lib | 系统函数库,及内核模块 modules(驱动程序) |
/mnt | 一般用于临时挂载目录 |
/opt | 第三方软件放置的目录,所以出厂 Qt 应用程序及所用到歌曲歌词等都放在这个目 录下 |
/proc | 此目录是一个虚拟的文件系统,它的数据在内存中,如内核、进程、外部设备和 网络状态等,所以不占磁盘空间 |
/run | 放置系统运行时所需要文件, 以前防止在/var/run 中, 后来拆分成独立的/run 目录。 重启后重新生成对应的目录数据 |
/sbin | 放置 sbin 目录下的指令是系统指令,只有 root 用户可以执行 |
/sys | 与/proc 目录一样,是虚拟的文件系统,记录核心系统的硬件信息 |
/tmp | 存放临时文件目录 |
/usr | Linux 核心目录,目录下包括非常多的文件,如共享文件,一些可执行文件等等 |
/var | 存放系统执行过程经常改变的文件 |
uboot编译烧写完之后,使用 tftp 从 Ubuntu 中下载 zImage 和设备树文件 。
将RT-Thread 内核移植到其他芯片上去,就要在TH-Thread内核中根据要移植的芯片做硬件适配修改
(1)开发板重启,进入uboot环境
开发板重启,3s之内快速按回车键,便可以进入uboot;若不按键,则直接会启动内核。
(2)给uboot设置环境变量
有两种方式,需要哪种就用哪种:ip地址需要根据实际环境更改(以下从种方法设置一次环境变量即可)
确保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
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
格式为:
只烧写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卡启动
烧写成功后从串口看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 处
先在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
以下内容是基于正点原子的 Alpha I.MX 开发板的一些关于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 基础上加入了对自家开发 板的支持 |
解释:每条指令中间的两句中,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 就会寿终正寝。
编译后生成 alientek_uboot.tar.bz2 压缩包,其中包括 u-boot.bin 、 u-boot.imx 文件等等 ,压缩包中的文件解释如下。 u-boot.imx 文件是NXP 的 CPU 专用文件
uboot.lds
文件,是ARM 芯片所使用的 u-boot 链接脚本文件 。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。
信息查询
命令 | 描述 |
---|---|
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操作
剩下的参考手册,在此不给出
链接文件 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
关于汇编部分。。。
直接将NXP官方的uboot烧写到正点原子开发板中发现:
在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 开发板的板级文件夹。
arch/arm/kernel/vmlinux.lds 中指明了了 Linux 内核入口,入口为 stext ,该文件在 arch/arm/kernel/head.S 中
arch/arm/kernel/head.S stext 是 Linux 内核的入口地址
init/main.c 启动内核
buildroot 和 uboot LinuxKernal 类似,需要先下载源码,然后配置相关参数,如设置交叉编译器、设置目标CPU参数等,最重要的是选择需要的第三方软件或库,配置好以后进行编译,编译成功后在一个文件夹存放编译好的结果,也就是根文件系统。
下载源码
官网地址为 https://buildroot.org/ ,下载相应的版本如,buildroot-2019.02.6.tar.bz2
利用 menuconfig配置 buildroot
编译 buildroot
sudo make ,编译完成以后就会在 buildroot-2019.02.6/output/images
下生成根文件系统
解释含义:字符设备驱动就是一个一个字节,按照字节流读写操作的设备,读写数据是分先后顺序的。比如,点灯、按键、IIC、SPI、LCD等都是字符设备,设备的驱动叫做字符设备驱动。
操作过程:如LED驱动加载好以后,会生成 /dev/led
的驱动文件,(/dev是设备目录,/led是具体的驱动的名称)应用程序使用 open() 函数打开文件 dev/led,使用完成后用 close()函数关闭这个文件。open()和close()就是打开和关闭LED驱动的函数。若要点灯,则使用 write()函数操作向驱动写入数据,这个数据就是要关闭还是打开LED的控制参数。若要获取LED灯的状态,则用read()函数从驱动中读取相应的状态。用户空间不能直接对内核进行操作 ,必须通过调用库的方法实现对内核的调用以及底层驱动的访问 。每一个系统调用,在驱动中都有与之对应的一个驱动函数,在 Linux 内核文件 include/linux/fs.h
中 file_operations 的结构体是 Linux 内核驱动操作函数集合。
四步:驱动模块的加载与卸载、字符设备的注册于注销、实现字符设备的具体操作函数、添加 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加载改设备驱动的读写操作,步骤如下:)
modprobe chrdevbase.ko
加载驱动文件,用cat /proc/devices 查看当前系统中所有设备mknod /dev/chrdevbase c 200 0
c 表示是字符设备 200是主设备号 0是次设备号./chrdevbaseApp /dev/chrdevbase 1
读取 ./chrdevbaseApp /dev/chrdevbase 2
写入rmmod chrdevbase.ko
Linux 下的任何外设驱动,最终都是要配置相应的硬件寄存器。
地址映射:对于 32 位的处理器来说,虚拟地址范围是 2^32=4GB,但是开发板的DDR只有512MB (物理地址),经过MMU可以将 512MB的物理内存映射到整个4GB的虚拟内存空间。这个时候肯定存在多个虚拟地址映射到同一个物理地址的问题,这个暂不深究。Linux 内核启动的时候会初始化 MMU,设置好内存映射,设置好以后 CPU 访问的都是虚拟地址 。
**I/O内存:**当外部寄存器或内存映射到内存空间时,称为I/O内存。将寄存器的物理地址映射到虚拟地址后,就可以通过指针直接访问地址
代码的调用关系如下图所示:
编写的APP就是编写linux应用。
makefile 文件代码解释:
旧:使用设备时用 register_chrdev 函数注册字符设备 , 不使用设备时用 unregister_chrdev 函数注销字符设备 ,驱动模块加载成功后需要用 mknod 命令手动创建设备节点。
新:编写新字符设备驱动,并且**在驱动模块加载的时候自动创建设备节点文件**
笔记:.dts文件的路径:arch\arm\boot\dts\imx6ull-alientek-emmc.dts
修改设备树文件时,设备节点最好添加到好区分的地方,一级节点是最好的地方,修改后,DTC 工具源码在 Linux 内核的 scripts/dtc
目录下:编译 .dts 文件的命令是,在Linux源码根目录下,命令make dtbs
设备树是用来**描述板子上的设备信息**的,不同的设备其信息不同,反映到设备树中就是属性不同。
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()
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)对。
一旦硬件发生变化,则需要同步修改设备树文件,因为设备树是描述板子硬件信息的文件。比如 有两个芯片 fxls8471, fxls8471接到了板子的 IIC 接口上,那么就需要在 IIC 这个节点上再追加2个子节点。
imx6ull.dtsi :所有 使用 i.mx6ull 这个SOC 的开发板使用的文件(不能直接修改这个文件,否则会影响使用这个SOC的其他开发板)imx6ull-alientek-emmc.dts :正点原子alpha开发板使用的设备树文件,在这个文件中修改,只影响自己的开发板。追加方法如下:
根节点 ”/“:在 /proc/device-tree 目录下,根节点属性表现为一个个的文件,“compatible”、“model”和“name” 等文件,其内容就是
子节点: 根节点下的子节点在同上目录下,表现为一个个文件夹,“compatible”、“model”和“name” 等,内容就是.dts文件中的信息
根节点:在同上目录下,,表现为一个个文件。
- 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环境变量的值。
之前过程:在驱动文件中 xxx.c 定义寄存器的物理地址 → io_remap 内存映射,得到虚拟地址 → 操作寄存器对应的虚拟地址初始化GPIO
使用设备树之后的过程:
第一步、使用新编译好的imx6ull-alientek-emmc.dtb 启动Linux 内核,启动后在/proc/device-tree/目录中查看“alphaled”节点 。
第二步、在代码中,初始化时获取设备树节点属性,
第三步、利用获取到的reg值,进行寄存器映射,初始化GPIO
结果验证:
(1)修改设备树,添加节点,编译;并将编译成的dtb文件发送到 home/alientek/linux/tftp 目录下。启动开发板,看是否成功添加成功节点(因为我将 alphaled 这个节点添加到了 / 一级子节点下面,所以在 /proc/device-tree/ 目录下可以直接看到这个节点)
(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
测试加载驱动过程
depmod //第一次加载设备使用
modprobe chrdevbase.ko //加载驱动
cat /proc/devices //查看当前系统中有没有 **chrdevbase** 这个设备
lsmod //查看当前系统中存在的模块
rmmod chrdevbase.ko //卸载驱动
pinctrl 子系统:针对 PIN 引脚的配置 drivers/pinctrl
gpio 子系统 :针对 GPIO 的配置
面向对象,驱动分离和分层的思想。一般的开发流程是:首先,修改设备树,添加相应的节点,重点设置 reg 属性。然后,在驱动文件中利用 of 函数获取 reg 属性,进行内存映射并初始化 GPIO (初始化GPIO过程:设备 pin 脚的复用、上下拉、速度等功能,然后设置 GPIO 为输入/输出等模式,即先设置pin脚的功能,然后设置pin引脚对应的GPIO)
pinctrl 子系统主要工作内容如下:
①、获取设备树中 pin 信息。
②、根据获取到的 pin 信息来设置 pin 的复用功能
③、根据获取到的 pin 信息来设置 pin 的电气特性,比如上/下拉、速度、驱动能力等。
这样就可以直接使用这两个子系统来配置GPIO ,简化开发流程
不同的外设使用不同的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
注意: IO配置寄存器整体上分为两种, MUX 寄存器用于配置IO引脚的复用功能; PAD寄存器用于配置IO的电气特性。
用于初始化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,低电平有效*/
};
设置好设备树以后,使用 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脚是否已经被其他外设使用。
编写驱动代码
注意: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.
当一个线程要访问某个共享资源的时候首先要先获取相应的锁, 锁只能被一个线程持有,只要此线程不释放持有的锁,那么其他的线程就不能获取此锁。 对于**自旋锁**而言,如果自旋锁正在被线程 A 持有,线程 B 想要获取自旋锁,那么线程 B 就会处于忙循环-旋转-等待状态,线程 B 不会进入休眠状态或者说去做其他的处理,而是会一直傻傻的在那里“转圈圈”的等待锁可用。
通俗的讲,自旋锁的“自旋”也就是“原地打转”的意思,“原地打转”的目的是为了等待自旋锁可以用,可以访问共享资源。 比如现在有个公用电话亭,一次肯定只能进去一个人打电话,现在电话亭里面有人正在打电话,相当于获得了自旋锁。此时你到了电话亭门口,因为里面有人,所以你不能进去打电话,相当于没有获取自旋锁,这个时候你肯定是站在原地等待,你可能因为无聊的等待而转圈圈消遣时光,反正就是哪里也不能去,要一直等到里面的人打完电话出来。终于,里面的人打完电话出来了,相当于释放了自旋锁,这个时候你就可以使用电话亭打电话了,相当于获取到了自旋锁 。
自旋锁只适用于短时期的轻量级加锁,原因就是等待自旋锁的线程会一直处于自旋状态,会浪费处理器时间,降低系统性能,所以自旋锁的持有时间不能太长 。
**死锁现象:**自旋锁会自动禁止抢占,也就说当线程 A得到锁以后会暂时禁止内核抢占 。线程A在持有锁期间若调用了引起休眠或者阻塞的函数,那么线程A自动放弃CPU使用权,但是线程A休眠没有执行完,就不会解锁,线程B 一直等待A解锁,并且这个时候还禁止内核抢占。一直无法释放锁,那么死锁就发生了。
自锁使用注意:
①、因为在等待自旋锁的时候处于“自旋”状态,因此锁的持有时间不能太长,一定要短,否则的话会降低系统性能。如果临界区比较大,运行时间比较长的话要选择其他的并发处理方式,比如稍后要讲的信号量和互斥体。
②、自旋锁保护的临界区内不能调用任何可能导致线程休眠的 API 函数,否则的话可能导致死锁。
③、不能递归申请自旋锁,因为一旦通过递归的方式申请一个你正在持有的锁,那么你就必须“自旋”,等待锁被释放,然而你正处于“自旋”状态,根本没法释放锁。结果就是自己把自己锁死了!
④、在编写驱动程序的时候我们必须考虑到驱动的可移植性,因此不管你用的是单核的还是多核的 SOC,都将其当做多核 SOC 来编写驱动程序
信号量常常用于**控制对共享资源的访问** 。
通俗的解释:比如 A 与 B、 C 合租了一套房子,这个房子只有一个厕所,一次只能一个人使用。某一天早上 A 去上厕所了,过了一会 B 也想用厕所,因为 A 在厕所里面,所以 B 只能等到 A 用来了才能进去。B 要么就一直在厕所门口等着,等 A 出来,这个时候就相当于**自旋锁。 B 也可以告诉 A,让 A 出来以后通知他一下,然后 B 继续回房间睡觉,这个时候相当于信号量。可以看出,使用信号量会提高处理器的使用效率,毕竟不用一直傻乎乎的在那里“自旋”等待。但是,信号量的开销要比自旋锁大,因为信号量使线程进入休眠状态以后会切换线程,切换线程就会有开销。总结一下信号量的特点:
①、因为信号量可以使等待资源线程进入休眠状态,因此适用于占用资源比较久的场合。
②、因此信号量不能用于中断**中,因为信号量会引起休眠,中断不能休眠。
③、如果共享资源的持有时间比较短,那就不适合使用信号量了,因为频繁的休眠、切换线程引起的开销要远大于信号量带来的那点优势 。
信号量有一个信号量值,相当于一个房子有 10 把钥匙,这 10 把钥匙就相当于信号量值为10。因此,可以通过信号量来控制访问共享资源的访问数量,如果要想进房间,那就要先获取一把钥匙,信号量值减 1,直到 10 把钥匙都被拿走,信号量值为 0,这个时候就不允许任何人进入房间了,因为没钥匙了。如果有人从房间出来,那他要归还他所持有的那把钥匙,信号量值加 1,此时有 1 把钥匙了,那么可以允许进去一个人。相当于通过信号量控制访问资源的线程数,在初始化的时候将信号量值设置的大于 1,那么这个信号量就是计数型信号量,计数型信号量不能用于互斥访问,因为它允许多个线程同时访问共享资源。
互斥访问表示一次只有一个线程可以访问共享资源 。
①、mutex 可以导致休眠,因此不能在中断中使用 mutex,中断中只能使用自旋锁。
②、和信号量一样, mutex 保护的临界区可以调用引起阻塞的 API 函数。
③、因为一次只有一个线程可以持有 mutex,因此,必须由 mutex 的持有者释放 mutex。并且 mutex 不能递归上锁和解锁。
实验目的:实现一次只允许一个 app 来访问 LED 灯,在这个APP访问led期间不允许其他操作,直到这个APP释放设备,才会允许对LED的其他操作。
原子操作实验: 在open 函数中申请原子变量,在release 函数中释放原子变量。
自旋锁实验:自旋锁保护的临界区尽可能要短。因此不能和原子实验中一样在open中申请release中释放。思路:可以使用一个变量如 dev_status 表示设备的使用情况,设备被使用变量+1,设备被释放后变量-1,只需要用自旋锁保护这个变量,而不是保护整个是被使用的过程。真正实现设备的互斥访问的是 dev_status 变量,使用自旋锁对这个变量做保护。
**信号量实验:**信号量导致休眠,因此 信号量保护的临界区没有运行时间限制。在驱动open函数申请信号量,在release中释放信号量,但是一定要注意 信号量不可以在中断中使用。信号量的头文件为
内核定时器基本介绍
Linux 内核中有大量的函数需要时间管理,比如周期性的调度程序、延时程序等,最常用的就是定时器。硬件定时器提供时钟源,通过设置时钟源的频率,系统周期性产生定时中断,系统使用定时中断来记时。为 中断周期性产生频率就是系统频率,比如1000Hz。在Linux源码下,利用make menuconfig 在界面下设置,->kernel Features ->Timer frequency([=y]),默认100Hz。设置完在根目录下的 .config
文件找到 CONFIG_HZ = 100
。
高低时钟频率的优缺点:
关于系统时钟频率:高节拍率会提高系统时间精度 ,比如1000Hz的精度就是 1ms,但是100Hz精度只有10ms;但是高节拍率会导致处理器频繁的处理中断,中断服务函数占用处理器的时间增加。
注意的点:
**内核定时器并不是周期性运行的,超时以后就会自动关闭, **因此如果想实现周期性定时,就需要在定时处理函数中重新开启定时器。
Linux 内核提供了完善的中断框架 ,只需要申请中断然后编写中断服务函数即可,不需要一系列复杂的寄存器配置。
正常的中断流程:
①、使能中断,初始化相应的寄存器。
②、注册中断服务函数,也就是向 irqTable 数组的指定标号处写入中断服务函数
②、中断发生以后进入 IRQ 中断服务函数,在 IRQ 中断服务函数在数组 irqTable 里面查找具体的中断处理函数,找到以后执行相应的中断处理函数
platform 框架下的设备驱动 :为了实现驱动的可重用性,Linux包含了驱动的分离与分层的思路。
一个 I2C 控制器下可以挂多个 I2C 从设备 , 不同的I2C从设备有不同的器件地址,主控制器通过 I2C 设备的器件地址访问指定的 I2C设备
读写数据时序:
寄存器 | 功能 |
---|---|
I2Cx_IADR | bit[7:1] 有效,保存I2C从设备地址数据,访问某个从设备时,将设备地址写到ADR中 |
I2Cx_IFDR | bit5:0] ,设置 IC2 的波特率 |
I2Cx_I2CR | 控制寄存器, |
I2Cx_I2DR | bit[7:0]有效,数据寄存器。发送数据时,将数据写入到寄存器,接收数据时,直接读取寄存器中的内容 |
I2C1_I2SR | 表征I2C总线的工作状态 |
实验过程:通过I.MX6U的 I2C1 读取 AP3216C内部的上传感器的值,并在LCD 上显示。
分别 初始化:IO(引脚复用、中断) I2C1(波特率设置) AP3216C
设备驱动的分离与分层
整体流程:
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设备的地址,这个地址需要查找芯片手册
修改完之后,在 /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_client
和 i2c_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驱动结构体 */
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 匹配*/
};
驱动开发作用:基于Linux系统内核的基础上,开发 alpha 开发板的驱动,可以实现在应用程序中读取SPI接口传感器的数据。
全称:Serial Perripheral Interface,串行外围设备接口 速度方面:I2C最多400MHz ,而 SPI 可以到达几十 MHz 。
SPI信号线:
工作时序图实例:
- 主机驱动: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函数用来匹配驱动和设备。
三条线: TXD RXD GND
电平标准:
要做的工作:在设备树中添加所要使用的串口节点信
息。当系统启动以后串口驱动和设备匹配成功,相应的串口就会被驱动起来,生成
/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 接口上
USB 只能主机与设备之间进行数据通信,主机与主机、设备与设备之间是不能通信的。在
一个 USB 系统中,仅有一个 USB 主机,但是可以有多个 USB 设备。
USB OTG :增加一根 ID 线,通过 ID 线电平的高低判断,USB 作为主机(host)还是从机(device),高电平时,OTG设备从机模式,低电平时,主机模式
USB 描述符 :描述 USB 信息(设备描述符、配置描述符、字符串描述符、接口描述符、端口描述符)
格式: 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)的话就必须加入此选项,此选项指示编译的时候生成调试所需的符号信息 |
SPI 通讯协议是没有设备地址的,它是通过片信号来寻址的。主机通过不同的片选信号选择需要通信的从机设备。
IIC 通讯协议是有设备地址的(一般是 8位) ,其中最高位是读写标志位,剩下的7位是根据硬件设计来的。
输入电压范围:2.3V至3.3V
基于iic 总线模式,温度范围 -40~85℃,精度±0.5℃;湿度范围:0% - 100% 精度为±3%RH
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的值如下:
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
初始化以及读写流程
先发送设备地址的 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接口测试的时候,读写数据都是失败的。
MS5611支持SPI和I2C通信,可以通过上拉PS引脚( Protocol Select)选择I2C协议,下拉PS引脚则选择SPI协议
iic模式时,接线如下:
PS接高电平,复用为iic模式,CSB接地,设备地址为0xee
SPI模式下:
MS5611 | cpu引脚 (以下是开发板的 SPI3接口) |
---|---|
VCC | 3.3v |
GND | GND |
CSB (片选信号,低电平有效) | UART2_TX_DATA |
PS | GND (下拉ps引脚选择SPI模式) |
SCLK (时钟信号) | UART2_RX_DATA |
SDA(SDI) | UART2_CTS_B |
SDO | UART2_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
接线:
VCC | 3.3v |
---|---|
GND | GND |
CSB (片选信号,低电平有效) | GND |
PS | 3.3 V |
SCLK (时钟信号) | UART4_TX_DATA |
SDA | UART4_RX_DATA |
设备地址:0xee 因为将csb引脚 拉低了。
读取 prom中 的值:
与字符设备的区别:
驱动框架: