一、GCC
1、gcc 命令
gcc [选项] [文件名字]
主要选项如下:
-c:只编译不链接为可执行文件,编译器将输入的.c 文件编译为.o 的目标文件。
-o:<输出文件名>用来指定编译结束以后的输出文件名,如果不使用这个选项的话 GCC 默 认编译出来的可执行文件名字为 a.out。
-g:添加调试信息,如果要使用调试工具(如 GDB)的话就必须加入此选项,此选项指示编 译的时候生成调试所需的符号信息。
-O:对程序进行优化编译,如果使用此选项的话整个源代码在编译、链接的的时候都会进 行优化,这样产生的可执行文件执行效率就高。
-O2:比-O 更幅度更大的优化,生成的可执行效率更高,但是整个编译过程会很慢。
二、Cortex-A7 MPCore 架构
Cortex-A7 MPcore 处理器支持 1~4 核,通常是和 Cortex-A15 组成 big.LITTLE 架构的, Cortex-A15 作为大核负责高性能运算,比如玩游戏啥的,Cortex-A7 负责普通应用,因为 CortexA7 省电.
big.LITTLE 处理的设计旨在为适当的作业分配恰当的处理器。Cortex-A78 [1]核心是已开发的性能最高的 ARM 核心,而 Cortex-A55 [1] 核心是已开发的中端解决方案的最高效率动态的 ARM 核心。可以利用 Cortex-A78 [1]核心的性能来承担繁重的工作负载,而 Cortex-A55 [1] 可以最有效地处理智能手机的大部分工作负载。这些操作包括操作系统活动、用户界面和其他持续运行、始终连接的任务。
Cortex-A7MPCore 使用 ARMv7-A 架构:
①、SIMDv2 扩展整形和浮点向量操作
②、提供了与 ARM VFPv4 体系结构兼容的高性能的单双精度浮点指令,支持全功能的 IEEE754。
③、支持大物理扩展(LPAE),最高可以访问 40 位存储地址,也就是最高可以支持 1TB 的 内存。
④、支持硬件虚拟化。
⑥、支持 Generic Interrupt Controller(GIC)V2.0。
⑦、支持 NEON,可以加速多媒体和信号处理算法。
1、Cortex-A 处理器运行模型
以前的 ARM 处理器有 7 中运行模型:User、FIQ、IRQ、Supervisor(SVC)、Abort、Undef 和 System,其中 User 是非特权模式,其余 6 中都是特权模式。但新的 Cortex-A 架构加入了 TrustZone 安全扩展,所以就新加了一种运行模式:Monitor,新的处理器架构还支持虚拟化扩 展,因此又加入了另一个运行模式:Hyp,所以 Cortex-A7 处理器有 9 种处理模式
除了 User(USR)用户模式以外,其它 8 种运行模式都是特权模式。这几个 运行模式可以通过软件进行任意切换,也可以通过中断或者异常来进行切换。大多数的程序都 运行在用户模式,用户模式下是不能访问系统所有资源的,有些资源是受限的,要想访问这些 受限的资源就必须进行模式切换。但是用户模式是不能直接进行切换的,用户模式下需要借助 异常来完成模式切换,当要切换模式的时候,应用程序可以产生异常,在异常的处理过程中完 成处理器模式切换。
2、寄存器
ARM 架构提供了 16 个 32 位的通用寄存器(R0~R15)供软件使用,前 15 个(R0~R14)可以用 作通用的数据存储,R15 是程序计数器 PC,用来保存将要执行的指令。ARM 还提供了一个当 前程序状态寄存器 CPSR 和一个备份程序状态寄存器 SPSR,SPSR 寄存器就是 CPSR 寄存器的 备份。
Cortex-A7 有 9 种运行模式,每一种运行模式都有一组与之对应的寄存 器组。每一种模式可见的寄存器包括 15 个通用寄存器(R0~R14)、一两个程序状态寄存器和一个 程序计数器 PC。在这些寄存器中,有些是所有模式所共用的同一个物理寄存器,有一些是各模 式自己所独立拥有的。
,CortexA 内核寄存器组成如下:
①、34 个通用寄存器,包括 R15 程序计数器(PC),这些寄存器都是 32 位的。
②、8 个状态寄存器,包括 CPSR 和 SPSR。
③、Hyp 模式下独有一个 ELR_Hyp 寄存器。
2.1、通用寄存器
R0~R15 就是通用寄存器,通用寄存器可以分为以下三类:
①、未备份寄存器,即 R0~R7。 ②、备份寄存器,即 R8~R14。 ③、程序计数器 PC,即 R15。
2.1.1 未备份寄存器
未备份寄存器指的是 R0~R7 这 8 个寄存器,因为在所有的处理器模式下这 8 个寄存器都是 同一个物理寄存器,在不同的模式下,这 8 个寄存器中的数据就会被破坏。所以这 8 个寄存器 并没有被用作特殊用途。
2.1.2 备份寄存器
备份寄存器中的 R8~R12 这 5 个寄存器有两种物理寄存器,在快速中断模式下(FIQ)它们对 应着 Rx_irq(x=8~12)物理寄存器,其他模式下对应着 Rx(8~12)物理寄存器。FIQ 是快速中断模 式,看名字就是知道这个中断模式要求快速执行! FIQ 模式下中断处理程序可以使用 R8~R12 寄存器,因为 FIQ 模式下的 R8~R12 是独立的,因此中断处理程序可以不用执行保存和恢复中 断现场的指令,从而加速中断的执行过程。
备份寄存器 R13 一共有 8 个物理寄存器,其中一个是用户模式(User)和系统模式(Sys)共用 的,剩下的 7 个分别对应 7 种不同的模式。R13 也叫做 SP,用来做为栈指针。基本上每种模式 都有一个自己的 R13 物理寄存器,应用程序会初始化 R13,使其指向该模式专用的栈地址,这 就是常说的初始化 SP 指针。
备份寄存器 R14 一共有 7 个物理寄存器,其中一个是用户模式(User)、系统模式(Sys)和超 级监视模式(Hyp)所共有的,剩下的 6 个分别对应 6 种不同的模式。R14 也称为连接寄存器(LR), LR 寄存器在 ARM 中主要用作如下两种用途:
①、每种处理器模式使用 R14(LR)来存放当前子程序的返回地址,如果使用 BL 或者 BLX 来调用子函数的话,R14(LR)被设置成该子函数的返回地址,在子函数中,将 R14(LR)中的值赋 给 R15(PC)即可完成子函数返回,比如在子程序中可以使用如下代码:
②、当异常发生以后,该异常模式对应的 R14 寄存器被设置成该异常模式将要返回的地址, R14 也可以当作普通寄存器使用。
2.1.3 程序计数器 R15
程序计数器 R15 也叫做 PC,R15 保存着当前执行的指令地址值加 8 个字节,这是因为 ARM 的流水线机制导致的。ARM 处理器 3 级流水线:取指->译码->执行,这三级流水线循环执行, 比如当前正在执行第一条指令的同时也对第二条指令进行译码,第三条指令也同时被取出存放 在 R15(PC)中。我们喜欢以当前正在执行的指令作为参考点,也就是以第一条指令为参考点, 那么 R15(PC)中存放的就是第三条指令,换句话说就是 R15(PC)总是指向当前正在执行的指令 地址再加上 2 条指令的地址。对于 32 位的 ARM 处理器,每条指令是 4 个字节,所以: R15 (PC)值 = 当前执行的程序位置 + 8 个字节。
2.2 程序状态寄存器
所有的处理器模式都共用一个 CPSR 物理寄存器,因此 CPSR 可以在任何模式下被访问。 CPSR 是当前程序状态寄存器,该寄存器包含了条件标志位、中断禁止位、当前处理器模式标志 等一些状态位以及一些控制位。所有的处理器模式都共用一个 CPSR 必然会导致冲突,为此, 除了 User 和 Sys 这两个模式以外,其他 7 个模式每个都配备了一个专用的物理状态寄存器,叫 做 SPSR(备份程序状态寄存器),当特定的异常中断发生时,SPSR 寄存器用来保存当前程序状 态寄存器(CPSR)的值,当异常退出以后可以用 SPSR 中保存的值来恢复 CPSR。
因为 User 和 Sys 这两个模式不是异常模式,所以并没有配备 SPSR,因此不能在 User 和 Sys 模式下访问 SPSR,会导致不可预知的结果。由于 SPSR 是 CPSR 的备份,因此 SPSR 和 CPSR 的寄存器结构相同,
三、ARM 汇编基础
Cortex-A 芯片一上电 SP 指针还没初始化,C 环境还没准备 好,所以肯定不能运行 C 代码,必须先用汇编语言设置好 C 环境,比如初始化 DDR、设置 SP 指针等等,当汇编把 C 环境设置好了以后才可以运行 C 代码。所以 Cortex-A 一开始肯定是汇 编代码,其实 STM32 也一样的,一开始也是汇编,以 STM32F103 为例,启动文件 startup_stm32f10x_hd.s 就是汇编文件。
对于 Cortex-A 芯片来讲,大部分芯片在上电以后 C 语言环境还没准备好,所以第一行程序 肯定是汇编的,至于要写多少汇编程序,那就看你能在哪一步把 C 语言环境准备好。所谓的 C 语言环境就是保证 C 语言能够正常运行。C 语言中的函数调用涉及到出栈入栈,出栈入栈就要 对堆栈进行操作,所谓的堆栈其实就是一段内存,这段内存比较特殊,由 SP 指针访问,SP 指 针指向栈顶。芯片一上电 SP 指针还没有初始化,所以 C 语言没法运行,对于有些芯片还需要 初始化 DDR,因为芯片本身没有 RAM,或者内部 RAM 不开放给用户使用,用户代码需要在 DDR 中运行,因此一开始要用汇编来初始化 DDR 控制器。
1、GNU汇编语法
我们要编写的是 ARM 汇编,编译使用的 GCC 交叉编译器,所以我们的汇编代码要符合 GNU 语法。
GNU 汇编语法适用于所有的架构,并不是 ARM 独享的,GNU 汇编由一系列的语句组成, 每行一条语句,每条语句有三个可选部分,如下:
label:instruction @ comment
label 即标号,表示地址位置,有些指令前面可能会有标号,这样就可以通过这个标号得到 指令的地址,标号也可以用来表示数据地址。注意 label 后面的“:”,任何以“:”结尾的标识 符都会被识别为一个标号。 instruction 即指令,也就是汇编指令或伪指令。 @符号,表示后面的是注释,就跟 C 语言里面的“/”和“/”一样,其实在 GNU 汇编文 件中我们也可以使用“/”和“/”来注释。 comment 就是注释内容。
比如如下代码:
上面代码中“add:”就是标号,“MOVS R0,#0X12”就是指令,最后的“@设置 R0=0X12”就是 注释。
ARM 中的指令、伪指令、伪操作、寄存器名等可以全部使用大写,也可以全部使用 小写,但是不能大小写混用。
用户可以使用.section 伪操作来定义一个段。
.text 表示代码段。.data 初始化的数据段。.bss 未初始化的数据段。 .rodata 只读数据段。
可以自己使用.section 来定义一个段,每个段以段名开始,以下一段名或者文件结 尾结束
常见的伪操作有:
.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。
GNU 汇编同样也支持函数,函数格式如下:
函数名:
函数体
返回语句
“Undefined_Handler”就是函数名,“ldr r0, =Undefined_Handler”是函数体,“bx r0”是函数 返回语句,“bx”指令是返回指令,函数返回语句不是必须的。
2、Cortex-A7 常用汇编指令
2.1、 处理器内部数据传输指令
使用处理器做的最多事情就是在处理器内部来回的传递数据,常见的操作有:
①、将数据从一个寄存器传递到另外一个寄存器。
②、将数据从一个寄存器传递到特殊寄存器,如 CPSR 和 SPSR 寄存器。
③、将立即数传递到寄存器。 数据传输常用的指令有三个:MOV、MRS 和 MSR,
2.2 存储器访问指令
ARM 不能直接访问存储器,比如 RAM 中的数据,I.MX6UL 中的寄存器就是 RAM 类型 的,我们用汇编来配置 I.MX6UL 寄存器的时候需要借助存储器访问指令,一般先将要配置的值 写入到 Rx(x=0~12)寄存器中,然后借助存储器访问指令将 Rx 中的数据写入到 I.MX6UL 寄存器中。读取 I.MX6UL 寄存器也是一样的,只是过程相反。常用的存储器访问指令有两种:LDR 和 STR。
2.3、 压栈和出栈指令
通常会在 A 函数中调用 B 函数,当 B 函数执行完以后再回到 A 函数继续执行。要想 再跳回 A 函数以后代码能够接着正常运行,那就必须在跳到 B 函数之前将当前处理器状态保存 起来(就是保存 R0~R15 这些寄存器值),当 B 函数执行完成以后再用前面保存的寄存器值恢复 R0~R15 即可。保存 R0~R15 寄存器的操作就叫做现场保护,恢复 R0~R15 寄存器的操作就叫做 恢复现场。在进行现场保护的时候需要进行压栈(入栈)操作,恢复现场就要进行出栈操作。压栈 的指令为 PUSH,出栈的指令为 POP,PUSH 和 POP 是一种多存储和多加载指令,即可以一次 操作多个寄存器数据,他们利用当前的栈指针 SP 来生成地址。
假如我们现在要将 R0~R3 和 R12 这 5 个寄存器压栈,当前的 SP 指针指向 0X80000000,处理器的堆栈是向下增长的,使用的汇编代码如下:PUSH {R0~R3, R12} @将 R0~R3 和 R12 压栈
假如我们现在要再将 LR 进行压栈,汇编代码如下:PUSH {LR} @将 LR 进行压栈
如果我们要出栈的话 就是使用如下代码:
POP {LR} @先恢复 LR
POP {R0~R3,R12} @在恢复 R0~R3,R12
出栈的就是从栈顶,也就是 SP 当前执行的位置开始,地址依次减小来提取堆栈中的数据 到要恢复的寄存器列表中。
PUSH 和 POP 的另外一种写法是“STMFD SP!”和“LDMFD SP!”,
2.4、跳转指令
有多种跳转操作,比如: ①、直接使用跳转指令 B、BL、BX 等。 ②、直接向 PC 寄存器里面写入数据。 上述两种方法都可以完成跳转操作,但是一般常用的还是 B、BL 或 BX,
2.4.1 、B指令
B 指令会将 PC 寄存器的值设置为跳转目标地址, 一旦执行 B 指 令,ARM 处理器就会立即跳转到指定的目标地址。如果要调用的函数不会再返回到原来的执行 处,那就可以用 B 指令
上述代码就是典型的在汇编中初始化 C 运行环境,然后跳转到 C 文件的 main 函数中运行,
2.4.2、BL指令
BL 指令相比 B 指令,在跳转之前会在寄存器 LR(R14)中保存当前 PC 寄存器值,所以可以 通过将 LR 寄存器中的值重新加载到 PC 中来继续从跳转之前的代码处运行,这是子程序调用 一个基本但常用的手段。比如 Cortex-A 处理器的 irq 中断服务函数都是汇编写的,主要用汇编 来实现现场的保护和恢复、获取中断号等。但是具体的中断处理过程都是 C 函数,所以就会存 在汇编中调用 C 函数的问题。而且当 C 语言版本的中断处理函数执行完成以后是需要返回到 irq 汇编中断服务函数,因为还要处理其他的工作,一般是恢复现场。这个时候就不能直接使用 B 指令了,因为 B 指令一旦跳转就再也不会回来了,这个时候要使用 BL 指令。
2.5、算术运算指令
2.6、逻辑运算指令
四、汇编LED灯实验
使用库函数来初始化 STM32 的一个 IO 为输出功能,代 码中重点要做的事情有以下几个:
①、使能指定 GPIO 的时钟。
②、初始化 GPIO,比如输出功能、上拉、速度等等。
③、STM32 有的 IO 可以作为其它外设引脚,也就是 IO 复用,如果要将 IO 作为其它外设 引脚使用的话就需要设置 IO 的复用功能。
④、最后设置 GPIO 输出高电平或者低电平。
1、I.MX6U IO 命名
I.MX6ULL 的 IO 分为两类:SNVS 域的和通用的,这两类 IO 本 质上都是一样的。
形如“IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO00”的就是 GPIO 命名,命 名形式就是“IOMUXC_SW_MUC_CTL_PAD_XX_XX”,后面的“XX_XX”就是 GPIO 命名, 比如:GPIO1_IO01、UART1_TX_DATA、JTAG_MOD 等等。I.MX6ULL 的 GPIO 并不像 STM32 一样以 PA0~15 这样命名,他是根据某个 IO 所拥有的功能来命名的。
I.MX6U 的 GPIO 一共有 5 组:GPIO1、GPIO2、GPIO3、GPIO4 和 GPIO5, 其中 GPIO1 有 32 个 IO,GPIO2 有 22 个 IO,GPIO3 有 29 个 IO、GPIO4 有 29 个 IO,GPIO5 最少,只有 12 个 IO,这样一共有 124 个 GPIO。
2、I.MX6U IO 配置
HYS(bit16):对应图 8.1.4.2 中 HYS,用来使能迟滞比较器,当 IO 作为输入功能的时候有 效,用于设置输入接收器的施密特触发器是否使能。如果需要对输入波形进行整形的话可以使 能此位。此位为 0 的时候禁止迟滞比较器,为 1 的时候使能迟滞比较器。
PUS(bit15:14):对应图 8.1.4.2 中的 PUS,用来设置上下拉电阻的,一共有四种选项可以选 择,如表 8.1.4.1 所示:
PUE(bit13):图 8.1.4.2 没有给出来,当 IO 作为输入的时候,这个位用来设置 IO 使用上下 拉还是状态保持器。当为 0 的时候使用状态保持器,当为 1 的时候使用上下拉。状态保持器在 IO 作为输入的时候才有用,顾名思义,就是当外部电路断电以后此 IO 口可以保持住以前的状 态。
PKE(bit12):对应图 8.1.4.2 中的 PKE,此位用来使能或者禁止上下拉/状态保持器功能,为 0 时禁止上下拉/状态保持器,为 1 时使能上下拉和状态保持器。
ODE(bit11):对应图 8.1.4.2 中的 ODE,当 IO 作为输出的时候,此位用来禁止或者使能开 路输出,此位为 0 的时候禁止开路输出,当此位为 1 的时候就使能开路输出功能。
SPEED(bit7:6):对应图 8.1.4.2 中的 SPEED,当 IO 用作输出的时候,此位用来设置 IO 速 度,设置如表 8.1.4.2 所示:
DSE(bit5:3):对应图 8.1.4.2 中的 DSE,当 IO 用作输出的时候用来设置 IO 的驱动能力, 总共有 8 个可选选项,如表 8.1.4.3 所示:
SRE(bit0):对应图 8.1.4.2 中的 SRE,设置压摆率,当此位为 0 的时候是低压摆率,当为 1 的时候是高压摆率。这里的压摆率就是 IO 电平跳变所需要的时间,比如从 0 到 1 需要多少时 间,时间越小波形就越陡,说明压摆率越高;反之,时间越多波形就越缓,压摆率就越低。如 果你的产品要过 EMC 的话那就可以使用小的压摆率,因为波形缓和,如果你当前所使用的 IO 做高速通信的话就可以使用高压摆率。
3、I.MX6U GPIO 配置
GPIO 是一个 IO 众多复用功能中的一种,比 如 GPIO1_IO00 这个 IO 可以复用为:I2C2_SCL、GPT1_CAPTURE1、ANATOP_OTG1_ID、 ENET1_REF_CLK 、 MQS_RIGHT 、 GPIO1_IO00 、 ENET1_1588_EVENT0_IN 、 SRC_SYSTEM_RESET 和 WDOG3_WDOG_B 这 9 个功能,GPIO1_IO00 是其中的一种,我们 想要把 GPIO1_IO00 用作哪个外设就复用为哪个外设功能即可。
左上角部分的 GPIO 框图就是,当 IO 用作 GPIO 的时候需要设置的寄存器,一共有八个: DR、GDIR、PSR、ICR1、ICR2、EDGE_SEL、IMR 和 ISR。
1、DR 寄存器,此寄存器是数据寄存器:
此寄存器是 32 位的,一个 GPIO 组最大只有 32 个 IO,因此 DR 寄存器中的每个位都对应 一个 GPIO。当 GPIO 被配置为输出功能以后,向指定的位写入数据那么相应的 IO 就会输出相 应的高低电平,比如要设置 GPIO1_IO00 输出高电平,那么就应该设置 GPIO1.DR=1。当 GPIO被配置为输入模式以后,此寄存器就保存着对应 IO 的电平值,每个位对对应一个 GPIO,例如, 当 GPIO1_IO00 这个引脚接地的话,那么 GPIO1.DR 的 bit0 就是 0。
2、GDIR 寄存器,这是方向寄存器:
GDIR 寄存器也是 32 位的,此寄存器用来设置某个 IO 的工作方向,是输入还是输出。同 样的,每个 IO 对应一个位,如果要设置 GPIO 为输入的话就设置相应的位为 0,如果要设置为 输出的话就设置为 1。比如要设置 GPIO1_IO00 为输入,那么 GPIO1.GDIR=0;
3、PSR 寄存器,这是 GPIO 状态寄存器:
PSR 寄存器也是一个 GPIO 对应一个位,读取相应的位即可获取对应的 GPIO 的状 态,也就是 GPIO 的高低电平值。功能和输入状态下的 DR 寄存器一样。
4、ICR1和ICR2这两个寄存器,都是中断控制寄存器,
ICR1用于配置低16个GPIO, ICR2 用于配置高 16 个 GPIO。
ICR1 用于 IO0~15 的配置, ICR2 用于 IO16~31 的配置。ICR1 寄存器中一个 GPIO 用两个 位,这两个位用来配置中断的触发方式,和 STM32 的中断很类似。
5、 IMR 寄存器,这是中断屏蔽寄存器。
IMR 寄存器也是一个 GPIO 对应一个位,IMR 寄存器用来控制 GPIO 的中断禁止和使能, 如果使能某个 GPIO 的中断,那么设置相应的位为 1 即可,反之,如果要禁止中断,那么就设 置相应的位为 0 即可。
6、寄存器 ISR,ISR 是中断状态寄存器
ISR 寄存器也是 32 位寄存器,一个 GPIO 对应一个位,只要某个 GPIO 的中断发生,那么 ISR 中相应的位就会被置 1。所以,我们可以通过读取 ISR 寄存器来判断 GPIO 中断是否发生, 相当于 ISR 中的这些位就是中断标志位。当我们处理完中断以后,必须清除中断标志位,清除 方法就是向 ISR 中相应的位写 1,也就是写 1 清零。
7、EDGE_SEL 寄存器,这是边沿选择寄存器
EDGE_SEL 寄存器用来设置边沿中断,这个寄存器会覆盖 ICR1 和 ICR2 的设置,同样是一 个 GPIO 对应一个位。如果相应的位被置 1,那么就相当与设置了对应的 GPIO 是上升沿和下降 沿(双边沿)触发。
4、I.MX6U GPIO 时钟使能
STM32 的每个外设都有 一个外设时钟,GPIO 也不例外,要使用某个外设,必须要先使能对应的时钟。I.MX6U 其实也 一样的,每个外设的时钟都可以独立的使能或禁止,这样可以关闭掉不使用的外设时钟,起到 省电的目的。
CMM 有 CCM_CCGR0~CCM_CCGR6 这 7 个寄存器,这 7 个寄存器控制着 I.MX6U 的所有外设时钟开关。
CCM_CCGR0 是个 32 位寄存器,其中每 2 位控制一个外设的时钟,比如 bit31:30 控制着 GPIO2 的外设时钟,两个位就有 4 种操作方式,
要将 I.MX6U 的 IO 作为 GPIO 使用,我们需要一下 几步:
①、使能 GPIO 对应的时钟。 ②、设置寄存器 IOMUXC_SW_MUX_CTL_PAD_XX_XX,设置 IO 的复用功能,使其复用 为 GPIO 功能。 ③、设置寄存器 IOMUXC_SW_PAD_CTL_PAD_XX_XX,设置 IO 的上下拉、速度等等。 ④、第②步已经将 IO 复用为了 GPIO 功能,所以需要配置 GPIO,设置输入/输出、是否使 用中断、默认输出电平等。
5、实验程序编写
1、使能 GPIO1 时钟
2、设置 GPIO1_IO03 的复用功能
3、配置 GPIO1_IO03
4、设置 GPIO
5、控制 GPIO 的输出电平
.global _start
_start:
ldr r0, =0X020C4070
ldr r1,=0xffffffff
str r1, [r0]
ldr r0,=0x020e0068;
ldr r1,=0x5
str r1,[r0]
ldr r0,=0x020e02f4
ldr r1,=0x10b0
str r1,[r0]
ldr r0, =0X0209C004
ldr r1, =0X0000008
str r1,[r0]
ldr r0, =0X0209C000
ldr r1, =0
str r1,[r0]
loop:
b loop
5.1、arm-linux-gnueabihf-gcc 编译文件
因为本试验就一个 led.s 源文件,所以编译比较简 单。先将 led.s 编译为对应的.o 文件,在终端中输入如下命令:
arm-linux-gnueabihf-gcc -g -c led.s -o led.o
上述命令就是将 led.s 编译为 led.o,其中“-g”选项是产生调试信息,GDB 能够使用这些 调试信息进行代码调试。“-c”选项是编译源文件,但是不链接。“-o”选项是指定编译产生的文 件名字,这里我们指定 led.s 编译完成以后的文件名字为 led.o。执行上述命令以后就会编译生 成一个 led.o 文件,
led.o 文件并不是我们可以下载到开发板中运行的文件,一个工程中所有的 C 文件和汇编文件都会编译生成一个对应的.o 文件,我们需要将这.o 文件链接起来组合成可执行 文件。
5.2、arm-linux-gnueabihf-ld 链接文件
arm-linux-gnueabihf-ld 用来将众多的.o 文件链接到一个指定的链接位置。我们在学习 SMT32 的时候基本就没有听过“链接”这个词,我们一般用 MDK 编写好代码,然后点击“编 译”,MDK 或者 IAR 就会自动帮我们编译好整个工程,最后再点击“下载”就可以将代码下载 到开发板中。这是因为链接这个操作 MDK 或者 IAR 已经帮你做好了,后面我就以 MDK 为例 给大家讲解。
我们现在需要做的就是确定一下本试验最终的可执行文件其运行起始地址,也就是 链接地址。这里我们要区分“存储地址”和“运行地址”这两个概念,“存储地址”就是可执 行文件存储在哪里,可执行文件的存储地址可以随意选择。“运行地址”就是代码运行的时候 所处的地址,这个我们在链接的时候就已经确定好了,代码要运行,那就必须处于运行地址 处,否则代码肯定运行出错。比如 I.MX6U 支持 SD 卡、EMMC、NAND 启动,因此代码可以 存储到 SD 卡、EMMC 或者 NAND 中,但是要运行的话就必须将代码从 SD 卡、EMMC 或者 NAND 中拷贝到其运行地址(链接地址)处,“存储地址”和“运行地址”可以一样,比如 STM32 的存储起始地址和运行起始地址都是 0X08000000。
本教程所有的裸机例程都是烧写到 SD 卡中,上电以后 I.MX6U 的内部 boot rom 程序会将 可执行文件拷贝到链接地址处,这个链接地址可以在 I.MX6U 的内部 128KB RAM 中 (0X900000~0X91FFFF),也可以在外部的 DDR 中。本教程所有裸机例程的链接地址都在 DDR 中,链接起始地址为 0X87800000。I.MX6U-ALPHA 开发板的 DDR 容量有两种:512MB 和 256MB,起始地址都为 0X80000000,只不过 512MB 的终止地址为 0X9FFFFFFF,而 256MB 容 量的终止地址为 0X8FFFFFFF。之所以选择 0X87800000 这个地址是因为后面要讲的 Uboot 其 链接地址就是 0X87800000,这样我们统一使用 0X87800000 这个链接地址,不容易记混。
确定了链接地址以后就可以使用 arm-linux-gnueabihf-ld 来将前面编译出来的 led.o 文件链 接到 0X87800000 这个地址,使用如下命令:
arm-linux-gnueabihf-ld -Ttext 0X87800000 led.o -o led.elf
上述命令中-Ttext 就是指定链接地址,“-o”选项指定链接生成的 elf 文件名,这里我们命名 为 led.elf。上述命令执行完以后就会在工程目录下多一个 led.elf 文件
led.elf 文件也不是我们最终烧写到 SD 卡中的可执行文件,我们要烧写的.bin 文件,因此还 需要将 led.elf 文件转换为.bin 文件,这里我们就需要用到 arm-linux-gnueabihf-objcopy 这个工具 了。
5.3、arm-linux-gnueabihf-objcopy 格式转换
arm-linux-gnueabihf-objcopy 更像一个格式转换工具,我们需要用它将 led.elf 文件转换为 led.bin 文件,命令如下: arm-linux-gnueabihf-objcopy -O binary -S -g led.elf led.bin
上述命令中,“-O”选项指定以什么格式输出,后面的“binary”表示以二进制格式输出, 选项“-S”表示不要复制源文件中的重定位信息和符号信息,“-g”表示不复制源文件中的调试 信息。
5.4、arm-linux-gnueabihf-objdump 反汇编
大多数情况下我们都是用 C 语言写试验例程的,有时候需要查看其汇编代码来调试代码, 因此就需要进行反汇编,一般可以将 elf 文件反汇编,比如如下命令:
arm-linux-gnueabihf-objdump -D led.elf > led.dis
上述代码中的“-D”选项表示反汇编所有的段,反汇编完成以后就会在当前目录下出现一 个名为 led.dis 文件。
5.5 创建 Makefile 文件
如果我们修改了 led.s 文件,那么就需要在重复一次上面的这些命令,太麻烦了,这个时候 我们就可以使用第三章讲解的 Makefile 文件了
6、代码烧写
STM32 等其他的单片机的时候,编译完代码以后可以直接通过 MDK 或者 IAR 下载到内部的 flash 中。但是 I.MX6U 虽然内部有 96K 的 ROM,但是这 96K 的 ROM 是 NXP 自己用的,不向用户开放。所以相当于说 I.MX6U 是没有内部 flash 的,但是我们的代码得有地 方存放啊,为此,I.MX6U 支持从外置的 NOR Flash、NAND Flash、SD/EMMC、SPI NOR Flash 和 QSPI Flash 这些存储介质中启动,所以我们可以将代码烧写到这些存储介质中中。在这些存 储介质中,除了 SD 卡以外,其他的一般都是焊接到了板子上的,我们没法直接烧写。但是 SD 卡是活动的,是可以从板子上插拔的,我们可以将 SD 卡插到电脑上,在电脑上使用软件将.bin 文件烧写到 SD 卡中,然后再插到板子上就可以了。其他的几种存储介质是我们量产的时候用 到的,量产的时候代码就不可能放到 SD 卡里面了,毕竟 SD 是活动的,不牢固,而其他的都是 焊接到板子上的,很牢固。
因此,我们在调试裸机和 Uboot 的时候是将代码下载到 SD 中,因为方便嘛,当调试完成 以后量产的时候要将裸机或者 Uboot 烧写到 SPI NOR Flash、EMMC、NAND 等这些存储介质 中的。
五、I.MX6U 启动方式详解
1、启动方式选择
BOOT 的处理过程是发生在 I.MX6U 芯片上电以后,芯片会根据 BOOT_MODE[1:0]的设置 来选择 BOOT 方式。BOOT_MODE[1:0]的值是可以改变的,有两种方式,一种是改写 eFUSE(熔 丝),一种是修改相应的 GPIO 高低电平。第一种修改 eFUSE 的方式只能修改一次,后面就不能 再修改了,所以我们不使用。我们使用的是通过修改 BOOT_MODE[1:0]对应的 GPIO 高低电平 来选择启动方式,所有的开发板都使用的这种方式,I.MX6U 有一个 BOOT_MODE1 引脚和 BOOT_MODE0 引脚,这两个引脚对应这 BOOT_MODE[1:0]。
其中 BOOT_MODE1 和 BOOT_MODE0 在芯片内部是有 100KΩ下拉电阻的,所以默认是 0。BOOT_MODE1 和 BOOT_MODE0 这两个引脚我们也接到了底板的拨码开关上,这样我们 就可以通过拨码开关来控制 BOOT_MODE1 和 BOOT_MODE0 的高低电平。
BOOT_MODE1 和 BOOT_MODE0 在芯片内部是有 100KΩ下拉电阻的,所以默认是 0。BOOT_MODE1 和 BOOT_MODE0 这两个引脚我们也接到了底板的拨码开关上,这样我们 就可以通过拨码开关来控制 BOOT_MODE1 和 BOOT_MODE0 的高低电平。以 BOOT_MODE1 为例,当我们把 BOOT_CFG 的第一个开关拨到“ON”的时候,就相当于 BOOT_MODE1 引脚 通过 R88 这个 10K 电阻接到了 3.3V 电源,芯片内部的 BOOT_MODE1 又是 100K 下拉电阻接 地,因此此时 BOOT_MODE1 的电压就是 100/(10+100)*3.3V= 3V,这是个高电平,因此 BOOT_CFG 的中的 8 个开关拨到“ON”就是高电平,拨到“OFF”就是低电平。
I.MX6U 有四个 BOOT 模式,这四个 BOOT 模式由 BOOT_MODE[1:0]来控制,也就是 BOOT_MODE1 和 BOOT_MODE0 这两 IO。
1.1 、串行下载
当 BOOT_MODE1 为 0,BOOT_MODE0 为 1 的时候此模式使能,串行下载的意思就是可 以通过 USB 或者 UART 将代码下载到板子上的外置存储设备中,我们可以使用 OTG1 这个 USB 口向开发板上的 SD/EMMC、NAND 等存储设备下载代码。我们需要将 BOOT_MODE1 拨到 “OFF”,将 BOOT_MODE0 拨到“ON”。这个下载是需要用到 NXP 提供的一个软件,一般用来最终量产的时候将代码烧写到外置存储设备中的,我们后面讲解如何使用。
1.2、 内部 BOOT 模式
当 BOOT_MODE1 为 1,BOOT_MODE0 为 0 的时候此模式使能,在此模式下,芯片会执 行内部的 boot ROM 代码,这段 boot ROM 代码会进行硬件初始化(一部分外设),然后从 boot 设 备(就是存放代码的设备、比如 SD/EMMC、NAND)中将代码拷贝出来复制到指定的 RAM 中, 一般是 DDR。
2、BOOT ROM 初始化内容
当我们设置 BOOT 模式为“内部 BOOT 模式”以后,I.MX6U 内部的 boot ROM 代码就会 执行,这个 boot ROM 代码都会做什么处理呢?首先肯定是初始化时钟,boot ROM 设置的系统 时钟如图
在图中 BT_FREQ 模式为 0,可以看到,boot ROM 会将 I.MX6U 的内核时钟设置为 396MHz,也就是主频为 396Mhz。System PLL=528Mhz,USB PLL=480MHz,AHB=132MHz, IPG=66MHz。
内部 boot ROM 为了加快执行速度会打开 MMU 和 Cache,下载镜像的时候 L1 ICache 会打 开,验证镜像的时候 L1 DCache、L2 Cache 和 MMU 都会打开。一旦镜像验证完成,boot ROM 就会关闭 L1 DCache、L2 Cache 和 MMU。 中断向量偏移会被设置到 boot ROM 的起始位置,当 boot ROM 启动了用户代码以后就可 以重新设置中断向量偏移了。一般是重新设置到我们用户代码的开始地方。
3、启动设备
当 BOOT_MODE 设置为内部 BOOT 模式以后,可以从以下设备中启动:
①、接到 EIM 接口的 CS0 上的 16 位 NOR Flash。
②、接到 EIM 接口的 CS0 上的 OneNAND Flash。
③、接到 GPMI 接口上的 MLC/SLC NAND Flash,NAND Flash 页大小支持 2KByte、4KByte 和 8KByte,8 位宽。
④、Quad SPI Flash。
⑤、接到 USDHC 接口上的 SD/MMC/eSD/SDXC/eMMC 等设备。
⑥、SPI 接口的 EEPROM。
I.MX6U 同样提供了 eFUSE 和 GPIO 配置两种,eFUSE 就不讲 解了。我们重点看如何通过 GPIO 来选择启动设备,因为所有的 I.MX6U 开发板都是通过 GPIO 来配置启动设备的。正如启动模式由 BOOT_MODE[1:0]来选择一样,启动设备是通过BOOT_CFG1[7:0]、BOOT_CFG2[7:0]和 BOOT_CFG4[7:0]这 24 个配置 IO,这 24 个配置 IO 刚 好对应着 LCD 的 24 根数据线 LCD_DATA0~LCDDATA23,当启动完成以后这 24 个 IO 就可以 作为 LCD 的数据线使用。这 24 根线和 BOOT_MODE1、BOOT_MODE0 共同组成了 I.MX6U 的启动选择引脚。
在图中大部分的 IO 都接地了,只有几个 IO 接高,尤其是 BOOT_CFG4[7:0] 这 8 个 IO 都 10K 电阻下拉接地,所以我们压根就不需要去关注 BOOT_CFG4[7:0]。我们需要 重点关注的就只剩下了 BOOT_CFG2[7:0]和 BOOT_CFG1[7:0]这 16 个 IO。
4、镜像烧写
学习 STM32 的时候我们可以直接将编译生成的.bin 文件烧写到 STM32 内部 flash 里面,但 是 I.MX6U 不能直接烧写编译生成的.bin 文件,我们需要在.bin 文件前面添加一些头信息构成 满足 I.MX6U 需求的最终可烧写文件,I.MX6U 的最终可烧写文件组成如下:
①、Image vector table,简称 IVT,IVT 里面包含了一系列的地址信息,这些地址信息在 ROM 中按照固定的地址存放着。
②、Boot data,启动数据,包含了镜像要拷贝到哪个地址,拷贝的大小是多少等等。
③、Device configuration data,简称 DCD,设备配置信息,重点是 DDR3 的初始化配置
④、用户代码可执行文件,比如 led.bin。
最终烧写到 I.MX6U 中的程序其组成为:IVT+Boot data+DCD+.bin。
所以第八章 中的 imxdownload 所生成的 load.imx 就是在 led.bin 前面加上 IVT+Boot data+DCD。内部 Boot ROM 会将 load.imx 拷贝到 DDR 中,用户代码是要一定要从 0X87800000 这个地方开始的,因 为链接地址为 0X87800000,load.imx 在用户代码前面又有 3KByte 的 IVT+Boot Data+DCD 数 据,下面会讲为什么是 3KByte,因此 load.imx 在 DDR 中的起始地址就是 0X87800000- 3072=0X877FF400。
4.1 IVT 和 Boot Data 数据
load.imx 最前面的就是 IVT 和 Boot Data,IVT 包含了镜像程序的入口点、指向 DCD 的指 针和一些用作其它用途的指针。内部 Boot ROM 要求 IVT 应该放到指定的位置,不同的启动设 备位置不同,而 IVT 在整个 load.imx 的最前面,其实就相当于要求 load.imx 在烧写的时候应该 烧写到存储设备的指定位置去。整个位置都是相对于存储设备的起始地址的偏移,
以 SD/EMMC 为例,IVT 偏移为 1Kbyte,IVT+Boot data+DCD 的总大小为 4KByte1KByte=3KByte。假如 SD/EMMC 每个扇区为 512 字节,那么 load.imx 应该从第三个扇区开始 烧写,前两个扇区要留出来。load.imx 从第 3KByte 开始才是真正的.bin 文件。那么 IVT 里面究 竟存放着什么东西呢?
从图 可以看到,第一个存放的就是 header(头),header 格式如图:
,Tag 为一个字节长度,固定为 0XD1,Length 是两个字节,保存着 IVT 长 度,为大端格式,也就是高字节保存在低内存中。最后的 Version 是一个字节,为 0X40 或者0X41。
图 9.4.1.4 是我们截取的 load.imx 的一部分内容,从地址 0X00000000~0X000025F,共 608 个字节的数据。我们将前 44 个字节的数据按照 4 个字节一组组合在一起就是:0X402000D1、 0X87800000、0X00000000、0X877FF42C、0X877FF420、0X877FF400、0X00000000、0X00000000、 0X877FF000、0X00200000、0X00000000。这 44 个字节的数据就是 IVT 和 Boot Data 数据,按 照图 9.4.1.2 和图 9.4.1.4 所示的 IVT 和 Boot Data 所示的格式对应起来如表 9.4.1.1 所示:
4.2、DCD数据
I.MX6U 提出了一个 DCD(Device Config Data)的概念,和 IVT、Boot Data 一样,DCD 也是添加到 load.imx 里面的,紧跟在 IVT 和 Boot Data 后面,IVT 里面也指定了 DCD 的位置。DCD 其实就是 I.MX6U 寄存器地址和对应 的配置信息集合,Boot ROM 会使用这些寄存器地址和配置集合来初始化相应的寄存器,比如 开启某些外设的时钟、初始化 DDR 等等。DCD 区域不能超过 1768Byte。
DCD 的 header 和 IVT 的 header 类似
其中 Tag 是单字节,固定为 0XD2,Length 为两个字节,表示 DCD 区域的大小,包含 header, 同样是大端模式,Version 是单字节,固定为 0X40 或者 0X41。
图 9.4.2.1 中的 CMD 就是要初始化的寄存器地址和相应的寄存器值
Tag 为一个字节,固定为 0XCC。Length 是两个字节,包含写入的命令数据长 度,包含 header,同样是大端模式。Parameter 为一个字节,这个字节的每个位含义如图
bytes 表示是目标位置宽度,单位为 byte,可以选择 1、2、和 4 字节。flags 是命令控制标志位。
Address 和 Vlalue/Mask 就是要初始化的寄存器地址和相应的寄存器值,注 意采用的是大端模式!DCD 结构就分析到这里,在分析 IVT 的时候我们就已经说过了,DCD 数据是从图 9.4.1.4 的 0X2C 地址开始的。
六、C语言版LED灯实验
我们有两部分文件要做:
①、汇编文件 汇编文件只是用来完成 C 语言环境搭建。
②、C 语言文件 C 语言文件就是完成我们的业务层代码的,其实就是我们实际例程要完成的功能
.global _start
_start:
mrs r0,cpsr
bic r0,r0,#0x1f
orr r0,r0,#0x13
msr cpsr, r0
ldr sp, =0X80200000
b main
此汇编部分程序执行完成,就几行代码,用来设置处理器运行到 SVC 模式下、然后初始 化 SP 指针、最终跳转到 C 文件的 main 函数中。
DCD 数据包含了 DDR 配置 参数,I.MX6U 内部的 Boot ROM 会读取 DCD 数据中的 DDR 配置参数然后完成 DDR 初始化 的。
在 I.MX6U 工作 在 396MHz(Boot ROM 设 置的 396MHz)的 主 频 的 时候 delay_short(0x7ff)基本能够实现大约 1ms 的延时,所以 delay()函数我们可以用来完成 ms 延时。
1、 链接脚本
arm-linux-gnueabihf-ld -Ttext 0X87800000 -o ledc.elf $^
上面语句中我们是通过“-Ttext”来指定链接地址是 0X87800000 的,这样的话所有的文件 都会链接到以 0X87800000 为起始地址的区域。但是有时候我们很多文件需要链接到指定的区 域,或者叫做段里面,比如在 Linux 里面初始化函数就会放到 init 段里面。因此我们需要能够 自定义一些段,这些段的起始地址我们可以自由指定,同样的我们也可以指定一个文件或者函 数应该存放到哪个段里面去。要完成这个功能我们就需要使用到链接脚本,看名字就知道链接 脚本主要用于链接的,用于描述文件应该如何被链接在一起形成最终的可执行文件。其主要目 的是描述输入文件中的段如何被映射到输出文件中,并且控制输出文件中的内存排布。比如我 们编译生成的文件一般都包含 text 段、data 段等等。
链接脚本的语法很简单,就是编写一系列的命令,这些命令组成了链接脚本,每个命令是 一个带有参数的关键字或者一个对符号的赋值,可以使用分号分隔命令。像文件名之类的字符 串可以直接键入,也可以使用通配符“*”。最简单的链接脚本可以只包含一个命令“SECTIONS”, 我们可以在这一个“SECTIONS”里面来描述输出文件的内存布局。我们一般编译出来的代码 都包含在 text、data、bss 和 rodata 这四个段内,假设现在的代码要被链接到 0X10000000 这个 地址,数据要被链接到 0X30000000 这个地方,下面就是完成此功能的最简单的链接脚本:
第 1 行我们先写了一个关键字“SECTIONS”,后面跟了一个大括号,这个大括号和第 7 行 的大括号是一对,这是必须的。看起来就跟 C 语言里面的函数一样。
第 2 行对一个特殊符号“.”进行赋值,“.”在链接脚本里面叫做定位计数器,默认的定位 计数器为 0。我们要求代码链接到以 0X10000000 为起始地址的地方,因此这一行给“.”赋值0X10000000,表示以 0X10000000 开始,后面的文件或者段都会以 0X10000000 为起始地址开 始链接。
第 3 行的“.text”是段名,后面的冒号是语法要求,冒号后面的大括号里面可以填上要链 接到“.text”这个段里面的所有文件,“(.text)”中的“”是通配符,表示所有输入文件的.text 段都放到“.text”中。
第 4 行,我们的要求是数据放到 0X30000000 开始的地方,所以我们需要重新设置定位计 数器“.”,将其改为 0X30000000。如果不重新设置的话会怎么样?假设“.text”段大小为 0X10000, 那么接下来的.data 段开始地址就是 0X10000000+0X10000=0X10010000,这明显不符合我们的 要求。所以我们必须调整定位计数器为 0X30000000。
第 5 行跟第 3 行一样,定义了一个名为“.data”的段,然后所有文件的“.data”段都放到 这里面。但是这一行多了一个“ALIGN(4)”,这是什么意思呢?这是用来对“.data”这个段的起 始地址做字节对齐的,ALIGN(4)表示 4 字节对齐。也就是说段“.data”的起始地址要能被 4 整 除,一般常见的都是 ALIGN(4)或者 ALIGN(8),也就是 4 字节或者 8 字节对齐。
第 6 行定义了一个“.bss”段,所有文件中的“.bss”数据都会被放到这个里面,“.bss”数 据就是那些定义了但是没有被初始化的变量。 上面就是链接脚本最基本的语法格式,我们接下
七、模仿 STM32 驱动开发格式实验
1、I.MX6U 寄存器定义
先将同属于一个外设的所有寄存器编写到一个结构体里面
使用“volatile”进行了修饰,目的是防止编 译器优化。
2、定义 IO 复用寄存器组的基地址
3、定义访问指针
八、官方 SDK 移植实验
1、I.MX6ULL 官方 SDK 包简介
IOMUXC_SetPinMux(IOMUXC_GPIO1_IO03_GPIO1_IO03, 0);
IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO03_GPIO1_IO03, 0X10B0);
IOMUXC_SetPinMux 是 用 来 设 置 IO 复 用 功 能 的 , 最 终 肯 定 设 置 的 是 寄 存 器 “IOMUXC_SW_MUX_CTL_PAD_XX”。
函数 IOMUXC_SetPinConfig 设置的是 IO 的上下拉、 速度等的,也就是寄存器“IOMUXC_SW_PAD_CTL_PAD_XX
函数 IOMUXC_SetPinMux 在文件 fsl_iomuxc.h 中定义,函数源码如下:
static inline void IOMUXC_SetPinMux(uint32_t muxRegister,
uint32_t muxMode,
uint32_t inputRegister,
uint32_t inputDaisy,
uint32_t configRegister,
uint32_t inputOnfield)
{
*((volatile uint32_t *)muxRegister) =
IOMUXC_SW_MUX_CTL_PAD_MUX_MODE(muxMode) | IOMUXC_SW_MUX_CTL_PAD_SION(inputOnfield);
if (inputRegister)
{
*((volatile uint32_t *)inputRegister) = IOMUXC_SELECT_INPUT_DAISY(inputDaisy);
}
}
函数 IOMUXC_SetPinMux 有 6 个参数,这 6 个参数的函数如下:
muxRegister : IO 的 复 用 寄 存 器 地 址 , 比 如 GPIO1_IO03 的 IO 复 用 寄 存 器 SW_MUX_CTL_PAD_GPIO1_IO03 的地址为 0X020E0068。
muxMode: IO 复用值,也就是 ALT0~ALT8,对应数字 0~8,比如要将 GPIO1_IO03 设置 为 GPIO 功能的话此参数就要设置为 5
inputRegister:外设输入 IO 选择寄存器地址,有些 IO 在设置为其他的复用功能以后还需 要设置 IO 输入寄存器,比如 GPIO1_IO03 要复用为 UART1_RX 的话还需要设置寄存器 UART1_RX_DATA_SELECT_INPUT,此寄存器地址为 0X020E0624。
inputDaisy:寄存器 inputRegister 的值,比如 GPIO1_IO03 要作为 UART1_RX 引脚的话此 参数就是 1。
configRegister:未使用,函数 IOMUXC_SetPinConfig 会使用这个寄存器。
inputOnfield : IO 软 件 输 入 使 能 , 以 GPIO1_IO03 为 例 就 是 寄 存 器 SW_MUX_CTL_PAD_GPIO1_IO03 的 SION 位(bit4)。如果需要使能 GPIO1_IO03 的软件输入功 能的话此参数应该为 1,否则的话就为 0。
九、BSP 工程管理实验
十、 蜂鸣器实验
十一、按键输入实验
I.MX6U 的 IO 不仅能作 为输出,而且也可以作为输入。
1、按键输入简介
按键就两个状态:按下或弹起,将按键连接到一个 IO 上,通过读取这个 IO 的值就知道按 键是按下的还是弹起的。至于按键按下的时候是高电平还是低电平要根据实际电路来判断。当 GPIO 连接按键的时候就要做为输入 使用。
十二、主频和时钟配置实验
默认配置下 I.MX6U 工作频率为 396MHz。但是 I.MX6U 系列标准的工作频率为 528MHz,有些 型号甚至可以工作到 696MHz。
1 、I.MX6U 时钟系统详解
1.1、系统时钟来源
I.MX6U-ALPHA 开发板的系统时钟来源于两部分:32.768KHz 和 24MHz 的晶振,其中 32.768KHz 晶振是 I.MX6U 的 RTC 时钟源,24MHz 晶振是 I.MX6U 内核 和其它外设的时钟源,也是我们重点要分析的。
1.2、7 路 PLL 时钟源
I.MX6U 的外设有很多,不同的外设时钟源不同,NXP 将这些外设的时钟源进行了分组, 一共有 7 组,这 7 组时钟源都是从 24MHz 晶振 PLL 而来的,因此也叫做 7 组 PLL。
①、 ARM_PLL(PLL1),此路 PLL 是供 ARM 内核使用的,ARM 内核时钟就是由此 PLL 生成的,此 PLL 通过编程的方式最高可倍频到 1.3GHz。
②、528_PLL(PLL2),此路 PLL 也叫做 System_PLL,此路 PLL 是固定的 22 倍频,不可编程修改因此,此路 PLL 时钟=24MHz * 22 = 528MHz,这也是为什么此 PLL 叫做 528_PLL 的 原因。此 PLL 分出了 4 路 PFD,分别为:PLL2_PFD0~PLL2_PFD3,这 4 路 PFD 和 528_PLL 共同作为其它很多外设的根时钟源。通常 528_PLL 和这 4 路 PFD 是 I.MX6U 内部系统总线的 时钟源,比如内处理逻辑单元、DDR 接口、NAND/NOR 接口等等。
③、USB1_PLL(PLL3),此路 PLL 主要用于 USBPHY,此 PLL 也有四路 PFD,为: PLL3_PFD0~PLL3_PFD3,USB1_PLL 是固定的 20 倍频,因此 USB1_PLL=24MHz *20=480MHz。 USB1_PLL虽然主要用于USB1PHY,但是其和四路PFD同样也可以作为其他外设的根时钟源。
④、USB2_PLL(PLL7,没有写错!就是 PLL7,虽然序号标为 4,但是实际是 PLL7),看名 字就知道此路PLL是给USB2PHY使用的。同样的,此路PLL固定为20倍频,因此也是480MHz。
⑤、ENET_PLL(PLL6),此路 PLL 固定为 20+5/6 倍频,因此 ENET_PLL=24MHz * (20+5/6) = 500MHz。此路 PLL 用于生成网络所需的时钟,可以在此 PLL 的基础上生成 25/50/100/125MHz 的网络时钟。
⑥、VIDEO_PLL(PLL5),此路 PLL 用于显示相关的外设,比如 LCD,此路 PLL 的倍频可以 调整,PLL 的输出范围在 650MHz~1300MHz。此路 PLL 在最终输出的时候还可以进行分频, 可选 1/2/4/8/16 分频。
⑦、AUDIO_PLL(PLL4),此路 PLL 用于音频相关的外设,此路 PLL 的倍频可以调整,PLL 的输出范围同样也是 650MHz~1300MHz,此路 PLL 在最终输出的时候也可以进行分频,可选 1/2/4 分频。
1.3 、时钟树简介
一共有三部分:CLOCK_SWITCHER、CLOCK ROOT GENERATOR 和 SYSTEM CLOCKS。其中左边的 CLOCK_SWITCHER 就是我们上一小节讲解的那 7 路 PLL 和 8 路 PFD,右边的 SYSTEM CLOCKS 就是芯片外设,中间的 CLOCK ROOT GENERATOR 是最 复杂的!这一部分就像“月老”一样,给左边的CLOCK_SWITCHER和右边的SYSTEM CLOCKS 进行牵线搭桥。外设时钟源是有多路可以选择的,CLOCK ROOT GENERATOR 就负责从 7 路 PLL 和 8 路 PFD 中选择合适的时钟源给外设使用。具体操作肯定是设置相应的寄存器
①、此部分是时钟源选择器,ESAI 有 4 个可选的时钟源:PLL4、PLL5、PLL3_PFD2 和 pll3_sw_clk 。 具 体 选 择 哪 一 路 作 为 ESAI 的时钟源是由寄存器 CCM->CSCMR2 的 ESAI_CLK_SEL 位来决定的,用户可以自由配置,配置如图 16.1.3.3 所示:
②、此部分是 ESAI 时钟的前级分频,分频值由寄存器 CCM_CS1CDR 的 ESAI_CLK_PRED 来确定的,可设置 1~8 分频,假如现在 PLL4=650MHz,我们选择 PLL4 作为 ESAI 时钟,前级 分频选择 2 分频,那么此时的时钟就是 650/2=325MHz。
③、此部分又是一个分频器,对②中输出的时钟进一步分频,分频值由寄存器 CCM_CS1CDR 的 ESAI_CLK_PODF 来决定,可设置 1~8 分频。假如我们设置为 8 分频的话, 经过此分频器以后的时钟就是 325/8=40.625MHz。因此最终进入到 ESAI 外设的时钟就是 40.625MHz。
1.4、内核时钟设置
①、内核时钟源来自于 PLL1,假如此时 PLL1 为 996MHz。
②、通过寄存器 CCM_CACRR 的 ARM_PODF 位对 PLL1 进行分频,可选择 1/2/4/8 分频, 假如我们选择 2 分频,那么经过分频以后的时钟频率是 996/2=498MHz。
③、大家不要被此处的 2 分频给骗了,此处没有进行 2 分频(我就被这个 2 分频骗了好久, 主频一直配置不正确!)。
④、经过第②步 2 分频以后的 498MHz 就是 ARM 的内核时钟,也就是 I.MX6U 的主频。
经过上面几步的分析可知,假如我们要设置内核主频为 528MHz,那么 PLL1 可以设置为 1056MHz,寄存器 CCM_CACRR 的 ARM_PODF 位设置为 2 分频即可。同理,如果要将主频设 置为 696MHz,那么 PLL1 就可以设置为 696MHz,CCM_CACRR 的 ARM_PODF 设置为 1 分 频即可。现在问题很清晰了,寄存器 CCM_CACRR 的 ARM_PODF 位很好设置,PLL1 的频率 可以通过寄存器 CCM_ANALOG_PLL_ARMn 来设置。接下来详细的看一下 CCM_CACRR 和 CCM_ANALOG_PLL_ARMn 这两个寄存器,CCM_CACRR 寄存器结构如图 16.1.4.2 所示
寄存器 CCM_CACRR 只有 ARM_PODF 位,可以设置为 0~7,分别对应 1~8 分频。如果要 设置为2分频的话CCM_CACRR就要设置为1。再来看一下寄存器CCM_ANALOG_PLL_ARMn,
ENABLE: 时钟输出使能位,此位设置为 1 使能 PLL1 输出,如果设置为 0 的话就关闭 PLL1 输出。
DIV_SELECT: 此位设置 PLL1 的输出频率,可设置范围为:54~108,PLL1 CLK = Fin * div_seclec/2.0,Fin=24MHz。如果 PLL1 要输出 1056MHz 的话,div_select 就要设置为 88。
在修改 PLL1 时钟频率的时候我们需要先将内核时钟源改为其他的时钟源,PLL1 可选择的 时钟源如图 16.1.4.4 所示:
①、pll1_sw_clk 也就是 PLL1 的最终输出频率。
②、此处是一个选择器,选择 pll1_sw_clk 的时钟源,由寄存器 CCM_CCSR 的 PLL1_SW_CLK_SEL 位决定 pll1_sw_clk 是选择 pll1_main_clk 还是 step_clk。正常情况下应该 选择 pll1_main_clk,但是如果要对 pll1_main_clk(PLL1)的频率进行调整的话,比如我们要设置 PLL1=1056MHz,此时就要先将 pll1_sw_clk 切换到 step_clk 上。等 pll1_main_clk 调整完成以后 再切换回来。
③、此处也是一个选择器,选择 step_clk 的时钟源,由寄存器 CCM_CCSR 的 STEP_SEL 位 来决定 step_clk 是选择 osc_clk 还是 secondary_clk。一般选择 osc_clk,也就是 24MHz 的晶振。
寄存器 CCM_CCSR 我们只用到了 STEP_SEL、PLL1_SW_CLK_SEL 这两个位,一个是用 来选择 step_clk 时钟源的,一个是用来选择 pll1_sw_clk 时钟源的。 到这里,修改 I.MX6U 主频的步骤就很清晰了,修改步骤如下:
①、 设置寄存器 CCSR 的 STEP_SEL 位,设置 step_clk 的时钟源为 24M 的晶振。
②、设置寄存器 CCSR 的 PLL1_SW_CLK_SEL 位,设置 pll1_sw_clk 的时钟源为 step_clk=24MHz,通过这一步我们就将 I.MX6U 的主频先设置为 24MHz,直接来自于外部的 24M 晶振。
③、设置寄存器 CCM_ANALOG_PLL_ARMn,将 pll1_main_clk(PLL1)设置为 1056MHz。
④、设置寄存器 CCSR 的 PLL1_SW_CLK_SEL 位,重新将 pll1_sw_clk 的时钟源切换回 pll1_main_clk,切换回来以后的 pll1_sw_clk 就等于 1056MHz。
⑤、最后设置寄存器 CCM_CACRR 的 ARM_PODF 为 2 分频,I.MX6U 的内核主频就为 1056/2=528MHz。
1.5、PFD时钟设置
PLL2、PLL3 和 PLL7 固定为 528MHz、480MHz 和 480MHz,PLL4~PLL6 都是针对特殊外设 的,用到的时候再设置。因此,接下来重点就是设置 PLL2 和 PLL3 的各自 4 路 PFD,NXP 推 荐的这 8 路 PFD 频率如表 16.1.5.1 所示:
先设置 PLL2 的 4 路 PFD 频率,用到寄存器是 CCM_ANALOG_PFD_528n,寄存器结构如图所示:
寄存器 CCM_ANALOG_PFD_528n 其实分为四组,分别对应 PFD0~PFD3,每组 8 个 bit,
我们就以 PFD0 为例,看一下如何设置 PLL2_PFD0 的频率。PFD0 对应的寄存器位如下:
PFD0_FRAC: PLL2_PFD0 的分频数,PLL2_PFD0 的计算公式为 528x18/PFD0_FRAC,此 为可设置的范围为 12~35 。 如 果 PLL2_PFD0 的频率要设置为 352MHz 的 话 PFD0_FRAC=528x18/352=27。
PFD0_STABLE: 此位为只读位,可以通过读取此位判断 PLL2_PFD0 是否稳定。
PFD0_CLKGATE: PLL2_PFD0 输出使能位,为 1 的时候关闭 PLL2_PFD0 的输出,为 0 的 时候使能输出。
如果我们要设置 PLL2_PFD0 的频率为 352MHz 的话就需要设置 PFD0_FRAC 为 27, PFD0_CLKGATE 为 0 。 PLL2_PFD1~PLL2_PFD3 设置类似,频率计算公式都是 528*18/PFDX_FRAC(X=1~3) ,因此 PLL2_PFD1=594MHz 的话, PFD1_FRAC=16 ; PLL2_PFD2=400MHz 的话 PFD2_FRAC 不能整除,因此取最近的整数值,即 PFD2_FRAC=24, 这样 PLL2_PFD2 实际为 396MHz;PLL2_PFD3=297MHz 的话,PFD3_FRAC=32。
设 置 PLL3_PFD0~PLL3_PFD3 这 4 路 PFD 的 频 率 , 使 用 到 的 寄 存 器 是 CCM_ANALOG_PFD_480n,此寄存器结构如图 16.1.5.2 所示:
寄存器 CCM_ANALOG_PFD_480n 和 CCM_ANALOG_PFD_528n 的结构是一模一样的,只是一个是 PLL2 的,一个是 PLL3 的。寄存器位的含义也是一样的,只 是 频 率 计 算 公 式 不 同 , 比 如 PLL3_PFDX=480*18/PFDX_FRAC(X=0~3) 。 如 果 PLL3_PFD0=720MHz 的话,PFD0_FRAC=12;如果 PLL3_PFD1=540MHz 的话,PFD1_FRAC=16; 如果 PLL3_PFD2=508.2MHz 的话,PFD2_FRAC=17;如果 PLL3_PFD3=454.7MHz 的话, PFD3_FRAC=19。
1.6、AHB、IPG 和 PERCLK 根时钟设置
7 路 PLL 和 8 路 PFD 设置完成以后最后还需要设置 AHB_CLK_ROOT 和 IPG_CLK_ROOT 的时钟,I.MX6U 外设根时钟可设置范围如图 16.1.6.1 所示:
AHB_CLK_ROOT 最高可以设置 132MHz, IPG_CLK_ROOT和PERCLK_CLK_ROOT最高可以设置66MHz。那我们就将AHB_CLK_ROOT、 IPG_CLK_ROOT 和 PERCLK_CLK_ROOT 分 别 设 置 为 132MHz 、 66MHz 、 66MHz 。 AHB_CLK_ROOT 和 IPG_CLK_ROOT 的设计如图 16.1.6.2 所示:
①、此选择器用来选择 pre_periph_clk 的时钟源,可以选择 PLL2、PLL2_PFD2、PLL2_PFD0 和 PLL2_PFD2/2。寄存器 CCM_CBCMR 的 PRE_PERIPH_CLK_SEL 位决定选择哪一个,默认 选择 PLL2_PFD2,因此 pre_periph_clk=PLL2_PFD2=396MHz。
②、此选择器用来选择 periph_clk 的时钟源,由寄存器 CCM_CBCDR 的 PERIPH_CLK_SEL 位与 PLL_bypass_en2 组成的或来选择。当 CCM_CBCDR 的 PERIPH_CLK_SEL 位为 0 的时候 periph_clk=pr_periph_clk=396MHz。
③、通过 CBCDR 的 AHB_PODF 位来设置 AHB_CLK_ROOT 的分频值,可以设置 1~8 分 频,如果想要 AHB_CLK_ROOT=132MHz 的话就应该设置为 3 分频:396/3=132MHz。图 16.1.6.2 中虽然写的是默认 4 分频,但是 I.MX6U 的内部 boot rom 将其改为了 3 分频!
④、通过 CBCDR 的 IPG_PODF 位来设置 IPG_CLK_ROOT 的分频值,可以设置 1~4 分频, IPG_CLK_ROOT 时钟源是 AHB_CLK_ROOT,要想 IPG_CLK_ROOT=66MHz 的话就应该设置 2 分频:132/2=66MHz。
PERCLK_CLK_ROOT 时钟频率,其时钟结构图如图:
从 图 16.1.6.3 可 以 看 出 , PERCLK_CLK_ROOT 来 源 有 两 种 : OSC(24MHz) 和 IPG_CLK_ROOT,由寄存器 CCM_CSCMR1 的 PERCLK_CLK_SEL 位来决定,如果为 0 的话 PERCLK_CLK_ROOT 的时钟源就是 IPG_CLK_ROOT=66MHz 。可以通过寄存器 CCM_CSCMR1 的 PERCLK_PODF 位来设置分频,如果要设置 PERCLK_CLK_ROOT 为 66MHz 的话就要设置为 1 分频。
CCM_CBCDR、CCM_CBCMR 和 CCM_CSCMR1,我 们依次来看一下这些寄存器,CCM_CBCDR 寄存器结构如图
PERIPH_CLK2_PODF:periph2 时钟分频,可设置 0~7,分别对应 1~8 分频。
PERIPH2_CLK_SEL:选择 peripheral2 的主时钟,如果为 0 的话选择 PLL2,如果为 1 的 话选择 periph2_clk2_clk。修改此位会引起一次与 MMDC 的握手,所以修改完成以后要等待握 手完成,握手完成信号由寄存器 CCM_CDHIPR 中指定位表示。
PERIPH_CLK_SEL:peripheral 主时钟选择,如果为 0 的话选择 PLL2,如果为 1 的话选 择 periph_clk2_clock。修改此位会引起一次与 MMDC 的握手,所以修改完成以后要等待握手完 成,握手完成信号由寄存器 CCM_CDHIPR 中指定位表示。
AXI_PODF:axi 时钟分频,可设置 0~7,分别对应 1~8 分频。
AHB_PODF:ahb 时钟分频,可设置 0~7,分别对应 1~8 分频。修改此位会引起一次与 MMDC 的握手,所以修改完成以后要等待握手完成,握手完成信号由寄存器 CCM_CDHIPR 中 指定位表示。 IPG_PODF:ipg 时钟分频,可设置 0~3,分别对应 1~4 分频。
AXI_ALT_CLK_SEL:axi_alt 时钟选择,为 0 的话选择 PLL2_PFD2,如果为 1 的话选择 PLL3_PFD1。
AXI_CLK_SEL:axi 时钟源选择,为 0 的话选择 periph_clk,为 1 的话选择 axi_alt 时钟。
FABRIC_MMDC_PODF:fabric/mmdc 时钟分频设置,可设置 0~7,分别对应 1~8 分频。
PERIPH2_CLK2_PODF:periph2_clk2 的时钟分频,可设置 0~7,分别对应 1~8 分频。
寄存器 CCM_CBCMR:
LCDIF1_PODF:lcdif1 的时钟分频,可设置 0~7,分别对应 1~8 分频。
PRE_PERIPH2_CLK_SEL:pre_periph2 时钟源选择,00 选择 PLL2,01 选择 PLL2_PFD2, 10 选择 PLL2_PFD0,11 选择 PLL4。
PERIPH2_CLK2_SEL:periph2_clk2 时钟源选择为 0 的时候选择 pll3_sw_clk,为 1 的时候 选择 OSC。 PRE_PERIPH_CLK_SEL:pre_periph 时钟源选择,00 选择 PLL2,01 选择 PLL2_PFD2,10 选 择 PLL2_PFD0,11 选择 PLL2_PFD2/2。
PERIPH_CLK2_SEL:peripheral_clk2 时钟源选择,00 选择 pll3_sw_clk,01 选择 osc_clk, 10 选择 pll2_bypass_clk。
此寄存器主要用于外设时钟源的选择,比如 QSPI1、ACLK、GPMI、BCH 等外设,我们重 点看一下下面两个位: PERCLK_CK_SEL:perclk 时钟源选择,为 0 的话选择 ipg clk,为 1 的话选择 osc clk。
PERCLK_PODF:perclk 的时钟分频,可设置 0~7,分别对应 1~8 分频。
在修改如下时钟选择器或者分频器的时候会引起与 MMDC 的握手发生: ①、mmdc_podf ②、periph_clk_sel ③、periph2_clk_sel ④、arm_podf ⑤、ahb_podf 发生握手信号以后需要等待握手完成,寄存器 CCM_CDHIPR 中保存着握手信号是否完成, 如果相应的位为 1 的话就表示握手没有完成,如果为 0 的话就表示握手完成,很简单,这里就 不详细的列举寄存器 CCM_CDHIPR 中的各个位了。 另外在修改 arm_podf 和 ahb_podf 的时候需要先关闭其时钟输出,等修改完成以后再打开, 否则的话可能会出现在修改完成以后没有时钟输出的问题。
十三、GPIO 中断实验
中断系统是一个处理器重要的组成部分,中断系统极大的提高了 CPU 的执行效率。
1、STM32 中断系统回顾
STM32 的中断系统主要有以下几个关键点:
①、中断向量表。 ②、NVIC(内嵌向量中断控制器)。 ③、中断使能。 ④、中断服务函数
1.1、中断向量表
中断向量表是一个表,这个表里面存放的是中断向量。中断服务程序的入口地址或存放中 断服务程序的首地址成为中断向量,因此中断向量表是一系列中断服务程序入口地址组成的表。 这些中断服务程序(函数)在中断向量表中的位置是由半导体厂商定好的,当某个中断被触发以 后就会自动跳转到中断向量表中对应的中断服务程序(函数)入口地址处。中断向量表在整个程 序的最前面。
我们说 ARM 处理器都是从地址 0X00000000 开始运行的,但是我们学习 STM32 的时候 代码是下载到 0X8000000 开始的存储区域中。因此中断向量表是存放到 0X8000000 地址处 的,而不是 0X00000000,这样不是就出错了吗?为了解决这个问题,Cortex-M 架构引入了一 个新的概念——中断向量表偏移,通过中断向量表偏移就可以将中断向量表存放到任意地址 处,中断向量表偏移配置在函数 SystemInit 中完成,通过向 SCB_VTOR 寄存器写入新的中断 向量表首地址即可
I.MX6U 所使用的 Cortex-A7 内核也有中断向量表和中断向量 表偏移,而且其含义和 STM32 是一模一样的!只是用到的寄存器不同而已,概念完全相同!
1.2、NVIC(内嵌向量中断控制器)
中断系统得有个管理机构,对于 STM32 这种 Cortex-M 内核的单片机来说这个管理机构叫 做 NVIC,全称叫做 Nested Vectored Interrupt Controller。既 然 Cortex-M 内核有个中断系统的管理机构—NVIC,那么 I.MX6U 所使用的 Cortex-A7 内核是 不是也有个中断系统管理机构?答案是肯定的,不过 Cortex-A 内核的中断管理机构不叫做 NVIC,而是叫做 GIC,全称是 general interrupt controller。
1.3、中断使能
要使用某个外设的中断,肯定要先使能这个外设的中断,以 STM32F103 的 PE2 这个 IO 为 例,假如我们要使用 PE2 的输入中断肯定要使用如下代码来使能对应的中断:
1.4、中断服务函数
我们使用中断的目的就是为了使用中断服务函数,当中断发生以后中断服务函数就会被调 用,我们要处理的工作就可以放到中断服务函数中去完成。同样以 STM32F103 的 PE2 为例, 其中断服务函数如下所示:
当 PE2 引脚的中断触发以后就会调用其对应的中断处理函数 EXTI2_IRQHandler,我们可 以在函数 EXTI2_IRQHandler 中添加中断处理代码。同理,I.MX6U 也有中断服务函数,当某个 外设中断发生以后就会调用其对应的中断服务函数。
2、Cortex-A7 中断系统简介
跟 STM32 一样,Cortex-A7 也有中断向量表,中断向量表也是在代码的最前面。CortexA7 内核有 8 个异常中断。
中断向量表里面都是中断服务函数的入口地址,因此一款芯片有什么中断都是可以从中断 向量表看出来的。
对于 Cortex-M 内 核来说,中断向量表列举出了一款芯片所有的中断向量,包括芯片外设的所有中断。对于 CotexA 内核来说并没有这么做,在表 17.1.2.1 中有个 IRQ 中断, Cortex-A 内核 CPU 的所有外部中 断都属于这个 IRQ 中断,当任意一个外部中断发生的时候都会触发 IRQ 中断。在 IRQ 中断服 务函数里面就可以读取指定的寄存器来判断发生的具体是什么中断,进而根据具体的中断做出 相应的处理。这些外部中断和 IRQ 中断的关系如图 17.1.2.1 所示:
左侧的 Software0_IRQn~PMU_IRQ2_IRQ 这些都是 I.MX6U 的中断,他 们都属于 IRQ 中断。当图 17.1.2.1 左侧这些中断中任意一个发生的时候 IRQ 中断都会被触发, 所以我们需要在 IRQ 中断服务函数中判断究竟是左侧的哪个中断发生了,然后再做出具体的处 理。
①、复位中断(Rest),CPU 复位以后就会进入复位中断,我们可以在复位中断服务函数里面 做一些初始化工作,比如初始化 SP 指针、DDR 等等。
②、未定义指令中断(Undefined Instruction),如果指令不能识别的话就会产生此中断。
③、软中断(Software Interrupt,SWI),由 SWI 指令引起的中断,Linux 的系统调用会用 SWI 指令来引起软中断,通过软中断来陷入到内核空间。
④、指令预取中止中断(Prefetch Abort),预取指令的出错的时候会产生此中断。
⑤、数据访问中止中断(Data Abort),访问数据出错的时候会产生此中断。
⑥、IRQ 中断(IRQ Interrupt),外部中断,前面已经说了,芯片内部的外设中断都会引起此 中断的发生。
⑦、FIQ 中断(FIQ Interrupt),快速中断,如果需要快速处理中断的话就可以使用此中断。
3、GIC 控制器简介
3.1、GIC 控制器总览
GIC 是 ARM 公司给 Cortex-A/R 内核提供的一个中断控制器,类似 Cortex-M 内核中的 NVIC。目前 GIC 有 4 个版本:V1~V4,V1 是最老的版本,已经被废弃了。V2~V4 目前正在大 量的使用。GIC V2 是给 ARMv7-A 架构使用的,比如 Cortex-A7、Cortex-A9、Cortex-A15 等, V3 和 V4 是给 ARMv8-A/R 架构使用的,也就是 64 位芯片使用的。I.MX6U 是 Cortex-A 内核 的,因此我们主要讲解 GIC V2。GIC V2 最多支持 8 个核。ARM 会根据 GIC 版本的不同研发 出不同的 IP 核,那些半导体厂商直接购买对应的 IP 核即可,比如 ARM 针对 GIC V2 就开发出 了 GIC400 这个中断控制器 IP 核。当 GIC 接收到外部中断信号以后就会报给 ARM 内核,但是 ARM 内核只提供了四个信号给 GIC 来汇报中断情况:VFIQ、VIRQ、FIQ 和 IRQ,他们之间的 关系如图 17.1.3.1 所示:
VFIQ:虚拟快速 FIQ。 VIRQ:虚拟外部 IRQ。 FIQ:快速中断 IRQ。 IRQ:外部中断 IRQ。
VFIQ 和 VIRQ 是针对虚拟化的,我们不讨论虚拟化,剩下的就是 FIQ 和 IRQ 了,我们前 面都讲了很多次了。本教程我们只使用 IRQ,所以相当于 GIC 最终向 ARM 内核就上报一个 IRQ 信号。那么 GIC 是如何完成这个工作的呢?
左侧部分就是中断源,中间部分就是 GIC 控制器,最右侧就是中断控制器向 处理器内核发送中断信息。我们重点要看的肯定是中间的 GIC 部分,GIC 将众多的中断源分为 分为三类:
①、SPI(Shared Peripheral Interrupt),共享中断,顾名思义,所有 Core 共享的中断,这个是最 常见的,那些外部中断都属于 SPI 中断(注意!不是 SPI 总线那个中断) 。比如按键中断、串口 中断等等,这些中断所有的 Core 都可以处理,不限定特定 Core。
②、PPI(Private Peripheral Interrupt),私有中断,我们说了 GIC 是支持多核的,每个核肯定 有自己独有的中断。这些独有的中断肯定是要指定的核心处理,因此这些中断就叫做私有中断。
③、SGI(Software-generated Interrupt),软件中断,由软件触发引起的中断,通过向寄存器 GICD_SGIR 写入数据来触发,系统会使用 SGI 中断来完成多核之间的通信。
3.2、中断 ID
中断源有很多,为了区分这些不同的中断源肯定要给他们分配一个唯一 ID,这些 ID 就是 中断 ID。每一个 CPU 最多支持 1020 个中断 ID,中断 ID 号为 ID0~ID1019。这 1020 个 ID 包 含了 PPI、SPI 和 SGI,那么这三类中断是如何分配这 1020 个中断 ID 的呢?这 1020 个 ID 分 配如下:
ID0~ID15:这 16 个 ID 分配给 SGI。
ID16~ID31:这 16 个 ID 分配给 PPI。
ID32~ID1019:这 988 个 ID 分配给 SPI,像 GPIO 中断、串口中断等这些外部中断 ,至于 具体到某个 ID 对应哪个中断那就由半导体厂商根据实际情况去定义了。
3.3、GIC 逻辑分块
GIC 架构分为了两个逻辑块:Distributor 和 CPU Interface,也就是分发器端和 CPU 接口端。
Distributor(分发器端):此逻辑块负责处理各个中断事件的分发问 题,也就是中断事件应该发送到哪个 CPU Interface 上去。分发器收集所有的中断源,可以控制 每个中断的优先级,它总是将优先级最高的中断事件发送到 CPU 接口端。分发器端要做的主要 工作如下:
①、全局中断使能控制。 ②、控制每一个中断的使能或者关闭。 ③、设置每个中断的优先级。 ④、设置每个中断的目标处理器列表。 ⑤、设置每个外部中断的触发模式:电平触发或边沿触发。 ⑥、设置每个中断属于组 0 还是组 1。
CPU Interface(CPU 接口端):CPU 接口端听名字就知道是和 CPU Core 相连接的,因此在 图 17.1.3.2 中每个 CPU Core 都可以在 GIC 中找到一个与之对应的 CPU Interface。CPU 接口端 就是分发器和 CPU Core 之间的桥梁,CPU 接口端主要工作如下:
①、使能或者关闭发送到 CPU Core 的中断请求信号。
②、应答中断。
③、通知中断处理完成。
④、设置优先级掩码,通过掩码来设置哪些中断不需要上报给 CPU Core。
⑤、定义抢占策略。
⑥、当多个中断到来的时候,选择优先级最高的中断通知给 CPU Core。
3.4、CP15 协处理器
CP15 协处理器一般用于存储系统管理,但是在中断中也会使用到,CP15 协处理器一共有 16 个 32 位寄存器。CP15 协处理器的访问通过如下另个指令完成:
MRC: 将 CP15 协处理器中的寄存器数据读到 ARM 寄存器中。
MCR: 将 ARM 寄存器的数据写入到 CP15 协处理器寄存器中。 MRC 就是读 CP15 寄存器,MCR 就是写 CP15 寄存器,MCR 指令格式如下
cond:指令执行的条件码,如果忽略的话就表示无条件执行。
opc1:协处理器要执行的操作码。
Rt:ARM 源寄存器,要写入到 CP15 寄存器的数据就保存在此寄存器中。
CRn:CP15 协处理器的目标寄存器。
CRm:协处理器中附加的目标寄存器或者源操作数寄存器,如果不需要附加信息就将 CRm 设置为 C0,否则结果不可预测。
opc2:可选的协处理器特定操作码,当不需要的时候要设置为 0。
MRC 的指令格式和 MCR 一样,只不过在 MRC 指令中 Rt 就是目标寄存器,也就是从 CP15 指定寄存器读出来的数据会保存在 Rt 中。而 CRn 就是源寄存器,也就是要读取的写处 理器寄存器。
假如我们要将 CP15 中 C0 寄存器的值读取到 R0 寄存器中,那么就可以使用如下命令:
MRC p15, 0, r0, c0, c0, 0
CP15 协处理器有 16 个 32 位寄存器,c0~c15,本章来看一下 c0、c1、c12 和 c15 这四个寄 存器
3.4.1 c0 寄存器
当 MRC/MCR 指令中的 CRn=c0,opc1=0,CRm=c0,opc2=0 的时候就表示 此时的 c0 就是 MIDR 寄存器,也就是主 ID 寄存器,这个也是 c0 的基本作用。
bit31:24:厂商编号,0X41,ARM。 bit23:20:内核架构的主版本号,ARM 内核版本一般使用 rnpn 来表示,比如 r0p1,其中 r0 后面的 0 就是内核架构主版本号。 bit19:16:架构代码,0XF,ARMv7 架构。 bit15:4:内核版本号,0XC07,Cortex-A7 MPCore 内核。 bit3:0:内核架构的次版本号,rnpn 中的 pn,比如 r0p1 中 p1 后面的 1 就是次版本号。
3.4.2、 c1 寄存器
当 MRC/MCR 指令中的 CRn=c1,opc1=0,CRm=c0,opc2=0 的时候就表示 此时的 c1 就是 SCTLR 寄存器,也就是系统控制寄存器,这个是 c1 的基本作用。SCTLR 寄存 器主要是完成控制功能的,比如使能或者禁止 MMU、I/D Cache 等,
bit13:V , 中断向量表基地址选择位,为 0 的话中断向量表基地址为 0X00000000,软件可 以使用 VBAR 来重映射此基地址,也就是中断向量表重定位。为 1 的话中断向量表基地址为 0XFFFF0000,此基地址不能被重映射。 bit12:I,I Cache 使能位,为 0 的话关闭 I Cache,为 1 的话使能 I Cache。 bit11:Z,分支预测使能位,如果开启 MMU 的话,此位也会使能。 bit10:SW,SWP 和 SWPB 使能位,当为 0 的话关闭 SWP 和 SWPB 指令,当为 1 的时候 就使能 SWP 和 SWPB 指令。 bit9:3:未使用,保留。 bit2:C,D Cache 和缓存一致性使能位,为 0 的时候禁止 D Cache 和缓存一致性,为 1 时 使能。 bit1:A,内存对齐检查使能位,为 0 的时候关闭内存对齐检查,为 1 的时候使能内存对齐 检查。 bit0:M,MMU 使能位,为 0 的时候禁止 MMU,为 1 的时候使能 MMU。
3.4.3、c12 寄存器
当 MRC/MCR 指令中的 CRn=c12,opc1=0,CRm=c0,opc2=0 的时候就表 示此时 c12 为 VBAR 寄存器,也就是向量表基地址寄存器。设置中断向量表偏移的时候就需要 将新的中断向量表基地址写入 VBAR 中,比如在前面的例程中,代码链接的起始地址为 0X87800000,而中断向量表肯定要放到最前面,也就是 0X87800000 这个地址处。所以就需要 设置 VBAR 为 0X87800000,设置命令如下:
3.4.4、c15 寄存器
MRC p15, 4, r1, c15, c0, 0 ; 获取 GIC 基础地址,基地址保存在 r1 中。
获取到 GIC 基地址以后就可以设置 GIC 相关寄存器了,比如我们可以读取当前中断 ID, 当前中断 ID 保存在 GICC_IAR 中,寄存器 GICC_IAR 属于 CPU 接口端寄存器,寄存器地址 相对于 CPU 接口端起始地址的偏移为 0XC,
MRC p15, 4, r1, c15, c0, 0 ;获取 GIC 基地址
ADD r1, r1, #0X2000 ;GIC 基地址加 0X2000 得到 CPU 接口端寄存器起始地址
LDR r0, [r1, #0XC] ;读取 CPU 接口端起始地址+0XC 处的寄存器值,也就是寄存器 ;GIC_IAR 的值
下,通过 c0 寄存器可以获取到处理器内 核信息;通过 c1 寄存器可以使能或禁止 MMU、I/D Cache 等;通过 c12 寄存器可以设置中断 向量偏移;通过 c15 寄存器可以获取 GIC 基地址。
3.5、 中断使能
中断使能包括两部分,一个是 IRQ 或者 FIQ 总中断使能,另一个就是 ID0~ID1019 这 1020 个中断源的使能。
3.5.1、IRQ 和 FIQ 总中断使能
IRQ 和 FIQ 分别是外部中断和快速中断的总开关,就类似家里买的进户总电闸,然后 ID0~ID1019 这 1020 个中断源就类似家里面的各个电器开关。要想开电视,那肯定要保证进户 总电闸是打开的,因此要想使用 I.MX6U 上的外设中断就必须先打开 IRQ 中断(本教程不使用 FIQ)。
寄存器 CPSR 的 I=1 禁止 IRQ,当 I=0 使 能 IRQ;F=1 禁止 FIQ,F=0 使能 FIQ。
3.5.2 ID0~ID1019 中断使能和禁止
GIC 寄存器 GICD_ISENABLERn 和 GICD_ ICENABLERn 用来完成外部中断的使能和禁 止,对于 Cortex-A7 内核来说中断 ID 只使用了 512 个。一个 bit 控制一个中断 ID 的使能,那么 就需要 512/32=16 个 GICD_ISENABLER 寄存器来完成中断的使能。同理,也需要 16 个 GICD_ICENABLER 寄存器来完成中断的禁止。其中 GICD_ISENABLER0 的 bit[15:0]对应 ID15~0 的 SGI 中断,GICD_ISENABLER0 的 bit[31:16]对应 ID31~16 的 PPI 中断。剩下的 GICD_ISENABLER1~GICD_ISENABLER15 就是控制 SPI 中断的。
3.6、中断优先级设置
3.6.1、优先级数配置
Cortex-A7 的中断优先级也可以分为抢占优先级和子优先级,两者同样是可以配置 的。GIC 控制器最多可以支持 256 个优先级,数字越小,优先级越高!Cortex-A7 选择了 32 个 优先级。在使用中断的时候需要初始化 GICC_PMR 寄存器,此寄存器用来决定使用几级优先 级,寄存器结构如图 17.1.6.1 所示:
GICC_PMR 寄存器只有低 8 位有效,这 8 位最多可以设置 256 个优先级,其他优先级数设 置如表 17.1.6.1 所示:
I.MX6U 是 Cortex-A7 内核,所以支持 32 个优先级,因此 GICC_PMR 要设置为 0b11111000。
3.6.2、抢占优先级和子优先级位数设置
抢占优先级和子优先级各占多少位是由寄存器 GICC_BPR 来决定的,GICC_BPR 寄存器结构如图 17.1.6.2 所示:
寄存器 GICC_BPR 只有低 3 位有效,其值不同,抢占优先级和子优先级占用的位数也不同,
为了简单起见,一般将所有的中断优先级位都配置为抢占优先级,比如 I.MX6U 的优先级 位数为 5(32 个优先级),所以可以设置 Binary point 为 2,表示 5 个优先级位全部为抢占优先级。
3.6.3、优先级设置
如果优先级个数为 32 的话,使用寄 存器 D_IPRIORITYR 的 bit7:4 来设置优先级,也就是说实际的优先级要左移 3 位。比如要设置 ID40 中断的优先级为 5,示例代码如下:
GICD_IPRIORITYR[40] = 5 << 3;
有关优先级设置的内容就讲解到这里,优先级设置主要有三部分:
①、设置寄存器 GICC_PMR,配置优先级个数,比如 I.MX6U 支持 32 级优先级。
②、设置抢占优先级和子优先级位数,一般为了简单起见,会将所有的位数都设置为抢占 优先级。
③、设置指定中断 ID 的优先级,也就是设置外设优先级。
十三、EPIT定时器实验
定时器是最常用的外设,常常需要使用定时器来完成精准的定时功能,I.MX6U 提供了多 种硬件定时器,有些定时器功能非常强大。
1、EPIT 定时器简介
EPIT 的全称是:Enhanced Periodic Interrupt Timer,直译过来就是增强的周期中断定时器, 它主要是完成周期性中断定时的。学过 STM32 的话应该知道,STM32 里面的定时器还有很多 其它的功能,比如输入捕获、PWM 输出等等。但是 I.MX6U 的 EPIT 定时器只是完成周期性中 断定时的,仅此一项功能!至于输入捕获、PWM 输出等这些功能,I.MX6U 由其它的外设来完 成。
EPIT 是一个 32 位定时器,在处理器几乎不用介入的情况下提供精准的定时中断,软件使 能以后 EPIT 就会开始运行,EPIT 定时器有如下特点:
①、时钟源可选的 32 位向下计数器。 ②、12 位的分频值。 ③、当计数值和比较值相等的时候产生中断
①、这是个多路选择器,用来选择 EPIT 定时器的时钟源,EPIT 共有 3 个时钟源可选择, ipg_clk、ipg_clk_32k 和 ipg_clk_highfreq。
②、这是一个 12 位的分频器,负责对时钟源进行分频,12 位对应的值是 0~4095,对应着 1~4096 分频。
③、经过分频的时钟进入到 EPIT 内部,在 EPIT 内部有三个重要的寄存器:计数寄存器 (EPIT_CNR)、加载寄存器(EPIT_LR)和比较寄存器(EPIT_CMPR),这三个寄存器都是 32 位的。 EPIT 是一个向下计数器,也就是说给它一个初值,它就会从这个给定的初值开始递减,直到减 为 0,计数寄存器里面保存的就是当前的计数值。如果 EPIT 工作在 set-and-forget 模式下,当计数寄存器里面的值减少到 0,EPIT 就会重新从加载寄存器读取数值到计数寄存器里面,重新开 始向下计数。比较寄存器里面保存的数值用于和计数寄存器里面的计数值比较,如果相等的话 就会产生一个比较事件。
④、比较器。 ⑤、EPIT 可以设置引脚输出,如果设置了的话就会通过指定的引脚输出信号。
⑥、产生比较中断,也就是定时中断。
EPIT 定时器有两种工作模式:set-and-forget 和 free-running,这两个工作模式的区别如下:
set-and-forget 模式:EPITx_CR(x=1,2)寄存器的 RLD 位置 1 的时候 EPIT 工作在此模式 下,在此模式下 EPIT 的计数器从加载寄存器 EPITx_LR 中获取初始值,不能直接向计数器寄存 器写入数据。不管什么时候,只要计数器计数到 0,那么就会从加载寄存器 EPITx_LR 中重新 加载数据到计数器中,周而复始。
free-running 模式:EPITx_CR 寄存器的 RLD 位清零的时候 EPIT 工作在此模式下,当计数 器计数到0以后会重新从0XFFFFFFFF开始计数,并不是从加载寄存器EPITx_LR中获取数据。
CLKSRC(bit25:24):EPIT 时钟源选择位,为 0 的时候关闭时钟源,1 的时候选择选择 Peripheral 时钟(ipg_clk),为 2 的时候选择 High-frequency 参考时钟(ipg_clk_highfreq),为 3 的时 候选择 Low-frequency 参考时钟(ipg_clk_32k)。
PRESCALAR(bit15:4):EPIT 时钟源分频值,可设置范围 0~4095,分别对应 1~4096 分频。
RLD(bit3):EPIT 工作模式,为 0 的时候工作在 free-running 模式,为 1 的时候工作在 set-and-forget 模式。本章例程设置为 1,也就是工作在 set-and-forget 模式。
OCIEN(bit2):比较中断使能位,为 0 的时候关闭比较中断,为 1 的时候使能比较中断,本 章试验要使能比较中断。
ENMOD(bit1):设置计数器初始值,为 0 时计数器初始值等于上次关闭 EPIT 定时器以后 计数器里面的值,为 1 的时候来源于加载寄存器。
EN(bit0):EPIT 使能位,为 0 的时候关闭 EPIT,为 1 的时候使能 EPIT。
寄存器 EPITx_SR 只有一个位有效,那就是 OCIF(bit0),这个位是比较中断标志位,为 0 的 时候表示没有比较事件发生,为 1 的时候表示有比较事件发生。当比较中断发生以后需要手动 清除此位,此位是写 1 清零的。
寄存器 EPITx_LR、EPITx_CMPR 和 EPITx_CNR 分别为加载寄存器、比较寄存器和计数寄 存器,这三个寄存器都是用来存放数据的,很简单。
EPIT 的配置步骤如下:
1、设置 EPIT1 的时钟源
设置寄存器 EPIT1_CR 寄存器的 CLKSRC(bit25:24)位,选择 EPIT1 的时钟源。
2、设置分频值
设置寄存器 EPIT1_CR 寄存器的 PRESCALAR(bit15:4)位,设置分频值。
3、设置工作模式
设置寄存器 EPIT1_CR 的 RLD(bit3)位,设置 EPTI1 的工作模式。
4、设置计数器的初始值来源
设置寄存器 EPIT1_CR 的 ENMOD(bit1)位,设置计数器的初始值来源。
5、使能比较中断
我们要使用到比较中断,因此需要设置寄存器 EPIT1_CR 的 OCIEN(bit2)位,使能比较中 断。
6、设置加载值和比较值
设置寄存器 EPIT1_LR 中的加载值和寄存器 EPIT1_CMPR 中的比较值,通过这两个寄存器 就可以决定定时器的中断周期。 7、EPIT1 中断设置和中断服务函数编写
使能 GIC 中对应的 EPIT1 中断,注册中断服务函数,如果需要的话还可以设置中断优先 级。最后编写中断服务函数。
8、使能 EPIT1 定时器
配置好 EPIT1 以后就可以使能 EPIT1 了,通过寄存器 EPIT1_CR 的 EN(bit0)位来设置。 通过以上几步我们就配置好 EPIT 了,通过 EPIT 的比较中断来实现 LED0 的翻转。
十四、定时器按键消抖实验
用到按键就要处理因为机械结构带来的按键 抖动问题,也就是按键消抖。前面的实验中都是直接使用了延时函数来实现消抖,因为简单, 但是直接用延时函数来实现消抖会浪费 CPU 性能,因为在延时函数里面 CPU 什么都做不了。 如果按键使用中断的话更不能在中断里面使用延时函数,因为中断服务函数要快进快出!
1、定时器按键消抖简介
是在按键按下以后延时一段时间再 去读取按键值,如果此时按键值还有效那就表示这是一次有效的按键,中间的延时就是消抖的。 但是这有一个缺点,就是延时函数会浪费 CPU 性能,因为延时函数就是空跑。如果按键是用中 断方式实现的,那就更不能在中断服务函数里面使用延时函数,因为中断服务函数最基本的要 求就是快进快出!上一章我们学习了 EPIT 定时器,定时器设置好定时时间,然后 CPU 就可以 做其他事情去了,定时时间到了以后就会触发中断,然后在中断中做相应的处理即可。因此, 我们可以借助定时器来实现消抖,按键采用中断驱动方式,当按键按下以后触发按键中断,在 按键中断中开启一个定时器,定时周期为 10ms,当定时时间到了以后就会触发定时器中断,最 后在定时器中断处理函数中读取按键的值,如果按键值还是按下状态那就表示这是一次有效的 按键。
如何使用 EPIT1 来配合按键 KEY 来实现具体的消抖,步骤如下:
1、配置按键 IO 中断
配置按键所使用的 IO,因为要使用到中断驱动按键,所以要配置 IO 的中断模式。
2、初始化消抖用的定时器
上面已经讲的很清楚了,消抖要用定时器来完成,所以需要初始化一个定时器,这里使用 上一章讲解的 EPIT1 定时器,也算是对 EPIT1 定时器的一次巩固。定时器的定时周期为 10ms, 也可根据实际情况调整定时周期。
3、编写中断处理函数
需要编写两个中断处理函数:按键对应的 GPIO 中断处理函数和 EPIT1 定时器的中断处理 函数。在按键的中断处理函数中主要用于开启 EPIT1 定时器,EPIT1 的中断处理函数才是重点, 按键要做的具体任务都是在定时器 EPIT1 的中断处理函数中完成的,比如控制蜂鸣器打开或关 闭。
十五、高精度延时实验
延时函数是很常用的 API 函数,在前面的实验中我们使用循环来实现延时函数,但是使用 循环来实现的延时函数不准确,误差会很大。虽然使用到延时函数的地方精度要求都不会很严 格(要求严格的话就使用硬件定时器了),但是延时函数肯定是越精确越好,这样延时函数就可 以使用在某些对时序要求严格的场合。
1、高精度延时简介
1.1、GPT 定时器简介
在使用 STM32 的时候可以使用 SYSTICK 来实现高精度延 时。I.MX6U 没有 SYSTICK 定时器,但是 I.MX6U 有其他定时器啊,比如第十八章讲解的 EPIT 定时器。本章我们使用 I.MX6U 的 GPT 定时器来实现高精度延时,顺便学习一下 GPT 定时器, GPT 定时器全称为 General Purpose Timer。
GPT 定时器是一个 32 位向上定时器(也就是从 0X00000000 开始向上递增计数),GPT 定时 器也可以跟一个值进行比较,当计数器值和这个值相等的话就发生比较事件,产生比较中断。 GPT 定时器有一个 12 位的分频器,可以对 GPT 定时器的时钟源进行分频,GPT 定时器特性如 下:
①、一个可选时钟源的 32 位向上计数器。 ②、两个输入捕获通道,可以设置触发方式。 ③、三个输出比较通道,可以设置输出模式。 ④、可以生成捕获中断、比较中断和溢出中断。 ⑤、计数器可以运行在重新启动(restart)或(自由运行)free-run 模式。 GPT 定时器的可选时钟源如图 20.1.1.1 所示:
一共有五个时钟源,分别为:ipg_clk_24M、GPT_CLK(外部时钟)、 ipg_clk、ipg_clk_32k 和 ipg_clk_highfreq。本例程选择 ipg_clk 为 GPT 的时钟源,ipg_clk=66MHz。
①、此部分为 GPT 定时器的时钟源,前面已经说过了,本章例程选择 ipg_clk 作为 GPT 定 时器时钟源。
②、此部分为 12 位分频器,对时钟源进行分频处理,可设置 0~4095,分别对应 1~4096 分 频。
③、经过分频的时钟源进入到 GPT 定时器内部 32 位计数器。
④和⑤、这两部分是 GPT 的两路输入捕获通道,本章不讲解 GPT 定时器的输入捕获。
⑥、此部分为输出比较寄存器,一共有三路输出比较,因此有三个输出比较寄存器,输出 比较寄存器是 32 位的。
⑦、此部分位输出比较中断,三路输出比较中断,当计数器里面的值和输出比较寄存器里 面的比较值相等就会触发输出比较中断。
GPT 定时器有两种工作模式:重新启动(restart)模式和自由运行(free-run)模式,这两个工作 模式的区别如下:
重新启动(restart)模式:当 GPTx_CR(x=1,2)寄存器的 FRR 位清零的时候 GPT 工作在此 模式。在此模式下,当计数值和比较寄存器中的值相等的话计数值就会清零,然后重新从 0X00000000 开始向上计数,只有比较通道 1 才有此模式!向比较通道 1 的比较寄存器写入任何 数据都会复位 GPT 计数器。对于其他两路比较通道(通道 2 和 3),当发生比较事件以后不会 复位计数器。
自由运行(free-run)模式:当 GPTx_CR(x=1,2)寄存器的 FRR 位置 1 时候 GPT 工作在此模 式下,此模式适用于所有三个比较通道,当比较事件发生以后并不会复位计数器,而是继续计 数,直到计数值为 0XFFFFFFFF,然后重新回滚到 0X00000000。
SWR(bit15):复位 GPT 定时器,向此位写 1 就可以复位 GPT 定时器,当 GPT 复位完成以 后此为会自动清零。
FRR(bit9):运行模式选择,当此位为 0 的时候比较通道 1 工作在重新启动(restart)模式。当 此位为 1 的时候所有的三个比较通道均工作在自由运行模式(free-run)。
CLKSRC(bit8:6):GPT 定时器时钟源选择位,为 0 的时候关闭时钟源;为 1 的时候选择 ipg_clk 作为时钟源;为 2 的时候选择 ipg_clk_highfreq 为时钟源;为 3 的时候选择外部时钟为 时钟源;为 4 的时候选择 ipg_clk_32k 为时钟源;为 5 的时候选择 ip_clk_24M 为时钟源。本章 例程选择 ipg_clk 作为 GPT 定时器的时钟源,因此此位设置位 1(0b001)。
ENMOD(bit1):GPT 使能模式,此位为 0 的时候如果关闭 GPT 定时器,计数器寄存器保 存定时器关闭时候的计数值。此位为 1 的时候如果关闭 GPT 定时器,计数器寄存器就会清零。
EN(bit):GPT 使能位,为 1 的时候使能 GPT 定时器,为 0 的时候关闭 GPT 定时器。
寄存器 GPTx_PR 我们用到的重要位就一个:PRESCALER(bit11:0),这就是 12 位分频值, 可设置 0~4095,分别对应 1~4096 分频。
ROV(bit5):回滚标志位,当计数值从 0XFFFFFFFF 回滚到 0X00000000 的时候此位置 1。
IF2~IF1(bit4:3):输入捕获标志位,当输入捕获事件发生以后此位置 1,一共有两路输入捕 获通道。如果使用输入捕获中断的话需要在中断处理函数中清除此位。
OF3~OF1(bit2:0):输出比较中断标志位,当输出比较事件发生以后此位置 1,一共有三路 输出比较通道。如果使用输出比较中断的话需要在中断处理函数中清除此位。
GPT 定时器的计数寄存器 GPTx_CNT,这个寄存器保存着 GPT 定时器的当前 计数值。最后看一下 GPT 定时器的输出比较寄存器 GPTx_OCR,每个输出比较通道对应一个 输出比较寄存器,因此一个 GPT 定时器有三个 OCR 寄存器,它们的作都是相同的。以输出比 较通道 1 为例,其输出比较寄存器为 GPTx_OCR1,这是一个 32 位寄存器,用于存放 32 位的 比较值。当计数器值和寄存器 GPTx_OCR1 中的值相等就会产生比较事件,如果使能了比较中 断的话就会触发相应的中断。
1.2、定时器实现高精度延时原理
如果设置 GPT 定时器的时钟源为 ipg_clk=66MHz,设置 66 分频,那么进入 GPT 定时器的最终时钟频率就是 66/66=1MHz,周期为 1us。GPT 的计数器每计一个数就表示“过去” 了 1us。如果计 10 个数就表示“过去”了 10us。通过读取寄存器 GPTx_CNT 中的值就知道计 了个数,比如现在要延时 100us,那么进入延时函数以后纪录下寄存器 GPTx_CNT 中的值为 200, 当 GPTx_CNT 中的值为 300 的时候就表示 100us 过去了,也就是延时结束。GPTx_CNT 是个 32 位寄存器,如果时钟为 1MHz 的话,GPTx_CNT 最多可以实现 0XFFFFFFFFus=4294967295us ≈4294s≈72min。也就是说 72 分钟以后 GPTx_CNT 寄存器就会回滚到 0X00000000,也就是溢 出,所以需要在延时函数中要处理溢出的情况。关于定时器实现高精度延时的原理就讲解到这 里,原理还是很简单的,高精度延时的实现步骤如下:
1、设置 GPT1 定时器
首先设置 GPT1_CR 寄存器的 SWR(bit15)位来复位寄存器 GPT1。复位完成以后设置寄存 器 GPT1_CR 寄存器的 CLKSRC(bit8:6)位,选择 GPT1 的时钟源为 ipg_clk。设置定时器 GPT1 的工作模式,
2、设置 GPT1 的分频值
设置寄存器 GPT1_PR 寄存器的 PRESCALAR(bit111:0)位,设置分频值。
3、设置 GPT1 的比较值
如果要使用 GPT1 的输出比较中断,那么 GPT1 的输出比较寄存器 GPT1_OCR1 的值可以 根据所需的中断时间来设置。本章例程不使用比较输出中断,所以将 GPT1_OCR1 设置为最大 值,即:0XFFFFFFFF。
4、使能 GPT1 定时器
设置好 GPT1 定时器以后就可以使能了,设置 GPT1_CR 的 EN(bit0)位为 1 来使能 GPT1 定 时器。
5、编写延时函数
GPT1定时器已经开始运行了,可以根据前面介绍的高精度延时函数原理来编写延时函数, 针对 us 和 ms 延时分别编写两个延时函数。
十六、UART 串口通信实验
不管是单片机开发还是嵌入式 Linux 开发,串口都是最常用到的外设。可以通过串口将开 发板与电脑相连,然后在电脑上通过串口调试助手来调试程序。
在嵌入式 Linux 中一般使用串口作为控制台,所以掌握串口是必备的技能。
1、I.MX6U 串口简介
1.1、UART 通信格式
串口全称叫做串行接口,通常也叫做 COM 接口,串行接口指的是数据一个一个的顺序传 输,通信线路简单。使用两条线即可实现双向通信,一条用于发送,一条用于接收。串口通信 距离远,但是速度相对会低,串口是一种很常用的工业接口。I.MX6U 自带的 UART 外设就是 串口的一种,UART 全称是 UniversalAsynchronous Receiver/Trasmitter,也就是异步串行收发器。 既然有异步串行收发器,那肯定也有同步串行收发器,学过 STM32 的同学应该知道,STM32 除了有 UART 外 ,还有 另 外一 个 叫 做 USART 的 东 西。 USART 的全称是 Universal Synchronous/Asynchronous Receiver/Transmitter,也就是同步/异步串行收发器。相比 UART 多了 一个同步的功能,在硬件上体现出来的就是多了一条时钟线。一般 USART 是可以作为 UART 使用的,也就是不使用其同步的功能。
UART 作为串口的一种,其工作原理也是将数据一位一位的进行传输,发送和接收各用一 条线,因此通过 UART 接口与外界相连最少只需要三条线:TXD(发送)、RXD(接收)和 GND(地 线)。图 21.1.1.1 就是 UART 的通信格式:
空闲位:数据线在空闲状态的时候为逻辑“1”状态,也就是高电平,表示没有数据线空闲, 没有数据传输。
起始位:当要传输数据的时候先传输一个逻辑“0”,也就是将数据线拉低,表示开始数据 传输。
数据位:数据位就是实际要传输的数据,数据位数可选择 5~8 位,我们一般都是按照字节 传输数据的,一个字节 8 位,因此数据位通常是 8 位的。低位在前,先传输,高位最后传输。
奇偶校验位:这是对数据中“1”的位数进行奇偶校验用的,可以不使用奇偶校验功能。
停止位:数据传输完成标志位,停止位的位数可以选择 1 位、1.5 位或 2 位高电平,一般都 选择 1 位停止位。
波特率:波特率就是 UART 数据传输的速率,也就是每秒传输的数据位数,一般选择 9600、 19200、115200 等。
1.2、UART 电平标准
UART 一般的接口电平有 TTL 和 RS-232,一般开发板上都有 TXD 和 RXD 这样的引脚, 这些引脚低电平表示逻辑 0,高电平表示逻辑 1,这个就是 TTL 电平。RS-232 采用差分线,-3~- 15V 表示逻辑 1,+3~+15V 表示逻辑 0。
TTL 接口部分有 VCC、GND、RXD、TXD、 RTS 和 CTS。RTS 和 CTS 基本用不到,使用的时候通过杜邦线和其他模块的 TTL 接口相连即 可。
RS-232 电平需要 DB9 接口,I.MX6U-ALPHA 开发板上的 COM3(UART3)口就是 RS-232 接 口的,如图 21.1.1.3 所示:
由于现在的电脑都没有 DB9 接口了,取而代之的是 USB 接口,所以就催生出了很多 USB 转串口 TTL 芯片,比如 CH340、PL2303 等。通过这些芯片就可以实现串口 TTL 转 USB。I.MX6UALPHA开发板就使用CH340 芯片来完成UART1和电脑之间的连接,只需要一条USB 线即可。
2、I.MX6U UART 简介
I.MX6U 一共 有 8 个 UART,其主要特性如下:
①、兼容 TIA/EIA-232F 标准,速度最高可到 5Mbit/S。
②、支持串行 IR 接口,兼容 IrDA,最高可到 115.2Kbit/s。
③、支持 9 位或者多节点模式(RS-485)。
④、1 或 2 位停止位。
⑤、可编程的奇偶校验(奇校验和偶校验)。
⑥、自动波特率检测(最高支持 115.2Kbit/S)。
UART 的时钟源是由寄存器 CCM_CSCDR1 的 UART_CLK_SEL(bit)位来选择的,当为 0 的 时候 UART 的时钟源为 pll3_80m(80MHz),如果为 1 的时候 UART 的时钟源为 osc_clk(24M), 一般选择 pll3_80m 作为 UART 的时钟源。寄存器 CCM_CSCDR1 的 UART_CLK_PODF(bit5:0) 位是 UART 的时钟分频值,可设置 0~63,分别对应 1~64 分频,一般设置为 1 分频,因此最终 进入 UART 的时钟为 80MHz。
UART 的控制寄存器 1,即 UARTx_UCR1(x=1~8),此寄存器的结构如图 21.1.2.1 所示:
ADBR(bit14):自动波特率检测使能位,为 0 的时候关闭自动波特率检测,为 1 的时候使 能自动波特率检测。 UARTEN(bit0):UART 使能位,为 0 的时候关闭 UART,为 1 的时候使能 UART。
UART 的控制寄存器 2,即:UARTx_UCR2,此寄存器结构如图 21.1.2.2 所 示:
IRTS(bit14):为 0 的时候使用 RTS 引脚功能,为 1 的时候忽略 RTS 引脚。
PREN(bit8):奇偶校验使能位,为 0 的时候关闭奇偶校验,为 1 的时候使能奇偶校验。
PROE(bit7):奇偶校验模式选择位,开启奇偶校验以后此位如果为 0 的话就使用偶校 验,此位为 1 的话就使能奇校验。
STOP(bit6):停止位数量,为 0 的话 1 位停止位,为 1 的话 2 位停止位。
WS(bit5):数据位长度,为 0 的时候选择 7 位数据位,为 1 的时候选择 8 位数据位。
TXEN(bit2):发送使能位,为 0 的时候关闭 UART 的发送功能,为 1 的时候打开 UART 的发送功能。
RXEN(bit1):接收使能位,为 0 的时候关闭 UART 的接收功能,为 1 的时候打开 UART 的接收功能。
SRST(bit0):软件复位,为 0 的是时候软件复位 UART,为 1 的时候表示复位完成。复位 完成以后此位会自动置 1,表示复位完成。此位只能写 0,写 1 会被忽略掉。
本章实验就用到了寄存器 UARTx_UCR3 中的位 RXDMUXSEL(bit2),这个位应该始终为 1,
下寄存器 UARTx_USR2,这个是 UART 的状态寄存器 2,此寄存器结构如图 21.1.2.4 所示:
TXDC(bit3):发送完成标志位,为 1 的时候表明发送缓冲(TxFIFO)和移位寄存器为空,也 就是发送完成,向 TxFIFO 写入数据此位就会自动清零。
RDR(bit0):数据接收标志位,为 1 的时候表明至少接收到一个数据,从寄存器 UARTx_URXD 读取数据接收到的数据以后此位会自动清零。
寄 存 器 UARTx_UFCR 、 UARTx_UBIR 和 UARTx_UBMR , 寄 存 器 UARTx_UFCR 中我们要用到的是位 RFDIV(bit9:7),用来设置参考时钟分频。
通过这三个寄存器可以设置 UART 的波特率,波特率的计算公式如下:
通过 UARTx_UFCR 的 RFDIV 位、UARTx_UBMR 和 UARTx_UBIR 这三者的配合即可得 到我们想要的波特率。比如现在要设置 UART 波特率为 115200,那么可以设置 RFDIV 为 5(0b101),也就是 1 分频,因此 Ref Freq=80MHz。设置 UBIR=71,UBMR=3124,根据上面的 公式可以得到:
最后来看一下寄存器 UARTx_URXD 和 UARTx_UTXD,这两个寄存器分别为 UART 的接 收和发送数据寄存器,这两个寄存器的低八位为接收到的和要发送的数据。读取寄存器UARTx_URXD 即可获取到接收到的数据,如果要通过 UART 发送数据,直接将数据写入到寄 存器 UARTx_UTXD 即可。
UART1 的配置步骤如下:
1、设置 UART1 的时钟源
设置 UART 的时钟源为 pll3_80m,设置寄存器 CCM_CSCDR1 的 UART_CLK_SEL 位为 0 即可。
2、初始化 UART1
初始化 UART1 所使用 IO,设置 UART1 的寄存器 UART1_UCR1~UART1_UCR3,设置内 容包括波特率,奇偶校验、停止位、数据位等等。
4、使能 UART1
UART1 初始化完成以后就可以使能 UART1 了,设置寄存器 UART1_UCR1 的位 UARTEN 为 1。
5、编写 UART1 数据收发函数
编写两个函数用于 UART1 的数据收发操作。
十七、串口格式化函数移植实验
1、串口格式化函数简介
格式化函数说的是 printf、sprintf 和 scanf 这样的函数,分为格式化输入和格式化输出两类 函数。学习 C 语言的时候常常通过 printf 函数在屏幕上显示字符串,通过 scanf 函数从键盘获取 输入。这样就有了输入和输出了,实现了最基本的人机交互。学习 STM32 的时候会将 printf 映 射到串口上,这样即使没有屏幕,也可以通过串口来和开发板进行交互。在 I.MX6U-ALPHA 开 发板上也可以使用此方法,将 printf 和 scanf 映射到串口上,这样就可以使用 SecureCRT 作为开 发板的终端,完成与开发板的交互。也可以使用 printf 和 sprintf 来实现各种各样的格式化字符 串,方便我们后续的开发。
十八、DDR3 实验
一般 Cortex-A 芯 片自带的 RAM 很小,比如 I.MX6U 只有 128KB 的 OCRAM。如果要运行 Linux 的话完全不够 用的,所以必须要外接一片 RAM 芯片,I.MX6U 支持 LPDDR2、LPDDR3/DDR3,I.MX6U-ALPHA 开发板上选择的是 DDR3
1、DDR3 内存简介
1.1、何为 RAM 和 ROM?
RAM:随机存储器,可以随时进行读写操作,速度很快,掉电以后数据会丢失。比如内存 条、SRAM、SDRAM、DDR 等都是 RAM。RAM 一般用来保存程序数据、中间结果,比如我 们在程序中定义了一个变量 a,然后对这个 a 进行读写操作。
ROM:只读存储器,笔者认为目前“只读存储器”这个定义不准确。比如我们买手机,通 常会告诉你这个手机是 4+64 或 6+128 配置,说的就是 RAM 为 4GB 或 6GB,ROM 为 64G 或 128GB。但是这个 ROM 是 Flash,比如 EMMC 或 UFS 存储器,因为历史原因,很多人还是将 Flash 叫做 ROM。但是 EMMC 和 UFS,甚至是 NAND Flash,这些都是可以进行写操作的!只 是写起来比较麻烦,要先进行擦除,然后再发送要写的地址或扇区,最后才是要写入的数据,相比于 RAM,向 ROM 或者 Flash 写入数据要复杂很多,因此意味着速度就会变慢(相比 RAM),但是 ROM 和 Flash 可以将容量做的很大,而且掉电以后数据不会丢失,适合用来存储资料,比如音 乐、图片、视频等信息。
RAM 速度快,可以直接和 CPU 进行通信,但是掉电以后数据会丢失,容量不 容易做大(和同价格的 Flash 相比)。ROM(目前来说,更适合叫做 Flash)速度虽然慢,但是容量 大、适合存储数据。对于正点原子的 I.MX6U-ALPHA 开发板而言,256MB/512MB 的 DDR3 就 是 RAM,而 512MB NANF Flash 或 8GB EMMC 就是 ROM。
1.2、SRAM 简介
SRAM 的全称叫做 Static Random-Access Memory,也就是静态随机存储器,这里的“静态” 说的就是只要 SRAM 上电,那么 SRAM 里面的数据就会一直保存着,直到 SRAM 掉电。对于 RAM 而言需要可以随机的读取任意一个地址空间内的数据,因此采用了地址线和数据线分离的方式,这里就以 STM32F103/F407 开发板常用的 IS62WV51216 这颗 SRAM 芯片为例简单的 讲解一下 SRAM,这是一颗 16 位宽(数据位为 16 位)、1MB 大小的 SRAM,芯片框图如图 23.1.2.1 所示:
①、地址线
这部分是地址线,一共 A0~A18,也就是 19 根地址线,因此可访问的地址大小就是 2^19=524288=512KB。不是说 IS62WV51216 是个 1MB 的 SRAM 吗?为什么地址空间只有 512KB?前面我们说了 IS62WV51216 是 16 位宽的,也就是一次访问 2 个字节,因此需要对 512KB 进行乘 2 处理,得到 512KB*2=1MB。位宽的话一般有 8 位/16 位/32 位,根据实际需求 选择即可,一般都是根据处理器的 SRAM 控制器位宽来选择 SRAM 位宽。
②、数据线
这部分是 SRAM 的数据线,根据 SRAM 位宽的不同,数据线的数量要不同,8 位宽就有 8 根数据线,16 位宽就有 16 根数据线,32 位宽就有 32 根数据线。IS62WV51216 是一个 16 位宽 的 SRAM,因此就有 16 根数据线,一次访问可以访问 16bit 的数据,也就是 2 个字节。因此就 有高字节和低字节数据之分,其中 IO0~IO7 是低字节数据,IO8~IO15 是高字节数据。
③、控制线
SRAM 要工作还需要一堆的控制线,CS2 和 CS1 是片选信号,低电平有效,在一个系统中 可能会有多片 SRAM(目的是为了扩展 SRAM 大小或位宽),这个时候就需要 CS 信号来选择当 前使用哪片 SRAM。另外,有的 SRAM 内部其实是由两片 SRAM 拼接起来的,因此就会提供 两个片选信号。
OE 是输出使能信号,低电平有效,也就是主控从 SRAM 读取数据。
WE 是写使能信号,低电平有效,也就是主控向 SRAM 写数据。
UB 和 LB 信号,前面我们已经说了,IS62WV51216 是个 16 位宽的 SRAM,分为高字节和 低字节,那么如何来控制读取高字节数据还是低字节数据呢?这个就是 UB 和 LB 这两个控制 线的作用,这两根控制线都是低电平有效。UB 为低电平的话表示访问高字节,LB 为低电平的 话表示访问低字节。
SDRAM 比 SRAM 容量大,但是价格更低。 SRAM 突出的特点就是无需刷新(SDRAM 需要刷新,后面会讲解),读写速度快!所以 SRAM 通常作为 SOC 的内部 RAM 使用或 Cache 使用,比如 STM32 内存的 RAM 或 I.MX6U 内部的 OCRAM 都是 SRAM。
1.3、SDRAM 简介
SDRAM 全称是 Synchronous Dynamic Random Access Memory,翻译过来就是同步动态随机存储器,“同步”的意思是 SDRAM 工作 需要时钟线,“动态”的意思是 SDRAM 中的数据需要不断的刷新来保证数据不会丢失,“随机” 的意思就是可以读写任意地址的数据。
与 SRAM 相比,SDRAM 集成度高、功耗低、成本低、适合做大容量存储,但是需要定时 刷新来保证数据不会丢失。因此 SDRAM 适合用来做内存条,SRAM 适合做高速缓存或 MCU 内部的 RAM。SDRAM 目前已经发展到了第四代,分别为:SDRAM、DDR SDRAM、DDR2 SDRAM、DDR3 SDRAM、DDR4 SDRAM。
就以 STM32 开发板最常用的华邦 W9825G6KH 为例,W9825G6KH 是一款 16 位宽(数据位为 16 位)、32MB 的 SDRAM、速度一 般为 133MHz、166MHz 或 200MHz。W9825G6KH 框图如图 23.1.3.1 所示:
①、控制线
CLK:时钟线,SDRAM 是同步动态随机存储器,“同步”的意思就是时钟,因此需要一根 额外的时钟线,这是和 SRAM 最大的不同,SRAM 没有时钟线。
CKE:时钟使能信号线,SRAM 没有 CKE 信号。
CS:片选信号,这个和 SRAM 一样,都有片选信号。
RAS:行选通信号,低电平有效,SDRAM 和 SRAM 的寻址方式不同,SDRAM 按照行、 列来确定某个具体的存储区域。因此就有行地址和列地址之分,行地址和列地址共同复用同一 组地址线,要访问某一个地址区域,必须要发送行地址和列地址,指定要访问哪一行?哪一列? RAS 是行选通信号,表示要发送行地址,行地址和列地址访问方式如图 23.1.3.2 所示:
CAS:列选通信号,和 RAS 类似,低电平有效,选中以后就可以发送列地址了。
WE:写使能信号,低电平有效。
②、A10 地址线
A10 是地址线,那么这里为什么要单独将 A10 地址线给提出来呢?因为 A10 地址线还有另 外一个作用,A10 还控制着 Auto-precharge,也就是预充电。这里又提到了预充电的概念,SDRAM 芯片内部会分为多个 BANK,关于 BANK 我们稍后会讲解。SDRAM 在读写完成以后,如果要 对同一个 BANK 中的另一行进行寻址操作就必须将原来有效的行关闭,然后发送新的行/列地 址,关闭现在工作的行,准备打开新行的操作就叫做预充电。一般 SDSRAM 都支持自动预充电 的功能。
③、地址线
对于 W9825G6KH 来说一共有 A0~A12,共 13 根地址线,但是我们前面说了 SDRAM 寻址 是按照行地址和列地址来访问的,因此这 A0~A12 包含了行地址和列地址。不同的 SDRAM 芯 片,根据其位宽、容量等的不同,行列地址数是不同的,这个在 SDRAM 的数据手册里面会也 清楚的。比如 W9825G6KH 的 A0~A8 是列地址,一共 9 位列地址,A0~A12 是行地址,一共 13 位,因此可寻址范围为:29*213=4194304B=4MB,W9825G6KH 为 16 位宽(2 个字节),因此 还需要对 4MB 进行乘 2 处理,得到 4*2=8MB,但是 W9825G6KH 是一个 32MB 的 SDRAM 啊, 为什么算出来只有 8MB,仅仅为实际容量的 1/4。不要急,这个就是我们接下来要讲的 BANK, 8MB 只是一个 BANK 的容量,W9825G6KH 一共有 4 个 BANK。
④、BANK 选择线
S0 和 BS1 是 BANK 选择信号线,在一片 SDRAM 中因为技术、成本等原因,不可能做 一个全容量的 BANK。而且,因为 SDRAM 的工作原理,单一的 BANK 会带来严重的寻址冲 突,减低内存访问效率。为此,人们在一片 SDRAM 中分割出多块 BANK,一般都是 2 的 n 次 方,比如 2,4,8 等。图 23.1.1.2 中的⑤就是 W9825G6KH 的 4 个 BANK 示意图,每个 SDRAM 数据手册里面都会写清楚自己是几 BANK。前面我们已经计算出来了一个 BANK 的大小为 8MB, 那么四个 BANK 的总容量就是 8MB*4=32MB。
既然有4个BANK,那么在访问的时候就需要告诉SDRAM,我们现在需要访问哪个BANK, BS0 和 BS1 就是为此而生的,4 个 BANK 刚好 2 根线,如果是 8 个 BANK 的话就需要三根线, 也就是 BS0~BS2。BS0、BS1 这两个线也是 SRAM 所没有的。
⑤、BANK 区域
关于 BANK 的概念前面已经讲过了,这部分就是 W9825G6KH 的 4 个 BANK 区域。这个 概念也是 SRAM 所没有的。
⑥、数据线
W9825G6KH 是 16 位宽的 SDRAM,因此有 16 根数据线,DQ0~DQ15,不同的位宽其数 据线数量不同,这个和 SRAM 是一样的。
⑦、高低字节选择
W9825G6KH 是一个 16 位的 SDRAM,因此就分为低字节数据和高字节数据,LDQM 和 UDQM 就是低字节和高字节选择信号,这个也和 SRAM 一样。
1.4、DDR 简介
DDR 内存是 SDRAM 的升级版本,SDRAM 分为 SDR SDRAM、 DDR SDRAM、DDR2 SDRAM、DDR3 SDRAM、DDR4 SDRAM。可以看出 DDR 本质上还是 SDRAM,只是随着技术的不断发展,DDR 也在不断的更新换代。先来看一下 DDR,也就是 DDR1,人们对于速度的追求是永无止境的,当发现 SDRAM 的速度不够快的时候人们就在思 考如何提高 SDRAM 的速度,DDR SDRAM 由此诞生。
DDR 全称是 Double Data Rate SDRAM,也就是双倍速率 SDRAM,看名字就知道 DDR 的 速率(数据传输速率)比 SDRAM 高 1 倍!这 1 倍的速度不是简简单单的将 CLK 提高 1 倍, SDRAM 在一个 CLK 周期传输一次数据,DDR 在一个 CLK 周期传输两次数据,也就是在上升 沿和下降沿各传输一次数据,这个概念叫做预取(prefetch),相当于 DDR 的预取为 2bit,因此 DDR 的速度直接加倍!比如 SDRAM 速度一般是 133~200MHz,对应的传输速度就是 133~200MT/s,在描述 DDR 速度的时候一般都使用 MT/s,也就是每秒多少兆次数据传输。 133MT/S 就是每秒 133M 次数据传输,MT/s 描述的是单位时间内传输速率。同样 133~200MHz 的频率,DDR 的传输速度就变为了 266~400MT/S,所以大家常说的 DDR266、DDR400 就是这 么来的。
DDR2 在 DDR 基础上进一步增加预取(prefetch),增加到了 4bit,相当于比 DDR 多读取一 倍的数据,因此 DDR2 的数据传输速率就是 533~800MT/s,这个也就是大家常说的 DDR2 533、 DDR2 800。当然了,DDR2 还有其他速度,这里只是说最常见的几种。
DDR3 在 DDR2 的基础上将预取(prefetch)提高到 8bit,因此又获得了比 DDR2 高一倍的传 输速率,因此在总线时钟同样为 266~400MHz 的情况下,DDR3 的传输速率就是 1066~1600MT/S。 I.MX6U 的 MMDC 外设用于连接 DDR,支持 LPDDR2、DDR3、DDR3L,最高支持 16 位数据 位宽。总线速度为 400MHz(实际是 396MHz),数据传输速率最大为 800MT/S。
LPDDR3、DDR3 和 DDR3L 的区别,这三个都是 DDR3,但是区别主要在于工作电压,LPDDR3 叫做低功耗 DDR3,工作电压为 1.2V。DDR3 叫做标压 DDR3,工作电压为 1.5V,一般台式内 存条都是 DDR3。DDR3L 是低压 DDR3,工作电压为 1.35V,一般手机、嵌入式、笔记本等都 使用 DDR3L。
NT5CC256M16EP-EK 是一款容量为 4Gb,也就是 512MB 大小、 16 位宽、1.35V、传输速率为 1866MT/S 的 DDR3L 芯片。
①、控制线
ODT:片上终端使能,ODT 使能和禁止片内终端电阻。
ZQ:输出驱动校准的外部参考引脚,此引脚应该外接一个 240 欧的电阻到 VSSQ 上,一般 就是直接接地了。
RESET:复位引脚,低电平有效。
CKE:时钟使能引脚。 A12:A12 是地址引脚,但是有也有另外一个功能,因此也叫做 BC 引脚,A12 会在 READ和 WRITE 命令期间被采样,以决定 burst chop 是否会被执行。
CK 和 CK#:时钟信号,DDR3 的时钟线是差分时钟线,所有的控制和地址信号都会在 CK 对的上升沿和 CK#的下降沿交叉处被采集。
CS#:片选信号,低电平有效。
RAS#、CAS#和 WE#:行选通信号、列选通信号和写使能信号。
②、地址线
A[14:0]为地址线,A0~A14,一共 15 根地址线,根据 NT5CC256M16ER-EK 的数据手册可 知,列地址为 A0~A9,共 10 根,行地址为 A0~A14,共 15 根,因此一个 BANK 的大小就是 210*2152=32MB2=64MB,根据图 23.1.4.2 可知一共有 8 个 BANK,因此 DDR3L 的容量就 是 64*8=512MB。
③、BANK 选择线
一片 DDR3 有 8 个 BANK,因此需要 3 个线才能实现 8 个 BANK 的选择,BA0~BA2 就是 用于完成 BANK 选择的。
④、BANK 区域
DDR3 一般都是 8 个 BANK 区域。
⑤、数据线
因为是 16 位宽的,因此有 16 根数据线,分别为 DQ0~DQ15。
⑥、数据选通引脚
DQS 和 DQS#是数据选通引脚,为差分信号,读的时候是输出,写的时候是输入。LDQS(有 的叫做 DQSL)和 LDQS#(有的叫做 DQSL#)对应低字节,也就是 DQ0~7,UDQS(有的叫做 DQSU) 和 UDQS#(有的叫做 DQSU#),对应高字节,也就是 DQ8~15。
⑦、数据输入屏蔽引脚
DM 是写数据输入屏蔽引脚。
2、DDR3 关键时间参数
2.1、传输速率
比如 1066MT/S、1600MT/S、1866MT/S 等,这个是首要考虑的,因为这个决定了 DDR3 内 存的最高传输速率。
2.2、tRCD 参数
tRCD 全称是 RAS-to-CAS Delay,也就是行寻址到列寻址之间的延迟。DDR 的寻址流程是 先指定 BANK 地址,然后再指定行地址,最后指定列地址确定最终要寻址的单元。BANK 地址 和行地址是同时发出的,这个命令叫做“行激活”(Row Active)。行激活以后就发送列地址和具 体的操作命令(读还是写),这两个是同时发出的,因此一般也用“读/写命令”表示列寻址。在 行有效(行激活)到读写命令发出的这段时间间隔叫做 tRCD,如图 23.2.1 所示:
tRCD 为 13.91ns,这个我们在初始化 DDR3 的时候需要配置。
NT5CC256M16ER-EK 这个 DDR3 的 CL-TRCD-TRP 时间参数为“13- 13-13”。因此 tRCD=13,这里的 13 不是 ns 数,而是 CLK 时间数,表示 13 个 CLK 周期。
2.3、CL 参数
当列地址发出以后就会触发数据传输,但是数据从存储单元到内存芯片 IO 接口上还需要一段 时间,这段时间就是非常著名的 CL(CAS Latency),也就是列地址选通潜伏期,如图 23.2.4 所 示:
一般 tRCD 和 CL 大小一样。
2.4、AL 参数
在 DDR 的发展中,提出了一个前置 CAS 的概念,目的是为了解决 DDR 中的指令冲突, 它允许 CAS 信号紧随着 RAS 发送,相当于将 DDR 中的 CAS 前置了。但是读/写操作并没有因 此提前,依旧要保证足够的延迟/潜伏期,为此引入了 AL(Additive Latency),单位也是时钟周期 数。AL+CL 组成了 RL(Read Latency),从 DDR2 开始还引入了写潜伏期 WL(Write Latency), WL 表示写命令发出以后到第一笔数据写入的潜伏期。引入 AL 以后的读时序如图 23.2.5 所示:
2.5、tRC 参数
tRC 是两个 ACTIVE 命令,或者 ACTIVE 命令到 REFRESH 命令之间的周期,DDR3L 数 据手册会给出这个值,比如 NT5CC256M16EP-EK 的 tRC 值为 47.91ns,
2.6、tRAS 参数
tRAS 是 ACTIVE 命令到 PRECHARGE 命令之间的最小时间
3 、I.MX6U MMDC 控制器简介
3.1、 MMDC 控制器
MMDC 就是 I.MX6U 的内存控制器,MMDC 是一个多模的 DDR 控制器,可以连接 16 位宽的 DDR3/DDR3L、16 位 宽的 LPDDR2,MMDC 是一个可配置、高性能的 DDR 控制器。MMDC 外设包含一个内核 (MMDC_CORE)和 PHY(MMDC_PHY),内核和 PHY 的功能如下:
MMDC 内核:内核负责通过 AXI 接口与系统进行通信、DDR 命令生成、DDR 命令优化、 读/写数据路径。
MMDC PHY:PHY 负责时序调整和校准,使用特殊的校准机制以保障数据能够在 400MHz 被准确捕获。
MMDC 的主要特性如下:
①、支持 DDR3/DDR3Lx16、支持 LPDDR2x16,不支持 LPDDR1MDDR 和 DDR2。
②、支持单片 256Mbit~8Gbit 容量的 DDR,列地址范围:8-12 位,行地址范围 11-16bit。2 个片选信号。
③、对于 DDR3,最大支持 8bit 的突发访问。
④、对于 LPDDR2 最大支持 4bit 的突发访问。
⑤、MMDC 最大频率为 400MHz,因此对应的数据速率为 800MT/S。
⑥、支持各种校准程序,可以自动或手动运行。支持 ZQ 校准外部 DDR 设备,ZQ 校准 DDR I/O 引脚、校准 DDR 驱动能力。
3.2 、MMDC 控制器信号引脚
DDR 对于硬件要求非常严格,因此 DDR 的引脚都是独立的,一般没有复用功能,只做为 DDR 引脚使用。
由于图 23.3.2.1 中的引脚是 DDR 专属的,因此就不存在所谓的 DDR 引脚复用配置,只需 要设置 DDR 引脚的电气属性即可,注意,DDR 引脚的电气属性寄存器和普通的外设引脚电气 属性寄存器不同!
3.3、 MMDC 控制器时钟源
前面说了很多次,I.MX6U 的 DDR 或者 MDDC 的时钟频率为 400MHz,那么这 400MHz 时 钟源怎么来的呢?这个就要查阅 I.MX6ULL 参考手册的《Chapter 18 Clock Controller Module(CCM)》章节。MMDC 时钟源如图 23.3.3.1 所示:
①、pre_periph2 时钟选择器,也就是 periph2_clkd 的前级选择器,由 CBCMR 寄存器的 PRE_PERIPH2_CLK_SEL 位(bit22:21)来控制,一共有四种可选方案,如表 23.3.3.1 所示:
I.MX6U 内部 boot rom 就是设置 PLL2_PFD2 作为 MMDC 的最终时 钟源,这就是 I.MX6U 的 DDR 频率为 400MHz 的原因。
②、periph2_clk 时钟选择器,由 CBCDR 寄存器的 PERIPH2_CLK_SEL 位(bit26)来控制, 当为 0 的时候选择 pll2_main_clk 作为 periph2_clk 的时钟源,当为 1 的时候选择 periph2_clk2_clk 作为 periph2_clk 的时钟源。这里肯定要将 PERIPH2_CLK_SEL 设置为 0,也就是选择 pll2_main_clk 作为 periph2_clk 的时钟源,因此 periph2_clk=PLL2_PFD0=396MHz。
③、最后就是分频器,由 CBCDR 寄存器的 FABRIC_MMDC_PODF 位(bit5:3)设置分频值, 可设置 0~7,分别对应 1~8 分频,要配置 MMDC 的时钟源为 396MHz,那么此处就要设置为 1 分频,因此 FABRIC_MMDC_PODF=0。
十九、RGBLED显示实验
LCD 液晶屏是常用到的外设,通过 LCD 可以显示绚丽的图形、界面等,提高人机交互的 效率。I.MX6U 提供了一个 eLCDIF 接口用于连接 RGB 接口的液晶屏。
1、 LCD 和 eLCDIF 简介
1.1 、LCD 简介
LCD 全称是 Liquid Crystal Display,也就是液晶显示器,是现在最常用到的显示器,手机、 电脑、各种人机交互设备等基本都用到了 LCD,最常见就是手机和电脑显示器了。
LCD 的构造是在两片平行的玻璃基板当中放置液晶盒,下基板玻璃上设置 TFT(薄膜晶体 管),上基板玻璃上设置彩色滤光片,通过 TFT 上的信号与电压改变来控制液晶分子的转动方 向,从而达到控制每个像素点偏振光出射与否而达到显示目的。
1.1.1、分辨率
LCD 显示器都是由一个一个的像素点组成,像素点就类似一个灯(在 OLED 显示 器中,像素点就是一个小灯),这个小灯是 RGB 灯,也就是由 R(红色)、G(绿色)和 B(蓝色)这三 种颜色组成的,而 RGB 就是光的三原色。1080P 的意思就是一个 LCD 屏幕上的像素数量是 1920*1080 个,也就是这个屏幕一列 1080 个像素点,一共 1920 列,
X 轴就是 LCD 显示器的横轴,Y 轴就是显 示器的竖轴。图中的小方块就是像素点,一共有 1920x1080=2073600 个像素点。左上角的 A 点 是第一个像素点,右下角的 C 点就是最后一个像素点。2K 就是 25601440 个像素点,4K 是 3840*2160 个像素点。很明显,在 LCD 尺寸不变的情况下,分辨率越高越清晰。同样的,分辨 率不变的情况下,LCD 尺寸越小越清晰。比如我们常用的 24 寸显示器基本都是 1080P 的,而 我们现在使用的 5 寸的手机基本也是 1080P 的,但是手机显示细腻程度就要比 24 寸的显示器 要好很多!
由此可见,LCD 显示器的分辨率是一个很重要的参数,但是并不是分辨率越高的 LCD 就 越好。衡量一款 LCD 的好坏,分辨率只是其中的一个参数,还有色彩还原程度、色彩偏离、亮 度、可视角度、屏幕刷新率等其他参数。
1.1.2、像素格式
一般一个 R、 G、B 这三部分分别使用 8bit 的数据,那么一个像素点就是 8bit*3=24bit,也就是说一个像素点 3 个字节,这种像素格式称为 RGB888。如果再加入 8bit 的 Alpha(透明)通道的话一个像素点就是 32bit,也就是 4 个字节,这种像素格式称为 ARGB8888。
一个像素点是 4 个字节,其中 bit31~bit24 是 Alpha 通道,bit23~bit16 是 RED 通道,bit15~bit14 是 GREEN 通道,bit7~bit0 是 BLUE 通道。所以红色对应的值就是 0X00FF0000,蓝色对应的值就是 0X000000FF,绿色对应的值为 0X0000FF00。通过调节 R、G、 B的比例可以产生其它的颜色,比如0X00FFFF00就是黄色,0X00000000就是黑色,0X00FFFFFF 就是白色。大家可以打开电脑的“画图”工具,在里面使用调色板即可获取到想要的颜色对应 的数值。
1.1.3、LCD屏幕接口
LCD 屏幕或者说显示器有很多种接口,比如在显示器上常见的 VGA、HDMI、DP 等等, 但是I.MX6U-ALPHA开发板不支持这些接口。I.MX6U-ALPHA支持RGB接口的LCD,RGBLCD 接口的信号线如表 24.1.1.1 所示:
R[7:0]、G[7:0]和B[7:0]这24根是数据线,DE、VSYNC、 HSYNC 和 PCLK 这四根是控制信号线。RGB LCD 一般有两种驱动模式:DE 模式和 HV 模式, 这两个模式的区别是 DE 模式需要用到 DE 信号线,而 HV 模式不需要用到 DE 信号线,在 DE 模式下是可以不需要 HSYNC 信号线的,即使不接 HSYNC 信号线 LCD 也可以正常工作。
ATK-7016 的屏幕接口原理图如图 24.1.1.4 所示:
图中 J1 就是对外接口,是一个 40PIN 的 FPC 座(0.5mm 间距),通过 FPC 线,可以连接 到 I.MX6U-ALPHA 开发板上面,从而实现和 I.MX6U 的连接。该接口十分完善,采用 RGB888 格式,并支持 DE&HV 模式,还支持触摸屏和背光控制。右侧的几个电阻,并不是都焊接的, 而是可以用户自己选择。默认情况,R1 和 R6 焊接,设置 LCD_LR 和 LCD_UD,控制 LCD 的 扫描方向,是从左到右,从上到下(横屏看)。而 LCD_R7/G7/B7 则用来设置 LCD 的 ID,由于 RGBLCD 没有读写寄存器,也就没有所谓的 ID,这里我们通过在模块上面,控制 R7/G7/B7 的 上/下拉,来自定义 LCD 模块的 ID,帮助 MCU 判断当前 LCD 面板的分辨率和相关参数,以提 高程序兼容性。这几个位的设置关系如表 24.1.1.2 所示:
ATK-7016 模块,就设置 M2:M0=010 即可。这样,我们在程序里面,读取 LCD_R7/G7/B7, 得到 M0:M2 的值,从而判断 RGBLCD 模块的型号,并执行不同的配置,即可实现不同 LCD 模 块的兼容。
1.1.4、LCD 时间参数
如果将 LCD 显示一帧图像的过程想象成绘画,那么在显示的过程中就是用一根“笔”在不 同的像素点画上不同的颜色。这根笔按照从左至右、从上到下的顺序扫描每个像素点,并且在 像素画上对应的颜色,当画到最后一个像素点的时候一幅图像就绘制好了。假如一个 LCD 的分 辨率为 1024*600,那么其扫描如图 24.1.1.5 所示:
LCD 是怎么扫描显示一帧图像的。一帧图像也是由一行一行 组成的。HSYNC 是水平同步信号,也叫做行同步信号,当产生此信号的话就表示开始显示新的 一行了,所以此信号都是在图 24.1.1.5 的最左边。当 VSYNC 信号是垂直同步信号,也叫做帧 同步信号,当产生此信号的话就表示开始显示新的一帧图像了,所以此信号在图 24.1.1.5 的左 上角。
到有一圈“黑边”,真正有效的显示区域是中间的白色部分。那这一圈 “黑边”是什么东西呢?这就要从显示器的“祖先”CRT 显示器开始说起了,CRT 显示器就是 以前很常见的那种大屁股显示器,CRT 显示器屁股后面是个电子枪,这个电子枪就是我们上面说的“画笔”,电子枪打出的电子撞 击到屏幕上的荧光物质使其发光。只要控制电子枪从左到右扫完一行(也就是扫描一行),然后 从上到下扫描完所有行,这样一帧图像就显示出来了。也就是说,显示一帧图像电子枪是按照 ‘Z’形在运动,当扫描速度很快的时候看起来就是一幅完成的画面了。
当显示完一行以后会发出 HSYNC 信号,此时电子枪就会关闭,然后迅速的移动到屏幕的 左边,当 HSYNC 信号结束以后就可以显示新的一行数据了,电子枪就会重新打开。在 HSYNC 信号结束到电子枪重新打开之间会插入一段延时,这段延时就图 24.1.1.5 中的 HBP。当显示完 一行以后就会关闭电子枪等待 HSYNC 信号产生,关闭电子枪到 HSYNC 信号产生之间会插入 一段延时,这段延时就是图 24.1.1.5 中的 HFP 信号。同理,当显示完一帧图像以后电子枪也会关闭,然后等到 VSYNC 信号产生,期间也会加入一段延时,这段延时就是图 24.1.1.5 中的 VFP。 VSYNC 信号产生,电子枪移动到左上角,当 VSYNC 信号结束以后电子枪重新打开,中间也会 加入一段延时,这段延时就是图 24.1.1.5 中的 VBP。
HBP、HFP、VBP 和 VFP 就是导致图 24.1.1.5 中黑边的原因,但是这是 CRT 显示器存在黑 边的原因,现在是 LCD 显示器,不需要电子枪了,那么为何还会有黑边呢?这是因为 RGB LCD 屏幕内部是有一个 IC 的,发送一行或者一帧数据给 IC,IC 是需要反应时间的。通过这段反应 时间可以让 IC 识别到一行数据扫描完了,要换行了,或者一帧图像扫描完了,要开始下一帧图 像显示了。因此,在 LCD 屏幕中继续存在 HBP、HFP、VPB 和 VFP 这四个参数的主要目的是 为了锁定有效的像素数据。
1.1.5、RGB LCD 屏幕时序
HSYNC:行同步信号,当此信号有效的话就表示开始显示新的一行数据,查阅所使用的 LCD 数据手册可以知道此信号是低电平有效还是高电平有效,假设此时是低电平有效。
HSPW:有些地方也叫做 thp,是 HSYNC 信号宽度,也就是 HSYNC 信号持续时间。HSYNC 信号不是一个脉冲,而是需要持续一段时间才是有效的,单位为 CLK。
HBP:有些地方叫做 thb,前面已经讲过了,术语叫做行同步信号后肩,单位是 CLK
HOZVAL:有些地方叫做 thd,显示一行数据所需的时间,假如屏幕分辨率为 1024*600, 那么 HOZVAL 就是 1024,单位为 CLK。
HFP:有些地方叫做 thf,前面已经讲过了,术语叫做行同步信号前肩,单位是 CLK。
当 HSYNC 信号发出以后,需要等待 HSPW+HBP 个 CLK 时间才会接收到真正有效的像素 数据。当显示完一行数据以后需要等待 HFP 个 CLK 时间才能发出下一个 HSYNC 信号,所以 显示一行所需要的时间就是:HSPW + HBP + HOZVAL + HFP。
VSYNC:帧同步信号,当此信号有效的话就表示开始显示新的一帧数据,查阅所使用的 LCD 数据手册可以知道此信号是低电平有效还是高电平有效,假设此时是低电平有效。
VSPW:有些地方也叫做 tvp,是 VSYNC 信号宽度,也就是 VSYNC 信号持续时间,单位 为 1 行的时间。
VBP:有些地方叫做 tvb,前面已经讲过了,术语叫做帧同步信号后肩,单位为 1 行的时 间。
LINE:有些地方叫做 tvd,显示一帧有效数据所需的时间,假如屏幕分辨率为 1024*600, 那么 LINE 就是 600 行的时间。
VFP:有些地方叫做 tvf,前面已经讲过了,术语叫做帧同步信号前肩,单位为 1 行的时间。
显示一帧所需要的时间就是:VSPW+VBP+LINE+VFP 个行时间,最终的计算公式: T = (VSPW+VBP+LINE+VFP) * (HSPW + HBP + HOZVAL + HFP)
1.1.5、像素时钟
像素时钟就是 RGB LCD 的时钟信号,以 ATK7016 这款屏幕为例,显示一帧图像所需要的 时钟数就是:
= (VSPW+VBP+LINE+VFP) * (HSPW + HBP + HOZVAL + HFP) = (3 + 20 + 600 + 12) * (20 + 140 + 1024 + 160) = 635 * 1344 = 853440。
显示一帧图像需要853440个时钟数,那么显示60帧就是:853440 * 60 = 51206400≈51.2M, 所以像素时钟就是 51.2MHz。
①、此部分是一个选择器,用于选择哪个 PLL 可以作为 LCDIF 时钟源,由寄存器 CCM_CSCDR2 的位 LCDIF1_PRE_CLK_SEL(bit17:15)来决定,LCDIF1_PRE_CLK_SEL 选择设 置如表 24.1.1.4 所示:
②、此部分是 LCDIF 时钟的预分频器,由寄存器 CCM_CSCDR2 的位 LCDIF1_PRED 来决 定预分频值。可设置值为 0~7,分别对应 1~8 分频。
③、此部分进一步分频,由寄存器 CBCMR 的位 LCDIF1_PODF 来决定分频值。可设置值 为 0~7,分别对应 1~8 分频
④、此部分是一个选择器,选择 LCDIF 最终的根时钟,由寄存器 CSCDR2 的位 LCDIF1_CLK_SEL 决定,LCDIF1_CLK_SEL 选择设置如表 24.1.1.5 所示:
这里肯定选择 PLL5 出来的那一路时钟作为 LCDIF 的根时钟,因此 LCDIF1_CLK_SEL 设 置为 0。LCDIF 既然选择了 PLL5 作为时钟源,那么还需要初始化 PLL5,LCDIF 的时钟是由 PLL5 和图 24.1.1.8 中的②、③这两个分频值决定的,所以需要对这三个进行合理的设置以搭配 出所需的时钟值,我们就以 ATK7016 屏幕所需的 51.2MHz 为例,看看如何进行配置。
PLL5 频率设置涉及到四个寄存器:CCM_PLL_VIDEO、CCM_PLL_VIDEO_NUM、 CCM_PLL_VIDEO_DENOM 、 CCM_MISC2 。 其 中 CCM_PLL_VIDEO_NUM 和 CCM_PLL_VIDEO_DENOM 这两个寄存器是用于小数分频的,我们这里为了简单不使用小数 分频,因此这两个寄存器设置为 0。
PLL5 的时钟计算公式如下:
PLL5_CLK = OSC24M * (loopDivider + (denominator / numerator)) / postDivider
不使用小数分频的话 PLL5 时钟计算公式就可以简化为 :
PLL5_CLK = OSC24M * loopDivider / postDivider
OSC24M 就是 24MHz 的有源晶振,现在的问题就是设置 loopDivider 和 postDivider。先来 看一下寄存器 CCM_PLL_VIDEO,此寄存器结构如图 24.1.1.9 所示:
POST_DIV_SLECT(bit20:19):此位和寄存器 CCM_ANALOG_CCMSC2 的 VIDEO_DIV 位 共同决定了 postDivider,为 0 的话是 4 分频,为 1 的话是 2 分频,为 2 的话是 1 分频。本章设 置为 2,也就是 1 分频。
ENABLE(bit13):PLL5(PLL_VIDEO)使能位,为 1 的话使能 PLL5,为 0 的话关闭 PLL5
DIV_SELECT(bit6:0):loopDivider 值,范围为 27~54,本章设置为 32。
寄存器 CCM_ANALOG_MISC2 的位 VIDEO_DIV(bit31:30)与寄存器 CCM_PLL_VIDEO 的 位 POST_DIV_SLECT(bit20:19)共同决定了 postDivider,通过这两个的配合可以获得 2、4、8、 16 分频。本章将 VIDEO_DIV 设置为 0,也就是 1 分频,因此 postDivider 就是 1,loopDivider 设置为 32,PLL5 的时钟频率就是:
PLL5_CLK = OSC24M * loopDivider / postDivider = 24M * 32 / 1 = 768MHz。
PLL5 此时为 768MHz,在经过图 24.1.1.8 中的②和③进一步分频,设置②中为 3 分频,也 就是寄存器 CCM_CSCDR2 的位 LCDIF1_PRED(bit14:12)为 2。设置③中为 5 分频,就是寄存器 CCM_CBCMR 的位 LCDIF1_PODF(bit25:23)为 4。设置好以后最终进入到 LCDIF 的时钟频率就 是:768/3/5 =51.2MHz,这就是我们需要的像素时钟频率。
1.1.6、显存
在讲像素格式的时候就已经说过了,如果采用 ARGB8888 格式的话一个像素需要 4 个字节 的内存来存放像素数据,那么 1024x600 分辨率就需要 1024x600x4=2457600B≈2.4MB 内存。但 是 RGB LCD 内部是没有内存的,所以就需要在开发板上的 DDR3 中分出一段内存作为 RGB LCD 屏幕的显存,我们如果要在屏幕上显示什么图像的话直接操作这部分显存即可。
1.2、 eLCDIF 接口
eLCDIF 是 I.MX6U 自带的液晶屏幕接口,用于连接 RGB LCD 接口的屏幕,eLCDIF 接口 特性如下:
①、支持 RGB LCD 的 DE 模式。
②、支持 VSYNC 模式以实现高速数据传输。
③、支持 ITU-R BT.656 格式的 4:2:2 的 YCbCr 数字视频,并且将其转换为模拟 TV 信号。
④、支持 8/16/18/24/32 位 LCD。 eLCDIF 支持三种接口:MPU 接口、VSYNC 接口和 DOTCLK 接口,这三种接口区别如下:
1.2.1、MPU 接口
MPU 接口用于在 I.MX6U 和 LCD 屏幕直接传输数据和命令,这个接口用于 6080/8080 接 口的 LCD 屏幕,比如我们学习 STM32 的时候常用到的 MCU 屏幕。如果寄存器 LCDIF_CTRL 的位 DOTCLK_MODE、DVI_MODE 和 VSYNC_MODE 都为 0 的话就表示 LCDIF 工作在 MPU 接口模式。
1.2.2、VSYNC 接口
VSYNC 接口时序和 MPU 接口时序基本一样,只是多了 VSYNC 信号来作为帧同步,当 LCDIF_CTRL 的位 VSYNC_MODE 为 1 的时候此接口使能。
1.2.3、DOTCLK 接口
DOTCLK 接口就是用来连接 RGB LCD 接口屏幕的, 它包括 VSYNC、HSYNC、DOTCLK 和 ENABLE(可选的)这四个信号,这样的接口通常被称为 RGB 接口。
eLCDIF 要驱动起来 RGB LCD 屏幕,重点是配置好上一小节讲解的那些时间参数即可,这 个通过配置相应的寄存器就可以了,所以我们接下来看一下 eLCDIF 接口的几个重要的寄存器, 首先看一下 LCDIF_CTRL 寄存器,此寄存器结构如图 24.1.2.1 所示:
SFTRST(bit31):eLCDIF 软复位控制位,当此位为 1 的话就会强制复位 LCD。
CLKGATE(bit30):正常运行模式下,此位必须为 0!如果此位为 1 的话时钟就不会进入到 LCDIF。
BYPASS_COUNT(bit19):如果要工作在 DOTCLK 模式的话就此位必须为 1。
VSYNC_MODE(bit18):此位为 1 的话 LCDIF 工作在 VSYNC 接口模式。
DOTCLK_MODE(bit17):此位为 1 的话 LCDIF 工作在 DOTCLK 接口模式。
INPUT_DATA_SWIZZLE(bit15:14):输入数据字节交换设置,此位为 0 的话不交换字节也 就是小端模式;为 1 的话交换所有字节,也就是大端模式;为 2 的话半字交换;为 3 的话在每 个半字内进行字节交换。本章我们设置为 0,也就是不使用字节交换。
CSC_DATA_SWIZZLE(bit13:12) : CSC 数 据 字 节 交 换 设 置 , 交 换 方 式 和 INPUT_DATA_SWIZZLE 一样,本章设置为 0,不使用字节交换
LCD_DATABUS_WIDTH(bit11:10):LCD 数据总线宽度,为 0 的话总线宽度为 16 位;为 1 的话总线宽度为 8 位;为 2 的话总线宽度为 18 位;为 3 的话总线宽度为 24 位。本章我们使 用 24 位总线宽度。
WORD_LENGTH(bit9:8):输入的数据格式,也就是像素数据宽度,为 0 的话每个像素 16 位;为 1 的话每个像素 8 位;为 2 的话每个像素 18 位;为 3 的话每个像素 24 位。
MASTER(bit5):为 1 的话设置 eLCDIF 工作在主模式。
DATA_FORMAT_16_BIT(bit3):当此位为 1 并且 WORD_LENGTH 为 0 的时候像素格式 为 ARGB555,当此位为 0 并且 WORD_LENGTH 为 0 的时候像素格式为 RGB565。
DATA_FORMAT_18_BIT(bit2):只有当 WORD_LENGTH 为 2 的时候此位才有效,此位 为 0 的话低 18 位有效,像素格式为 RGB666,高 14 位数据无效。当此位为 1 的话高 18 位有 效,像素格式依旧是 RGB666,但是低 14 位数据无效。
DATA_FORMAT_24_BIT(bit1):只有当 WORD_LENGTH 为 3 的时候此位才有效,为 0 的 时候表示全部的 24 位数据都有效。为 1 的话实际输入的数据有效位只有 18 位,虽然输入的是 24 位数据,但是每个颜色通道的高 2 位数据会被丢弃掉。
RUN(bit0):eLCDIF 接口运行控制位,当此位为 1 的话 eLCDIF 接口就开始传输数据,也 就是 eLCDIF 的使能位。
寄 存 器 LCDIF_CTRL1 , 此 寄 存 器 我 们 只 用 到 位 BYTE_PACKING_FORMAT(bit19:16),此位用来决定在 32 位的数据中哪些字节的数据有效,默 认值为 0XF,也就是所有的字节有效,当为 0 的话表示所有的字节都无效。如果显示的数据是 24 位(ARGB 格式,但是 A 通道不传输)的话就设置此位为 0X7。
寄存器 LCDIF_TRANSFER_COUNT,这个寄存器用来设置所连接的 RGB LCD 屏幕分辨率大小,此寄存器结构如图 24.1.2.2 所示:
寄存器LCDIF_TRANSFER_COUNT分为两部分,高16位和低16位,高16位是V_COUNT, 是 LCD 的垂直分辨率。低 16 位是 H_COUNT,是 LCD 的水平分辨率。如果 LCD 分辨率为 1024*600 的话,那么 V_COUNT 就是 600,H_COUNT 就是 1024。
寄存器 LCDIF_VDCTRL0,这个寄存器是 VSYNC 和 DOTCLK 模式控制寄 存器 0,寄存器结构如图 24.1.2.3 所示:
VSYNC_OEB(bit29):VSYNC 信号方向控制位,为 0 的话 VSYNC 是输出,为 1 的话 VSYNC 是输入。
ENABLE_PRESENT(bit28):EBABLE 数据线使能位,也就是 DE 数据线。为 1 的话使能 ENABLE 数据线,为 0 的话关闭 ENABLE 数据线。
VSYNC_POL(bit27):VSYNC 数据线极性设置位,为 0 的话 VSYNC 低电平有效,为 1 的 话 VSYNC 高电平有效,要根据所使用的 LCD 数据手册来设置。
HSYNC_POL(bit26):HSYNC 数据线极性设置位,为 0 的话 HSYNC 低电平有效,为 1 的 话 HSYNC 高电平有效,要根据所使用的 LCD 数据手册来设置。
DOTCLK_POL(bit25):DOTCLK 数据线(像素时钟线 CLK) 极性设置位,为 0 的话下降沿 锁存数据,上升沿捕获数据,为 1 的话相反,要根据所使用的 LCD 数据手册来设置。
ENABLE_POL(bit24):EANBLE 数据线极性设置位,为 0 的话低电平有效,为 1 的话高 电平有效。
VSYNC_PERIOD_UNIT(bit21):VSYNC 信号周期单位,为 0 的话 VSYNC 周期单位为像 素时钟。为 1 的话 VSYNC 周期单位是水平行,如果使用 DOTCLK 模式话就要设置为 1。
VSYNC_PULSE_WIDTH_UNIT(bit20) : VSYNC 信 号 脉 冲 宽 度 单 位 , 和 VSYNC_PERIOD_UNUT 一样,如果使用 DOTCLK 模式的话要设置为 1。
VSYNC_PULSE_WIDTH(bit17:0):VSPW 参数设置位。
寄存器 LCDIF_VDCTRL1,这个寄存器是 VSYNC 和 DOTCLK 模式控制寄 存器 1,此寄存器只有一个功能,用来设置 VSYNC 总周期,就是:屏幕高度+VSPW+VBP+VFP。
下寄存器 LCDIF_VDCTRL2,这个寄存器分为高 16 位和低 16 位两部分,高 16 位是 HSYNC_PULSE_WIDTH,用来设置 HSYNC 信号宽度,也就是 HSPW。低 16 位是 HSYNC_PERIOD,设置 HSYNC 总周期,就是:屏幕宽度+HSPW+HBP+HFP。
寄存器 LCDIF_VDCTRL3,此寄存器结构如图 24.1.2.4 所示:
HORIZONTAL_WAIT_CNT(bit27:16):此位用于 DOTCLK 模式,用于设置 HSYNC 信号 产生到有效数据产生之间的时间,也就是 HSPW+HBP。
VERTICAL_WAIR_CNT(bit15:0):和 HORIZONTAL_WAIT_CNT 一样,只是此位用于 VSYNC 信号,也就是 VSPW+VBP。
寄存器 LCDIF_VDCTRL4,此寄存器结构如图 24.1.2.5 所示:
SYNC_SIGNALS_ON(bit18):同步信号使能位,设置为 1 的话使能 VSYNC、HSYNC、 DOTCLK 这些信号。 DOTCLK_H_VALID_DATA_CNT(bit15:0):设置 LCD 的宽度,也就是水平像素数量。
寄存器 LCDIF_CUR_BUF 和 LCDIF_NEXT_BUF,这两个寄存器分别为当前 帧和下一帧缓冲区,也就是 LCD 显存。一般这两个寄存器保存同一个地址,也就是划分给 LCD 的显存首地址。
使用 I.MX6U 的 eLCDIF 接口来驱动 ALIENTEK 的 ATK7016 这款屏幕,配置步骤如下:
1、初始化 LCD 所使用的 IO
首先肯定是初始化 LCD 所示使用的 IO,将其复用为 eLCDIF 接口 IO。
2、设置 LCD 的像素时钟
查阅所使用的 LCD 屏幕数据手册,或者自己计算出的时钟像素,然后设置 CCM 相应的寄 存器。
3、配置 eLCDIF 接口
设置 LCDIF 的寄存器 CTRL、CTRL1、TRANSFER_COUNT、VDCTRL0~4、CUR_BUF 和 NEXT_BUF。根据 LCD 的数据手册设置相应的参数。
4、编写 API 函数
驱动 LCD 屏幕的目的就是显示内容,所以需要编写一些基本的 API 函数,比如画点、画 线、画圆函数,字符串显示函数等。
二十、RTC 实时时钟实验
实时时钟是很常用的一个外设,通过实时时钟我们就可以知道年、月、日和时间等信息。 因此在需要记录时间的场合就需要实时时钟,可以使用专用的实时时钟芯片来完成此功能,但 是现在大多数的 MCU 或者 MPU 内部就已经自带了实时时钟外设模块。
1、I.MX6U RTC 简介
I.MX6U 内部也有 个 RTC 模块,但是不叫作“RTC”,而是叫做“SNVS”。
SNVS 直译过来就是安全的非易性存储,SNVS 里面主要是一些低功耗的外设,包括一个 安全的实时计数器(RTC)、一个单调计数器(monotonic counter)和一些通用的寄存器。
SNVS 里面的外设在芯片掉电以后由电池供电继续运行,I.MX6UALPHA 开发板上有一个纽扣电池,这个纽扣电池就是在主电源关闭以后为 SNVS 供电的
因为纽扣电池在掉电以后会继续给 SNVS 供电,因此实时计数器就会一直运行,这样的话 时间信息就不会丢失,除非纽扣电池没电了。在有纽扣电池作为后备电源的情况下,不管系统 主电源是否断电,SNVS 都正常运行。SNVS 有两部分:SNVS_HP 和 SNVS_LP,系统主电源断 电以后 SNVS_HP 也会断电,但是在后备电源支持下,SNVS_LP 是不会断电的,而且 SNVS_LP 是和芯片复位隔离开的,因此 SNVS_LP 相关的寄存器的值会一直保存着。
SNVS 分为两个子模块:SNVS_HP 和 SNVS_LP,也就是高功耗域(SNVS_HP)和低功耗域 (SNVS_LP),这两个域的电源来源如下:
SNVS_LP:专用的 always-powered-on 电源域,系统主电源和备用电源都可以为其供电。
SNVS_HP:系统(芯片)电源。
①、VDD_HIGH_IN 是系统(芯片)主电源,这个电源会同时供给给 SNVS_HP 和 SNVS_LP。
②、VDD_SNVS_IN 是纽扣电池供电的电源,这个电源只会供给给 SNVS_LP,保证在系 统主电源 VDD_HIGH_IN 掉电以后 SNVS_LP 会继续运行。
③、SNVS_HP 部分。
④、SNVS_LP 部分,此部分有个 SRTC,这个就是我们本章要使用的 RTC。
其实不管是 SNVS_HP 还是 SNVS_LP,其内部都有一个 SRTC,但是因为 SNVS_HP 在系 统电源掉电以后就会关闭,所以我们本章使用的是 SNVS_LP 内部的 SRTC。毕竟我们肯定都 不想开发板或者设备每次关闭以后时钟都被清零,然后开机以后先设置时钟。
SRTC,其本质就是一个定时 器,和我们在第八章讲的 EPIT 定时器一样,只要给它提供时钟,它就会一直运行。SRTC 需要 外界提供一个 32.768KHz 的时钟,I.MX6U-ALPHA 核心板上的 32.768KHz 的晶振就是提供这 个时钟的。寄存器 SNVS_LPSRTCMR 和 SNVS_LPSRTCLR 保存着秒数,直接读取这两个寄存 器的值就知道过了多长时间了。一般以 1970 年 1 月 1 日为起点,加上经过的秒数即可得到现在 的时间和日期,原理还是很简单的。SRTC 也是带有闹钟功能的,可以在寄存器 SNVS_LPAR 中 写入闹钟时间值,当时钟值和闹钟值匹配的时候就会产生闹钟中断,要使用时钟功能的话还需 要进行一些设置,本章我们就不使用闹钟了。
与 SRTC 相关的部分寄存器,首先是 SNVS_HPCOMR 寄 存器,这个寄存器我们只用到了位:NPSWA_EN(bit31),这个位是非特权软件访问控制位,如 果非特权软件要访问 SNVS 的话此位必须为 1。
接下来看一下寄存器SNVS_LPCR寄存器,此寄存器也只用到了一个位:SRTC_ENV(bit0),此位为 1 的话就使能 STC 计数器。
寄存器 SNVS_SRTCMR 和 SNVS_SRTCLR,这两个寄存器保存着 RTC 的秒 数。
①、SRTC 计数器是 32 位的,不是 47 位! ②、SNVS_SRTCMR 的 bit14:0 这 15 位是 SRTC 计数器的高 15 位。 ③、SNVS_SRTCLR 的 bit31:bit15 这 17 位是 SRTC 计数器的低 17 位。
使用 I.MX6U 的 SNVS_LP 的 SRTC, 配置步骤如下: 1、初始化 SNVS_SRTC 初始化 SNVS_LP 中的 SRTC。 2、设置 RTC 时间 第一次使用 RTC 肯定要先设置时间。 3、使能 RTC 配置好 RTC 并设置好初始时间以后就可以开启 RTC 了。
二十一、I2C 实验
I2C 是最常用的通信接口,众多的传感器都会提供 I2C 接口来和主控相连,比如陀螺仪、 加速度计、触摸屏等等。所以 I2C 是做嵌入式开发必须掌握的,I.MX6U 有 4 个 I2C 接口,可 以通过这 4 个 I2C 接口来连接一些 I2C 外设。
1、 I2C & AP3216C 简介
1.1 、I2C 简介
I2C 是很常见的一种总线协议,I2C 是 NXP 公司设计的,I2C 使用两条线在主控制器和从 机之间进行数据通信。一条是 SCL(串行时钟线),另外一条是 SDA(串行数据线),这两条数据 线需要接上拉电阻,总线空闲的时候 SCL 和 SDA 处于高电平。I2C 总线标准模式下速度可以 达到 100Kb/S,快速模式下可以达到 400Kb/S。I2C 总线工作是按照一定的协议来运行的,接下 来就看一下 I2C 协议。
I2C 是支持多从机的,也就是一个 I2C 控制器下可以挂多个 I2C 从设备,这些不同的 I2C 从设备有不同的器件地址,这样 I2C 主控制器就可以通过 I2C 设备的器件地址访问指定的 I2C 设备了,一个 I2C 总线连接多个 I2C 设备如图 26.1.1.1 所示:
SDA 和 SCL 这两根线必须要接一个上拉电阻,一般是 4.7K。其余的 I2C 从 器件都挂接到 SDA 和 SCL 这两根线上,这样就可以通过 SDA 和 SCL 这两根线来访问多个 I2C 设备。
I2C 协议有关的术语:
1、起始位
顾名思义,也就是 I2C 通信起始标志,通过这个起始位就可以告诉 I2C 从机,“我”要开始 进行 I2C 通信了。在 SCL 为高电平的时候,SDA 出现下降沿就表示为起始位,如图 26.1.1.2 所 示:
2、停止位
停止位就是停止 I2C 通信的标志位,和起始位的功能相反。在 SCL 位高电平的时候,SDA 出现上升沿就表示为停止位,如图 26.1.1.3 所示:
3、数据传输
I2C 总线在数据传输的时候要保证在 SCL 高电平期间,SDA 上的数据稳定,因此 SDA 上 的数据变化只能在 SCL 低电平期间发生,如图 26.1.1.4 所示:
4、应答信号
当 I2C 主机发送完 8 位数据以后会将 SDA 设置为输入状态,等待 I2C 从机应答,也就是 等到 I2C 从机告诉主机它接收到数据了。应答信号是由从机发出的,主机需要提供应答信号所 需的时钟,主机发送完 8 位数据以后紧跟着的一个时钟信号就是给应答信号使用的。从机通过 将 SDA 拉低来表示发出应答信号,表示通信成功,否则表示通信失败。
5、I2C 写时序
主机通过 I2C 总线与从机之间进行通信不外乎两个操作:写和读,I2C 总线单字节写时序 如图 26.1.1.5 所示:
图 26.1.1.5 就是 I2C 写时序,我们来看一下写时序的具体步骤:
1)、开始信号。
2)、发送 I2C 设备地址,每个 I2C 器件都有一个设备地址,通过发送具体的设备地址来决 定访问哪个 I2C 器件。这是一个 8 位的数据,其中高 7 位是设备地址,最后 1 位是读写位,为 1 的话表示这是一个读操作,为 0 的话表示这是一个写操作。
3)、 I2C 器件地址后面跟着一个读写位,为 0 表示写操作,为 1 表示读操作。
4)、从机发送的 ACK 应答信号。
5)、重新发送开始信号。
6)、发送要写写入数据的寄存器地址。
7)、从机发送的 ACK 应答信号。
8)、发送要写入寄存器的数据。
9)、从机发送的 ACK 应答信号。
10)、停止信号。
6、I2C 读时序
I2C 单字节读时序比写时序要复杂一点,读时序分为 4 大步,第一步是发送设备地址,第 二步是发送要读取的寄存器地址,第三步重新发送设备地址,最后一步就是 I2C 从器件输出要 读取的寄存器值,我们具体来看一下这几步。 1)、主机发送起始信号。 2)、主机发送要读取的 I2C 从设备地址。 3)、读写控制位,因为是向 I2C 从设备发送数据,因此是写信号。 4)、从机发送的 ACK 应答信号。 5)、重新发送 START 信号。 6)、主机发送要读取的寄存器地址。 7)、从机发送的 ACK 应答信号。 8)、重新发送 START 信号。 9)、重新发送要读取的 I2C 从设备地址。 10)、读写控制位,这里是读信号,表示接下来是从 I2C 从设备里面读取数据。 11)、从机发送的 ACK 应答信号。 12)、从 I2C 器件里面读取到的数据。 13)、主机发出 NO ACK 信号,表示读取完成,不需要从机再发送 ACK 信号了。 14)、主机发出 STOP 信号,停止 I2C 通信。
2、 I.MX6U I2C 简介
I.MX6U 提供了 4 个 I2C 外设,通过这四个 I2C 外设即可完成与 I2C 从器件进行通信, I.MX6U 的 I2C 外设特性如下: ①、与标准 I2C 总线兼容。 ②、多主机运行 ③、软件可编程的 64 中不同的串行时钟序列。 ④、软件可选择的应答位。 ⑤、开始/结束信号生成和检测。 ⑥、重复开始信号生成。 ⑦、确认位生成。 ⑧、总线忙检测
I.MX6U 的 I2C 支持两种模式:标准模式和快速模式,标准模式下 I2C 数据传输速率最高 是 100Kbits/s,在快速模式下数据传输速率最高为 400Kbits/s。
I2Cx_IADR(x=1~4)寄存器,这是 I2C 的地址寄存器,此寄存器结构如图 26.1.2.1 所示:
寄存器 I2Cx_IADR 只有 ADR(bit7:1)位有效,用来保存 I2C 从设备地址数据。当我们要访 问某个 I2C 从设备的时候就需要将其设备地址写入到 ADR 里面。接下来看一下寄存器 I2Cx_IFDR,这个是 I2C 的分频寄存器,
寄存器 I2Cx_IFDR 也只有 IC(bit5:0)这个位,用来设置 I2C 的波特率,I2C 的时钟源可以选 择 IPG_CLK_ROOT=66MHz,通过设置 IC 位既可以得到想要的 I2C 波特率。IC 位可选的设置 如图 26.1.2.3 所示:
不像其他外设的分频设置一样可以随意设置,图 26.1.2.3 中列出了 IC 的所有可选值。比如 现在I2C的时钟源为66MHz,我们要设置I2C的波特率为100KHz,那么IC就可以设置为0X15, 也就是 640 分频。66000000/640=103.125KHz≈100KHz。
寄存器 I2Cx_I2CR,这个是 I2C 控制寄存器,此寄存器结构如图 26.1.2.4 所 示:
寄存器 I2Cx_I2CR 的各位含义如下:
IEN(bit7):I2C 使能位,为 1 的时候使能 I2C,为 0 的时候关闭 I2C。
IIEN(bit6):I2C 中断使能位,为 1 的时候使能 I2C 中断,为 0 的时候关闭 I2C 中断。
MSTA(bit5):主从模式选择位,设置 IIC 工作在主模式还是从模式,为 1 的时候工作在主 模式,为 0 的时候工作在从模式。
MTX(bit4):传输方向选择位,用来设置是进行发送还是接收,为 0 的时候是接收,为 1 的 时候是发送。
TXAK(bit3):传输应答位使能,为 0 的话发送 ACK 信号,为 1 的话发送 NO ACK 信号。
RSTA(bit2):重复开始信号,为 1 的话产生一个重新开始信号。
寄存器 I2Cx_I2SR,这个是 I2C 的状态寄存器,寄存器结构如图 26.1.2.5 所 示:
寄存器 I2Cx_I2SR 的各位含义如下:
ICF(bit7):数据传输状态位,为 0 的时候表示数据正在传输,为 1 的时候表示数据传输完 成。
IAAS(bit6):当为 1 的时候表示 I2C 地址,也就是 I2Cx_IADR 寄存器中的地址是从设备地 址。
IBB(bit5):I2C 总线忙标志位,当为 0 的时候表示 I2C 总线空闲,为 1 的时候表示 I2C 总 线忙。
IAL(bit4):仲裁丢失位,为 1 的时候表示发生仲裁丢失。
SRW(bit2):从机读写状态位,当 I2C 作为从机的时候使用,此位用来表明主机发送给从机 的是读还是写命令。为 0 的时候表示主机要向从机写数据,为 1 的时候表示主机要从从机读取 数据。
IIF(bit1):I2C 中断挂起标志位,当为 1 的时候表示有中断挂起,此位需要软件清零。
RXAK(bit0):应答信号标志位,为 0 的时候表示接收到 ACK 应答信号,为 1 的话表示检 测到 NO ACK 信号。
最后一个寄存器就是 I2Cx_I2DR,这是 I2C 的数据寄存器,此寄存器只有低 8 位有效,当 要发送数据的时候将要发送的数据写入到此寄存器,如果要接收数据的话直接读取此寄存器即 可得到接收到的数据。
3、AP3216C 简介
三合一环境传感器:AP3216C,AP3216C 是由敦南科技推出的一款传感器,其支持环境光强度(ALS)、接近距离(PS)和红外线强度(IR)这 三个环境参数检测。该芯片可以通过 IIC 接口与主控制相连,并且支持中断,AP3216C 的特点 如下: ①、I2C 接口,快速模式下波特率可以到 400Kbit/S
②、多种工作模式选择:ALS、PS+IR、ALS+PS+IR、PD 等等。
③、内建温度补偿电路。
④、宽工作温度范围(-30°C ~ +80°C)。
⑤、超小封装,4.1mm x 2.4mm x 1.35mm
⑥、环境光传感器具有 16 位分辨率。
⑦、接近传感器和红外传感器具有 10 位分辨率。
AP3216C 常被用于手机、平板、导航设备等,其内置的接近传感器可以用于检测是否有物 体接近,比如手机上用来检测耳朵是否接触听筒,如果检测到的话就表示正在打电话,手机就 会关闭手机屏幕以省电。也可以使用环境光传感器检测光照强度,可以实现自动背光亮度调节。
AP3216 的设备地址为 0X1E,同几乎所有的 I2C 从器件一样,AP3216C 内部也有一些寄存 器,通过这些寄存器我们可以配置 AP3216C 的工作模式,并且读取相应的数据。AP3216C 我 们用的寄存器如表 26.1.3.1 所示:
0X00 这个寄存器是模式控制寄存器,用来设置 AP3216C 的工作模式, 一般开始先将其设置为 0X04,也就是先软件复位一次 AP3216C。接下来根据实际使用情况选 择合适的工作模式,比如设置为 0X03,也就是开启 ALS+PS+IR。从 0X0A~0X0F 这 6 个寄存 器就是数据寄存器,保存着 ALS、PS 和 IR 这三个传感器获取到的数据值。如果同时打开 ALS、 PS 和 IR 则读取间隔最少要 112.5ms,因为 AP3216C 完成一次转换需要 112.5ms。
本章的配 置步骤如下:
1、初始化相应的 IO
初始化 I2C1 相应的 IO,设置其复用功能,如果要使用 AP3216C 中断功能的话,还需要设 置 AP3216C 的中断 IO。
2、初始化 I2C1
初始化 I2C1 接口,设置波特率。
3、初始化 AP3216C
初始化 AP3216C,读取 AP3216C 的数据。
二十二、SPI 实验
SPI 是很常用的通信接口,也可以通过 SPI 来连接众多的传感器。相比 I2C 接 口,SPI 接口的通信速度很快,I2C 最多 400KHz,但是 SPI 可以到达几十 MHz。I.MX6U 也有 4 个 SPI 接口,可以通过这 4 个 SPI 接口来连接一些 SPI 外设。
1、SPI & ICM-20608 简介
,I2C 是串行通信的一种,只需要两根线就可以完成主机和从机之间 的通信,但是 I2C 的速度最高只能到 400KHz,如果对于访问速度要求比价高的话 I2C 就不适 合了。本章我们就来学习一下另外一个和 I2C 一样广泛使用的串行通信:SPI,SPI 全称是 Serial Perripheral Interface,也就是串行外围设备接口。SPI 是 Motorola 公司推出的一种同步串行接口 技术,是一种高速、全双工的同步通信总线,SPI 时钟频率相比 I2C 要高很多,最高可以工作 在上百 MHz。SPI 以主从方式工作,通常是有一个主设备和一个或多个从设备,一般 SPI 需要 4 根线,但是也可以使用三根线(单向传输)
这四根线如下:
①、CS/SS,Slave Select/Chip Select,这个是片选信号线,用于选择需要进行通信的从设备。 I2C 主机是通过发送从机设备地址来选择需要进行通信的从机设备的,SPI 主机不需要发送从机 设备,直接将相应的从机设备片选信号拉低即可。 ②、SCK,Serial Clock,串行时钟,和 I2C 的 SCL 一样,为 SPI 通信提供时钟。
③、MOSI/SDO,Master Out Slave In/Serial Data Output,简称主出从入信号线,这根数据线 只能用于主机向从机发送数据,也就是主机输出,从机输入。
④、MISO/SDI,Master In Slave Out/Serial Data Input,简称主入从出信号线,这根数据线只 能用户从机向主机发送数据,也就是主机输入,从机输出。
SPI 通信都是由主机发起的,主机需要提供通信的时钟信号。主机通过 SPI 线连接多个从 设备的结构如图 27.1.1.1 所示:
SPI 有四种工作模式,通过串行时钟极性(CPOL)和相位(CPHA)的搭配来得到四种工作模式:
①、CPOL=0,串行时钟空闲状态为低电平。
②、CPOL=1,串行时钟空闲状态为高电平,此时可以通过配置时钟相位(CPHA)来选择具 体的传输协议。
③、CPHA=0,串行时钟的第一个跳变沿(上升沿或下降沿)采集数据。
④、CPHA=1,串行时钟的第二个跳变沿(上升沿或下降沿)采集数据。
跟 I2C 一样,SPI 也是有时序图的,以 CPOL=0,CPHA=0 这个工作模式为例,SPI 进行全 双工通信的时序如图 27.1.1.3 所示:
SPI 的时序图很简单,不像 I2C 那样还要分为读时序和写时序,因 为 SPI 是全双工的,所以读写时序可以一起完成。图 27.1.1.3 中,CS 片选信号先拉低,选中要 通信的从设备,然后通过 MOSI 和 MISO 这两根数据线进行收发数据,MOSI 数据线发出了 0XD2 这个数据给从设备,同时从设备也通过 MISO 线给主设备返回了 0X66 这个数据。这个就 是 SPI 时序图。
2、I.MX6U ECSPI 简介
I.MX6U 自带的 SPI 外设叫做 ECSPI,全称是 Enhanced Configurable Serial Peripheral Interface, 别看前面加了个“EC”就以为和标准 SPI 有啥不同的,其实就是 SPI。ECSPI 有 6432 个接收 FIFO(RXFIFO)和 6432 个发送 FIFO(TXFIFO),ECSPI 特性如下:
①、全双工同步串行接口。②、可配置的主/从模式。 ③、四个片选信号,支持多从机。 ④、发送和接收都有一个 32x64 的 FIFO。 ⑤、片选信号 SS/CS,时钟信号 SCLK 极性可配置。 ⑥、支持 DMA。
I.MX6U 的 ECSPI 可以工作在主模式或从模式,本章我们使用主模式,I.MX6U 有 4 个 ECSPI,每个 ECSPI 支持四个片选信号,也就说,如果你要使用 ECSPI 的硬件片选信号的话, 一个 ECSPI 可以支持 4 个外设。如果不使用硬件的片选信号就可以支持无数个外设,本章实验 我们不使用硬件片选信号,因为硬件片选信号只能使用指定的片选 IO,软件片选的话可以使用 任意的 IO。
ECSPI 的几个重要的寄存器,首先看一下 ECSPIx_CONREG(x=1~4)寄 存器,这是 ECSPI 的控制寄存器,此寄存器结构如图 27.1.2.1 所示:
寄存器 ECSPIx_CONREG 各位含义如下:
BURST_LENGTH(bit31:24):突发长度,设置 SPI 的突发传输数据长度,在一次 SPI 发送 中最大可以发送 2^12bit 数据。可以设置 0X000~0XFFF,分别对应 1~2^12bit。我们一般设置突 发长度为一个字节,也就是 8bit,BURST_LENGTH=7。
CHANNEL_SELECT(bit19:18):SPI 通道选择,一个 ECSPI 有四个硬件片选信号,每个片 选信号是一个硬件通道,虽然我们本章实验使用的软件片选,但是 SPI 通道还是要选择的。可 设置为 0~3,分别对应通道 0~3。I.MX6U-ALPHA 开发板上的 ICM-20608 的片选信号接的是 ECSPI3_SS0,也就是 ECSPI3 的通道 0,所以本章实验设置为 0。
DRCTL(bit17:16):SPI 的 SPI_RDY 信号控制位,用于设置 SPI_RDY 信号,为 0 的话不关 心 SPI_RDY 信号;为 1 的话 SPI_RDY 信号为边沿触发;为 2 的话 SPI_DRY 是电平触发。
PRE_DIVIDER(bit15:12):SPI 预分频,ECSPI 时钟频率使用两步来完成分频,此位设置的 是第一步,可设置 0~15,分别对应 1~16 分频。
POST_DIVIDER(bit11:8):SPI 分频值,ECSPI 时钟频率的第二步分频设置,分频值为 2^POST_DIVIDER。
CHANNEL_MODE(bit7:4):SPI 通道主/从模式设置,CHANNEL_MODE[3:0]分别对应 SPI 通道 3~0,为 0 的话就是设置为从模式,如果为 1 的话就是主模式。比如设置为 0X01 的话就是 设置通道 0 为主模式。
SMC(bit3):开始模式控制,此位只能在主模式下起作用,为 0 的话通过 XCH 位来开启 SPI 突发访问,为 1 的话只要向 TXFIFO 写入数据就开启 SPI 突发访问。
XCH(bit2):此位只在主模式下起作用,当 SMC 为 0 的话此位用来控制 SPI 突发访问的开 启。
HT(bit1):HT 模式使能位,I.MX6ULL 不支持。
EN(bit0):SPI 使能位,为 0 的话关闭 SPI,为 1 的话使能 SPI。
下寄存器 ECSPIx_CONFIGREG,这个也是 ECSPI 的配置寄存器,此寄存器结构如图 27.1.2.2 所示:
寄存器 ECSPIx_CONFIGREG 用到的重要位如下:
HT_LENGTH(bit28:24):HT 模式下的消息长度设置,I.MX6ULL 不支持。
SCLK_CTL(bit23:20):设置 SCLK 信号线空闲状态电平,SCLK_CTL[3:0]分别对应通道 3~0,为 0 的话 SCLK 空闲状态为低电平,为 1 的话 SCLK 空闲状态为高电平。
DATA_CTL(bit19:16):设置 DATA 信号线空闲状态电平,DATA_CTL[3:0]分别对应通道 3~0,为 0 的话 DATA 空闲状态为高电平,为 1 的话 DATA 空闲状态为低电平。
SS_POL(bit15:12):设置 SPI 片选信号极性设置,SS_POL[3:0]分别对应通道 3~0,为 0 的 话片选信号低电平有效,为 1 的话片选信号高电平有效。
SCLK_POL(bit7:4):SPI 时钟信号极性设置,也就是 CPOL,SCLK_POL[3:0]分别对应通 道 3~0,为 0 的话 SCLK 高电平有效(空闲的时候为低电平),为 1 的话 SCLK 低电平有效(空闲 的时候为高电平)。
SCLK_PHA(bit3:0):SPI时钟相位设置,也就是CPHA,SCLK_PHA[3:0]分别对应通道3~0, 为 0 的话串行时钟的第一个跳变沿(上升沿或下降沿)采集数据,为 1 的话串行时钟的第二个跳 变沿(上升沿或下降沿)采集数据。
通过 SCLK_POL 和 SCLK_PHA 可以设置 SPI 的工作模式。
寄存器 ECSPIx_PERIODREG,这个是 ECSPI 的采样周期寄存器,此寄存器 结构如图 27.1.2.3 所示:
寄存器 ECSPIx_PERIODREG 用到的重要位如下:
CSD_CTL(bit21:16):片选信号延时控制位,用于设置片选信号和第一个 SPI 时钟信号之 间的时间间隔,范围为 0~63。 CSRC(bit15):SPI 时钟源选择,为 0 的话选择 SPI CLK 为 SPI 的时钟源,为 1 的话选择 32.768KHz 的晶振为 SPI 时钟源。我们一般选择 SPI CLK 作为 SPI 时钟源,SPI CLK 时钟来源 如图 27.1.2.4 所示:
①、这是一个选择器,用于选择根时钟源,由寄存器 CSCDR2 的位 ECSPI_CLK_SEL 来控 制,为 0 的话选择 pll3_60m 作为 ECSPI 根时钟源。为 1 的话选择 osc_clk 作为 ECSPI 时钟源。 本章我们选择 pll3_60m 作为 ECSPI 根时钟源。 ②、ECSPI 时钟分频值,由寄存器 CSCDR2 的位 ECSPI_CLK_PODF 来控制,分频值为 2^ECSPI_CLK_PODF。本章我们设置为 0,也就是 1 分频。 ③、最终进入 ECSPI 的时钟,也就是 SPI CLK=60MHz。
SAMPLE_PERIO:采样周期寄存器,可设置为 0~0X7FFF 分别对应 0~32767 个周期。
下寄存器 ECSPIx_STATREG,这个是 ECSPI 的状态寄存器,此寄存器结构如图 27.1.2.5 所示:
寄存器 ECSPIx_STATREG 用到的重要位如下:
TC(bit7):传输完成标志位,为 0 表示正在传输,为 1 表示传输完成。
RO(bit6):RXFIFO 溢出标志位,为 0 表示 RXFIFO 无溢出,为 1 表示 RXFIFO 溢出。
RF(bit5):RXFIFO 空标志位,为 0 表示 RXFIFO 不为空,为 1 表示 RXFIFO 为空。
RDR(bit4):RXFIFO 数据请求标志位,此位为 0 表示 RXFIFO 里面的数据不大于 RX_THRESHOLD,此位为 1 的话表示 RXFIFO 里面的数据大于 RX_THRESHOLD。
RR(bit3):RXFIFO 就绪标志位,为 0 的话 RXFIFO 没有数据,为 1 的话表示 RXFIFO 中 至少有一个字的数据。 TF(bit2):TXFIFO 满标志位,为 0 的话表示 TXFIFO 不为满,为 1 的话表示 TXFIFO 为 满。
TDR(bit1):TXFIFO 数据请求标志位,为 0 表示 TXFIFO 中的数据大于 TX_THRESHOLD, 为 1 表示 TXFIFO 中的数据不大于 TX_THRESHOLD。
TE(bit0):TXFIFO 空标志位,为 0 表示 TXFIFO 中至少有一个字的数据,为 1 表示 TXFIFO 为空。
最后就是两个数据寄存器,ECSPIx_TXDATA 和 ECSPIx_RXDATA,这两个寄存器都是 32 位的,如果要发送数据就向寄存器 ECSPIx_TXDATA 写入数据,读取及存取 ECSPIx_RXDATA 里面的数据就可以得到刚刚接收到的数据。
3、ICM-20608 简介
ICM-20608 是 InvenSense 出品的一款 6 轴 MEMS 传感器,包括 3 轴加速度和 3 轴陀螺仪。
陀螺仪和加速度计都 是 16 位的 ADC,并且支持 I2C 和 SPI 两种协议,使用 I2C 接口的话通信速度最高可以达到 400KHz,使用 SPI 接口的话通信速度最高可达到 8MHz。I.MX6U-ALPHA 开发板上的 ICM20608 通过 SPI 接口和 I.MX6U 连接在一起。ICM-20608 特性如下:
①、陀螺仪支持 X,Y 和 Z 三轴输出,内部集成 16 位 ADC,测量范围可设置:±250,± 500,±1000 和±2000°/s。 ②、加速度计支持 X,Y 和 Z 轴输出,内部集成 16 位 ADC,测量范围可设置:±2g,±4g, ±4g,±8g 和±16g。 ③、用户可编程中断。 ④、内部包含 512 字节的 FIFO。 ⑤、内部包含一个数字温度传感器。 ⑥、耐 10000g 的冲击。 ⑦、支持快速 I2C,速度可达 400KHz。 ⑧、支持 SPI,速度可达 8MHz。
ICM-20608 也是通过读写寄存器来配置 和读取传感器数据,使用 SPI 接口读写寄存器需要 16 个时钟或者更多(如果读写操作包括多个 字节的话),第一个字节包含要读写的寄存器地址,寄存器地址最高位是读写标志位,如果是读 的话寄存器地址最高位要为 1,如果是写的话寄存器地址最高位要为 0,剩下的 7 位才是实际的 寄存器地址,寄存器地址后面跟着的就是读写的数据。
二十三、多点电容触摸屏实验
随着智能手机的发展,电容触摸屏也得到了飞速的发展。相比电阻触摸屏,电容触摸屏有 很多的优势,比如支持多点触控、不需要按压,只需要轻轻触摸就有反应。
1、多点电容触摸简介
ATK-7016 这款屏幕其实是由 TFT LCD+触摸屏组合起来的。底下是 LCD 面板,上面是触 摸面板,将两个封装到一起就成了带有触摸屏的 LCD 屏幕。电容触摸屏也是需要一个驱动 IC 的,驱动 IC 一般会提供一个 I2C 接口给主控制器,主控制器可以通过 I2C 接口来读取驱动 IC 里面的触摸坐标数据。ATK-7016、ATK-7084 这两款屏幕使用的触摸控制 IC 是 FT5426,ATK4342 使用的驱动 IC 是 GT9147。这三个电容屏触摸 IC 都是 I2C 接口的,使用方法基本一样。
FT5426 这款驱动 IC 采用 15*28 的驱动结构,也就是 15 个感应通道,28 个驱动通道,最 多支持 5 点电容触摸。ATK-7016 的电容触摸屏部分有 4 个 IO 用于连接主控制器:SCL、SDA、 RST 和 INT,SCL 和 SDA 是 I2C 引脚,RST 是复位引脚,INT 是中断引脚。一般通过 INT 引 脚来通知主控制器有触摸点按下,然后在 INT 中断服务函数中读取触摸数据。也可以不使用中 断功能,采用轮询的方式不断查询是否有触摸点按下,本章实验我们使用中断方式来获取触摸 数据。
二十四、LCD 背光调节实验
不管是使用显示器还是手机,其屏幕背光都是可以调节的,通过调节背光就可以控制屏幕 的亮度。在户外阳光强烈的时候可以通过调高背光来看清屏幕,在光线比较暗的地方可以调低 背光,防止伤眼睛并且省电。
1、LCD 背光调节简介
给这个背光控制引脚输入高电平就会 点亮背光,输入低电平就会关闭背光。假如我们不断的打开和关闭背光,当速度足够快的时候 就不会感觉到背光关闭这个过程了。这个正好可以使用 PWM 来完成,PWM 全称是 PulseWidth Modulation,也就是脉冲宽度调制,PWM 信号如图 29.1.1 所示:
PWM 信号有两个关键的术语:频率和占空比,频率就是开关速度,把一次开关算作一个周 期,那么频率就是 1 秒内进行了多少次开关。占空比就是一个周期内高电平时间和低电平时间 的比例,一个周期内高电平时间越长占空比就越大,反之占空比就越小。占空比用百分之表示, 如果一个周期内全是低电平那么占空比就是 0%,如果一个周期内全是高电平那么占空比就是 100%。
①、此部分是一个选择器,用于选择 PWM 信号的时钟源,一共有三种时钟源:ipg_clk、 ipg_clk_highfreq 和 ipg_clk_32k。
②、这是一个 12 位的分频器,可以对①中选择的时钟源进行分频。
③、这是 PWM 的 16 位计数器寄存器,保存着 PWM 的计数值。
④、这是 PWM 的 16 位周期寄存器,此寄存器用来控制 PWM 的频率。
⑤、这是 PWM 的 16 位采样寄存器,此寄存器用来控制 PWM 的占空比。
⑥、此部分是 PWM 的中断信号,PWM 是提供中断功能的,如果使能了相应的中断的话就 会产生中断。
⑦、此部分是 PWM 对应的输出 IO,产生的 PWM 信号就会从对应的 IO 中输出,I.MX6UALPHA 开发板的 LCD 背光控制引脚连接在 I.MX6U 的 GPIO1_IO8 上,GPIO1_IO8 可以复用 为 PWM1_OUT。
可以通过配置相应的寄存器来设置 PWM 信号的频率和占空比,PWM 的 16 位计数器是个 向上计数器,此计数器会从 0X0000 开始计数,直到计数值等于寄存器 PWMx_PWMPR(x=1~8) + 1,然后计数器就会重新从 0X0000 开始计数,如此往复。所以寄存器 PWMx_PWMPR 可以设 置 PWM 的频率。
PWM 的频率。 在一个周期内,PWM 从 0X0000 开始计数的时候,PWM 引脚先输出高电平(默认情况下, 可以通过配置输出低电平)。采样 FIFO 中保存的采样值会在每个时钟和计数器值进行比较,当 采样值和计数器相等的话 PWM 引脚就会改为输出低电平(默认情况下,同样可以通过配置输出 高电平)。计数器会持续计数,直到和周期寄存器 PWMx_PWMPR(x=1~8) + 1 的值相等,这样一个周期就完成了。所以,采样 FIFO 控制着占空比,而采样 FIFO 里面的值来源于采样寄存器 PWMx_PWMSAR,因此相当于 PWMx_PWMSAR 控制着占空比。至此,PWM 信号的频率和占 空比设置我们就知道该如何去做了。
PWM 开启以后会按照默认值运行,并产生 PWM 波形,而这个默认的 PWM 一般并不是我 们需要的波形。如果这个 PWM 波形控制着设备的话就会导致设备因为接收到错误的 PWM 信 号而运行错误,严重情况下可能会损坏设备,甚至人身安全。因此,在开启 PWM 之前最好设 置好 PWMx_PWMPR 和 PWMx_PWMSAR 这两个寄存器,也就是设置好 PWM 的频率和占空 比。
当我们向 PWMx_PWMSAR 寄存器写入采样值的时候,如果 FIFO 没满的话其值会被存储 到 FIFO 中。如果 FIFO 满的时候写入采样值就会导致寄存器 PWMx_PWMSR 的位 FWE(bit6)置 1,表示 FIFO 写错误,FIFO 里面的值也并不会改变。FIFO 可以在任何时候写入,但是只有在 PWM 使能的情况下读取。寄存器 PWMx_SR 的位 FIFOAV(bit2:0)记录着当前 FIFO 中有多少个 数据。从采样寄存器 PWMx_PWMSAR 读取一次数据,FIFO 里面的数据就会减一,每产生一个 周期的 PWM 信号,FIFO 里面的数据就会减一,相当于被用掉了。PWM 有个 FIFO 空中断,当 FIFO 为空的时候就会触发此中断,可以在此中断处理函数中向 FIFO 写入数据。
FWM(bit27:26):FIFO 水位线,用来设置 FIFO 空余位置为多少的时候表示 FIFO 为空。设 置为 0 的时候表示 FIFO 空余位置大于等于 1 的时候 FIFO 为空;设置为 1 的时候表示 FIFO 空 余位置大于等于 2 的时候 FIFO 为空;设置为 2 的时候表示 FIFO 空余位置大于等于 3 的时候 FIFO 为空;设置为 3 的时候表示 FIFO 空余位置大于等于 4 的时候 FIFO 为空。
STOPEN(bit25):此位用来设置停止模式下 PWM 是否工作,为 0 的话表示在停止模式下 PWM 继续工作,为 1 的话表示停止模式下关闭 PWM。
DOZEN(bit24):此位用来设置休眠模式下 PWM 是否工作,为 0 的话表示在休眠模式下 PWM 继续工作,为 1 的话表示休眠模式下关闭 PWM。
WAITEN(bit23):此位用来设置等待模式下 PWM 是否工作,为 0 的话表示在等待模式下 PWM 继续工作,为 1 的话表示等待模式下关闭 PWM。
DEGEN(bit22):此位用来设置调试模式下 PWM 是否工作,为 0 的话表示在调试模式下 PWM 继续工作,为 1 的话表示调试模式下关闭 PWM。
BCTR(bit21):字节交换控制位,用来控制 16 位的数据进入 FIFO 的字节顺序。为 0 的时 候不进行字节交换,为 1 的时候进行字节交换。
HCRT(bit20):半字交换控制位,用来决定从 32 位 IP 总线接口传输来的哪个半字数据写 入采样寄存器的低 16 位中。
POUTC(bit19:18):PWM 输出控制控制位,用来设置 PWM 输出模式,为 0 的时候表示 PWM 先输出高电平,当计数器值和采样值相等的话就输出低电平。为 1 的时候相反,当为 2 或 者 3 的时候 PWM 信号不输出。本章我们设置为 0,也就是一开始输出高电平,当计数器值和 采样值相等的话就改为低电平,这样采样值越大高电平时间就越长,占空比就越大。
CLKSRC(bit17:16):PWM 时钟源选择,为 0 的话关闭;为 1 的话选择 ipg_clk 为时钟源; 为 2 的话选择 ipg_clk_highfreq 为时钟源;为 3 的话选择 ipg_clk_32k 为时钟源。本章我们设置 为 1,也就是选择 ipg_clk 为 PWM 的时钟源,因此 PWM 时钟源频率为 66MHz。
PRESCALER(bit15:4):分频值,可设置为 0~4095,对应着 1~4096 分频。
SWR(bit3):软件复位,向此位写 1 就复位 PWM,此位是自清零的,当复位完成以后此位 会自动清零。
REPEAT(bit2:1):重复采样设置,此位用来设置 FIFO 中的每个数据能用几次。可设置 0~3, 分别表示 FIFO 中的每个数据能用 1~4 次。本章我们设置为 0,即 FIFO 中的每个数据只能用一 次。
EN(bit0):PWM 使能位,为 1 的时候使能 PWM,为 0 的时候关闭 PWM。
寄存器 PWM1_PWMIR 寄存器,这个是 PWM 的中断控制寄存器
CIE(bit2):比较中断使能位,为 1 的时候使能比较中断,为 0 的时候关闭比较中断。
RIE(bit1):翻转中断使能位,当计数器值等于采样值并回滚到 0X0000 的时候就会产生此 中断,为 1 的时候使能翻转中断,为 0 的时候关闭翻转中断。
FIE(bit0):FIFO 空中断,为 1 的时候使能,为 0 的时候关闭。
状态寄存器 PWM1_PWMSR
FWE(bit6):FIFO 写错误事件,为 1 的时候表示发生了 FIFO 写错误。 CMP(bit5):FIFO 比较事件发标志位,为 1 的时候表示发生 FIFO 比较事件。 ROV(bit4):翻转事件标志位,为 1 的话表示翻转事件发生。 FE(bit3):FIFO 空标志位,为 1 的时候表示 FIFO 位空。 FIFOAV(bit2:1):此位记录 FIFO 中的有效数据个数,有效值为 0~4,分别表示 FIFO 中有 0~4 个有效数据。
寄存器 PWM1_PWMPR 寄存器,这个是 PWM 周期寄存器
,寄存器 PWM1_PWMPR 只有低 16 位有效,当 PWM 计数器的值等 于 PERIOD+1 的时候就会从 0X0000 重新开始计数,开启另一个周期。PWM 的频率计算公式 如下:
PWMO(Hz) = PCLK(Hz) / (PERIOD + 2)
其中 PCLK 是最终进入 PWM 的时钟频率,假如 PCLK 的频率为 1MHz,现在我们要产生 一个频率为 1KHz 的 PWM 信号,那么就可以设置 PERIOD = 1000000 / 1000 – 2 = 998。
寄存器 PWM1_PWMSAR,这是采样寄存器,用于设置占空比的,
此寄存器也是只有低 16 位有效,为采样值。通过这个采样值即可调整占空比,当计数器的 值小于 SAMPLE 的时候输出高电平(或低电平)。当计数器值大于等于 SAMPLE,小于寄存器 PWM1_PWMPR 的 PERIO 的时候输出低电平(或高电平)。同样在上面的例子中,假如我们要设 置 PWM 信号的占空比为 50%,那么就可以将 SAMPLE 设置为(PERIOD + 2) / 2 = 1000 / 2=500。
使用 I.MX6U 的 PWM1,PWM1 的输出引脚为 GPIO1_IO8,配置步骤如下: 1、配置引脚 GPIO1_IO8 配置 GPIO1_IO08 的复用功能,将其复用为 PWM1_OUT 信号线。 2、初始化 PWM1 初始化 PWM1,配置所需的 PWM 信号的频率和默认占空比。 3、设置中断 因为 FIFO 中的采样值每个周期都会少一个,所以需要不断的向 FIFO 中写入采样值,防止 其为空。我们可以使能 FIFO 空中断,这样当 FIFO 为空的时候就会触发相应的中断,然后在中 断处理函数中向 FIFO 写入采样值。 4、使能 PWM1 配置好 PWM1 以后就可以开启了。
二十五、ADC实验
通过读取 GPIO 引脚的高低电平我们可以知道输入的是 1 还 0,但是我们并不能知道它实 际的电压是多少。ADC 的存在就是让你知道的更加清楚,ADC 可以让你知道某个 IO 的具体电 压值。有很多传感器都是模拟信号输出的,也就是输出电压值,我们需要测量到其具体的电压 值,然后在使用对应的公式进行计算,得到最终的数字值。
1、ADC简介
1.1、什么是 ADC
ADC,Analog to Digital Converter 的缩写,中文名称模数转换器。它可以将外部的模拟信号 转化成数字信号。对于 GPIO 口来说高于某个电压值,它读出来的只有高电平,低于就是低电 平。
ADC 有几个比较重要的参数:
测量范围:测量范围对于 ADC 来说就好比尺子的量程,ADC 测量范围决定了你外接的设 备其信号输出电压范围,不能超过 ADC 的测量范围。如果所使用的外部传感器输出的电压信 号范围和所使用的 ADC 测量范围不符合,那么就需要自行设计相关电压转换电路。
分辨率:就是尺子上的能量出来的最小测量刻度,例如我们常用的厘米尺它的最小刻度就 是 1 毫米,表示最小测量精度就是 1 毫米。假如 ADC 的测量范围为 0-5V,分辨率设置为 12 位,那么我们能测出来的最小电压就是 5V 除以 2 的 12 次方,也就是 5/4096=0.00122V。很明 显,分辨率越高,采集到的信号越精确,所以分辨率是衡量 ADC 的一个重要指标。
精度:是影响结果准确度的因素之一,比如在厘米尺上我们能测量出大概多少毫米的尺度 但是毫米后一点点我们却不能准确的量出。经过计算我们 ADC 在 12 位分辨率下的最小测量值 是 0.00122V 但是我们 ADC 的精度最高只能到 11 位也就是 0.00244V。也就是 ADC 测量出 0.00244V 的结果是要比 0.00122V 要可靠,也更准确。
采样时间:当 ADC 在某时刻采集外部电压信号的时候,此时外部的信号应该保持不变,但 实际上外部的信号是不停变化的。所以在 ADC 内部有一个保持电路,保持某一时刻的外部信 号,这样 ADC 就可以稳定采集了,保持这个信号的时间就是采样时间。
采样率:也就是在一秒的时间内采集多少次。很明显,采样率越高越好,当采样率不够的 时候可能会丢失部分信息,所以 ADC 采样率是衡量 ADC 性能的另一个重要指标。
总之,只要是需要模拟信号转为数字信号的场合,那么肯定要用到 ADC。很多数字传感器 内部会集成 ADC,传感器内部使用 ADC 来处理原始的模拟信号,最终给用户输出数字信号。
1.2 、I.MX6ULL ADC 简介
I.MX6ULL 提供了两个 12 位 ADC 通道和 10 个输入接口给我们使用。I.MX6ULL 的 ADC 外设特性如下: 1)、线性连续逼近算法,分辨率高达 12 位。 2)、多达 10 个通道可以选择。 3)、最高采样率 1MS/s。 4)、多达 8 个单端外部模拟输入。 5)、单次或连续转换(单次转换后自动返回空闲状态)。 6)、可以配置为 12/10/8 位。 7)、可配置的采样时间和转换速度/功率 8)、支持转换完成、硬件平均完成标志和中断。 9)、自我校准模式
ADC 有三种工作状态:禁止状态(Disabled)、闲置状态(Idle)、工作状态(Performing conversions)
禁止状态:ADC 模块被禁止工作。 闲置状态:当前转换已经完成,下次转换尚未准备时的状态,当异步时钟输出被关闭,ADC 进入该状态时,ADC 此时处于最低功耗状态。 工作状态:当 ADC 初始化完成后,并设置好输入通道后,将进入的状态。转换过程中也一 直保持在工作状态。
ADCx_CFG(x=1~2)寄存器,这 是 ADC1 的配置寄存器,此寄存器结构如图 C1.1.1.1 所示:
OVWREN (bit16):数据复写使能位,为 1 的时候使能复写功能,为 0 的时候关闭复写功 能。
AVGS(bit15:14):硬件平均次数,只有当 ADC1_GC 寄存器的 AVGE 位为 1 的时候才有效。
ADTRG(bit13):转换触发选择。为 0 的时候选择软件触发,为 1 的时候,不选择软件触发。
REFSEL(bit12:11):参考电压选择,为 00 时选择 VREFH/VREFL 这两个引脚上的电压为 参考电压,正点原子 ALPHA 开发板上 VREFH 为 3.3V,VREFL 为 0V。
ADHSC(bit10):高速转换使能位,当为 0 时为正常模式,为 1 时为高速模式。
ADSTS(bit9:8):设置 ADC 的采样周期,与 ADLSMP 位一起决定采样周期,如表 C1.1.1.2 所示:
ADIV(bit6:5):时钟分频选择,为 00 的时候不分频,为 01 的时候 2 分频,为 10 的时候 4 分频,为 11 的时候 8 分频。 ADLSMP(bit4):长采样周期使能位,当值为 0 时为短采样周期模式,为 1 时为长采样周期 模式。搭配 ADSTS 位一起控制 ADC 的采样周期,见表 C1.1.2。
MODE(bit3:2):选择转换精度,设置如表 C1.1.3:
ADICLK(bit1:0):输入时钟源选择,为 00 的时候选择 IPG Clock,为 01 的时候选择 IPG Clock/2,为 10 的时候无效,为 11 的时候选择呢 ADACK。本教程我们设置为 11,也就是选择 ADACK 为 ADC 的时钟源。
通用控制寄存器 ADCx_GC,
CAL(bit7):当该位写入 1 时,硬件校准功能将会启动,校准过程中该位会一直保持 1,校 准完成后会清 0,校准完成后需要检查一下 ADC_GS[CALF]位,确认校准结果。
ADCO(bit6):连续转换使能位,只有在开启了硬件平均功能时有效,为 0 时只能转换一次 或一组,当 ADCO 为 1 时可以连续转换或多组。
AVGE(bit5):硬件平均使能位。为 0 时关闭,为 1 时使能。
ACFE(bit4):比较功能使能位。为 0 时关闭,为 1 时使能。
ACFGT(bit3):配置比较方法,如果为 0 的话就比较转换结果是否小于 ADC_CV 寄存器 值,如果为 1 的话就比较装换结果是否大于或等于 ADC_CV 寄存器值。
ACREN(bit2):范围比较功能使能位。为 0 的话仅和 ADC_CV 里的 CV1 比较,为 1 的话 和 ADC_CV 里的 CV1、CV2 比较。
DMAEN(bit1):DMA 功能使能位,为 0 是关闭,为 1 是开启
ADACKEN(bit0):异步时钟输出使能位,为 0 是关闭,为 1 时开启。
通用状态寄存器 ADCx_GS,寄存器结构如图 C1.1.1.3 所示:
此寄存器对应的位含义如下: AWKST(bit2):异步唤醒中断状态,为 1 时表示发生了异步唤醒中断。为 0 时没有发生异 步中断。
CALF(bit1):校准失败标志位,为 0 的时候表示校准正常完成,为 1 的时候表示校准失败。 ADACT(bit0):转换活动标志,为 0 的时候表示转换没有进行,为 1 的时候表示正在进行
状态寄存器 ADCx_HS,此寄存器结构如图 C1.1.1.4 所示:
此寄存器只有一个位 COCO0,这是转换完成标志位,此位为只读位,当关闭比较功能和 硬件平均以后每次转换完整此位就会被置 1。使能硬件平均以后,只有在设置的转换次数达到 以后此位才置 1。
下控制寄存器 ADCx_HC0,此寄存器结构如图 C1.1.1.5 所示:
来看一下此寄存器对应的位:
AIEN(bit7):转换完成中断控制位,为 1 的时候打开转换完成中断,为 0 的时候关闭。
ADCH(bit4:0):转换通道选择,可以设置为 00000~01111 分别对应通道 0~15。11001 为内 部通道,用于 ADC 自测。
数据结果寄存器 ADCx_R0,顾名思义,此寄存器保存 ADC 数据结果,也就是 转换值,寄存器结构如图 C1.1.1.6 所示:
只有 bit11:0 这 12 位有效,此 12 位用来保存 ADC 转换结果。
1、初始化 ADC1_CH1 初始化 ADC1_CH1,配置 ADC 位数,时钟源,采样时间等。 2、校准 ADC ADC 在使用之前需要校准一次。 4、使能 ADC 配置好 ADC 以后就可以开启了。 5、读取 ADC 值 ADC 正常工作以后就可以读取 ADC 值。
二十六、U-Boot 使用实验
1、U-Boot 简介
Linux 系统要启动就必须需要一个 bootloader 程序,也就说芯片上电以后先运行一段 bootloader程序。这段bootloader程序会先初始化DDR等外设,然后将Linux内核从flash(NAND, NOR FLASH,SD,MMC 等)拷贝到 DDR 中,最后启动 Linux 内核。当然了,bootloader 的实 际工作要复杂的多,但是它最主要的工作就是启动 Linux 内核,bootloader 和 Linux 内核的关系 就跟 PC 上的 BIOS 和 Windows 的关系一样,bootloader 就相当于 BIOS。所以我们要先搞定 bootloader,很庆幸,有很多现成的 bootloader 软件可以使用,比如 U-Boot、vivi、RedBoot 等 等,其中以 U-Boot 使用最为广泛,为了方便书写,本书会将 U-Boot 写为 uboot。
uboot 的全称是 Universal Boot Loader,uboot 是一个遵循 GPL 协议的开源软件,uboot 是一 个裸机代码,可以看作是一个裸机综合例程。现在的 uboot 已经支持液晶屏、网络、USB 等高 级功能。
半导体厂 商会自己维护一个版本的 uboot,这个版本的 uboot 相当于是他们定制的。既然是定制的,那么 肯定对自家的芯片支持会很全,虽然 uboot 官网的源码中一般也会支持他们的芯片,但是绝对 是没有半导体厂商自己维护的 uboot 全面。
U-Boot 2016.03 (Oct 16 2023 - 19:59:57 +0800)
# 第 1 行是 uboot 版本号和编译时间,可以看出,当前的 uboot 版本号是 2016.03,编译时间
CPU: Freescale i.MX6ULL rev1.1 792 MHz (running at 396 MHz)
CPU: Industrial temperature grade (-40C to 105C) at 43C
#第 3 和第 4 行是 CPU 信息,可以看出当前使用的 CPU 是飞思卡尔的 I.MX6ULL(I.MX 以前属于飞思卡尔,然而飞思卡尔被 NXP 收购了),频率为 792MHz,但是此时运行在 396MHz。这颗芯片是工业级的,结温为-40°C~105°C。
Reset cause: POR
第 5 行是复位原因,当前的复位原因是 POR。I.MX6ULL 芯片上有个 POR_B 引脚,将这个引脚拉低即可复位 I.MX6ULL
Board: I.MX6U ALPHA|MINI
第 6 行是板子名字,当前的板子名字为“I.MX6U ALPHA|MINI”。
I2C: ready
DRAM: 512 MiB
第 8 行提示当前板子的 DRAM(内存)为 512MB,如果是 NAND 版本的话内存为 256MB。
MMC: FSL_SDHC: 0, FSL_SDHC: 1
第 9 行提示当前有两个 MMC/SD 卡控制器:FSL_SDHC(0)和 FSL_SDHC(1)。I.MX6ULL支持两个 MMC/SD,正点原子的 I.MX6ULL EMMC 核心板上 FSL_SDHC(0)接的 SD(TF)卡,0FSL_SDHC(1)接的 EMMC。
*** Warning - bad CRC, using default environment
In: serial
Out: serial
Err: serial
第 12~14 是标准输入、标准输出和标准错误所使用的终端,这里都使用串口(serial)作为终端。
switch to partitions #0, OK
mmc0 is current device
第 15 和 16 行是切换到 emmc 的第 0 个分区上,因为当前的 uboot 是 emmc 版本的,也就是从 emmc 启动的。我们只是为了方便将其烧写到了 SD 卡上,但是它的“内心”还是 EMMC的。所以 uboot 启动以后会将 emmc 作为默认存储器,当然了,你也可以将 SD 卡作为 uboot 的存储器,这个我们后面会讲解怎么做。
Net: FEC1
Error: FEC1 address not set.
2、U-Boot 命令使用
进入 uboot 的命令行模式以后输入“help”或者“?”,然后按下回车即可查看当前 uboot 所 支持的命令
2.1、信息查询命令
常用的和信息查询有关的命令有 3 个:bdinfo、printenv 和 version。先来看一下 bdinfo 命 令,此命令用于查看板子信息,直接输入“bdinfo”即可,
命令“printenv”用于输出环境变量信息,uboot 也支持 TAB 键自动补全功能
2.2、环境变量操作命令
2.2.1、修改环境变量
环境变量的操作涉及到两个命令:setenv 和 saveenv,命令 setenv 用于设置或者修改环境变 量的值。命令 saveenv 用于保存修改后的环境变量,一般环境变量是存放在外部 flash 中的, uboot 启动的时候会将环境变量从 flash 读取到 DRAM 中。所以使用命令 setenv 修改的是 DRAM 中的环境变量值,修改以后要使用 saveenv 命令将修改后的环境变量保存到 flash 中,否则的话 uboot 下一次重启会继续使用以前的环境变量值。
有时候我们修改的环境变量值可能会有空格,比如 bootcmd、bootargs 等,这个时候环境变 量值就得用单引号括起来,比如下面修改环境变量 bootargs 的值:
2.2.2、新建环境变量
命令 setenv 也可以用于新建命令,用法和修改环境变量一样,比如我们新建一个环境变量 author,author 的值为我的名字拼音:zuozhongkai,那么就可以使用如下命令:
2.2.3、删除环境变量
删除环境变量也是使用命令 setenv, 要删除一个环境变量只要给这个环境变量赋空值即可,比如我们删除掉上面新建的 author 这个 环境变量,命令如下:
2.3、内存操作命令
内存操作命令就是用于直接对 DRAM 进行读写操作的,常用的内存操作命令有 md、nm、 mm、mw、cp 和 cmp。我们依次来看一下这些命令都是做什么的。
2.3.1、md命令
md 命令用于显示内存值,格式如下:
命令中的[.b .w .l]对应 byte、word 和 long,也就是分别以 1 个字节、2 个字节、4 个字节 来显示内存值。address 就是要查看的内存起始地址,[# of objects]表示要查看的数据长度,这个数据长度单位不是字节,而是跟你所选择的显示格式有关。比如你设置要查看的内存长度为 20(十六进制为 0x14),如果显示格式为.b 的话那就表示 20 个字节;如果显示格式为.w 的话就 表示 20 个 word,也就是 20x2=40 个字节;如果显示格式为.l 的话就表示 20 个 long,也就是 20x4=80 个字节。另外要注意:
uboot 命令中的数字都是十六进制的!不是十进制的!
上面说了,uboot 命令里面的数字都是十六进制的,所以可以不用写“0x”前缀,十进制 的 20 其十六进制为 0x14,所以命令 md 后面的个数应该是 14,如果写成 20 的话就表示查看 32(十六进制为 0x20)个字节的数据。分析下面三个命令的区别:
md.b 80000000 10
md.w 80000000 10
md.l 80000000 10
上面这三个命令都是查看以 0X80000000 为起始地址的内存数据,第一个命令以.b 格式显 示,长度为 0x10,也就是 16 个字节;第二个命令以.w 格式显示,长度为 0x10,也就是 162=32 个字节;最后一个命令以.l 格式显示,长度也是 0x10,也就是 164=64 个字节。
2.3.2、nm命令
nm 命令用于修改指定地址的内存值,命令格式如下:
nm [.b, .w, .l] address
nm 命令同样可以以.b、.w 和.l 来指定操作格式,比如现在以.l 格式修改 0x80000000 地址 的数据为 0x12345678。输入命令:
nm.l 80000000
在图 30.4.3.2 中,80000000 表示现在要修改的内存地址,0500e031 表示地址 0x80000000 现 在的数据,?后面就可以输入要修改后的数据 0x12345678,输入完成以后按下回车,然后再输 入‘q’即可退出,如图 30.4.3.3 所示:
2.3.3、mm命令
mm 命令也是修改指定地址内存值的,使用 mm 修改内存值的时候地址会自增,而使用命 令 nm 的话地址不会自增。比如以.l 格式修改从地址 0x80000000 开始的连续 3 个内存块(3*4=12 个字节)的数据为 0X05050505,操作如图 30.4.3.5 所示:
2.3.4 、mw 命令
命令 mw 用于使用一个指定的数据填充一段内存,命令格式如下:
mw [.b, .w, .l] address value [count]
mw 命令同样可以以.b、.w 和.l 来指定操作格式,address 表示要填充的内存起始地址,value 为要填充的数据,count 是填充的长度。比如使用.l 格式将以 0X80000000 为起始地址的 0x10 个 内存块(0x10 * 4=64 字节)填充为 0X0A0A0A0A,命令如下:
mw.l 80000000 0A0A0A0A 10
2.3.5 cp命令
cp 是数据拷贝命令,用于将 DRAM 中的数据从一段内存拷贝到另一段内存中,或者把 Nor Flash 中的数据拷贝到 DRAM 中。命令格式如下:cp [.b, .w, .l] source target count
cp 命令同样可以以.b、.w 和.l 来指定操作格式,source 为源地址,target 为目的地址,count 为拷贝的长度。我们使用.l 格式将 0x80000000 处的地址拷贝到 0X80000100 处,长度为 0x10 个 内存块(0x10 * 4=64 个字节),命令如下所示:
cp.l 80000000 80000100 10
2.3.6、cmp 命令
cmp 是比较命令,用于比较两段内存的数据是否相等,命令格式如下:
cmp [.b, .w, .l] addr1 addr2 count
cmp 命令同样可以以.b、.w 和.l 来指定操作格式,addr1 为第一段内存首地址,addr2 为第 二段内存首地址,count 为要比较的长度。我们使用.l 格式来比较 0x80000000 和 0X80000100 这 两个地址数据是否相等,比较长度为 0x10 个内存块(16 * 4=64 个字节),命令如下所示:cmp.l 80000000 80000100 10
2.4、网络操作命令
=> setenv ipaddr 192.168.66.16
=> setenv ethaddr b8:ae:1d:01:00:00
=> setenv gatewayip 192.168.66.1
=> setenv netmask 255.255.255.0
=> setenv serverip 192.168.66.6
2.4.1 nfs命令
nfs(Network File System)网络文件系统,通过 nfs 可以在计算机之间通过网络来分享资源, 比如我们将 linux 镜像和设备树文件放到 Ubuntu 中,然后在 uboot 中使用 nfs 命令将 Ubuntu 中 的 linux 镜像和设备树下载到开发板的 DRAM 中。这样做的目的是为了方便调试 linux 镜像和 设备树,也就是网络调试,通过网络调试是 Linux 开发中最常用的调试方法。原因是嵌入式 linux 开发不像单片机开发,可以直接通过 JLINK 或 STLink 等仿真器将代码直接烧写到单片机内部 的 flash 中,嵌入式 Linux 通常是烧写到 EMMC、NAND Flash、SPI Flash 等外置 flash 中,但是 嵌入式 Linux 开发也没有 MDK,IAR 这样的 IDE,更没有烧写算法,因此不可能通过点击一个 “download”按钮就将固件烧写到外部 flash 中。虽然半导体厂商一般都会提供一个烧写固件的 软件,但是这个软件使用起来比较复杂,这个烧写软件一般用于量产的。其远没有 MDK、IAR 的一键下载方便,在 Linux 内核调试阶段,如果用这个烧写软件的话将会非常浪费时间,而这 个时候网络调试的优势就显现出来了,可以通过网络将编译好的 linux 镜像和设备树文件下载 到 DRAM 中,然后就可以直接运行。
uboot 中的 nfs 命令格式如下所 示: nfs [loadAddress] [[hostIPaddr:]bootfilename]
loadAddress 是要保存的 DRAM 地址,[[hostIPaddr:]bootfilename]是要下载的文件地址。这 里我们将正点原子官方编译出来的 Linux 镜像文件 zImage 下载到开发板 DRAM 的 0x80800000 这个地址处。
2.5、EMMC 和 SD 卡操作命令
uboot 支持 EMMC 和 SD 卡,因此也要提供 EMMC 和 SD 卡的操作命令。一般认为 EMMC 和 SD 卡是同一个东西,所以没有特殊说明,本教程统一使用 MMC 来代指 EMMC 和 SD 卡。 uboot 中常用于操作 MMC 设备的命令为“mmc”。
2.5.1、mmc info 命令
mmc info 命令用于输出当前选中的 mmc info 设备的信息,输入命令“mmc info”即可。
2.5.2、mmc rescan 命令
mmc rescan 命令用于扫描当前开发板上所有的 MMC 设备,包括 EMMC 和 SD 卡,输入 “mmc rescan”即可。
2.5.3、mmc list 命令
mmc list 命令用于来查看当前开发板一共有几个 MMC 设备,输入“mmc list”。
2.5.4、mmc dev 命令
mmc dev 命令用于切换当前 MMC 设备,命令格式如下:
mmc dev [dev] [part]
[dev]用来设置要切换的 MMC 设备号,[part]是分区号。如果不写分区号的话默认为分区 0。
2.5.5、mmc part 命令
有时候 SD 卡或者 EMMC 会有多个分区,可以使用命令“mmc part”来查看其分区,比如 查看 EMMC 的分区情况,输入如下命令:
mmc dev 1 //切换到 EMMC
mmc part //查看 EMMC 分区
如果 EMMC 里 面烧写了 Linux 系统的话,EMMC 是有 3 个分区的,第 0 个分区存放 uboot,第 1 个分区存放 Linux 镜像文件和设备树,第 2 个分区存放根文件系统。但是在图 30.4.5.6 中只有两个分区,那 是因为第 0 个分区没有格式化,所以识别不出来,实际上第 0 个分区是存在的。一个新的 SD 卡默认只有一个分区,那就是分区 0,所以前面讲解的 uboot 烧写到 SD 卡,其实就是将 u-boot.bin烧写到了 SD 卡的分区 0 里面。后面学习 Linux 内核移植的时候再讲解怎么在 SD 卡中创建并 格式化第二个分区,并将 Linux 镜像文件和设备树文件存放到第二个分区中。
如果要将 EMMC 的分区 2 设置为当前 MMC 设备,可以使用如下命令: mmc dev 1 2
2.5.6、mmc read 命令
mmc read 命令用于读取 mmc 设备的数据,命令格式如下: mmc read addr blk# cnt
addr 是数据读取到 DRAM 中的地址,blk 是要读取的块起始地址(十六进制),一个块是 512 字节,这里的块和扇区是一个意思,在 MMC 设备中我们通常说扇区,cnt 是要读取的块数量(十 六进制)。比如从 EMMC 的第 1536(0x600)个块开始,读取 16(0x10)个块的数据到 DRAM 的 0X80800000 地址处,命令如下:
mmc dev 1 0 //切换到 MMC 分区 0
mmc read 80800000 600 10 //读取数据
2.5.7、mmc write 命令
要将数据写到 MMC 设备里面,可以使用命令“mmc write”,格式如下: mmc write addr blk# cnt
addr 是要写入 MMC 中的数据在 DRAM 中的起始地址,blk 是要写入 MMC 的块起始地址 (十六进制),cnt 是要写入的块大小,一个块为 512 字节。我们可以使用命令“mmc write”来升 级 uboot,也就是在 uboot 中更新 uboot。这里要用到 nfs 或者 tftp 命令,通过 nfs 或者 tftp 命令 将新的 u-boot.bin 下载到开发板的 DRAM 中,然后再使用命令“mmc write”将其写入到 MMC 设备中。我们就来更新一下 SD 中的 uboot,先查看一下 SD 卡中的 uboot 版本号,注意编译时 间,输入命令:
2.5.8、mmc erase 命令
如果要擦除 MMC 设备的指定块就是用命令“mmc erase”,命令格式如下: mmc erase blk# cnt
2.6、FAT 格式文件系统操作命令
有时候需要在 uboot 中对 SD 卡或者 EMMC 中存储的文件进行操作,这时候就要用到文件 操作命令,跟文件操作相关的命令有:fatinfo、fatls、fstype、fatload 和 fatwrite,但是这些文件 操作命令只支持 FAT 格式的文件系统!!
2.6.1、fatinfo 命令
fatinfo 命令用于查询指定 MMC 设备分区的文件系统信息,格式如下:
fatinfo [] interface 表示接口,
比如 mmc,dev 是查询的设备号,part 是要查询的分区。比如我们要查 询 EMMC 分区 1 的文件系统信息,命令如下: fatinfo mmc 1:1
2.6.2、fatls 命令
fatls 命令用于查询 FAT 格式设备的目录和文件信息,命令格式如下:
fatls [] [directory] interface 是要查询的接口,比如 mmc,dev 是要查询的设备号,part 是要查询的分区,directory 是要查询的目录。比如查询 EMMC 分区 1 中的所有的目录和文件,输入命令:fatls mmc 1:1
2.6.3、fstype 命令
fstype 用于查看 MMC 设备某个分区的文件系统格式,命令格式如下:
正点原子 EMMC 核心板上的 EMMC 默认有 3 个分区,我们来查看一下这三个分区的文件 系统格式,输入命令:
从上图可以看出,分区 0 格式未知,因为分区 0 存放的 uboot,并且分区 0 没有格式化,所 以文件系统格式未知。分区 1 的格式为 fat,分区 1 用于存放 linux 镜像和设备树。分区 2 的格 式为 ext4,用于存放 Linux 的根文件系统(rootfs)。
2.6.4、fatload 命令
fatload 命令用于将指定的文件读取到 DRAM 中,命令格式如下:
interface 为接口,比如 mmc,dev 是设备号,part 是分区,addr 是保存在 DRAM 中的起始 地址,filename 是要读取的文件名字。bytes 表示读取多少字节的数据,如果 bytes 为 0 或者省 略的话表示读取整个文件。pos 是要读的文件相对于文件首地址的偏移,如果为 0 或者省略的 话表示从文件首地址开始读取。我们将 EMMC 分区 1 中的 zImage 文件读取到 DRAM 中的 0X80800000 地址处,命令如下:
fatload mmc 1:1 80800000 zImage
2.6.5、fatwrite 命令
interface 为接口,比如 mmc,dev 是设备号,part 是分区,addr 是要写入的数据在 DRAM 中的起始地址,filename 是写入的数据文件名字,bytes 表示要写入多少字节的数据。我们可以 通过 fatwrite 命令在 uboot 中更新 linux 镜像文件和设备树。
2.7 、EXT 格式文件系统操作命令
uboot 有 ext2 和 ext4 这两种格式的文件系统的操作命令,常用的就四个命令,分别为: ext2load、ext2ls、ext4load、ext4ls 和 ext4write。这些命令的含义和使用与 fatload、fatls 和 fatwrite 一样,只是 ext2 和 ext4 都是针对 ext 文件系统的。比如 ext4ls 命令,EMMC 的分区 2 就是 ext4 格式的,使用 ext4ls 就可以查询 EMMC 的分区 2 中的文件和目录,输入命令:ext4ls mmc 1:2
2.8、BOOT 操作命令
uboot 的本质工作是引导 Linux,所以 uboot 肯定有相关的 boot(引导)命令来启动 Linux。常
用的跟 boot 有关的命令有:bootz、bootm 和 boot。
2.8.1、bootz 命令
要启动 Linux,需要先将 Linux 镜像文件拷贝到 DRAM 中,如果使用到设备树的话也需要 将设备树拷贝到 DRAM 中。可以从 EMMC 或者 NAND 等存储设备中将 Linux 镜像和设备树文 件拷贝到 DRAM,也可以通过 nfs 或者 tftp 将 Linux 镜像文件和设备树文件下载到 DRAM 中。 不管用那种方法,只要能将 Linux 镜像和设备树文件存到 DRAM 中就行,然后使用 bootz 命令 来启动,bootz 命令用于启动 zImage 镜像文件,bootz 命令格式如下:
bootz [addr [initrd[:size]] [fdt]]
命令 bootz 有三个参数,addr 是 Linux 镜像文件在 DRAM 中的位置,initrd 是 initrd 文件在 DRAM 中的地址,如果不使用 initrd 的话使用‘-’代替即可,fdt 就是设备树文件在 DRAM 中 的地址。
如果我们要从 EMMC 中启 动 Linux 系统的话只需要使用命令 fatload 将 zImage 和 imx6ull-14x14-emmc-7-1024x600-c.dtb 从 EMMC 的分区 1 中拷贝到 DRAM 中,然后使用命令 bootz 启动即可。
2.8.2、bootm 命令
bootm 和 bootz 功能类似,但是 bootm 用于启动 uImage 镜像文件。如果不使用设备树的话 启动 Linux 内核的命令如下: bootm addr addr 是 uImage 镜像在 DRAM 中的首地址。 如果要使用设备树,那么 bootm 命令和 bootz 一样,命令格式如下: bootm [addr [initrd[:size]] [fdt]] 其中 addr 是 uImage 在 DRAM 中的首地址,initrd 是 initrd 的地址,fdt 是设备树(.dtb)文件 在 DRAM 中的首地址,如果 initrd 为空的话,同样是用“-”来替代。
2.8.3、boot 命令
boot 命令也是用来启动 Linux 系统的,只是 boot 会读取环境变量 bootcmd 来启动 Linux 系 统,bootcmd 是一个很重要的环境变量!其名字分为“boot”和“cmd”,也就是“引导”和“命 令”,说明这个环境变量保存着引导命令,其实就是启动的命令集合,具体的引导命令内容是可 以修改的。比如我们要想使用 tftp 命令从网络启动 Linux 那么就可以设置 bootcmd 为“tftp 80800000 zImage; tftp 83000000 imx6ull-14x14-emmc-7-1024x600-c.dtb; bootz 80800000 - 83000000”,然后使用 saveenv 将 bootcmd 保存起来。然后直接输入 boot 命令即可从网络启动 Linux 系统,命令如下:
二十七、字符设备驱动开发
1、字符设备驱动简介
字符设备是 Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节 流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、IIC、SPI, LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。
Linux 应用程序对驱动程序的调用如图 40.1.1 所示:
在 Linux 中一切皆为文件,驱动加载成功以后会在“/dev”目录下生成一个相应的文件,应 用程序通过对这个名为“/dev/xxx”(xxx 是具体的驱动文件名字)的文件进行相应的操作即可实 现对硬件的操作。比如现在有个叫做/dev/led 的驱动文件,此文件是 led 灯的驱动文件。应用程 序使用 open 函数来打开文件/dev/led,使用完成以后使用 close 函数关闭/dev/led 这个文件。open 和 close 就是打开和关闭 led 驱动的函数,如果要点亮或关闭 led,那么就使用 write 函数来操 作,也就是向此驱动写入数据,这个数据就是要关闭还是要打开 led 的控制参数。如果要获取 led 灯的状态,就用 read 函数从驱动中读取相应的状态。
应用程序运行在用户空间,而 Linux 驱动属于内核的一部分,因此驱动运行于内核空间。 当我们在用户空间想要实现对内核的操作,比如使用 open 函数打开/dev/led 这个驱动,因为用 户空间不能直接对内核进行操作,因此必须使用一个叫做“系统调用”的方法来实现从用户空 间“陷入”到内核空间,这样才能实现对底层驱动的操作。open、close、write 和 read 等这些函 数是由 C 库提供的,在 Linux 系统中,系统调用作为 C 库的一部分。当我们调用 open 函数的 时候流程如图 40.1.2 所示:
2、字符设备驱动开发步骤
学习裸机或者 STM32 的时候关于驱动的开发就是初始化相应的外设寄存器,在 Linux 驱 动开发中肯定也是要初始化相应的外设寄存器,这个是毫无疑问的。只是在 Linux 驱动开发中 我们需要按照其规定的框架来编写驱动,所以说学 Linux 驱动开发重点是学习其驱动框架。
2.1、驱动模块的加载和卸载
Linux 驱动有两种运行方式,第一种就是将驱动编译进 Linux 内核中,这样当 Linux 内核启 动的时候就会自动运行驱动程序。第二种就是将驱动编译成模块(Linux 下模块扩展名为.ko),在 Linux 内核启动以后使用“insmod”命令加载驱动模块。在调试驱动的时候一般都选择将其编译 为模块,这样我们修改驱动以后只需要编译一下驱动代码即可,不需要编译整个 Linux 代码。 而且在调试的时候只需要加载或者卸载驱动模块即可,不需要重启整个系统。总之,将驱动编 译为模块最大的好处就是方便开发,当驱动开发完成,确定没有问题以后就可以将驱动编译进 Linux 内核中,当然也可以不编译进 Linux 内核中,具体看自己的需求。
模块有加载和卸载两种操作,我们在编写驱动的时候需要注册这两种操作函数,模块的加载和 卸载注册函数如下: module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数
module_init 函数用来向 Linux 内核注册一个模块加载函数,参数 xxx_init 就是需要注册的 具体函数,当使用“insmod”命令加载驱动的时候,xxx_init 这个函数就会被调用。module_exit() 函数用来向 Linux 内核注册一个模块卸载函数,参数 xxx_exit 就是需要注册的具体函数,当使 用“rmmod”命令卸载具体驱动的时候 xxx_exit 函数就会被调用。字符设备驱动模块加载和卸 载模板如下所示:
第 2 行,定义了个名为 xxx_init 的驱动入口函数,并且使用了“__init”来修饰。 __
__第 9 行,定义了个名为 xxx_exit 的驱动出口函数,并且使用了“__exit”来修饰。
第 15 行,调用函数 module_init 来声明 xxx_init 为驱动入口函数,当加载驱动的时候 xxx_init 函数就会被调用。
第16行,调用函数module_exit来声明xxx_exit为驱动出口函数,当卸载驱动的时候xxx_exit 函数就会被调用。
驱动编译完成以后扩展名为.ko,有两种命令可以加载驱动模块:insmod和 modprobe,insmod 是最简单的模块加载命令,此命令用于加载指定的.ko 模块。
insmod 命令不能解决模块的依赖关系,比如 drv.ko 依赖 first.ko 这个模块,就必须先使用 insmod 命令加载 first.ko 这个模块,然后再加载 drv.ko 这个模块。但是 modprobe 就不会存在这 个问题,modprobe 会分析模块的依赖关系,然后会将所有的依赖模块都加载到内核中,因此 modprobe 命令相比 insmod 要智能一些。modprobe 命令主要智能在提供了模块的依赖性分析、 错误检查、错误报告等功能,推荐使用 modprobe 命令来加载驱动。modprobe 命令默认会去 /lib/modules/目录中查找模块,比如本书使用的 Linux kernel 的版本号为 4.1.15, 因此 modprobe 命令默认会到/lib/modules/4.1.15 这个目录中查找相应的驱动模块,一般自己制 作的根文件系统中是不会有这个目录的,所以需要自己手动创建。
驱动模块的卸载使用命令“rmmod”即可。也可以使用“modprobe -r”命令卸载驱动使用 modprobe 命令可以卸载掉驱动模块所依赖的其他模块,前提是这些依赖模块已经没 有被其他模块所使用,否则就不能使用 modprobe 来卸载驱动模块。所以对于模块的卸载,还是 推荐使用 rmmod 命令。
2.2、字符设备注册与注销
对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模 块的时候也需要注销掉字符设备。字符设备的注册和注销函数原型如下所示:
register_chrdev 函数用于注册字符设备,此函数一共有三个参数,这三个参数的含义如下: major:主设备号,Linux 下每个设备都有一个设备号,设备号分为主设备号和次设备号两 部分,关于设备号后面会详细讲解。 name:设备名字,指向一串字符串。 fops:结构体 file_operations 类型指针,指向设备的操作函数集合变量。
unregister_chrdev 函数用户注销字符设备,此函数有两个参数,这两个参数含义如下: major:要注销的设备对应的主设备号。 name:要注销的设备对应的设备名。
一般字符设备的注册在驱动模块的入口函数 xxx_init 中进行,字符设备的注销在驱动模块 的出口函数 xxx_exit 中进行。
选择没有被使用的主设备 号,输入命令“cat /proc/devices”可以查看当前已经被使用掉的设备号
root@ATK-IMX6U:~# cat /proc/devices
Character devices:
1 mem
4 /dev/vc/0
4 tty
5 /dev/tty
5 /dev/console
5 /dev/ptmx
7 vcs
10 misc
13 input
29 fb
81 video4linux
89 i2c
90 mtd
108 ppp
116 alsa
128 ptm
136 pts
153 spi
180 usb
189 usb_device
207 ttymxc
216 rfcomm
226 drm
248 icm20608
249 ttyLP
250 iio
251 watchdog
252 ptp
253 pps
254 rtc
Block devices:
1 ramdisk
259 blkext
7 loop
8 sd
31 mtdblock
65 sd
66 sd
67 sd
68 sd
69 sd
70 sd
71 sd
128 sd
129 sd
130 sd
131 sd
132 sd
133 sd
134 sd
135 sd
179 mmc
2.3、实现设备的具体操作函数
file_operations 结构体就是设备的具体操作函数。
2.3.1、能够对 chrtest 进行打开和关闭操作
设备打开和关闭是最基本的要求,几乎所有的设备都得提供打开和关闭的功能。因此我们 需要实现 file_operations 中的 open 和 release 这两个函数。
2.3.2、对 chrtest 进行读写操作
假设 chrtest 这个设备控制着一段缓冲区(内存),应用程序需要通过 read 和 write 这两个函 数对 chrtest 的缓冲区进行读写操作。所以需要实现 file_operations 中的 read 和 write 这两个函 数。
2.3.3、添加 LICENSE 和作者信息
最后我们需要在驱动中加入 LICENSE 信息和作者信息,其中 LICENSE 是必须添加的,否 则的话编译的时候会报错,作者信息可以添加也可以不添加。LICENSE 和作者信息的添加使用 如下两个函数:
MODULE_LICENSE() //添加模块 LICENSE 信息 LICENSE 采用 GPL 协议。
MODULE_AUTHOR() //添加模块作者信息
3、 Linux 设备号
3.1、设备号的组成
为了方便管理,Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分 组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。Linux 提供了 一个名为 dev_t 的数据类型表示设备号。dev_t 定义在文件 include/linux/types.h 里面。
dev_t 是__u32 类型的,而__u32 定义在文件 include/uapi/asm-generic/int-ll64.h 里。dev_t 其实就是 unsigned int 类型,是一个 32 位的数据类型。这 32 位的数据构成了主设备号和次设备号两部分,其中高 12 位为主设备号,低 20 位为次设备号。因此 Linux 系统中主设备号范围为 0~4095,所以大家在选择主设备号的时候一定不要超过这个范围。在文 件 include/linux/kdev_t.h 中提供了几个关于设备号的操作函数(本质是宏),
第 6 行,宏 MINORBITS 表示次设备号位数,一共是 20 位。
第 7 行,宏 MINORMASK 表示次设备号掩码。
第 9 行,宏 MAJOR 用于从 dev_t 中获取主设备号,将 dev_t 右移 20 位即可。
第 10 行,宏 MINOR 用于从 dev_t 中获取次设备号,取 dev_t 的低 20 位的值即可。
第 11 行,宏 MKDEV 用于将给定的主设备号和次设备号的值组合成 dev_t 类型的设备号。
3.2、设备号的分配
3.2.1、静态分配设备号
3.2.2、动态分配设备号
静态分配设备号需要我们检查当前系统中所有被使用了的设备号,然后挑选一个没有使用 的。而且静态分配设备号很容易带来冲突问题,Linux 社区推荐使用动态分配设备号,在注册字 符设备之前先申请一个设备号,系统会自动给你一个没有被使用的设备号,这样就避免了冲突。 卸载驱动的时候释放掉这个设备号即可,设备号的申请函数如下:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
函数 alloc_chrdev_region 用于申请设备号,此函数有 4 个参数: dev:保存申请到的设备号。 baseminor:次设备号起始地址,alloc_chrdev_region 可以申请一段连续的多个设备号,这 些设备号的主设备号一样,但是次设备号不同,次设备号以 baseminor 为起始地址地址开始递 增。一般 baseminor 为 0,也就是说次设备号从 0 开始。 count:要申请的设备号数量。 name:设备名字。
注销字符设备之后要释放掉设备号,设备号释放函数如下:
void unregister_chrdev_region(dev_t from, unsigned count)
此函数有两个参数: from:要释放的设备号。 count:表示从 from 开始,要释放的设备号数量。
printk 相当于 printf 的孪生兄妹,printf 运行在用户态,printk 运行在内核态。在内核中想要向控制台输出或显示一些内容,必须使用 printk 这个函数。不同之处在于,printk 可以根据日志级别对消息进行分类,一共有 8 个消息级 别,这 8 个消息级别定义在文件 include/linux/kern_levels.h 里面,定义如下:
上述代码就是设置“gsmi: Log Shutdown Reason\n”这行消息的级别为 KERN_EMERG。在 具体的消息前面加上 KERN_EMERG 就可以将这条消息的级别设置为 KERN_EMERG。
#define CONSOLE_LOGLEVEL_DEFAULT 7
CONSOLE_LOGLEVEL_DEFAULT 控制着哪些级别的消息可以显示在控制台上,此宏默认 为 7,意味着只有优先级高于 7 的消息才能显示在控制台上。 这个就是 printk 和 printf 的最大区别,可以通过消息级别来决定哪些消息可以显示在控制 台上。默认消息级别为 4,4 的级别比 7 高,所示直接使用 printk 输出的信息是可以显示在控制 台上的。
static inline long copy_to_user(void __user *to, const void *from, unsigned long n) 参数 to 表示目的,参数 from 表示源,参数 n 表示要复制的数据长度。如果复制成功,返 回值为 0,如果复制失败则返回负数。
atoi 函数将字符串格式的数字转换为真实的数字。
二十八、嵌入式 Linux LED 驱动开发实验
1、Linux 下 LED 灯驱动原理
Linux 下的任何外设驱动,最终都是要配置相应的硬件寄存器。所以本章的 LED 灯驱动最 终也是对 I.MX6ULL 的 IO 口进行配置,与裸机实验不同的是,在 Linux 下编写驱动要符合 Linux 的驱动框架。
1.1、地址映射
MMU 全称叫做 Memory Manage Unit,也就是内存管理单元。在老版本的 Linux 中要求处理器必须有 MMU,但是现在 Linux 内核已经支持无 MMU 的处理器了。MMU 主要完成的功能如下:
①、完成虚拟空间到物理空间的映射。
②、内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性
下第①点,也就是虚拟空间到物理空间的映射,也叫做地址映射。首先了 解两个地址概念:虚拟地址(VA,Virtual Address)、物理地址(PA,PhyscicalAddress)。对于 32 位 的处理器来说,虚拟地址范围是 2^32=4GB,我们的开发板上有 512MB 的 DDR3,这 512MB 的 内存就是物理内存,经过 MMU 可以将其映射到整个 4GB 的虚拟空间,如图 41.1.1 所示:
物理内存只有 512MB,虚拟内存有 4GB,那么肯定存在多个虚拟地址映射到同一个物理地 址上去,虚拟地址范围比物理地址范围大的问题处理器自会处理,这里我们不要去深究。
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.1.1、ioremap 函数
ioremap 函 数 用 于 获 取 指 定 物 理 地 址 空 间 对 应 的 虚 拟 地 址 空 间 , 定 义 在 arch/arm/include/asm/io.h 文件中,定义如下:
ioremap 是个宏,有两个参数:cookie 和 size,真正起作用的是函数__arm_ioremap,此函 数有三个参数和一个返回值,这些参数和返回值的含义如下: phys_addr:要映射给的物理起始地址。 size:要映射的内存空间大小。 mtype:ioremap 的类型,可以选择 MT_DEVICE、MT_DEVICE_NONSHARED、 MT_DEVICE_CACHED 和 MT_DEVICE_WC,ioremap 函数选择 MT_DEVICE。 返回值:__iomem 类型的指针,指向映射后的虚拟空间首地址。
假如我们要获取 I.MX6ULL 的 IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 寄存器对应 的虚拟地址,使用如下代码即可:
宏 SW_MUX_GPIO1_IO03_BASE 是寄存器物理地址,SW_MUX_GPIO1_IO03 是映射后 的虚拟地址。对于 I.MX6ULL 来说一个寄存器是 4 字节(32 位)的,因此映射的内存长度为 4。 映射完成以后直接对 SW_MUX_GPIO1_IO03 进行读写操作即可。
1.1.2、iounmap 函数
卸载驱动的时候需要使用 iounmap 函数释放掉 ioremap 函数所做的映射,iounmap 函数原 型如下:
iounmap 只有一个参数 addr,此参数就是要取消映射的虚拟地址空间首地址。假如我们现 在要取消掉 IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 寄存器的地址映射,使用如下代码 即可:
iounmap(SW_MUX_GPIO1_IO03);
1.2、I/O 内存访问函数
这里说的 I/O 是输入/输出的意思,并不是我们学习单片机的时候讲的 GPIO 引脚。这里涉 及到两个概念:I/O 端口和 I/O 内存。当外部寄存器或内存映射到 IO 空间时,称为 I/O 端口。 当外部寄存器或内存映射到内存空间时,称为 I/O 内存。但是对于 ARM 来说没有 I/O 空间这个 概念,因此 ARM 体系下只有 I/O 内存(可以直接理解为内存)。使用 ioremap 函数将寄存器的物 理地址映射到虚拟地址以后,我们就可以直接通过指针访问这些地址,但是 Linux 内核不建议 这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作。
1.2.1、读操作函数
readb、readw 和 readl 这三个函数分别对应 8bit、16bit 和 32bit 读操作,参数 addr 就是要 读取写内存地址,返回值就是读取到的数据。
1.2.2、写操作函数
writeb、writew 和 writel 这三个函数分别对应 8bit、16bit 和 32bit 写操作,参数 value 是要 写入的数值,addr 是要写入的地址。
二十九、新字符设备驱动实验
字符 设备驱动开发重点是使用 register_chrdev 函数注册字符设备,当不再使用设备的时候就使用 unregister_chrdev 函数注销字符设备,驱动模块加载成功以后还需要手动使用 mknod 命令创建 设备节点。register_chrdev 和 unregister_chrdev 这两个函数是老版本驱动使用的函数,现在新的 字符设备驱动已经不再使用这两个函数,而是使用Linux内核推荐的新字符设备驱动API函数。
1、新字符设备驱动原理
1.1、分配和释放设备号
使用 register_chrdev 函数注册字符设备的时候只需要给定一个主设备号即可,但是这样会 带来两个问题:
①、需要我们事先确定好哪些主设备号没有使用。
②、会将一个主设备号下的所有次设备号都使用掉,比如现在设置 LED 这个主设备号为 200,那么 0~1048575(2^20-1)这个区间的次设备号就全部都被 LED 一个设备分走了。这样太浪 费次设备号了!一个 LED 设备肯定只能有一个主设备号,一个次设备号。
解决这两个问题最好的方法就是要使用设备号的时候向 Linux 内核申请,需要几个就申请 几个,由 Linux 内核分配设备可以使用的设备号。
如果没有指定设备号的话就使用如下函数来申请设备号:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
如果给定了设备的主设备号和次设备号就使用如下所示函数来注册设备号即可:
int register_chrdev_region(dev_t from, unsigned count, const char *name)
参数 from 是要申请的起始设备号,也就是给定的设备号;参数 count 是要申请的数量,一 般都是一个;参数 name 是设备名字。
注 销 字 符 设 备 之 后 要 释 放 掉 设 备 号 , 不 管 是 通 过 alloc_chrdev_region 函 数 还 是 register_chrdev_region 函数申请的设备号,统一使用如下释放函数: void unregister_chrdev_region(dev_t from, unsigned count)
第 1~3 行,定义了主/次设备号变量 major 和 minor,以及设备号变量 devid。 第 5 行,判断主设备号 major 是否有效,在 Linux 驱动中一般给出主设备号的话就表示这 个设备的设备号已经确定了,因为次设备号基本上都选择 0,这算个 Linux 驱动开发中约定俗 成的一种规定了。 第 6 行,如果 major 有效的话就使用 MKDEV 来构建设备号,次设备号选择 0。 第 7 行,使用 register_chrdev_region 函数来注册设备号。 第 9~11 行,如果 major 无效,那就表示没有给定设备号。此时就要使用 alloc_chrdev_region 函数来申请设备号。设备号申请成功以后使用 MAJOR 和 MINOR 来提取出主设备号和次设备号,当然了,第 10 和 11 行提取主设备号和次设备号的代码可以不要。
1.2、新的字符设备注册方法
1.2.1、字符设备结构
在 Linux 中使用 cdev 结构体表示一个字符设备,cdev 结构体在 include/linux/cdev.h 文件中 的定义如下:
在 cdev 中有两个重要的成员变量:ops 和 dev,这两个就是字符设备文件操作函数集合 file_operations 以及设备号 dev_t。编写字符设备驱动之前需要定义一个 cdev 结构体变量,这个 变量就表示一个字符设备,如下所示: struct cdev test_cdev;
1.2.2、cdev_init 函数
定义好 cdev 变量以后就要使用 cdev_init 函数对其进行初始化,cdev_init 函数原型如下:
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
参数 cdev 就是要初始化的 cdev 结构体变量,参数 fops 就是字符设备文件操作函数集合。
1.2.3、cdev_add 函数
cdev_add 函数用于向 Linux 系统添加字符设备(cdev 结构体变量),首先使用 cdev_init 函数完成对 cdev 结构体变量的初始化,然后使用 cdev_add 函数向 Linux 系统添加这个字符设备。 cdev_add 函数原型如下:
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
参数 p 指向要添加的字符设备(cdev 结构体变量),参数 dev 就是设备所使用的设备号,参 数 count 是要添加的设备数量。完善示例代码 42.1.2.2,加入 cdev_add 函数,内容如下所示:
1.2.4、cdev_del 函数
卸载驱动的时候一定要使用 cdev_del 函数从 Linux 内核中删除相应的字符设备,cdev_del 函数原型如下: void cdev_del(struct cdev *p)
2、自动创建设备节点
当我们使用 modprobe 加载驱动程序以后还需要使用命令 “mknod”手动创建设备节点。本节就来讲解一下如何实现自动创建设备节点,在驱动中实现 自动创建设备节点的功能以后,使用 modprobe 加载驱动模块成功的话就会自动在/dev 目录下 创建对应的设备文件。
2.1、 mdev 机制
udev 是一个用户程序,在 Linux 下通过 udev 来实现设备文件的创建与删除,udev 可以检 测系统中硬件设备状态,可以根据系统中硬件设备状态来创建或者删除设备文件。比如使用 modprobe 命令成功加载驱动模块以后就自动在/dev 目录下创建对应的设备节点文件,使用 rmmod 命令卸载驱动模块以后就删除掉/dev 目录下的设备节点文件。使用 busybox 构建根文件 系统的时候,busybox 会创建一个 udev 的简化版本—mdev,所以在嵌入式 Linux 中我们使用mdev 来实现设备节点文件的自动创建与删除,Linux 系统中的热插拔事件也由 mdev 管理,在 /etc/init.d/rcS 文件中如下语句:
echo /sbin/mdev > /proc/sys/kernel/hotplug
2.1.1、创建和删除类
自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在 cdev_add 函数后面添 加自动创建设备节点相关代码。首先要创建一个 class 类,class 是个结构体,定义在文件 include/linux/device.h 里面。class_create 是类创建函数,class_create 是个宏定义,内容如下:
struct class *class_create (struct module *owner, const char *name)
class_create 一共有两个参数,参数 owner 一般为 THIS_MODULE,参数 name 是类名字。 返回值是个指向结构体 class 的指针,也就是创建的类。 卸载驱动程序的时候需要删除掉类,类删除函数为 class_destroy,函数原型如下:
void class_destroy(struct class *cls); 参数 cls 就是要删除的类。
2.1.2、创建设备
上一小节创建好类以后还不能实现自动创建设备节点,我们还需要在这个类下创建一个设 备。使用 device_create 函数在类下面创建设备,device_create 函数原型如下:
device_create 是个可变参数函数,参数 class 就是设备要创建哪个类下面;参数 parent 是父 设备,一般为 NULL,也就是没有父设备;参数 devt 是设备号;参数 drvdata 是设备可能会使用 的一些数据,一般为 NULL;参数 fmt 是设备名字,如果设置 fmt=xxx 的话,就会生成/dev/xxx 这个设备文件。返回值就是创建好的设备。
同样的,卸载驱动的时候需要删除掉创建的设备,设备删除函数为 device_destroy,函数原 型如下:
void device_destroy(struct class *class, dev_t devt)参数 class 是要删除的设备所处的类,参数 devt 是要删除的设备号。
2.1.3、设置文件私有数据
每个硬件设备都有一些属性,比如主设备号(dev_t),类(class)、设备(device)、开关状态(state) 等等,在编写驱动的时候你可以将这些属性全部写成变量的形式,如下所示:
三十、Linux 设备树
掌握设备树是 Linux 驱动开发人员必 备的技能!因为在新版本的 Linux 中,ARM 相关的驱动全部采用了设备树(也有支持老式驱动 的,比较少),最新出的 CPU 其驱动开发也基本都是基于设备树的,比如 ST 新出的 STM32MP157、 NXP 的 I.MX8 系列等。我们所使用的Linux版本为 4.1.15,其支持设备树,所以正点原子 I.MX6UALPHA 开发板的所有 Linux 驱动都是基于设备树的。
1、什么是设备输?
设备树(Device Tree),将这个词分开就是“设备”和“树”,描述设备树的文件叫做 DTS(Device Tree Source),这个 DTS 文件采用树形结构描述板级设备,也就是开发板上的设备信息,比如 CPU 数量、 内存基地址、IIC 接口上接了哪些设备、SPI 接口上接了哪些设备等等,如图 43.1.1 所示:
树的主干就是系统总线,IIC 控制器、GPIO 控制器、SPI 控制器等都是接 到系统主线上的分支。IIC 控制器有分为 IIC1 和 IIC2 两种,其中 IIC1 上接了 FT5206 和 AT24C02 这两个 IIC 设备,IIC2 上只接了 MPU6050 这个设备。DTS 文件的主要功能就是按照图 43.1.1 所示的结构来描述板子上的设备信息,DTS 文件描述设备信息是有相应的语法规则要求的,稍 后我们会详细的讲解 DTS 语法规则。
一个 SOC 可以作出很多不同的板子,这些不同的板子肯 定是有共同的信息,将这些共同的信息提取出来作为一个通用的文件,其他的.dts 文件直接引 用这个通用文件即可,这个通用文件就是.dtsi 文件,类似于 C 语言中的头文件。一般.dts 描述 板级信息(也就是开发板上有哪些 IIC 设备、SPI 设备等),.dtsi 描述 SOC 级信息(也就是 SOC 有 几个 CPU、主频是多少、各个外设控制器信息等)。
简而言之就是,Linux 内核中 ARM 架构下有太多的冗余的垃圾板 级信息文件,导致 linus 震怒,然后 ARM 社区引入了设备树。
2、 DTS、DTB 和 DTC
设备树源文件扩展名为.dts,但是我们在前面移植 Linux 的时候却一直在使 用.dtb 文件,那么 DTS 和 DTB 这两个文件是什么关系呢?DTS 是设备树源码文件,DTB 是将 DTS 编译以后得到的二进制文件。将.c 文件编译为.o 需要用到 gcc 编译器,那么将.dts 编译为.dtb 需要什么工具呢?需要用到 DTC 工具!DTC 工具源码在 Linux 内核的 scripts/dtc 目录下, scripts/dtc/Makefile 文件内容如下:
hostprogs-y := dtc
always := $(hostprogs-y)
dtc-objs:= dtc.o flattree.o fstree.o data.o livetree.o treesource.o srcpos.o checks.o util.o
dtc-objs += dtc-lexer.lex.o dtc-parser.tab.o
DTC 工具依赖于 dtc.c、flattree.c、fstree.c 等文件,最终编译并链接出 DTC 这 个主机文件。如果要编译 DTS 文件的话只需要进入到 Linux 源码根目录下,然后执行如下命 令: make all 或者: make dtbs
“make all”命令是编译 Linux 源码中的所有东西,包括 zImage,.ko 驱动模块以及设备 树,如果只是编译设备树的话建议使用“make dtbs”命令。
基于 ARM 架构的 SOC 有很多种,一种 SOC 又可以制作出很多款板子,每个板子都有一 个对应的 DTS 文件,那么如何确定编译哪一个 DTS 文件呢?我们就以 I.MX6ULL 这款芯片对 应的板子为例来看一下,打开 arch/arm/boot/dts/Makefile,有如下内容:
当选中 I.MX6ULL 这个 SOC 以后(CONFIG_SOC_IMX6ULL=y),所有使用到 I.MX6ULL 这个 SOC 的板子对应的.dts 文件都会被编译为.dtb。如果我们使用 I.MX6ULL 新做 了一个板子,只需要新建一个此板子对应的.dts 文件,然后将对应的.dtb 文件名添加到 dtb- $(CONFIG_SOC_IMX6ULL)下,这样在编译设备树的时候就会将对应的.dts 编译为二进制的.dtb 文件。
3、DTS语法
3.1、.dtsi 头文件
和 C 语言一样,设备树也支持头文件,设备树的头文件扩展名为.dtsi。在 imx6ull-alientekemmc.dts 中有如下所示内容:
不是说设备树的扩展名是.dtsi 吗?为什么也可以直接引用 C 语言中的.h 头文件呢?这里并没有错,.dts 文件引用 C 语言中的.h 文件,甚至也可以引用.dts 文 件,打开 imx6ull-14x14-evk-gpmi-weim.dts 这个文件,此文件中有如下内容:
#include “imx6ull-14x14-evk.dts”
因此在.dts 设备树文件中,可以通过 “#include”来引用.h、.dtsi 和.dts 文件。只是,我们在编写设备树头文件的时候最好选择.dtsi 后 缀。
一般.dtsi 文件用于描述 SOC 的内部外设信息,比如 CPU 架构、主频、外设寄存器地址范 围,比如 UART、IIC 等等。比如 imx6ull.dtsi 就是描述 I.MX6ULL 这颗 SOC 内部外设情况信息 的。
3.2、设备节点
设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设 备节点,每个节点都通过一些属性信息来描述节点信息,属性就是键—值对。
第 1 行,“/”是根节点,每个设备树文件只有一个根节点。细心的同学应该会发现,imx6ull.dtsi 和 imx6ull-alientek-emmc.dts 这两个文件都有一个“/”根节点,这样不会出错吗?不会的,因为 这两个“/”根节点的内容会合并成一个根节点。
第 2、6 和 17 行,aliases、cpus 和 intc 是三个子节点,在设备树中节点命名格式如下:
node-name@unit-address
其中“node-name”是节点名字,为 ASCII 字符串,节点名字应该能够清晰的描述出节点的 功能,比如“uart1”就表示这个节点是 UART1 外设。“unit-address”一般表示设备的地址或寄 存器首地址,如果某个节点没有地址或者寄存器的话“unit-address”可以不要,比如“cpu@0”、 “interrupt-controller@00a01000”。
cpu0:cpu@0
上述命令并不是“node-name@unit-address”这样的格式,而是用“:”隔开成了两部分,“:” 前面的是节点标签(label),“:”后面的才是节点名字,格式如下所示:label: node-name@unit-address
引入 label 的目的就是为了方便访问节点,可以直接通过&label 来访问这个节点,比如通过 &cpu0 就可以访问“cpu@0”这个节点,而不需要输入完整的节点名字。再比如节点 “intc: interrupt-controller@00a01000”,节点 label 是 intc,而节点名字就很长了,为“interruptcontroller@00a01000”。很明显通过&intc 来访问“interrupt-controller@00a01000”这个节点要方 便很多!
第 10 行cpu0 也是一个节点,只是 cpu0 是 cpus 的子节点。
每个节点都有不同属性,不同的属性又有不同的内容,属性都是键值对,值可以为空或任 意的字节流。设备树源码中常用的几种数据形式如下所示:
①、字符串
compatible = “arm,cortex-a7”; 上述代码设置 compatible 属性的值为字符串“arm,cortex-a7”。
②、32 位无符号整数
reg = <0>;上述代码设置 reg 属性的值为 0,reg 的值也可以设置为一组值,比如: reg = <0 0x123456 100>;
③、字符串列表
属性值也可以为字符串列表,字符串和字符串之间采用“,”隔开,如下所示:
compatible = “fsl,imx6ull-gpmi-nand”, “fsl, imx6ul-gpmi-nand”;
上述代码设置属性 compatible 的值为“fsl,imx6ull-gpmi-nand”和“fsl, imx6ul-gpmi-nand”。
3.3、标准属性
节点是由一堆的属性组成,节点都是具体的设备,不同的设备需要的属性不同,用户可以 自定义属性。除了用户自定义属性,有很多属性是标准属性,Linux 下的很多外设驱动都会使用 这些标准属性,本节我们就来学习一下几个常用的标准属性。
3.3.1、compatible 属性
compatible 属性也叫做“兼容性”属性,这是非常重要的一个属性!compatible 属性的值是 一个字符串列表,compatible 属性用于将设备和驱动绑定起来。字符串列表用于选择设备所要 使用的驱动程序,compatible 属性的值格式如下所示:
“manufacturer,model”
其中 manufacturer 表示厂商,model 一般是模块对应的驱动名字。比如 imx6ull-alientekemmc.dts 中 sound 节点是 I.MX6U-ALPHA 开发板的音频设备节点,I.MX6U-ALPHA 开发板上 的音频芯片采用的欧胜(WOLFSON)出品的 WM8960,sound 节点的 compatible 属性值如下:
compatible = “fsl,imx6ul-evk-wm8960”,“fsl,imx-audio-wm8960”;
属性值有两个,分别为“fsl,imx6ul-evk-wm8960”和“fsl,imx-audio-wm8960”,其中“fsl” 表示厂商是飞思卡尔,“imx6ul-evk-wm8960”和“imx-audio-wm8960”表示驱动模块名字。sound 这个设备首先使用第一个兼容值在 Linux 内核里面查找,看看能不能找到与之匹配的驱动文件, 如果没有找到的话就使用第二个兼容值查。
一般驱动程序文件都会有一个 OF 匹配表,此 OF 匹配表保存着一些 compatible 值,如果设 备节点的 compatible 属性值和 OF 匹配表中的任何一个值相等,那么就表示设备可以使用这个 驱动。比如在文件 imx-wm8960.c 中有如下内容:
第 632~635 行的数组 imx_wm8960_dt_ids 就是 imx-wm8960.c 这个驱动文件的匹配表,此 匹配表只有一个匹配值“fsl,imx-audio-wm8960”。如果在设备树中有哪个节点的 compatible 属 性值与此相等,那么这个节点就会使用此驱动文件。
第 642 行,wm8960 采用了 platform_driver 驱动模式,关于 platform_driver 驱动后面会讲 解。此行设置.of_match_table 为 imx_wm8960_dt_ids,也就是设置这个 platform_driver 所使用的 OF 匹配表。
3.3.2、model 属性
model 属性值也是一个字符串,一般 model 属性描述设备模块信息,比如名字什么的,比 如: model = “wm8960-audio”;
3.3.3、status 属性
status 属性看名字就知道是和设备状态有关的,status 属性值也是字符串,字符串是设备的 状态信息,可选的状态如表 43.3.3.1 所示:
3.3.4、#address-cells 和#size-cells 属性
这两个属性的值都是无符号 32 位整形,#address-cells 和#size-cells 这两个属性可以用在任 何拥有子节点的设备中,用于描述子节点的地址信息。#address-cells 属性值决定了子节点 reg 属 性中地址信息所占用的字长(32 位),#size-cells 属性值决定了子节点 reg 属性中长度信息所占的字长(32 位)。#address-cells 和#size-cells 表明了子节点应该如何编写 reg 属性值,一般 reg 属性 都是和地址有关的内容,和地址相关的信息有两种:起始地址和地址长度,reg 属性的格式一为
每个“address length”组合表示一个地址范围,其中 address 是起始地址,length 是地址长 度,#address-cells 表明 address 这个数据所占用的字长,#size-cells 表明 length 这个数据所占用 的字长,比如:
第 3,4 行,节点 spi4 的#address-cells = <1>,#size-cells = <0>,说明 spi4 的子节点 reg 属 性中起始地址所占用的字长为 1,地址长度所占用的字长为 0。
第 8 行,子节点 gpio_spi: gpio_spi@0 的 reg 属性值为 <0>,因为父节点设置了#addresscells = <1>,#size-cells = <0>,因此 addres=0,没有 length 的值,相当于设置了起始地址,而没 有设置地址长度。
第 14,15 行,设置 aips3: aips-bus@02200000 节点#address-cells = <1>,#size-cells = <1>, 说明 aips3: aips-bus@02200000 节点起始地址长度所占用的字长为 1,地址长度所占用的字长也 为 1。
第 19 行,子节点 dcp: dcp@02280000 的 reg 属性值为<0x02280000 0x4000>,因为父节点设 置了#address-cells = <1>,#size-cells = <1>,address= 0x02280000,length= 0x4000,相当于设置 了起始地址为 0x02280000,地址长度为 0x40000。
3.3.5、reg 属性
reg 属性前面已经提到过了,reg 属性的值一般是(address,length)对。reg 属性一般用于描 述设备地址空间资源信息,一般都是某个外设的寄存器地址范围信息,比如在 imx6ull.dtsi 中有 如下内容:
上述代码是节点 uart1,uart1 节点描述了 I.MX6ULL 的 UART1 相关信息,重点是第 326 行 的 reg 属性。其中 uart1 的父节点 aips1: aips-bus@02000000 设置了#address-cells = <1>、#sizecells = <1>,因此 reg 属性中 address=0x02020000,length=0x4000。查阅《I.MX6ULL 参考手册》 可知,I.MX6ULL 的 UART1 寄存器首地址为 0x02020000,但是 UART1 的地址长度(范围)并没 有 0x4000 这么多,这里我们重点是获取 UART1 寄存器首地址。
3.3.6、ranges 属性
ranges属性值可以为空或者按照(child-bus-address,parent-bus-address,length)格式编写的数字 矩阵,ranges 是一个地址映射/转换表,ranges 属性每个项目由子地址、父地址和地址空间长度 这三部分组成:
child-bus-address:子总线地址空间的物理地址,由父节点的#address-cells 确定此物理地址 所占用的字长。
parent-bus-address:父总线地址空间的物理地址,同样由父节点的#address-cells 确定此物 理地址所占用的字长。
length:子地址空间的长度,由父节点的#size-cells 确定此地址长度所占用的字长。
如果 ranges 属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换, 对于我们所使用的 I.MX6ULL 来说,子地址空间和父地址空间完全相同,因此会在 imx6ull.dtsi 中找到大量的值为空的 ranges 属性。
第 5 行,节点 soc 定义的 ranges 属性,值为<0x0 0xe0000000 0x00100000>,此属性值指定 了一个 1024KB(0x00100000)的地址范围,子地址空间的物理起始地址为 0x0,父地址空间的物 理起始地址为 0xe0000000。
第 10 行,serial 是串口设备节点,reg 属性定义了 serial 设备寄存器的起始地址为 0x4600, 寄存器长度为 0x100。经过地址转换,serial 设备可以从 0xe0004600 开始进行读写操作, 0xe0004600=0x4600+0xe0000000。
3.3.7、name 属性
name 属性值为字符串,name 属性用于记录节点名字,name 属性已经被弃用,不推荐使用 name 属性,一些老的设备树文件可能会使用此属性。
3.3.8、device_type 属性
device_type 属性值为字符串,IEEE 1275 会用到此属性,用于描述设备的 FCode,但是设 备树没有 FCode,所以此属性也被抛弃了。此属性只能用于 cpu 节点或者 memory 节点。 imx6ull.dtsi 的 cpu0 节点用到了此属性,内容如下所示:
3.4、根节点 compatible 属性
每个节点都有 compatible 属性,根节点“/”也不例外,imx6ull-alientek-emmc.dts 文件中根 节点的 compatible 属性内容如下所示:
compatible 有两个值:“fsl,imx6ull-14x14-evk”和“fsl,imx6ull”。前面我们说了, 设备节点的 compatible 属性值是为了匹配 Linux 内核中的驱动程序,那么根节点中的 compatible 属性是为了做什么工作的? 通过根节点的 compatible 属性可以知道我们所使用的设备,一般第 一个值描述了所使用的硬件设备名字,比如这里使用的是“imx6ull-14x14-evk”这个设备,第二 个值描述了设备所使用的 SOC,比如这里使用的是“imx6ull”这颗 SOC。Linux 内核会通过根 节点的 compoatible 属性查看是否支持此设备,如果支持的话设备就会启动 Linux 内核。接下来 我们就来学习一下 Linux 内核在使用设备树前后是如何判断是否支持某款设备的。
3.4.1、使用设备树之前设备匹配方法
在没有使用设备树以前,uboot 会向 Linux 内核传递一个叫做 machine id 的值,machine id 也就是设备 ID,告诉 Linux 内核自己是个什么设备,看看 Linux 内核是否支持。Linux 内核是 支持很多设备的,针对每一个设备(板子),Linux内核都用MACHINE_START和MACHINE_END 来定义一个 machine_desc 结构体来描述这个设备,比如在文件 arch/arm/mach-imx/machmx35_3ds.c 中有如下定义:
上述代码就是定义了“Freescale MX35PDK”这个设备,其中 MACHINE_START 和 MACHINE_END 定义在文件 arch/arm/include/asm/mach/arch.h 中,内容如下:
3.4.2、使用设备树以后的设备匹配方法
当 Linux 内 核 引 入 设 备 树 以 后 就 不 再 使 用 MACHINE_START 了 , 而 是 换 为 了 DT_MACHINE_START。DT_MACHINE_START 也定义在文件 arch/arm/include/asm/mach/arch.h 里面,定义如下:
DT_MACHINE_START 和 MACHINE_START 基本相同,只是.nr 的设置不同, 在 DT_MACHINE_START 里面直接将.nr 设置为~0。说明引入设备树以后不会再根据 machine id 来检查 Linux 内核是否支持某个设备了。
打开文件 arch/arm/mach-imx/mach-imx6ul.c,有如下所示内容:
machine_desc 结构体中有个.dt_compat 成员变量,此成员变量保存着本设备兼容属性,示 例代码 43.3.4.5 中设置.dt_compat = imx6ul_dt_compat,imx6ul_dt_compat 表里面有"fsl,imx6ul" 和"fsl,imx6ull"这两个兼容值。只要某个设备(板子)根节点“/”的 compatible 属性值与 imx6ul_dt_compat 表中的任何一个值相等,那么就表示 Linux 内核支持此设备。imx6ull-alientekemmc.dts 中根节点的 compatible 属性值如下:
compatible = “fsl,imx6ull-14x14-evk”, “fsl,imx6ull”;
Linux 内核是如何根据设备树根节点的 compatible 属性来匹配出对 应的 machine_desc,Linux 内核调用 start_kernel 函数来启动内核,start_kernel 函数会调用 setup_arch 函数来匹配 machine_desc,setup_arch 函数定义在文件 arch/arm/kernel/setup.c 中,函 数内容如下(有缩减):
第 918 行,调用 setup_machine_fdt 函数来获取匹配的 machine_desc,参数就是 atags 的首 地址,也就是 uboot 传递给 Linux 内核的 dtb 文件首地址,setup_machine_fdt 函数的返回值就是 找到的最匹配的 machine_desc。 函数 setup_machine_fdt 定义在文件 arch/arm/kernel/devtree.c 中,内容如下(有缩减):
第 218 行,调用函数 of_flat_dt_match_machine 来获取匹配的 machine_desc,参数 mdesc_best 是 默 认 的 machine_desc ,参数 arch_get_next_mach 是 个 函 数 , 此 函 数 定 义 在 定 义 在 arch/arm/kernel/devtree.c 文件中。找到匹配的 machine_desc 的过程就是用设备树根节点的 compatible 属性值和 Linux 内核中 machine_desc 下.dt_compat 的值比较,看看那个相等,如果相 等的话就表示找到匹配的 machine_desc,arch_get_next_mach 函数的工作就是获取 Linux 内核中 下一个 machine_desc 结构体。
Linux 内核通过根节点 compatible 属性找到对应的设备的函数调用过程
3.5 向节点追加或修改内容
产品开发过程中可能面临着频繁的需求更改,比如第一版硬件上有一个 IIC 接口的六轴芯 片 MPU6050,第二版硬件又要把这个 MPU6050 更换为 MPU9250 等。一旦硬件修改了,我们 就要同步的修改设备树文件,毕竟设备树是描述板子硬件信息的文件。假设现在有个六轴芯片 fxls8471,fxls8471 要接到 I.MX6U-ALPHA 开发板的 I2C1 接口上,那么相当于需要在 i2c1 这 个节点上添加一个 fxls8471 子节点。先看一下 I2C1 接口对应的节点,打开文件 imx6ull.dtsi 文 件,找到如下所示内容:
示例代码 43.3.5.1 就是 I.MX6ULL 的 I2C1 节点,现在要在 i2c1 节点下创建一个子节点, 这个子节点就是 fxls8471,最简单的方法就是在 i2c1 下直接添加一个名为 fxls8471 的子节点, 如下所示:
第 947~950 行就是添加的 fxls8471 这个芯片对应的子节点。但是这样会有个问题!i2c1 节 点是定义在 imx6ull.dtsi 文件中的,而 imx6ull.dtsi 是设备树头文件,其他所有使用到 I.MX6ULL 这颗 SOC 的板子都会引用 imx6ull.dtsi 这个文件。直接在 i2c1 节点中添加 fxls8471 就相当于在 其他的所有板子上都添加了 fxls8471 这个设备,但是其他的板子并没有这个设备啊!因此,按 照示例代码 43.3.5.2 这样写肯定是不行的。 这里就要引入另外一个内容,那就是如何向节点追加数据,我们现在要解决的就是如何向 i2c1 节点追加一个名为 fxls8471 的子节点,而且不能影响到其他使用到 I.MX6ULL 的板子。 I.MX6U-ALPHA 开发板使用的设备树文件为 imx6ull-alientek-emmc.dts,因此我们需要在 imx6ull-alientek-emmc.dts 文件中完成数据追加的内容,方式如下:
第 1 行,&i2c1 表示要访问 i2c1 这个 label 所对应的节点,也就是 imx6ull.dtsi 中的“i2c1: i2c@021a0000”。 第 2 行,花括号内就是要向 i2c1 这个节点添加的内容,包括修改某些属性的值。 打开 imx6ull-alientek-emmc.dts,找到如下所示内容:
示例代码 43.3.5.4 就是向 i2c1 节点添加/修改数据,比如第 225 行的属性“clock-frequency” 就表示 i2c1 时钟为 100KHz。“clock-frequency”就是新添加的属性。 第 228 行,将 status 属性的值由原来的 disabled 改为 okay。 第 230~234 行,i2c1 子节点 mag3110,因为 NXP 官方开发板在 I2C1 上接了一个磁力计芯 片 mag3110,正点原子的 I.MX6U-ALPHA 开发板并没有使用 mag3110。 第 236~242 行,i2c1 子节点 fxls8471,同样是因为 NXP 官方开发板在 I2C1 上接了 fxls8471 这颗六轴芯片。 因为示例代码 43.3.5.4 中的内容是 imx6ull-alientek-emmc.dts 这个文件内的,所以不会对 使用 I.MX6ULL 这颗 SOC 的其他板子造成任何影响。这个就是向节点追加或修改内容,重点 就是通过&label 来访问节点,然后直接在里面编写要追加或者修改的内容。
4、 创建小型模板设备树
5、Linux 内核解析 DTB 文件
Linux 内核在启动的时候会解析 DTB 文件,然后在/proc/device-tree 目录下生成相应的设备 树节点文件。接下来我们简单分析一下 Linux 内核是如何解析 DTB 文件的,流程如图 43.7.1 所 示:
6、绑定信息文档
设备树是用来描述板子上的设备信息的,不同的设备其信息不同,反映到设备树中就是属 性不同。那么我们在设备树中添加一个硬件对应的节点的时候从哪里查阅相关的说明呢?在 Linux 内核源码中有详细的.txt 文档描述了如何添加节点,这些.txt 文档叫做绑定文档,路径为: Linux 源码目录/Documentation/devicetree/bindings
比如我们现在要想在 I.MX6ULL 这颗 SOC 的 I2C 下添加一个节点,那么就可以查看 Documentation/devicetree/bindings/i2c/i2c-imx.txt,此文档详细的描述了 I.MX 系列的 SOC 如何 在设备树中添加 I2C 设备节点
有时候使用的一些芯片在 Documentation/devicetree/bindings 目录下找不到对应的文档,这 个时候就要咨询芯片的提供商,让他们给你提供参考的设备树文件。
7、设备树常用 OF 操作函数
设备树描述了设备的详细信息,这些信息包括数字类型的、字符串类型的、数组类型的, 我们在编写驱动的时候需要获取到这些信息。比如设备树使用 reg 属性描述了某个外设的寄存 器地址为 0X02005482,长度为 0X400,我们在编写驱动的时候需要获取到 reg 属性的0X02005482 和 0X400 这两个值,然后初始化外设。Linux 内核给我们提供了一系列的函数来获 取设备树中的节点或者属性信息,这一系列的函数都有一个统一的前缀“of_”,所以在很多资 料里面也被叫做 OF 函数。这些 OF 函数原型都定义在 include/linux/of.h 文件中。
7.1、查找节点的 OF 函数
设备都是以节点的形式“挂”到设备树上的,因此要想获取这个设备的其他属性信息,必 须先获取到这个设备的节点。Linux 内核使用 device_node 结构体来描述一个节点,此结构体定 义在文件 include/linux/of.h 中,定义如下:
7.1.1、of_find_node_by_name 函数
of_find_node_by_name 函数通过节点名字查找指定的节点,函数原型如下:
struct device_node *of_find_node_by_name(struct device_node *from, const char *name);
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。 name:要查找的节点名字。 返回值:找到的节点,如果为 NULL 表示查找失败。
7.1.2、of_find_node_by_type 函数
of_find_node_by_type 函数通过 device_type 属性查找指定的节点,函数原型如下:
struct device_node *of_find_node_by_type(struct device_node *from, const char *type)
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。 type:要查找的节点对应的 type 字符串,也就是 device_type 属性值。 返回值:找到的节点,如果为 NULL 表示查找失败。
7.1.3、of_find_compatible_node 函数
of_find_compatible_node 函数根据 device_type 和 compatible 这两个属性查找指定的节点, 函数原型如下:
struct device_node *of_find_compatible_node(struct device_node *from, const char *type, const char *compatible)
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。 type:要查找的节点对应的 type 字符串,也就是 device_type 属性值,可以为 NULL,表示 忽略掉 device_type 属性。 compatible:要查找的节点所对应的 compatible 属性列表。 返回值:找到的节点,如果为 NULL 表示查找失败
7.1.4、of_find_matching_node_and_match 函数
of_find_matching_node_and_match 函数通过 of_device_id 匹配表来查找指定的节点,函数原 型如下:
struct device_node *of_find_matching_node_and_match(struct device_node *from, const struct of_device_id *matches, const struct of_device_id **match)
函数参数和返回值含义如下: from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。 matches:of_device_id 匹配表,也就是在此匹配表里面查找节点。 match:找到的匹配的 of_device_id。 返回值:找到的节点,如果为 NULL 表示查找失败
7.1.5、of_find_node_by_path 函数
of_find_node_by_path 函数通过路径来查找指定的节点,函数原型如下:
inline struct device_node *of_find_node_by_path(const char *path)
函数参数和返回值含义如下: path:带有全路径的节点名,可以使用节点的别名,比如“/backlight”就是 backlight 这个 节点的全路径。 返回值:找到的节点,如果为 NULL 表示查找失败
7.2、查找父/子节点的 OF 函数
7.2.1、of_get_parent 函数
of_get_parent 函数用于获取指定节点的父节点(如果有父节点的话),函数原型如下:
struct device_node *of_get_parent(const struct device_node *node)
node:要查找的父节点的节点。 返回值:找到的父节点。
7.2.2、of_get_next_child 函数
of_get_next_child 函数用迭代的方式查找子节点,函数原型如下:
struct device_node *of_get_next_child(const struct device_node *node, struct device_node *prev)
node:父节点。 prev:前一个子节点,也就是从哪一个子节点开始迭代的查找下一个子节点。可以设置为 NULL,表示从第一个子节点开始。 返回值:找到的下一个子节点。
7.3 提取属性值的 OF 函数
节点的属性信息里面保存了驱动所需要的内容,因此对于属性值的提取非常重要,Linux 内 核中使用结构体 property 表示属性,此结构体同样定义在文件 include/linux/of.h 中,
Linux 内核也提供了提取属性值的 OF 函数,我们依次来看一下
7.3.1、of_find_property 函数
of_find_property 函数用于查找指定的属性,函数原型如下:
property *of_find_property(const struct device_node *np, const char *name, int *lenp)
np:设备节点。 name: 属性名字。 lenp:属性值的字节数 返回值:找到的属性。
7.3.2、of_property_count_elems_of_size 函数
of_property_count_elems_of_size 函数用于获取属性中元素的数量,比如 reg 属性值是一个 数组,那么使用此函数可以获取到这个数组的大小,此函数原型如下:
int of_property_count_elems_of_size(const struct device_node *np, const char *propname, int elem_size)
np:设备节点。 proname: 需要统计元素数量的属性名字。 elem_size:元素长度。 返回值:得到的属性元素数量
7.3.3、of_property_read_u32_index 函数
of_property_read_u32_index 函数用于从属性中获取指定标号的 u32 类型数据值(无符号 32 位),比如某个属性有多个 u32 类型的值,那么就可以使用此函数来获取指定标号的数据值,此 函数原型如下:
int of_property_read_u32_index(const struct device_node *np, const char *propname, u32 index, u32 *out_value)
np:设备节点。 proname: 要读取的属性名字。 index:要读取的值标号。 out_value:读取到的值 返回值:0 读取成功,负值,读取失败,-EINVAL 表示属性不存在,-ENODATA 表示没有 要读取的数据,-EOVERFLOW 表示属性值列表太小。
7.3.4、 of_property_read_u8_array 函数 of_property_read_u16_array 函数of_property_read_u32_array 函数 of_property_read_u64_array 函数
这 4 个函数分别是读取属性中 u8、u16、u32 和 u64 类型的数组数据,比如大多数的 reg 属 性都是数组数据,可以使用这 4 个函数一次读取出 reg 属性中的所有数据。
np:设备节点。 proname: 要读取的属性名字。 out_value:读取到的数组值,分别为 u8、u16、u32 和 u64。 sz:要读取的数组元素数量。
返回值:0,读取成功,负值,读取失败,-EINVAL 表示属性不存在,-ENODATA 表示没 有要读取的数据,-EOVERFLOW 表示属性值列表太小。
7.3.5、of_property_read_u8 函数 of_property_read_u16 函数 of_property_read_u32 函数 of_property_read_u64 函数
np:设备节点。 proname: 要读取的属性名字。 out_value:读取到的数组值。 返回值:0,读取成功,负值,读取失败,-EINVAL 表示属性不存在,-ENODATA 表示没 有要读取的数据,-EOVERFLOW 表示属性值列表太小。
7.3.6、of_property_read_string 函数
of_property_read_string 函数用于读取属性中字符串值,函数原型如下:
int of_property_read_string(struct device_node *np, const char *propname, const char **out_string)
np:设备节点。 proname: 要读取的属性名字。 out_string:读取到的字符串值。 返回值:0,读取成功,负值,读取失败。
7.3.7、of_n_addr_cells 函数
of_n_addr_cells 函数用于获取#address-cells 属性值,函数原型如下:
int of_n_addr_cells(struct device_node *np)
函数参数和返回值含义如下: np:设备节点。 返回值:获取到的#address-cells 属性值。
7.3.8、of_n_size_cells 函数
of_size_cells 函数用于获取#size-cells 属性值,函数原型如下:
int of_n_size_cells(struct device_node *np)
函数参数和返回值含义如下: np:设备节点。 返回值:获取到的#size-cells 属性值。
7.4 其他常用的 OF 函数
7.4.1、of_device_is_compatible 函数
of_device_is_compatible 函数用于查看节点的 compatible 属性是否有包含 compat 指定的字 符串,也就是检查设备节点的兼容性,函数原型如下:
int of_device_is_compatible(const struct device_node *device, const char *compat)
函数参数和返回值含义如下: device:设备节点。 compat:要查看的字符串。 返回值:0,节点的 compatible 属性中不包含 compat 指定的字符串;正数,节点的 compatible 属性中包含 compat 指定的字符串。
7.4.2、of_get_address 函数
of_get_address 函数用于获取地址相关属性,主要是“reg”或者“assigned-addresses”属性 值,函数原型如下:
const __be32 *of_get_address(struct device_node *dev, int index, u64 *size, unsigned int *flags)
函数参数和返回值含义如下: dev:设备节点。 index:要读取的地址标号。 size:地址长度。 flags:参数,比如 IORESOURCE_IO、IORESOURCE_MEM 等 返回值:读取到的地址数据首地址,为 NULL 的话表示读取失败。
7.4.3、of_translate_address 函数
of_translate_address 函数负责将从设备树读取到的地址转换为物理地址,函数原型如下:
u64 of_translate_address(struct device_node *dev, const __be32 *in_addr)
函数参数和返回值含义如下: dev:设备节点。 in_addr:要转换的地址。 返回值:得到的物理地址,如果为 OF_BAD_ADDR 的话表示转换失败。
7.4.4、of_address_to_resource 函数
IIC、SPI、GPIO 等这些外设都有对应的寄存器,这些寄存器其实就是一组内存空间,Linux 内核使用 resource 结构体来描述一段内存空间,“resource”翻译出来就是“资源”,因此用 resource 结构体描述的都是设备资源信息,resource 结构体定义在文件 include/linux/ioport.h 中,定义如 下:
对于 32 位的 SOC 来说,resource_size_t 是 u32 类型的。其中 start 表示开始地址,end 表示 结束地址,name 是这个资源的名字,flags 是资源标志位,一般表示资源类型,可选的资源标志 定义在文件 include/linux/ioport.h 中,如下所示:
大 家 一 般 最 常 见 的 资 源 标 志 就 是 IORESOURCE_MEM 、 IORESOURCE_REG 和 IORESOURCE_IRQ 等。接下来我们回到 of_address_to_resource 函数,此函数看名字像是从设 备树里面提取资源值,但是本质上就是将 reg 属性值,然后将其转换为 resource 结构体类型, 函数原型如下所示
nt of_address_to_resource(struct device_node *dev, int index, struct resource *r)
函数参数和返回值含义如下: dev:设备节点。 index:地址资源标号。 r:得到的 resource 类型的资源值。 返回值:0,成功;负值,失败。
7.4.5、of_iomap 函数
of_iomap 函数用于直接内存映射,以前我们会通过 ioremap 函数来完成物理地址到虚拟地 址的映射,采用设备树以后就可以直接通过 of_iomap 函数来获取内存地址所对应的虚拟地址, 不需要使用 ioremap 函数了。当然了,你也可以使用 ioremap 函数来完成物理地址到虚拟地址 的内存映射,只是在采用设备树以后,大部分的驱动都使用 of_iomap 函数了。of_iomap 函数本 质上也是将 reg 属性中地址信息转换为虚拟地址,如果 reg 属性有多段的话,可以通过 index 参 数指定要完成内存映射的是哪一段,of_iomap 函数原型如下:
void __iomem *of_iomap(struct device_node *np, int index)
np:设备节点。 index:reg 属性中要完成内存映射的段,如果 reg 属性只有一段的话 index 就设置为 0。 返回值:经过内存映射后的虚拟内存首地址,如果为 NULL 的话表示内存映射失败。 关于设备树常用的 OF 函数就先讲解到这里,Linux 内核中关于设备树的 OF 函数不仅仅只 有前面讲的这几个,还有很多 OF 函数我们并没有讲解,这些没有讲解的 OF 函数要结合具体 的驱动,比如获取中断号的 OF 函数、获取 GPIO 的 OF 函数等等,这些 OF 函数我们在后面的 驱动实验中再详细的讲解。
三十一、设备树下的 LED 驱动实验
三十二、pinctrl 和 gpio 子系统实验
1、pinctrl 子系统
1.1、pinctrl 子系统简介
Linux 驱动讲究驱动分离与分层,pinctrl 和 gpio 子系统就是驱动分离与分层思想下的产物, 驱动分离与分层其实就是按照面向对象编程的设计思想而设计的设备驱动框架。
我们先来回顾一下上一章是怎么初始化 LED 灯所使用的 GPIO,步骤如下:
①、修改设备树,添加相应的节点,节点里面重点是设置 reg 属性,reg 属性包括了 GPIO 相关寄存器。
② 、 获 取 reg 属 性 中 IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 和 IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03 这两个寄存器地址,并且初始化这两个寄存器,这 两个寄存器用于设置 GPIO1_IO03 这个 PIN 的复用功能、上下拉、速度等。
③、在②里面将 GPIO1_IO03 这个 PIN 复用为了 GPIO 功能,因此需要设置 GPIO1_IO03 这个 GPIO 相关的寄存器,也就是 GPIO1_DR 和 GPIO1_GDIR 这两个寄存器
Linux 内核针对 PIN 的配置推出了 pinctrl 子系统,对于 GPIO 的配置推出了 gpio 子系统。
pinctrl 子系统主要工作内容如下: ①、获取设备树中 pin 信息。 ②、根据获取到的 pin 信息来设置 pin 的复用功能 ③、根据获取到的 pin 信息来设置 pin 的电气特性,比如上/下拉、速度、驱动能力等。 对于我们使用者来讲,只需要在设备树里面设置好某个 pin 的相关属性即可,其他的初始 化工作均由 pinctrl 子系统来完成,pinctrl 子系统源码目录为 drivers/pinctrl。
1.2、I.MX6ULL 的 pinctrl 子系统驱动
1.2.1、PIN 配置信息详解
要使用 pinctrl 子系统,我们需要在设备树里面设置 PIN 的配置信息,毕竟 pinctrl 子系统要 根据你提供的信息来配置 PIN 功能,一般会在设备树里面创建一个节点来描述 PIN 的配置信 息。打开 imx6ull.dtsi 文件,找到一个叫做 iomuxc 的节点,如下所示:
iomuxc 节点就是 I.MX6ULL 的 IOMUXC 外设对应的节点,看起来内容很少,没看出什么 跟 PIN 的配置有关的内容啊,别急!打开 imx6ull-alientek-emmc.dts,找到如下所示内容:
示例代码 45.1.2.2 就是向 iomuxc 节点追加数据,不同的外设使用的 PIN 不同、其配置也不 同,因此一个萝卜一个坑,将某个外设所使用的所有 PIN 都组织在一个子节点里面。示例代码 45.1.2.2 中 pinctrl_hog_1 子节点就是和热插拔有关的 PIN 集合,比如 USB OTG 的 ID 引脚。 pinctrl_flexcan1 子节点是 flexcan1 这个外设所使用的 PIN,pinctrl_wdog 子节点是 wdog 外设所 使用的 PIN。如果需要在 iomuxc 中添加我们自定义外设的 PIN,那么需要新建一个子节点,然 后将这个自定义外设的所有 PIN 配置信息都放到这个子节点中
2、gpio 子系统
2.1、 gpio 子系统简介
pinctrl 子系统重点是设置 PIN(有的 SOC 叫做 PAD)的复用 和电气属性,如果 pinctrl 子系统将一个 PIN 复用为 GPIO 的话,那么接下来就要用到 gpio 子系 统了。gpio 子系统顾名思义,就是用于初始化 GPIO 并且提供相应的 API 函数,比如设置 GPIO 为输入输出,读取 GPIO 的值等。gpio 子系统的主要目的就是方便驱动开发者使用 gpio,驱动 开发者在设备树中添加 gpio 相关信息,然后就可以在驱动程序中使用 gpio 子系统提供的 API 函数来操作 GPIO,Linux 内核向驱动开发者屏蔽掉了 GPIO 的设置过程,极大的方便了驱动开 发者使用 GPIO。
pinctrl 配置好以后就是设置 gpio 了,SD 卡驱动程序通过读取 GPIO1_IO19 的值来判断 SD 卡有没有插入,但是 SD 卡驱动程序怎么知道 CD 引脚连接的 GPIO1_IO19 呢?肯定是需要设 备树告诉驱动啊!在设备树中 SD 卡节点下添加一个属性来描述 SD 卡的 CD 引脚就行了,SD 卡驱动直接读取这个属性值就知道 SD 卡的 CD 引脚使用的是哪个 GPIO 了。SD 卡连接在 I.MX6ULL 的 usdhc1 接口上,在 imx6ull-alientek-emmc.dts 中找到名为“usdhc1”的节点,这个 节点就是 SD 卡设备节点,如下所示:
第 765 行,此行本来没有,是作者添加的,usdhc1 节点作为 SD 卡设备总节点,usdhc1 节 点需要描述 SD 卡所有的信息,因为驱动要使用。本行就是描述 SD 卡的 CD 引脚 pinctrl 信息 所在的子节点,因为 SD 卡驱动需要根据 pincrtl 节点信息来设置 CD 引脚的复用功能等。762~764 行的 pinctrl-0~2 都是 SD 卡其他 PIN 的 pincrtl 节点信息。但是大家会发现,其实在 usdhc1 节点 中并没有“pinctrl-3 = <&pinctrl_hog_1>”这一行,也就是说并没有指定 CD 引脚的 pinctrl 信息, 那么 SD 卡驱动就没法设置 CD 引脚的复用功能啊?这个不用担心,因为在“iomuxc”节点下 引用了 pinctrl_hog_1 这个节点,所以 Linux 内核中的 iomuxc 驱动就会自动初始化 pinctrl_hog_1 节点下的所有 PIN。
第 766 行,属性“cd-gpios”描述了 SD 卡的 CD 引脚使用的哪个 IO。属性值一共有三个, 我们来看一下这三个属性值的含义,“&gpio1”表示 CD 引脚所使用的 IO 属于 GPIO1 组,“19” 表示 GPIO1 组的第 19 号 IO,通过这两个值 SD 卡驱动程序就知道 CD 引脚使用了 GPIO1_IO19 这 GPIO。“GPIO_ACTIVE_LOW”表示低电平有效,如果改为“GPIO_ACTIVE_HIGH”就表 示高电平有效。
根据上面这些信息,SD 卡驱动程序就可以使用 GPIO1_IO19 来检测 SD 卡的 CD 信号了, 打开 imx6ull.dtsi,在里面找到如下所示内容:
gpio1 节点信息描述了 GPIO1 控制器的所有信息,重点就是 GPIO1 外设寄存器基地址以及 兼 容 属 性 。
第 505 行,设置 gpio1 节点的 compatible 属性有两个,分别为“fsl,imx6ul-gpio”和“fsl,imx35- gpio”,在 Linux 内核中搜索这两个字符串就可以找到 I.MX6UL 的 GPIO 驱动程序。
第 506 行,reg 属性设置了 GPIO1 控制器的寄存器基地址为 0X0209C000,
第 509 行,“gpio-controller”表示 gpio1 节点是个 GPIO 控制器。
第 510 行,“#gpio-cells”属性和“#address-cells”类似,#gpio-cells 应该为 2,表示一共有 两个 cell,第一个 cell 为 GPIO 编号,比如“&gpio1 3”就表示 GPIO1_IO03。第二个 cell 表示 GPIO 极 性 , 如 果 为 0(GPIO_ACTIVE_HIGH) 的 话 表 示 高 电 平 有 效 , 如 果 为 1(GPIO_ACTIVE_LOW)的话表示低电平有效。
2.2、GPIO 驱动程序简介
gpio1 节点的 compatible 属性描述了兼容性,在 Linux 内核中搜索“fsl,imx6ul-gpio”和 “fsl,imx35-gpio”这两个字符串,查找 GPIO 驱动文件。drivers/gpio/gpio-mxc.c 就是 I.MX6ULL 的 GPIO 驱动文件,在此文件中有如下所示 of_device_id 匹配表:
第 156 行的 compatible 值为“fsl,imx35-gpio”,和 gpio1 的 compatible 属性匹配,因此 gpiomxc.c 就是 I.MX6ULL 的 GPIO 控制器驱动文件。gpio-mxc.c 所在的目录为 drivers/gpio,打开这 个目录可以看到很多芯片的 gpio 驱动文件, “gpiolib”开始的文件是 gpio 驱动的核心文件,
在 gpio-mxc.c 文件中有如下所示内容:
可以看出 GPIO 驱动也是个平台设备驱动,因此当设备树中的设备节点与驱动的 of_device_id 匹配以后 probe 函数就会执行,在这里就是 mxc_gpio_probe 函数,这个函数就是 I.MX6ULL 的 GPIO 驱动入口函数。我们简单来分析一下 mxc_gpio_probe 这个函数,函数内容 如下:
gpio-mxc.c 的重点工 作就是维护 mxc_gpio_port,mxc_gpio_port 就是对 I.MX6ULL GPIO 的抽象。
mxc_gpio_port 的 bgc 成员变量很重要,因为稍后的重点就是初始化 bgc。
3、gpio 子系统 API 函数
3.1、gpio_request 函数
gpio_request 函数用于申请一个 GPIO 管脚,在使用一个 GPIO 之前一定要使用 gpio_request 进行申请,函数原型如下: int gpio_request(unsigned gpio, const char *label)
gpio:要申请的 gpio 标号,使用 of_get_named_gpio 函数从设备树获取指定 GPIO 属性信 息,此函数会返回这个 GPIO 的标号。
label:给 gpio 设置个名字。 返回值:0,申请成功;其他值,申请失败。
3.2、gpio_free 函数
如果不使用某个 GPIO 了,那么就可以调用 gpio_free 函数进行释放。函数原型如下:
void gpio_free(unsigned gpio) 函数参数和返回值含义如下: gpio:要释放的 gpio 标号。
3.3、gpio_direction_input 函数
此函数用于设置某个 GPIO 为输入,函数原型如下所示:
int gpio_direction_input(unsigned gpio) 函数参数和返回值含义如下: gpio:要设置为输入的 GPIO 标号。 返回值:0,设置成功;负值,设置失败。
3.4、gpio_direction_output 函数
此函数用于设置某个 GPIO 为输出,并且设置默认输出值,函数原型如下:
int gpio_direction_output(unsigned gpio, int value) 函数参数和返回值含义如下: gpio:要设置为输出的 GPIO 标号。 value:GPIO 默认输出值。 返回值:0,设置成功;负值,设置失败。
3.5、gpio_get_value 函数
此函数用于获取某个 GPIO 的值(0 或 1),此函数是个宏,定义所示:
#define gpio_get_value __gpio_get_value __
__int __gpio_get_value(unsigned gpio) 函数参数和返回值含义如下:
gpio:要获取的 GPIO 标号。 返回值:非负值,得到的 GPIO 值;负值,获取失败。
3.6 、gpio_set_value 函数
此函数用于设置某个 GPIO 的值,此函数是个宏,定义如下
#define gpio_set_value __gpio_set_value __
__void __gpio_set_value(unsigned gpio, int value)
函数参数和返回值含义如下: gpio:要设置的 GPIO 标号。 value:要设置的值。 返回值:无
3.7、与 gpio 相关的 OF 函数
3.7.1、of_gpio_named_count 函数
of_gpio_named_count 函数用于获取设备树某个属性里面定义了几个 GPIO 信息,要注意的 是空的 GPIO 信息也会被统计到,比如:
gpios = <0
&gpio1 1 2
0
&gpio2 3 4>;
上述代码的“gpios”节点一共定义了 4 个 GPIO,但是有 2 个是空的,没有实际的含义。 通过 of_gpio_named_count 函数统计出来的 GPIO 数量就是 4 个,此函数原型如下:
int of_gpio_named_count(struct device_node *np, const char *propname)
函数参数和返回值含义如下: np:设备节点。 propname:要统计的 GPIO 属性。 返回值:正值,统计到的 GPIO 数量;负值,失败。
3.7.2、of_gpio_count 函数
和 of_gpio_named_count 函数一样,但是不同的地方在于,此函数统计的是“gpios”这个属 性的 GPIO 数量,而 of_gpio_count 函数可以统计任意属性的 GPIO 信息,函数原型如下 所示:
int of_gpio_count(struct device_node *np) 函数参数和返回值含义如下: np:设备节点。 返回值:正值,统计到的 GPIO 数量;负值,失败。
3.7.3、of_get_named_gpio 函数
此函数获取 GPIO 编号,因为 Linux 内核中关于 GPIO 的 API 函数都要使用 GPIO 编号, 此函数会将设备树中类似<&gpio5 7 GPIO_ACTIVE_LOW>的属性信息转换为对应的 GPIO 编 号,此函数在驱动中使用很频繁!函数原型如下:
int of_get_named_gpio(struct device_node *np, const char *propname, int index)
函数参数和返回值含义如下: np:设备节点。 propname:包含要获取 GPIO 信息的属性名。index:GPIO 索引,因为一个属性里面可能包含多个 GPIO,此参数指定要获取哪个 GPIO 的编号,如果只有一个 GPIO 信息的话此参数为 0。 返回值:正值,获取到的 GPIO 编号;负值,失败。
三十三、Linux 蜂鸣器实验
三十三、Linux 并发与竞争
Linux是一个多任务操作系统,肯定会存在多个任务共同操作同一段内存或者设备的情况, 多个任务甚至中断都能访问的资源叫做共享资源,就和共享单车一样。在驱动开发中要注意对 共享资源的保护,也就是要处理对共享资源的并发访问。
1、并发与竞争
1.1、并发与竞争简介
Linux 系统是个多任务操作系统,会存在多个任务同时访问同一片内存区域,这些任务可 能会相互覆盖这段内存中的数据,造成内存数据混乱。针对这个问题必须要做处理,严重的话 可能会导致系统崩溃。现在的 Linux 系统并发产生的原因很复杂,总结一下有下面几个主要原 因:
①、多线程并发访问,Linux 是多任务(线程)的系统,所以多线程访问是最基本的原因。
②、抢占式并发访问,从 2.6 版本内核开始,Linux 内核支持抢占,也就是说调度程序可以 在任意时刻抢占正在运行的线程,从而运行其他的线程。
③、中断程序并发访问,这个无需多说,学过 STM32 的同学应该知道,硬件中断的权利可 是很大的。
④、SMP(多核)核间并发访问,现在 ARM 架构的多核 SOC 很常见,多核 CPU 存在核间并 发访问。
并发访问带来的问题就是竞争,学过FreeRTOS和UCOS的同学应该知道临界区这个概念, 所谓的临界区就是共享数据段,对于临界区必须保证一次只有一个线程访问,也就是要保证临 界区是原子访问的,注意这里的“原子”不是正点原子的“原子”。我们都知道,原子是化学反 应不可再分的基本微粒,这里的原子访问就表示这一个访问是一个步骤,不能再进行拆分。如 果多个线程同时操作临界区就表示存在竞争,我们在编写驱动的时候一定要注意避免并发和防 止竞争访问。
1.2、保护内容是什么
防止并发访问共享资源,换句话说就是要保护共享资源,防止进行并发访问。 那么问题来了,什么是共享资源?现实生活中的公共电话、共享单车这些是共享资源,我们都 很容易理解,那么在程序中什么是共享资源?也就是保护的内容是什么?我们保护的不是代码, 而是数据!某个线程的局部变量不需要保护,我们要保护的是多个线程都会访问的共享数据。
2、原子操作
原子操作就是指不能再进一步分割的操作,一般原子操作用于变量 或者位操作。假如现在要对无符号整形变量 a 赋值,值为 3,对于 C 语言来讲很简单,直接就 是: a = 3 但是 C 语言要先编译为成汇编指令,ARM 架构不支持直接对寄存器进行读写操作,比如 要借助寄存器 R0、R1 等来完成赋值操作。假设变量 a 的地址为 0X3000000,“a=3”这一行 C 语言可能会被编译为如下所示的汇编代码:
实际的结果要比示例代码复杂的多。从上述 代码可以看出,C 语言里面简简单单的一句“a=3”,编译成汇编文件以后变成了 3 句,那么程 序在执行的时候肯定是按照示例代码 47.2.1.1 中的汇编语句一条一条的执行。假设现在线程 A 要向 a 变量写入 10 这个值,而线程 B 也要向 a 变量写入 20 这个值,我们理想中的执行顺序如 图 47.2.1.1 所示:
但是实际上的执行流程可能如图 47.2.1.2 所示:
按照图 47.2.1.2 所示的流程,线程 A 最终将变量 a 设置为了 20,而并不是要求的 10!线程 B 没有问题。这就是一个最简单的设置变量值的并发与竞争的例子,要解决这个问题就要保证 示例代码 47.2.1.2 中的三行汇编指令作为一个整体运行,也就是作为一个原子存在。Linux 内核 提供了一组原子操作 API 函数来完成此功能,Linux 内核提供了两组原子操作 API 函数,一组 是对整形变量进行操作的,一组是对位进行操作的,我们接下来看一下这些 API 函数。
2.1 原子整形操作 API 函数
Linux 内核定义了叫做 atomic_t 的结构体来完成整形数据的原子操作,在使用中用原子变 量来代替整形变量,此结构体定义在 include/linux/types.h 文件中,定义如下:
如果要使用原子操作 API 函数,首先要先定义一个 atomic_t 的变量,如下所示:
atomic_t a; //定义 a
也可以在定义原子变量的时候给原子变量赋初值,如下所示:
atomic_t b = ATOMIC_INIT(0); //定义原子变量 b 并赋初值为 0
可以通过宏 ATOMIC_INIT 向原子变量赋初值。
原子变量有了,接下来就是对原子变量进行操作,比如读、写、增加、减少等等,Linux 内 核提供了大量的原子操作 API 函数,如表 47.2.2.1 所示:
如果使用 64 位的 SOC 的话,就要用到 64 位的原子变量,Linux 内核也定义了 64 位原子 结构体,如下所示:
Cortex-A7 是 32 位的架构,所 以本书中只使用表 47.2.2.1 中的 32 位原子操作函数。原子变量和相应的 API 函数使用起来很简 单,参考如下示例:
2.2、原子位操作 API 函数
位操作也是很常用的操作,Linux 内核也提供了一系列的原子位操作 API 函数,只不过原 子位操作不像原子整形变量那样有个 atomic_t 的数据结构,原子位操作是直接对内存进行操作, API 函数如表 47.2.3.1 所示:
3、自旋锁
3.1、自旋锁简介
原子操作只能对整形变量或者位进行保护,但是,在实际的使用环境中怎么可能只有整形 变量或位这么简单的临界区。举个最简单的例子,设备结构体变量就不是整型变量,我们对于 结构体中成员变量的操作也要保证原子性,在线程 A 对结构体变量使用期间,应该禁止其他的 线程来访问此结构体变量,这些工作原子操作都不能胜任,需要本节要讲的锁机制,在 Linux 内核中就是自旋锁。
当一个线程要访问某个共享资源的时候首先要先获取相应的锁,锁只能被一个线程持有, 只要此线程不释放持有的锁,那么其他的线程就不能获取此锁。对于自旋锁而言,如果自旋锁 正在被线程 A 持有,线程 B 想要获取自旋锁,那么线程 B 就会处于忙循环-旋转-等待状态,线 程 B 不会进入休眠状态或者说去做其他的处理,而是会一直傻傻的在那里“转圈圈”的等待锁 可用。比如现在有个公用电话亭,一次肯定只能进去一个人打电话,现在电话亭里面有人正在 打电话,相当于获得了自旋锁。此时你到了电话亭门口,因为里面有人,所以你不能进去打电 话,相当于没有获取自旋锁,这个时候你肯定是站在原地等待,你可能因为无聊的等待而转圈 圈消遣时光,反正就是哪里也不能去,要一直等到里面的人打完电话出来。终于,里面的人打 完电话出来了,相当于释放了自旋锁,这个时候你就可以使用电话亭打电话了,相当于获取到 了自旋锁。
自旋锁的“自旋”也就是“原地打转”的意思,“原地打转”的目的是为了等待自旋锁可以 用,可以访问共享资源。把自旋锁比作一个变量 a,变量 a=1 的时候表示共享资源可用,当 a=0 的时候表示共享资源不可用。现在线程 A 要访问共享资源,发现 a=0(自旋锁被其他线程持有), 那么线程 A 就会不断的查询 a 的值,直到 a=1。从这里我们可以看到自旋锁的一个缺点:那就 等待自旋锁的线程会一直处于自旋状态,这样会浪费处理器时间,降低系统性能,所以自旋锁 的持有时间不能太长。所以自旋锁适用于短时期的轻量级加锁,如果遇到需要长时间持有锁的 场景那就需要换其他的方法了,这个我们后面会讲解。
Linux 内核使用结构体 spinlock_t 表示自旋锁,结构体定义如下所示:
在使用自旋锁之前,肯定要先定义一个自旋锁变量,定义方法如下所示: spinlock_t lock; //定义自旋锁 定义好自旋锁变量以后就可以使用相应的 API 函数来操作自旋锁
3.2、 自旋锁 API 函数
表47.3.2.1中的自旋锁API函数适用于SMP或支持抢占的单CPU下线程之间的并发访问, 也就是用于线程与线程之间,被自旋锁保护的临界区一定不能调用任何能够引起睡眠和阻塞的 API 函数,否则的话会可能会导致死锁现象的发生。自旋锁会自动禁止抢占,也就说当线程 A 得到锁以后会暂时禁止内核抢占。如果线程 A 在持有锁期间进入了休眠状态,那么线程 A 会自 动放弃 CPU 使用权。线程 B 开始运行,线程 B 也想要获取锁,但是此时锁被 A 线程持有,而 且内核抢占还被禁止了!线程 B 无法被调度出去,那么线程 A 就无法运行,锁也就无法释放, 好了,死锁发生了!
表 47.3.2.1 中的 API 函数用于线程之间的并发访问,如果此时中断也要插一脚,中断也想 访问共享资源,那该怎么办呢?首先可以肯定的是,中断里面可以使用自旋锁,但是在中断里 面使用自旋锁的时候,在获取锁之前一定要先禁止本地中断(也就是本 CPU 中断,对于多核 SOC 来说会有多个 CPU 核),否则可能导致锁死现象的发生,如图 47.3.2.1 所示:
,线程 A 先运行,并且获取到了 lock 这个锁,当线程 A 运行 functionA 函 数的时候中断发生了,中断抢走了 CPU 使用权。右边的中断服务函数也要获取 lock 这个锁, 但是这个锁被线程 A 占有着,中断就会一直自旋,等待锁有效。但是在中断服务函数执行完之前,线程 A 是不可能执行的,线程 A 说“你先放手”,中断说“你先放手”,场面就这么僵持着, 死锁发生! 最好的解决方法就是获取锁之前关闭本地中断,Linux 内核提供了相应的 API 函数,如表 47.3.2.2 所示:
使用 spin_lock_irq/spin_unlock_irq 的时候需要用户能够确定加锁之前的中断状态,但实际 上内核很庞大,运行也是“千变万化”,我们是很难确定某个时刻的中断状态,因此不推荐使用 spin_lock_irq/spin_unlock_irq。建议使用 spin_lock_irqsave/spin_unlock_irqrestore,因为这一组函 数会保存中断状态,在释放锁的时候会恢复中断状态。一般在线程中使用 spin_lock_irqsave/ spin_unlock_irqrestore,在中断中使用 spin_lock/spin_unlock,示例代码如下所示:
下半部(BH)也会竞争共享资源,有些资料也会将下半部叫做底半部。关于下半部后面的 章节会讲解,如果要在下半部里面使用自旋锁,可以使用表 47.3.2.3 中的 API 函数:
3.3、其他类型的锁
在自旋锁的基础上还衍生出了其他特定场合使用的锁,这些锁在驱动中其实用的不多,更 多的是在 Linux 内核中使用,本节我们简单来了解一下这些衍生出来的锁。
3.3.1、读写自旋锁
现在有个学生信息表,此表存放着学生的年龄、家庭住址、班级等信息,此表可以随时被 修改和读取。此表肯定是数据,那么必须要对其进行保护,如果我们现在使用自旋锁对其进行 保护。每次只能一个读操作或者写操作,但是,实际上此表是可以并发读取的。只需要保证在 修改此表的时候没人读取,或者在其他人读取此表的时候没有人修改此表就行了。也就是此表 的读和写不能同时进行,但是可以多人并发的读取此表。像这样,当某个数据结构符合读/写或 生产者/消费者模型的时候就可以使用读写自旋锁。 读写自旋锁为读和写操作提供了不同的锁,一次只能允许一个写操作,也就是只能一个线 程持有写锁,而且不能进行读操作。但是当没有写操作的时候允许一个或多个线程持有读锁, 可以进行并发的读操作。Linux 内核使用 rwlock_t 结构体表示读写锁,结构体定义如下(删除了 条件编译):
读写锁操作 API 函数分为两部分,一个是给读使用的,一个是给写使用的,这些 API 函数 如表 47.3.3.1 所示:
3.3.2、顺序锁
顺序锁在读写锁的基础上衍生而来的,使用读写锁的时候读操作和写操作不能同时进行。 使用顺序锁的话可以允许在写的时候进行读操作,也就是实现同时读写,但是不允许同时进行 并发的写操作。虽然顺序锁的读和写操作可以同时进行,但是如果在读的过程中发生了写操作, 最好重新进行读取,保证数据完整性。顺序锁保护的资源不能是指针,因为如果在写操作的时 候可能会导致指针无效,而这个时候恰巧有读操作访问指针的话就可能导致意外发生,比如读 取野指针导致系统崩溃。Linux 内核使用 seqlock_t 结构体表示顺序锁,结构体定义如下:
3.3.3、自旋锁使用注意事项
①、因为在等待自旋锁的时候处于“自旋”状态,因此锁的持有时间不能太长,一定要 短,否则的话会降低系统性能。如果临界区比较大,运行时间比较长的话要选择其他的并发处 理方式,比如稍后要讲的信号量和互斥体。 ②、自旋锁保护的临界区内不能调用任何可能导致线程休眠的 API 函数,否则的话可能 导致死锁。 ③、不能递归申请自旋锁,因为一旦通过递归的方式申请一个你正在持有的锁,那么你就 必须“自旋”,等待锁被释放,然而你正处于“自旋”状态,根本没法释放锁。结果就是自己 把自己锁死了! ④、在编写驱动程序的时候我们必须考虑到驱动的可移植性,因此不管你用的是单核的还 是多核的 SOC,都将其当做多核 SOC 来编写驱动程序。
3.4、信号量
3.4.1、信号量简介
信号量是同步 的一种方式。Linux 内核也提供了信号量机制,信号量常常用于控制对共享资源的访问。举一个 很常见的例子,某个停车场有 100 个停车位,这 100 个停车位大家都可以用,对于大家来说这 100 个停车位就是共享资源。假设现在这个停车场正常运行,你要把车停到这个这个停车场肯 定要先看一下现在停了多少车了?还有没有停车位?当前停车数量就是一个信号量,具体的停 车数量就是这个信号量值,当这个值到 100 的时候说明停车场满了。停车场满的时你可以等一 会看看有没有其他的车开出停车场,当有车开出停车场的时候停车数量就会减一,也就是说信 号量减一,此时你就可以把车停进去了,你把车停进去以后停车数量就会加一,也就是信号量 加一。这就是一个典型的使用信号量进行共享资源管理的案例,在这个案例中使用的就是计数 型信号量。
相比于自旋锁,信号量可以使线程进入休眠状态,比如 A 与 B、C 合租了一套房子,这个 房子只有一个厕所,一次只能一个人使用。某一天早上 A 去上厕所了,过了一会 B 也想用厕 所,因为 A 在厕所里面,所以 B 只能等到 A 用来了才能进去。B 要么就一直在厕所门口等着, 等 A 出来,这个时候就相当于自旋锁。B 也可以告诉 A,让 A 出来以后通知他一下,然后 B 继 续回房间睡觉,这个时候相当于信号量。可以看出,使用信号量会提高处理器的使用效率,毕 竟不用一直傻乎乎的在那里“自旋”等待。但是,信号量的开销要比自旋锁大,因为信号量使 线程进入休眠状态以后会切换线程,切换线程就会有开销。总结一下信号量的特点:
①、因为信号量可以使等待资源线程进入休眠状态,因此适用于那些占用资源比较久的场 合。 ②、因此信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠。 ③、如果共享资源的持有时间比较短,那就不适合使用信号量了,因为频繁的休眠、切换 线程引起的开销要远大于信号量带来的那点优势。
3.4.2、信号量API函数
Linux 内核使用 semaphore 结构体表示信号量,结构体内容如下所示:
要想使用信号量就得先定义,然后初始化信号量。有关信号量的 API 函数如表 47.4.2.1 所 示:
3.5、互斥体
3.5.1、互斥体简介
在 FreeRTOS 和 UCOS 中也有互斥体,将信号量的值设置为 1 就可以使用信号量进行互斥 访问了,虽然可以通过信号量实现互斥,但是 Linux 提供了一个比信号量更专业的机制来进行 互斥,它就是互斥体—mutex。互斥访问表示一次只有一个线程可以访问共享资源,不能递归申 请互斥体。在我们编写 Linux 驱动的时候遇到需要互斥访问的地方建议使用 mutex。Linux 内核使用 mutex 结构体表示互斥体,定义如下(省略条件编译部分):
在使用 mutex 之前要先定义一个 mutex 变量。在使用 mutex 的时候要注意如下几点: ①、mutex 可以导致休眠,因此不能在中断中使用 mutex,中断中只能使用自旋锁。 ②、和信号量一样,mutex 保护的临界区可以调用引起阻塞的 API 函数。 ③、因为一次只有一个线程可以持有 mutex,因此,必须由 mutex 的持有者释放 mutex。并 且 mutex 不能递归上锁和解锁。
3.5.2、互斥体API函数
三十四、Linux内核定时器实验
1、Linux 时间管理和内核定时器简介
1.1、内核时间管理简介
UCOS 或 FreeRTOS 是需要一个硬件定时器 提供系统时钟,一般使用 Systick 作为系统时钟源。同理,Linux 要运行,也是需要一个系统时 钟的,至于这个系统时钟是由哪个定时器提供的,笔者没有去研究过 Linux 内核,但是在 CortexA7 内核中有个通用定时器。
Linux 内核中有大量的函数需要时间管理,比如周期性的调度程序、延时程序、对于我们驱 动编写者来说最常用的定时器。硬件定时器提供时钟源,时钟源的频率可以设置, 设置好以后 就周期性的产生定时中断,系统使用定时中断来计时。中断周期性产生的频率就是系统频率, 也叫做节拍率(tick rate)(有的资料也叫系统频率),比如 1000Hz,100Hz 等等说的就是系统节拍 率。系统节拍率是可以设置的,单位是 Hz,我们在编译 Linux 内核的时候可以通过图形化界面 设置系统节拍率,按照如下路径打开配置界面:
高节拍率和低节 拍率的优缺点:
①、高节拍率会提高系统时间精度,如果采用 100Hz 的节拍率,时间精度就是 10ms,采用 1000Hz 的话时间精度就是 1ms,精度提高了 10 倍。高精度时钟的好处有很多,对于那些对时 间要求严格的函数来说,能够以更高的精度运行,时间测量也更加准确。
②、高节拍率会导致中断的产生更加频繁,频繁的中断会加剧系统的负担,1000Hz 和 100Hz 的系统节拍率相比,系统要花费 10 倍的“精力”去处理中断。中断服务函数占用处理器的时间 增加,但是现在的处理器性能都很强大,所以采用 1000Hz 的系统节拍率并不会增加太大的负 载压力。根据自己的实际情况,选择合适的系统节拍率,本教程我们全部采用默认的 100Hz 系 统节拍率。
Linux 内核使用全局变量 jiffies 来记录系统从启动以来的系统节拍数,系统启动的时候会 将 jiffies 初始化为 0,jiffies 定义在文件 include/linux/jiffies.h 中,定义如下:
jiffies_64 和 jiffies 其实是同一个东西,jiffies_64 用于 64 位系统,而 jiffies 用于 32 位系统。 为了兼容不同的硬件,jiffies 其实就是 jiffies_64 的低 32 位,jiffies_64 和 jiffies 的结构如图 50.1.1.3 所示:
当我们访问 jiffies 的时候其实访问的是 jiffies_64 的低 32 位,使用 get_jiffies_64 这个函数 可以获取 jiffies_64 的值。在 32 位的系统上读取 jiffies 的值,在 64 位的系统上 jiffes 和 jiffies_64 表示同一个变量,因此也可以直接读取 jiffies 的值。所以不管是 32 位的系统还是 64 位系统, 都可以使用 jiffies。
HZ 表示每秒的节拍数,jiffies 表示系统运行的 jiffies 节拍数,所以 jiffies/HZ 就 是系统运行时间,单位为秒。不管是 32 位还是 64 位的 jiffies,都有溢出的风险,溢出以后会重 新从 0 开始计数,相当于绕回来了,因此有些资料也将这个现象也叫做绕回。假如 HZ 为最大 值 1000 的时候,32 位的 jiffies 只需要 49.7 天就发生了绕回,对于 64 位的 jiffies 来说大概需要 5.8 亿年才能绕回,因此 jiffies_64 的绕回忽略不计。处理 32 位 jiffies 的绕回显得尤为重要, Linux 内核提供了如表 50.1.1.1 所示的几个 API 函数来处理绕回。
如果 unkown 超过 known 的话,time_after 函数返回真,否则返回假。如果 unkown 没有超 过 known 的话 time_before 函数返回真,否则返回假。time_after_eq 函数和 time_after 函数类似, 只是多了判断等于这个条件。同理,time_before_eq 函数和 time_before 函数也类似。比如我们 要判断某段代码执行时间有没有超时,此时就可以使用如下所示代码:
timeout 就是超时时间点,比如我们要判断代码执行时间是不是超过了 2 秒,那么超时时间 点就是 jiffies+(2*HZ),如果 jiffies 大于 timeout 那就表示超时了,否则就是没有超时。第 4~6 行就是具体的代码段。第 9 行通过函数 time_before 来判断 jiffies 是否小于 timeout,如果小于的话 就表示没有超时。 为了方便开发,Linux 内核提供了几个 jiffies 和 ms、us、ns 之间的转换函数,如表 50.1.1.2 所示:
1.2、内核定时器简介
Linux 内核定时器 采用系统时钟来实现,并不是我们在裸机篇中讲解的 PIT 等硬件定时器。Linux 内核定时器使 用很简单,只需要提供超时时间(相当于定时值)和定时处理函数即可,当超时时间到了以后设 置的定时处理函数就会执行,和我们使用硬件定时器的套路一样,只是使用内核定时器不需要 做一大堆的寄存器初始化工作。在使用内核定时器的时候要注意一点,内核定时器并不是周期 性运行的,超时以后就会自动关闭,因此如果想要实现周期性定时,那么就需要在定时处理函 数中重新开启定时器。Linux 内核使用 timer_list 结构体表示内核定时器,timer_list 定义在文件 include/linux/timer.h 中,定义如下(省略掉条件编译):
要使用内核定时器首先要先定义一个 timer_list 变量,表示定时器,tiemr_list 结构体的 expires 成员变量表示超时时间,单位为节拍数。比如我们现在需要定义一个周期为 2 秒的定时 器,那么这个定时器的超时时间就是 jiffies+(2HZ),因此 expires=jiffies+(2HZ)。function 就是 定时器超时以后的定时处理函数,我们要做的工作就放到这个函数里面,需要我们编写这个定 时处理函数。 定义好定时器以后还需要通过一系列的 API 函数来初始化此定时器,这些函数如下:
1.2.1、init_timer 函数
init_timer 函数负责初始化 timer_list 类型变量,当我们定义了一个 timer_list 变量以后一定 要先用 init_timer 初始化一下。init_timer 函数原型如下:
void init_timer(struct timer_list *timer)
函数参数和返回值含义如下: timer:要初始化定时器。 返回值:没有返回值。
1.2.2、add_timer 函数
add_timer 函数用于向 Linux 内核注册定时器,使用 add_timer 函数向内核注册定时器以后, 定时器就会开始运行,函数原型如下:
void add_timer(struct timer_list *timer) 函数参数和返回值含义如下: timer:要注册的定时器。 返回值:没有返回值。
1.2.3、del_timer 函数
del_timer 函数用于删除一个定时器,不管定时器有没有被激活,都可以使用此函数删除。 在多处理器系统上,定时器可能会在其他的处理器上运行,因此在调用 del_timer 函数删除定时 器之前要先等待其他处理器的定时处理器函数退出。del_timer 函数原型如下:
int del_timer(struct timer_list * timer) 函数参数和返回值含义如下: timer:要删除的定时器。 返回值:0,定时器还没被激活;1,定时器已经激活。
1.2.4、del_timer_sync 函数
del_timer_sync 函数是 del_timer 函数的同步版,会等待其他处理器使用完定时器再删除, del_timer_sync 不能使用在中断上下文中。del_timer_sync 函数原型如下所示:
int del_timer_sync(struct timer_list *timer) 函数参数和返回值含义如下: timer:要删除的定时器。 返回值:0,定时器还没被激活;1,定时器已经激活。
1.2.5、mod_timer 函数
mod_timer 函数用于修改定时值,如果定时器还没有激活的话,mod_timer 函数会激活定时 器!函数原型如下:
int mod_timer(struct timer_list *timer, unsigned long expires) 函数参数和返回值含义如下: timer:要修改超时时间(定时值)的定时器。 expires:修改后的超时时间。 返回值:0,调用 mod_timer 函数前定时器未被激活;1,调用 mod_timer 函数前定时器已 被激活。
1.3、Linux 内核短延时函数
有时候我们需要在内核中实现短延时,尤其是在 Linux 驱动中。Linux 内核提供了毫秒、微 秒和纳秒延时函数,这三个函数如表 50.1.3.1 所示:
三十五、Linux 中断实验
1、 Linux 中断简介
1.1 、Linux 中断 API 函数
先来回顾一下裸机实验里面中断的处理方法: ①、使能中断,初始化相应的寄存器。 ②、注册中断服务函数,也就是向 irqTable 数组的指定标号处写入中断服务函数 ②、中断发生以后进入 IRQ 中断服务函数,在 IRQ 中断服务函数在数组 irqTable 里面查找 具体的中断处理函数,找到以后执行相应的中断处理函数。
1.1.1、中断号
每个中断都有一个中断号,通过中断号即可区分不同的中断,有的资料也把中断号叫做中 断线。在 Linux 内核中使用一个 int 变量表示中断号。
1.1.2、request_irq 函数
在 Linux 内核中要想使用某个中断是需要申请的,request_irq 函数用于申请中断,request_irq 函数可能会导致睡眠,因此不能在中断上下文或者其他禁止睡眠的代码段中使用 request_irq 函 数。request_irq 函数会激活(使能)中断,所以不需要我们手动去使能中断,request_irq 函数原型 如下:
函数参数和返回值含义如下: irq:要申请中断的中断号。 handler:中断处理函数,当中断发生以后就会执行此中断处理函数。 flags:中断标志,可以在文件 include/linux/interrupt.h 里面查看所有的中断标志,这里我们 介绍几个常用的中断标志,如表 51.1.1.1 所示:
比如 I.MX6U-ALPHA 开发板上的 KEY0 使用 GPIO1_IO18,按下 KEY0 以后为低电平,因 此可以设置为下降沿触发,也就是将 flags 设置为 IRQF_TRIGGER_FALLING。表 51.1.1.1 中的 这些标志可以通过“|”来实现多种组合。 name:中断名字,设置以后可以在/proc/interrupts 文件中看到对应的中断名字。 dev:如果将 flags 设置为 IRQF_SHARED 的话,dev 用来区分不同的中断,一般情况下将 dev 设置为设备结构体,dev 会传递给中断处理函数 irq_handler_t 的第二个参数。 返回值:0 中断申请成功,其他负值 中断申请失败,如果返回-EBUSY 的话表示中断已经 被申请了。
1.1.3、free_irq 函数
使用中断的时候需要通过 request_irq 函数申请,使用完成以后就要通过 free_irq 函数释放 掉相应的中断。如果中断不是共享的,那么 free_irq 会删除中断处理函数并且禁止中断。free_irq 函数原型如下所示:
函数参数和返回值含义如下: irq:要释放的中断。 dev:如果中断设置为共享(IRQF_SHARED)的话,此参数用来区分具体的中断。共享中断 只有在释放最后中断处理函数的时候才会被禁止掉。 返回值:无。
1.1.4、中断处理函数
使用 request_irq 函数申请中断的时候需要设置中断处理函数,中断处理函数格式如下所示: irqreturn_t (*irq_handler_t) (int, void *) 第一个参数是要中断处理函数要相应的中断号。第二个参数是一个指向 void 的指针,也就 是个通用指针,需要与 request_irq 函数的 dev 参数保持一致。用于区分共享中断的不同设备, dev 也可以指向设备数据结构。中断处理函数的返回值为 irqreturn_t 类型,irqreturn_t 类型定义 如下所示:
可以看出 irqreturn_t 是个枚举类型,一共有三种返回值。一般中断服务函数返回值使用如 下形式: return IRQ_RETVAL(IRQ_HANDLED)
1.1.5、中断使能与禁止函数
常用的中断使用和禁止函数如下所示:
void enable_irq(unsigned int irq)
void disable_irq(unsigned int irq)
enable_irq 和 disable_irq 用于使能和禁止指定的中断,irq 就是要禁止的中断号。disable_irq函数要等到当前正在执行的中断处理函数执行完才返回,因此使用者需要保证不会产生新的中 断,并且确保所有已经开始执行的中断处理程序已经全部退出。在这种情况下,可以使用另外 一个中断禁止函数:
void disable_irq_nosync(unsigned int irq)
disable_irq_nosync 函数调用以后立即返回,不会等待当前中断处理程序执行完毕。上面三 个函数都是使能或者禁止某一个中断,有时候我们需要关闭当前处理器的整个中断系统,也就 是在学习 STM32 的时候常说的关闭全局中断,这个时候可以使用如下两个函数:
local_irq_enable()
local_irq_disable()
local_irq_enable 用于使能当前处理器中断系统,local_irq_disable 用于禁止当前处理器中断 系统。假如 A 任务调用 local_irq_disable 关闭全局中断 10S,当关闭了 2S 的时候 B 任务开始运 行,B 任务也调用 local_irq_disable 关闭全局中断 3S,3 秒以后 B 任务调用 local_irq_enable 函 数将全局中断打开了。此时才过去 2+3=5 秒的时间,然后全局中断就被打开了,此时 A 任务要 关闭 10S 全局中断的愿望就破灭了,然后 A 任务就“生气了”,结果很严重,可能系统都要被 A 任务整崩溃。为了解决这个问题,B 任务不能直接简单粗暴的通过 local_irq_enable 函数来打 开全局中断,而是将中断状态恢复到以前的状态,要考虑到别的任务的感受,此时就要用到下 面两个函数:
local_irq_save(flags)
local_irq_restore(flags)
这两个函数是一对,local_irq_save 函数用于禁止中断,并且将中断状态保存在 flags 中。 local_irq_restore 用于恢复中断,将中断到 flags 状态。
1.2、上半部与下半部
在有些资料中也将上半部和下半部称为顶半部和底半部,都是一个意思。我们在使用 request_irq 申请中断的时候注册的中断服务函数属于中断处理的上半部,只要中断触发,那么 中断处理函数就会执行。我们都知道中断处理函数一定要快点执行完毕,越短越好,但是现实 往往是残酷的,有些中断处理过程就是比较费时间,我们必须要对其进行处理,缩小中断处理 函数的执行时间。比如电容触摸屏通过中断通知 SOC 有触摸事件发生,SOC 响应中断,然后 通过 IIC 接口读取触摸坐标值并将其上报给系统。但是我们都知道 IIC 的速度最高也只有 400Kbit/S,所以在中断中通过 IIC 读取数据就会浪费时间。我们可以将通过 IIC 读取触摸数据 的操作暂后执行,中断处理函数仅仅相应中断,然后清除中断标志位即可。这个时候中断处理 过程就分为了两部分:
上半部:上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可 以放在上半部完成。
下半部:如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部 去执行,这样中断处理函数就会快进快出。
Linux 内核将中断分为上半部和下半部的主要目的就是实现中断处理函数的快进快 出,那些对时间敏感、执行速度快的操作可以放到中断处理函数中,也就是上半部。剩下的所 有工作都可以放到下半部去执行,比如在上半部将数据拷贝到内存中,关于数据的具体处理就 可以放到下半部去执行。至于哪些代码属于上半部,哪些代码属于下半部并没有明确的规定, 一切根据实际使用情况去判断,这个就很考验驱动编写人员的功底了。这里有一些可以借鉴的 参考点:
①、如果要处理的内容不希望被其他中断打断,那么可以放到上半部。
②、如果要处理的任务对时间敏感,可以放到上半部。
③、如果要处理的任务与硬件有关,可以放到上半部
④、除了上述三点以外的其他任务,优先考虑放到下半部。
1.2.1、软中断
一开始 Linux 内核提供了“bottom half”机制来实现下半部,简称“BH”。后面引入了软中 断和 tasklet 来替代“BH”机制,完全可以使用软中断和 tasklet 来替代 BH,从 2.5 版本的 Linux 内核开始 BH 已经被抛弃了。Linux 内核使用结构体 softirq_action 表示软中断, softirq_action 结构体定义在文件 include/linux/interrupt.h 中,内容如下:
在 kernel/softirq.c 文件中一共定义了 10 个软中断
static struct softirq_action softirq_vec[NR_SOFTIRQS];
NR_SOFTIRQS 是枚举类型,定义在文件 include/linux/interrupt.h 中,定义如下:
一共有 10 个软中断,因此 NR_SOFTIRQS 为 10,因此数组 softirq_vec 有 10 个 元素。softirq_action 结构体中的 action 成员变量就是软中断的服务函数,数组 softirq_vec 是个 全局数组,因此所有的 CPU(对于 SMP 系统而言)都可以访问到,每个 CPU 都有自己的触发和 控制机制,并且只执行自己所触发的软中断。但是各个 CPU 所执行的软中断服务函数确是相同 的,都是数组 softirq_vec 中定义的 action 函数。要使用软中断,必须先使用 open_softirq 函数注 册对应的软中断处理函数,open_softirq 函数原型如下:
void open_softirq(int nr, void (*action)(struct softirq_action *))
nr:要开启的软中断,在示例代码 51.1.2.3 中选择一个。 action:软中断对应的处理函数。 返回值:没有返回值。
注册好软中断以后需要通过 raise_softirq 函数触发,raise_softirq 函数原型如下:
void raise_softirq(unsigned int nr)
nr:要触发的软中断,在示例代码 51.1.2.3 中选择一个。 返回值:没有返回值。 软中断必须在编译的时候静态注册!Linux 内核使用 softirq_init 函数初始化软中断, softirq_init 函数定义在 kernel/softirq.c 文件里面,函数内容如下:
从示例代码 51.1.2.4 可以看出,softirq_init 函数默认会打开 TASKLET_SOFTIRQ 和 HI_SOFTIRQ。
1.2.2、tasklet
tasklet 是利用软中断来实现的另外一种下半部机制,在软中断和 tasklet 之间,建议大家使 用 tasklet。Linux 内核使用 tasklet_struct 结构体来表示 tasklet:
第 489 行的 func 函数就是 tasklet 要执行的处理函数,用户定义函数内容,相当于中断处理 函数。如果要使用 tasklet,必须先定义一个 tasklet,然后使用 tasklet_init 函数初始化 tasklet, taskled_init 函数原型如下:
void tasklet_init(struct tasklet_struct * *t,void (**func)(unsigned long), unsigned long data);
t:要初始化的 tasklet func:tasklet 的处理函数。 data:要传递给 func 函数的参数
也 可 以 使 用 宏 DECLARE_TASKLET 来 一 次 性 完 成 tasklet 的 定 义 和 初 始 化 , DECLARE_TASKLET 定义在 include/linux/interrupt.h 文件中,定义如下:
DECLARE_TASKLET(name, func, data)
其中 name 为要定义的 tasklet 名字,这个名字就是一个 tasklet_struct 类型的时候变量,func 就是 tasklet 的处理函数,data 是传递给 func 函数的参数。
在上半部,也就是中断处理函数中调用 tasklet_schedule 函数就能使 tasklet 在合适的时间运 行,tasklet_schedule 函数原型如下
void tasklet_schedule(struct tasklet_struct *t)
t:要调度的 tasklet,也就是 DECLARE_TASKLET 宏里面的 name。
1.2.3、工作队列
工作队列是另外一种下半部执行方式,工作队列在进程上下文执行,工作队列将要推后的 工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重 新调度。因此如果你要推后的工作可以睡眠那么就可以选择工作队列,否则的话就只能选择软 中断或 tasklet。
Linux 内核使用 work_struct 结构体表示一个工作,内容如下(省略掉条件编译)
这些工作组织成工作队列,工作队列使用 workqueue_struct 结构体表示,内容如下(省略掉 条件编译):
Linux 内核使用工作者线程(worker thread)来处理工作队列中的各个工作,Linux 内核使用 worker 结构体表示工作者线程,worker 结构体内容如下:
每个 worker 都有一个工作队列,工作者线程处理自己工 作队列中的所有工作。在实际的驱动开发中,我们只需要定义工作(work_struct)即可,关于工作 队列和工作者线程我们基本不用去管。简单创建工作很简单,直接定义一个 work_struct 结构体 变量即可,然后使用 INIT_WORK 宏来初始化工作,INIT_WORK 宏定义如下:
#define INIT_WORK(_work, _func) _
__work 表示要初始化的工作,_func 是工作对应的处理函数。
也可以使用 DECLARE_WORK 宏一次性完成工作的创建和初始化,宏定义如下:
#define DECLARE_WORK(n, f)
n 表示定义的工作(work_struct),f 表示工作对应的处理函数。 和 tasklet 一样,工作也是需要调度才能运行的,工作的调度函数为 schedule_work,函数原 型如下所示:
bool schedule_work(struct work_struct *work) 函数参数和返回值含义如下:
work:要调度的工作。 返回值:0 成功,其他值 失败。
1.3、设备树中断信息节点
如果使用设备树的话就需要在设备树中设置好中断属性信息,Linux 内核通过读取设备树 中的中断属性信息来配置中断。对于中断控制器而言,设备树绑定信息参考文档 Documentation/devicetree/bindings/arm/gic.txt。打开 imx6ull.dtsi 文件,其中的 intc 节点就是 I.MX6ULL 的中断控制器节点,节点内容如下所示:
第 2 行,compatible 属性值为“arm,cortex-a7-gic”在 Linux 内核源码中搜索“arm,cortex-a7- gic”即可找到 GIC 中断控制器驱动文件。 第 3 行,#interrupt-cells 和#address-cells、#size-cells 一样。表示此中断控制器下设备的 cells 大小,对于设备而言,会使用 interrupts 属性描述中断信息,#interrupt-cells 描述了 interrupts 属性的 cells 大小,也就是一条信息有几个 cells。每个 cells 都是 32 位整形值,对于 ARM 处理的 GIC 来说,一共有 3 个 cells,这三个 cells 的含义如下:第一个 cells:中断类型,0 表示 SPI 中断,1 表示 PPI 中断。 第二个 cells:中断号,对于 SPI 中断来说中断号的范围为 0~987,对于 PPI 中断来说中断 号的范围为 0~15。 第三个 cells:标志,bit[3:0]表示中断触发类型,为 1 的时候表示上升沿触发,为 2 的时候 表示下降沿触发,为 4 的时候表示高电平触发,为 8 的时候表示低电平触发。bit[15:8]为 PPI 中 断的 CPU 掩码。
第 4 行,interrupt-controller 节点为空,表示当前节点是中断控制器。
对于 gpio 来说,gpio 节点也可以作为中断控制器,比如 imx6ull.dtsi 文件中的 gpio5 节点内 容如下所示:
第 4 行,interrupts 描述中断源信息,对于 gpio5 来说一共有两条信息,中断类型都是 SPI, 触发电平都是 IRQ_TYPE_LEVEL_HIGH。不同之处在于中断源,一个是 74,一个是 75,打开 可以打开《IMX6ULL 参考手册》的“Chapter 3 Interrupts and DMA Events”章节,找到表 3-1, 有如图 50.1.3.1 所示的内容:
从图 50.1.3.1 可以看出,GPIO5 一共用了 2 个中断号,一个是 74,一个是 75。其中 74 对 应 GPIO5_IO00~GPIO5_IO15 这低 16 个 IO,75 对应 GPIO5_IO16~GPIOI5_IO31 这高 16 位 IO。 第 8 行,interrupt-controller 表明了 gpio5 节点也是个中断控制器,用于控制 gpio5 所有 IO 的中断。 第 9 行,将#interrupt-cells 修改为 2。
打开 imx6ull-alientek-emmc.dts 文件,找到如下所示内容:
fxls8471 是 NXP 官方的 6ULL 开发板上的一个磁力计芯片,fxls8471 有一个中断引脚链接 到了 I.MX6ULL 的 SNVS_TAMPER0 因脚上,这个引脚可以复用为 GPIO5_IO00。 第 5 行,interrupt-parent 属性设置中断控制器,这里使用 gpio5 作为中断控制器。 第 6 行,interrupts 设置中断信息,0 表示 GPIO5_IO00,8 表示低电平触发。
与中断有关的设备树属性信息:
①、#interrupt-cells,指定中断源的信息 cells 个数。
②、interrupt-controller,表示当前节点为中断控制器。
③、interrupts,指定中断号,触发方式等。
④、interrupt-parent,指定父中断,也就是中断控制器。
1.4、获取中断号
编写驱动的时候需要用到中断号,我们用到中断号,中断信息已经写到了设备树里面,因 此可以通过 irq_of_parse_and_map 函数从 interupts 属性中提取到对应的设备号,函数原型如下:
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
函数参数和返回值含义如下: dev:设备节点。 index:索引号,interrupts 属性可能包含多条中断信息,通过 index 指定要获取的信息。 返回值:中断号。
如果使用 GPIO 的话,可以使用 gpio_to_irq 函数来获取 gpio 对应的中断号,函数原型如 下:
int gpio_to_irq(unsigned int gpio) 函数参数和返回值含义如下: gpio:要获取的 GPIO 编号。 返回值:GPIO 对应的中断号
三十六、Linux 阻塞和非阻塞 IO 实验
阻塞和非阻塞 IO 是 Linux 驱动开发里面很常见的两种设备访问模式,在编写驱动的时候 一定要考虑到阻塞和非阻塞。
1、 阻塞和非阻塞 IO
1.1 、阻塞和非阻塞简介
这里的“IO”并不是我们学习 STM32 或者其他单片机的时候所说的“GPIO”(也就是引脚)。 这里的 IO 指的是 Input/Output,也就是输入/输出,是应用程序对驱动设备的输入/输出操作。当 应用程序对设备驱动进行操作的时候,如果不能获取到设备资源,那么阻塞式 IO 就会将应用程 序对应的线程挂起,直到设备资源可以获取为止。对于非阻塞 IO,应用程序对应的线程不会挂 起,它要么一直轮询等待,直到设备资源可以使用,要么就直接放弃。阻塞式 IO 如图 52.1.1.1 所示:
图 52.1.1.1 中应用程序调用 read 函数从设备中读取数据,当设备不可用或数据未准备好的 时候就会进入到休眠态。等设备可用的时候就会从休眠态唤醒,然后从设备中读取数据返回给 应用程序。非阻塞 IO 如图 52.1.2 所示:
从图 52.1.1.2 可以看出,应用程序使用非阻塞访问方式从设备读取数据,当设备不可用或 数据未准备好的时候会立即向内核返回一个错误码,表示数据读取失败。应用程序会再次重新 读取数据,这样一直往复循环,直到数据读取成功。
应用程序可以使用如下所示示例代码来实现阻塞访问:
从示例代码 52.1.1.1 可以看出,对于设备驱动文件的默认读取方式就是阻塞式的,所以我 们前面所有的例程测试 APP 都是采用阻塞 IO。
第 4 行使用 open 函数打开“/dev/xxx_dev”设备文件的时候添加了参数“O_NONBLOCK”, 表示以非阻塞方式打开设备,这样从设备中读取数据的时候就是非阻塞方式的了。
1.2 、等待队列
1.2.1、等待队列头
1、等待队列头 阻塞访问最大的好处就是当设备文件不可操作的时候进程可以进入休眠态,这样可以将 CPU 资源让出来。但是,当设备文件可以操作的时候就必须唤醒进程,一般在中断函数里面完 成唤醒工作。Linux 内核提供了等待队列(wait queue)来实现阻塞进程的唤醒工作,如果我们要 在驱动中使用等待队列,必须创建并初始化一个等待队列头,等待队列头使用结构体 wait_queue_head_t 表示,wait_queue_head_t 结构体定义在文件 include/linux/wait.h 中,结构体内 容如下所示:
定义好等待队列头以后需要初始化,使用 init_waitqueue_head 函数初始化等待队列头,函 数原型如下:
void init_waitqueue_head(wait_queue_head_t *q)
参数 q 就是要初始化的等待队列头。 也可以使用宏 DECLARE_WAIT_QUEUE_HEAD 来一次性完成等待队列头的定义的初始 化。
1.2.2、等待队列项
等待队列头就是一个等待队列的头部,每个访问设备的进程都是一个队列项,当设备不可 用的时候就要将这些进程对应的等待队列项添加到等待队列里面。结构体 wait_queue_t 表示等 待队列项,结构体内容如下:
使用宏 DECLARE_WAITQUEUE 定义并初始化一个等待队列项,宏的内容如下:
DECLARE_WAITQUEUE(name, tsk)
name 就是等待队列项的名字,tsk 表示这个等待队列项属于哪个任务(进程),一般设置为 current , 在 Linux 内核中 current 相 当 于 一 个 全 局 变 量 , 表 示 当 前 进 程 。 因 此 宏 DECLARE_WAITQUEUE 就是给当前正在运行的进程创建并初始化了一个等待队列项。
1.2.3、将队列项添加/移除等待队列头
当设备不可访问的时候就需要将进程对应的等待队列项添加到前面创建的等待队列头中, 只有添加到等待队列头中以后进程才能进入休眠态。当设备可以访问以后再将进程对应的等待 队列项从等待队列头中移除即可,等待队列项添加 API 函数如下:
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
q:等待队列项要加入的等待队列头。 wait:要加入的等待队列项。 返回值:无。
等待队列项移除 API 函数如下:
void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
q:要删除的等待队列项所处的等待队列头。 wait:要删除的等待队列项。返回值:无。
1.2.4、等待唤醒
当设备可以使用的时候就要唤醒进入休眠态的进程,唤醒可以使用如下两个函数:
void wake_up(wait_queue_head_t *q)
void wake_up_interruptible(wait_queue_head_t *q)
参数 q 就是要唤醒的等待队列头,这两个函数会将这个等待队列头中的所有进程都唤醒。 wake_up 函数可以唤醒处于 TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 状态的进 程,而 wake_up_interruptible 函数只能唤醒处于 TASK_INTERRUPTIBLE 状态的进程。
1.2.5、等待事件
除了主动唤醒以外,也可以设置等待队列等待某个事件,当这个事件满足以后就自动唤醒 等待队列中的进程,和等待事件有关的 API 函数如表 52.1.2.1 所示:
1.3、轮询
如果用户应用程序以非阻塞的方式访问设备,设备驱动程序就要提供非阻塞的处理方式, 也就是轮询。poll、epoll 和 select 可以用于处理轮询,应用程序通过 select、epoll 或 poll 函数来 查询设备是否可以操作,如果可以操作的话就从设备读取或者向设备写入数据。当应用程序调 用 select、epoll 或 poll 函数的时候设备驱动程序中的 poll 函数就会执行,因此需要在设备驱动 程序中编写 poll 函数。我们先来看一下应用程序中使用的 select、poll 和 epoll 这三个函数。
1.3.1、select 函数
select 函数原型如下:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)
nfds:所要监视的这三类文件描述集合中,最大文件描述符加 1。
readfds、writefds 和 exceptfds:这三个指针指向描述符集合,这三个参数指明了关心哪些 描述符、需要满足哪些条件等等,这三个参数都是 fd_set 类型的,fd_set 类型变量的每一个位 都代表了一个文件描述符。readfds 用于监视指定描述符集的读变化,也就是监视这些文件是否 可以读取,只要这些集合里面有一个文件可以读取那么 seclect 就会返回一个大于 0 的值表示文 件可以读取。如果没有文件可以读取,那么就会根据 timeout 参数来判断是否超时。可以将 readfs 设置为 NULL,表示不关心任何文件的读变化。writefds 和 readfs 类似,只是 writefs 用于监视 这些文件是否可以进行写操作。exceptfds 用于监视这些文件的异常。
比如我们现在要从一个设备文件中读取数据,那么就可以定义一个 fd_set 变量,这个变量 要传递给参数 readfds。当我们定义好一个 fd_set 变量以后可以使用如下所示几个宏进行操作:
D_ZERO 用于将 fd_set 变量的所有位都清零,FD_SET 用于将 fd_set 变量的某个位置 1, 也就是向 fd_set 添加一个文件描述符,参数 fd 就是要加入的文件描述符。FD_CLR 用于将 fd_set变量的某个位清零,也就是将一个文件描述符从 fd_set 中删除,参数 fd 就是要删除的文件描述 符。FD_ISSET 用于测试一个文件是否属于某个集合,参数 fd 就是要判断的文件描述符。
timeout:超时时间,当我们调用 select 函数等待某些文件描述符可以设置超时时间,超时时 间使用结构体 timeval 表示,结构体定义如下所示:
返回值:0,表示的话就表示超时发生,但是没有任何文件描述符可以进行操作;-1,发生 错误;其他值,可以进行操作的文件描述符个数。
使用 select 函数对某个设备驱动文件进行读非阻塞访问的操作示例如下所示:
1.3.2、poll 函数
在单个线程中,select 函数能够监视的文件描述符数量有最大的限制,一般为 1024,可以 修改内核将监视的文件描述符数量改大,但是这样会降低效率!这个时候就可以使用 poll 函数, poll 函数本质上和 select 没有太大的差别,但是 poll 函数没有最大文件描述符限制,Linux 应用 程序中 poll 函数原型如下所示:
int poll(struct pollfd *fds, nfds_t nfds, int timeout)
fds:要监视的文件描述符集合以及要监视的事件,为一个数组,数组元素都是结构体 pollfd 类型的,pollfd 结构体如下所示:
fd 是要监视的文件描述符,如果 fd 无效的话那么 events 监视事件也就无效,并且 revents 返回 0。events 是要监视的事件,可监视的事件类型如下所示
revents 是返回参数,也就是返回的事件,由 Linux 内核设置具体的返回事件。 nfds:poll 函数要监视的文件描述符数量。 timeout:超时时间,单位为 ms。 返回值:返回 revents 域中不为 0 的 pollfd 结构体个数,也就是发生事件或错误的文件描述 符数量;0,超时;-1,发生错误,并且设置 errno 为错误类型。
使用 poll 函数对某个设备驱动文件进行读非阻塞访问的操作示例如下所示:
1.3.3、epoll 函数
传统的 selcet 和 poll 函数都会随着所监听的 fd 数量的增加,出现效率低下的问题,而且 poll 函数每次必须遍历所有的描述符来检查就绪的描述符,这个过程很浪费时间。为此,epoll 应运而生,epoll 就是为处理大并发而准备的,一般常常在网络编程中使用 epoll 函数。应用程 序需要先使用 epoll_create 函数创建一个 epoll 句柄,epoll_create 函数原型如下:
int epoll_create(int size)
函数参数和返回值含义如下: size:从 Linux2.6.8 开始此参数已经没有意义了,随便填写一个大于 0 的值就可以。 返回值:epoll 句柄,如果为-1 的话表示创建失败。 epoll 句柄创建成功以后使用 epoll_ctl 函数向其中添加要监视的文件描述符以及监视的事 件,epoll_ctl 函数原型如下所示:
in