46、linux开发笔记(主线更新)

一、安装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)
{
	/* 取消映射 */
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值