linux裸机、驱动学习笔记(持续更新)

一、裸机开发

I.MX6ULL芯片简介

NXP出品的,528~900MHz的Cortex-A7内核的MPU。

(1)Cortex-A7架构

七种处理模式
linux裸机、驱动学习笔记(持续更新)_第1张图片

Cortex-A 寄存器组

是 Cortex-A 的内核寄存器组,注意不是芯片的外设寄存器
ARM 架构提供了 16 个 32 位的通用寄存器(R0-R15)供软件使用,前 15 个(R0-R14)可以用作通用的数据存储R15 是程序计数器 PC,用来保存将要执行的指令。ARM 还提供了一个当前程序状态寄存器 CPSR 和一个备份程序状态寄存器 SPSR,SPSR 寄存器就是 CPSR 寄存器的备份
linux裸机、驱动学习笔记(持续更新)_第2张图片

我们讲了 Cortex-A7 有 9 种运行模式,每一种运行模式都有一组与之对应的寄存器组。

每一种模式可见的寄存器包括 15 个通用寄存器(R0~R14)、一两个程序状态寄存器和一个程序计数器 PC。

在这些寄存器中,有些是所有模式所共用的同一个物理寄存器,有一些是各模式自己所独立拥有的。
linux裸机、驱动学习笔记(持续更新)_第3张图片
浅色字体的是与 User 模式所共有的寄存器
蓝绿色背景的是各个模式所独有的寄存器

通用寄存器

R0~R15 就是通用寄存器,通用寄存器可以分为以下三类:
①、未备份寄存器,即 R0~R7。
②、备份寄存器,即 R8~R14。
③、程序计数器 PC,即 R15。

在不同的模式下,未备份寄存器中的数据就会被破坏。所以这 8 个寄存器并没有被用作特殊用途。

备份寄存器有快速中断模式的物理寄存器Rx_irq(x=8~12)。独立,中断处理程序可以不用执行保存和恢复中断现场的指令,从而加速中断的执行过程。
R13是(SP),做为栈指针。8个物理寄存器,七个对应不同模式,一个是用户模式(User)和系统模式(Sys)共用的。应用程序会初始化 R13,使其指向该模式专用的栈地址,这就是常说的初始化 SP 指针。
R14是连接寄存器(LR)。7 个物理寄存器,其中一个是用户模式(User)、系统模式(Sys)和超级监视模式(Hyp)所共有的,剩下的 6 个分别对应 6 种不同模式。
LR 寄存器在 ARM 中主要用作如下两种用途:

  1. 每种处理器模式使用 R14(LR)来存放当前子程序的返回地址,如果使用 BL 或者BLX来调用子函数的话,R14(LR)被设置成该子函数的返回地址。在子函数中,将 R14(LR)中的值赋给R15(PC)即可完成子函数返回
  2. 当异常发生以后,该异常模式对应的 R14 寄存器被设置成该异常模式将要返回的地址,R14 也可以当作普通寄存器使用。
    程序计数器 R15,也叫PC。总是指向当前正在执的的指令地址再加上 2 条指令的地址。ARM 处理器 3 级流水线:取指->译码->执行
程序状态寄存器

所有的处理器模式都共用一个 CPSR 物理寄存器,因此 CPSR 可以在任何模式下被访问。
所有的处理器模式都共用一个 CPSR 必然会导致冲突,为此,除了 User 和 Sys 这两个模式以外,其他 7 个模式每个都配备了一个专用的物理状态寄存器,叫做 SPSR(备份程序状态寄存器),当特定的异常中断发生时,SPSR 寄存器用来保存当前程序状态寄存器(CPSR)的值,当异常退出以后可以用 SPSR 中保存的值来恢复 CPSR。
User 和 Sys 这两个模式不是异常模式,所以并没有配备 SPSR。
SPSR 和 CPSR 的寄存器结构相同。

总结

Cortex- A 内核寄存器组成如下:
①、34 个通用寄存器,包括 R15 程序计数器(PC),这些寄存器都是 32 位的。
②、8 个状态寄存器,包括 CPSR 和 SPSR。
③、Hyp 模式下独有一个 ELR_Hyp 寄存器

(2)ARM 汇编基础

对于 Cortex-A 芯片来讲,大部分芯片在上电以后 C 语言环境还没准备好,所以第一行程序肯定是汇编的。
汇编要初始化SP(堆栈) 指针,有些芯片还需要初始化DDR。这些都是个C语言搭环境的!!!

GNU 汇编语法

汇编文件.S 或者 .s
将 MDK 下的汇编文件直接复制到 IAR 下去编译就会出错,因为 MDK 和 IAR 的编译器不同,所以汇编的语法会有区别。
我们要编写的是== ARM汇编,编译使用的 GCC 交叉编译器,所以我们的汇编代码要符合 GNU 语法==
GNU 汇编语法适用于所有的架构

语法模板
label:instruction @ comment

label 即标号,表示地址位置。任何以“:”结尾的标识符都会被识别为一个标号。
instruction 即指令,也就是汇编指令或伪指令。
**@**符号,表示后面的是注释

注意!ARM 中的指令、伪指令、伪操作、寄存器名等可以全部使用大写,也可以全部使用
小写,但是不能大小写混用。

伪操作

用户可以使用**.section** 伪操作来定义一个段,汇编系统预定义了一些段名:
.text 表示代码段。
.data 初始化的数据段。
.bss 未初始化的数据段。
.rodata 只读数据段。
我们当然可以自己使用.section 来定义一个段,每个段以段名开始,以下一段名或者文件结
尾结束,比如:

.section .testsection @定义一个 testsetcion 段

常见的伪操作有:

汇编程序的默认入口标号是_start
.byte 定义单字节数据,比如.byte 0x12。
.short 定义双字节数据,比如.short 0x1234。
.long 定义一个 4 字节数据,比如.long 0x12345678。
.equ 赋值语句,格式为:.equ 变量名,表达式,比如.equ num, 0x12,表示 num=0x12。
.align 数据字节对齐,比如:.align 4 表示 4 字节对齐。
.end 表示源文件结束。
.global 定义一个全局符号,格式为:.global symbol,比如:.global _start。
.section 定义一个段

GNU 汇编同样也支持函数,函数格式如下:

函数名:
函数体
返回语句

Cortex-A7 常用汇编指令

传输指令

处理器内部传递数据!!!
linux裸机、驱动学习笔记(持续更新)_第4张图片

存储器访问指令

ARM 不能直接访问存储器,比如 RAM 中的数据
我们用汇编来配置 I.MX6UL 寄存器的时候需要借助存储器访问指令,一般先将要配置的值写入到 Rx(x=0~12)寄存器中,然后借助存储器访问指令将 Rx 中的数据写入到 I.MX6UL 寄存器
读取 I.MX6UL 寄存器也是一样的,只是过程相反
linux裸机、驱动学习笔记(持续更新)_第5张图片
linux裸机、驱动学习笔记(持续更新)_第6张图片
立即数就是突然蹦出来的数,不是存到某些容器(内存,寄存器)中的数。可以理解为变量。

LDR 和 STR,这两个是数据加载和存储指令,但是每次只能读写存储器中的一个数据。
STM 和 LDM 就是多存储和多加载,可以连续的读写存储器中的多个连续数据。
FD 是 Full Descending 的缩写,即满递减的意思。向下增长的堆栈。

压栈和出栈指令

我们通常会在 A 函数中调用 B 函数,当 B 函数执行完以后再回到 A 函数继续执行。要想再跳回 A 函数以后代码能够接着正常运行,那就必须在跳到 B 函数之前将当前处理器状态保存起来(就是保存 R0~R15 这些寄存器值),当 B 函数执行完成以后再用前面保存的寄存器值恢复R0-R15 即可。
保存 R0-R15 寄存器的操作就叫做现场保护,恢复 R0~R15 寄存器的操作就叫做恢复现场。在进行现场保护的时候需要进行压栈(入栈)操作,恢复现场就要进行出栈操作。
linux裸机、驱动学习笔记(持续更新)_第7张图片
假如我们现在要将 R0~R3 和 R12 这 5 个寄存器压栈,当前的 SP 指针指向 0X80000000,处理器的堆栈是向下增长的,使用的汇编代码如下:

PUSH {R0~R3, R12} @将 R0~R3 和 R12 压栈

linux裸机、驱动学习笔记(持续更新)_第8张图片

跳转指令

linux裸机、驱动学习笔记(持续更新)_第9张图片
BL 指令相比 B 指令,在跳转之前会在寄存器 LR(R14)中保存当前 PC 寄存器值,所以可以通过将 LR 寄存器中的值重新加载到 PC 中来继续从跳转之前的代码处运行,这是子程序调用一个基本但常用的手段。比如 Cortex-A 处理器的 irq 中断服务函数都是汇编写的

算术运算指令

linux裸机、驱动学习笔记(持续更新)_第10张图片

逻辑运算指令

linux裸机、驱动学习笔记(持续更新)_第11张图片

第二篇 裸机开发

I2C协议

I2C 是最常用的通信接口,众多的传感器都会提供 I2C 接口来和主控相连,比如陀螺仪、加速度计、触摸屏等等。

I2C 协议有关的术语

linux裸机、驱动学习笔记(持续更新)_第12张图片
linux裸机、驱动学习笔记(持续更新)_第13张图片

写读时序
linux裸机、驱动学习笔记(持续更新)_第14张图片linux裸机、驱动学习笔记(持续更新)_第15张图片

有时候我们需要读写多个字节,多字节读写时序和单字节的基本一致,只是在读写数据的
时候可以连续发送多个自己的数据,其他的控制时序都是和单字节一样的。

SPI 实验

相比 I2C 接口,SPI 接口的通信速度很快,I2C 最多 400KHz,但是 SPI 可以到达几十 MHz
SPI 以主从方式工作,通常是有一个主设备和一个或多个从设备,一般 SPI 需要4 根线,但是也可以使用三根线(单向传输)

SPI 通信都是由主机发起的,主机需要提供通信的时钟信号。主机通过 SPI 线连接多个从设备的结构如图 linux裸机、驱动学习笔记(持续更新)_第16张图片

第三篇 系统移植篇

==U-Boot、Linux kernel 和 rootfs ==这三者一起构成了一个完整的 Linux 系统,
在移植 Linux之前我们需要先移植一个 bootloader 代码,这个 bootloader 代码用于启动 Linux 内核bootloader有很多,常用的就是 U-Boot。
移植好 U-Boot 以后再移植 Linux 内核,移植完 Linux 内核以后Linux 还不能正常启动,还需要再移植一个根文件系统(rootfs),根文件系统里面包含了一些最
常用的命令和文件。

UBOOT

U-Boot、Linux kernel 和 rootfs 这三者一起构成了一个完整的 Linux 系统
这段bootloader程序会先初始化DDR等外设,然后将Linux内核从flash(NAND,NOR FLASH,SD,MMC等)拷贝到 DDR 中,最后启动 Linux 内核。

linux裸机、驱动学习笔记(持续更新)_第17张图片

U-Boot 命令使用

help 或者 ?可查看当前 uboot 所支持的命令
我们输入“help(或?) 命令名”既可以查看命令的详细用法linux裸机、驱动学习笔记(持续更新)_第18张图片
信息查询命令
常用的和信息查询有关的命令有 3 个:bdinfo、printenv 和 version。
命令bdinfo 用于查看板子信息
命令printenv 用于输出环境变量信息
命令 version 用于查看 uboot 的版本号

环境变量操作命令
1、修改环境变量
环境变量的操作涉及到两个命令:setenv 和 saveenv
命令 setenv 用于设置或者修改环境变量的值。
命令 saveenv 用于保存修改后的环境变量
一般环境变量是存放在外部 flash 中的,uboot 启动的时候会将环境变量从 flash 读取到 DRAM 中。所以使用命令 setenv 修改的是 DRAM中的环境变量值,修改以后要使用 saveenv 命令将修改后的环境变量保存到 flash 中,否则的话uboot 下一次重启会继续使用以前的环境变量值。

有时候我们修改的环境变量值可能会有空格,比如 bootcmd、bootargs 等,这个时候环境变量值就得用单引号括起来

2、新建环境变量
命令 setenv 也可以用于新建命令,用法和修改环境变量一样

3、删除环境变量
删除一个环境变量只要给这个环境变量赋空值即可

内存操作命令
内存操作命令就是用于直接对 DRAM 进行读写操作的
常用的内存操作命令有 md、nm、mm、mw、cp 和 cmp。

md 命令用于显示内存值,格式如下:

md[.b, .w, .l] address [# of objects]		//显示内存值

[.b .w .l]对应 byte、word 和 long,也就是分别以 1 个字节、2 个字节、4 个字节来显示内存值。
address 就是要查看的内存起始地址,
[# of objects]表示要查看的数据长度。(uboot 命令里面的数字都是十六进制的,所以可以不用写“0x”前缀)
linux裸机、驱动学习笔记(持续更新)_第19张图片

nm 命令用于修改指定地址的内存值,地址不会自增
nm [.b, .w, .l] address

mm 命令也是修改指定地址内存值的,地址会自增

mw 命令用于使用一个指定的数据填充一段内存
mw [.b, .w, .l] address value [count]

cp 命令用于将 DRAM 中的数据从一段内存拷贝到另一段内存中,或者把 Nor Flash 中的数据拷贝到 DRAM 中
cp [.b, .w, .l] source target count

cmp 是比较命令,用于比较两段内存的数据是否相等
cmp [.b, .w, .l] addr1 addr2 count

网络操作命令
uboot 支持大量的网络相关命令,比如 dhcp、ping、nfs 和 tftpboot,我们接下来依次学习一下这几个和网络有关的命令。
linux裸机、驱动学习笔记(持续更新)_第20张图片

setenv ipaddr 192.168.1.50
setenv ethaddr b8:ae:1d:01:00:00
setenv gatewayip 192.168.1.1
setenv netmask 255.255.255.0
setenv serverip 192.168.1.253
saveenv

注意,网络地址环境变量的设置要根据自己的实际情况,确保 Ubuntu 主机和开发板的 IP地址在同一个网段内,比如我现在的开发板和电脑都在 192.168.1.0 这个网段内,所以设置开发板的 IP 地址为 192.168.1.50,我的 Ubuntu 主机的地址为 192.168.1.253,因此 serverip 就是192.168.1.253。ethaddr 为网络 MAC 地址,是一个 48bit 的地址,如果在同一个网段内有多个开发板的话一定要保证每个开发板的 ethaddr 是不同的,否则通信会有问题!设置好网络相关的环境变量以后就可以使用网络相关命令了。

ping 命令

dhcp 命令
dhcp 用于从路由器获取 IP 地址,前提得开发板连接到路由器上的

nfs 命令
nfs(Network File System)网络文件系统,通过 nfs 可以在计算机之间通过网络来分享资源,比如我们将 linux 镜像和设备树文件放到 Ubuntu 中,然后在 uboot 中使用 nfs 命令将 Ubuntu 中 的 linux 镜像和设备树下载到开发板的 DRAM 中。

nfs [loadAddress] [[hostIPaddr:]bootfilename]

loadAddress 是要保存的 DRAM 地址,[[hostIPaddr:]bootfilename]是要下载的文件地址。

BOOT 操作命令

uboot 的本质工作是引导 Linux,所以 uboot 肯定有相关的 boot(引导)命令来启动 Linux。常用的跟 boot 有关的命令有:bootz、bootm 和 boot。

1、bootz 命令
不管用那种方法,只要能将 Linux 镜像和设备树文件存到 DRAM 中就行,然后使用 bootz 命令来启动

bootz [addr [initrd[:size]] [fdt]]

addr 是 Linux 镜像文件在 DRAM 中的位置,
initrd 是 initrd 文件在DRAM 中的地址,如果不使用 initrd 的话使用‘-’代替即可,
fdt 就是设备树文件在 DRAM 中的地址。

2、bootm 命令
bootm 用于启动 uImage 镜像文件
如果不使用设备树的话,启动 Linux 内核的命令如下:

bootm addr

addr 是 uImage 镜像在 DRAM 中的首地址。
如果要使用设备树,那么 bootm 命令和 bootz 一样,命令格式如下:

bootm [addr [initrd[:size]] [fdt]]

其中 addr 是 uImage 在 DRAM 中的首地址,initrd 是 initrd 的地址,fdt 是设备树(.dtb)文件
在 DRAM 中的首地址,如果 initrd 为空的话,同样是用“-”来替代。

3、boot 命令
boot 命令也是用来启动 Linux 系统的
boot 会读取环境变量 bootcmd 来启动 Linux 系统,bootcmd 是一个很重要的环境变量!

第三篇 驱动开发

linux裸机、驱动学习笔记(持续更新)_第21张图片
linux裸机、驱动学习笔记(持续更新)_第22张图片

驱动模块的加载和卸载

Linux 驱动有两种运行方式
第一种就是将驱动编译进 Linux 内核中,这样当 Linux 内核启动的时候就会自动运行驱动程序。
第二种就是将驱动编译成模块(Linux 下模块扩展名为.ko),在Linux 内核启动以后使用“insmod”命令加载驱动模块。

模块有加载和卸载两种操作,我们在编写驱动的时候需要注册这两种操作函数,模块的加载和卸载注册函数如下:

module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数

当使用“insmod”命令加载驱动的时候,xxx_init 这个函数就会被调用
当使用“rmmod”命令卸载具体驱动的时候 xxx_exit 函数就会被调用

驱动编译完成以后扩展名为.ko。有两种命令可以加载驱动模块:insmod和 modprobe

字符设备注册与注销

当驱动模块加载成功以后需要注册字符设备,
同样,卸载驱动模块的时候也需要注销掉字符设备

static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
static inline void unregister_chrdev(unsigned int major, const char *name)

major:主设备号,Linux 下每个设备都有一个设备号,设备号分为主设备号和次设备号两
部分,关于设备号后面会详细讲解。
name:设备名字,指向一串字符串。
fops:结构体 file_operations 类型指针,指向设备的操作函数集合变量。

实现设备的具体操作函数

file_operations 结构体就是设备的具体操作函数

open、read、write 和 release

添加 LICENSE 和作者信息

最后我们需要在驱动中加入 LICENSE 信息和作者信息,其中 LICENSE 是必须添加的

MODULE_LICENSE() //添加模块 LICENSE 信息
MODULE_AUTHOR() //添加模块作者信息

Linux 设备号

为了方便管理,Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成
主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。
Linux 提供了一个名为 dev_t 的数据类型表示设备号

你可能感兴趣的:(linux)