一、安装Vmware16
1.1、Vmware16的下载
1.2、Vmware16的安装

二、安装Ubuntu20
2.1、Ubuntu20的下载
2.2、Ubuntu20的安装
1、新建一个文件夹并把下载好的Ubuntu20镜像放在里面
2、打开Vmware并新建虚拟机
2.3、首次设置root密码
如果想二次修改密码,使用命令:
sudo passwd root //修改root用户密码
sudo passwd haut //修改haut用户密码
sudo passwd 用户名 //.......
2.4、安装vmware tools
工具能实现界面自动分辨率调整、主机与虚拟机之间拖拉文件等功能,是虚拟机必不可少的软件目前新版本的系统镜像都有自带的open-vm-tools工具,老版本没有,总之:如果系统已经自带open-vm-tools且可以自动分辨率调整,就不需要安装vmware tools的,否则,需要安装后者。
(Ubuntu16、Ubuntu18不带open-vm-tools,需要用方式一进行安装)
方式一、安装vmware自带的vmware-tools工具(此方式将不再适合Ubuntu18以上的版本)
说明:安装vmware-tools后尽量不要更新(能自动调整屏幕就行、传输文件要习惯用SSH传输)
方式二、安装open-vm-tools代替vmware-tools(适合Ubuntu20及以上且不带open-vm-tools的版本)
# 一、确保当前系统未安装有vmware-tools和open-vm-tools
sudo vmware-uninstall-tools.pl
sudo apt autoremove open-vm-tools
# 二、安装open-vm-tools
sudo apt install open-vm-tools
sudo apt install open-vm-tools-desktop
sudo reboot
三、配置Ubuntu20
3.1、设置虚拟终端光标
3.2、关闭系统自动更新
如果想手动更新系统,可以在终端中执行如下命令:
sudo apt update //获取最新版本的软件包列表(仅是列表)
sudo apt list --upgradable //列出哪些软件可以被更新(哪些软件出了新版本)
sudo apt upgrade //下载需要更新的软件包,并更新所有可以被更新的软件
3.3、安装文本编辑器vim
3.4、安装程序编译器GCC
3.5、安装visual studio code
我们需要安装的插件有下面几个:
1)、 C/C++,必须。
2)、 C/C++ Snippets,代码重构
3)、 C/C++ Advanced Lint,静态检测 。
4)、 Code Runner,代码运行。
5)、 Include AutoComplete,头文件自动包含。
6)、 Rainbow Brackets,彩虹花括号。
7)、 One Dark Pro, VSCode 的主题。
8)、 GBKtoUTF8,将 GBK 转换为 UTF8。
9)、 ARM,ARM 汇编语法高亮显示。
10)、 Chinese(Simplified),中文环境。
11)、 vscode-icons, VSCode 图标插件,主要是资源管理器下各个文件夹的图标。
12)、 compareit,比较插件,可以用于比较两个文件的差异。
13)、 DeviceTree,设备树语法插件。
14)、 TabNine,一款 AI 自动补全插件,强烈推荐,谁用谁知道!
其它:这里重点说一下两个常用的快捷键(可以设置成如下快捷键)
返回/前进:Alt + <- 和Alt + ->
注意:在界面顶部的中间,也有这两个快捷的按钮
vs code的相关配置繁琐而又复杂,相关配置在另一个文章中有详细介绍,这里不再赘述。
3.6、安装SSH远程服务
SSH服务是远程控制服务,允许远程客户端用网络的方式登录主机,安装好之后,我们就可以在 Windwos下使用终端软件登陆到Ubuntu20。(终端软件推荐使用MobaXterm)
若安装出问题请执行命令行:sudo apt-get update (获取最新的应用列表)
MobaXterm是一个超级终端工具之一,其他的终端软件也可以,想获取安装包可以取官网下载社区版,或者加入QQ群649692007,在群文件免费获取**版本。
===========================>常用命令列举如下<===========================
sudo apt-get update //系统更新
sudo apt-get install openssh-server //安装SSH服务端
sudo apt-get install openssh-client //安装SSH客户端
sudo /etc/init.d/ssh start //启动SSH
sudo /etc/init.d/ssh stop //关闭SSH
sudo /etc/init.d/ssh status //查看status状态
sudo /etc/init.d/ssh restart //重启SSH
ps -e|grep ssh //ps -e:查看进程,grep ssh:搜索ssh
3.7、设置网络静态IP地址
图中的相关命令如下:
1、sudo vim /etc/netplan/01-network-manager-all.yaml
2、sudo systemctl stop NetworkManager
3、sudo systemctl restart networking
=>网络配置文件01-network-manager-all.yaml添加的内容
ethernets:
ens33:
dhcp4: no
dhcp6: no
addresses: [192.168.1.116/24]
optional: true
gateway4: 192.168.1.1
nameservers:
addresses: [192.168.1.1, 114.114.114.114]
四、开发IMX6ULL
学习嵌入式linux最好选择一款开发板来学习,这样能更好的接触底层硬件的工作原理,目前市面上教学平台的 开发板的CPU型号有IMX6ULL(单核A7+32bit)、STM32MP157(双核A7+单核M4+32bits)、RK3399(双A72大核+四A53小核+64bit)等。前两款适合底层驱动学习,后者适合上层应用开发。(=>这里我选择imx6ull系列的开发板作为linux的学习平台<=)
4.01、IMX6ULL处理器简介
NXP的IMX6ULL系列芯片是一款基于ARM Cortex-A7内核的低功耗高性能且低成本的应用处理器,处理器的内部功能框图和CPU丝印命名规则如下:
4.02、IMX6ULL开发板选择
4.03、IMX6ULL启动方式解析
(1)imx6ull的几种启动方式:
IMX6ULL支持多种启动方式以及启动设备,比如可以从SD/EMMC、NAND Flash、QSPI Flash等启动。用户可以根据实际情况,选择合适的启动设备。不同的启动方式其启动方式和启动要求也不一样。
IMX6ULL有四个BOOT模式,这四个BOOT模式由BOOT_MODE[1:0]来控制,也就是BOOT_MODE1 和 BOOT_MODE0 这两 IO,BOOT 模式配置如表所示:
BOOT_MODE[1:0]分别是两个IO口,这两个IO口接在了拨码开关1-2上,由拨码开关控制。串行下载的意思就是可以通过 USB 或者 UART 将代码下载到板子上的外置存储设备中,我们可以使用 OTG1这个USB口向开发板上的 SD/EMMC、NAND等存储设备下载代码。内部BOOT模式是指CPU执行内部bootROM代码,这段BootROM代码会进行硬件初始化(一部分外设),然后从 boot 设备(就是存放代码的设备、比如 SD/EMMC、 NAND)中将代码拷贝出来复制到指定的 RAM 中,一般是 DDR。
对于内部BOOT模式启动,BootROM代码都干了什么?:把时钟打开、读取相关引脚电平确定去哪个存储设备读取用户代码、读取存储设备中的用户代码到内部的RAM中去。读取尺寸如图: 以eMMC为例,BootROM程序先把这4KB的数据(IVT表+BootData+DCD表)读取到内存中去,然后根据这4KB的数据来初始化设备,从这4KB数据中,BootROM程序就知道了把真正的bin程序放置到哪里去。
mfgtools_for_6ULL工具是官方提供的串口烧写软件,其主要工作方式是:首先把一个定制的linux内核加载到DDR中,并运行此内核,然后通过命令把文件放入到指定的位置(包括分区)。把拨码开关设置成USB串行启动,上电后即可打开软件进行下载。前两个开关用来切换下载与运行,后六个开关用来设置用哪个存储设备启动<一般都是前两个开关频繁使用>,如下图示:
(2)裸机程序与emmc:
由(1)可知,要想让芯片自带的bootROM程序准确的启动我们用户的bin程序,就需要在bin程序添加头部信息,故完整可下载的程序为:IVT表+BootData+DCD表+用户bin,uboot本身也是一个裸机程序,也需要添加头部信息后才能下载到emmc flash中。
首先,关于EMMC FLASH,需要先大概了解emmc的物理分区:
=>分为四个区:Boot Area Partitions、RPMB Partition、General Purpose Partitions和User Data Area。
Boot Area Partitions:主要用来存放bootloader(分区1和分区2可以看成两个完全一致的分区)。
RPMB Partition:未使用。
General Purpose Partitions:未使用。
User Data Area:主要用来存放linux内核和rootfs
其次,关于mfgtools_for_6ULL工具,我们来看一下它是如何对emmc进行分区的:
分区文件shell文件:.mfgtools_for_6ULL\Profiles\Linux\OS Firmware\mksdcard.sh
#!/bin/sh
# partition size in MB
BOOT_ROM_SIZE=10
# call sfdisk to create partition table
# destroy the partition table
node=$1
dd if=/dev/zero of=${node} bs=1024 count=1 # 清除前1k数据
sfdisk --force -uM ${node} << EOF
${BOOT_ROM_SIZE},500,0c # 分区1从10M开始,大小为500M,0c为文件系统类型的代码
600,,83 # 分区2从600M开始,大小为剩余空间,83为文件系统类型的代码
EOF
可见,分区是从10M开始的(用户可设定,但要跳过前几个分区,从用户区开始),前10M用于存放裸机(uboot)程序,其实就是让裸机程序位于Boot Aera partition,这部分比较安全,多数型号的EMMC(Boot1=4M,Boot2=4M)。 说明:EMMC的前几个区即使未经过分区,内核驱动也是可以识别的,但用户区若不分区则识别不了。
4.04、IMX6ULL开发目录创建
在Ubuntu20的虚拟机上配置环境,我创建的用户名为haut,在haut的用户目录下创建一个名为itop_imx6ull的目录,之后所有的开发文件和软件都放在此目录下:
4.05、Ubuntu20上配置tftp服务
tftp是一个简单的基于udp的文本文件传输协议,我们用它将内核镜像和设备树下载到开发板内存中,并指定地址,只在Ubuntu20虚拟机上配置好tftp服务器即可。
参考4.4,在 /home/haut/itop_imx6ull 目录下创建 tftp 的根目录:
1、mkdir tftp_boot # 创建tftp服务的根目录
2、chmod 777 tftp_boot # 修改文件夹权限
3、sudo apt-get install tftp-hpa tftpd-hpa # 安装tftp服务
4、sudo vim /etc/default/tftpd-hpa # 修改tftp配置文件
5、sudo service tftpd-hpa restart # 重启tftp服务
4.06、Ubuntu20上配置nfs服务
我们将开发板的文件系统放在 PC 端(Ubuntu20),开发板的文件系统类型设置为 nfs, 就可以挂载文件系统了。具体步骤(在 Ubuntu 上操作)。
参考4.4,在 /home/haut/itop_imx6ull 目录下创建 nfs 的根目录:
1、mkdir nfs_rootfs # 创建nfs服务的根目录
2、chmod 777 nfs_rootfs # 修改文件夹权限
3、sudo apt-get install nfs-kernel-server # 安装nfs服务
4、sudo vim /etc/exports # 修改nfs配置文件、在文件末尾添加如下行
5、 sudo service nfs-kernel-server restart # 重启tftp服务
sudo /etc/init.d/nfs-kernel-server restart
rw:读写访问 |no_wdelay:如果多个用户要写入NFS目录,则立即写入,当使用async时,无需此设置。
sync:所有数据在请求时写入共享 |no_hide:共享NFS目录的子目录
async:NFS在写入数据前可以相应请求 |subtree_check:如果共享/usr/bin之类的子目录时,强制NFS检查父目录的权限
secure:NFS通过1024以下的安全TCP/IP端口发送 |no_subtree_check:和上面相对,不检查父目录权限
insecure:NFS通过1024以上的端口发送 |all_squash:共享文件的UID和GID映射匿名用户anonymous,适合公用目录。
wdelay:如果多个用户写入NFS目录,则归组写入(默认)|no_all_squash:保留共享文件的UID和GID
root_squash root 用户的所有请求映射成如 anonymous 用户一样的权限 no_root_squas root 用户具有根目录的完全管理访问权限
说明:下面4.7要用到4.5和4.6的配置。
4.07、IMX6ULL用Uboot启动内核
(1)使用uboot配置开发板的网络参数:
setenv ethact FEC1 # 网卡选择
setenv ipaddr 192.168.1.15 # 设置网卡IP地址 (视情况而定)
setenv serverip 192.168.1.16 # 设置服务器IP地址 (视情况而定)
setenv netmask 255.255.255.0 # 网络子网掩码
setenv gatewayip 192.168.1.1 # 局域网网关地址
setenv ethaddr 08:07:03:A0:03:02 # 设置网卡MAC地址
saveenv # 保存环境变量到Flash
########################### 以上是注释版、完整的命令如下 ############################
setenv ethact FEC1
setenv ipaddr 192.168.1.15
setenv serverip 192.168.1.16
setenv netmask 255.255.255.0
setenv gatewayip 192.168.1.1
setenv ethaddr 08:07:03:A0:03:02
saveenv
(2)设置传递给内核的参数bootargs:
# =>不同启动方式的bootargs配置不同,以nfs网络文件系统的为例:
setenv bootargs 'console=ttymxc0,115200 # 表示终端为ttymxc0,串口波特率为115200
root=/dev/nfs # 告诉内核以nfs启动
rw # 文件系统操作权限
nfsroot=${serverip}:/home/haut/itop_imx6ull/nfs_rootfs # nfs的根目录的绝对地址
ip=${ipaddr}:${serverip}:${gatewayip}:${netmask}::eth0:off' # 本地地址:服务器地址:网关:子网掩码::eth0:off
saveenv # 保存设置的环境变量
########################### 以上是注释版、完整的命令如下 ############################
setenv bootargs console=ttymxc0,115200 root=/dev/nfs rw nfsroot=192.168.1.16:/home/haut/itop_imx6ull/nfs_rootfs ip=192.168.1.15:192.168.1.16:192.168.1.1:255.255.255.0::eth0:off
saveenv
(3)配置Uboot启动时自动执行的命令:
# =>不同内核加载方式的bootcmd配置不同,以tftp命令加载内核为例:
setenv bootcmd 'tftp 80800000 zImage; # 加载kernel到DRAM
tftp 83800000 topeet_emmc_4_3.dtb; # 加载设备树到DRAM
bootz 80800000 - 83800000' # 启动内核<内核地址>+<设备树地址>
saveenv # 保存设置的环境变量
########################### 以上是注释版、完整的命令如下 ############################
setenv bootcmd 'tftp 80800000 zImage; tftp 83800000 imx6ull_itop_emmc_4_3.dtb; bootz 80800000 - 83800000'
saveenv
(@)内核启动说明与总结:
加载内核和设备树到DDR是很简单的操作,也好理解。最重要的是最后一步的bootz命令,传递bootargs参数给内核也是靠它来完成的,相对较复杂,bootz的执行流程图如下:
启动流程总结:将内核和设备树移到DDR中===>校验内核===>传递参数===>跳转执行内核
4.08、Ubuntu20上安装交叉编译器
1、 arm 表示这是编译 arm 架构代码的编译器。
2、 linux 表示运行在 linux 环境下。
3、 gnueabihf 表示嵌入式二进制接口。
4、 gcc 表示是 gcc 工具。
4.09、Uboot2016-NXP启动流程
4.09.1、Cortex-A7 MPCore
以前的ARM处理器有7种运行模型:User、FIQ、IRQ、Supervisor(SVC)、Abort、Undef和System,其中
User是非特权模式,其余6中都是特权模式。但新的Cortex-A架构加入了TrustZone安全扩展,所以就新加了一种
运行模式:Monitor,新的处理器架构还支持虚拟化扩展,因此又加入了另一个运行模式:Hyp,所以Cortex-A7处理
器有9种处理模式:
---------------------------------------------------------------------------------
模式 描述
---------------------------------------------------------------------------------
User(USR) 用户模式,非特权模式,大部分程序运行的时候就处于此模式。
FIQ 快速中断模式,进入 FIQ 中断异常
IRQ 一般中断模式。
Supervisor(SVC) 超级管理员模式,特权模式,供操作系统使用。
Monitor(MON) 监视模式?这个模式用于安全扩展模式。
Abort(ABT) 数据访问终止模式,用于虚拟存储以及存储保护。
Hyp(HYP) 超级监视模式?用于虚拟化扩展。
Undef(UND) 未定义指令终止模式。
System(SYS) 系统模式,用于运行特权级的操作系统任务
---------------------------------------------------------------------------------
除了 User(USR)用户模式以外,其它 8 种运行模式都是特权模式。这几个运行模式可以通过软件进行任意
切换,也可以通过中断或者异常来进行切换。大多数的程序都运行在用户模式,用户模式下是不能访问系统所有资
源的,有些资源是受限的,要想访问这些受限的资源就必须进行模式切换。但是用户模式是不能直接进行切换的,
用户模式下需要借助异常来完成模式切换,当要切换模式的时候,应用程序可以产生异常,在异常的处理过程中完
成处理器模式切换。
当中断或者异常发生以后,处理器就会进入到相应的异常模式种,每一种模式都有一组寄存器供异常处理程
序使用,这样的目的是为了保证在进入异常模式以后,用户模式下的寄存器不会被破坏。
如果学过 STM32 和 UCOS、 FreeRTOS 就会知道, STM32 只有两种运行模式,特权模式和非特权模式,
但是 Cortex-A 就有 9 种运行模式。
4.09.2、Cortex-A7寄存器组
Cortex-A7 有 9 种运行模式,每一种运行模式都有一组与之对应的寄存器组。每一种模式可见的寄存器包括 15 个通用寄存器(R0~R14)、一两个程序状态寄存器和一个程序计数器 PC。在这些寄存器中,有些是所有模式所共用的同一个物理寄存器,有一些是各模式自己所独立拥有的:
图中浅色字体的是与 User 模式所共有的寄存器,蓝绿色背景的是各个模式所独有的寄存器。可以看出,在所有的模式中,低寄存器组(R0~R7)是共享同一组物理寄存器的,只是一些高寄存器组在不同的模式有自己独有的寄存器,比如 FIQ 模式下 R8~R14 是独立的物理寄存器。假如某个程序在 FIQ 模式下访问 R13 寄存器,那它实际访问的是寄存器 R13_fiq,如果程序处于 SVC 模式下访问 R13 寄存器,那它实际访问的是寄存器 R13_svc。下面介绍几个重要寄存器:
SP => 堆栈指针 => 指向堆栈栈顶(堆栈可能向上增长,也可能向下增长)
LR => 链接寄存器 => 如果使用BL或者BLX来调用子函数的话,R14(LR)被设置成该子函数的返回地址
PC => 程序计数器 => (PC)值=当前执行的程序位置+8个字节
CPSR => 当前程序状体寄存器
SPSR => 备份程序状态寄存器
4.09.3、Cortex-A7常用汇编
<1>使用处理器做的最多事情就是在处理器内部来回的传递数据,常见的操作有:
①、将数据从一个寄存器传递到另外一个寄存器。
②、将数据从一个寄存器传递到特殊寄存器,如 CPSR 和 SPSR 寄存器。
③、将立即数传递到寄存器
数据传输常用的指令有三个: MOV、MRS和MSR。
指令 目的 源 描述
MOV R0, R1 将R1里面的数据复制到 R0 中,R0=R1。
MOV R0, #0x12 将立即数0x12传递给 R0 中,R0=0x12。
MRS R0, CPSR 将特殊寄存器 CPSR 里面的数据复制到 R0 中。(涉及特殊寄存器)
MSR CPSR, R1 将 R1 里面的数据复制到特殊寄存器 CPSR 里中。(涉及特殊寄存器)
<2>ARM 不能直接访问存储器,比如 RAM 中的数据,需要借助存储器访问指令,一般先将要配置的值
写入到 Rx(x=0~12)寄存器中,然后借助存储器访问指令将 Rx 中的数据写入到存储器中,读取时过程相反
指令 目的 源 描述
LDR Rd, [Rn, #offset] 从存储器 Rn+offset 的位置读取数据存放到 Rd 中。
STR Rd, [Rn, #offset] 将 Rd 中的数据写入到存储器中的 Rn+offset 位置。
示例1:
LDR R0, =0X0209C004 @将寄存器地址 0X0209C004 加载到 R0 中,即 R0=0X0209C004
LDR R1, [R0] @读取地址 0X0209C004 中的数据到 R1 寄存器中
示例2:
LDR R0, =0X0209C004 @(伪指令)将寄存器地址 0X0209C004 加载到 R0 中,即 R0=0X0209C004
LDR R1, =0X20000002 @R1 保存要写入到寄存器的值,即 R1=0X20000002
STR R1, [R0] @将 R1 中的值写入到 R0 中所保存的地址中
LDR 和 STR 都是按照字进行读取和写入的,也就是操作的 32 位数据,如果要按照字节、半字进行操作的话可以在指令“LDR”后面加上 B 或 H,比如按字节操作的指令就是 LDRB 和STRB,按半字操作的指令就是 LDRH 和STRH。
<3>我们通常会在A函数中调用B函数,当B函数执行完以后再回到A函数继续执行.要想再跳回A函数以后
代码能够接着正常运行,那就必须在跳到B函数之前将当前处理器状态保存起来(就是保存 R0~R15这些
寄存器值),当B函数执行完成以后再用前面保存的寄存器值恢复 R0~R15 即可。保存 R0~R15 寄存器
的操作就叫做现场保护,恢复 R0~R15 寄存器的操作就叫做恢复现场。在进行现场保护的时候需要进
行压栈(入栈)操作,恢复现场就要进行出栈操作。压栈的指令为 PUSH,出栈的指令为POP,PUSH 和POP
是一种多存储和多加载指令,即可以一次操作多个寄存器数据,他们利用当前的栈指针 SP 来生成地址.
PUSH <reg list> @ 将寄存器列表存入栈中。
POP <reg list> @ 从栈中恢复寄存器列表。
示例1:XX指下一个地址
PUSH {R0~R3, R12} @将 R0~R3 和 R12压栈 =>XX、R12、R3、R2、R1、R0、XX(<-SP)
PUSH {LR} @将 LR 进行压栈 =>XX、R12、R3、R2、R1、R0、LR、XX(<-SP)
======
POP {LR} @先恢复 LR =>XX、R12、R3、R2、R1、R0、XX(<-SP)
POP {R0~R3,R12} @在恢复 R0~R3,R12 =>XX(<-SP)
<4>算术运算指令,汇编中也可以进行算术运算,比如加减乘除。
指令 计算公式 备注
ADD Rd, Rn, Rm Rd = Rn + Rm 加法运算,指令为 ADD
ADD Rd, Rn, #immed Rd = Rn + #immed 加法运算,指令为 ADD
------------------------------------------------------------------------
ADC Rd, Rn, Rm Rd = Rn + Rm + 进位 带进位的加法运算,指令为 ADC
ADC Rd, Rn, #immed Rd = Rn + #immed + 进位 带进位的加法运算,指令为 ADC
------------------------------------------------------------------------
SUB Rd, Rn, Rm Rd = Rn - Rm 减法
SUB Rd, #immed Rd = Rd - #immed 减法
SUB Rd, Rn, #immed Rd = Rn - #immed 减法
------------------------------------------------------------------------
SBC Rd, Rn, #immed Rd = Rn - #immed - 借位 带借位的减法
SBC Rd, Rn ,Rm Rd = Rn - Rm - 借位 带借位的减法
------------------------------------------------------------------------
MUL Rd, Rn, Rm Rd = Rn * Rm 乘法(32 位)
UDIV Rd, Rn, Rm Rd = Rn / Rm 无符号除法
SDIV Rd, Rn, Rm Rd = Rn / Rm 有符号除法
------------------------------------------------------------------------
在嵌入式开发中最常会用的就是加减指令,乘除基本用不到。
<5>逻辑运算指令,C 语言进行 CPU 寄存器配置的时候常常需要用到逻辑运算符号,比如“&”、“|”等
使用汇编语言的时候也可以使用逻辑运算指令。
指令 计算公式 备注
AND Rd, Rn Rd = Rd &Rn
AND Rd, Rn, #immed Rd = Rn &#immed 按位与
AND Rd, Rn, Rm Rd = Rn & Rm
------------------------------------------------------------------------
ORR Rd, Rn Rd = Rd | Rn
ORR Rd, Rn, #immed Rd = Rn | #immed 按位或
ORR Rd, Rn, Rm Rd = Rn | Rm
------------------------------------------------------------------------
BIC Rd, Rn Rd = Rd & (~Rn)
BIC Rd, Rn, #immed Rd = Rn & (~#immed) 位清除
BIC Rd, Rn , Rm Rd = Rn & (~Rm)
------------------------------------------------------------------------
ORN Rd, Rn, #immed Rd = Rn | (#immed) 按位或非
ORN Rd, Rn, Rm Rd = Rn | (Rm)
------------------------------------------------------------------------
EOR Rd, Rn Rd = Rd ^ Rn
EOR Rd, Rn, #immed Rd = Rn ^ #immed 按位异或
EOR Rd, Rn, Rm Rd = Rn ^ Rm
------------------------------------------------------------------------
要 想 详 细 的 学 习 ARM 的 所 有 指 令 请 参 考 《 ARM ArchitectureReference Manual
ARMv7-A and ARMv7-R edition.pdf》和《ARM Cortex-A(armV7)编 程手册 V4.0.pdf》这两份文档。
4.09.4、 位置相关/无关码
<1>位置无关码
B、BL、MOV、ADR、ADD、SUB、...
<2>位置相关码
LDR、STR、...
<-1>何为位置无关码
-------------------------------------------------------------------------------------------
start.s文件内容如下:
_start: # _start是一个链接的地址,在链接时确定,之后就固定了
.....
b _start; # 跳转到链接地址_start(无论程序所处位置如何,此指令效果不变,故为位置无关码)
-------------------------------------------------------------------------------------------
<-2>这里单独讲解一个指令ADR
示例:
adr r0, _start; # 伪指令,根据当前指令的链接地址与_start的差,计算_start的运行地址,并存r0中
说明:ADR指令多用于代码重定位,用于代码运行过程中,获取某个标号当前所在的地址(运行地址)。
4.09.5、汇编代码示例(重点)
@ =>代码段
.text
.global _start @ .global表示_start是一个全局符号,会在链接器链接时用到
_start: @ 标签_start,汇编程序的默认入口是_start
ldr sp, =(0x80000000+0x100000) @ 设置堆栈
b main @ 跳转到main函数
b . @ 原地循环
@ =>初始化的数据段
.data
st:
.long 0x80809090
.long 0xA0A0D0D0
@ =>未初始化的数据段
.bss
.long 0x0
.long 0x0
@ =>定义新段(只读数据段)
.section .rodata @ 自定义一个段,段名为.rodata
.align 2,0x00 @ 2^2=4字节对齐,空隙用0x00填充
.long 0x000A000B
.align 2,0x00 @ 2^2=4字节对齐,空隙用0x00填充
.byte 0xAB
.align 4,0x00 @ 2^4=16字节对齐,空隙用0x00填充
.byte 0xAC
.align 4,0x00 @ 2^4=16字节对齐,空隙用0x00填充
.byte 0xDD
.end @ 汇编代码结束标志,之后的所有代码将被忽略
@ .text、.data、.bss都是汇编伪指令
@ 汇编并没有.rodata的伪指令,需要自己定义
@ 4字节对齐含义:代码放在(0、4、8、C、0、4..)这样地址是4的倍数的位置
4.09.6、程序链接脚本示例(重点)
<1>代码的几种地址详解
链接地址:程序在链接时指定的地址,即代码编写者设定的代码的目标地址。
运行地址:运行地址是代码运行时所处的地址。注意,运行地址需要等于链接地址,不然代码可能出错。
加载地址:程序代码在bin文件中的地址。
存储地址:程序代码存储的地址,即bin文件被烧录到存储器的地址。
注意:虽然链接地址程序员设置的目标运行地址,加载地址程序员设置的目标存储地址,但这并不意味着
就一定链接地址=运行地址,加载地址=存储地址,具体相不相等取决于你是怎么烧录和重定位代码的。
<2>一个基于imx6ull的链接脚本示例
SECTIONS
{
. = 0x87800000; # 设置初始链接地址
. = ALIGN(4); # 设置地址4字节对齐
__text_start = .; # __text_start = 代码段链接地址首地址
.text : AT(0)
{
start.o (.text) # 文件名 (.text),意为把文件中的代码段放在此处
main.o (.text)
*(.text)
}
__text_end = .; # __text_end = 代码段链接地址末地址
. = ALIGN(4);
.rodata : { *(.rodata) }
. = ALIGN(4);
.data :
{
*(.data)
}
. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(.COMMON)}
__bss_end = .;
__data_linkaddr = ADDR(.data); # __data_linkaddr = 数据段的链接地址首地址
__data_sizeof = SIZEOF(.data); # __data_sizeof = 数据段的长度
__data_loadaddr = LOADADDR(.data); # __data_loadaddr = 数据段的首加载地址首地址
}
4.09.7、__attribute__() 使用方式(重点)
GNU C 的一大特色就是__attribute__ 机制。__attribute__ 可以设置函数属性(Function Attribute )、变量属性(Variable Attribute )和类型属性(Type Attribute )。
=>这里需要注意几个点<重点>:
<1>、__attribute__() 是GNU C的扩展语法。
<2>、C语言中的变量名会变成汇编的标号,变量值会变为汇编中的数据。
<3>、C语言中也可以定义一个没有变量值的变量名,最后会变为一个汇编的标号。
<4>、C语言中的变量、函数所处于的段、所采用的对齐方式等是由编译器自行决定的。
<5>、<4>是一般默认的情况,当然用户也可以使用__attribute__()自定义变量等的相关属性
=>使用示例如下:
#include <stdio.h>
/* 定义:变量名+变量值 */
long i1 __attribute__((section(".__vec"),aligned(4))) = 0x000000AA; /* 用__attribute__设置变量ℹ1放在.__vec段,并以4字节对齐 */
long i2 __attribute__((section(".__vec"),aligned(16))) = 0x000000BB; /* 用__attribute__设置变量ℹ2放在.__vec段,并以16字节对齐 */
int ax = 6699; /* 变量ax未用_attribute__进行属性设置,默认放在.data段 */
/* 定义:仅变量名 */
char _st[0] __attribute__((section(".st"))) ; /* 定义一个标号,它没有占空间,放置在.st段(标号必须被放入到链接脚本中某位置,否则没有意义) */
/* 如果你不把_st加入到链接脚本中,那它在链接过程中会被随机指定,也就失去了存在的意义 */
/* 函数 */
int main(int argc,char* argv[]){
return 0;
}
/* 执行如下指令: */
arm-linux-gnueabihf-gcc -nostdlib -c text.c -o text.o //编译但不链接
arm-linux-gnueabihf-ld text.o -Ttext 0X87800000 -o text.elf //链接
arm-linux-gnueabihf-objdump -D text.elf //反汇编
/* 反汇编显示如下: */
ext.elf: 文件格式 elf32-littlearm
Disassembly of section .text:
87800000 <main>:
87800000: b480 push {r7}
87800002: b083 sub sp, #12
87800004: af00 add r7, sp, #0
87800006: 6078 str r0, [r7, #4]
87800008: 6039 str r1, [r7, #0]
8780000a: 2300 movs r3, #0
8780000c: 4618 mov r0, r3
8780000e: 370c adds r7, #12
87800010: 46bd mov sp, r7
87800012: f85d 7b04 ldr.w r7, [sp], #4
87800016: 4770 bx lr
Disassembly of section .data:
87810018 <ax>:
87810018: 00001a2b andeq r1, r0, fp, lsr #20
Disassembly of section .__vec:
87810020 <i1>:
87810020: 000000aa andeq r0, r0, sl, lsr #1
...
87810030 <i2>:
87810030: 000000bb strheq r0, [r0], -fp
...
Disassembly of section .comment:
................................
# 说明1:因为没有链接过程,所以所有的段都是从0开始的。
# 说明2:0长数组不是c语言的里面东西,它是GNC C的扩展。
4.09.8、Uboot2016-nxp程序起点
Uboot或者C程序的"入口"是由链接脚本决定的,如果没有编译过 uboot 的话,链接脚本为arch/arm/cpu/u-boot.lds。但是这个不是最终使用的链接脚本,最终的链接脚本是在这个链接脚本的基础上生成的。编译一下 uboot,编译完成以后就会在 uboot 根目录下生成 u-boot.lds 文件,打开此文件可以看到如下内容:
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
{
. = 0x00000000;
. = ALIGN(4);
.text :
{
*(.__image_copy_start)
*(.vectors)
arch/arm/cpu/armv7/start.o (.text*)
*(.text*)
}
/* 由于文件太长,这里省略大部分,具体可以自行查看此文件... ... ... */
}
第三行的 ENTRY(_start) 指定了程序的入口地址,在Uboot源码中搜索标号 _start: 。这里需要注意的是,最好打开 区分大小写 和 全字匹配 。搜索结果如下,_start 这个标号在 arch/arm/lib/vectors.S这个文件里定义,这就是Uboot程序的" 入口 "。(注意 .section ".vectors","ax" 这句,它是指明了此代码所处的段,而不是由编译器默认分配的)
4.09.9、Uboot2016-nxp编译控制分析
在linux内核里面,采用menuconfig机制来配置哪些文件将被编译,其原理就利用图形界面产生环境变量,所有的环境变量配置结果都将被写入到主目录的.config文件中。如下图所示:
4.09.10、Uboot2016-nxp整体启动流程分析
4.10、Uboot2016-NXP代码编译
这里以NXP官方提供的Uboot源码为例,这是NXP针对IMX6ULL芯片做过适配的。
(1)创建Uboot编译目录:
(2)Uboot2016源码文件介绍:
0、arch/arm/lib/vectors.S # 存放的向量表,程序入口 _start: 也此文件中
1、arch/arm/cpu/armv7/start.S # _start 执行的第1句代码就是跳到此文件的中 reset: 处
2、arch/arm/include/asm # 存放arch/cpu文件夹下的源文件所对应的头文件
3、board/freescale/***/***.c # 板级文件夹(***.c文件)
4、include/configs/ ***.h # 板级头件夹(***.h文件)
(3)添加开发板
(4)配置、编译Uboot
4.11、Linux-V4.1.15-NXP代码编译
(1)配置编译器
(2)添加开发板
(3)配置、编译内核
设备树相关:(dtc是设备树的编译器)
.dts # 设备树源文件
.dtsi # 设备树头文件
.dtb # 设备树编译后的二进制文件
设备树编译命令1:make dtbs //编译所有的设备树
设备树编译命令2:make yourDIR/xxx.dtb //编译指定的设备树
4.12、Linux-V4.1.15-内核启动分析
start_kernel(void);
//设置架构(根据设备树选择使用哪个机器的初始化代码)
setup_arch(&command_line);
//根据设备树选择机器,并把机器的初始化函数赋值给mdesc
//每个机器的初始化信息使用DT_MACHINE_START宏声明,放在.arch.info.init段中
const struct machine_desc *mdesc;
mdesc = setup_machine_fdt(__atags_pointer);//设置机器的设备树(根据设备树匹配机器)
of_flat_dt_match_machine(mdesc_best, arch_get_next_mach);//设备树与机器匹配
machine_desc = mdesc; //保存为全局变量machine_desc
....
unflatten_device_tree(); //解析二进制设备树,解析为节点类型为device_node的树形结构
__unflatten_device_tree(initial_boot_params, &of_root, early_init_dt_alloc_memory_arch);
...
nflatten_dt_node(blob, mem, &start, NULL, mynodes, 0, false);
...
....
...
//剩余初始化
rest_init();
...
kernel_thread(kernel_init, NULL, CLONE_FS); //创建一个内核线程,线程函数为kernel_init
kernel_init_freeable();
do_basic_setup();
...
driver_init();
...
platform_bus_init(); //(1)=>平台设备总线初始化
bus_register(&platform_bus_type);
...
...
do_initcalls();
for(do_initcall_level(level=0->7)){
//__initcall0_start:__initcall7_start为几个初始化段的起始地址
//别名为"early"、"core"、"postcore"、"arch"、"subsys"、"fs"、"device"、"late",
//使用相应的宏,定义的函数,就会被放在相应的段中,然后通过指针依次调用
//例如:arch_initcall(customize_machine); 把customize_machine函数放在段__initcall3中
for (fn = initcall_levels[level=0]; fn < initcall_levels[0+1]; fn++)
do_one_initcall(*fn); //调用__initcall0_start->__initcall0_end里的所有函数
...
for (fn = initcall_levels[level=2]; fn < initcall_levels[0+1]; fn++)//遍历所有被宏postcore_initcall()声明的函数
do_one_initcall(*fn); //(2)=>当*fn = i2c_init()时,为i2c总线(也叫子系统)初始化
bus_register(&i2c_bus_type);
...
for (fn = initcall_levels[level=3]; fn < initcall_levels[3+1]; fn++){ //遍历所有被宏arch_initcall()声明的函数
//调用__initcall3_start->__initcall3_end里的所有函数(arch),其中有一个函数customize_machine()
do_one_initcall(*fn); //当*fn = customize_machine()时,执行如下:
//machine_desc在setup_arch()函数中已被赋值
//以imx6ull为例,搜索找到DT_MACHINE_START(IMX6UL,xxx)
//可知,这里调用的是imx6ul_init_machine函数
machine_desc->init_machine(){ //imx6ul_init_machine
//(3)=>platform:平台,populate:填充=>将设备树中的设备描述转换为相应的内核设备(platform_device)并进行注册
of_platform_populate(root=NULL, matches=of_default_bus_match_table, lookup=NULL, parent=NULL);
root = of_find_node_by_path("/");
for_each_child_of_node(root, child) //遍历root的第一层子节点child = root->child1-childn
{
of_platform_bus_create(bus=child, matches, lookup, parent, true) //把当前节点当成总线处理
{
struct platform_device *dev;
dev = of_platform_device_create_pdata(bus, bus_id=NULL, platform_data=NULL, parent);
if (!dev || !of_match_node(matches, bus)) //如果创建失败,或者当前节点不与of_default_bus_match_table匹配
return 0; //则停止对其子节点的遍历,递归的终止条件
for_each_child_of_node(bus, child) { //递归遍历根深的子节点
rc = of_platform_bus_create(child, matches, lookup, &dev->dev, strict);
}
}
}
//从root开始遍历,创建platform_device,直到与of_default_bus_match_table不匹配为截止
//注1:是先创建再判断,第一出现不匹配的那个节点也会被创建platform_device
//注2:i2c和spi等总线节点(控制器)会被解析platform_device,但其子节点信息不会在此处解析,因为i2c和spi等总线节点的
//compatible属性与of_default_bus_match_table不匹配,所以解析到总线节点处就不会往下解析了。(注意此时还没有platform_driver)
}
}
for (fn = initcall_levels[level=4]; fn < initcall_levels[4+1]; fn++){ //遍历所有被宏subsys_initcall()声明的函数
//调用__initcall4_start->__initcall4_end里的所有函数(subsys),其中有一个函数i2c_adap_imx_init
do_one_initcall(*fn); //当fn = i2c_adap_imx_init()时,执行如下:(struct platform_driver i2c_imx_driver)
platform_driver_register(drv=&i2c_imx_driver){ //注册i2c总线(也叫子系统)的platform_driver,(i2c控制器信息已被解析成platform_device)
__platform_driver_register(drv, THIS_MODULE);
driver_register(&drv->driver);
i2c_imx_probe(struct platform_device *pdev); //注册成功则调用驱动的probe函数
i2c_add_numbered_adapter(&i2c_imx->adapter); //注册带编号的i2c适配器
i2c_add_adapter(adap);//发现无编号,则调用自动分配编号的函数注册适配器
__i2c_add_numbered_adapter(adapter);
id = idr_alloc(&i2c_adapter_idr, adap, adap->nr, adap->nr+1, GFP_KERNEL); //成功分配idr
i2c_register_adapter(adap); //=>注册适配器
adap->dev.bus = &i2c_bus_type;
adap->dev.type = &i2c_adapter_type;
res = device_register(&adap->dev);
of_i2c_register_devices(adap); //(4)=>解析i2c控制器的子节点,并再i2c上注册成设备
acpi_i2c_register_devices(adap);
acpi_i2c_install_space_handler(adap);
}
}
...
}
...
kernel_thread(kthreadd, NULL, CLONE_FS|CLONE_FILES);
五、Linux驱动开发
5.01、Linux驱动前言
(1)、linux下的驱动开发分为三大类:
<1>字符设备驱动 # 使用最多的
<2>块设备驱动 # 存储设备
<3>网络设备驱动 # 网络设备
一个设备并不是说一定只属于某一个类型,比如USB-WIFI、SDIO-WIFI,属于网络设备驱动,因为他又有USB和SDIO,因此也属于字符设备驱动。
(2)、驱动就是获取外设、或者传感器数据,控制外设。数据会提交给应用程序。
(3)、Linux操作系统内核和驱动程序运行在内核空间,应用程序运行在用户空间。
其中关于 C 库以及如何通过系统调用“陷入” 到内核空间这个我们不用去管,我们重点关注的是应用程序和具体的驱动,应用程序使用到的函数在具体驱动程序中都有与之对应的函数,比如应用程序中调用了 open 这个函数,那么在驱动程序中也得有一个名为 open 的函数。每一个系统调用,在驱动中都有与之对应的一个驱动函数,在 Linux 内核文件 include/linux/fs.h 中有个叫做file_operations 的结构体,此结构体就是 Linux 内核驱动操作函数集合,内容如下所示:
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*mremap)(struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
};
<*>简单介绍一下 file_operation 结构体中比较重要的、常用的函数:
第 2 行, owner 拥有该结构体的模块的指针,一般设置为 THIS_MODULE。
第 3 行, llseek 函数用于修改文件当前的读写位置。
第 4 行, read 函数用于读取设备文件。
第 5 行, write 函数用于向设备文件写入(发送)数据。
第 9 行, poll 是个轮询函数,用于查询设备是否可以进行非阻塞的读写。
第 10 行, unlocked_ioctl 函数提供对于设备的控制功能,与应用程序中的 ioctl 函数对应。
第 11 行, compat_ioctl 函数与 unlocked_ioctl 函数功能一样,区别在于在 64 位系统上,32 位的应用程序调用将会使用此函数。在 32 位的系统上运行 32 位的应用程序调用的是unlocked_ioctl。
第 12 行, mmap 函数用于将将设备的内存映射到进程空间中(也就是用户空间),一般帧缓冲设备会使用此函数,比如 LCD 驱动的显存,将帧缓冲(LCD 显存)映射到用户空间中以后应用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制。
第 13 行, open 函数用于打开设备文件。
第 15 行, release 函数用于释放(关闭)设备文件,与应用程序中的 close 函数对应。
第 16 行, fasync 函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中。
第 17 行, aio_fsync 函数与 fasync 函数的功能类似,只是 aio_fsync 是异步刷新待处理的数据。
<*>在字符设备驱动开发中最常用的就是上面这些函数,关于其他的函数大家可以查阅相关文档。我们在字符设备驱动开发中最主要的工作就是实现上面这些函数,不一定全部都要实现,
但是像 open、 release、 write、 read 等都是需要实现的,当然了,具体需要实现哪些函数还是要
看具体的驱动要求。
(4)、Linux驱动程序可以编译到内核里面,也就是zImage,也可以编译成模块,即.ko文件。
(5)、模块有加载和卸载两种操作,编写驱动的时候需要注册这两种操作函数,模块的加载和卸载注册函数如下:
module_init(xxx_init); # 注册模块加载函数
module_exit(xxx_exit); # 注册模块卸载函数
module_init 函数用来向 Linux 内核注册一个模块加载函数,参数 xxx_init 就是需要注册的具体函数,当使用“insmod”命令加载驱动的时候, xxx_init 这个函数就会被调用。 module_exit()函数用来向 Linux 内核注册一个模块卸载函数,参数 xxx_exit 就是需要注册的具体函数,当使用“rmmod”命令卸载具体驱动的时候xxx_exit 函数就会被调用。
(6)、模块编译好之后生成xxx.ko文件,把其拷贝到文件系统里面,使用如下命令进行安装操作:
方式一: insmod xxx.ko # 加载驱动 方式二: modprobe xxx.ko # 加载驱动
rmmod xxx.ko # 卸载驱动 modprobe -r xxx.ko # 卸载驱动
insmod命令不能解决模块的依赖关系,比如drv.ko依赖first.ko这个模块,就必须先使用insmod命令加first.ko这个模块,然后再加载drv.ko这个模块。但是modprobe就不会存在这个题,modprobe会分析模块的依赖关系,然后会将所有的依赖模块都加载到内核中,因此modprobe命令相比insmod要智能一些(注意modprobe -r 也会卸载依赖,但rmmod不会卸载依赖)。modprobe 命令主要智能在提供了模块的依赖性分析、错误检查、错误报告等功能,推荐使用 modprobe 命令来加载驱动。modprobe 命令默认会去/lib/modules/<kernel-version>目录中查找模块,比如使用的 Linux kernel 的版本号为 4.1.15,自己构建的文件系统一般没有这个目录,需要手动创建。
对于一个新的模块,使用modprobe加载的时候,需要先调用一下depmod 命令。
<&&>:方式一、二的加载方式都属于手动加载,可以用lsmod查看手动加载了哪些模块(lsmod不能查看编译到内核中的驱动模块)。
(7)、驱动加载后,常用的查看命令
lsmod # 显示当前手动加载的驱动(重启后会消失)
cat /proc/devices # 查看注册了哪些设备(主设备号 设备名)
ls -lah /dev # 查看设备文件(节点),旧版本驱动需手动创建,新版本可在代码中创建
(8)、关于设备号
Linux将设备号分为两部分:主设备号和次设备号,主设备号用高12位,次设备号用低20位。系统中主设备号范围为0~4095。在文件 include/linux/kdev_t.h 中提供了几个关于设备号的操作函数(本质是宏),如下所示:
=>>include/linux/kdev_t.h
6 #define MINORBITS 20
7 #define MINORMASK ((1U << MINORBITS) - 1)
8
9 #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
10 #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
11 #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
第 6 行,宏 MINORBITS 表示次设备号位数,一共是 20 位。
第 7 行,宏 MINORMASK 表示次设备号掩码。
第 9 行,宏 MAJOR 用于从 dev_t 中获取主设备号,将 dev_t 右移 20 位即可。
第 10 行,宏 MINOR 用于从 dev_t 中获取次设备号,取 dev_t 的低 20 位的值即可。
第 11 行,宏 MKDEV 用于将给定的主设备号和次设备号的值组合成 dev_t 类型的设备号。
5.02、Linux驱动模板(旧)
(1)、创建vscode工程、添加内核头文件
(2)、编写驱动源文件 chrdevbase.c
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/ide.h>
#define CHRDEVBASE_MAJOR 200 /* 主设备号 */
#define CHRDEVBASE_NAME "chrdevbase" /* 设备名 */
static char readbuf[100]; /* 读缓冲 */
static char writebuf[100]; /* 写缓冲 */
static char kerneldata[] = {"kernel data!"};
/*
* @description : 打开设备
* @param - inode : 传递给驱动的inode
* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
* 一般在open的时候将private_data指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int chrdevbase_open(struct inode *inode, struct file *filp)
{
printk("=>chrdevbase open!\r\n");
return 0;
}
/*
* @description : 从设备读取数据
* @param - filp : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 要读取的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
int retvalue = 0;
/* 向用户空间发送数据 */
memcpy(readbuf, kerneldata, sizeof(kerneldata)); /* 把kerneldata拷贝到readbuf中 */
retvalue = copy_to_user(buf, readbuf, cnt); /* 注意:readbuf是内核空间的内存,用户不能直接访问,需使用copy_to_user()函数*/
if(retvalue == 0){
printk("=>kernel senddata ok!\n");
}else{
printk("=>kernel senddata failed!\n");
}
printk("=>chrdevbase read!\n");
return 0;
}
/*
* @description : 向设备写数据
* @param - filp : 设备文件,表示打开的文件描述符
* @param - buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
int retvalue = 0;
/* 接收用户空间传递给内核的数据并且打印出来 */
retvalue = copy_from_user(writebuf, buf, cnt);
if(retvalue == 0){
printk("=>kernel recevdata:%s\r\n", writebuf);
}else{
printk("=>kernel recevdata failed!\r\n");
}
printk("=>chrdevbase write!\r\n");
return 0;
}
/*
* @description : 关闭/释放设备
* @param - filp : 要关闭的设备文件(文件描述符)
* @return : 0 成功;其他 失败
*/
static int chrdevbase_release(struct inode *inode, struct file *filp)
{
//printk("=>chrdevbase release!\n"); //此处的printk和app中的printf冲突
return 0;
}
/*
* 设备操作函数结构体
*/
static struct file_operations chrdevbase_fops = {
.owner = THIS_MODULE,
.open = chrdevbase_open,
.read = chrdevbase_read,
.write = chrdevbase_write,
.release = chrdevbase_release,
};
/*
* @description : 驱动入口函数
* @param : 无
* @return : 0 成功;其他 失败
*/
static int __init chrdevbase_init(void)
{
int retvalue = 0;
retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &chrdevbase_fops); /* 注册一个字符设备 (要确保CHRDEVBASE_MAJOR没有被占用)*/
if(retvalue < 0){
printk("=>chrdevbase driver register failed!\r\n");
}
printk("=>chrdevbase init!\n");
return 0;
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static void __exit chrdevbase_exit(void)
{
unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME); /* 注销一个字符设备 */
printk("=>chrdevbase exit!\r\n");
}
/* 指定驱动的入口和出口函数 */
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);
/* 设置驱动的LICENSE和作者信息 */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("QQ GROUP:649692007");
(3)、编写编译驱动使用的Makefile文件 Makefile
KERNELDIR := /home/haut/itop_imx6ull/software/Linux_Source_nxpV4.1.15 # 内核路径
CURRENT_PATH := $(shell pwd) # 当前路径
obj-m := chrdevbase.o
build: kernel_modules
kernel_modules: # 执行内核路径下的 Makefile 编译此文件
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
执行make,就可以编译 chrdevbase.c 并生成二进制文件 chrdevbase.ko
(4)、编写测试App程序 chrdevbaseApp.c
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
static char usrdata[] = {"usr data!"};
/*
* @description : main主程序
* @param - argc : argv数组元素个数
* @param - argv : 具体参数
* @return : 0 成功;其他 失败
* ./chrdevbaseApp /dev/chrdevbase 1 # 从驱动读数据
* ./chrdevbaseApp /dev/chrdevbase 2 # 向驱动写数据
*/
int main(int argc, char *argv[])
{
int fd, retvalue;
char *filename;
char readbuf[100], writebuf[100];
if(argc != 3){
printf("Error Usage!\r\n");
return -1;
}
filename = argv[1];
/* 打开驱动文件 */
fd = open(filename, O_RDWR);
if(fd < 0){
printf("Can't open file %s\r\n", filename);
return -1;
}
/* 从驱动文件读取数据 */
if(atoi(argv[2]) == 1){
retvalue = read(fd, readbuf, 50);
if(retvalue < 0){
printf("read file %s failed!\r\n", filename);
}else{
/* 读取成功,打印出读取成功的数据 */
printf("read data:%s\r\n",readbuf);
}
}
/* 向设备驱动写数据 */
if(atoi(argv[2]) == 2){
memcpy(writebuf, usrdata, sizeof(usrdata));
retvalue = write(fd, writebuf, 50);
if(retvalue < 0){
printf("write file %s failed!\r\n", filename);
}
}
/* 关闭设备 */
retvalue = close(fd);
if(retvalue < 0){
printf("Can't close file %s\r\n", filename);
return -1;
}
return 0;
}
执行arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp
执行 file chrdevbaseApp 可以查看文件的属性,和平台信息。
(5)、测试
<1> 加载驱动:
depmod
modprobe chrdevbase.ko
<2> 创建设备节点:
mknod /dev/chrdevbase c 200 0 # c:字符设备,200:主设备号,0:次设备号
<3> 运行App程序
./chrdevbaseApp /dev/chrdevbase 1 # 读
./chrdevbaseApp /dev/chrdevbase 2 # 写
(6)、说明
使用 register_chrdev() 函数注册设备时,要传入的主设备号是没有被使用的,而且一旦注册成功,主设备号下面的所有次设备号都会被此设备占用,最后还需要自己创建设备节点。后续的新模板会使用其它的函数,不仅可以申请设备号,而且还充分利用了次设备号。
5.03、Linux驱动之LED(旧)
Linux内核启动的时候会初始化MMU,设置好内存映射,设置好后CPU访问的都是虚拟地址。比如I.MX6ULL 的GPIO1_IO03引脚的复用寄存器IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03的地址为0X020E0068。如果没有开启 MMU 的话直接向0X020E0068这个寄存器地址写入数据就可以配置 GPIO1_IO03 的复用功能。现在开启了MMU,并且设置了内存映射,因此就不能直接向 0X020E0068 这个地址写入数据了。我们必须得到0X020E0068这个物理地址在Linux系统里面对应的虚拟地址,这里就涉及到了物理内存和虚拟内存之间的转换,需要用到两个函数: ioremap 和 iounmap。
(1)、电路
(2)、驱动程序 led.c
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#define LED_MAJOR 200 /* 主设备号 */
#define LED_NAME "led" /* 设备名字 */
#define LEDOFF 0 /* 关灯 */
#define LEDON 1 /* 开灯 */
/* 寄存器物理地址 */
#define CCM_CCGR1_BASE (0X020C406C) //时钟
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068) //复用
#define SW_PAD_GPIO1_IO03_BASE (0X020E02F4) //电气
#define GPIO1_DR_BASE (0X0209C000) //方向
#define GPIO1_GDIR_BASE (0X0209C004) //输出
/* 映射后的寄存器虚拟地址指针 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;
/*
* @description : LED打开/关闭
* @param - sta : LEDON(0) 打开LED,LEDOFF(1) 关闭LED
* @return : 无
*/
void led_switch(u8 sta)
{
u32 val = 0;
if(sta == LEDON) {
val = readl(GPIO1_DR);
val &= ~(1 << 3);
writel(val, GPIO1_DR);
}else if(sta == LEDOFF) {
val = readl(GPIO1_DR);
val|= (1 << 3);
writel(val, GPIO1_DR);
}
}
/*
* @description : 打开设备
* @param - inode : 传递给驱动的inode
* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
* 一般在open的时候将private_data指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int led_open(struct inode *inode, struct file *filp)
{
return 0;
}
/*
* @description : 从设备读取数据
* @param - filp : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 要读取的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}
/*
* @description : 向设备写数据
* @param - filp : 设备文件,表示打开的文件描述符
* @param - buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
int retvalue;
unsigned char databuf[1];
unsigned char ledstat;
retvalue = copy_from_user(databuf, buf, cnt);
if(retvalue < 0) {
printk("kernel write failed!\r\n");
return -EFAULT;
}
ledstat = databuf[0]; /* 获取状态值 */
if(ledstat == LEDON) {
led_switch(LEDON); /* 打开LED灯 */
} else if(ledstat == LEDOFF) {
led_switch(LEDOFF); /* 关闭LED灯 */
}
return 0;
}
/*
* @description : 关闭/释放设备
* @param - filp : 要关闭的设备文件(文件描述符)
* @return : 0 成功;其他 失败
*/
static int led_release(struct inode *inode, struct file *filp)
{
return 0;
}
/* 设备操作函数 */
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release,
};
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static int __init led_init(void)
{
int retvalue = 0;
u32 val = 0;
/* 初始化LED */
/* 1、寄存器地址映射 */
IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4); //时钟
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4); //复用
SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4); //电气
GPIO1_DR = ioremap(GPIO1_DR_BASE, 4); //方向
GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4); //输出
/* 2、使能GPIO1时钟 */
val = readl(IMX6U_CCM_CCGR1);
val &= ~(3 << 26); /* 清除以前的设置 */
val |= (3 << 26); /* 设置新值 */
writel(val, IMX6U_CCM_CCGR1);
/* 3、设置GPIO1_IO03的复用功能,将其复用为GPIO1_IO03,最后设置IO属性。 */
writel(5, SW_MUX_GPIO1_IO03);
/*寄存器SW_PAD_GPIO1_IO03设置IO属性
*bit 16:0 HYS关闭
*bit [15:14]: 00 默认下拉
*bit [13]: 0 kepper功能
*bit [12]: 1 pull/keeper使能
*bit [11]: 0 关闭开路输出
*bit [7:6]: 10 速度100Mhz
*bit [5:3]: 110 R0/6驱动能力
*bit [0]: 0 低转换率
*/
writel(0x10B0, SW_PAD_GPIO1_IO03);
/* 4、设置GPIO1_IO03为输出功能 */
val = readl(GPIO1_GDIR);
val &= ~(1 << 3); /* 清除以前的设置 */
val |= (1 << 3); /* 设置为输出 */
writel(val, GPIO1_GDIR);
/* 5、默认关闭LED */
val = readl(GPIO1_DR);
val |= (1 << 3);
writel(val, GPIO1_DR);
/* 6、注册字符设备驱动 */
retvalue = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);
if(retvalue < 0){
printk("register chrdev failed!\r\n");
return -EIO;
}
return 0;
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static void __exit led_exit(void)
{
/* 取消映射 */