IMX6ULL裸机开发大全

第一章:板上资源

第一章:板上资源

开发板资源介绍

开发板资源如图所示:

第二章:准备开发环境

第二章:准备开发环境

2.准备开发环境

2.1 100ASK_IMX6ULL开发板接线与启动

​ 在后面的操作里,都是通过串口与板子进行”交流”。串口是串行接口的简称,是指数据一位一位地顺序传送,其特点是通信线路简单。 ​ 在电脑上安装好MobaXterm后,使micro USB数据线,连接电脑和开发板上的6号接口(USB转串口)。

  1. 连接串口线和电源线

​ 首先如下图所示将串口线与电脑、板子连接,开发板插上电源。

​ 其中特别需要注意的几点: ​ a) 板子的启动选择拨到正确的启动方式,保证该启动方式里面有系统可以运行; ​ b) 板子如图所示插上配套的电源到电源接口,电源开关暂时不用打开;

  1. 安装USB串口驱动

​ 接好USB串口线后,Windows会自动安装驱动(安装可能比较慢,等一分钟左右)。打开电脑的“设备管理器”,在“端口 (COM和LPT)”项下,可以看到如下图中的“(COM21)”。这里的“COM21”可能与你电脑上的不一样,记住你电脑显示的数字。

​ 如果电脑没有显示出端口号,就需要手动安装驱动,从驱动精灵官网(www.drivergenius.com )下载一个驱动精灵,安装、运行、检测,会自动安装上串口驱动。

  1. 选择启动方式

​ 板子上的红色拨码开关用来设置启动方式,支持这3种方式:EMMC启动、SD卡启动、USB烧写。 ​ 板子背后画有一个表格,表示这3种方式如何设置。 ​ 表格如下:

BOOT CFG
BOOT SW1(LCD_DATA5) SW2(LCD_DATA11) SW3(BOOT_MODE0) SW4(BOOT_MODE1)
EMMC OFF OFF ON OFF
SD ON ON ON OFF
USB X X OFF ON

​ 这3种启动方式的设置示意图如下:

​ 要注意的是,设置为USB启动时,不能插上SD卡、TF卡。 ​ 刚出厂的板子在EMMC上烧写了系统,你可以设置为EMMC启动方式。

  1. 设置串口工具,启动开发板

如果是在windows下,则​ 打开MobaXterm,点击左上角的“Session”,在弹出的界面选中“Serial”,如下图所示选择端口号(前面设备管理器显示的端口号COM21)、波特率(Speed 115200)、流控(Flow Control: none),最后点击“OK”即可。

注意:流控(Flow Control)一定要选择none,否则你将无法在MobaXterm中向串口输入数据。

​ 随后显示一个黑色的窗口, 此时打开板子的电源开关,将收到板子串口发过来的数据,如下图所示。

2.2 安装SDK、设置工具链

​ 安装SDK、设置工具链 ​ 当你想开发应用程序、内核、驱动程序时,需要先安装、设置交叉编译工具链。 ​ 当你想更新内核时,当你想进行驱动开发时,都需要编译内核。 ​ 首先,安装VMWare、打开BSP包中的vmware Ubuntu映象,启动; ​ 然后,设置Ubuntu,让它能上网; ​ 最后,打开http://wiki.100ask.org/100ask_imx6ull ,参考下图章节安装SDK、设置工具链:

第三章:IMX6ULL启动流程

第三章:IMX6ULL启动流程

3.1 IMX6ULL启动方式

3.1 IMX6ULL启动方式

3.1.1 芯片手册讲解

​ IMX6ULL芯片内部有一个boot ROM,上电后boot ROM上的程序就会运行。它会根据BOOT_MODE[1:0]的值,以及eFUSE或GPIO的值决定后续的启动流程。 ​ 注:eFUSE即熔丝,只能烧写一次,一般正式发布产品时烧写最终值;平时调试时通过GPIO来设置开发板的启动方式。 ​ boot ROM上的程序功能强大,可以从USB口或串口下载程序并把它烧写到Flash等设备上,也可以从SD卡或EMMC、Flash等设备上读出程序、运行程序。

​ 问题来了:

​ ① boot ROM是从USB口下载、运行程序,还是从SD卡等设备上读出、运行程序,谁决定? ​ BOOT_MODE[1:0]的值来自于2个引脚BOOT_MODE1、BOOT_MODE0。这2个引脚在上电时是输入引脚,芯片启动后采集这2个引脚的值,存入BOOT_MODE寄存器。以后这2个引脚就可以用于其他功能,不会影响到BOOT_MODE寄存器。 ​ BOOT_MODE[1:0]的值确定了4种启动模式,如下图:

​ 00模式在我们的开发过程中很少用到,简单介绍一下:在这种模式下,GPIO的值被忽略。Boot ROM会根据eFUSE的值来选择启动设备、设置启动设备。但是,对于刚出厂的芯片eFUSE值可能是错乱的、不适合你的设备的,怎么办?eFUSE中有一个值BT_FUSE_SEL,它的出厂值是0,表示eFUSE未被烧写。boot ROM程序发现BT_FUSE_SEL为0时,它会通过USB或串口来下载程序;发现BT_FUSE_SEL为1时,才会根据eFUSE的值选择启动设备,读出、运行该设备上的程序。

​ 01模式,boot ROM程序通过USB或串口下载、运行程序,这个模式可以用来烧写EMMC等设备。我们的开发板出厂时,就是通过这个模式下载、烧写出厂程序的。

​ 10模式,称之为内部模式,简单地说就是从SD卡、EMMC等设备启动程序。这就引入下面第2个问题。

​ ② 如何选择启动设备?

​ 00模式下是通过eFUSE的值选择启动设备,我们不关心。

​ 10模式下既可以通过eFUSE的值也可以通过GPIO的值来选择启动设备,但是到底通过谁来决定?eFUSE中有一个值BT_FUSE_SEL,对,又是它。它的初始值为0,表示eFUSE未被烧写。在10模式下,当BT_FUSE_SEL为0时就会通过GPIO来选择启动设备;当BT_FUSE_SEL为1时就会通过eFUSE来选择启动设备。

​ 在开发阶段,我们使用GPIO来选择设备,这就引入下面第3个问题。

​ ③ 如何通过eFUSE或GPIO选择、设置启动设备?

​ 通过eFUSE或GPIO不仅能选择启动设备,还可以设置启动设备。

​ 为什么还需要设置?比如Nand Flash参数各有不同,有些的页大小是2048,有些是4096。这些参数不同,boot ROM程序读Nand Flash的方法就不同,我们必须把这些参数告诉boot ROM:通过eFUSE或GPIO来标明这些参数。

​ 首先看看要设置哪些eFUSE或GPIO来选择不同的启动设备。

​ 从上图可知,既可以使用eFUSE也可以使用GPIO来选择启动设备,换句话说GPIO可以覆盖eFUSE的值。哪些GPIO覆盖哪些eFUSE?这可以查看IMX6ULL芯片手册《Chapter 8: System Boot》里的《GPIO boot overrides》,我们把它摘出来放在3.1.3小节里。

​ 选择启动设备后,还需要标明一些参数。

​ 比如选择EMMC启动时,EMMC接在哪一个接口,eSDHC1还是eSDHC2?它的速度如何?

​ 比如选择TF卡启动时,TF卡接在哪一个接口,eSDHC1还是eSDHC2?它的速度如何?

​ 假设使用EMMC启动,或是TF卡启动,怎么设置eFUSE或GPIO?这些信息可以查询IMX6ULL芯片手册《Chapter 5: Fusemap》,摘录如下。

​ 当BOOT_MODE设置为0b00时,通过eFUSE选择启动设备,通过eFUSE获得设备的参数。

​ 当BOOT_MODE设置为0b10时,通过eFUSE或GPIO来选择启动设备,获得设备的参数;使用eFUSE还是GPIO由eFUSE中的BT_FUSE_SEL决定,它默认是0,表示使用GPIO。

​ 以BOOT_MODE为0b10为例,解析一下上图。

​ 要设置为SD卡、TF卡启动,有2个设置方法:

​ a. 设置eFUSE的BOOT_CFG1[7:5]为0b010,或

​ b. 查看《3.1.3 GPIO boot overrides》确定BOOT_CFG1[7:5]对应的GPIO为LCD1_DATA07~05,把这3个引脚设置为0b010。

​ 根据SD卡、TF卡的性能,可以设置eFUSE或GPIO来表示它能否提供更高的速度:

​ a. 设置eFUSE的BOOT_CFG1[4:0],或

​ b. 查看《3.1.3 GPIO boot overrides》确定BOOT_CFG1[4:0]对应的GPIO为LCD1_DATA04~00,设置这些引脚。

​ IMX6ULL有两个SD卡、TF卡接口,使用哪一个接口?请看下表:

​ a. 设置eFUSE的BOOT_CFG2[4:3]可以确定使用eSDHC1或eSDHC2,或

​ b. 查看《3.1.3 GPIO boot overrides》确定BOOT_CFG2[4:3]对应的GPIO为LCD1_DATA12~11,设置这些引脚

​ 通过eFUSE或GPIO,还可以标明启动设备的更多参数,具体细节可以参考芯片手册《Chapter 5: Fusemap》,作为硬件开发人员需要去细细研究;作为软件开发人员,实际上只需要看开发板手册知道怎么设置启动开关即可。

3.1.2 100ASK_IMX6ULL启动方式选择

​ 100ASK_IMX6ULL开发板上的红色拨码开关用来设置启动方式、选择启动设备,支持这3种方式:EMMC启动、SD卡启动、USB烧写。

​ 板子背后画有一个表格,表示这3种方式如何设置。

​ 表格如下:

BOOT CFG
BOOT SW1(LCD_DATA5) SW2(LCD_DATA11) SW3(BOOT_MODE0) SW4(BOOT_MODE1)
EMMC OFF OFF ON OFF
SD ON ON ON OFF
USB X X OFF ON
​ 拔码开关中的SW3、SW4用来设置BOOT_MODE,ON表示0,OFF表示1。 ​ 所以当SW3、SW4设置为ON、OFF时,BOOT_MODE为0b10,将会使用SD卡、TF卡、EMMC等设备启动。 ​ 刚出厂的开发板中BT_FUSE_SEL默认为0,表示使用GPIO来设置参数。即使用LCD1_DATA07~05来选择启动设备。 ​ 100ASK_IMX6ULL开发板只支持SD/TF卡、EMMC启动,LCD1_DATA07~05为0b010时选择SD/TF卡启动,LCD1_DATA07~05为0b011时选择EMMC启动。这两种启动设备对应的LCD1_DATA07~06的值相同,都是0b01,这在核心板上已经通过电阻设置好,我们只需要在拨码开关上设置SW1(对应LCD1_DATA05)就可以。

​ IMX6ULL上有2个EMMC Flash接口,也复用为2个SD/TF卡接口,通过LCD1_DATA12~11来选择接口。0b00对应eSDHC1接口,0b01对应eSDHC2接口。LCD1_DATA12的值在核心板上已经通过电阻设置好。LCD1_DATA11的值通过拨码开关SW2来设置:ON表示0,对应eSDHC1接口,100ASK_IMX6ULL的TF卡接口使用了eSDHC1接口;OFF表示1,对应eSDHC2接口,100ASK_IMX6ULL的EMMC接口使用了eSDHC2接口。

​ 这3种启动方式的设置示意图如下:

​ 要注意的是,设置为USB启动时,不能插上SD卡、TF卡。 ​ 刚出厂的板子在EMMC上烧写了系统,你可以设置为EMMC启动方式。

3.1.3 GPIO boot override

​ IMX6ULL中既可以通过eFUSE也可以通过GPIO来选择、设置启动设备,在手册里大部分场合只列出了eFUSE,对应的GPIO需要查表:IMX6ULL芯片手册《Chapter 8: System Boot》里的《GPIO boot overrides》。

​ 我们把它摘录出来。

第三章:IMX6ULL启动流程

3.2 IMX6ULL启动流程

3.2 IMX6ULL启动流程

​ 这个启动流程可以猜测出来,假设板子设置为SD/TF卡启动,boot ROM程序会做什么?把程序从SD/TF卡读出来,运行。

​ 从哪里读?从SD/TF卡读,这需要先初始化SD/TF卡:根据eFUSE或GPIO的设置初始化SD/TF卡。

​ 读到哪里去?读到内存即DDR去,这需要先初始化DDR。

​ 除了初始化启动设备、初始化DDR,还需要初始化什么?也许要初始化时钟,让CPU跑得更快一点。

​ 总结起来就是:初始化CPU、时钟等,初始化内存,初始化启动设备,从启动设备上把程序读入内存,运行内存的程序。

​ 官方的启动流程如下,这个流程图比较粗糙,总结起来就是:

​ a. 检查CPU ID

​ b. 检查Reset Type,冷启动、唤醒的启动过程是不一样的

​ c. 检查启动模式BOOT_MODE,检查eFUSE或GPIO

​ d. 根据上述检查从USB口、UART口或是某个启动设备下载boot image

​ e. 认证image

​ f. 启动

​ 对于具体的启动设备,IMX6ULL芯片手册《Chapter 8: System Boot》中有对应章节描述更为细致的启动流程。基本上就是对这些启动设备根据eFUSE或GPIO的设置进行初始化,尝试更高的工作频率等。 ​ 在往后的学习中,如果涉及这些细节,我们再描述。 ​ 假设使用SD/TF卡启动,卡上的程序有多大?它应该被复制到DDR哪里去?这些问题,请看《3.3 IMX6ULL映像文件制作与使用》。

第三章:IMX6ULL启动流程

3.3 IMX6ULL映像文件

3.3 IMX6ULL映像文件

3.3.1 格式概述

​ 如果您有S3C2440或其他单片机的学习经验,可以知道程序的二进制版本,比如lcd.bin可以直接烧写到Flash上。它们是自启动的,什么意思?比如一上电,运行的是lcd.bin前面的代码,它会初始化内存,把自己从Flash上复制到内存里去执行。请记住:自己把自己复制到内存。 ​ 但是对于IMX6ULL,烧写在EMMC、SD/TF卡上的程序,并不能“自己复制自己”。一上电首先运行的是boot ROM上的程序,它从EMMC、SD/TF卡上把程序复制进内存里。 ​ 所以:boot ROM程序需要知道从启动设备哪个位置读程序,读多大的程序,复制到哪里去。 ​ 所以:启动设备上,不能仅仅烧写bin文件,需要在添加额外的信息。

​ 还有一个问题,IMX6ULL的boot ROM程序可以把程序读到DDR里,那需要先初始化DDR。每种板子接的DDR可能不一样,boot ROM程序需要初始化这些不同的DDR。boot ROM从哪里得到这些不同的参数?

​ 还有,IMX6ULL支持各种启动设备,比如各种Nor Flash。为了通用,boot ROM程序将会使用最保守的参数,也就是最慢的时序来访问Nor Flash。为加快启动程序,boot ROM程序可以根据我们提供的信息初始化硬件,让它以更优的参数运行。

​ 这些参数信息,被称为“Device Configuration Data”,设备配置数据(DCD),这些DCD将会跟bin文件一起打包烧写在启动设备上。boot ROM程序会从启动设备上读出DCD数据,根据DCD来写对应的寄存器以便初始化芯片。DCD中列出的是对某些寄存器的读写操作,我们可以在DCD中设置DDR控制器的寄存器值,可以在DCD中使用更优的参数设置必需的硬件。这样boot ROM程序就会帮我们初始化DDR和其他硬件,然后才可以把bin程序读到DDR中并运行。

​ 总结起来,烧写在EMMC、SD卡或是TF卡上的,除了程序本身,还有位置信息、DCD信息,这些内容合并成一个映像文件,如下图:

​ 这4部分内容合并成为一个映像文件,烧写在EMMC、SD卡或TF卡等启动设备的某个固定地址,boot ROM程序去这个固定地址读出映像文件。启动设备不同,固定地址不同,如下图:

3.3.2 格式详解

​ 先贴出一张图,然后再细细讲解:

​ 下面的讲解图中,列有C语言格式的结构体,这些结构体来源于U-boot的tools目录下的imximage.h。对于程序员,有时候看结构体可以更快地理解映像文件的格式。

​ (1). Image Vector Table(IVT):

​ IVT会被放在固定的地址,IVT中是一系列的地址,boot ROM程序会根据这些地址来确定映像文件中其他部分在哪里。

​ IVT格式如下:

​ 要注意的是上图中这4项:

​ a. header:

​ 里面有3项:tag、length、version。length表示IVT的大小,它是32字节。要注意是的,它是大字节序的。

​ b. entry:

​ 用户程序运行时第1条指令的地址,就是程序的链接地址、程序被复制到内存哪里

​ c. dcd:

​ 映像被复制到内存后,其中的DCD数据的地址。

​ d. boot data:

​ 映像被复制到内存后,其中的boot data的地址。

​ e. self:

​ 映像被复制到内存后,IVT自己所在的地址。

​ (2). Boot data:

​ 映像被复制到内存后,IVT自己所在的地址。

​ a. start:

​ 这是映像文件在内存中的地址,注意,它不等于IVT在内存中的地址。

​ 什么意思?假设IVT被保存在启动设备TF卡1024偏移地址处,IVT被复制到内存地址0x87000000,那么start=0x87000000-1024。

​ 所以start表示的是启动设备开头的数据,被复制到内存哪里去。

​ 从它的含义也可以推理出:boot ROM程序会把启动设备开头的数据,复制到内存;而不仅仅是从IVT开始复制。

​ b. length:

​ 保存在启动设备上的整个映像文件的长度,从0地址开始(不是从IVT开始)。

​ c. plugin:

​ 这是一个标记位,当它为1时表示这个映像文件是“plugin”,即插件。

​ boot ROM程序可以支持有限的启动设备,如果你想双持更多的启动设备比如网络启动、CDROM启动,就需要提供对应的驱动。这些驱动就是“plugin”,我们的教程不涉及,该标记位为0。

​ Boot data就是用来表示映像文件应该被复制到哪里去,以前它的大小。boot ROM程序就是根据它来把整个映像文件复制到内存去的。

​ (3). DCD:

​ DCD的作用在前面讲解过,简单地说就是设备的配置信息,里面保存有一些寄存器值。

​ 实际上DCD还可以更复杂,它支持多种命令:write data、check data、nop、unlock。我们可以通过write data命令写寄存器,通过check data命令等待寄存器就绪。

​ DCD格式如下:

​ DCD以Header开始,里面的TAG为0xD2表示它是DCD,里面还标明了DCD的大小、版本。

​ 接下来就是各个“CMD”,你可以在一个“CMD”里操作多个寄存器,比如在一个“write data command”中,写多个寄存器。

​ 以“write data command”为例简单介绍一下,它的格式为:

​ 上图中,TAG为0xCC表示这是“write data command”;Length表示命令的大小;Parameter的作用稍后再说。

​ 既然是写命令,那自然就有“地址、值”,上图中就是多个“Address、Value/Mask”。为何还有Mask?这要结合Parameter来讲解:

​ Parameter中b[2:0]用来表示写操作的字节数,是以字节、半字(2 byte),还是字(4 byte)来操作。

​ 而b[4]、b[3]决定了是写值(write value),清位(clear bitmask),还是设位(set bitmask)。

​ 对于其他命令,共格式可以参考IMX6UL的芯片手册,这里就不再介绍了。

​ (4). User code and data:

​ 就是用户程序或数据,原原本本地添加到映像文件里就可以。

​ https://github.com/NXPmicro/mfgtools

3.3.1 实例

​ 我们制作映像文件的目的什么?把我们自己的程序烧写到启动设备,让boot ROM程序启动它。

​ 所以制作映像文件的起点是:我们编写的程序。制作过程中各填值的计算方法如下图所示。

上图中各步骤细说如下:

① 确定入口地址entry:

我们的程序运行时要放在内存中哪一个位置,这是我们决定的。它被称为入口地址、链接地址。

② 确定映像文件在内存中的地址start:

boot ROM程序启动时,会把“initial load region”读出来,“initial load region”里含有IVT、Boot data、DCD。boot ROM根据DCD初始化设备后,再把整个映像文件读到内存。

在启动设备上,“initial load region”之后紧跟着我们的程序,反过来说就是我们程序的前面,放着“initial load region”。假设“initial load region”的大小为load_size,那么在内存中“initial load region”的位置start = entry – load_size。

注意:“initial load region”位于启动设备0位置,它的头部并不是IVT,而是一些无用的数据(或是分区信息)。

③ 确定IVT在内存中的地址self:

我们知道IVT在启动设备上某个固定的位置:ivt_offset。那么在内存中它的位置可以如下计算:

self = start + ivt_offset = entry – load_size + ivt_offset

④ 确定Boot data在内存中的地址boot_data:

IVT的大小是32字节,IVT之后就是Boot data,而IVT中的boot_data值表示Boot data在内存中的位置,计算如下:

boot_data = self + 32 = entry – load_size + ivt_offset + 32

⑤ 确定DCD在内存中的地址dcd:

Boot data的大小是12字节,Boot data之后就是DCD,而IVT中的dcd值表示DCD在内存中的位置,计算如下:

dcd = boot_data + 12 = entry – load_size + ivt_offset + 44

⑥ 写入DCD的数据:

DCD是用初始化硬件的,特别是初始化DDR。而DDR的初始化非常的复杂、专业,我们一般是使用硬件厂家提供的代码。

在后面的程序中你可以看到,我们是使用类似下面的指令来制作映象文件:

./tools/mkimage -n ./tools/imximage.cfg.cfgtmp -T imximage -e 0x80100000 -d led.bin led.imx

​ 上述命令中的imximage.cfg.cfgtmp就是厂家提供的,内部截取部分贴出来:

从上图也可以看到imximage.cfg.cfgtmp文件中基本是对寄存器的写操作。

​ mkimage程序来自u-boot,它会把imximage.cfg.cfgtmp中的内容转换为DCD数据。我们并不打算讲解DCD的内容,只需要了解它的大概作用:

a. 设置时钟:DDR也需要时钟,这很好理解

b. 设置引脚:DDR需要很多引脚

c. 设置DDR控制器:Multi-mode DDR controller (MMDC)

⑦ 写入用户程序

⑧ 经过上述7个步骤,整个映像文件就构造出来了,可以把它烧入启动设备。

​ 我们提供的示例程序001_led中有一个文件:led.img,它就是映象文件,可以直接烧入TF卡。用软件Hex Editor Neo打开led.img,选择doble word方式显示,可以看到如下内容,你可以自行验证一下映像文件中各个值。

第三章:IMX6ULL启动流程

3.4 映像文件烧写、运行

3.4 映像文件烧写、运行

​ 我们编译出来的映像文件有2类后缀:imx、img。imx文件开头就是IVT,可以把它烧写到TF卡1024偏移位置处;img文件开头是1024字节的0值数据,后面才是IVT等,它可以通过win32diskimger等工具直接烧写到TF卡0偏移位置处。

​ 另外,我们还可以通过USB把imx文件直接下载到板子上,并运行。

​ 注意:通过USB下载方式,可以烧写程序到EMMC、TF卡上,但是并非“直接烧写”。它的过程如下:

​ a. 通过USB下载u-boot到内存,

​ b. 通过USB下载用户程序到内存,

​ c. 通过USB发送命令运行u-boot,

​ d. 用u-boot烧写把内存中的用户程序烧写到EMMC、TF卡上

​ 最新版本的IMX6ULL烧写工具名为uuu:Universal Update Utility,又名mfgtools 3.0。

源码地址为:

https://github.com/NXPmicro/mfgtools

编译好的可执行程序下载地址:

https://github.com/NXPmicro/mfgtools/releases

3.4.1 使用USB下载、运行裸机程序

​ 使用USB来下载、运行裸机程序,是最简单的方法,不需要烧写。

步骤如下:

① 开发板设置为USB启动,或称为USB下载模式:

对于100ASK_IMX6ULL开发板,启动开关设置为下图所示的样子:

② 使用USB线连接电脑和开发板的OTG口:

对于100ASK_IMX6ULL开发板,接线如下所示:

接好线后上电。

③ 使uuu命令下载、运行IMX文件:

我们提供的程序中在tools目录下预先放置了uuu.exe(windows用)、uuu(Linux用)。

以Windows为例,如下操作:

a. 进入程序目录,打开命令行:

b. 执行命令:

在命令行中执行:tools\uuu.exe led.imx ,如下图所示。如果一切正常,可以看到开发板上的绿灯开始闪烁。

如果不成功,请确认:

a. 开发板的启动开关是否设置为USB模式

b. 开发板不要插上TF卡

c. 开发板复位一下,再执行uuu命令

d. 要下载运行的是imx文件,不是bin文件,也不是img文件

3.4.2 使用读卡器烧写裸机程序到TF卡上并运行

​ 这需要借助读卡器,在电脑上烧写TF卡。步骤如下:

​ a. 烧写TF卡:

​ 把TF卡通过读卡器接到电脑上,使用win32diskimager把img文件烧写到SD卡上,如下图所示操作,烧写成功后会有提示:

​ b. 启动开发板:

​ 把烧写好的TF卡插到开发板,开发板设置为SD/TF启动模式(如下),上电即可:

​ 注意:使用win32diskimager烧写时,一定要选择img文件,不能选择imx文件。

3.4.3 使用USB下载、烧写程序到EMMC

​ USB只有下载、运行的功能,烧写要借助u-boot。所以我们要下载2个文件:u-boot-dtb.imx、我们的程序比如led.imx,然后运行u-boot,执行u-boot命令来烧写。

步骤如下:

① 开发板设置为USB启动,或称为USB下载模式:

对于100ASK_IMX6ULL开发板,启动开关设置为下图所示的样子:

② 使用USB线连接电脑和开发板的OTG口:

对于100ASK_IMX6ULL开发板,接线如下所示:

接好线后上电。

③ 使uuu命令下载、运行IMX文件:

我们提供的程序中在tools目录下预先放置了uuu.exe(windows用)、uuu(Linux用)。

以Windows为例,如下操作:

a. 进入程序目录,打开命令行:

b. 执行命令:

在命令行中执行:tools\uuu.exe -b emmc tools\u-boot-dtb.imx led.imx ,如下图所示。

命令解析:“-b emmc”表示要烧写emmc,需要借助于tools\u-boot-dtb.imx,要烧写的文件是led.imx。

如果一切正常,把开发板设置为EMMC启动后重新上电,可以看到绿灯闪烁。

如果烧写不成功,请确认:

a. 开发板的启动开关是否设置为USB模式

b. 开发板不要插上TF卡

c. 开发板复位一下,再执行uuu命令

d. 烧写成功后,开发板断电,设置为EMMC启动,再重新上电观察效果

第四章:LED程序

第四章:LED程序

4. LED程序

4.1 硬件知识_LED原理图

​ 当我们学习C语言的时候,我们会写个Hello程序。

​ 那当我们写ARM程序,也该有一个简单的程序引领我们入门,这个程序就是点亮LED。

​ 我们怎样去点亮一个LED呢?

​ 分为三步:

​ ① 看原理图,确定控制LED的引脚;

​ ② 看主芯片的芯片手册,确定如何设置控制这个引脚;

​ ③ 写程序;

​ LED样子有很多种,像插脚的,贴片的。

​ 它们长得完全不一样,因此我们在原理图中将它抽象出来。

​ 点亮LED需要通电源,同时为了保护LED,加个电阻减小电流。

​ 控制LED灯的亮灭,可以手动开关LED,但在电子系统中,不可能让人来控制开关,通过编程,利用芯片的引脚去控制开关。

​ LED的驱动方式,常见的有四种。

​ 方式1:使用引脚输出3.3V点亮LED,输出0V熄灭LED。

​ 方式2:使用引脚拉低到0V点亮LED,输出3.3V熄灭LED。

​ 有的芯片为了省电等原因,其引脚驱动能力不足,这时可以使用三极管驱动。

​ 方式3:使用引脚输出1.2V点亮LED,输出0V熄灭LED。

​ 方式4:使用引脚输出0V点亮LED,输出1.2V熄灭LED。

​ 由此,主芯片引脚输出高电平/低电平,即可改变LED状态,而无需关注GPIO引脚输出的是3.3V还是1.2V。

​ 所以简称输出1或0:

​ 逻辑1-->高电平

​ 逻辑0-->低电平

4.2 普适的GPIO引脚操作方法

​ GPIO: General-purpose input/output,通用的输入输出口。

4.2.1 GPIO模块一般结构

a. 有多组GPIO,每组有多个GPIO

b. 使能:电源/时钟

c. 模式(Mode):引脚可用于GPIO或其他功能

d. 方向:引脚Mode设置为GPIO时,可以继续设置它是输出引脚,还是输入引脚

e. 数值:对于输出引脚,可以设置寄存器让它输出高、低电平

​ 对于输入引脚,可以读取寄存器得到引脚的当前电平

4.2.2 GPIO寄存器操作

a. 芯片手册一般有相关章节,用来介绍:power/clock

​ 可以设置对应寄存器使能某个GPIO模块(Module)

​ 有些芯片的GPIO是没有使能开关的,即它总是使能的

b. 一个引脚可以用于GPIO、串口、USB或其他功能,

​ 有对应的寄存器来选择引脚的功能

c. 对于已经设置为GPIO功能的引脚,有方向寄存器用来设置它的方向:输出、输入

d. 对于已经设置为GPIO功能的引脚,有数据寄存器用来写、读引脚电平状态

GPIO寄存器的2种操作方法:

原则:不能影响到其他位

  1. 直接读写:读出、修改对应位、写入

​ 要设置bit n:

val = data_reg;
val = val | (1<<n);
data_reg = val;

​ 要清除bit n:

val = data_reg;
val = val & ~(1<<n);
data_reg = val;
  1. set-and-clear protocol:

​ set_reg, clr_reg, data_reg 三个寄存器对应的是同一个物理寄存器,

​ 要设置bit n:set_reg = (1<<n);

​ 要清除bit n:clr_reg = (1<<n);

4.2.3 GPIO的其他功能:防抖动、中断、唤醒

后续章节再介绍

4.3 IMX6ULL的GPIO操作方法

​ CCM: Clock Controller Module (时钟控制模块)

​ IOMUXC : IOMUX Controller,IO复用控制器

​ GPIO: General-purpose input/output,通用的输入输出口

4.3.1 IMX6ULL的GPIO模块结构

​ 参考资料:芯片手册《Chapter 26: General Purpose Input/Output (GPIO)》

​ 有5组GPIO(GPIO1~GPIO5),每组引脚最多有32个,但是可能实际上并没有那么多。

​ GPIO1有32个引脚:GPIO1_IO0~GPIO1_IO31;

​ GPIO2有22个引脚:GPIO2_IO0~GPIO2_IO21;

​ GPIO3有29个引脚:GPIO3_IO0~GPIO3_IO28;

​ GPIO4有29个引脚:GPIO4_IO0~GPIO4_IO28;

​ GPIO5有12个引脚:GPIO5_IO0~GPIO5_IO11;

​ GPIO的控制涉及4大模块:CCM、IOMUXC、GPIO模块本身,框图如下:

4.3.2 CCM用于设置是否向GPIO模块提供时钟

​ 参考资料:芯片手册《Chapter 18: Clock Controller Module (CCM)》

​ GPIOx要用CCM_CCGRy寄存器中的2位来决定该组GPIO是否使能。哪组GPIO用哪个CCM_CCGR寄存器来设置,请看上图红框部分。

​ CCM_CCGR寄存器中某2位的取值含义如下:

① 00:该GPIO模块全程被关闭

② 01:该GPIO模块在CPU run mode情况下是使能的;在WAIT或STOP模式下,关闭

③ 10:保留

④ 11:该GPIO模块全程使能

​ GPIO2时钟控制:

​ GPIO1、GPIO5时钟控制:

​ GPIO3时钟控制:

​ GPIO4时钟控制:

4.3.3 IOMUXC:引脚的模式(Mode、功能)

​ 参考资料:芯片手册《Chapter 32: IOMUX Controller (IOMUXC)》。

​ 对于某个/某组引脚,IOMUXC中有2个寄存器用来设置它:

​ ① 选择功能:

​ IOMUXC_SW_MUX_CTL_PAD_< PADNAME > :Mux pad xxx,选择某个pad的功能

​ IOMUXC_SW_MUX_CTL_GRP_< GROUP NAME >:Mux grp xxx,选择某组引脚的功能

​ 某个引脚,或是某组预设的引脚,都有8个可选的模式(alternate (ALT) MUX_MODE)。

比如:

​ ② 设置上下拉电阻等参数:

​ IOMUXC_SW_PAD_CTL_PAD_< PAD_NAME >:pad pad xxx,设置某个pad的参数

​ IOMUXC_SW_PAD_CTL_GRP_< GROUP NAME >:pad grp xxx,设置某组引脚的参数

​ 比如:

4.3.4 GPIO模块内部

​ 框图如下:

我们暂时只需要关心3个寄存器:

① GPIOx_GDIR:设置引脚方向,每位对应一个引脚,1-output,0-input

② GPIOx_GDIR:设置输出引脚的电平,每位对应一个引脚,1-高电平,0-低电平

③ GPIOx_PSR:读取引脚的电平,每位对应一个引脚,1-高电平,0-低电平

第五章:LED程序涉及的编程知识

第五章:LED程序涉及的编程知识

5. LED程序涉及的编程知识

5.1 ARM架构的简单介绍

​ 目前IMX6UL是使用Cortex-A7架构,本小节简单介绍一下Cortex-A7架构的基础知识,比如运行模式、寄存器组等。

​ 参考资料:

5.1.1 运行模式

​ Cortex-A7架构的运行模式有9种,分别为User、Sys(System)、FIQ、IRQ、ABT(Abort)、SVC(Supervisor)、UND(Undef)、MON(Monitor)、Hyp模式,如下表:

模式 描述
User 用户模式,非特权模式,大部分程序运行的
时候就处于此模式
Sys(System) 系统模式,用于运行特权级的操作系统任务
FIQ 快速中断模式,进入 FIQ 中断异常
IRQ 一般中断模式
ABT(Abort) 数据访问终止模式,用于虚拟存储以及存储保护
SVC(Supervisor) 超级管理员模式,供操作系统使用
UND(Undef) 未定义指令终止模式
MON(Monitor) 用于安全扩展模式
Hyp 用于虚拟化扩展

​ 除了User模式属于非特权模式,其它8种处理器模式都是特权模式。

​ 运行模式可以通过软件进行任意切换,也可以通过中断或者异常来进行切换。大多数的程序都运行在用户模式,用户模式下是不能访问系统所有资源的,有些资源是受限的,要想访问这些受限的资源就必须进行模式切换。但是用户模式是不能直接进行切换的,用户模式下需要借助异常来完成模式切换,当要切换模式的时候,应用程序可以产生异常,在异常的处理过程中完成处理器模式切换。

5.1.2 寄存器组

​ 本节我们要讲的是 Cortex-A7 内核寄存器组,而不是芯片外设寄存器。

​ 上一小节我们讲了 Cortex-A7 有 9 种运行模式,每一种运行模式都有一组与之对应的寄存器组,如下图:

​ 浅色字体是与 User 模式所共有的寄存器,浅蓝色背景是各个模式所独有的寄存器,即在所有的模式中,低寄存器组(R0~R7)是共享同一组物理寄存器的,只是一些高寄存器组在不同的模式有自己独有的寄存器,比如 FIQ 模式下 R8~R14 是独立的物理寄存器。

​ 如果某个程序处于 FIQ 模式下访问寄存器 R13(SP),那它实际访问的是寄存器 SP_fiq

​ 如果某个程序处于 SVC 模式下访问寄存器 R13(SP),那它实际访问的是寄存器 SP_svc

9 种运行模式的寄存器合计有34个,可以分为:

  1. 未备份寄存器,即 R0~R7
  2. 备份寄存器,即 R8~R14
  3. 程序计数器 ,即 R15
  4. 程序状态寄存器

下面一一介绍以上4类寄存器。

5.1.2.1 未备份寄存器

​ 未备份寄存器指的是 R0~R7,因为在所有的运行模式下R0~R7寄存器都是同一个物理寄存器,在不同的模式下,R0~R7寄存器中的数据就会被破坏,所以R0~R7寄存器并没有被用作特殊用途。

5.1.2.2 备份寄存器

​ 备份寄存器中的 R8~R12 寄存器有两种物理寄存器,在快速中断模式下(FIQ)它们对应着Rx_irq(x=8~12)物理寄存器,其他模式下对应着 Rx(8~12)物理寄存器。FIQ 是快速中断模式,这个中断模式要求快速执行!因为 FIQ 模式下的 R8~R12 是独立的,因此中断处理程序可以不用执行保存和恢复中断现场的指令,从而加速中断的执行过程。

​ 备份寄存器 R13(SP) ,也叫栈指针,有 8 个物理寄存器,其中一个是User和Sys模式共用的,剩下的 7 个分别对应 7 种不同的模式。

​ 备份寄存器 R14(LR) ,也叫链接寄存器,有 7 个物理寄存器,其中一个是User、Sys和Hyp模式所共有的,剩下的 6 个分别对应 6 种不同的模式,主要有如下用途:

​ 使用 R14(LR)来存放当前子程序的返回地址,如果使用 BL 或者 BLX来调用子函数的话,R14(LR)被设置成该子函数的返回地址,在子函数中,将 R14(LR)中的值赋给 R15(PC)即可完成子函数返回,如mov pc,lr

5.1.2.3 程序计数器

​ 程序计数器 R15(PC),保存着当前执行指令地址值加 8 个字节

​ 因为ARM处理器是三级流水线:取指->译码->执行,循环执行。比如当前正在执行第一条指令的同时也对第二条指令进行译码,第三条指令也同时被取出存放在 R15(PC)中,即 R15(PC)总是指向当前正在执行指令地址再加上 2 条指令的地址,对于 32 位的 ARM 处理器,每条指令是 4 个字节,

​ 所以R15(PC) = 当前执行指令地址 + 8个字节

5.1.2.4 程序状态寄存器

​ 程序状态寄存器PSR可以分成当前程序状态寄存器CPSR与备份程序状态寄存器SPSR。

​ 所有运行模式都共用一个 CPSR 物理寄存器,因此 CPSR 可以在任何模式下被访问,该寄存器包含条件标志位、中断禁止位、当前运行模式标志等一些状态位以及一些控制位。但是所有运行模式都共用一个 CPSR 必然会导致冲突,因此除了 User 和 Sys 模式以外,其他 7 个模式都配备一个专用的物理状态寄存器,叫做 备份程序状态寄存器(SPSR),当特定异常中断发生时,SPSR用来保存CPSR的值,当异常退出以后可以用 SPSR 中保存的值来恢复 CPSR。

​ 由于 SPSR 是 CPSR 的备份,因此 SPSR 和 CPSR 的寄存器结构相同,如下图:

​ N(bit31):当两个有符号整数运算(补码表示)时,结果用N表示,N=1/0 表示 负数/正数

​ Z(bit30):对于 CMP 指令,Z=1 表示进行比较的两个数大小相等

​ C(bit29):

​ 在加法指令中,当结果产生了进位,则C=1,表示无符号数运算发生上溢,其它情况下 C=0

​ 在减法指令中,当运算中发生借位,则C=0,表示无符号数运算发生下溢,其它情况下 C=1

​ 对于包含移位操作的非加/减法运算指令,C 中包含最后一次溢出的位的数值

​ 对于其它非加/减运算指令,C 位的值通常不受影响

​ V(bit28):对于加/减法运算指令,当操作数和运算结果表示为二进制的补码表示的带符号数时,V=1 表示符号位溢出,通常其他位不影响 V 位

​ Q(bit27):仅 ARM v5TE_J 架构支持,表示饱和状态,Q=1/0 表示累积饱和/累积不饱和

​ IT1:0 和 IT7:2一起组成 IT[7:0],作为 IF-THEN 指令执行状态

​ J(bit24)和T(bit5):控制指令执行状态,表明本指令是ARM指令还是Thumb指令,如表

J T 描述
0 0 ARM
0 1 Thumb
1 1 ThumbEE
1 0 Jazelle

​ GE3:0:SIMD 指令有效,大于或等于

​ E(bit9):大小端控制位,E=1/0 表示大/小端模式

​ A(bit8):禁止异步中断位,A=1 表示禁止异步中断

​ I(bit7):I=1/0 代表 禁止/使能 IRQ

​ F(bit6):F=1/0 代表 禁止/使能 FIQ

​ M[4:0]:运行模式控制位,如表

M[4:0] 运行模式
10000 User 模式
10001 FIQ 模式
10010 IRQ 模式
10011 Supervisor(SVC)模式
10110 Monitor(MON)模式
10111 Abort(ABT)模式
11010 Hyp(HYP)模式
11011 Undef(UND)模式
11111 System(SYS)模式

5.2 汇编与机器码、汇编指令

参考资料:

根据指令复杂度来区分,所有CPU可以分为2类:

  1. CISC

    复杂指令集计算机,Complex Instruction Set Computer,比如x86

  2. RISC

    精简指令集计算机,Reduced Instruction Set Computing,比如ARM,RISC-V

比如,对于加法运算:a = a + b,它涉及4个步骤的操作:读出a,读出b,计算a+b,把结果写回a。

  1. 使用CISC(复杂指令集计算机,比如x86)提供的加法指令,只需要一条指令即可完成这4步操作。当然,这一个指令需要多个CPU周期才可以完成。

  2. 而RISC不提供“一站式”的加法指令,需调用四条单CPU周期指令完成两数相加:内存a加载到寄存器,内存b加载到寄存器,两个寄存器中数相加,寄存器结果存入内存a

​ ARM芯片属于精简指令集计算机(RISC:Reduced Instruction Set Computing),它所用的指令比较简单,有如下特点:

  1. 对内存只有读、写指令

  2. 对于数据的运算是在CPU内部实现

  3. 使用RISC指令的CPU复杂度小一点,易于设计

5.2.1 汇编与机器码

​ 上面的例子中,数值a原来是保存在内存里的,执行了某条指令后,它的值被读入内存,那问题来了:

  1. 什么指令,可以让CPU从内存里把数据读进来?

    比如:

mov r0, #addr_a // 把变量a的地址传给CPU寄存器r0
ldr r1, [r0]    // 从r0所指的内存把数值读进CPU寄存器r1
  1. 读进来后,这个数保存在哪里?

    当然是保存在CPU内部了,存在某个寄存器里,上面的代码用寄存器r1来保存该值

  2. 如何处理数据?

    CPU执行加法指令,比如:

add r1, r1, r2 // 在CPU内部,r1=r1+r2
  1. 最终数据怎么写入内存?

    CPU执行指令,比如:

str r1, [r0]   // 将r1的值写入r0所指的内存

​ 上面例子中,mov、add、ldr、str等都是汇编指令,或者说它们是“助记符”──帮助我们记忆的。记忆什么呢?这些指令其实是一个一个数值,我们去记这些数值有难度,所以就用mov表示某个指令的数值,用add表示某个指令的数值,对应的,这些指令的数值就是机器码,即汇编指令是机器码的助记符

ARM指令机器码是有一定格式,如下:

cond op1 op 指令类型
not 1111 00x - 数据处理和杂项指令,如MOV
not 1111 010 - 加载/存储指令,如LDR/STR
not 1111 011 0 加载/存储指令,如LDR/STR
not 1111 011 1 媒体指令(英文:Media instructions)
not 1111 10x - 分支指令,如B、BL; 块数据传输指令,如
LDM/STM、POP/PUSH
not 1111 11x - 协处理器指令
1111 - - 无条件指令,如BL

​ 下面讲解几种常用的汇编指令。

​ 参考资料: ARM® and Thumb®-2 Instruction Set Quick Reference Card.pdf (ARM指令快速参考卡)

​ 文档所在目录: 资料光盘 00_UserManual\参考资料\Arm架构参考资料\ ARM® and Thumb®-2 Instruction Set Quick Reference Card.pdf

5.2.2 汇编指令

​ 汇编指令的格式,如下:

label:                  
	instruction @ comment

label,即标签,表示地址位置,可以通过label得到指令/数据地址

instruction,即指令,表示汇编指令或伪指令

@ comment,@表示后面是注释,comment表示注释内容

​ 比如:

add:                                
	mov r0, #0 @ 将R0寄存器设置成0

​ 上面汇编代码中,add表示标签,mov r0, #0表示指令,@ 将R0寄存器设置成0 表示 注释

​ 常用的汇编指令一般有mov、bl/b、add/sub、ldm/stm、push/pop等等,下面一一介绍。

5.2.2.1 mov

mov r1, #10  @ 将10赋值给寄存器r1,即r1=10

​ 指令执行过程,如下:

  1. 取指

    ​ 假设从内存的addrA地址取机器码e3a0100a(即mov r1, #10指令)

  2. 译码

    ​ 原来是MOV指令

  3. 执行

    ​ CPU内部寄存器R1等于10

    ​ 其中,机器码e3a0100a,MOV指令各位的解析如下:

​ [31:28]位是条件码0xe;[15:12]位是寄存器R1,即0x1;[12:0]位是立即数10,即0x00a

5.2.2.2 bl

1 bl test_tag
2 mov r1, #10
3 
4 test_tag:
5 	mov r3, #0
6 	mov pc, lr

​ 第1行,跳转到test_tag标签处执行mov r3, #0指令,并且将mov r1, #10指令的地址存储到 LR 寄存器

​ 第6行,返回到mov r1, #10指令地址,并且执行mov r1, #10指令

​ 指令执行过程,如下:

  1. CPU从内存的addrA地址取机器码eb000000(即bl test_tag指令),执行后,PC跳转到test_tag标签位置,即内存的addrA+8地址,从上图可知,其实test_tag标签的地址是mov r3, #0指令的地址。同时自动将内存的addrA+4地址存储在寄存器LR中

  2. CPU从内存的addrA+8地址取机器码e3a03000(即mov r3, #0指令),执行,CPU内部寄存器R3等于0

  3. CPU从内存的addrA+12地址取机器码e1a0f00e(即mov pc, lr指令),执行,PC跳转到内存的addrA+4地址

  4. CPU从内存的addrA+4地址取机器码e3a0100a(即mov r1, #10指令),执行,CPU内部寄存器R1等于10

    其中,机器码eb000000,BL指令各位的解析如下:

​ imm[23:0]是PC值与标签的偏移值除以4,但是此处的偏移值是0,为什么尼?这是因为ARM采用三级流水线的方法,即取指、译码、执行指令。所以当ARM执行addrA地址的bl test_tag指令时,但是PC已经指向addrA+8地址进行取mov r3, #0指令,所以此处的偏移值是0

5.2.2.3 b

1 b test_tag
2 mov r1, #10
3
4 test_tag:
5 	mov r3, #0

​ 第1行,只是跳转到test_tag标签处执行mov r3, #0指令,没有跳转回去执行mov r1, #10指令

​ 指令B与指令BL,大同小异,此处就不一一分析了,可以参数指令BL,它们的区别:是否将B/BL指令的下一条指令的地址存储到寄存器LR,BL指令会存储,B指令不会存储。

5.2.2.4 add/sub

1 mov r1, #10
2 add r2, r1, #4
3 sub r2, r1, #4

​ 第1行,将寄存器r1加上4后,赋值给寄存器r2

​ 第2行,将寄存器r1减去4后,赋值给寄存器r2

​ 指令执行过程,如下:

​ CPU从内存的addrA+4地址取机器码e2812004(即add r2, r1, #4指令),执行后,CPU内部寄存器R2等于14

​ CPU从内存的addrA+8地址取机器码e2412004(即sub r2, r1, #4指令),执行后,CPU内部寄存器R2等于6

​ 其中,机器码e2812004,ADD指令各位的解析如下:

​ [19: 16]位是源寄存器R1,即1;[15: 12]位是目标寄存器R2,即2;[11: 0]位是立即数4,即0x004;

​ 其中,机器码e2412004,SUB指令各位的解析如下:

​ [19: 16]位是源寄存器R1,即1;[15: 12]位是目标寄存器R2,即2;[11: 0]位是立即数4,即0x004;

5.2.2.5 ldr/str

1 mov r0, #400H @ 0x400
2 mov r1, #aH   @ 0xa
3 str r1, [r0]
4 ldr r2, [r0]

​ 第3行,将寄存器R1的值0xa存储到寄存器R0指向的地址0x400

​ 第4行,将寄存器R0指向地址0x400的数据赋值给寄存器R2

​ 指令执行过程,如下:

  1. CPU从内存的addrA地址取机器码e3a00b01(即mov r0, #400H指令),执行后,CPU内部寄存器R0等于0x400

  2. CPU从内存的addrA+4地址取机器码e3a0100a(即mov r1, #aH指令),执行后,CPU内部寄存器R1等于0xa

  3. CPU从内存的addrA+8地址取机器码e5801000(即str r1, [r0]指令),执行后,寄存器R1的0xa数据存储到寄存器R0指向的地址0x400,即内存的0x400地址的值为0xa

  4. CPU从内存的addrA+12地址取机器码e5902000(即ldr r2, [r0]指令),执行后,寄存器R0指向的地址0x400的数据存储到CPU内部寄存器R2,即CPU内部寄存器R2等于0xa

    其中,机器码e5801000,STR指令各位的解析如下:

​ [19: 16]位是目标寄存器R0,即0;[15: 12]位是源寄存器R1,即1;

​ 其中,机器码e5902000,LDR指令各位的解析如下:

​ [19: 16]位是源寄存器R0,即0;[15: 12]位是目标寄存器R2,即2;

ldr sp,=0x80200000

​ 这个是一条伪指令,即实际中并不存在这个指令,它会被拆分成几个真正的ARM指令,实现一样的效果,将0x80200000赋值给寄存器sp,即sp=0x80200000

​ 指令执行过程,如下:

​ ldr sp,=0x80200000这条伪指令,被翻译成两条指令来执行,先将0x80200000存储到内存地址addrA+4处,然后通过LDR指令把寄存器SP设置成0x80200000。

​ 如何分析ldr sp, [pc, #-4]指令的机器码e51fd004?读者可以根据上图LDR指令机器码的格式,自行进行分析。温馨提示:imm12[11: 0]位是源寄存器Rn的偏移值。

5.2.2.6 ldm/stm

​ ldm,多数据加载,将某地址的值赋值给某寄存器

​ stm,多数据存储,将某寄存器的值存储到某地址

​ 格式:

ldm{cond} Rn{!}, reglist
stm{cond} Rn{!}, reglist

参数说明:

​ cond:前四个条件是用于数据块操作,后四个条件是用于堆栈操作

​ IA : 每次传送后地址加4,其中寄存器从左到右执行,例如:STMIA R0,{R1,LR} 先存R1,再存LR

​ IB : 每次传送前地址加4,同上

​ DA : 每次传送后地址减4,其中寄存器从右到左执行,例如:STMDA R0,{R1,LR} 先存LR,再存R1

​ DB : 每次传送前地址减4,同上

​ FD : 满递减堆栈

​ FA : 满递增堆栈

​ ED : 空递减堆栈

​ EA : 空递增堆栈

​ Rn:基址寄存器,即寄存器的值是起始地址

​ !:表示最后的地址写回到Rn中

​ reglist:表示寄存器范围,用 , 隔开,如{R1,R2,R6-R9}

​ 数据块操作:

1 ldr r1,=0x10000000     
2 
3 ldmib r1!, {r0,r4-r6}
4 stmda r1!, {r0,r4-r6}

​ 第1行,将起始地址0x10000000赋值给r1

​ 第3行,因为使用ib,所以每次传送前地址加4,具体操作如下:

​ 将0X10000004地址的内容赋值给R0

​ 将0X10000008地址的内容赋值给R4

​ 将0X1000000C地址的内容赋值给R5

​ 将0X10000010地址的内容赋值给R6

​ 由于!,最后的地址写回到R1中,R1=0X10000010

​ 第4行,因为使用da,所以每次传送后地址减4,具体操作如下:

​ 将R6存储到0X10000010地址

​ 将R5存储到0X1000000C地址

​ 将R4存储到0X10000008地址

​ 将R0存储到0X10000004地址

​ 由于!,最后的地址写回到R1中,R1=0X10000000

​ 如下图所示:

​ 堆栈操作:满递减堆栈

1 ldr sp,=0x80200000
2
3 stmfd sp!, {r0-r2} @ 入栈
4 ldmfd sp!, {r0-r2} @ 出栈

​ 第1行,将0x80200000赋值给sp,作为堆栈的起始地址

​ 第3行,入栈,具体操作如下:

​ 将R2存储到0X80200000地址

​ 将R1存储到0X801FFFFC地址

​ 将R0存储到0X801FFFF8地址

​ 第4行,出栈,具体操作如下:

​ 将0X801FFFF8地址的内容赋值给R0

​ 将0X801FFFFC地址的内容赋值给R1

​ 将0X80200000地址的内容赋值给R2

​ 如下图所示:

​ 上述第3,4行汇编代码,就是所谓的入栈,出栈。也可以用push,pop指令完成入栈,出栈,如下

1 ldr sp,=0x80200000
2
3 push {r0-r2} @ 入栈
4 pop {r0-r2}  @ 出栈

5.3 进制

​ 目前计算机对数据的表示方式,有十六进制、十进制、八进制与二进制。

5.3.1 如何理解它们的区别?

​ 十六进制,逢十六进一,每一位由0~F组成,习惯用0x前缀表示或用H后缀表示

0xA或AH

​ 十进制,逢十进一,每一位由0~9组成,无前缀或用D后缀表示

10或10D

​ 八进制,逢八进一,每一位由0~7组成,习惯用0前缀表示或用O后缀表示

012或12O

​ 二进制,逢二进一,每一位由0~1组成,习惯用0b前缀表示或用B后缀表示

0b1010或1010B

5.3.2 在C语言中怎么表示这些进制呢?

十六进制:int a = 0xA;   // 0x前缀
十进制:  int a = 10;
八进制:  int a = 012;   // 0前缀
二进制:  int a = 0b1010;// 0b前缀

5.3.3 十六进制与二进制转换关系

​ 在嵌入式开发中经常需要对十六进制与二进制进行转换

​ 如何快速的转换2/16进制? 首先记住8 4 2 1 ——>二进制权重

​ 将二进制0b01101110101转换成十六进制:将二进制从右到左,每四个分成一组:

​ 结果就是0x375

​ 将十六进制0xABC1转换成二进制:将十六进制从右到左,每个分成四位:

​ 结果就是1010 1011 1100 0001

5.4 大/小端模式与位操作

5.4.1 大/小端模式

​ 大端模式(Big-endian),是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中

​ 小端模式(Little-endian),是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中

​ 比如:0x12345678,在大/小端模式的存储位置如下:

内存地址 大端模式 小端模式
addr+3 0x78 0x12
addr+2 0x56 0x34
addr+1 0x34 0x56
addr 0x12 0x78

5.4.2 位操作

5.4.2.1 移位

1 int a = 0x6; // 二进制是0b0110
2 int b = a<<1;
3 int c = a>>1;

​ 第2行,对a左移一位,从0b0110->0b1100,即b=0xC

​ 第3行,对a右移一位,从0b0110->0b0011,即b=0x3

5.4.2.2 取反

1 int a = 0x6; // 二进制是0b0110
2 int b = ~a;

​ 第2行,对a按位取反,从0b0110->0b1001,即b=0x9

5.4.2.3 位与

​ 只有对应的两个二进位都为1时,结果位才为1

1 int a = 0x6; // 二进制是0b0110
2 int b = 0x7; // 二进制是0b0111
3 
4 int c = a&b;

​ 第4行,a&b,二进制是0b0110,即c=0x6

5.4.2.4 位或

​ 只要对应的二个二进位有一个为1时,结果位就为1

1 int a = 0x6; // 二进制是0b0110
2 int b = 0x7; // 二进制是0b0111
3 
4 int c = a|b;

​ 第4行,a|b,二进制是0b0111,即c=0x7

5.4.2.5 置位

1 int a = 0x6;     // 二进制是0b0110
2 
3 int a |= (1<<3);

​ 第3行,将变量a的bit3置1。1<<3 = 0b1000,然后0b1000|0b0110=0b1110,即a=0xe

5.4.2.6 清位

1 int a = 0x6;     // 二进制是0b0110
2 
3 int a &= ~(1<<2);

​ 第3行,将变量a的bit2清位。~(1<<2) = 0b1011,然后0b1011&0b0110=0b0010,即a=0x2

5.5 汇编程序调用C程序

​ 在C程序和ARM汇编程序之间相互调用时必须遵守ATPCS规则,ATPCS规定了一些函数间调用的基本规则。

​ 参考资料:

5.5.1 ATPCS规则

​ ATPCS即ARM-THUMB procedure call standard(ARM-Thumb过程调用标准)的简称,是基于ARM指令集和THUMB指令集过程调用的规范,规定了调用函数如何传递参数,被调用函数如何获取参数,以何种方式传递函数返回值。

​ 寄存器R0~R15在ATPCS规则的使用:

5.5.2 汇编程序如何向C程序的函数传递参数

5.5.3 C程序如何返回结果给汇编程序

5.5.4 C函数为何要用栈

​ 总的来说,栈的作用就是:保存现场/上下文,传递参数

​ 保存现场,也叫保存上下文

​ 现场,相当于案发现场,总有一些现场的情况,要记录下来的,否则被别人破坏掉之后,你就无法恢 复现场了。而此处说的现场,就是指CPU运行的时候,用到了一些寄存器,比如R0~R3,LR等等,对于这些寄存器的值,如果你不保存而直接跳转到函数中去执行,那么很可能会被破坏了,因为函数执行需要用到这些寄存器。

​ 因此在函数调用之前,应该将这些寄存器等现场,暂时保持起来,等调用函数执行完毕返回后,再恢复现场,这样CPU就可以正确的继续执行了。

​ 保存寄存器的值,一般用的是push指令,将对应的某些寄存器的值,一个个放到栈中,即所谓的入栈

​ 然后待被调用的子函数执行完毕的时候,再调用pop,把栈中的一个个的值,赋值给对应的入栈的寄存器,即所谓的出栈

​ 当函数被调用并且参数大于4个时,(不包括第4个参数)第4个参数后面的参数就保存在栈中。

5.6 C语言中读写寄存器

​ 每一个寄存器都有一个地址,只要找到寄存器地址,通过指针指向寄存器地址单元,通过读写指针值,就可以获得寄存器值。

​ 首先,定义一个指针,指针类型根据寄存器大小决定,同时需要加上volatile关键字让编译器不要优化此指针,比如,CCM_CCGR1寄存器值是32位,此处定义为unsigned int *指针类型,寄存器地址为0x20C406C

volatile unsigned int *CCM_CCGR1 = (volatile unsigned int *)(0x20C406C);

​ 然后,对寄存器进行读写操作

val = *CCM_CCGR1;       // 读寄存器
*CCM_CCGR1 |= (3<<30);  // 写寄存器,将CCM_CCGR1寄存器的[31:30]位置1

5.7 start.S解析

2 .text
3 .global _start
4 _start:
@ 相当于一个函数,_start是函数名,下面汇编指令是函数内容
4 _start:
5
6  //设置栈
7  ldr sp,=0x80200000
8
9  bl clean_bss
10
11 bl main
12
13 halt:
14 b halt
@ 相当于一个函数,clean_bss是函数名,下面汇编指令是函数内容
16 clean_bss:
17 /* 清除BSS段 */
18 ldr r1, =__bss_start
19 ldr r2, =__bss_end
20 mov r3, #0
21 clean:       @ 下面汇编指令相当于循环体,直到R1与R2相等
22 str r3, [r1]
23 add r1, r1, #4
24 cmp r1, r2
25 bne clean
26
27 	mov pc, lr @ 函数执行完毕,返回

5.8 根据led.dis分析代码的整体运行流程

​ 在分析led.dis文件前,我们再把imx6ull芯片如何将led.bin文件复制到内存DDR中过程,简单整体过一篇。

​ 如下图,imx6ull芯片一上电后,会先执行bootRom程序,此程序是芯片出厂时已经固定的程序,除了芯片原厂,咱们是无法修改的。

bootRom有什么作用?下面一一讲解。

  1. bootRom会把EMMC或TF卡的前4K数据读入到芯片内部RAM运行

  2. bootRom根据DCD进行初始化DDR。

  3. bootRom根据IVT,从EMMC或TF卡中将led.bin读到DDR的0x80100000地址

  4. 跳转到DDR的0x80100000地址执行

​ 目前led.bin程序已经复制到内存中,CPU开始从内存0x80100000地址开始执行机器码,每一条机器码是32位/4字节,此处的机器码就是led.bin中的机器码,那我们能不能打开led.bin文件,看到里面的机器码?答案是可以的。如下图:

​ 前面介绍过大/小端模式,你是否记得?如果忘记了,可以回头看一下。 ​ 此处可以看到机器码e59fd028(指令:ldr sp,=0x80200000)的存储形式:

地址 机器码
00000000 28
00000001 d0
00000002 9f
00000003 e5

​ 没错,imx6ull的存储方式是小端模式,换一句话说,ARM存储方式一般都是小端模式。 ​ 但是bin文件的机器码不方便阅读,所以我们一般会通过objdump进行反汇编,得到人类容易读的led.dis文件。 ​ 如下图:

​ 下面我们就来分析一下led.dis文件,但是在阅读此小节前,尽量把前一小节《1.7 start.S解析》完全理解懂,不然阅读此小节,有点云里雾里。

    1)  CPU执行的第一条机器码就是内存地址0x80100000存储的e59fd028机器码,对应的指令是ldr sp, [pc, #40],相当于Start.S文件的ldr sp,=0x80200000指令,寄存器SP的值等于0x80200000
80100000: e59fd028 ldr sp, [pc, #40] ; 80100030 <clean+0x14>
  1. 每执行完一条机器码,会自动执行下一条内存地址0x80100004存储的eb000001机器码,对应的指令是bl 80100010,相当于Start.S文件的bl clean_bss指令。
80100004: eb000001 bl 80100010 <clean_bss>
....
80100010 <clean_bss>:
80100010: e59f101c ldr r1, [pc, #28] ; 80100034 <clean+0x18>
80100014: e59f201c ldr r2, [pc, #28] ; 80100038 <clean+0x1c>
  1. 跳转到内存地址0x80100010,执行e59f101c机器码,对应的指令是ldr r1, [pc, #28],相当于Start.S文件的ldr r1, =__bss_start指令。

  2. 此处clean_bss相当于一个函数体,CPU会自动让内存地址加4,向下执行机器码,直到执行mov pc, lr指令后,才返回内存地址0x80100008处执行fa000057机器码,对应的指令是blx 8010016c,相当于Start.S文件的bl main指令。

    到此,CPU跳转到C语言的main()函数,继续执行。

    为了让大家深入理解C语言函数的调用执行过程中,汇编指令如何执行,此处简单分析main()函数

​ 如上图所示

​ 1.进入main()函数后,先将寄存器R7、LR入栈,保存现场/上下文,方便main()函数执行完毕后返回,并且将当前栈指向的内存地址赋值给寄存器R7,如图

​ 2. 调用led_init()函数,因为没有参数传递,所以直接调用BL指令进行跳转,即bl 8010003c指令

​ 3. 调用led_ctl(1)函数,此处只有一个参数,通过寄存器R0进行传递,即movs r0, #1指令,然后通过BL指令进行跳转,即bl 801000f8指令,关于参数传递问题,可以参考前面《5.5 汇编程序调用C程序》。

​ 4. 调用delay(1000000)函数,此处只有一个参数,通过寄存器R0进行传递,然后通过BL指令进行跳转

​ 5. 调用led_ctl(0)函数,此处只有一个参数,通过寄存器R0进行传递,然后通过BL指令进行跳转

​ 6. 调用delay(1000000)函数,此处只有一个参数,通过寄存器R0进行传递,然后通过BL指令进行跳转

​ 7. while(1)循环体到此已经结束,但是需要循环执行循环体的内容,通过B指令进行跳转到循环体开头,即b.n 80100174指令,执行内存地址0x80100174处的指令,也就是led_ctl(1)函数对应的汇编指令movs r0, #1

​ 到此,进入并执行main()函数对应的汇编指令分析已经结束,如果读者有兴趣可以分析一下,led_init()、led_ctl()与delay()函数的汇编指令。

第六章:Makefile与GCC

第六章:Makefile与GCC

6. Makefile与GCC

6.1 交叉编译器

6.1.1 什么是交叉编译

​ 简单地说,我们在PC机上编译程序时,这些程序是在PC机上运行的。我们想让一个程序在ARM板子上运行,怎么办?

​ ARM板性能越来越强,可以认为ARM板就相当于一台PC,当然可以在ARM板上安装开发工具,比如安装ARM版本的GCC,这样就可以在ARM板上编译程序,在ARM板上直接运行这个程序。

​ 但是,有些ARM板性能弱,或者即使它的性能很强也强不过PC机,所以更多时候我们是在PC机上开发、编译程序,再把这个程序下载到ARM板上去运行。

​ 这就引入一个问题:

​ 1) 我们使用工具比如说gcc编译出的程序是给PC机用的,这程序里的指令是X86指令。

​ 2)那么能否使用同一套工具给ARM板编译程序?

​ 显示不行,因为X86的指令肯定不能在ARM板子上运行。所以我们需要使用另一套工具:交叉编译工具链。

​ 为何叫“交叉”?

​ 首先,我们是在PC机上使用这套工具链来编译程序;

​ 然后再把程序下载到ARM板运行;

​ 如果程序不对,需要回到PC机修改程序、编译程序,再把程序下载到ARM板上运行、验证。如此重复。

​ 在这个过程中,我们一会在PC上写程序、编译程序,一会在ARM板上运行、验证,中间来来回回不断重复,所以称之为“交叉”。对于所用的工具链,它是在PC机上给ARM板编译程序,称之为“交叉工具链”。

​ 有很多种交叉工具链,举例如下:

​ 1) Ubuntu平台:交叉工具链有arm-linux-gcc编译器

​ 2) Windows 平台:利用ADS(ARM开发环境),使用armcc编译器。

​ 3) Windows平台:利用cygwin环境,运行arm-elf-gcc编译器。

6.1.2 为什么需要使用交叉编译

​ 1) 因为有些目的平台上不允许或不能够安装所需要的编译器,而我们又需要这个编译器的某些功能;

​ 2) 因为有些目的平台上的资源贫乏,无法运行我们所需要编译器;

​ 3) 因为目的平台还没有建立,连操作系统都没有,根本谈不上运行什么编译器。

6.1.3 验证实例

​ 下面这个例子,我们准备源文件main.c,然后我们采用gcc编译后可执行程序放在目标板上运行看看是否能运行起来,如下:

​ main.c

01 	#include <stdio.h>
02
03	int main()
04	{
05		printf("100ask\n");
06		return 0;
07	}

​ 在虚拟机编译运行:

$ gcc main.c –o 100ask
$ ./100ask
100ask
$

​ 在上面的运行结果,没有任问题,然后我们将这个可执行程序放到目标板上,如下:

$ chmod 777 100ask
$ ./100ask
./100ask: line 1: syntax error: unexpected “(”
$

​ 报错无法运行。说明为X86平台制作的可执行文件,不能在其他架构平台上运行。交叉编译就是为了解决这个问题。

​ 为了方便实验,我们在Ubuntu中使用gcc来做实验,如果想使用交叉编译,参考章节《第二章1.2 安装SDK、设置工具链》,安装好工具链,设置好环境变量后,将所有的gcc替换为arm-linux- gcc就可以完成交叉编译。

​ 其中:

​ gcc是在x86架构指令用的。

​ arm-linux- gcc是RSIC(精简指令集)ARM架构上面使用。

​ 他们会把源程序编译出不同的汇编指令然后生成不同平台的可执行文件。

6.2 gcc编译器1_gcc常用选项__gcc编译过程详解

6.2.1 gcc编译过程详解

​ 一个C/C++文件要经过预处理(preprocessing)、编译(compilation)、汇编(assembly)和连接(linking)等4步才能生成可执行文件,编译流程图如下。

6.1.2.1 预处理:

​ C/C++源文件中,以“#”开头的命令被称为预处理命令,如包含命令“#include”、宏定义命令“#define”、条件编译命令“#if”、“#ifdef”等。预处理就是将要包含(include)的文件插入原文件中、将宏定义展开、根据条件编译命令选择要使用的代码,最后将这些东西输出到一个“.i”文件中等待进一步处理。

6.1.2.2 编译:

​ 对预处理后的源码进行词法和语法分析,生成目标系统的汇编代码文件,后缀名为“.s”。

6.1.2.3 汇编:

​ 对汇编代码进行优化,生成目标代码文件,后缀名为“.o”。

6.1.2.4 链接:

​ 解析目标代码中的外部引用,将多个目标代码文件连接为一个可执行文件。

​ 编译器利用这4个步骤中的一个或多个来处理输入文件,源文件的后缀名表示源文件所用的语言,后缀名控制着编译器的缺省动作

后缀名 语言种类 后期操作
.c C源程序 预处理、编译、汇编
.C C++源程序 预处理、编译、汇编
.cc C++源程序 预处理、编译、汇编
.cxx C++源程序 预处理、编译、汇编
.m Objective-C源程序 预处理、编译、汇编
.i 预处理后的C文件 编译、汇编
.ii 预处理后的C++文件 编译、汇编
.s 汇编语言源程序 汇编
.S 汇编语言源程序 预处理、汇编
.h 预处理器文件 通常不出现在命令行上

​ 其他后缀名的文件被传递给连接器(linker),通常包括:

​ .o:目标文件(Object file,OBJ文件)

​ .a:归档库文件(Archive file)

​ 在编译过程中,除非使用了“-c”,“-S”或“-E”选项(或者编译错误阻止了完整的过程),否则最后的步骤总是连接。在连接阶段中,所有对应于源程序的.o文件,“-l”选项指定的库文件,无法识别的文件名(包括指定的“.o”目标文件和“.a”库文件)按命令行中的顺序传递给连接器。

6.2.2 gcc命令

​ gcc命令格式是:

gcc [选项] 文件列表

​ gcc命令用于实现c程序编译的全过程。文件列表参数指定了gcc的输入文件,选项用于定制gcc的行为。gcc根据选项的规则将输入文件编译生成适当的输出文件。

​ gcc的选项非常多,常用的选项,它们大致可以分为以下几类 。并且使用一个例子来描述这些选项,创建一个mian.c源文件,代码为如下:

​ main.c:

01 	#include <stdio.h>
02
03	#define HUNDRED 100
04
05	int main()
06	{
07		printf("%d ask\n",HUNDRED);
08		return 0;
09	}

注明: 代码目录在裸机Git仓库 NoosProgramProject/ (6_Makefile与GCC/001_gcc_01001_gcc_01)文件夹下。

6.2.2.1 过程控制选项

​ 过程控制选项用于控制gcc的编译过程。无过程控制选项时,gcc将默认执行全部编译过程,产生可执行代码。常用的过程控制选项有:

​ (1)预处理选项(-E)

​ C/C++源文件中,以“#”开头的命令被称为预处理命令,如包含命令“#include”、宏定义命令“#define”、条件编译命令“#if”、“#ifdef”等。预处理就是将要包含(include)的文件插入原文件中、将宏定义展开、根据条件编译命令选择要使用的代码,最后将这些东西输出到一个“.i”文件中等待进一步处理。使用例子如下:

$ gcc -E main.c -o main.i

​ 运行结果,生成main.i,main.i的内容(由于头文件展开内容过多,我将截取部分关键代码):

extern int ftrylockfile (FILE *__stream) __attribute__  ((__nothrow__ , __leaf__)) ;

extern void funlockfile (FILE *__stream) __attribute__  ((__nothrow__ , __leaf__));

# 942 "/usr/include/stdio.h" 3 4

# 2 "main.c" 2

# 5 "main.c"

int main()

{

printf("%d ask\n",100);

return 0;

}

你会发现头文件被展开和printf函数中调用HUNDRED这个宏被展开。

​ (2)编译选项(-S)

​ 编译就是把C/C++代码(比如上述的“.i”文件)“翻译”成汇编代码。使用例子如下:

$ gcc -S main.c -o main.s

​ 运行结果,生成main.s,main.s的内容:

  1         .file   "main.c"
  2         .text
  3         .section        .rodata
  4 .LC0:
  5         .string "%d ask\n"
  6         .text
  7         .globl  main
  8         .type   main, @function
  9 main:
 10 .LFB0:
 11         .cfi_startproc
 12         pushq   %rbp
 13         .cfi_def_cfa_offset 16
 14         .cfi_offset 6, -16
 15         movq    %rsp, %rbp
 16         .cfi_def_cfa_register 6
 17         movl    $100, %esi
 18         leaq    .LC0(%rip), %rdi
 19         movl    $0, %eax
 20         call    printf@PLT
 21         movl    $0, %eax
 22         popq    %rbp
 23         .cfi_def_cfa 7, 8
 24         ret
 25         .cfi_endproc
 26 .LFE0:
 27         .size   main, .-main
 28         .ident  "GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0"
 29         .section        .note.GNU-stack,"",@progbits

​ (3)汇编选项(-c)

​ 汇编就是将上述的“.s”文件汇编代码翻译成符合一定格式的机器代码,在Linux系统上一般表现为ELF目标文件(OBJ文件)

$ gcc -c main.c -o main.o

​ 运行结果,生成main.o(将源文件转为一定格式的机器代码)。

6.2.2.2 输出选项

​ 输出选项用于指定gcc的输出特性等,常用的选项有:

​ (1)输出目标选项(-o filename

​ -o选项指定生成文件的文件名为filename。使用例子如下

$ gcc main.c -o main

​ 运行结果,生成可执行程序main,如下:

$ ls
main.c main
$ ./main
$ 100 ask

​ 其中,如果无此选项时使用默认的文件名,各编译阶段有各自的默认文件名,可执行文件的默认名为a.out。使用例子如下:

$ gcc main.c

​ 运行结果,生成可执行文件a.out,如下:

$ ls
a.out main.c
$ ./a.out
$ 100 ask

​ (2)输出所有警告选项(-Wall)

​ 显示所有的警告信息,而不是只显示默认类型的警告。建议使用。我们见上面的main.c稍微修改一下,b此节代码目录在裸机Git仓库 NoosProgramProject/(6_Makefile与GCC/001_gcc_02)文件夹下,如下:

​ main.c:

01 	#include <stdio.h>
02
03	#define HUNDRED 100
04
05	int main()
06	{
07		int a = 0;
08		printf("%d ask\n",HUNDRED);
09		return 0;
10	}

​ 编译不添加-Wall选项编译,没有任何警告信息,编译结果如下:

$ gcc main.c -o main.c

​ 编译添加-Wall选项编译,现实所有警告信息,编译结果如下:

$ gcc main.c -Wall -o main.c

main.c: In function ‘main’:

main.c:7:6: warning: unused variable ‘a’ [-Wunused-variable]

 int a=0;

   ^

6.2.2.3 头文件选项

​ 头文件选项(-Idirname

​ 将dirname目录加入到头文件搜索目录列表中。当gcc在默认的路径中没有找到头文件时,就到本选项指定的目录中去找。在上面的例子中创建一个目录,然后创建一个头文件test.h。然后main.c里面增加#include“test.h”,代码目录在裸机Git仓库 NoosProgramProject/(6_Makefile与GCC/001_gcc_03) 文件夹下,使用例子如下:

$ tree

.

├── inc

│  └── test.h

└── main.c

 

1 directory, 2 files

$

​ test.h:

01 #ifndef __TEST_H

02 #define __TEST_H

03 /*

04   code

05 */

06 #endif

​ 运行结果,这样就可以引用指定文件下的目录的头文件,如下:

$ gcc main.c -I inc -o main

​ 如果不添加头文件选项,编译运行结果,如下:

$ gcc main.c -o main

  main.c:2:18: fatal error: test.h: No such file or directory

  compilation terminated.

​ 会产生错误提示,无法找到test.h头文件。

6.2.2.3 链接库选项

(详细使用方法查看下一节:gcc编译器2_深入讲解链接过程)

​ 1) 添加库文件搜索目录(-Ldirname

​ 将dirname目录加入到库文件的搜索目录列表中。

​ 2) 加载库名选项(-lname

​ 加载名为libname.a或libname.so的函数库。例如:-lm表示链接名为libm.so的函数库。

​ 3) 静态库选项(-static)

​ 使用静态库。注意:在命令行中,静态库夹在的库必须位于调用该库的目标文件之后。

6.2.2.4 代码优化选项

​ gcc提供几种不同级别的代码优化方案,分别是0,1,2,3和s级,用-Olevel选项表示。默认0级,即不进行优化。典型的优化选项:

​ (1)-O :基本优化,使代码执行的更快。

​ (2)-O2:胜读优化,产生尽可能小和快的代码。如无特殊要求,不建议使用O2以上的优化。

​ (3)-Os:生成最小的可执行文件,适合用于嵌入式软件。

6.2.2.5 调试选项

​ 代码目录在**git仓库(6_Makefile与GCC/001_gcc_02)**文件夹下

​ gcc支持数种调试选项:

​ -g 产生能被GDB调试器使用的调试信息。

​ 调试例子如下,首先需要编译,操作步骤如下:

$ gcc main.c -g -o main

​ GDB调试示例:

​ (1)run命令

​ 调试运行,使用run命令开始执行被调试的程序,run命令的格式:

​ run [运行参数]

$ gdb -q main		<---进入调试程序
Reading symbols from output...done.
(gdb) run			<---开始执行程序
Starting program: /home/100ask/makefile/ 
100 ask
[Inferior 1 (process 7425) exited normally]
(gdb)

​ (2)list命令

​ 列出源代码,使用list命令来查看源程序以及行号信息,list命令的格式:

​ list [行号]

(gdb) list 1			<---列出地一行附近的源码,每次10行
#include <stdio.h>

#define HUNDRED 100

int main()
{
	int a = 100;

	printf("%d ask\n",HUNDRED);
	return 0;
(gdb) <Enter>			<---按Enter键,列出下10行源码
}
(gdb) 

​ (3)设置断点

​ 1)break命令,设置断点命令,break命令的格式: break <行号> | <函数名>

(gdb) break 7

Breakpoint 1 at 0x40052e: file main.c, line 7.

(gdb)  

​ 2)info break命令,查 看断点命令:

(gdb) info break

Num  Type    Disp Enb Address      What

1  breakpoint  keep y  0x000000000040052e in main at main.c:7

(gdb)  

​ 3)delete breakpoint命令,删除断点命令, delete breakpoint命令的格式: delete breakpoint <断点号>

(gdb) delete breakpoint 1

(gdb) info break

No breakpoints or watchpoints.

(gdb) 

​ (4)跟踪运行结果

​ 1)print命令,显示变量的值,print命令的格式:print[/格式] <表达式>

​ 2)display命令,设置自动现实命令,display命令的格式: display <表达式>

​ 3)step和 next命令,单步执行命令,step和next命令的格式:step <行号> 或 next <行号>

​ 4)continue命令,继续执行命令。

(gdb) break 7
Breakpoint 1 at 0x40052e: file main.c, line 7.
(gdb) break 9
Breakpoint 2 at 0x400535: file main.c, line 9.
(gdb) run
Starting program:/home/100ask/makefile/

Breakpoint 1, main () at main.c:7
7		int a = 100;
(gdb) continue
Continuing.

Breakpoint 2, main () at main.c:9
9		printf("%d ask\n",HUNDRED);
(gdb) print a
$1 = 100
(gdb) 

6.2.3 编译错误警告

​ 在写代码的时候,其实应该养成一个好的习惯就是任何的警告错误,我们都不要错过,

​ 编译错误必然是要解决的,因为会导致生成目标文件。但是警告可能往往会被人忽略,但是有时候,编译警告会导致运行结果不是你想要的内容。接下来我们来简单分析一下gcc的编译警告如何处理,例子如下:

​ main.c

 01 #include <stdio.h>
 02 #include "hander.h"
 03 
 04 int main()
 05 {
 06         float a = 0.0;
 07         int b = a;
 08         char c = 'a'
 09 
 10         printf("100ask: \n",a);
 11 
 12         return 0;
 13 }

​ 上面文件中有三处错误:

​ 第2行:包含了一个不存在的头文件。

​ 第8行:语句后面没有加分号。

​ 第10行:书写格式错误,变量a没有对应的输出格式。

​ 我们对上面的文件进行编译,还记得上面我们讲的编译警告选项吗?我们在编译的时候加上它(-Wall),如下:

$ gcc main.c -Wall -o output
main.c: In function ‘main’:
main.c:2:20: fatal error: hander.h: No such file or directory
compilation terminated.

​ 错误警告信息分析:在展开第二行的hander.h头文件的时候,产生编译错误,没有hander.h文件或者目录。接着我们把hander.h头文件去掉,在编译一次:

$ gcc -Wall main.c  -o output
main.c: In function ‘main’:
main.c:10:2: error: expected ‘,’ or ‘;’ before ‘printf’
  printf("100ask: \n",a);
  ^
main.c:8:7: warning: unused variable ‘c’ [-Wunused-variable]
  char c = 'a'
       ^
main.c:7:6: warning: unused variable ‘b’ [-Wunused-variable]
  int b = a;
      ^

​ 错误警告信息分析:有一个错误和两个警告。一个错误是指第10行prntf之前缺少分号。两个警告是指第7行和第8行的变量没有使用。那么我继续解决错误信息和警告,将两个警告的变量删除和printf前添加分号,然后继续编译,如下:

$ gcc -Wall main.c  -o output
main.c: In function ‘main’:
main.c:8:9: warning: too many arguments for format [-Wformat-extra-args]
  printf("100ask: \n",a);
         ^

​ 错误警告信息分析:还是有警告信息,该警告指的是printf中的格式参数太多,也就是没有添加变量a的输出格式,继续解决错误信息和警告,添加变量a的输出格式,然后继续编译,如下:

$ gcc -Wall main.c  -o output
$ tree
.
├── main.c
└── output

​ 最终编译成功,输出目标文件。

6.3 gcc编译器2_深入讲解链接过程

​ 你会发现,可执行文件文件会比源代码大了。这是因为编译的最后一步链接会解析代码中的外部应用。然后将汇编生成的OBJ文件,系统库的OBJ文件,库文件链接起来。它们全部链接生成最后的可执行文件,从而使可执行文件比源代码大。我们用一个例子来说明上面描述,代码使用**(代码目录在裸机Git仓库 NoosProgramProject/(6_Makefile与GCC/001_gcc_01)文件夹下)**如下:

$ gcc main.c -c
$ gcc -o output main.o
$ gcc -o output_static main.o  --static
$ ls -alh
drwxrwxr-x 2 tym tym 4.0K 2月  20 07:27 .
drwxrwxr-x 6 tym tym 4.0K 2月  20 07:25 ..
-rw-rw-r-- 1 tym tym   96 2月  20 07:25 main.c
-rw-rw-r-- 1 tym tym 1.5K 2月  20 07:26 main.o
-rwxrwxr-x 1 tym tym 8.5K 2月  20 07:27 output
-rwxrwxr-x 1 tym tym 892K 2月  20 07:27 output_static

​ 从上面的例子可以看出output_static比output大很多。

6.3.1 动态链接库和静态链接库使用例程

​ 静态库和动态库,是根据链接时期的不同来划分。

​ 静态库:在链接阶段被链接的,所以生成的可执行文件就不受库的影响,即使库被删除,程序依然可以成功运行。链接静态库从某种意义上来说是一种复制粘贴,被链接后库就直接嵌入可执行程序中了,这样系统空间有很大的浪费,而且一旦发现系统中有bug,就必须一一把链接该库的程序找出来,然后重新编译,十分麻烦。静态库是不是一无是处了呢?不是的,如果代码在其他系统上运行,且没有相应的库时,解决办法就是使用静态库。而且由于动态库是在程序运行的时候被链接,因此动态库的运行速度比较慢。

​ 动态库:是在程序执行的时候被链接的。程序执行完,库仍需保留在系统上,以供程序运行时调用。而动态库刚好弥补了这个缺陷,因为动态库是在程序运行时被链接的,所以磁盘上只需保留一份副本,一次节约了空间,如果发现bug或者是要升级,只要用新的库把原来的替换掉就可以了。

​ 下面我们创建三个文件main.c,add.c,add.h,讲解静态库链接和动态库链接,如下:

​ main.c:

#include <stdio.h>
#include "add.h"

int main(int argc, char *argv[])
{
	printf("%d\n",add(10, 10));
	printf("%d\n",add(20, 20));
	return 0;
}

​ add.c:

#include "add.h"

int add(int a, int b)
{
	return a + b;
}

​ add.h:

#ifndef __ADD_H
#define __ADD_H

int add(int a, int b);

#endif

**注明:**代码目录在裸机Git仓库 NoosProgramProject/(6_Makefile与GCC/001_gcc_04)文件夹下。

6.3.1.1 静态库链接

​ 静态库名字一般为“libxxx.a”。利用静态库编译生成的可执行文件比较大,因为整个函数库的所有数据都被整合进了可执行文件中。

优点:

​ 1.不需要外部函数库支持。

​ 2.加载速度快。

缺点:

​ 1.静态库升级,程序需要重新编译。

​ 2.多个程序调用相同库,静态库会重复调入内存,造成内存的浪费。

​ 静态库的制作,如下:

$ gcc add.c -o add.o -c 
$ ar -rc libadd.a add.o

​ 静态库的使用,例子如下:

$ gcc main.c -o output -ladd -L.

​ 运行结果:

$ ./output
20
40

6.3.1.2 动态库链接

​ 动态库名字一般为“libxxx.so”,又称共享库。动态库在编译的时候没有被编译进可执行文件,所以可执行文件比较小。需要动态申请并调用相应的库才能运行。

​ **优点:**多个程序可以使用同一个动态库,节省内存。

​ **缺点:**加载速度慢。

​ 动态库的制作,如下:

$ gcc -shared -fPIC lib.c -o libtest.so 

$ sudo cp libtest.so /usr/lib/  

​ 动态库的使用,如下:

$ gcc main.c -L. -ltest -o output

​ 运行结果:

$ ./output
20
40

6.4 Makefile的引入及规则

6.4.1 为什么需要Makefile?

​ 在上一章节对GCC编译器描述,以及如何进行C源程序编译。在上一章节的例子中,我们都是在终端执行gcc命令来完成源文件的编译。感觉挺方便的,这是因为工程中的源文件只有一两个,在终端直接执行编译命令,确实快捷方便。但是现在一些项目工程中的源文件不计其数,其按类型、功能、模块分别放在若干个目录中,如果仍然使用在终端输入若干条命令,那显然不切实际,开发效率极低。程序员肯定不会被这些繁琐的事情,影响自己的开发进度。如果我们能够编写一个管理编译这些文件的工具,使用这个工具来描述这些源文件的编译,如何重新编译。为此“make”工具就此诞生。并且由Makefile负责管理整个编译流程,Makefile定义了一系列的规则来指定哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为 Makefile就像一个Shell脚本一样,也可以执行操作系统的命令,极大的提高了软件开发的效率。

6.4.2 Makefile的引入

​ Makefile的引入是为了简化我们编译流程,提高我们的开发进度。下面我们用一个例子来说明Makefile如何简化我们的编译流程。我们创建一个工程内容分别main.c,sub.c,sub.h,add.c,add.h五个文件。sub.c负责计算两个数减法运算,add.c负责计算两个数加法运算,然后编译出可执行文件。其源文件内容如下:

​ main.c:

#include <stdio.h>
#include "add.h"
#include "sub.h"

int main()
{
	printf("100 ask, add:%d\n", add(10, 10));
	printf("100 ask, sub:%d\n", sub(20, 10));
	return 0;
}

​ add.c:

#include "add.h"

int add(int a, int b)
{
	return a + b;
}

​ add.h:

#ifndef __ADD_H
#define __ADD_H

int add(int a, int b);

#endif 

​ sub.c:

#include "sub.h"

int sub(int a, int b)
{
	return a - b;
}

​ sub.h:

#ifndef __SUB_H
#define __SUB_H

int sub(int a, int b);

#endif 

代码目录在裸机Git仓库 NoosProgramProject/(6_Makefile与GCC/001_Makefile_01)文件夹下。

​ 我们使用gcc对上面工程进行编译及生成可执行程序,在终端输入如下命令,如下:

$ gcc main.c sub.c add.c -o ouput
$ ls
add.c  add.h  main.c  output  sub.c  sub.h
$ ./output 
100 ask, add:20
100 ask, sub:10

​ 上面的命令是通过gcc编译器对 main.c sub.c add.c这三个文件进行编译,及生成可执行程序output,并执行可执行文件产生结果。

​ 从上面的例子看起来比较简单,一条命令完成三个源程序的编译并产生结果,这是因为目前只有三个源文件,如果有上千个源文件,或者存在依赖文件的修改,你执行上面那条命令,将会重新编译,你会发现一天的工作都是等待漫长的编译。这样消耗的时间是非常恐怖的。我们肯定想哪个文件被修改了,只编译这个被修改的文件即可。其它没有修改的文件就不需要再次重新编译了,为此我们改变我们的编译方法,使用命令如下:

$ gcc -c main.c 
$ gcc -c sub.c
$ gcc -c add.c
$ gcc main.o sub.o add.o -o output

​ 我们将上面一条命令变成了四条,分别编译成源文件的目标文件,最后再将所有的目标文件链接成可执行文件。虽然这个增加了命令,但是可以解决,当其中一个源文件的内容发生了变化,我们只需要修改单独重新生成对应的目标文件,然后重新链接成可知执行文件,不用全部重新编译。假如我们修改了add.c文件,只需要重新编译生成add.c的目标文件,然后再将所有的.o文件链接成可执行文件,如下:

$ gcc -c add.c
$ gcc main.o sub.o add.o -o output

​ 这样的方式虽然可以节省时间,但是仍然存在几个问题,如下:

​ 1)如果源文件的数目很多,那么我们需要花费大量的时间,敲命令执行。

​ 2)如果源文件的数目很多,然后修改了很多文件,后面发现忘记修改了什么。

​ 3)如果头文件的内容修改,替换,更换目录,所有依赖于这个头文件的源文件全部需要重新编译。

​ 这些问题我们不可能一个一个去找和排查,所有引入Makefile,正可以解决上述问题。我们对上面的问题整理,编写Makefile,如下:

​ Makefile:

output: main.o add.o sub.o
        gcc -o output main.o add.o sub.o
main.o: main.c
        gcc -c main.c
add.o: add.c
        gcc -c add.c
sub.o: sub.c
        gcc -c sub.c

clean:
        rm *.o output

​ Makefile编写好后只需要执行make命令,就可以自动帮助我们编译工程。注意,make命令必须要在Makefile的当前目录执行,如下:

$ ls
add.c  add.h  main.c  Makefile  sub.c  sub.h
$ make
gcc -c main.c
gcc -c add.c
gcc -c sub.c
gcc -o output main.o add.o sub.o
$ ls
add.c  add.h  add.o  main.c  main.o  Makefile  output  sub.c  sub.h  sub.o

​ 通过make命令就可以生成相对应的目标文件.o和可执行文件。如果我们在此使用make命令编译,如下:

$ make
make: 'output' is up to date.

​ 再次使用make编译器会返回,可执行程序为最新的结果,我们依旧修改一下add.c,然后在编译,如下:

$ make
gcc -c add.c
gcc -o output main.o add.o sub.o

​ 会发现,它自动只重新编译生成我们修改源文件的目标文件.c和可执行文件。

​ 通过上述例子,Makefile的引入,将我们上面的三个问题解决了,它可以帮助我们快速的编译,只更新修改过的文件,这样在一个很庞大的工程中,只有第一次编译时间比较长,第二次开始大大缩短编译时间,节省了我们的开发周期。不过上面的Makefile仍然有问题,就是工程中的源文件不断增加,如果按照上面的写法,你会发现,Makefile会越来越臃肿。下面我们讲解如何解决这个臃肿的问题。

6.4.3 Makefile的规则

6.4.3.1 命名规则:

​ 一般来说将Makefile命名为Makefile或makefile都可以,当很多源文件的名字是小些,所以一般使用Makefile。Makefile也可以为其他名字,比如makefile.linux,但你需用make的参数(-f or --file)制定对应的文件,示例如下:

make -f makefile.linux

6.4.3.2 基本语法规则:

目标(target):依赖(prerequisites)

[Tab]命令(command)

​ 1)target:需要生成的目标文件

​ 2)prerequisites:生成该target所依赖的一些文件

​ 3)command:生成该目标需要执行的命令

​ 三者的关系:target依赖于 prerequisites中的文件,其生成规则定义在command中。

​ 举例,比如我们平时要编译一个文件:

$ gcc main.c -o main

​ 换成Makefile的书写格式:

01 main:main.c

02 gcc main.c -o main

​ **注意:Makefile文件里的命令必须要使用Tab。**不能使用空格。

6.4.3.3 目标生成规则:

目标生成:

​ 1)检查规则中的依赖文件是否存在。

​ 2)若依赖文件不存在,则寻找是否有规则用来生成该依赖文件。

目标生成流程,如下:

目标更新:

​ 1)检查目标的所有依赖,任何一个依赖有更新时,就要重新生成目标。

​ 2)目标文件比依赖文件更新时间晚,则需要更新。

目标更新流程,如下:

我们使用上面的例子,Makefile内容如下:

output: main.o add.o sub.o
        gcc -o output main.o add.o sub.o
main.o: main.c
        gcc -c main.c
add.o: add.c
        gcc -c add.c
sub.o: sub.c
        gcc -c sub.c

clean:
        rm *.o output

​ 编译执行:

$ make
gcc -c main.c
gcc -c add.c
gcc -c sub.c
gcc -o output main.o add.o sub.o

​ make命令会检测寻找目标的依赖是否存在,不存在,则会寻找生成依赖的命令。当我们修改某一个文件时,比如之修改add.c文件,然后重新make,如下:

$ make
gcc -c add.c
gcc -o output main.o add.o sub.o

​ 会发现make命令,会检查更新。然后只编译更新的软件。

6.5 Makefile的语法

6.5.1 变量的定义及取值

​ Makefile也支持变量定义,变量的定义也让的我们的Makefile更加简化,可复用。

​ **变量的定义:**一般采用大写字母,赋值方式像C语言的赋值方式一样,如下:

DIR = ./100ask/

​ **变量取值:**使用括号将变量括起来再加美元符,如下:

FOO = $(DIR)

​ 变量可让Makefile简化可复用,上面个的Makefile文件,内容如下:

output: main.o add.o sub.o
        gcc -o output main.o add.o sub.o
main.o: main.c
        gcc -c main.c
add.o: add.c
        gcc -c add.c
sub.o: sub.c
        gcc -c sub.c

clean:
        rm *.o output

​ 我们可以将其优化,如下:

#Makefile变量定义
OBJ = main.o add.o sub.o
output: $(OBJ)
        gcc -o output $(OBJ)
main.o: main.c
        gcc -c main.c
add.o: add.c
        gcc -c add.c
sub.o: sub.c
        gcc -c sub.c

clean:
        rm $(OBJ) output

​ 我们分析一下上面简化过的Makefile,第一行是注释,Makefile的注释采用‘#’,而且不支持像C语言中的多行注释。第二行我们定义了变量OBJ,并赋值字符串”main.o,add.o,sub.o“。其中第三,四,十三行,使用这个变量。这样用到用一个字符串的地方直接调用这个变量,无需重复写一大段字符串。

​ Makefile除了使用‘=’进行赋值,还有其他赋值方式,比如‘:=’和‘?=’,接下来我们来对比一下这几种的区别:

6.5.2.1 赋值符‘=’

​ 我们使用一个例子来说明赋值符‘=’的用法。Makefile内容如下:

01	PARA = 100
02	CURPARA = $(PARA)
03	PARA = ask
04
05	print:
06		@echo $(CURPARA)

​ 分析代码:第一行定义变量PARA,并赋值为“100”,第二行定义变量CURPARA,并赋值引用变量PARA,此时CURPARA的值和PARA的值是一样的,第三行,将变量PARA的变量修改为“ask”。第六行输出CURPARA的值,echo前面增加@符号,代表执行此条命令,不会在终端打印出来。

​ 通过命令“make print”执行Makefile,如下:

$ make print
ask

​ 从结果上看,变量CURPARA的值并不是“100”。其值为PARA最后一次赋值的值。说明,赋值符“=”,可以借助另外一个变量,可以将变量的真实值推到后面去定义。也就是变量的真实值取决于它所引用的变量的最后一次有效值。

​ 其实可以理解为在C语言中,定义一个指针变量指向一个变量的地址。如下:

01	int a = 10;
02	int *b = &a;
03	a=20;

6.5.2.2 赋值符‘:=’

​ 我们使用一个例子来说明赋值符‘:=’的用法。Makefile内容如下:

01	PARA = 100
02	CURPARA := $(PARA)
03	PARA = ask
04
05	print:
06		@echo $(CURPARA)

​ 代码分析:我们见上面的Makefile的第二行的“=”替换成“:=”,重新编译,如下:

$ make print
100
$

​ 从结果上看,变量CURPARA的值为“100”。“=”和“:=”的区别就在这里,“:=”只取第一次被赋值的值。

6.5.2.3 赋值符‘?=’

​ 我们两个Makefile来说明赋值符‘?=’的用法。如下:

​ 第一个Makefile:

PARA = 100
PARA ?= ask 

print:
	@echo $(PARA)

​ 编译结果:

$ make print
100
$

​ 第二个Makefile:

PARA ?= ask 

print:
	@echo $(PARA)

​ 编译结果:

$ make print
ask
$

​ 上面的例子说明,如果变量 PARA 前面没有被赋值,那么此变量就是“ask”,如果前面已经赋过值了,那么就使用前面赋的值。

6.5.2.4 赋值符‘+=’

​ Makefile 中的变量是字符串,有时候我们需要给前面已经定义好的变量添加一些字符串进去,此时就要使用到符号“+=”,比如如下:

01	OBJ = main.o add.o
02	OBJ += sub.o

​ 这样的结果是OBJ的值为:”main.o,add.o,sub.o“。说明“+=”用作与变量的追加。

6.5.2 系统自带变量

​ 系统自定义了一些变量,通常都是大写,比如CC,PWD,CLFAG等等,有些有默认值,有些没有,比如以下几种,如下:

​ 1)CPPFLAGS:预处理器需要的选项,如:-l

​ 2)CFLAGS:编译的时候使用的参数,-Wall -g -c

​ 3)LDFLAGS:链接库使用的选项,-L -l

​ 其中:默认值可以被修改,比如CC默认值是cc,但可以修改为gcc:CC=gcc

​ 使用的例子,如下:

01	OBJ = main.o add.o sub.o
02	output: $(OBJ)
03	        gcc -o output $(OBJ)
04	main.o: main.c
05	        gcc -c main.c
06	add.o: add.c
07	        gcc -c add.c
08	sub.o: sub.c
09	        gcc -c sub.c
10	
11	clean:
12	        rm $(OBJ) output

​ 使用系统自带变量,如下:

01	CC = gcc
02	OBJ = main.o add.o sub.o
03	output: $(OBJ)
04	        $(CC) -o output $(OBJ)
05	main.o: main.c
06	        $(CC) -c main.c
07	add.o: add.c
08	        $(CC) -c add.c
09	sub.o: sub.c
10	        $(CC) -c sub.c
11	
12	clean:
13	        rm $(OBJ) output

​ 在上面例子中,系统变量CC不改变默认值,也同样可以编译,修改的目的是为了明确使用gcc编译。

6.5.3 自动变量

​ Makefile的语法提供一些自动变量,这些变量可以让我们更加快速的完成Makefile的编写,其中自动变量只能在规则中的命令使用,常用的自动变量如下:

​ 1)$@:规则中的目标

​ 2)$<:规则中的第一个依赖文件

​ 3)$^:规则中的所有依赖文件

​ 我们上面的例子继续完善,修改为采用自动变量的格式,如下:

01	CC = gcc
02	OBJ = main.o add.o sub.o
03	output: $(OBJ)
04	        $(CC) -o $@ $^
05	main.o: main.c
06	        $(CC) -c $<
07	add.o: add.c
08	        $(CC) -c $<
09	sub.o: sub.c
10	        $(CC) -c $<
11	
12	clean:
13	        rm $(OBJ) output

​ 其中:第4行$^表示变量OBJ的值,即main.o add.o sub.o,第四,第六,第八行的$<分别表示main.c add.c sub.c。$@表示output。

6.5.4 模式规则

​ 模式规则实在目标及依赖中使用%来匹配对应的文件,我们依旧使用上面的例子,采用模式规则格式,如下:

01	CC = gcc
02	OBJ = main.o add.o sub.o
03	output: $(OBJ)
04	        $(CC) -o $@ $^
05	%.o: %.c
06	        $(CC) -c $<
07	
08	clean:
09	        rm $(OBJ) output

​ 其中:第五行%.o: %.表示如下。

​ 1.main.o由main.c生成

​ 2.add.o 由 add.c生成

​ 3.sub.o 由 sub.c生成

6.5.5 伪目标

​ 所谓伪目标就是这样一个目标,它不代表一个真正的文件名,在执行make时可以指定这个目标来执行其所在规则定义的命令,有时我们将一个伪目标成为标签。那么到底什么是伪目标呢?我们依旧通过上面的例子来说明伪目标是什么。

​ 我们执行make命令,然后在执行在执行命令make clean,如下:

$make
gcc -c main.c
gcc -c add.c
gcc -c sub.c
gcc -o output main.o add.o sub.o
$make clean
rm *.o output

​ 是不是发现没啥问题,接着我们做个手脚,在Makefile目录下创建一个clean的文件,然后依旧执行make和make clean,如下:

$touch clean
$make
gcc -c main.c
gcc -c add.c
gcc -c sub.c
gcc -o output main.o add.o sub.o
$make clean
make: 'clean' is up to date.

​ 为什么clean下的命令没有被执行?这是因为Makefile中定义的只执行命令的目标与工作目录下的实际文件出现名字冲突。而Makefile中clean目标没有任何依赖文件,所以目标被认为是最新的而不去执行规则所定义的命令。所以rm命令不会被执行。伪目标就是为了解决这个问题,我们在clean前面增加.PHONY:clean,如下:

01	CC = gcc
02	OBJ = main.o add.o sub.o
03	output: $(OBJ)
04	        $(CC) -o $@ $^
05	%.o: %.c
06	        $(CC) -c $<
07
08	.PHONY:clean
09	clean:
10	        rm $(OBJ) output

​ 运行结果:

$make
gcc -c main.c
gcc -c add.c
gcc -c sub.c
gcc -o output main.o add.o sub.o
$make clean
rm *.o output

​ 通过增加了伪目标之后,就是执行rm命令了。当一个目标被声明为伪目标后,make在执行规则时不会去试图去查找隐含规则来创建它。这样就提高了make的执行效率,也不用担心由于目标和文件名重名了。

​ 伪目标的两大好处:

​ 1.避免只执行命令的目标和工作目录下的实际文件出现名字冲突。

​ 2.提高执行Makefile时的效率

6.5.6 Makefile函数

​ Makefile提供了大量的函数,其中我们经常使用的函数主要有两个(wildcard,patsubst)。注意,Makefile中所有的函数必须要有返回值。创建一个文件夹src,在里下面创建两个文件,100.c,ask.c。如下:

.
├── Makefile
└── src
    ├── 100.c
    └── ask.c

**注明:**代码目录在裸机Git仓库 NoosProgramProject/(6_Makefile与GCC/001_Makefile_02)文件夹下。

6.5.6.1 wildcard函数

​ 用于查找指定目录下指定类型的文件,函数参数:目录+文件类型,如下:

$(wildcard 指定文件类型)

​ 其中,指定文件类型,如果不写路径,则默认为当前目录查找,例子如下:

01	SRC = $(wildcard ./src/*.c)
02
03	print:
04	        @echo $(SRC)

​ 执行命令make,结果如下:

$ make
./src/ask.c ./src/100.c

​ 其中,这条规则表示:找到目录./src下所有后缀为.c的文件,并赋值给变量SRC。命令执行完,SRC变量的值:./src/ask.c ./src/100.c

6.5.6.2 patsubst函数

​ 用于匹配替换。函数参数:原模式+目标模式+文件列表,如下:

$( patsubst 原模式, 目标模式, 文件列表)

​ 其中,从文件列表中查找出符合原模式文件类型的文件,然后一一替换成目标模式。举例:将./src目录下的.c结尾的文件,替换成.o文件,并赋值给obj。如下:

SRC = $(wildcard ./src/*.c)
OBJ = $(patsubst %.c, %.o, $(SRC))

print:
	@echo $(OBJ)

​ 执行命令make,结果如下:

$ make
./src/ask.o ./src/100.o

​ 其中,这条规则表示:把变量中所有后缀为.c的文件替换为.o。 命令执行完,OBJ变量的值:./src/ask.o ./src/100.o

6.6 Makefile实例

​ 在上面的例子中,我们都是把头文件,源文件放在同一个文件里面,这样不好用于维护,所以我们将其分类,把它变得更加规范一下,把所有的头文件放在文件夹:inc,把所有的源文件放在文件夹:src。(**代码目录在裸机Git仓库 NoosProgramProject/(6_Makefile与GCC/001_Makefile_03)文件夹下)。**如下:

$ tree
.
├── inc
│   ├── add.h
│   └── sub.h
├── Makefile
└── src
    ├── add.c
    ├── main.c
    └── sub.c

​ 其中Makefile的内容如下:

01	SOURCE = $(wildcard ./src/*.c)
02	OBJECT = $(patsubst %.c, %.o, $(SOURCE))
03	
04	INCLUEDS = -I ./inc
05	
06	TARGET  = 100ask
07	CC      = gcc
08	CFLAGS  = -Wall -g
09	
10	$(TARGET): $(OBJECT)
11	        @mkdir -p output/
12	        $(CC) $^ $(CFLAGES) -o output/$(TARGET)
13	
14	%.o: %.c
15	        $(CC) $(INCLUEDS) $(CFLAGES) -c $< -o $@
16	
17	.PHONY:clean
18	clean:
19	        @rm -rf $(OBJECT) output/

​ 分析:

​ 行1:获取当前目录下src所有.c文件,并赋值给变量SOURCE。

​ 行2:将./src目录下的.c结尾的文件,替换成.o文件,并赋值给变量OBJECT。

​ 行4:通过-I选项指明头文件的目录,并赋值给变量INCLUDES。

​ 行6:最终目标文件的名字100ask,赋值给TARGET。

​ 行7:替换CC的默认之cc,改为gcc。

​ 行8:将显示所有的警告信息选项和gdb调试选项赋值给变量CFLAGS。

​ 行11:创建目录output,并且不再终端现实该条命令。

​ 行12:编译生成可执行程序100ask,并将可执行程序生成到output目录

​ 行15:将源文件生成对应的目标文件。

​ 行17:伪目标,避免当前目录有同名的clean文件。

​ 行19:用与执行命令make clean时执行的命令,删除编译过程生成的文件。

​ 最后编译的结果,如下:

$ make
gcc -I ./inc  -c src/main.c -o src/main.o
gcc -I ./inc  -c src/add.c -o src/add.o
gcc -I ./inc  -c src/sub.c -o src/sub.o
gcc src/main.o src/add.o src/sub.o  -o output/100ask
$tree
.
├── inc
│   ├── add.h
│   └── sub.h
├── Makefile
├── output
│   └── 100ask
└── src
    ├── add.c
    ├── add.o
    ├── main.c
    ├── main.o
    ├── sub.c
    └── sub.o

​ 上面的Makefile文件算是比较完善了,不过项目开发中,代码需要不断的迭代,那么必须要有东西来记录它的变化,所以还需要对最终的可执行文件添加版本号,如下:

01	VERSION = 1.0.0
02	SOURCE  = $(wildcard ./src/*.c)
03	OBJECT   = $(patsubst %.c, %.o, $(SOURCE))
04
05	INCLUEDS = -I ./inc
06	
07	TARGET  = 100ask
08	CC      = gcc
09	CFLAGS  = -Wall -g
10	
11	$(TARGET): $(OBJECT)
12	        @mkdir -p output/
13	        $(CC) $^ $(CFLAGES) -o output/$(TARGET)_$(VERSION)
14	
15	%.o: %.c
16	        $(CC) $(INCLUEDS) $(CFLAGES) -c $< -o $@
17	
18	.PHONY:clean
19	clean:
20	        @rm -rf $(OBJECT) output/

​ 分析:

​ 行1:将版本号赋值给变量VERSION。

​ 行13:生成可执行文件的后缀添加版本号。

​ 编译结果:

$ tree
.
├── inc
│   ├── add.h
│   └── sub.h
├── Makefile
├── output
│   └── 100ask_1.0.0
└── src
    ├── add.c
    ├── add.o
    ├── main.c
    ├── main.o
    ├── sub.c
    └── sub.o

第七章:时钟体系

第七章:时钟体系

7. 时钟体系

​ 时钟信号是数字时序电路的“脉搏”,电路每接收到一个周期的时钟信号,就做一个相应的动作。因此,在允许的范围内,时钟信号的快慢直接决定着电路性能的好坏。在片上系统(SOC)中,不同的模块通常需要工作在不同的时钟频率。为了满足这些需求,芯片将时钟源信号进行稳定、倍频、分频、分发以及屏蔽(gate)等操作,产生不同频率的时钟信号。这些时钟信号和它们的管理电路构成了芯片的时钟体系,驱动着各种各样的功能模块协同工作。

6.1 IMX6ULL时钟体系介绍

6.1.1 晶体振荡电路

​ 时钟信号不是凭空产生的,芯片首先需有一个频率较低的源时钟信号。imx6ull包含两个偏压放大器(biased amplifier),当外部连接合适的晶振和负载电容时,能够分别产生24MHZ和32KHZ的时钟信号。下面是开发板的时钟引脚连接原理图:

​ 当imx6ull的时钟引脚XTALI和XTALO连上合适的晶振和电容时,模块XTALOSC24M会产生24MHZ的时钟信号。注意,输出频率到达24MHZ并不意味着模块XTALOSC24M已经稳定工作,仍然需要等待一段时间。一旦时钟信号稳定,可以通过设置寄存器来降低它的工作电流;但是注意,在关闭模块XTALOSC24M电源之前,相应的值应当被恢复,否则恢复供电时模块不能正常启动。

​ 另外,imx6ull还有一个内置的24MHZ RC振荡器,它以RTC时钟(32KHZ)做基准产生时钟信号。尽管能源消耗显著低于模块XTALOSC24M,RC振荡器的精确度存在较大误差,实际应用中应当避免使用它。

​ 当imx6ull的时钟引脚RTC_XTALI和RTC_XTALO连接32KHZ或32.768KHZ的晶振时,用模块RTC_XTAL产生32KHZ的RTC时钟信号。除此之外,imx6ull还包含一个内部的32KHZ振荡器,当时钟系统探测到RTC振荡器丢失时钟信号时,会自动切换到内部的32KHZ振荡器。但是由于该内部振荡器精度不如模块RTC_XTAL,不能作为替代品长时间使用。

​ RTC时钟信号主要用来记录时间,而XTALOSC24M的输出时钟信号为芯片的时钟体系提供基础的源时钟信号,是本篇讨论的重点。除此之外,芯片本身也可以直接接收时钟信号作为源时钟信号,这需要芯片外部提供一个稳定的时钟信号源。这种方式较不常用,本文不再进行描述。

6.1.2 锁相环电路

​ XTALOSC24M的时钟信号只有24MHZ,远远不能满足实际需求,在芯片中需要进行稳定和倍频操作,这主要是由锁相环电路完成的。

​ 锁相环(PLL)由鉴相器PD、低通滤波器LPF和压控振荡器VCO组成。鉴相器(PD)用来鉴别两个输入时钟信号之间的相位差,并输出误差电压,经过低通滤波(LPF)后,形成压控振荡器(VCO)的控制。压控振荡器的输出经过分频(DIV)后反馈到鉴相器与基准信号进行比较,最终,VCO的输出就会稳定下来。下图是锁相环工作原理示意图:

​ imx6ull包含7个锁相环电路,它们的输入时钟信号称为源时钟信号,可通过寄存器选择,通常为XTALOSC24M产生24MHZ时钟信号。它们的输出进过进一步选择和分频,形成不同的根时钟信号,分发到各个模块使用。这些锁相环电路及它们的分频器输出如下图所示:

​ 下面分别介绍这些PLL的功能。

​ 1. PLL1:

​ 被称为ARM_PLL,用来驱动ARM核心工作。它能够倍频达到1.3GHZ,注意这个频率超过了芯片能够工作的最大频率1GHZ。

​ 2. PLL2:

​ 被称为SYS_PLL或者528_PLL。它的倍频参数固定在x22,在使用XTALOSC24M产生的24MHZ时钟作为参考时钟时,产生528MHZ的输出。除了这个主输出,SYS_PLL还包含四个分频器,主输出和分频器的输出可用来作为根时钟。SYS_PLL的这些输出时钟信号的频率并不需要是确定或者精确的值,可在运行时动态进行改变。通常,它们用来驱动芯片内部的系统总线,内部处理逻辑,DDR接口以及NAND/NOR等等。

​ 3. PLL3:

​ 被称为USB1_PLL,用来驱动第一个USB物理层实体USBPHY1。它的倍频参数固定在x20,在使用24MHZ参考时钟时产生480MHZ的输出。除了主输出之外,USB1_PLL同样包含四个分频器,它们的输出用来作为需要固定频率的根时钟,比如UART和其它的串行接口,音频接口等。

​ 4. PLL4:

​ 被称为AUDIO_PLL,能够进行倍频和分频操作,产生低抖动、高精度标准音频时钟信号。AUIDIO_PLL的输出频率范围从650MHZ到1300MHZ,时钟的频率分辨率要好于1HZ。该输出时钟信号主要用来驱动串行音频接口或者作为外部音频解码器的参考时钟。另外,AUDIO_PLL的分频器,可对VCO的输出时钟信号进行/1、/2或/4分频。

​ 5. PLL5:

​ 被称为VIDEO_PLL。它同样具有倍频和分频功能,能够产生低抖动、高精度标准视频时钟信号。VIDEO_PLL的输出频率范围从650MHZ到1300MHZ,时钟的频率分辨率要好于1HZ。该输出时钟主要作为显示和视频接口的时钟信号。另外,VIDEO_PLL的分频器,可对VCO的输出时钟信号进行/1、/2、/4或/8分频。

​ 6. PLL6:

​ 被称为ENET_PLL。它的倍频参数固定为x20+(5/6),在使用24MHZ参考时钟时产生500MHZ的输出。它主要用来生成:(1)50MHZ或25MHZ时钟,用于外部以太网接口;(2)125MHZ时钟,用于精简的千兆以太网接口;(3)100MHZ时钟,用于通用功能。

​ 7. PLL7:

​ 被称为USB2_PLL,专门用于驱动第二个USB物理层实体USBPHY2。它的倍频参数固定为x20,输出480MHZ的时钟信号。

​ 上述锁相环电路都有自己专门的控制和状态寄存器,它们可独立配置为以下3个模式中的一种:

​ 1) Bypass模式:PLL输入的参考时钟直接传递到输出,由BYPASS位控制;

​ 2) 输出禁止模式:无论bypass时钟还是PLL生成的时钟均被禁止,无输出时钟信号,由ENABLE位控制;

​ 3) 断电模式:PLL中大部分电路断电,无输出时钟信号,由POWERDOWN位控制。

​ 以ARM_PLL为例,单独截取出来说明,见下图。PLL正常工作时,时钟信号通过路径1传输作为信号ref_armpll_clk输出;处于Bypass模式时,源时钟信号不经过PLL放大,由路径2直接输出;处于输出禁止模式时,时钟信号在armpll_enable处被屏蔽,ref_armpll_clk无输出;处于断电模式时,ARM_PLL大部分电路断电,电路不工作,ref_armpll_clk无输出信号。

​ 其中,两个锁相环电路SYS_PLL和USB1_PLL,分别带有四个分相器(PFD)对其产生的PLL输出信号进行分频(每个分相器可独立设置分频参数),用来产生额外的频率输出。由于分相器完全由数字器件组成且不包含反馈回路,我们只需要改变逻辑组合就能改变分频参数的值,不影响锁相环电路的锁定状态,因此分相器能够比锁相环更快的改变输出频率。除此之外,分相器的值还能在运行时改变,不需要在改变前后关闭和开启时钟的输出。

​ 注意,对于那些包含分相器的锁相环电路,每个分相器有自己独立的时钟屏蔽控制位。当相连的锁相环电路加电启动或者重新锁定时,分相器自动进入屏蔽状态,需要手动对每个PFD进行一次屏蔽和开启操作。

6.1.3 根时钟信号电路

​ 如前所述,锁相环电路的输出时钟信号并不能直接供其它模块使用。在imx6ull中,它们的输出时钟信号、PFD时钟信号以及对应的bypass时钟信号经过选择、分频后形成根时钟信号,才会分发到各个模块。这部分电路又细分为两部分,前半部分称为时钟切换电路(switcher),后半部分称为根时钟生成电路(root generator)。

​ 时钟切换电路主要对PLL1和PLL3的输出进行选择,被选中的信号形成pll1_sw_clk和pll3_sw_clk信号。另外,它还对PLL4和PLL5进行额外的分频操作,形成pll4_main_clk和pll5_main_clk信号。如下图所示:

​ 后续电路不直接使用上述PLL的输出,而是使用switcher形成的这些输出信号。例如,如果我们想改变CPU的工作频率,可以先修改CCSR[pll1_sw_clk_sel]将pll1_sw_clk切换到step_clk,然后修改PLL1的参数,等待其输出时钟信号稳定到新的频率上,再切换回PLL1的输出信号pll1_main_clk。因为使用了无抖动的多路选择器(glitchless multiplexer),在切换过程中CPU仍正常运行,我们将在第一个编程示例中演示上述过程。

​ 根时钟生成电路对前面的这些信号和PLL2的输出信号进一步筛选和分频形成根时钟信号,它们直接或者间接驱动芯片中的模块来实现各自功能。这里仅以总线相关的根时钟信号进行说明,如下图所示:

​ 其它的根时钟信号和模块对这些根时钟信号的使用参见CCM和模块各自的寄存器设置,这里不再展开叙述。本章第二个编程示例计算锁相环电路输出时钟和这些总线的根时钟的频率并打印,有兴趣的同学也可以参照示例代码和imx6ull手册计算其它的时钟信号的频率。

6.2 寄存器介绍

​ imx6ull时钟相关的寄存器主要分布在CCM和ANALOG_DIG这两个模块,它们均连接在AIPS-1总线上,它们的地址范围如下所示:

​ ANALOG_DIG模块主要负责晶体振荡电路和锁相环电路的相关设置。它的寄存器分为两类:CCM_ANALOG_PLL_xxx寄存器设置对应PLL的参数和工作状态,CCM_ANALOG_MISCx (x = 0-2)寄存器进行其它一些杂项的设置或状态显示,包括晶体振荡电路的控制参数。这些寄存器数量较多,这里不一一列出。

​ 另外需要注意,ANALOG_DIG与电源管理模块(PMU)共用这些CCM_ANALOG_MISCx寄存器,它们在PMU中被称为PMU_MISCx (x = 0-2)。由于晶体振荡电路在系统启动时已初始化完毕,输出频率固定的时钟信号,在芯片运行期间通常不需要修改设置,这里不再进行说明。

​ 而CCM模块控制根时钟信号的产生和分发,大部分寄存器用来对PLL及PFD产生的时钟信号进行分发和分频控制,如下表所示:

​ 以及下面的表,注意红框部分的寄存器CCM_CCGRx(x = 0-6),它们用来控制各个时钟信号在不同功耗模式下是否被屏蔽:

6.2.1 锁相环电路寄存器

​ 如前所述,imx6ull共包含7个锁相环电路,除ENET_PLL之外,其它的锁相环电路控制和状态寄存器的结构都很类似。以ARM_PLL为例,它的寄存器结构如下所示:

​ 其中,控制位BYPASS、ENABLE和POWERDOWN用来控制PLL的工作模式(bypass模式、输出禁止模式和断电模式);BYPASS_CLK_SRC选择输入时钟源,DIV_SELECT设置频率放大倍数。正常工作时,需要设置BYPASS和POWRDOWN为0,ENABLE为1。当LOCK值为1时,锁相环电路输出稳定的时钟信号。

​ 稳定工作时,ARM_PLL的输出频率为 Fref * DIV_SELECT/2,其它PLL的设置方法和输出频率计算公式与ARM_PLL类似,但略有差别。例如,锁相环电路USB1_PLL、USB2_PLL和SYS_PLL虽然都有自己的DIV_SELECT,它们应当被设为固定的值。

​ 需要特别说明的是,锁相环电路AUDIO_PLL和VIDEO_PLL还增加了额外的分频参数NUM和DENOM。以AUDIO_PLL为例,它的相应寄存器如下所示:

​ (1)CCM_ANALOG_PLL_AUDIO_NUM

​ (2)CCM_ANALOG_PLL_AUDIO_DENOM

​ 它们的输出频率为Fref * (DIV_SELECT + NUM/DENOM)。除此之外,AUIDIO_PLL和VIDEO_PLL还可以在时钟切换电路(switcher)中设置额外的分频参数为/1、/2、/4、/8或/16,这些值分布在寄存器CCM_ANALOG_PLL_AUDIO、CCM_ANALOG_PLL_VIDEO和CCM_ANALOG_MISC2中。

​ 除此之外,SYS_PLL和USB1_PLL还各自配有四个分相器,它们分别对SYS_PLL和USB1_PLL的输出时钟信号进行分频,分频参数分别在CCM_ANALOG_PFD_480n和CCM_ANALOG_PFD_528n中设置。这两个寄存器的结构完全一样,如下图所示:

每个PFD的输出频率为Fvco*18/PFD_FRAC,其中Fvco是相应PLL的输出频率,而PFD_FRAC的数值取值范围为12到35。

6.2.2 根时钟控制寄存器

​ 上述锁相环电路以及它们的bypass时钟信号、PFD输出信号,经过时钟切换电路(switcher)和根时钟生成电路(root generator)处理后形成各种根时钟信号。其中,时钟切换电路寄存器CCM_CCSR选择时钟信号pll1_sw_clk和pll3_sw_clk的来源,如下图所示:

​ 根时钟生成电路的寄存器也包含在CCM模块中,比如ARM根时钟信号由pll1_sw_clk分频得来,相应的分频寄存器CCM_CACRR如下图所示:

​ ARM的时钟信号频率为pll1_sw_clk/(ARM_PODF + 1)。

​ 而之前提到的总线根时钟信号由寄存器CCM_CBCDR进行选择和分频,如下图所示:

​ 其中各个字段的作用可参见根时钟信号电路一节中的原理图,具体设置步骤可参见后面的编程示例。其它根时钟的寄存器与其类似,详细的控制方式要参见imx6ull手册中CCM模块的寄存器描述,这里不再一一列举。

6.2.3 模块时钟屏蔽寄存器

​ 为了降低功耗,imx6ull可以工作在以下三种模式:

​ 对于每个时钟信号,imx6ull提供了在不同工作模式下是否屏蔽这些时钟信号的控制方法,这些控制位集中放在寄存器CCGRx(x = 0-6)中,每个CCGRx寄存器结构如下图所示:

​ 每两位为一个单位,控制一个时钟信号。比如,在寄存器CCGR0中,CG15控制时钟信号gpio2_clocks,CG14控制时钟信号uart2_clock等等。这两位值的含义如下所示:

CGx (x=0-15) 时钟信号活动状态描述
00 时钟信号在三个模式中均被屏蔽。
01 时钟信号仅在RUN模式开启,在其它模式中被屏蔽。
10 保留
11 时钟信号在RUN和WAIT模式开启,在STOP模式中被屏蔽。

​ 当用户某个模块驱动时,应当根据该模块是否需要在低功耗模式下工作来设置寄存器CCGRx中相应的值,以达到降低功耗的目的。另外,在正常工作模式中,用户也可以在模块空闲时将CCGRx中相应的值设为0,动态调节模块的功耗。

6.3 编程示例

6.3.1 改变CPU工作频率

​ 本示例动态改变CPU的工作频率,先将CPU设置工作在一个较低频率(81MHZ),然后切换至较高的工作频率(648MHZ)。为验证CPU频率的变化,本例使用忙等待的延时方式,控制LED灯闪烁--随着CPU频率的提升,延时时间变短,LED灯闪烁频率变快。

​ 正常工作时,CPU使用锁相环电路ARM_PLL的输出信号作为时钟源。在改变CPU频率之前,我们首先切换到其它时钟信号(示例中选择晶体振荡电路XTALOSC24M的输出),修改ARM_PLL设置并稳定在新的频率之后,再切换回ARM_PLL的输出时钟信号。这里我们再次引用时钟切换电路(switcher)的部分原理图,如下所示:

6.3.1.1 设置PLL1_SW_CLK的时钟路径

​ 改变CPU频率前,pll1_sw_clk时钟路径如图中路径1所示。我们首先将其切换至路径2,待ARM_PLL(PLL1)稳定输出后,再切换回路径1。这些时钟路径的控制位标示位于图中黄色方框处。对应的控制函数为set_pll1_sw_clk(int sel_pll1),当参数sel_pll1值为0时,选择路径2;当参数sel_pll1值非0时,选择路径1。函数set_pll1_sw_clk定义在文件switcher.c中,内容如下所示:

4 extern struct ccm_regs *ccm;
5 
    /**********************************************************************
  * 函数名称: set_pll1_sw_clk
  * 功能描述: 设置PLL1_SW_CLK的时钟路径
  * 输入参数: sel_pll1: 0-选择XTALOSC24M的输出,1-选择PLL1的输出
  * 输出参数: 无
  * 返 回 值: 无
  * 修改日期        版本号     修改人       修改内容
  * -----------------------------------------------
  * 2020/03/08     V1.0      今朝        创建
  ***********************************************************************/
    void sel_pll1_sw_clk(int sel_pll1)
{
    /* PLL1_SW_CLK_SEL: 0表示pll1_main_clk,1表示step_clk */
    if (sel_pll1)
        clr_bit(&ccm->ccsr, 2);     /* 选择pll1_main_clk */
    else {
        clr_bit(&ccm->ccsr, 8);     /* step_clk选择使用OSC的输出 */
        set_bit(&ccm->ccsr, 2);     /* 选择step_clk */
    }
}

6.3.1.2 重新设置ARM_PLL的输出频率

​ 切换完pll1_sw_clk时钟路径之后,我们就可以重新设置ARM_PLL的输出频率,相应的设置函数set_pll设置指定PLL的倍频参数并等待其输出频率稳定。为了简化函数接口,AUDIO_PLL和VIDEO_PLL的NUM和DENOM参数统一设置为0xF,而且不支持ENET_PLL的设置,感兴趣的同学可以自己添加相关代码。该函数位于文件pll.c中,内容如下:

struct anadig_regs *anadig = (struct anadig_regs *)ANADIG_BASE_ADDR;
  
static void wait_to_lock(u32 *pll_reg)
{
    while (read32(pll_reg) & LOCK_MASK == 0);   /* 等待指定的PLL进入锁定状态 */
}

/**********************************************************************
   * 函数名称: set_pll
   * 功能描述: 设置PLL的倍频参数并等待其进入锁定状态
   * 输入参数: pll: 指定PLL的标识,div: PLL的倍频参数
   * 输出参数: 无
   * 返 回 值: 无
   * 修改日期        版本号     修改人       修改内容
   * -----------------------------------------------
   * 2020/03/08     V1.0      今朝        创建
   ***********************************************************************/
void set_pll(pll_e pll, u32 div)
{
    switch (pll) {
        case ARM_PLL:
            if (div < 54 && div > 108) return;      /* ARM_PLL的倍频参数的有效范围为54到108 */
            write32(ENABLE_MASK | div, &anadig->analog_pll_arm);
            wait_to_lock(&anadig->analog_pll_arm);  /* 等待ARM_PLL锁定 */
            break;

        case USB1_PLL:      /* 设置USB1_PLL的倍频参数并等待锁定 */
            write32(ENABLE_MASK | (div&0x3), &anadig->analog_pll_usb1);
            wait_to_lock(&anadig->analog_pll_usb1);
            break;

        case USB2_PLL:      /* 设置USB2_PLL的倍频参数并等待锁定 */
            write32(ENABLE_MASK | (div&0x3), &anadig->analog_pll_usb2);
            wait_to_lock(&anadig->analog_pll_usb2);
            break;

        case SYS_PLL:       /* 设置SYS_PLL的倍频参数并等待锁定 */
            write32(ENABLE_MASK | (div&0x1), &anadig->analog_pll_sys);
            wait_to_lock(&anadig->analog_pll_sys);
            break;

        case AUDIO_PLL:
            if (div < 27 && div > 54) return;       /* AUDIO_PLL的倍频参数的有效范围为27到54 */

            /* 简便起见,AUDIO_PLL的分频参数NUM和DENOM都设置为0xF */
            write32(0xF, &anadig->analog_pll_video_num);
            write32(0xF, &anadig->analog_pll_video_denom);

            write32(ENABLE_MASK | div, &anadig->analog_pll_video);
            wait_to_lock(&anadig->analog_pll_video);/* 等待AUDIO_PLL锁定 */
            break;

        case VIDEO_PLL:
            if (div < 27 && div > 54) return;       /* VIDEO_PLL的倍频参数的有效范围为27到54 */

            /* 简便起见,VIDEO_PLL的分频参数NUM和DENOM都设置为0xF */
            write32(0xF, &anadig->analog_pll_audio_num);
            write32(0xF, &anadig->analog_pll_audio_denom);

            write32(ENABLE_MASK | div, &anadig->analog_pll_video);
            wait_to_lock(&anadig->analog_pll_video);/* 等待VIDEO_PLL锁定 */
            break;

        case ENET_PLL:
            /* ENET_PLL寄存器设置方式与其它PLL差别较大,为了简化函数接口,这里不支持对它的设置 */
            break;
    }
}

6.3.1.3 设置ARM_CLK_ROOT的分频参数

​ 时钟信号pll1_sw_clk在成为arm_clk_root送往CPU之前,还要在根时钟生成电路(root generator)经过一次分频操作,其分频参数的设置函数为setup_arm_podf,位于文件clkroot.c中,内容如下:

extern struct ccm_regs *ccm;

/**********************************************************************
  * 函数名称: setup_arm_podf
  * 功能描述: 设置ARM_CLK_ROOT的分频参数
  * 输入参数: 取值范围为1-8,ARM_CLK_ROOT = PLL1_SW_CLK / PODF
  * 输出参数: 无
  * 返 回 值: 无
  * 修改日期        版本号     修改人       修改内容
  * -----------------------------------------------
  * 2020/03/08     V1.0      今朝        创建
  ***********************************************************************/
void setup_arm_podf(u32 podf)
{
    if (podf < 1 || podf > 8) return;   /* ARM_PODF分频范围是1到8 */
    write32(podf-1, &ccm->cacrr);
}

6.3.1.4 修改led闪烁函数

​ 最后,为了方便比较led的闪烁频率,我们对led的接口函数稍作修改,增加led_toggle函数,led的状态改变一次(从亮到灭或从灭到亮),这里不再展示其代码。

​ 在main函数中,我们首先初始化并点亮led灯,设置CPU频率到81MHZ(ARM_PLL输出为648MHZ,分频参数为8),亮灯和灭灯各5次;然后设置CPU频率为648MHZ(ARM_PLL输出为1296MHZ,分频参数为2),之后无限闪灯。我们可通过肉眼观测到led的闪烁频率明显变快。main.c文件内容如下所示:

#include "regs.h"
#include "pll.h"
#include "clkroot.h"

struct ccm_regs *ccm = (struct ccm_regs *)CCM_BASE_ADDR;

#define LOOPS 1000000
static void busy_wait(void)
{
    for(u32 i = 0; i < LOOPS; i++); /* 忙等待进行延时 */
}

/* LED灯接口函数 */
extern void led_init(void);
extern void led_toggle(void);
void led_on(void);

/* PLL1时钟路径及其PODF分频参数设置 */
extern void setup_arm_podf(u32 podf);
extern void sel_pll1_sw_clk(int sel_pll1);

void main(void)
{
    /* 首先初始化并点亮LED灯 */
    int blinks = 0;
    led_init();
    led_on();

    sel_pll1_sw_clk(0);     /* 将ARM_ROOT时钟切换至OSC */
    setup_arm_podf(8);      /* ARM_ROOT的分频参数设置为8 */
    set_pll(ARM_PLL, 54);   /* 设置ARM_PLL: 24*54/2 = 648MHZ, ARM_ROOT: 81MHZ */
    sel_pll1_sw_clk(1);     /* 将ARM_ROOT切换回ARM_PLL,此时CPU工作频率为81MHZ */

    /* 循环点亮/熄灭LED共10次,观察LED闪烁频率 */
    for (blinks = 10; blinks > 0; blinks--)
    {
        busy_wait();
        led_toggle();
    }

    sel_pll1_sw_clk(0);     /* 将ARM_ROOT时钟切换至OSC */
    setup_arm_podf(2);      /* ARM_ROOT的分频参数设置为2 */
    set_pll(ARM_PLL, 108);  /* 设置ARM_PLL: 24*108/2 = 1296MHZ, ARM_ROOT: 648MHZ */
    sel_pll1_sw_clk(1);     /* 将ARM_ROOT切换回ARM_PLL,此时CPU工作频率为648MHZ */

    /* 无限循环点亮/熄灭LED,观察此时LED闪烁频率明显变快 */
    while(1)
    {
        busy_wait();
        led_toggle();
    }
}

/* 本程序的除法运算使用了GCC提供的函数,需要提供raise函数以正常编译 */
void raise(void)
{
}
最后说明一下,由于本程序用到了GCC的除法操作例程,需要添加一个空的raise函数,以通过编译。

注明: 代码目录在裸机Git仓库 NoosProgramProject/(7_时钟体系/fastcpu) 文件夹下。

6.2.1.4 参考章节《4-1.4编译程序》编译程序

6.2.1.5 参考章节《4-1.4映像文件烧写、运行》烧写、运行程序

​ 运行成功后可以观察到开发板用户绿色led灯闪烁频率由慢变快。

6.3.2 打印时钟信号的频率值

​ 上个示例中我们可以通过led的闪烁频率观察到CPU的频率确实变快了。为了得到imx6ull中时钟的确切值,我们在本例程中将它们通过串口打印出来,这里我们使用了uart模块的功能,其代码参见后面的uart串口编程章节。

​ 本例程仅打印各个PLL(不包括ENET_PLL)及其PFD的输出频率,以及前面介绍的总线根时钟频率,这些频率值的计算需要分为三步进行。

6.3.2.1 获取PLL的输出频率

​ 首先,我们需要确定PLL和PFD的输出频率,在文件pll.c中添加以下代码:

/**********************************************************************
   * 函数名称: get_pll
   * 功能描述: 获取PLL的输出频率
   * 输入参数: pll: 指定PLL的标识
   * 输出参数: 无
   * 返 回 值: PLL的输出频率
   * 修改日期        版本号     修改人       修改内容
   * -----------------------------------------------
   * 2020/03/08     V1.0      今朝        创建
   ***********************************************************************/
u32 get_pll(pll_e pll)
{
    u32 div, post_div, pll_num, pll_denom;

    switch (pll) {
        case ARM_PLL:
            div = read32(&anadig->analog_pll_arm);
            if (div & BYPASS_MASK)  /* 判断ARM_PLL是否处于Bypass模式 */
                return CKIH;
            else {
                div &= 0x7F;        /* 获取ARM_PLL的倍频参数 */
                return (CKIH * div) >> 1;   /* ARM_PLL的输出频率只有倍频后的一半 */
            }

        case USB1_PLL:
            div = read32(&anadig->analog_pll_usb1);
            if (div & BYPASS_MASK)  /* 判断USB1_PLL是否处于Bypass模式 */
                return CKIH;
            else {
                div = div&0x1 ? 22 : 20;    /* USB1_PLL只有两种倍频模式,1表示x22,0表示x20 */
                return CKIH * div;
            }

        case USB2_PLL:
            div = read32(&anadig->analog_pll_usb2);
            if (div & BYPASS_MASK)  /* 判断USB2_PLL是否处于Bypass模式 */
                return CKIH;
            else {
                div = div&0x1 ? 22 : 20;    /* USB2_PLL只有两种倍频模式,1表示x22,0表示x20 */
                return CKIH * div;
            }

        case SYS_PLL:
            div = read32(&anadig->analog_pll_sys);
            if (div & BYPASS_MASK)  /* 判断SYS_PLL是否处于Bypass模式 */
                return CKIH;
            else {
                div = div&0x1 ? 22 : 20;    /* SYS_PLL只有两种倍频模式,1表示x22,0表示x20 */
                return CKIH * div;
            }

        case AUDIO_PLL:
            div = read32(&anadig->analog_pll_audio);
            if (!(div & ENABLE_MASK))   /* 判断AUDIO_PLL是否处于禁止输出模式 */
                return 0;

            if (div & BYPASS_MASK)      /* 判断AUDIO_PLL是否处于Bypass模式 */
                return CKIH;
            else {
                post_div = (div & 0x3) >> 19;
                if (post_div == 3)      /* reserved value */
                    return 0;
                /* AUDIO_PLL的分频参数:0表示除以4,1表示除以2,2表示除以1 */
                post_div = 1 << (2 - post_div);

                pll_num = read32(&anadig->analog_pll_audio_num);
                pll_denom = read32(&anadig->analog_pll_audio_denom);

                return CKIH * (div + pll_num / pll_denom) / post_div;
            }

        case VIDEO_PLL:
            div = read32(&anadig->analog_pll_video);
            if (!(div & ENABLE_MASK))   /* 判断VIDEO_PLL是否处于禁止输出模式 */
                return 0;

            if (div & BYPASS_MASK)      /* 判断VIDEO_PLL是否处于禁止输出模式 */
                return CKIH;
            else {
                post_div = (div & 0x3) >> 19;
                if (post_div == 3)  /* reserved value */
                    return 0;
                /* VIDEO_PLL的分频参数:0表示除以4,1表示除以2,2表示除以1 */
                post_div = 1 << (2 - post_div);

                pll_num = read32(&anadig->analog_pll_video_num);
                pll_denom = read32(&anadig->analog_pll_video_denom);

                return CKIH * (div + pll_num / pll_denom) / post_div;
            }

        default:
            return 0;
    }
    /* NOTREACHED */
}

static void set_pfd(u32 *reg, pfd_e pfd, int gate, u32 frac)
{
    u32 value = read32(reg);    /* 读取指定PLL的PFD寄存器 */
    value &= ~PFD_MASK(pfd);
    if (gate) value |= PFD_GATE_MASK(pfd);  /* 设置是否屏蔽该PFD的输出 */
    value |= (frac<<PFD_SHIFT(pfd)) & PFD_FRAC_MASK(pfd);   /* 设置该PFD的分频参数 */
    write32(value, reg);

    while(read32(reg) & PFD_STABLE_MASK(pfd));
}

/**********************************************************************
  * 函数名称: set_pll_pfd
  * 功能描述: 设置SYS_PLL或USB1_PLL的PFD状态和分频参数
  * 输入参数: pll: 指定PLL的标识,pfd: 指定PFD的编号,gate: 是否屏蔽该PFD的输出,frac: PFD的分频参数
  * 输出参数: 无
  * 返 回 值: 无
  * 修改日期        版本号     修改人       修改内容
  * -----------------------------------------------
  * 2020/03/08     V1.0      今朝        创建
  ***********************************************************************/
void set_pll_pfd(pll_e pll, pfd_e pfd, int gate, u32 frac)
{
    u32 *reg;
    if (pll = SYS_PLL)
        reg = &anadig->analog_pfd_528;
    else if (pll = USB1_PLL)
        reg = &anadig->analog_pfd_480;
    else
        /* 只有SYS_PLL和USB1_PLL支持PFD输出 */
        return ;

    set_pfd(reg, pfd, gate, frac);
}

/**********************************************************************
  * 函数名称: get_pll_pfd
  * 功能描述: 获取SYS_PLL或USB1_PLL的PFD输出频率
  * 输入参数: pll: 指定PLL的标识,pfd: 指定PFD的编号
  * 输出参数: 无
  * 返 回 值: 无
  * 修改日期        版本号     修改人       修改内容
  * -----------------------------------------------
  * 2020/03/08     V1.0      今朝        创建
  ***********************************************************************/
u32 get_pll_pfd(pll_e pll, pfd_e pfd)
{
    u32 div;
    u64 freq;

    switch (pll) {
        case SYS_PLL:
            div = read32(&anadig->analog_pfd_528);
            freq = (u64)get_pll(SYS_PLL);
            break;
        case USB1_PLL:
            div = read32(&anadig->analog_pfd_480);
            freq = (u64)get_pll(USB1_PLL);
            break;
        default:
            /* 只有SYS_PLL和USB1_PLL支持PFD输出 */
            return 0;
    }

    /* PFD输出频率为fPLL x 18 / N(N是PFD的分频参数) */
    return (freq * 18) / PFD_FRAC_VALUE(div, pfd);
}

6.3.2.2 获取PLL1_SW_CLK的时钟频率

​ 其次,这些时钟信号要经过时钟切换电路(switcher)的筛选,在文件switcher.c中增加接口获取筛选后的时钟信号频率:

/**********************************************************************
  * 函数名称: get_pll1_sw_clk
  * 功能描述: 获取PLL1_SW_CLK的时钟频率
  * 输入参数: 无
  * 输出参数: 无
  * 返 回 值: PLL1_SW_CLK的时钟频率
  * 修改日期        版本号     修改人       修改内容
  * -----------------------------------------------
  * 2020/03/08     V1.0      今朝        创建
  ***********************************************************************/
u32 get_pll1_sw_clk(void)
{
    u32 reg = read32(&ccm->ccsr);

    if (reg & (1u<<2)) {        /* PLL1_SW_CLK_SEL:0表示pll1_main_clk,1表示step_clk */
        if (reg & (1u<<8)) {    /* STEP_SEL:1表示secondary_clk, 0表示OSC */
            if (reg & (1u<<3))  /* SECONDARY_CLK_SEL:1表示PLL2,0表示PLL2 PFD2 */
                return get_pll(SYS_PLL);
            else
                return get_pll_pfd(SYS_PLL, PFD2);
        } else
            return CKIH;        /* OSC输出 */
    } else
        return get_pll(ARM_PLL);
}

/**********************************************************************
  * 函数名称: get_pll3_sw_clk
  * 功能描述: 获取PLL3_SW_CLK的时钟频率
  * 输入参数: 无
  * 输出参数: 无
  * 返 回 值: PLL3_SW_CLK的时钟频率
  * 修改日期        版本号     修改人       修改内容
  * -----------------------------------------------
  * 2020/03/08     V1.0      今朝        创建
  ***********************************************************************/
u32 get_pll3_sw_clk(void)
{
    u32 reg = read32(&ccm->ccsr);
    if (reg & 1)    /* PLL3_SW_CLK_SEL: 1表示pll3,0表示pll3_bypass(即OSC输出) */
        return get_pll(USB1_PLL);
    else
        return CKIH;            /* OSC输出 */
}

/**********************************************************************
  * 函数名称: get_pll4_main_clk
  * 功能描述: 获取PLL4_MAIN_CLK的时钟频率
  * 输入参数: 无
  * 输出参数: 无
  * 返 回 值: PLL4_MAIN_CLK的时钟频率
  * 修改日期        版本号     修改人       修改内容
  * -----------------------------------------------
  * 2020/03/08     V1.0      今朝        创建
  ***********************************************************************/
extern struct anadig_regs *anadig;

u32 get_pll4_main_clk(void)
{
    u32 reg, audio_div;

    reg = read32(&anadig->pmu_misc2);
    /* AUDIO_DIV_MSB(23): AUDIO_DIV_LSB(15)
      * 00:除以1
      * 01:除以2
      * 10:除以1
      * 11:除以4
      */
    audio_div = reg & (1u<<15) ? (reg & (1u<<23) ? 4 : 2) : 1;

    return get_pll(AUDIO_PLL) / audio_div;
}

/**********************************************************************
  * 函数名称: get_pll5_main_clk
  * 功能描述: 获取PLL5_MAIN_CLK的时钟频率
  * 输入参数: 无
  * 输出参数: 无
  * 返 回 值: PLL5_MAIN_CLK的时钟频率
  * 修改日期        版本号     修改人       修改内容
  * -----------------------------------------------
  * 2020/03/08     V1.0      今朝        创建
  ***********************************************************************/
u32 get_pll5_main_clk(void)
{
    u32 reg, video_div;

    reg = read32(&anadig->pmu_misc2);
    /* AUDIO_DIV_MSB(31): AUDIO_DIV_LSB(30)
      * 00:除以1
      * 01:除以2
      * 10:除以1
      * 11:除以4
      */
    video_div = reg & (1u<<30) ? (reg & (1u<<31) ? 4 : 2) : 1;

    return get_pll(VIDEO_PLL) / video_div;
}

6.3.2.3 获取PLL1_SW_CLK的时钟频率

​ 最后,根时钟生成电路(root generator)对上述信号进一步选择和分频,得到根时钟信号,在文件clkroot.c中添加以下代码:

/**********************************************************************
   * 函数名称: get_arm_clk_root
   * 功能描述: 获取ARM_CLK_ROOT的频率
   * 输入参数: 无
   * 输出参数: 无
   * 返 回 值: ARM_CLK_ROOT的频率
   * 修改日期        版本号     修改人       修改内容
   * -----------------------------------------------
   * 2020/03/08     V1.0      今朝        创建
   ***********************************************************************/
u32 get_arm_clk_root(void)
{
    u32 reg, freq;

    reg = read32(&ccm->cacrr);
    reg = (reg & 0x7) + 1;              /* 获取ARM_PODF的分频范围 */
    freq = get_pll(ARM_PLL);

    return freq / reg;
}

/**********************************************************************
   * 函数名称: get_periph_clk
   * 功能描述: 获取PERIPH_CLK的频率
   * 输入参数: 无
   * 输出参数: 无
   * 返 回 值: PERIPH_CLK的频率
   * 修改日期        版本号     修改人       修改内容
   * -----------------------------------------------
   * 2020/03/08     V1.0      今朝        创建
   ***********************************************************************/
static u32 get_periph_clk(void)
{
    u32 reg, per_clk2_podf = 0, freq = 0;

    reg = read32(&ccm->cbcdr);

    /* PERIPH_CLK_SEL选择periph_clk的时钟源:1表示periph_clk2, 0表示pre_periph_clk */
    if (reg & (1u << 25)) {         /* 选择periph_clk2 */
        per_clk2_podf = (reg >> 27) & 0x7;    /* 获取PERIPH_CLK2_PODF */
        reg = read32(&ccm->cbcmr);
        reg = (reg >> 12) & 0x3;    /* PERIPH_CLK2_SEL */

        /* PERIPH_CLK2_SEL: 0表示pll3_sw_clk,1表示osc_clk,2表示pll2_bypass_clk(即osc_clk) */
        switch (reg) {
            case 0:
                freq = get_pll(USB1_PLL);
                break;
            case 1:
            case 2:
                freq = CKIH;
                break;
            default:
                break;
        }

        freq /= (per_clk2_podf + 1);
    } else {                        /* 选择pre_periph_clk */
        reg = read32(&ccm->cbcmr);
        reg = (reg >> 18) & 0x3;    /* PRE_PERIPH_CLK_SEL */

        /* PRE_PERIPH_CLK_SEL:0表示PLL2输出,1表示PLL2 PFD2输出,2表示PLL2 PFD0输出,3表示PLL2 PFD2输出频率的一半 */
        switch (reg) {
            case 0:
                freq = get_pll(SYS_PLL);
                break;
            case 1:
                freq = get_pll_pfd(SYS_PLL, PFD2);
                break;
            case 2:
                freq = get_pll_pfd(SYS_PLL, PFD0);
                break;
            case 3:     /* static / 2 divider */
                freq = get_pll_pfd(SYS_PLL, PFD2) / 2;
                break;
            default:
                break;
        }
    }

    return freq;
}

/**********************************************************************
  * 函数名称: get_ahb_clk_root
  * 功能描述: 获取AHB_CLK_ROOT的频率
  * 输入参数: 无
  * 输出参数: 无
  * 返 回 值: AHB_CLK_ROOT的频率
  * 修改日期        版本号     修改人       修改内容
  * -----------------------------------------------
  * 2020/03/08     V1.0      今朝        创建
  ***********************************************************************/
u32 get_ahb_clk_root(void)
{
    u32 reg, ahb_podf;

    reg = read32(&ccm->cbcdr);
    ahb_podf = (reg >> 10) & 0x7;   /* 获取AHB_PODF的分频设置 */

    return get_periph_clk() / (ahb_podf + 1);
}

/**********************************************************************
  * 函数名称: get_ipg_clk_root
  * 功能描述: 获取IPG_CLK_ROOT的频率
  * 输入参数: 无
  * 输出参数: 无
  * 返 回 值: IPG_CLK_ROOT的频率
  * 修改日期        版本号     修改人       修改内容
  * -----------------------------------------------
  * 2020/03/08     V1.0      今朝        创建
  ***********************************************************************/
u32 get_ipg_clk_root(void)
{
    u32 reg, ipg_podf;

    reg = read32(&ccm->cbcdr);
    ipg_podf = (reg >> 8) & 0x3;    /* 获取IPG_PODF的分频设置 */

    return get_ahb_clk_root() / (ipg_podf + 1);
}

/**********************************************************************
  * 函数名称: get_axi_clk_root
  * 功能描述: 获取AXI_CLK_ROOT的频率
  * 输入参数: 无
  * 输出参数: 无
  * 返 回 值: AXI_CLK_ROOT的频率
  * 修改日期        版本号     修改人       修改内容
  * -----------------------------------------------
  * 2020/03/08     V1.0      今朝        创建
  ***********************************************************************/
u32 get_axi_clk_root(void)
{
    u32 root_freq, axi_podf;
    u32 reg =  read32(&ccm->cbcdr);

    axi_podf = (reg >> 16) & 0x7;   /* 获取AXI_PODF的分频设置 */

    if (reg & (1u << 6)) {          /* AXI_SEL: 1表示axi_alt_clk,0表示periph_clk */
        if (reg & (1u << 7))        /* AXI_ALT_SEL:1表示PLL3 PFD1的输出,0表示PLL2 PFD2的输出 */
            root_freq = get_pll_pfd(USB1_PLL, PFD1);
        else
            root_freq = get_pll_pfd(SYS_PLL, PFD2);
    } else
        root_freq = get_periph_clk();   /* periph_clk */

    return  root_freq / (axi_podf + 1);
}

/**********************************************************************
  * 函数名称: get_fabric_mmdc_clk_root
  * 功能描述: 获取FABRIC_MMDC_CLK_ROOT的频率
  * 输入参数: 无
  * 输出参数: 无
  * 返 回 值: FABRIC_MMDC_CLK_ROOT的频率
  * 修改日期        版本号     修改人       修改内容
  * -----------------------------------------------
  * 2020/03/08     V1.0      今朝        创建
  ***********************************************************************/
u32 get_fabric_mmdc_clk_root(void)
{
    u32 cbcmr = read32(&ccm->cbcmr);
    u32 cbcdr = read32(&ccm->cbcdr);

    u32 freq, podf, per2_clk2_podf;

    podf = (cbcdr >> 3) & 0x7;  /* 获取FABRIC_MMDC_PODF的分频设置 */

    if (cbcdr & (1u << 26)) {   /* PERIPH2_CLK_SEL: 1选择periph2_clk2,0选择pre_periph2_clk */
        per2_clk2_podf = cbcdr & 0x7;   /* 获取PERIPH2_CLK2_PODF的分频设置 */
        if (cbcmr & (1u << 20)) /* PERIPH2_CLK2_SEL:1选择osc_clk,0选择pll3_sw_clk */
            freq = CKIH;
        else
            freq = get_pll(USB1_PLL);

        freq /= (per2_clk2_podf + 1);
    } else {                    /* pre_periph2_clk */
        extern u32 get_pll4_main_clk(void);
        /* PRE_PERIPH2_CLK_SEL:0表示PLL2输出,1表示PLL2 PFD2输出,2表示PLL2 PFD0输出,3表示_main_clk */
        switch ((cbcmr >> 21) & 0x3) {
            case 0:
                freq = get_pll(SYS_PLL);
                break;
            case 1:
                freq = get_pll_pfd(SYS_PLL, PFD2);
                break;
            case 2:
                freq = get_pll_pfd(SYS_PLL, PFD0);
                break;
            case 3:
                freq = get_pll4_main_clk();
                break;
        }
    }

    return freq / (podf + 1);
}

6.3.2.4 函数打印时钟值

​ 最终,所有的时钟通过函数show_clocks打印,该函数定义在main.c中:

 27 /**********************************************************************
 28  * 函数名称: show_clocks
 29  * 功能描述: 打印PLL输出时钟频率和总线根时钟频率
 30  * 输入参数: 无
 31  * 输出参数: 无
 32  * 返 回 值: 无
 33  * 修改日期        版本号     修改人       修改内容
 34  * -----------------------------------------------
 35  * 2020/03/08     V1.0      今朝        创建
 36  ***********************************************************************/
 37 void show_clocks(void)
 38 {
 39     u32 freq;
 40 
 41     printf("Show IMX6ULL Clocks: \r\n");
 42     freq = get_pll(ARM_PLL);
 43     printf("ARM_PLL     %8d MHz\r\n", freq / 1000000);
 44 
 45     freq = get_pll(SYS_PLL);
 46     printf("SYS_PLL     %8d MHz\r\n", freq / 1000000);
 47     freq = get_pll_pfd(SYS_PLL, PFD0);
 48     printf("|-SYS_PLL_PFD0  %8d MHz\r\n", freq / 1000000);
 49     freq = get_pll_pfd(SYS_PLL, PFD1);
 50     printf("|-SYS_PLL_PFD1  %8d MHz\r\n", freq / 1000000);
 51     freq = get_pll_pfd(SYS_PLL, PFD2);
 52     printf("|-SYS_PLL_PFD2  %8d MHz\r\n", freq / 1000000);
 53     freq = get_pll_pfd(SYS_PLL, PFD3);
 54     printf("|-SYS_PLL_PFD3  %8d MHz\r\n", freq / 1000000);
 55 
 56     freq = get_pll(USB1_PLL);
 57     printf("USB1_PLL    %8d MHz\r\n", freq / 1000000);
 58     freq = get_pll_pfd(USB1_PLL, PFD0);
 59     printf("|-USB1_PLL_PFD0 %8d MHz\r\n", freq / 1000000);
 60     freq = get_pll_pfd(USB1_PLL, PFD1);
 61     printf("|-USB1_PLL_PFD1 %8d MHz\r\n", freq / 1000000);
 62     freq = get_pll_pfd(USB1_PLL, PFD2);
 63     printf("|-USB1_PLL_PFD2 %8d MHz\r\n", freq / 1000000);
 64     freq = get_pll_pfd(USB1_PLL, PFD3);
 65     printf("|-USB1_PLL_PFD3 %8d MHz\r\n", freq / 1000000);
 66 
 67     freq = get_pll(USB2_PLL);
 68     printf("USB2_PLL    %8d MHz\r\n", freq / 1000000);
 69     freq = get_pll(AUDIO_PLL);
 70     printf("AUDIO_PLL   %8d MHz\r\n", freq / 1000000);
 71     freq = get_pll(VIDEO_PLL);
 72     printf("VIDEO_PLL   %8d MHz\r\n", freq / 1000000);
 73 
 74     printf("\r\n");
 75     freq = get_arm_clk_root();
 76     printf("ARM_CLK_ROOT    %8d KHZ\r\n", freq / 1000);
 77     freq = get_ahb_clk_root();
 78     printf("AHB_CLK_ROOT    %8d KHZ\r\n", freq / 1000);
 79     freq = get_ipg_clk_root();
 80     printf("IPG_CLK_ROOT    %8d KHZ\r\n", freq / 1000);
 81     freq = get_axi_clk_root();
 82     printf("AXI_CLK_ROOT    %8d KHZ\r\n", freq / 1000);
 83     freq = get_fabric_mmdc_clk_root();
 84     printf("FABRIC_MMDC_CLK_ROOT    %8d KHZ\r\n", freq / 1000);
 85     printf("\r\n");
 86 }

​ 在第二次设置完时钟后,调用show_clocks函数打印。我们通过串口看到CPU最终的频率为648000KHZ,即648MHZ,与程序中设置的一致。

注明: 代码目录在裸机Git仓库 NoosProgramProject/(7_时钟体系/showclocks) 文件夹下。

6.3.2.5 参考章节《4-1.4编译程序》编译程序

6.3.2.6 参考章节《4-1.4映像文件烧写、运行》烧写、运行程序

​ 运行成功后可以观察到串口打印所有的时钟频率信息。

6.3.3 补充说明

​ 第二个示例中CPU频率增加到8倍,然而led的闪烁频率似乎并没有变为原来的8倍。这是因为延时函数busy_wait执行过程访问了内存,内存的速度限制了程序的性能。反汇编showclock.elf可以看到以下代码:

​ 函数busy_wait在空循环过程中访问了三次内存,造成了性能瓶颈。解决方法之一是使用嵌入式汇编重新定义该函数,消除内存访问操作,代码如下:

static void busy_wait(void)
{
    __asm__ __volatile__ (
        "ldr r0, =3000000\n"
        "1:\n"
        "subs r0, r0, #1\n"
        "bne 1b\n"
        :::"r0");
}

​ 修改后可发现,在CPU频率设置为648MHZ之后,led灯闪烁频率明显比修改前快很多。另一个避免直接访问内存的方法是开启D-Cache,有兴趣的同学可以尝试一下。

​ 另外,为了更加直观的查看时钟频率,用户还可以将时钟信号通过CCM_CLKO1和CCM_CLKO2输出,使用示波器直接观察。相关的代码在文件clkout.c中,默认这些代码并不执行,有条件的同学可以开启这段代码进行实验。

第八章:UART串口编程

第八章:UART串口编程

8. UART串口编程

8.1 UART介绍

8.1.1 UART串口简介

​ UART全称是通用异步收发传输器(Universal Asynchronous Receiver/Transmitter)。串口顾名思义数据串行接口,即数据的传输是一位接一位传输,属于一种串行的数据总线,属于异步通讯,同时支持全双工数据传输(全双工数据传输:允许发送数据和接收数据在同一时刻发生) 。

​ 除了UART,另外还有一种叫USART,全称是通用同步/异步串行接收/发送器(Universal Synchronous/Asynchronous Receiver/Transmitter),USART比 UART多了同步通信功能,但是百分之90的工程应用中不会应用该同步功能,都是将USART当做UART使用,即采取异步串行通讯。一般开发板或者产品中都会将UART串口标为serial或COM。

8.1.2 UART硬件连接

​ 1) UART串口最精简的连接是TTL电平三线连接

​ UARTx_TXD:用于发送数据,应连接到接收设备的UARTx_RXD引脚上;

​ UARTx_RXD:用于接收数据,应连接到发送设备的UARTx_TXD引脚上;

​ GND:为双方提供一个相同的参考电平。

​ 上图为UART串口TTL电平硬件连接,此时使用标准的TTL电平来表示数据,高电平表示1,低电平表示0,标准TTL输入高电平最小2V,输出高电平最小2.4V,典型值为3.4V,输入低电平最大0.8V,输出低电平最大0.4V,典型值为0.2V。

​ 直接采用TTL电平进行串行通讯,由于其抗干扰能力差,导致传输距离短,且容易出现数据不可靠的情况。

​ 为提高抗干扰能力和传输距离,一般采用下面两种硬件连接方式。

​ 2)TTL电平转RS232电平

​ 硬件连接如下图

​ RS232电平规定逻辑“1”的电平为-5V~-15 V,逻辑“0”的电平为+5 V~+15 V,选用该电气标准以提高抗干扰能力。常用的TTL转RS232芯片有:MAX232,SP3232等。

​ 3)TTL电平转USB电平

​ 将TTL电平转换为USB电平(D+与D-一对差分信号采用NRZI编码实现通讯),提高抗干扰能力,常用的TTL转USB芯片有:PL2303,CH340, CP2102等

​ 100ASK_IMX6ULL采用的是上述方案中的第三种(TTL转USB方案)。

8.1.3 UART通讯数据格式

​ UART之间为何能够准确可靠的的发送和接收数据?

​ 首先我们需要约定好传输速率(每一秒传输的数据位数,即波特率),一般选择9600,19200,115200等。

​ 确定好传输速率后,我们还需要确定传输数据的格式,UART串口通信的数据包以帧为单位,常用的帧结构为:1位起始位+8位数据位+1位奇偶校验位(可选)+1位停止位。

​ 举例波形说明如下图:

​ 上图为:1位起始位+8位数据位+1位偶校验位+1位停止位 的波形。

​ 根据查找ASCII码表得知’ A’字符的ASCII值为41(十进制),将其转换成二进制应该为0100 0001,小端传输,即低位(LSB)在前,高位(MSB)在后,和上图所示一致。

​ 上图中各位的详细说明如下:

​ 1) 空闲位:

​ 平时没有数据时,数据线为高电平(逻辑1);

​ 2) 起始位:

​ 当需要发送数据时,UART将改变UARTx_TXD的状态,变为低电平,即为上图中的起始位(逻辑0);

​ 3) 数据位:

​ 可以有5、6、7或8位的数据,一般我们是按字节(8位)传输数据,发送方一位一位的改变数据线上的状态(高电平或低电平)将它们发送出去,传输数据时先传最低位,最后传送最高位。

​ 字符’A’的8位二进制字符是0100 0001,先发送最低位bit 0,其值为1;再发送bit 1,其值为0,如此继续;最后发送最高位bit 7,其值为0。

​ 4) 奇偶校验位

​ 如果使用了奇偶校验功能,有效数据位发送完毕后,还要发送1个校验位(奇偶校验位)。

​ 有两种校验方法:奇校验,偶校验-------数据位连同校验位中,“1”的数目等于奇数或偶数。奇偶校验只能检错,不能纠错的。而且只能检测1位误码,检测出有错后只能要求重发,没法纠正的。

​ 上图中使用的是偶较验,即8个数据位和1个校验位中,一共有偶数个“1”:2个。

​ 5) 停止位

​ 停止位(逻辑1)用来表示当前数据传输完毕。

​ 停止位的长度有三种:1位,1.5位,2位,通常我们选择1位即可。

8.2 IMX6ULL UART寄存器介绍

​ UART:Universal Asynchronous Receiver/Transmitter,通用异步收发传输器。

8.2.1 IMX6ULL UART模块简介

​ IMX6ULL共8个独立的UART通道,即8个UART,主要特性如下:

​ a.兼容高速串口标准TIA/EIA-232-F,高达5Mbit/s;

​ b.低速串行红外接口(IR),兼容Ir-DA(速度高达115.2Kbit/s);

​ c.支持9位或多点模式(RS-485)(自动从机地址检测);

​ d.1或2位停止位;

​ e.可编程奇偶校验(偶校验,奇校验,不校验);

​ f.自动波特率检测(最高支持115.2Kbit/s);

​ g.可屏蔽中断

​ h.软复位(SRST_B)

​ 以上只是列举了部分常用的特性,如需要更加详细的了解需要查看芯片手册《Chapter 55 Universal Asynchronous Receiver/Transmitter(UART)》。

8.2.2 IMX6ULL UART寄存器简介

​ IMX6ULL 8路UART通道,每一路通道都有17个寄存器,掌握其中一路通道,其他通道都是照葫芦画瓢,只是基地址变了和IO复用管脚不同了。

​ UART1的寄存器如下图:

​ 而我们本次实验使用到的只有上面红色框框9项。

8.2.2.1 UART1_URXD:

​ 主要用于接收串口数据的寄存器,只有低八位的空间是存储接收数据的,其他是一些判断位,基本用不上;

8.2.2.2 UART1_UTXD:

​ 用于发送串口数据的寄存器,只有低八位的空间用于发送数据,其他位保留不使用;

8.2.2.3 UART1_UCR1:

​ 控制寄存器1,用于设置串口各类功能的使能,例如自动波特率检测的使能,发送中断,串口DMA使能,串口使能等。而我们本章节只使用bit0,串口使能即可。

8.2.2.4 UART1_UCR2:

​ 控制寄存器2:主要用于设置串口的发送帧格式,帧长,是否奇偶校验,是否忽略有RTS,软复位等,本章只使用其中的5位,后续UART编程会详细讲解。

8.2.2.5 UART1_UCR3:

​ 控制寄存器3:我们只设置bit2,官方要求设置,属于芯片特点。

8.2.2.6 UART1_UFCR:

​ 串口FIFO控制寄存器,设置发送与接收的fifo的大小,最大32字节,串口时钟分频系数等,只要把RFDIV此位设置为不分频,其他用默认值即可,更详细使用会在后面的UART编程中讲解。

8.2.2.7 UART1_USR2:

​ 串口状态寄存器,该寄存器里面主要是一些串口的状态位,我们本章只使用了TXDC发送完成位与ROR接收数据就绪位。详细使用会在后面UART编程中讲解

8.2.2.8 UART1_UBIR与UART1_UBMR:

​ 用于设置波特率即每秒可传输的位数,后面UART编程会详细讲解。

​ 基本我们使用的寄存器就这些,其他都是一些功能扩展,想详细了解的可以查看芯片手册的《Chapter 55 Universal Asynchronous Receiver/Transmitter(UART)》

8.3 IMX6ULL UART编程

8.3.1 看原理图确定UART引脚

​ 从上图可知,采用的UART转USB的方案,使用的是UART1。查看IMX6ULL芯片手册《Chapter 55 Universal Asynchronous Receiver/Transmitter(UART)》中涉及关键字UART1的寄存器并设置它。

8.3.2 涉及的UART1寄存器配置

​ 寄存器配置共分为4步骤,该小节中的代码都在程序文件uart.c中

8.3.2.1 步骤1:配置并使能UART1时钟

​ 配置并使能UART1时钟,参考资料:芯片手册《Chapter 18: Clock Controller Module (CCM)》

​ ①需要配置UART模块的时钟(寄存器:CCM_CSCDR1)

​ 根据上图可知,我们需要设置CCM_CSCDR1 [UART_CLK_SEL]和CCM_CSCDR1 [UART_CLK_PODF]。

​ 由上图CCM_CSCDR1寄存器,我们可以了解到

​ CCM_CSCDR1 [UART_CLK_SEL]默认值为0;CCM_CSCDR1 [UART_CLK_PODF]默认值为0

​ 我们一般选择 pll3_80m 作为 UART 的时钟源,UART_CLK_PODF分频系数选1分频(不分频),最后得到UART的时钟频率为80MHz。

​ 正好默认值都为0满足我们的时钟需求,所以后续的编程实验,串口时钟这部分可以不设置,用默认值就可以了。

​ ②需要使能UART模块的时钟(寄存器:CCM_CCGR5)

​ 由上图CCM_CCGR5寄存器,我们可以了解到CCM_CCGR5[CG12] 的默认值为11

​ 参考章节《4-1.3 CCM用于设置是否向GPIO模块提供时钟》我们了解到11表示该模块全程使能,使用默认值,无需设置。

​ 因此时钟这块我们都不需要配置,直接使用默认值即可。

8.3.2.2 步骤2:复用相关GPIO为UART1功能

​ 复用相关GPIO为UART1功能,参考资料:芯片手册《Chapter 32: IOMUX Controller (IOMUXC)》

​ 根据前面硬件连接讲解,我们知道串口通信精简接法需要3根信号线,其中GND已由硬连接了,所以接下来我们需要配置剩余的两个GPIO引脚 (UART1_TXD与UART1_RXD)

​ ①需要配置UART1_TX复用功能

​ (寄存器:IOMUXC_SW_MUX_CTL_PAD_UART1_TX_DATA)

​ 由上图我们得知IOMUXC_SW_MUX_CTL_PAD_UART1_TX_DATA[MUX_MODE]的默认值为0101,因此我们需要将其改为0,用于UART_TX功能

​ ②需要配置UART1_RX复用功能

​ (寄存器:IOMUXC_SW_MUX_CTL_PAD_UART1_RX_DATA)

​ 由上图我们得知IOMUXC_SW_MUX_CTL_PAD_UART1_RX_DATA[MUX_MODE]的默认值为0101,因此我们需要将其改为0,用于UART_RX功能。(程序文件:uart.c)

IOMUXC_SW_MUX_CTL_PAD_UART1_TX_DATA		= (volatile unsigned int *)(0x20E0084);
IOMUXC_SW_MUX_CTL_PAD_UART1_RX_DATA		= (volatile unsigned int *)(0x20E0088);

*IOMUXC_SW_MUX_CTL_PAD_UART1_RX_DATA = 0;
*IOMUXC_SW_MUX_CTL_PAD_UART1_TX_DATA = 0;

​ ③ 需要配置UART1_TX硬件参数(寄存器:IOMUXC_SW_PAD_CTL_PAD_UART1_TX_DATA)

​ 参考章节《4-1.3 IOMUXC:引脚的模式(Mode、功能)》,经过比对,我们使用默认值(0x10b0)即可,无需配置。

​ ④ 需要配置UART1_RX硬件参数(寄存器:IOMUXC_SW_PAD_CTL_PAD_UART1_RX_DATA)

​ 参考章节《4-1.3 IOMUXC:引脚的模式(Mode、功能)》, 经过比对,我们使用默认值(0x10b0)即可,无需配置。

8.3.2.3 步骤3:设置UART1传输格式,波特率

​ 设置UART1传输格式,波特率

​ ① 配置寄存器UART1_UCR2(0x2020084)

​ 设置UART1传输格式

UART1->UCR2 |= (1<<14) |(1<<5) |(1<<2)|(1<<1);

​ [14] 1:忽略RTS引脚

​ [8] : 0: 关闭奇偶校验 默认为0,无需设置

​ [6] : 0: 停止位1位 默认为0,无需设置

​ [5] : 1: 数据长度8位

​ [2] : 1: 发送数据使能

​ [1] : 1: 接收数据使能

​ ②配置寄存器UART1_UCR3(0x2020088)

​ 根据官方文档表示 [RXDMUXSEL]需要设置为1。

​ [2] : 1:IM6ULL的UART用了这个MUXED模型,因此这一位需要置位为1

​ 设置串口模型

UART1->UCR3 |= (1<<2);

​ ③寄存器UART1_UFCR(0x2020090)

​ UART1_UFCR[9-7]:UART的时钟源分频系数

​ 我们选择101(即十进制的5),表示不分频。

UART1->UFCR = 5 << 7;    /* Uart的时钟clk:80MHz */

​ ④寄存器UART1_UBIR(0x20200A4), UART1_UBMR(0x20200A8)

​ 通过设置UART1_UBIR与UART1_UBMR,最终确定波特率。

​ IMX6ULL波特率计算公式:

​ a.设置115200的波特率即BaudRate = 115200;

​ b.UART1的时钟频率前面内容已确定80Mhz即Ref Freq = 80000000;

​ c.IMX6ULL波特率计算公式得115200 = 80000000 /(16*(UBMR + 1)/(UBIR+1));

​ d.选取一组满足上式的参数:UBMR、UBIR即可;

​ e. UART1_UBIR = 71 ; UART1_UBMR = 3124

UART1->UBIR = 71;
UART1->UBMR = 3124;

8.3.2.4 步骤4:使能UART1

​ 使能UART1,UART1_UCR1(0x2020080)寄存器如下:

​ 配置UART1_UCR1[0]:1表示使能UART, 0表示关闭UART。

Base->UCR1 |= (1 << 0);		/*使能当前串口*/

8.3.3 实现串口发送功能

8.3.3.1 步骤1: 编写UART1发送单字节函数

​ 写这个函数之前,我们需要了解什么时候才能发,什么时候不能发。

​ 只有当上一个数据发完的时候,我们才能继续发送,因此需要用到UART1_USR2寄存器中表示UART1发送状态的只读状态位[TXDC]。

​ UART1_USR2[3] : 0表示发送未完成 , 1表示发送已完成

​ 程序文件:uart.c

void PutChar(int c)						
{
	while (!((UART1->USR2) & (1<<3))); /*等待上个字节发送完毕*/
	UART1->UTXD = (unsigned char)c;		
}

8.3.3.2 步骤2:编写用于测试的main函数

​ 程序文件:main.c

#include "uart.h"

int  main()
{
	unsigned char cTestData = 'A'; /*用于测试发送的数据*/
 	Uart_Init()	 ;

	while(1)
	{	
		PutChar(cTestData);
	}
					
	return 0;
}

**注明:**整个完整工程代码目录在裸机Git仓库 NoosProgramProject/(8_UART串口编程/001_uart_txd_char)文件夹下。

8.3.3.3 步骤3:参考章节《4-1.4编译程序》编译程序
8.3.3.4 步骤4:参考章节《3-1.4映像文件烧写、运行》烧写、运行程序

​ 如果实验结果如上图电脑串口将会不断收到 ‘A’,表明实验成功。

8.3.4 实现串口接收功能

8.3.4.1 步骤1:编写UART1接收单字节函数

​ 编写UART1接收单字节函数,接收单字节时,我们也需要去判定UART1_USR2寄存器中的只读状态位[ROR]。

​ UART1_USR2[0] : 0表示没有接收数据就绪, 1表示接收数据准备就绪。

​ 程序文件:uart.c

unsigned char GetChar(void)						
{	
	while (!(UART1->USR2 & (1<<0)));  /*等待接收数据*/
	return (unsigned char)UART1->URXD;
}

8.3.4.2 步骤2:编写用于测试main函数

​ 编写用于测试main函数,我们让串口接收到的数据,交给上一小节中所写的发送函数中,从而实现串口回显。

​ 程序文件:main.c

#include "uart.h"

int  main()
{
	unsigned char cTestData ;       /*用于测试发送的数据*/
	Uart_Init()	 ;

	while(1)
	{	
		cTestData = GetChar() ;				/*等待从串口获取数据*/
		PutChar(cTestData)    ;				/*从串口发送数据*/
	}
					
	return 0;
}

​ 接着我们参照参考《4.4.4 编译程序》与参考《3.4 映像文件烧写、运行》,上机验证

​ 在键盘中输入的数据,会在终端回显出来表明实验成功,例如:如上图,在键盘上输入100ask.6ull,终端会完整回显出来。

**注明:**整个完整工程代码目录在裸机Git仓库 NoosProgramProject/(8_UART串口编程/002_uart_txd_char)文件夹下。

8.3.5 完善回显功能

​ 我们会发现上一小节所写的函数中有点问题,当我们按下回车的时候不会换行到行首,而是直接到行首,所以我们稍微修改main函数完善它。

​ 程序文件:main.c

while(1)
{	
    cTestData = GetChar() ;				/*等待从串口获取数据*/

    if (cTestData == '\r')  		/*添加回车换行\n\r*/
    { 
        PutChar('\n');
    }

    if (cTestData == '\n')
    {
        PutChar('\r');
    }

    PutChar(cTestData)    ;				/*从串口发送数据*/
}

​ 在获得数据的时候,判定回车符‘\r’和换行符‘\n’即可,重新烧写上机实验,按下回车键就能回车换行

**注明:**整个完整工程代码目录在裸机Git仓库 NoosProgramProject/(8_UART串口编程/003_uart_better_char)文件夹下。

8.3.6 实现串口发送字符串功能

8.3.6.1 步骤1:实现打印字符串函数

​ 实现打印字符串函数**,**在发送单字节的基础上,加上判断语句,实现连续打印字符。

​ 程序文件:my_printf.c

void PutStr(const char *s)				
{
	while (*s)
	{
		PutChar(*s);
		s++;
	}
}

8.3.6.2 步骤2:在main函数中添加打印字符串函数的调用

​ 在main函数中添加打印字符串函数的调用。

​ 程序文件:main.c

PutStr("Hello, world!\n\r");  /*发送字符串*/

​ **注明:**整个完整工程代码目录在裸机Git仓库 NoosProgramProject/(8_UART串口编程/004_uart_str)文件夹下。

8.3.6.3 步骤3:参考章节《4-1.4编译程序》编译程序
8.3.6.4 步骤4:参考章节《3-1.4映像文件烧写、运行》烧写、运行程序

​ 终端将打印出Hello,world!

Hello, world!

8.4 移植printf

8.4.1 在1.3章节工程文件基础上移植

​ ①在uart.c中加入raise函数,用于防止编译失败。

​ 程序文件:uart.c

119 int raise(int signum)/* raise函数,防止编译报错 */
120 {
121   return 0;
122 }

​ ②修改Makefile

​ my_printf.c中用到除法的求模运算,需要提供除法库。一般的交叉工具链里都提示有基本的数学运算,它们位于libgcc.a中。我们需要把libgcc.a也链接进程序里,需要修改Makefile。

​ 注意:链接指令中,每年“-L”表示库在哪里,即它的目录;“-l”表示哪个库,即库的名称,-lgcc 表示会链接“libgcc.a”库。

​ 对Makefile作如下修改:

​ a. 增加$(CC) -nostdlib -g -c -o my_printf.o my_printf.c

​ b. 在$(LD) -T imx6ull.lds -g start.o uart.o main.o my_printf.o -o my_printf.elf后添加

​ -lgcc –L<libgcc.a的路径>

​ 例如:$(LD) -T imx6ull.lds -g start.o uart.o main.o my_printf.o -o my_printf.elf -lgcc

-L/home/book/100ask_imx6ull-sdk/ToolChain/gcc-linaro-6.2.1*-2016.11-x86_64_arm-linux-gnueabihf/lib/gcc/arm-linux-gnueabihf/6.2.1

8.4.2 变参数函数移植

​ 函数参数列表包括了字符串(format)和变参(…)组合而成

​ 在vc6.0的头文件stdarg.h中找到

typedef char *  va_list;

#define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
#define va_start(ap,v)  ( ap = (va_list)&v + _INTSIZEOF(v) )
#define va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_end(ap)      ( ap = (va_list)0 )

​ 1) _INTSIZEOF(n) 用于获取其中一个变参类型占用的空间长度

​ 2) va_start(ap,v) 令ap指向第一个变参地址

​ 3) va_arg(ap,t) 取出一个变参,同时指针指向下一个变参

​ 4) va_end(ap) 将指针指向NULL,防止野指针

​ 移植以上的代码,编写一个属于自己的printf

​ 参考int printf(const char *format, ...)库函数,实现my_printf

​ 程序文件:my_printf.c

int printf(const char *fmt, ...) 
{
    va_list ap;

    va_start(ap, fmt);
    my_vprintf(fmt, ap);	
    va_end(ap);
    return 0;
}

8.4.3 编写my_vprintf(fmt, ap)

​ 参考int vprintf(const char *format, va_list ap)实现my_vprintf

​ 程序文件:my_printf.c

static int my_vprintf(const char *fmt, va_list ap) 
{
	char lead=' ';
	int  maxwidth=0;
	
	 for(; *fmt != '\0'; fmt++)		
	 {
		if (*fmt != '%') {			
			outc(*fmt);				
			continue;
	 	}

		 lead=' ';
		 maxwidth=0;
	 
		//format : %08d, %8d,%d,%u,%x,%f,%c,%s 
		    fmt++;
		if(*fmt == '0'){
			lead = '0';
			fmt++;	
		}

	
		while(*fmt >= '0' && *fmt <= '9'){
			maxwidth *=10;
		maxwidth += (*fmt - '0');
		fmt++;
	}
	
	switch (*fmt) {
	case 'd': out_num(va_arg(ap, int),          10,lead,maxwidth); break;
    case 'o': out_num(va_arg(ap, unsigned int),  8,lead,maxwidth); break;			
	case 'u': out_num(va_arg(ap, unsigned int), 10,lead,maxwidth); break;
	case 'x': out_num(va_arg(ap, unsigned int), 16,lead,maxwidth); break;
	case 'c': outc(va_arg(ap, int   )); break;		
    case 's': outs(va_arg(ap, char *)); break;		  		
			
    default:  
		outc(*fmt);
	    break;
		}
	}
	return 0;
}

8.4.4 编写out_c,outs与out_num函数

​ 利用之前我们实现的单字节打印函数void PutChar(int c)实现out_c,outs与out_num函数outc用于格式化输出中的%c的输出。

​ 程序文件:my_printf.c

static int outc(int c) 
{
	PutChar(c);
	return 0;
}

​ outs用于格式化输出中的%s的输出。

​ 程序文件:my_printf.c

static int outs (const char *s)
{
	while (*s != '\0')	
		PutChar(*s++);
	return 0;
}

​ out_num用于格式化输出中的%d,%o,%u,%x的输出。

​ 程序文件:my_printf.c

static int out_num(long n, int base,char lead,int maxwidth) 
{
    unsigned long m=0;
    char buf[MAX_NUMBER_BYTES], *s = buf + sizeof(buf);
    int count=0,i=0;


    *--s = '\0';

    if (n < 0){
        m = -n;
    }
    else{
        m = n;
    }

    do{
        *--s = hex_tab[m%base];
        count++;
    }while ((m /= base) != 0);

    if( maxwidth && count < maxwidth){
        for (i=maxwidth - count; i; i--)	
            *--s = lead;
    }

    if (n < 0)
        *--s = '-';

    return outs(s);
}

8.4.5 编写my_printf_test的测试函数

​ 程序文件:my_printf.c

int my_printf_test(void)
{
    printf("This is www.100ask.org   my_printf test\n\r") ;	
    printf("test char           =%c,%c\n\r", 'A','a') ;	
    printf("test decimal number =%d\n\r",    123456) ;
    printf("test decimal number =%d\n\r",    -123456) ;	
    printf("test hex     number =0x%x\n\r",  0x55aa55aa) ;	
    printf("test string         =%s\n\r",    "www.100ask.org") ;	
    printf("num=%08d\n\r",   12345);
    printf("num=%8d\n\r",    12345);
    printf("num=0x%08x\n\r", 0x12345);
    printf("num=0x%8x\n\r",  0x12345);
    printf("num=0x%02x\n\r", 0x1);
    printf("num=0x%2x\n\r",  0x1);

    printf("num=%05d\n\r", 0x1);
    printf("num=%5d\n\r",  0x1);

    return 0;
}

8.4.6 编写main测试程序

​ 在main函数中只需要调用8.3.5中实现的my_printf_test即可。

​ 程序文件:main.c

#include "my_printf.h"
#include "uart.h"
int  main()
{
    Uart_Init();
    my_printf_test();			
    return 0;
}

注明: 此例程代码目录在裸机Git仓库 NoosProgramProject/(8_UART串口编程/005_myprintf_test)。

8.4.6.1 步骤1:参考章节《4-1.4编译程序》编译程序
8.4.6.2 步骤2:参考章节《3-1.4映像文件烧写、运行》烧写、运行程序

​ 最后编译程序烧写至开发板,观察终端输出信息。

This is www.100ask.org   my_printf test
test char           =A,a
test decimal number =123456
test decimal number =-123456
test hex     number =0x55aa55aa
test string         =www.100ask.org
num=00012345
num=   12345
num=0x00012345
num=0x   12345
num=0x01
num=0x 1
num=00001
num=    1

​ 串口终端如果打印以上信息,证明实验成功!

​ **注意:**整个完整工程代码目录在裸机Git仓库 NoosProgramProject/(8_UART串口编程/005_printf_test文件夹下,可用于其它程序使用串口来打印或接收字符串。

第九章:重定位

第九章:重定位

9. 重定位

9.1 段的概念

​ 段是程序的组成元素。将整个程序分成一个一个段,并且给每个段起一个名字,然后在链接时就可以用这个名字来指示这些段,使得这些段排布在合适的位置。

​ 程序的段包括

​ 注意:

​ 下面将通过一个实例来直观地感受程序中的段,该实例的工程代码放在裸机Git仓库 NoosProgramProject/(9_重定位/001_segment) 文件夹下。

9.1.1 步骤1:在主函数文件中创建不同属性的全局变量

​ 程序文件:main.c

05 char g_charA = 'A';         	//存储在 .data段
06 const char g_charB = 'B';    //存储在 .rodata段
07 const char g_charC;			//存储在 .bss段
08 int g_intA = 0; 				//存储在 .bss段
09 int g_intB;					//存储在 .bss段

9.1.2 步骤2:创建链接脚本

​ 这里先用着链接脚本,具体如何使用会在《章节9-1.2 链接脚本分析》中详细说明

​ 链接脚本:imx6ull.lds

SECTIONS {
    . = 0x80100000;

    . = ALIGN(4);
    .text      :
    {
      *(.text)
    }

    . = ALIGN(4);
    .rodata : { *(.rodata) }

    . = ALIGN(4);
    .data : { *(.data) }

    . = ALIGN(4);
    __bss_start = .;
    .bss : { *(.bss) *(.COMMON) }
    __bss_end = .;
}

9.1.3 步骤3:在Makefile文件中指明使用链接脚本imx6ull.lds控制链接过程

# 使用链接脚本
$(LD) -T imx6ull.lds -g start.o uart.o main.o my_printf.o -o relocate.elf -lgcc -L/home/book/100ask_imx6ull-sdk/ToolChain/gcc-linaro-6.2.1-2016.11-x86_64_arm-linux-gnueabihf/lib/gcc/arm-linux-gnueabihf/6.2.1

9.1.4 步骤4:参考章节《4-1.4-1.4.4 编译程序》编译程序并查看反汇编文件relocate.dis

打开反汇编文件发现

​ 反汇编文件:relocate.dis

relocate.elf:     file format elf32-littlearm


Disassembly of section .text:	//代码段

80100000 <_start>:
80100000:	e59fd028 	ldr	sp, [pc, #40]	; 80100030 <clean+0x14>
80100004:	eb000001 	bl	80100010 <clean_bss>
80100008:	fb000070 	blx	801001d2 <main>

……(省略)

Disassembly of section .rodata:	//只读数据段

8010086c <g_charB>:
8010086c:	00000042 	andeq	r0, r0, r2, asr #32

……(省略)

Disassembly of section .data:	//数据段

8010098c <g_charA>:
8010098c:	00000041 	andeq	r0, r0, r1, asr #32

80100990 <hex_tab>:
80100990:	33323130 	teqcc	r2, #48, 2
80100994:	37363534 			; <UNDEFINED> instruction: 0x37363534
80100998:	62613938 	rsbvs	r3, r1, #56, 18	; 0xe0000
8010099c:	66656463 	strbtvs	r6, [r5], -r3, ror #8

Disassembly of section .bss:	//bss段,不保存在.bin文件中

801009a0 <__bss_start>:
801009a0:	00000000 	andeq	r0, r0, r0

801009a4 <IOMUXC_SW_MUX_CTL_PAD_UART1_RX_DATA>:
801009a4:	00000000 	andeq	r0, r0, r0

801009a8 <g_intA>:
801009a8:	00000000 	andeq	r0, r0, r0

801009ac <g_intB>:
801009ac:	00000000 	andeq	r0, r0, r0

801009b0 <g_charC>:
	...

……(省略)

Disassembly of section .comment:		//comment段,不保存在.bin文件中

……(省略)

9.2 链接脚本解析

​ 顾名思义,链接脚本控制程序的链接过程,它规定如何把输入文件内的段放入输出文件, 并控制输出文件内的各部分在程序地址空间内的布局。

​ 此节配套的源码在**裸机Git仓库 NoosProgramProject/(9_重定位/02_clean_bss)**目录内。

​ 为了在链接时使用链接脚本,需要在Makefile用**-T filename.lds**指定。否则在编译时将使用默认的链接脚本。

#使用链接脚本imx6ull.lds
$(LD) -T imx6ull.lds -g start.o uart.o main.o my_printf.o -o relocate.elf -lgcc -L/home/book/100ask_imx6ull-sdk/ToolChain/gcc-linaro-6.2.1-2016.11-x86_64_arm-linux-gnueabihf/lib/gcc/arm-linux-gnueabihf/6.2.1

​ 需要注意,对于结构较为简单的程序,也可以使用默认的链接脚本,并手动指定不同段在输出文件中的位置。

#将所有程序的.text段放在一起,起始地址设置为0x80100000
#将所有程序的.data段放在一起,起始地址设置为0x80102000
$(LD) -Ttext 0x80100000 -Tdata 0x80102000 -g start.o uart.o main.o my_printf.o -o relocate.elf -lgcc -L/home/book/100ask_imx6ull-sdk/ToolChain/gcc-linaro-6.2.1-2016.11-x86_64_arm-linux-gnueabihf/lib/gcc/arm-linux-gnueabihf/6.2.1

​ 默认的链接脚本无法进行一些段的复杂操作,所以下面的程序中我们一律使用链接脚本。

9.2.1 链接脚本语法

​ 本章节中所有的知识都来源于GNU官方文档:

​ http://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_mono/ld.html

​ 链接脚本的结构为

SECTIONS {
...
secname start BLOCK(align) (NOLOAD) : AT ( ldadr )
  { contents } >region :phdr =fill
...
}

​ 依照上述的结构我们来分析本章节中1.1.2中的链接脚本imx6ull.lds

9.2.2 解析链接脚本

​ 链接脚本:imx6ull.lds

01 SECTIONS {
02     . = 0x80100000;					//设定链接地址为0x80100000
03
04     . = ALIGN(4);					//将当前地址以4字节为标准对齐
05     .text      :						//创建段,其名称为 .text
06     {								//.text包含的内容为所有链接文件的数据段
07       *(.text)						// *:表示所有文件
08     }
09
10     . = ALIGN(4);					//将当前地址以4字节为标准对齐
11     .rodata : { *(.rodata) }			//.rodata存放在.text之后,包含所有链接文件的只读数据段
12	
13     . = ALIGN(4);
14     .data : { *(.data) }				//.data存放在.rodata之后,包含所有链接文件的只读数据段
15
16     . = ALIGN(4);
17     __bss_start = .;					//将当前地址的值存储为变量__bss_start
18     .bss : { *(.bss) *(.COMMON) }	//.bss存放在.data段之后, 包含所有文件的bss段和注释段
19     __bss_end = .;					//将当前地址的值存储为变量__bss_end
20 }

​ 根据上述链接脚本的配置,.bin文件中的数据结构如下图所示:

​ 上面我们写的链接脚本称为一体式链接脚本,与之相对的是分体式链接脚本,区别在于代码段(.text)和数据段(.data)的存放位置是否是分开的。

​ 例如现在的一体式链接脚本的代码段后面依次就是只读数据段、数据段、bss段,都是连续在一起的。 分体式链接脚本则是代码段、只读数据段,中间间隔很远之后才是数据段、bss段。

​ 分体式链接脚本实例:

SECTIONS {
    . = 0x80100000;					//设置链接地址为0x80100000,这也是.text段的起始地址

    . = ALIGN(4);
    .text      :
    {
      *(.text)
    }

    . = ALIGN(4);
    .rodata : { *(.rodata) }		//假设rodata段的结束地址为0x8010xxxx

    . = ALIGN(4);
.data 0x80800000 : { *(.data) }		//指定data段的起始地址为0x80200000,和rodata段之间较大间隔

……(省略)

​ 之后的代码更多的采用一体式链接脚本,原因如下:

​ 1. 分体式链接脚本适合单片机,因为单片机自带有flash,不需要将代码复制到内存占用空间。而我们的嵌入式系统内存非常大,没必要节省这点空间,并且有些嵌入式系统没有可以直接运行代码的Flash,就需要从存储设备如Nand Flash或者SD卡复制整个代码到内存;

​ 2. JTAG等调试器一般只支持一体式链接脚本;

9.2.3 清除bss段

​ 之前提到过bin文件中并不会保存bss段的值,因为这些值都是0,保存这些值没有意义并会使得bin文件臃肿。

​ 当程序运行涉及到bss段上的数据时,CPU会从bss段对应的内存地址去读取对应的值,为了确保从这段内存地址上读取到的bss段数值为0,在程序运行前需要将这一段内存地址上的数据清零,即清除bss段。

​ 这一节将通过汇编清除bss段数据,相关的工程代码放在目录 002_clean_bss

9.2.3.1 步骤1:修改汇编文件

​ 我们在汇编文件中实现清除bss段,具体思路就是将bss段对应的地址读取,并将地址上的数据依次清零。

​ 汇编文件:start.S

01
02 .text
03 .global  _start
04
05 _start:
06
07      /* 设置栈 */
08      ldr  sp,=0x80200000
09
10      /* 清除bss段 */
11      bl clean_bss
12
13      /* 跳转到主函数 */
14      bl main
15
16 halt:
17      b  halt
18
19 clean_bss:
20      ldr r1, =__bss_start	//将链接脚本变量__bss_start变量保存于r1
21      ldr r2, =__bss_end		//将链接脚本变量__bss_end变量保存于r2
22      mov r3, #0
23 clean:
24      strb r3, [r1]			//将当前地址下的数据清零
25      add r1, r1, #1			//将r1内存储的地址+1
26      cmp r1, r2				//相等:清零操作结束;否则继续执行clean函数清零bss段
27      bne clean
28
29      mov pc, lr

9.2.3.2 步骤2:在主函数汇中添加测试代码

​ 主函数中打印存放在bss段内数据的值。

​ 程序文件:main.c

37 int main (void)
38 {
39      Uart_Init();    //初始化uart串口
40
41      printf("g_intA = 0x%08x\n\r", g_intA);  //打印g_intA的值
42      printf("g_intB = 0x%08x\n\r", g_intB);  //打印g_intB的值
43
44      return 0;
45 }
46

9.2.3.3 步骤3:参考章节《4-1.4编译程序》编译程序

9.2.3.4 步骤4:参考章节《3-1.4映像文件烧写、运行》烧写、运行程序

​ 最终在终端中输出结果如下,保存在bss段中的变量g_intA, g_intB的值都为0,表明清除bss段成功。

g_intA = 0x00000000
g_intB = 0x00000000

9.3 重定位的引入

9.3.1 什么是重定位

​ 接触过S3C2440的朋友应该很熟悉,在程序运行之前我们需要手动将.bin文件上的全部代码从Nor Flash或SRAM拷贝到SDRAM上。对于imx6ull来说,这部分拷贝代码的操作由Boot Rom自动完成,板子上电后Boot Rom会将映像文件从启动设备(TF卡、eMMC)自动拷贝到DDR3内存上。上述拷贝代码的过程就是重定位。

​ 那么Boot Rom应该将映像文件拷贝到内存的哪个位置呢?这部分内容已经在章节《3-1.2 IMX6ULL启动流程 》中详细讨论过了。简而言之100ask_imx6ull的映像文件包含多个部分,其中.bin文件的起始地址由地址entry决定,需要在Makefile中手动配置。

./tools/mkimage -n ./tools/imximage.cfg.cfgtmp -T imximage -e **0x80100000** -d relocate.bin relocate.imx

​ 按照上述的配置,整个映像文件被自动重定位到DDR3内存上,其中.bin文件的起始地址为0x80100000。重定位结束后,CPU会从这个地址读取第一条指令开始执行程序。

9.3.2 汇编重定位data段

​ 下面我们将通过一个实例来说明为什么要重定位data段以及如何通过汇编重定位data段。

​ 在002_clean_bss代码的基础上,在主函数中添加测试代码,不断地打印data段中的数据g_charA。该程序放在**裸机Git仓库 NoosProgramProject/(9_重定位/003_without_relocation)**文件夹内。

​ 程序文件:main.c

37 int main (void)
38 {
39      Uart_Init();    //初始化uart串口
40
41      printf("\n\r");
42      /* 在串口上输出g_charA */
43      while (1)
44      {
45              PutChar(g_charA);
46              g_charA++;
47              delay(1000000);
48      }
49
50      return 0;
51 }

9.3.2.1 步骤1:参考章节《4-1.4编译程序》编译程序

9.3.2.2 步骤2:参考章节《3-1.4映像文件烧写、运行》烧写、运行程序

​ 最终在终端上成功打印字符g_charA的值。

ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
123456789:;<=>?@A

​ 在程序运行时,CPU需要不断地访问DDR3内存来获取g_charA的值,访问DDR3会花费大量的时间,那么如何提升访问的效率呢?

​ 答:在程序运行先前将data段的数据重定位到imx6ull的片内RAM上,因为CPU访问片内RAM的速度远快于访问DDR3的速度。

​ 下面我们将通过汇编重定位data段。该实例保存在**裸机Git仓库 NoosProgramProject/(9_重定位/004_manual_relocate_data)**文件夹内。

9.3.2.3 步骤1:参考芯片手册确定片内RAM的位置

​ 参考资料:芯片手册《Chapter 2: Memory Maps》

​ 参考芯片手册得到片内RAM的地址为:0x900000 ~ 0x91FFFF。所以我们将.data段重定位后的地址设置为0x900000。

9.3.2.4 步骤2:修改链接脚本

​ 创建一个变量用来存储.data段的起始加载地址。

. = ALIGN(4);
.rodata : { *(.rodata) }

. = ALIGN(4);

data_load_addr = .;			//将当前地址存储在变量中(大概的值为0x8010xxxx)

​ 将.data段的运行地址(runtime address)设定为0x900000。加载地址由变量data_load_addr确定。这样设置后,在.bin文件中.data段仍旧存储在.rodata段之后。但在程序运行时,CPU会从0x900000开始的空间内读取.data段的值。

.data 0x900000 : AT(data_load_addr)

​ 下面我们将重定位后.data段的起始地址存储在变量data_start,重定位后的.data段的结束地址存储在变量data_end,这两个变量将供汇编文件调用。

{
    data_start = . ;		//addr = 0x900000
    *(.data)
        data_end = . ;		//addr = 0x900000+SIZEOF(.data)
}

​ 修改后的链接脚本如下所示

​ 链接脚本imx6ull.lds

SECTIONS {
     . = 0x80100000;

     . = ALIGN(4);
     .text      :
     {
       *(.text)
     }

     . = ALIGN(4);
     .rodata : { *(.rodata) }

     . = ALIGN(4);

     data_load_addr = .;					
     .data 0x900000 : AT(data_load_addr) 
     {
       data_start = . ;
       *(.data)
       data_end = . ;
     }

     . = ALIGN(4);
     __bss_start = .;
     .bss : { *(.bss) *(.COMMON) }
     __bss_end = .;
 }

​ 通过上述操作,CPU虽然会去片内RAM中读取.data段数据,但实际上片内RAM并没有准备好.data段的数据,如下图所示。下面我们将通过汇编将DDR3内存上的.data段数据重定位到片内RAM上。

9.3.2.5 步骤3:修改汇编文件重定位.data段

​ 设置完栈后直接跳转到copy_data函数重定位data段

​ 汇编文件:start.S

      /* 设置栈 */
      ldr  sp,=0x80200000

      /* 重定位data段 */
      bl copy_data

      /* 清除bss段 */
      bl clean_bss

​ 实现copy_data函数

​ 汇编文件:start.S

 copy_data:
      /* 重定位data段 */
      ldr r1, =data_load_addr	/* data段的加载地址, 从链接脚本中得到, 0x8010xxxx */
      ldr r2, =data_start		/* data段重定位地址, 从链接脚本中得到, 0x900000 */
      ldr r3, =data_end			/* data段结束地址, 从链接脚本中得到,0x90xxxx */
 cpy:
      ldr r4, [r1]			/* 从r1读到r4 */
      str r4, [r2]			/* r4存放到r2 */
      add r1, r1, #4		/* r1+1 */
      add r2, r2, #4		/* r2+1 */
      cmp r2, r3			/* r2 r3比较 */
      bne cpy				/* 如果不等则继续拷贝 */

      mov pc, lr			/* 跳转回调用copy_data函数之前的地址 */

9.3.2.6 步骤3:参考章节《4-1.4编译程序》编译程序

9.3.2.7 步骤4:参考章节《4-1.4映像文件烧写、运行》烧写、运行程序

​ 将目录**裸机Git仓库 NoosProgramProject/中(9_重定位/003_without_relocation)和目录裸机Git仓库 NoosProgramProject/中(9_重定位/004_manual_relocate_data)**中的程序分别烧录、运行,发现重定位data段后终端上打印字符的速度明显变快。

9.4 C函数重定位data段和清除bss段

​ 到目前为止我们已经通过汇编实现了重定位data段和清除bss段。为了让汇编程序更加简洁,这一节中我们将通过C语言实现重定位data段和清除bss段。

9.4.1 通过汇编传递链接脚本变量

​ 这一小节中我们将通过汇编文件获得链接脚本中的变量,再将这些变量传递给C函数。工程文件放在裸机Git仓库 NoosProgramProject/(9_重定位/005_relocate_data_with_c)目录内

9.4.1.1 步骤1:修改汇编文件

​ 打开start.S将之前的汇编函数copy_data, clean_bss删除,改为直接调用C函数。在调用对应的C函数之前,需要通过寄存器r0~r4将C函数的参数准备好。

​ 汇编文件:start.S

 .text
 .global  _start

 _start:

      /* 设置栈 */
      ldr  sp,=0x80200000

      /* 重定位data段 */
      ldr r0, =data_load_addr	/* data段的加载地址 (0x8010....) */
      ldr r1, =data_start		/* data段重定位地址, 0x900000 */
      ldr r2, =data_end		/* data段结束地址(重定位后地址 0x90....) */
      sub r2, r2, r1		/* r2的值为data段的长度 */

      bl copy_data		/* 跳转到函数copy_data并将r1,r2,r3作为函数参数传入 */

      /* 清除bss段 */
      ldr r0, =__bss_start
      ldr r1, =__bss_end

      bl clean_bss		/* 跳转到函数clean_bss并将r0, r1作为函数参数传入*/

      /* 跳转到主函数 */
      bl main

 halt:
      b  halt

9.4.1.2 步骤2:创建程序文件init.c实现copy_data, clean_bss函数

​ 程序文件:init.c

 /* 从汇编得到参数src, dest, len的值 */
 void copy_data (volatile unsigned int *src, volatile unsigned int *dest, unsigned int len)
 {
      unsigned int i = 0;

      while (i < len)
      {
              *dest++ = *src++;
              i += 4;
      }
 }
 /* 从汇编得到参数start, end的值 */
 void clean_bss (volatile unsigned int *start, volatile unsigned int *end)
 {
      while (start <= end)
      {
              *start++ = 0;
      }
 }

​ 需要注意的是,上述两个函数的参数都是从汇编文件传入

9.4.1.3 步骤3:修改Makefile

​ 修改Makefile文件,编译init.c并链接init.o

​ 文件:Makefile

08 relocate.img : start.S  uart.c main.c my_printf.c init.c
09      $(CC) -nostdlib -g -c -o start.o start.S
10      $(CC) -nostdlib -g -c -o uart.o uart.c
11      $(CC) -nostdlib -g -c -o main.o main.c
12      $(CC) -nostdlib -g -c -o my_printf.o my_printf.c
13      $(CC) -nostdlib -g -c -o init.o init.c			
14	
15      $(LD) -T imx6ull.lds -g start.o uart.o main.o my_printf.o init.o -o relocate.elf -lgcc -L/home/book/100ask_imx6ull-sdk/ToolChain/gcc-linaro-6.2.1-2016.11-x86_64_arm-linux-gnueabihf/lib/gcc/arm-linux-gnueabihf/6.2.1

9.4.1.4 步骤3:参考章节《4-1.4编译程序》编译程序

9.4.1.5 步骤4:参考章节《4-1.4映像文件烧写、运行》烧写、运行程序

​ 程序成功运行,在终端成功输出字符串

ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
123456789:;<=>?@A

9.4.2 C函数直接调取链接脚本变量

​ 上一节中C函数需要通过汇编文件传入参数,在这一节我们将进一步改进C函数,使得C函数跳过汇编文件,直接从链接脚本中调用所需变量。

​ 工程文件放在**裸机Git仓库 NoosProgramProject/(9_重定位/006_relocate_data_with_c_modified)**目录内。

9.4.2.1 步骤1:修改汇编文件,改为直接调用C函数

​ 汇编文件:start.S

 .text
 .global  _start

 _start:

      /* 设置栈 */
      ldr  sp,=0x80200000

      /* 重定位data段 */
      bl copy_data

      /* 清除bss段 */
      bl clean_bss

      /* 跳转到主函数 */
      bl main

 halt:
      b  halt

9.4.2.2 步骤2:修改init.c 通过函数来获取参数

​ 程序文件:init.c

12 void copy_data (void)
13 {
14      /* 从链接脚本中直接获得参数 data_load_addr, data_start, data_end */
15      extern int data_load_addr, data_start, data_end;
16
17      volatile unsigned int *dest = (volatile unsigned int *)&data_start;
18      volatile unsigned int *end = (volatile unsigned int *)&data_end;
19      volatile unsigned int *src = (volatile unsigned int *)&data_load_addr;
20
21      /* 重定位数据 */
22      while (dest < end)
23      {
24              *dest++ = *src++;
25      }
26 }
38 void clean_bss(void)
39 {
40      /* 从lds文件中获得 __bss_start, __bss_end */
41      extern int __bss_end, __bss_start;
42
43      volatile unsigned int *start = (volatile unsigned int *)&__bss_start;
44      volatile unsigned int *end = (volatile unsigned int *)&__bss_end;
45
46      while (start <= end)
47      {
48              *start++ = 0;
49      }
50 }

9.4.2.3 步骤3:参考章节《4-1.4编译程序》编译程序

9.4.2.4 步骤4:参考章节《4-1.4映像文件烧写、运行》烧写、运行程序

​ 程序成功运行,在终端成功输出字符串

ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
123456789:;<=>?@A

9.4.3 总结:如何在C函数中使用链接脚本变量

​ 结合上面的例子,我们来总结一下如何在C函数中使用链接脚本中定义的变量

​ 1. 在C函数中声明该变量为外部变量,用extern修饰,例如: extern int _start;

​ 2. 使用取址符号(&)得到该变量的值,例如:int * p = & _ start; //p的值为lds文件中_start的值

​ 为什么在汇编文件中可以直接使用链接脚本中的变量,而在C函数中需要加上取址符号呢?

​ 原因:C函数中定义一个全局变量int g_i = 10;,程序中必然有4字节的空间留出来给这个变量g_i,然而链接脚本中的变量并像全局变量一样都保存在.bin文件中。如果我们在C程序中只用到链接脚本变量a1, a2, a3,那么程序只保存这3个变量。在编译程序时,会将这些要用到的变量保存在symbol_table符号表中,如下图所示:

​ 从上图中我们注意到:

​ 对于全局变量来说,地址里面存储的是变量存放的地址;可以通过&g_i得到变量存储的地址addr。

​ 对于链接脚本变量来说,地址里面存储的是变量的值;为了保持代码的一致性,通过&a1得到变量的值value。

9.5 重定位全部代码

9.5.1 C函数实现重定位全部代码

​ 虽然100ask_imx6ull的映像文件会自动被重定位到DDR3内存上,但对于一些采用其他芯片的板子,这一部分的操作需要我们手动去完成。例如S3C2440 上电后,因为硬件的限制,.bin文件的前4k程序需要将整个程序重定位到大小能够执行整个程序的SDRAM上。

​ 为了涉及到代码重定位所需知识,在这一节中我们将重定位整个.bin文件到片内RAM上。

​ 工程文件放在裸机Git仓库 NoosProgramProject/(9_重定位/007_relocate_all_with_c) 目录下。

​ 需要注意,虽然将全部代码重定位到片内RAM上可以加快命令的执行、数据的读取写入,但是这样的做法并不适合体积较大的程序,因为片内RAM只有128KB空间。

9.5.1.1 步骤1:修改链接脚本

​ 1. 修改链接地址为0x900000

​ 2. 删除与.data段相关的链接脚本变量。

​ 3. 添加变量_load_addr并将它的值设置为Makefile中entry地址的值,供C函数调用。

​ 链接脚本:imx6ull.lds

01 SECTIONS {
02     _load_addr = 0x80100000;
03
04     . = 0x900000;
05
06     . = ALIGN(4);
07     .text      :
08     {
09       *(.text)
10     }
11
12     . = ALIGN(4);
13     .rodata : { *(.rodata) }
14
15     . = ALIGN(4);
16     .data : { *(.data) }
17
18     . = ALIGN(4);
19     __bss_start = .;
20     .bss : { *(.bss) *(.COMMON) }
21     __bss_end = .;
22 }

9.5.1.2 步骤2:修改init.c

​ 重定位全部代码和重定位.data段原理相同。

​ 在这里只需要修改copy_data函数中调用的外部变量。

​ 程序文件:init.c

12 void copy_data (void)
13 {
14      /* 从链接脚本中获得参数 _start, __bss_start, */
15      extern int _load_addr, _start, __bss_start;
16
17      volatile unsigned int *dest = (volatile unsigned int *)&_start;        //_start = 0x900000
18      volatile unsigned int *end = (volatile unsigned int *)&__bss_start;    //__bss_start = 0x9xxxxx
19      volatile unsigned int *src = (volatile unsigned int *)&_load_addr;     //_load_addr = 0x80100000
20
21      /* 重定位数据 */
22      while (dest < end)
23      {
24              *dest++ = *src++;
25      }
26 }

9.5.1.3 步骤3:修改汇编文件

​ 重定位之后,需要使用绝对跳转命令ldr pc, = xxx,跳转到重定位后的地址。

​ 汇编文件:start.S

16      /* 跳转到主函数 */
17      // bl main			/* 相对跳转,程序仍在DDR3内存中执行 */
18      ldr pc, =main		/* 绝对跳转,程序在片内RAM中执行 */

9.5.1.4 步骤3:参考章节《4-1.4编译程序》编译程序

9.5.1.5 步骤4:参考章节《4-1.4映像文件烧写、运行》烧写、运行程序

​ 程序成功运行,在终端成功输出字符串

ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
123456789:;<=>?@A

9.5.2 位置无关码

​ 查看上述程序的反汇编发现,在重定位函数copy_data执行之前,已经涉及到了片内RAM上的地址,但此时片内RAM上并没有重定位的程序,那为什么程序还能正常运行呢?

​ 反汇编文件:relocate.dis

07 00900000 <_start>:
08   900000:    e59fd00c	ldr		sp, [pc, #12]   ; 900014 <halt+0x4>
09   900004:    fa00016f	blx		9005c8 <copy_data>
10   900008:    fb000180	blx		900612 <clean_bss>
11   90000c:    e59ff004	ldr		pc, [pc, #4]    ; 900018 <halt+0x8>

​ 答:这里的地址只是一个相对地址,是为了方便理解程序才这样标识。实际上CPU执行的第一条命令还是从0x80100000读取的,这是由entry决定的。

​ 第9行和第10行中的blx命令跳转到的地址是一个相对地址(pc + offset),而不是绝对地址(0x9005c8)。在反汇编文件中这样写,只是起到方便查看的作用,并不是真的跳转。

​ 下面我们来分析一下实际板子上电后,程序是如何执行的

​ 1. 程序被自动重定位到0x80100000,并从这个地址开始执行第一条指令,此时pc = 0x80100000 + 8 = 0x80100008。

​ 2. 计算可得offset = 0x9005c8 + 8 – 0x900004 = 0x5cc,所以反汇编文件中的第二条指令(存放在0x80100004)实际上是跳转到地址0x801005c8执行copy_data函数。

​ 3. 在执行完copy_data和clean_bss函数后,执行绝对跳转命令ldr pc, =main,使得pc = 0x90000c + 8 + 4 = 0x900018,所以之后的程序会跳到片内RAM中执行。

​ 那么我们应该如何写位置无关码呢?

​ 答:使用相对跳转命令 b或bl,并注意

​ a) 不可访问全局类变量(全局变量或static修饰的局部变量)

​ b) 不可访问有初始值的数组(初始值放在rodata里,需要绝对地址来访问)

第十章:异常与中断

第十章:异常与中断

10. 异常与中断

参考资料:

本章处理器架构的内容主要来自于ARM® Cortex™-A Series Programmer’s Guide version4.0。

10.1 ARM处理器模式和寄存器

​ ARM体系结构是一种基于模式的体系结构。在引入安全扩展之前,它具有7种处理器模式,如上表所示。有六个特权模式和一个非特权用户模式。特权是执行用户(非特权)模式无法完成的某些任务的能力。在用户模式下,对影响整个系统配置的操作存在一些限制,例如,MMU的配置和缓存操作。模式与异常事件相关。

​ TrustZone安全性扩展的引入了两个独立于特权模式和处理器模式的安全状态,并加入监控器模式(monitor mode)进行安全状态和非安全状态的切换。如下图所示:

​ 对于当前实现TrustZone扩展的处理器,系统安全是通过划分设备的所有硬件和软件资源属于安全世界或者非安全世界来实现的。处理器处于非安全状态时,它无法访问为安全状态分配的内存。

​ 在这种情况下,安全监控器充当在这两个世界之间切换的网关。如果实施了安全扩展,则在监视器模式下执行的软件将控制安全和不安全处理器状态之间的转换。

​ ARMv7-A体系结构虚拟化扩展,在现有的特权模式外,还添加了hypervisor mode(Hyp)。虚拟化使多个操作系统可以共存并在同一系统上运行。因此,ARM虚拟化扩展使在同一平台上运行多个操作系统成为可能。下图展示了hypervisor mode。

​ 如果实施了虚拟化扩展,则存在与以前的体系结构不同的特权模型。在非安全状态下,可以有三个特权级别,分别为PL0、PL1和PL2。

​ 下图显示了不同处理器状态下处理器可以处于的模式以及特权级别:

​ 处理器模式和安全状态以及特权等级的关系如下表所示:

​ 这些特权级别与TrustZone安全和普通(非安全)设置是分开的。

​ 特权级别定义了在当前安全状态下访问资源的能力,并不暗含在其他安全状态下访问资源的能力。

​ 通用操作系统(例如Linux)及其应用程序应在非安全状态下运行。安全状态一般由供应商特定的固件或对安全性敏感的软件来运行。一般在安全状态下运行的软件比在非安全状态下运行的软件具有更高的特权。

​ 当前的处理器模式和执行状态包含在当前程序状态寄存器(CPSR)中。可以通过异常或者在特权软件下显式地修改来进入到不同处理器模式。

10.1.1 寄存器

​ ARM体系结构提供了十六个32位通用寄存器(R0-R15)供软件使用0。其中的15个(R0-R14)可用于通用数据存储,而R15是程序计数器,其值随处理器执行指令而改变。显式地写入R15可以更改程序流程。软件还可以访问CPSR和SPSR中保存的先前运行模式的CPSR的副本。

​ 同一个寄存器可能在不同模式下对应物理上不同的位置。只有特定模式下才能访问到这些位置的寄存器。

​ 有些寄存器,不同的工作模式下有自己的副本,当切换到另一个工作模式时,那个工作模式的寄存器副本将被使用,这些寄存器被称为备份寄存器。备份寄存器在物理上使用不同的存储,通常仅在特定模式下才可以访问它们。下图中带阴影标记的寄存器都是备份寄存器。

​ 在所有模式下,“低位寄存器”和R15共享相同的物理存储位置。图3-5显示了某些模式下的某些“高位寄存器”被备份。例如,FIQ模式下使用R8-R12备份寄存器,也就是说,FIQ模式下对其的访问将转到另一个物理存储位置。对于除用户和系统模式以外的所有模式,R13和SPSR都是备份寄存器。

​ 对于备份寄存器,软件通常不会指定要访问哪个模式下的寄存器,这是由当前运行的模式隐含的。例如,访问R13的程序在用户模式下将访问R13_usr,在SVC模式下将访问R13_svc。

​ R13(在所有模式下)是堆栈指针,但是当堆栈操作不需要时,它可以用作通用寄存器。

​ R14(链接寄存器)保存BL分支指令的下一条指令的地址。当它不支持子程序的返回时,它也可以用作通用寄存器。R14_svc,R14_irq,R14_fiq,R14_abt和R14_und同样用于在发生中断和异常时,或者执行转移和链接指令时,备份R15的返回值。

​ R15是程序计数器并保存当前程序地址(实际上,在ARM状态下,它始终指向当前指令之前的八个字节,而在Thumb状态下,它始终指向当前指令之前的四个字节,这是原始ARM1的三级流水线的遗留特性)。在ARM状态下读取R15时,位[1:0]为零,位[31:2]包含PC值。在Thumb状态下,位[0]始终读为零。

​ R0-R14的复位值是不定的。在使用堆栈之前,必须通过引导代码初始化SP(堆栈指针)(针对每种模式)。ARM体系结构过程调用标准(AAPCS)或ARM嵌入式ABI(AEABI)指定了软件应如何使用通用寄存器,以便在不同的工具链或编程语言之间进行互操作。

Hypervisor模式

​ 支持虚拟化扩展的实现在管理程序(Hyp)模式下具有可用的其他寄存器。 Hypervisor模式以PL2特权级别运行。它可以访问自己的R13(SP_hyp)和SPSR副本。它使用用户模式下的链接寄存器存储函数的返回地址,并具有专用寄存器ELR_hyp来存储异常返回地址。 “ Hyp”模式仅在“正常”世界中可用,并提供虚拟化功能。

10.1.2 状态寄存器

​ 程序状态寄存器(CPSR,current programmer status register)包含处理器的状态和一些控制标记位。

​ 条件标记,bits[31:28]

​ 根据指令的执行结果设置,这些标记位是

​ N,bit[31]负数标记位

​ Z,bit[30]零标记位

​ C,bit[29]进位标记位

​ V,bit[28]溢出标记位

​ 这些条件标记位可以在任何模式下读写

​ GE[3:0],bit[19:16]一些SIMD指令使用

​ IT[7:2],bit[15:10]Thumb2指令集的If-then条件指令使用

​ J, bit[24] 处理器是否处于Jazelle状态,和T, bit[5]一起决定执行的指令集

​ E, bit[9] 大小端状态位,0表示小端,1表示大端。

​ Mask bits, bits[8:6]

​ A, bit[8] 异步中止禁止位Asynchronous abort mask bit.

​ I, bit[7] IRQ 禁止位

​ F, bit[6] FIQ 禁止位

​ Q,bit[27]为1的话,表明执行一些指令时出现饱和或者溢出,一般与DSP有关。

​ T, bit[5] Thumb指令位,和J,bit[24]位决定了处理器的指令集,ARM,Thumb,Jazzelle或者ThumbEE

​ M[4:0], bits[4:0]工作模式位,决定了处理器当前处于的工作模式。

​ 处理器可以使用直接写入CPSR模式位来实现模式之间切换。更常见的是,处理器会由于异常事件而自动切换模式。在用户模式下,无法改变处理器模式的PSR位[4:0]来切换模式和A,I和F位来使能或者禁止异步中止、IRQ和FIQ。

10.1.3 协处理器CP15

​ CP15是系统控制协处理器,可控制处理器核的许多功能。它可以包含16个32位主寄存器。对CP15的访问受权限控制,并且在用户模式下并非所有寄存器都可用。CP15寄存器访问指令指定所需的主寄存器,指令中的其他字段用于更精确地定义访问并增加CP15中的物理32位寄存器的数量。CP15中的16个主要寄存器的名称为c0至c15,但通常使用名称来引用。例如,CP15系统控制寄存器称为CP15.SCTLR。

​ 通过从一个通用寄存器(Rt)读取或写入位于CP15内的一组寄存器(CRn)中,可以控制系统架构的某些功能。该指令的Op1,Op2和CRm字段也可以用于选择寄存器或操作。 格式如下所示:

​ 从CP15寄存器读值到ARM寄存器

​ MRC p15, Op1, Rt, CRn, CRm, Op2 ; read a CP15 register into an ARM register

​ 从ARM寄存器写值到CP15寄存器

​ MCR p15, Op1, Rt, CRn, CRm, Op2 ; write a CP15 register from an ARM register

10.1.3.1 System control register (SCTLR)

​ SCTLR是通过CP15访问的寄存器,它控制存储器,系统功能,并提供反映处理器核中实现的功能的状态信息。

​ 系统控制寄存器只能从PL1或更高特权等级访问。

​ 各个位的描述如下:

​ •TE –Thumb异常使能。这可以控制异常进入ARM状态,还是Thumb状态。

​ •NMFI –是否支持不可屏蔽的FIQ(NMFI)支持。

​ •EE =异常字节序。这定义了在异常时的字节序,的CPSR.E位的值。

​ •U –使用对齐模型。

​ •FI – FIQ配置启用。

​ •V –该位选择异常向量表的基地址。

​ •I –指令缓存使能位。

​ •Z –分支预测使能位。

​ •C –缓存使能位。

​ •A –对齐检查使能位。

​ •M –启用MMU。

​ 引导代码序列的一部分通常将是设置CP15:SCTLR系统控制寄存器中的Z位,以启用分支预测功能。操作的代码如下:

MRC p15, 0, r0, c1, c0, 0 ; Read System Control Register configuration data

ORR r0, r0, #(1 << 2) ; Set C bit

ORR r0, r0, #(1 << 12) ; Set I bit

ORR r0, r0, #(1 << 11) ; Set Z bit

MCR p15, 0, r0, c1, c0, 0 ; Write System Control Register configuration data

10.2 异常处理

​ 异常(exception)是指处理器核中止正常的执行流程,并根据异常类型转去执行特定的软件例程(称为异常处理程序)。异常是通常需要采取补救措施或由特权软件更新系统状态以确保系统平稳运行的条件或系统事件。这称为处理异常。处理完异常后,特权软件将为处理器做好准备,以恢复发生异常之前的所有操作。其他体系结构可能会将ARM所谓的异常称为陷阱(traps)或中断(interrupts),但是,在ARM体系结构中,这些术语保留用于特定类型的异常。

​ 所有微处理器都必须响应外部异步事件,例如按下按钮或时钟达到某个值。通常,有专门的硬件可以激活处理器的输入线。这导致处理器核暂时停止当前的程序并执行特殊的特权处理程序例程。处理器核可以响应此类事件的速度可能是系统设计中的关键问题,称为中断等待时间(interrupt latency)。确实,在许多嵌入式系统中,并没有这样的主程序,系统的所有功能都由从中断代码来驱动,为这些中断分配优先级是设计的关键领域。系统不是通过不断地测试不同的标志位以查看是否有要做的事情,而是通过生成中断来通知处理器核,有事情必须要处理。复杂的系统有许多中断源,它们具有不同的优先级,并且支持中断嵌套,其中较高优先级的中断可以中断较低优先级的中断。

​ 在正常程序执行中,程序计数器在地址空间中递增,程序中的分支指令会修改执行流程,例如,函数调用,循环和条件代码。当发生异常时,此预定的执行顺序将中断,并暂时切换到异常处理程序以处理该异常。

​ 除了响应外部中断外,还有许多其他因素可能导致处理器核发生异常,包括外部(例如,复位),来自内存系统的异常终止以及内部(例如MMU生成的异常终止或通过SVC指令进行的OS调用)异常。处理异常会导致CPU核在模式之间切换并将某些寄存器复制到其他寄存器中。

10.2.1 异常的类型

​ ARMv7-A和ARMv7-R体系结构支持多种处理器模式,称为FIQ,IRQ,Supervisor,中止,未定义和系统的六种特权模式以及非特权的用户模式。如果实施了虚拟化扩展和安全扩展,则可以将Hyp模式和Monitor模式添加到列表中。当前模式可以在特权模式下改变,或者在发生异常时自动改变。

​ 非特权用户模式不能直接影响处理器核的异常行为,但是可以使用SVC异常以请求特权服务。这是用户应用程序请求操作系统来完成任务的方式。

​ 发生异常时,内核将保存当前状态和返回地址,进入特定模式,并可能禁用硬件中断。特定的异常程序处理从一个称为该异常的异常向量的固定内存地址开始执行。特权软件可以将异常向量表的起始位置编程到系统寄存器中,并且在获取相应的异常时会自动执行它们。

​ 存在以下异常类型:

​ (1)中断

​ ARMv7-A内核提供两种中断,称为IRQ和FIQ。

​ FIQ的优先级高于IRQ。由于FIQ在向量表中的位置以及FIQ模式下可用的更多的备份寄存器,因此FIQ还具有一些潜在的速度优势。这样可以节省将寄存器保存到堆栈时耗费的时钟周期。这两种异常通常都与处理器核上的输入引脚相关联-外部硬件会触发一条中断请求线,并在当前指令完成执行时引发相应的异常处理(假定不禁用该中断)。

​ FIQ和IRQ都是发给处理器核的物理信号,并且在触发时处理器核的FIQ和IRQ处于打开状态,它将处理相应的异常。几乎在所有系统上,通过使用中断控制器连接各种中断源。中断控制器对中断进行仲裁并确定优先级,然后依次提供串行化的单个信号,然后将其连接到内处理器核核的FIQ或IRQ引脚。

​ 由于IRQ和FIQ中断的发生与在任何给定时间内核所执行的软件都不直接相关,因此将它们分类为异步异常。

​ (2)中止

​ 中止可以在指令预取失败(预取中止)或数据访问失败(数据中止)时生成。它们可以来自外部存储器系统,在存储器访问时给出错误响应(可能表明指定的地址不对应于系统中的实际存储器)。另外,中止可以由内核的内存管理单元(MMU)生成。操作系统可以使用MMU中止来为应用程序动态分配内存。

​ 预取一条指令时,可以在指令流水线中中将其标记为已中止。仅当内核尝试执行它时,才导致预取中止异常。异常发生在指令执行之前。如果标记为中止的指令到达指令流水线的执行阶段之前刷新了指令流水线,则不会发生中止异常。数据中止异常发生在加载或存储指令执行时,并且是在尝试读取或写入数据之后发生的。

​ 如果中止是由于指令流的执行或尝试执行而产生的,则中止被描述为同步的,并且返回地址将提供导致该中止的指令的详细信息。

​ 异步的中止不是由执行指令生成,异步中止的返回地址可能不提供导致中止的原因的信息。

​ ARMv7体系结构分为精确的和不精确的异步中止。MMU产生的中止总是同步的。ARMv7体系结构不需要外部中止的类型是同步的。例如,在一个特定的实现上,页表翻译时报告的外部异常中止被认为是精确的,但这并不是所有处理器核都需要的。对于精确的异步中止,中止处理程序可以确定是哪条指令导致了中止,并且在该指令之后没有执行其他指令。这与不精确的异步异常中止相反,异步异常中止是外部存储器系统报告有关无法识别的访问的错误时的结果。在这种情况下,中止处理程序无法确定是哪条指令导致了问题,或者在产生中止的指令之后是否还会执行其他指令。

​ 例如,如果缓冲写入从外部存储系统接收到错误响应,则执行存储指令后很可能执行了其他指令。这意味着中止处理程序无法修复此问题并返回到应用程序。它所能做的就是杀死导致问题的应用程序。因此,设备探测需要特殊的处理,因为从外部报告的对不存在区域的读取中止将产生不精确的同步中止,即使将此类存储器标记为“strong odered”或“设备”。

​ 异步中止的检测由CPSR A位控制。如果将A位置1,CPU核将识别出外部存储系统的异步异常中止,但不会产生中止异常。取而代之的是,内核将中止挂起状态挂起,直到清除A位时才采取异常处理为止。内核代码将使用屏障指令来确保针对正确的应用程序识别未处理的异步中止。如果由于不精确的中止而不得不终止线程,则该线程必须是正确的线程。

​ (3)复位

​ 所有处理器核都有复位输入,并且在复位后将立即执行复位异常。它是最高优先级的异常,无法屏蔽。上电后,此异常用于在处理器核上执行代码以对其进行初始化。

​ (4)生成异常的指令

​ 某些指令的执行会产生异常。通常执行以下指令,以便从更高特权级别的软件中请求服务:

​ •Supervisor Call(SVC)指令使用户模式程序可以请求操作系统服务。

​ •如果实施了虚拟化扩展,则可以使用Hypervisor调用(HVC)指令,使虚拟机可以请求Hypervisor服务。

​ •如果实施了安全扩展,则可以使用(SMC)指令,使普通环境可以请求安全环境服务。

​ 任何试图执行处理器核无法识别的指令都会产生未定义的异常。

​ 发生异常时,CPU核将执行与该异常对应的处理程序。异常处理程序在内存中的存储位置称为异常向量。在ARM体系结构中,异常向量存储在称为异常向量表的表中。因此,用于特定异常的向量可以位于异常向量表起始位置的固定偏移处。该向量表的基地址由特权软件在系统寄存器中指定,以便处理器核可以在发生异常时找到相应的处理程序。

​ 可以为安全PL1,非安全PL1,安全监视器和非安全PL2特权级别分别配置单独的异常向量表。处理器核根据当前的特权等级和安全状态来查找对应的异常向量表。

​ 可以用ARM或Thumb代码编写异常处理程序。CP15 SCTLR.TE位用于指定异常处理程序将使用ARM还是Thumb指令集。处理异常时,必须保留处理器核先前的模式,状态和寄存器,以便可以在处理异常后恢复原来程序的执行。

10.2.2 异常优先级

​ 当异常同时发生时,将依次处理每个异常,然后返回原来执行的应用程序。所有异常不可能同时发生。例如,未定义指令(Undef)和supervisor call(SVC)异常是互斥的,因为它们都是由执行指令触发的。

​ 注意:ARM体系结构未定义何时采用异步异常。因此,异步异常相对于其他异常(同步和异步)的优先级由实现决定。

​ 所有异常均禁用IRQ,只有FIQ和复位禁用FIQ。这是由处理器核自动设置CPSR I(IRQ)和F(FIQ)位来完成的。

​ 可能同时产生多个异常,但是某些组合是互斥的。预取中止将一条指令标记为无效,因此不能与未定义的指令或SVC同时发生(当然,SVC指令也不能是未定义的指令)。这些指令不会导致任何内存访问,因此不会导致数据中止。该体系结构未定义何时必须采取异步异常,FIQ,IRQ或异步异常中止,但是采用IRQ或数据异常中止不会禁用FIQ异常这一事实意味着FIQ执行将优先于IRQ或异常中止异常。

​ 异常处理是通过使用称为向量表的内存区域来控制的。默认情况下,该地址位于字映射地址从0x00到0x1C的内存映射的底部。向量表可以从0x0移到0xFFFF0000。

​ 对于带有Security Extensions的内核,情况更加复杂。这里有三个向量表,非安全向量表,安全向量表和安全监视器向量表。对于带有Virtualization Extension的核心,有四个,添加了Hypervisor向量表。对于具有MMU的内核,所有这些向量地址都是虚拟的。下表总结了各种状态下异常的行为。

​ 进入异常时,CPSR的I和F设置如下表所示:

10.2.3 向量表

​ 向量表示触发异常时ARM核跳转到的指令表。这些指令位于内存中的特定位置。默认向量基址为0x00000000,但大多数ARM核允许将向量基址移至0xFFFF0000(或HIVECS)。所有Cortex-A系列处理器都允许这样做,这是Linux内核选择的默认地址。实现安全扩展的内核还可以使用CP15向量基地址寄存器为安全状态和非安全状态分别设置向量基地址。

​ 每种异常类型都有一个字的地址。因此,每个异常只能在向量表中放置一条指令(尽管从理论上讲,可以使用两条16位Thumb指令)。因此,向量表条目几乎总是包含以下两种形式的分支之一。

​ B

​ 这将执行PC相对跳转。它可以跳转到当前指令的前后32MB。

​ LDR PC,[PC,#offset]

​ 这将从地址相对于异常指令offset偏移量的值加载到PC。这样就可以将异常处理程序放置在32位内存地址空间内的任意地址处(但相对于B指令,要多花一些额外的指令周期)。

​ 当处理器核以Hyp mode运行时,它使用Hyp mode向量入口地址,这些入口地址是从Hyp mode的专用向量表中获取的。通过Hyp trap entry的特定异常过程进入系统管理程序模式,该过程利用向量表中先前保留的0x14地址。专用寄存器(Hyp Syndrome Register)向hypervisor提供有关进入管理程序的异常或其他原因的信息(例如,陷阱CP15操作)。

​ 异常向量和异常基地址

​ 发生异常时,处理器根据强制到跟异常的类型相应的地址去执行。这个地址被称为异常向量。一组异常向量包括从一个异常向量基地址开始的连续的8个字对齐的地址空间。8个异常向量组成异常向量表。

​ 可能的异常向量基地址或者异常向量表的量由扩展类型和架构类型决定,这里只讨论包含安全扩展的类型

​ 包含安全扩展类型的实现有如下向量表

​ 一个转去secure monitor模式的表,MVBAR保存了异常向量基地址

​ 一个转去secure PL1模式(不包括monitor模式)的表,secure状态下SCTLR.V位决定了向量表的基地址,如果V==0的话,secure VBAR保存了异常向量基地址;如果v==1的话,异常向量基地址为0xFFFF0000。

​ 一个转去非安全PL1模式的表,这是非安全状态的向量表,Non-secure状态下SCTLR.V位决定了向量表的基地址,如果V==0的话,Non-secure VBAR保存了异常向量基地址;如果v==1的话,异常向量基地址为0xFFFF0000。

10.2.4 FIQ and IRQ

​ FIQ保留用于需要保证快速响应时间的单个高优先级中断源,而IRQ用于系统中的所有其他中断。

​ 由于FIQ是向量表中的最后一项,因此FIQ处理程序可以直接放置在向量入口位置,并从该地址开始顺序运行。这避免了分支指令和任何相关的延迟,从而加快了FIQ响应时间。相对于其他模式,FIQ模式下可用的备份寄存器数量比较多,从而避免要将寄存器的值保存到栈上,提高了执行速度。

​ Linux通常不使用FIQ。由于内核与体系结构无关,因此它不具有多种形式的中断的概念。某些运行Linux的系统仍可以使用FIQ,但是由于Linux内核从不禁用FIQ,因此它们比系统中的其他任何事物都具有优先权,因此需要格外小心。

10.2.5 返回指令

​ 处理异常后,链接寄存器(LR)用于为存储返回地址。下表提供了包含此调整的异常的返回指令。

10.2.6 异常处理

​ 发生异常时,ARM内核会自动执行以下操作:

​ 1.将CPSR复制到SPSR_ ,这是特定(非用户)操作模式的备份寄存器。

​ 2.将返回地址存储在新模式的链接寄存器(LR)中。

​ 3.将CPSR模式位修改为与异常类型相关联的模式。

​ •其他CPSR模式位设置由CP15系统控制寄存器的值确定。

​ •T位设置为CP15 TE位给定的值。

​ •J位被清除,E位(字节序)被设置为EE(异常字节序)位的值。

​ 这使异常始终以ARM或Thumb状态运行,并且以小端或大端运行,无论CPU核在异常之前处于何种状态。

​ 4.将PC设置为指向异常向量表中的相关指令。

​ 在新模式下,CPU核将访问与该模式关联的寄存器。

​ 异常处理程序软件几乎总是需要在进入异常处理程序时立即将寄存器保存到堆栈中。FIQ模式具有更多的备份寄存器,因此可以编写不使用堆栈的简单处理程序。

​ 提供了一种特殊的汇编语言指令来帮助保存必要的寄存器,称为SRS(store return state存储返回状态)。该指令将LR和SPSR压入任何模式的堆栈;所使用的堆栈由指令操作数指定。

10.2.6.1 从异常处理程序返回

​ 要从异常处理程序返回,必须进行两个单独的操作:

​ 1.从保存的SPSR中恢复CPSR。

​ 2.将返回地址偏移量设置到PC。

​ 在ARM体系结构中,这可以通过使用RFE指令或以PC作为目标寄存器的任何标志设置数据处理操作(带有S后缀)来实现,例如SUBS PC,LR,#offset(注意S)。异常返回(RFE)指令将链接寄存器和SPSR从当前模式堆栈弹出。

​ 有多种方法可以实现此目的。

​ •可以使用数据处理指令来调整LR并将其复制到PC中,例如:

SUBS PC, LR, #4

​ 指定S表示同时将SPSR复制到CPSR。

​ 如果异常处理程序入口代码使用堆栈来存储在处理异常时必须保留的寄存器,则它可以使用带有^限定符的加载指令返回。例如,异常处理程序可以使用以下命令在一条指令中返回:

LDMFD sp!, {pc} ^

LDMFD sp!, {R0-R12,pc} ^

​ 在此示例中,^限定符表示SPSR同时复制到CPSR。

​ 为此,异常处理程序必须将以下内容保存到堆栈中:

​ —调用处理程序时,所有需要使用的工作寄存器。

​ —修改链接寄存器以产生与数据处理指令相同的效果。

​ 注意

​ 不能使用16位Thumb指令从异常中返回,因为这些指令无法还原CPSR。RFE指令从当前模式的堆栈中恢复PC和SPSR。

RFEFD sp!

10.2.7 中止处理程序

​ 中止处理程序代码在系统之间可能有很大差异。在许多嵌入式系统中,异常中止表示意外错误,处理程序将记录所有诊断信息,报告错误并让应用程序(或系统)退出。

​ 在使用MMU支持虚拟内存的系统中,中止处理程序可以将所需的虚拟页加载到物理内存中。实际上,它尝试解决最初中止的原因,然后返回中止的指令并重新执行它。

​ CP15寄存器提供了导致中止的存储器访问地址(故障地址寄存器Fault Address Register)和中止的原因(故障状态寄存器Fault Status Register)。原因可能是缺少访问权限,外部中止或地址转换错误。此外,链接寄存器(进行了–8或–4调整,取决于中止是由指令获取还是数据访问引起的),给出了导致中止异常的指令的地址。通过检查这些寄存器,最后执行的指令以及系统中可能的其他内容(例如转换表条目),中止处理程序可以确定要采取的操作。

10.2.8 未定义的指令处理

​ 如果CPU核尝试使用操作码执行一条指令(在ARM体系结构规范中描述为UNDEFINED),或者执行了协处理器指令但没有协处理器将其识别为可以执行的指令,则会导致未定义的指令异常。

​ 在某些系统中,代码可能包含用于协处理器(例如VFP协处理器)的指令,但是系统中不存在相应的VFP硬件。另外,VFP硬件有可能无法处理特定指令,而是想调用软件来对其进行仿真。或者,可能会禁用VFP硬件,采用异常处理,以便可以启用它,然后重新执行指令。

​ 通过未定义的指令向量调用此类仿真器。他们检查导致异常的指令操作码,并确定要采取的措施(例如,在软件中执行适当的浮点运算)。在某些情况下,可能必须将这些处理程序以菊花链方式链接在一起(例如,可能要模拟多个协处理器)。

​ 如果没有软件使用未定义的指令或协处理器指令,则异常的处理程序必须记录适当的调试信息,并杀死由于应用程序。

​ 在某些情况下,未定义指令异常的另一个用途是实现用户断点。

10.2.9 SVC异常处理

​ supervisor call(SVC)通常用于使用户模式代码能够访问OS功能。例如,如果用户代码想要访问系统的特权部分(例如执行文件I / O),则通常将使用SVC指令执行此操作。

​ 可以使用寄存器或者操作码中某个字段将参数传递给SVC处理程序。

​ 发生异常时,异常处理程序可能必须确定内核是处于ARM还是Thumb状态。

​ 特别是SVC处理程序,可能必须读取指令集状态。这是通过检查SPSR T位完成的。该位设置为Thumb状态,清除为ARM状态。

​ ARM和Thumb指令集都具有SVC指令。从Thumb状态调用SVC时,必须考虑以下因素:

​ •指令地址位于LR-2,而不是LR-4。

​ •指令本身是16位的,因此需要半字加载,

​ • SVC编号为8位而不是ARM状态下的24位。

​ 例11-1中显示了说明Linux内核使用SVC的代码

​ SVC#0指令使ARM核采用SVC异常(一种访问内核功能的机制)。寄存器R0定义所需的系统调用(在本例中为sys_write)。其他参数在寄存器中传递。对于sys_write,您需要R0告诉要写入的位置,R1指向要写入的字符,R2给出字符串的长度。

10.3 und异常模示程序示例

10.3.1 代码分析

​ 此节代码所在**裸机Git仓库 NoosProgramProject/(10_异常与中断/008_exception_undef)**目录下。

​ 通过在代码段里里插入一个未定义指令(0xdeadc0de),从而产生未定义指令异常。在未定义异常指令异常的处理函数里,调用printException函数,打印出当前的CPSR值,和产生异常的原因的字符串。

​ 在复位Reset_Handler里要分别设置好SVC模式和und模式的栈,这样我们就可以在各自的模式里调用C代码。通过如下指令,设置好异常向量的基地址

mcr p15, 0, r0, c12, c0, 0

​ 在异常向量表里,通过如下指令跳转到Undefined_Handler标签处

ldr pc, =Undefined_Handler

​ 在Undefined_Handler里将r0-r12和lr保存在und模式的栈上,然后调用printException打印当前的CPSR值,和产生异常的原因的字符串。最后将r0-r12从栈上恢复,lr从栈上弹出到PC,并同时将SPSR恢复到CPSR,从而返回去执行出现未定义异常指令的下一条指令。

​ 代码如下008_exception_undef\start.S:

.text
.global  _start, _vector_table
_start:
_vector_table:
	ldr 	pc, =Reset_Handler			 /* Reset				   */
	ldr 	pc, =Undefined_Handler		 /* Undefined instructions */
	//b Reset_Handler
	//b Undefined_Handler
	b halt//b SVC_Handler//ldr 	pc, =SVC_Handler			 /* Supervisor Call 	   */
	b halt//ldr 	pc, =PrefAbort_Handler		 /* Prefetch abort		   */
	b halt//ldr 	pc, =DataAbort_Handler		 /* Data abort			   */
	.word	0							 /* RESERVED			   */
	b halt//ldr 	pc, =IRQ_Handler			 /* IRQ interrupt		   */
	b halt//ldr 	pc, =FIQ_Handler			 /* FIQ interrupt		   */

.align 2
Undefined_Handler:
	/* 执行到这里之前:
	 * 1. lr_und保存有被中断模式中的下一条即将执行的指令的地址
	 * 2. SPSR_und保存有被中断模式的CPSR
	 * 3. CPSR中的M4-M0被设置为11011, 进入到und模式
	 * 4. 跳到0x4的地方执行程序 
	 */

	/* 在und异常处理函数中有可能会修改r0-r12, 所以先保存 */
	/* lr是异常处理完后的返回地址, 也要保存 */
	stmdb sp!, {r0-r12, lr}  
	
	/* 保存现场 */
	/* 处理und异常 */
	mrs r0, cpsr
	ldr r1, =und_string
	bl printException
	
	/* 恢复现场 */
	ldmia sp!, {r0-r12, pc}^  /* ^会把spsr的值恢复到cpsr里 */
	
und_string:
	.string "undefined instruction exception"

.align 2
Reset_Handler:
	/* Reset SCTlr Settings */
	mrc 	p15, 0, r0, c1, c0, 0	  /* read SCTRL, Read CP15 System Control register		*/
	bic 	r0,  r0, #(0x1 << 13)	  /* Clear V bit 13 to use normal exception vectors  	*/
	bic 	r0,  r0, #(0x1 << 12)	  /* Clear I bit 12 to disable I Cache					*/
	bic 	r0,  r0, #(0x1 <<  2)	  /* Clear C bit  2 to disable D Cache					*/
	bic 	r0,  r0, #(0x1 << 2)	  /* Clear A bit  1 to disable strict alignment 		*/
	bic 	r0,  r0, #(0x1 << 11)	  /* Clear Z bit 11 to disable branch prediction		*/
	bic 	r0,  r0, #0x1			  /* Clear M bit  0 to disable MMU						*/
	mcr 	p15, 0, r0, c1, c0, 0	  /* write SCTRL, Write to CP15 System Control register	*/

    cps     #0x1B                /* Enter undef mode                */
    ldr     sp, =0x80300000     /* Set up undef mode stack      */

    cps     #0x13                /* Enter Supervisor mode         */
    ldr     sp, =0x80200000     /* Set up Supervisor Mode stack  */
	ldr r0, =_vector_table
	mcr p15, 0, r0, c12, c0, 0  /* set VBAR, Vector Base Address Register*/
	//mrc p15, 0, r0, c12, c0, 0  //read VBAR

	bl clean_bss
	
	bl system_init

und_code:
	.word 0xdeadc0de  /* undefine instruction */
	//.word 0xFFFFFFFF

	bl main

halt:
	b  halt

clean_bss:
	/* 清除BSS段 */
	ldr r1, =__bss_start
	ldr r2, =__bss_end
	mov r3, #0
clean:
	cmp r1, r2
	strlt r3, [r1]
	add r1, r1, #4
	blt clean
	
	mov pc, lr

10.3.2 参考章节《4-1.4编译程序》编译程序

10.3.3 参考章节《4-1.4映像文件烧写、运行》烧写、运行程序

​ 此时观察串口打印

10.4 swi异常模示程序示例

10.4.1 代码分析

​ 此节配套源码所在裸机Git仓库 NoosProgramProject/(10_异常与中断/008_exception_swi) 目录下。

​ 然后通过swi 123,执行管理模式异常,程序跳转到异常向量表偏移0x8的地方执行,在异常向量表里通过如下指令跳转到SVC_Handler标签处执行。

ldr pc, =SVC_Handler

​ 在SVC_Handler里将r0-r12和lr保存在SVC模式的栈上,然后将lr的值移动到R4,调用printException函数打印出当前的CPSR值,和产生异常的原因的字符串。将R4减去4,赋值给R0,也就是swi指令所在的地址,然后调用printSWIVal函数打印出swi指令的参数。最后将r0-r12从栈上恢复,lr从栈上弹出到PC,并同时将SPSR恢复到CPSR,从而返回去执行swi指令的下一条指令。代码如下(008_exception_swi\start.S):

.text
.global  _start, _vector_table
_start:
_vector_table:
	ldr 	pc, =Reset_Handler			 /* Reset				   */
	ldr 	pc, =Undefined_Handler		 /* Undefined instructions */
	ldr 	pc, =SVC_Handler			 /* Supervisor Call 	   */
	b halt//ldr 	pc, =PrefAbort_Handler		 /* Prefetch abort		   */
	b halt//ldr 	pc, =DataAbort_Handler		 /* Data abort			   */
	.word	0							 /* RESERVED			   */
	b halt//ldr 	pc, =IRQ_Handler			 /* IRQ interrupt		   */
	b halt//ldr 	pc, =FIQ_Handler			 /* FIQ interrupt		   */

.align 2
Undefined_Handler:
	/* 执行到这里之前:
	 * 1. lr_und保存有被中断模式中的下一条即将执行的指令的地址
	 * 2. SPSR_und保存有被中断模式的CPSR
	 * 3. CPSR中的M4-M0被设置为11011, 进入到und模式
	 * 4. 跳到0x4的地方执行程序 
	 */

	/* 在und异常处理函数中有可能会修改r0-r12, 所以先保存 */
	/* lr是异常处理完后的返回地址, 也要保存 */
	stmdb sp!, {r0-r12, lr} 

	/* 保存现场 */
	/* 处理und异常 */
	mrs r0, cpsr
	ldr r1, =und_string
	bl printException
	
	/* 恢复现场 */
	ldmia sp!, {r0-r12, pc}^  /* ^会把spsr的值恢复到cpsr里 */
	
und_string:
	.string "undefined instruction exception"

.align 2
SVC_Handler:
	/* 执行到这里之前:
	 * 1. lr_svc保存有被中断模式中的下一条即将执行的指令的地址
	 * 2. SPSR_svc保存有被中断模式的CPSR
	 * 3. CPSR中的M4-M0被设置为10011, 进入到svc模式
	 * 4. 跳到0x08的地方执行程序 
	 */

	/* 保存现场 */
	/* 在swi异常处理函数中有可能会修改r0-r12, 所以先保存 */
	/* lr是异常处理完后的返回地址, 也要保存 */
	stmdb sp!, {r0-r12, lr}  

	mov r4, lr
	
	/* 处理swi异常 */
	mrs r0, cpsr
	ldr r1, =swi_string
	bl printException

	sub r0, r4, #4
	bl printSWIVal
	
	/* 恢复现场 */
	ldmia sp!, {r0-r12, pc}^  /* ^会把spsr的值恢复到cpsr里 */
	
swi_string:
	.string "swi exception"

.align 2
Reset_Handler:
	/* Reset SCTlr Settings */
	mrc 	p15, 0, r0, c1, c0, 0	  /* read SCTRL, Read CP15 System Control register		*/
	bic 	r0,  r0, #(0x1 << 13)	  /* Clear V bit 13 to use normal exception vectors  	*/
	bic 	r0,  r0, #(0x1 << 12)	  /* Clear I bit 12 to disable I Cache					*/
	bic 	r0,  r0, #(0x1 <<  2)	  /* Clear C bit  2 to disable D Cache					*/
	bic 	r0,  r0, #(0x1 << 2)	  /* Clear A bit  1 to disable strict alignment 		*/
	bic 	r0,  r0, #(0x1 << 11)	  /* Clear Z bit 11 to disable branch prediction		*/
	bic 	r0,  r0, #0x1			  /* Clear M bit  0 to disable MMU						*/
	mcr 	p15, 0, r0, c1, c0, 0	  /* write SCTRL, Write to CP15 System Control register	*/

   cps     #0x1B                /* Enter undef mode                */
   ldr     sp, =0x80300000     /* Set up undef mode stack      */

   cps     #0x13                /* Enter Supervisor mode         */
   ldr     sp, =0x80200000     /* Set up Supervisor Mode stack  */

	ldr r0, =_vector_table
	mcr p15, 0, r0, c12, c0, 0  /* set VBAR, Vector Base Address Register*/
	//mrc p15, 0, r0, c12, c0, 0  //read VBAR

	bl clean_bss
	
	bl system_init

und_code:
	.word 0xdeadc0de  /* undefine instruction */
	//.word 0xFFFFFFFF

swi_code:
	swi 0x123  /* 执行此命令, 触发SWI异常, 进入0x8执行 */

	bl main

halt:
	b  halt
       
clean_bss:
	/* 清除BSS段 */
	ldr r1, =__bss_start
	ldr r2, =__bss_end
	mov r3, #0
clean:
	cmp r1, r2
	strlt r3, [r1]
	add r1, r1, #4
	blt clean
	
	mov pc, lr

10.4.2 参考章节《4-1.4编译程序》编译程序

10.4.3 参考章节《4-1.4映像文件烧写、运行》烧写、运行程序

​ 此时观察串口打印

10.5 中断处理

​ 较旧的ARM体系结构版本使实现者在设计外部中断控制器时具有很大的自由度,而无需就中断的数量或类型或用于与中断控制器模块接口的软件模型达成协议。通用中断控制器v2(GIC)架构提供了更为严格的规范,不同厂商的中断控制器之间具有更高的一致性。这使中断处理程序代码更易于移植。

10.5.1 外部中断请求

​ ARM核如何具有两个外部中断请求FIQ和IRQ。这两个都是对电平触发,对低电平有效。各个不同的实现都有中断控制器,这些控制器接受来自各种外部源的中断请求并将它们映射为FIQ或IRQ,从而导致ARM核发生异常。

​ 通常,只有当相应的CPSR禁止位(分别为F和I位)清零并且相应的输入为有效时,才可以产生中断异常。

​ CPS指令提供了一种简单的机制来启用或禁用由CPSR A,I和F位(分别为异步中止,IRQ和FIQ)控制的异常。

​ CPS IE或CPS ID将分别启用或禁用异常。使用字母A,I和F中的一个或多个指定要启用或禁用的异常。省略了相应字母的异常将不会被修改。

​ 在Cortex-A系列处理器中,可以配置CPU核,以使FIQ不能被软件屏蔽。这被称为不可屏蔽FIQ,并由CPU核复位时采样的硬件配置的输入信号控制。发生FIQ异常后,它们仍将自动被屏蔽。

10.5.2 分配中断

​ 中断控制器接受和仲裁来自各种源的中断。控制器通常包含多个寄存器,这些寄存器使运行的软件能够屏蔽各个中断源,确认来自外部设备的中断,为各个中断源分配优先级并确定当前需要处理的中断源。

​ 此中断控制器可以是特定于系统的设计,也可以是ARM通用中断控制器(GIC)架构的实现。

10.5.3 简单的中断处

​ 这代表了最简单的中断处理程序。发生中断时,将禁用其他同类中断,直到稍后显式启用。我们只能在第一个中断请求完成时才能处理其他中断,并且在此期间没有更高优先级或更紧急的中断需要处理。这通常不适用于复杂的嵌入式系统,但是解释更复杂的的示例之前了解是很有用的,在这种情况下是不可重入的中断处理程序。

​ 处理中断所采取的步骤如下:

​ 1.外部硬件引发IRQ异常。ARM核自动执行几个步骤。当前模式下PC的内容存储在LR_IRQ中。CPSR寄存器被复制到SPSR_IRQ。CPSR内容被更新,设置模式位为IRQ模式,并且将I位设置为屏蔽其他IRQ。PC被设置为向量表中的IRQ入口。

​ 2.执行向量表中IRQ入口处(中断异常的分支)的指令。

​ 3.中断处理程序保存被中断程序的上下文,它将被该中断处理程序损坏的所有寄存器压入堆栈。当中断处理程序完成执行时,这些寄存器将从堆栈中弹出以恢复。

​ 4.中断处理程序确定中断源,然后调用响应的处理程序。

​ 5.通过将SPSR_IRQ复制到CPSR,并还原先前保存的上下文,准备将CPU核切换到先前的执行状态,最后从LR_IRQ恢复PC。

​ 相同的顺序也适用于FIQ中断。

10.5.4 嵌套中断处理

​ 嵌套中断处理是软件可以在完成对当前中断的处理之前接受另一个中断。这可以将中断进行优先级分级,降低高优先级事件的响应延迟,代价是增加了软件的复杂性。值得注意的是,嵌套中断处理是由软件根据中断优先级配置和中断控制而不是由硬件完成的。

​ 可重入中断处理程序在跳转到启用了中断的嵌套子程序或C函数之前,必须保存IRQ状态,然后切换CPU核模式,并为新的核心模式保存状态。这是因为新中断随时可能发生,这将导致CPU核存储新中断的返回地址并覆盖原始中断。当原始中断尝试返回主程序时,它将导致系统故障。为了防止这种情况,嵌套中断处理程序必须在重新启用中断之前更改CPU核模式。

​ 注意:如果一个程序可以在执行过程中中断,然后在先前执行完成之前再次调用,则该该程序是可重入的。

​ 必须在重新启用中断之前保留SPSR的值。如果不是,则任何新的中断都会覆盖SPSR_irq的值。解决方案是使用以下方法在重新启用中断之前将SPSR保存到栈上:

​ SRSFD sp !,#0x12

​ 此外,在中断处理程序代码中使用BL指令将导致LR_IRQ损坏。解决方法是在使用BL指令之前切换到Supervisor模式。因此,可重入的中断处理程序必须在引发IRQ异常必须采取以下步骤:

​ 1.中断处理程序保存被中断程序的上下文(即,它将被该处理程序破坏的所有寄存器(包括返回地址和SPSR_IRQ)压入备用CPU核模式堆栈中)。

​ 2.它确定必须处理的中断源,并清除外部硬件中的中断源(防止其立即触发另一个中断)。

​ 3.中断处理程序更改为CPU核为SVC模式,将CPSR I位置1(中断仍被禁用)。

​ 4.中断处理程序将异常返回地址保存在新模式的堆栈中,并重新启用中断。

​ 5.它调用适当的处理程序代码。

​ 6.完成后,中断处理程序将禁用IRQ并从堆栈中弹出异常返回地址。

​ 7.它直接从堆栈中恢复被中断程序的上下文。这包括还原PC和CPSR,CPSR切换回先前的执行模式。如果SPSR的I位未设置,则该操作还将重新使能中断。

10.6 通用中断处理器(GIC, Generic Interrupt Controller)

​ GIC体系结构定义了通用中断控制器(GIC),该控制器包括一组用于管理单核或多核系统中的中断的硬件资源。GIC提供了内存映射寄存器,可用于管理中断源和行为,以及(在多核系统中)用于将中断路由到各个CPU核。它使软件能够屏蔽,启用和禁用来自各个中断源的中断,以(在硬件中)对各个中断源进行优先级排序和生成软件触发中断。它还提供对TrustZone安全性扩展的支持。GIC接受系统级别中断的产生,并可以发信号通知给它所连接的每个内核,从而有可能导致IRQ或FIQ异常发生。

​ 从软件角度来看,GIC具有两个主要功能模块:

​ ① 仲裁单元(Distributor)

​ 系统中的所有中断源都连接到该单元。仲裁单元具有寄存器来控制各个中断源的属性,例如优先级、状态、安全性、路由信息和使能状态。仲裁单元通过连接的CPU接口单元确定将哪个中断转发给内核。

​ ② CPU接口单元(CPU Interface)

​ CPU核通过控制器的CPU接口单元接收中断。CPU接口单元寄存器用于屏蔽,识别和控制转发到CPU核的中断的状态。系统中的每个CPU核心都有一个单独的CPU接口。

​ 中断在软件中由一个称为中断ID的数字标识。中断ID唯一对应于一个中断源。软件可以使用中断ID来识别中断源并调用相应的处理程序来处理中断。呈现给软件的中断ID由系统设计确定,一般在SOC的数据手册有记录。

​ 中断可以有多种不同的类型:

​ ① 软件触发中断(SGI,Software Generated Interrupt)

​ 这是由软件通过写入专用仲裁单元的寄存器即软件触发中断寄存器(ICDSGIR)显式生成的。它最常用于CPU核间通信。SGI既可以发给所有的核,也可以发送给系统中选定的一组核心。中断号0-15保留用于SGI的中断号。用于通信的确切中断号由软件决定。

​ ② 专用外设中断(PPI,Private Peripheral Interrupt)

​ 这是由单个CPU核专用的外设生成的。PPI的中断号为16-31。它们标识CPU核专用的中断源,并且独立于另一个内核上的相同中断源,比如,每个核的计时器。

​ ③ 共享外设中断(SPI,Shared Peripheral Interrupt)

​ 这是由外设生成的,中断控制器可以将其路由到多个核。中断号为32-1020。SPI用于从整个系统可访问的各种外围设备发出中断信号。

​ 中断可以是边沿触发的(在中断控制器检测到相关输入的上升沿时认为中断触发,并且一直保持到清除为止)或电平触发(仅在中断控制器的相关输入为高时触发)。

​ 中断可以处于多种不同状态:

​ ① 非活动状态(Inactive)–这意味着该中断未触发。

​ ② 挂起(Pending)–这意味着中断源已被触发,但正在等待CPU核处理。待处理的中断要通过转发到CPU接口单元,然后再由CPU接口单元转发到内核。

​ ③ 活动(Active)–描述了一个已被内核接收并正在处理的中断。

​ ④ 活动和挂起(Active and pending)–描述了一种情况,其中CPU核正在为中断服务,而GIC又收到来自同一源的中断。

​ 中断的优先级和可接收中断的核都在仲裁单元(distributor)中配置。外设向仲裁单元触发的中断将标记为pending状态(或Active and Pending状态,如触发时果状态是active)。distributor确定可以传递给CPU核的优先级最高的pending中断,并将其转发给内核的CPU interface。通过CPU interface,该中断又向CPU核发出信号,此时CPU核将触发FIQ或IRQ异常。

​ 作为响应,CPU核执行异常处理程序。异常处理程序必须从CPU interface寄存器查询中断ID,并开始为中断源提供服务。完成后,处理程序必须写入CPU interface寄存器以报告处理结束。然后CPU interface准备转发distributor发给它的下一个中断。

​ 在处理中断时,中断的状态开始为pending,active,结束时变成inactive。中断状态反映在distributor寄存器中。

​ 下图是GIC控制器的逻辑结构:

10.6.1 配置

​ GIC作为内存映射的外围设备进行访问。所有内核都可以访问公共的distributor单元,但是CPU interface是备份的,也就是说,每个CPU核都使用相同的地址来访问其自己的专用CPU接口。一个CPU核不可能访问另一个CPU核的CPU接口。

​ Distributor拥有许多寄存器,可以通过它们配置各个中断的属性。这些可配置属性是:

​ •中断优先级。Distributor使用它来确定接下来将哪个中断转发到CPU接口。

​ •中断配置。这确定中断是对电平触发还是边沿触发。

​ •中断目标。这确定了可以将中断发给哪些CPU核。

​ •中断启用或禁用状态。只有Distributor中启用的那些中断变为挂起状态时,才有资格转发。

​ •中断安全性确定将中断分配给Secure还是Normal world软件。

​ •中断状态。

​ distributor还提供优先级屏蔽,通过该屏蔽可防止低于某个优先级的中断发送给CPU核。Distributor通过此方法确定是否可以将pending中断转发到特定的CPU核。

​ 每个CPU核上的CPU interface有助于发送给该CPU核的中断控制和处理。

10.6.2 初始化

​ distributor和CPU interface在复位时均被禁用。复位后,必须将GIC初始化,然后才能将中断传递给CPU核。

​ 在distributor中,软件必须配置优先级、目标核、安全性并启用单个中断。随后必须通过其控制寄存器使能。对于每个CPU接口,软件必须对优先级和抢占设置进行编程。每个CPU接口模块本身必须通过其控制寄存器使能。这为GIC做好了向内核传递中断的准备。

​ 在CPU核可以处理中断之前,软件会通过在向量表中设置有效的中断向量并清除CPSR中的中断屏蔽位来让CPU核可以接收中断。

​ 可以通过禁用distributor单元来禁用系统中的整个中断机制。可以通过禁用单个CPU的CPU接口模块或者在CPSR中设置屏蔽位来禁止向单个CPU核的中断传递。也可以在distributor中禁用(或启用)单个中断。

​ 为了使某个中断可以触发CPU核,必须将各个中断,distributor和CPU接口全部使能,并将CPSR中断屏蔽位清零。

10.6.3 GIC中断处理

​ 当CPU核接收到中断时,它会跳转到中断向量表执行。顶层中断处理程序读取CPU接口模块的Interrupt Acknowledge Register,以获取中断ID。

​ 除了返回中断ID之外,读取还会使该中断在distributor中标记为active状态。一旦知道了中断ID(标识中断源),顶层处理程序现在就可以分派特定于设备的处理程序来处理中断。

​ 当特定于设备的处理程序完成执行时,顶级处理程序将相同的中断ID写入CPU interface模块中的End of Interrupt register中断结束寄存器,指示中断处理结束。

​ 除了移除active状态之外,这将使最终中断状态变为inactive或pending(如果状态为inactive and pending),这将使CPU interface能够将更多待处理pending的中断转发给CPU核。这样就结束了单个中断的处理。

​ 同一内核上可能有多个中断等待服务,但是CPU interface一次只能发出一个中断信号。顶层中断处理程序重复上述顺序,直到读取特殊的中断ID值1023,表明该内核不再有任何待处理的中断。这个特殊的中断ID被称为伪中断ID(spurious interrupt ID)。

​ 伪中断ID是保留值,不能分配给系统中的任何设备。当顶级处理程序读取了伪中断ID时,它可以完成其执行,并为CPU核做好准备以继续执行被中断的任务。

10.7 中断控制器寄存器

10.7.1 Distributor 寄存器描述

10.7.1.1 Distributor Control Register, GICD_CTLR

​ [1] EnableGrp1使能,用于将pending Group 1中断从Distributor转发到CPU interfaces:

​ 0 Group 1中断不转发。

​ 1根据优先级规则转发Group 1中断。

​ [0] EnableGrp0使能,用于将pending Group 0中断从Distributor转发到CPU interfaces:

​ 0 Group 0中断不转发。

​ 1根据优先级规则转发Group 0中断。

10.7.1.2 Interrupt Controller Type Register, GICD_TYPER

​ [15:11] LSPI如果GIC实现了安全扩展,则此字段的值是已实现的可锁定SPI的最大数量,范围为0(0b00000)到31(0b11111)。如果此字段为0b00000,则GIC不会实现配置锁定。如果GIC没有实现安全扩展,则保留该字段。

​ [10] SecurityExtn指示GIC是否实施安全扩展。

​ 0未实施安全扩展。

​ 1实施了安全扩展。

​ [7:5] CPUNumber表示已实现的CPU interfaces的数量。已实现的CPU interfaces数量比该字段的值大1,例如,如果此字段为0b011,则有四个CPU interfaces。如果GIC实现了虚拟化扩展,则这也是虚拟CPU接口的数量。

​ [4:0] ITLinesNumber指示GIC支持的最大中断数。如果ITLinesNumber = N,则

​ 最大中断数为32*(N+1)。中断ID的范围是0到(ID的数量– 1)。例如:

​ 0b00011最多128条中断线,中断ID 0-127。

​ 中断的最大数量为1020(0b11111)。无论此字段定义的中断ID的范围如何,都将中断ID 1020-1023保留用于特殊目的。

10.7.1.3 Distributor Implementer Identification Register, GICD_IIDR

​ [31:24] ProductID产品标识ID。

​ [23:20]-保留。

​ [19:16] Variant Variant编号。通常是产品的主要版本号。

​ [15:12] Revision Revision编号。通常此字段用于区分产品的次版本号。

​ [11:0] 实现者包含实施GIC分销商的公司的JEP106代码:

​ [11:8] 实现者的JEP106 continuation code。对于ARM实现,此字段为0x4。

​ [7]始终为0。

​ [6:0] 实现者的JEP106code。对于ARM实现,位[7:0]为0x3B。

10.7.1.4 Interrupt Group Registers, GICD_IGROUPRn

​ [31:0] 组状态位,对于每个位:

​ 0相应的中断为Group 0。

​ 1相应的中断为Group 1。

10.7.1.5 Interrupt Set-Enable Registers, GICD_ISENABLERn

​ [31:0] 设置使能位对于SPI和PPI,每个位控制相应的中断从Distributor到CPU interfaces的转发:

​ 读到0 表明转发相应的中断被禁止,读到1表明可以转发相应的中断

​ 写0 没有效果,写1表示使能相应中断的转发

10.7.1.6 Interrupt Clear-Enable Registers, GICD_ICENABLERn

​ [31:0]清除SPI和PPI的使能位,每个位控制相应的中断从Distributor到CPU interfaces的转发:

​ 读到0 表明转发相应的中断被禁止,读到1表明可以转发相应的中断

​ 写0 没有效果,写1表示禁止相应中断的转发

10.7.1.7 Interrupt Set-Active Registers, GICD_ISACTIVERn

​ [31:0] Set-active的每个位:

​ 读取0相应的中断not active。

​ 1相应的中断active。

​ 写0无效。

​ 1 Activates相应的中断(如果尚未Activates)。 如果中断已处于Activates状态,则写入无效。向该位写入1后,随后读取该位将返回值1。

10.7.1.8 Interrupt Clear-Active Registers, GICD_ICACTIVERn

​ [31:0] Clear-active的每个位:

​ 读取0相应的中断not active。

​ 1相应的中断active。

​ 写0无效。

​ 1如果中断处于active状态,则Deactivates相应的中断。 如果中断已被Deactivates,写入无效。向该位写入1后,随后对该位的读取将返回值0。

10.7.1.9 Interrupt Priority Registers, GICD_IPRIORITYRn

​ [31:24]优先级,byte offset 3,每个优先级字段都具有一个优先级值,值越小,相应中断的优先级越高。

​ [23:16]优先级,byte offset 2

​ [15:8优先级,byte offset 1

​ [7:0]优先级,byte offset 0

10.7.1.10 Interrupt Processor Targets Registers, GICD_ITARGETSRn

​ [31:24] CPU目标,byte offset 3,处理器编号从0开始,并且CPU目标字段中的每个位均指代相应的处理器。例如,值0x3表示将挂起中断发送到处理器0和1。对于GICD_ITARGETSR0到GICD_ITARGETSR7,任何CPU目标字段的读取都将返回执行读取的处理器的编号。

​ [23:16] CPU目标,byte offset 2

​ [15:8] CPU目标,byte offset 1

​ [7:0] CPU目标,byte offset 0

10.7.1.11 Interrupt Configuration Registers, GICD_ICFGRn

​ [2F + 1:2F] Int_config,field F对于Int_config [1],即最高有效位[2F + 1],编码为:

​ 0相应的中断对电平敏感。

​ 1相应的中断沿触发。

​ Int_config [0]是最低有效位,即位[2F],但保留位。

​ 对于SGI:

​ Int_config [1]不可编程,RAO / WI。

​ 对于PPI和SPI:

​ Int_config [1]对于SPI,此位是可编程的。对于PPI,是否可编程由实现决定。对该位的读取始终正确反映相应的中断是电平敏感还是边沿触发。

10.7.1.12 Identification registers

​ [31:8]- 由实现定义。CoreLink和CoreSight外围设备ID寄存器方案要求保留这些位,RAZ,ARM强烈建议实现遵循此方案。

​ [7:4] GIC体系结构的ArchRev修订版字段。该字段的值取决于GIC架构版本:

​ •GICv1为0x1

​ •GICv2为0x2。

​ [3:0]-由实现定义。

10.7.2 CPU interface寄存器描述

10.7.2.1 CPU Interface Control Register, GICC_CTLR

​ [9] EOImodeNS 控制对GICC_EOIR和GICC_DIR寄存器的非安全访问:

​ 0 GICC_EOIR具有降低优先级和deactivate中断功能。对GICC_DIR的访问是未定义的。

​ 1 GICC_EOIR仅具有降低优先级功能。 GICC_DIR寄存器具有deactivate中断功能。

​ [6] IRQBypDisGrp1当CPU interface的IRQ信号被禁用时,该位控制是否向处理器发送bypass IRQ信号:

​ 0将bypass IRQ信号发送给处理器

​ 1 将bypass IRQ信号不发送到处理器。

​ 有关更多信息,请参阅第2-27页的中断信号旁路和GICv2旁路禁用。

​ [5] FIQBypDisGrp1当CPU interface的FIQ信号被禁用时,该位控制是否向处理器发送bypass FIQ信号:

​ 0将旁路FIQ信号发送给处理器

​ 1旁路FIQ信号不发送到处理器。

​ 有关更多信息,请参阅第2-27页的中断信号旁路和GICv2旁路禁用。

​ [0] EnableGrp1使能CPU interface向连接的处理器发出的组1中断的信号。

​ 0禁用中断信号

​ 1使能中断信号。

​ 注意

​ 当该位设置为0时,CPU interface将忽略转发给它的任何pending的组1中断。当该位置1时,CPU接口开始处理转发给它的pending的组1中断。更改生效需要一个很小但有限的时间。

10.7.2.2 Interrupt Priority Mask Register, GICC_PMR

​ [7:0]优先级 CPU interface的优先级屏蔽级别。如果中断的优先级高于此字段值,接口将中断信号通知处理器。如果GIC支持的优先级少于256个,则某些位为RAZ / WI,如下所示:

​ 128个级别 bit[0] = 0。

​ 64个级别bit [1:0] = 0b00。

​ 32个级别bit [2:0] = 0b000。

​ 16个级别bit [3:0] = 0b0000。

​ PS:imx6ull最多为32个级别

10.7.2.3 Binary Point Register, GICC_BPR

​ [2:0] Binary point 此字段的值控制如何将8bit中断优先级字段拆分为组优先级和子优先级,组优先级用来决定中断抢占。有关此字段如何确定分配给组优先级字段的中断优先级位的信息,请参见:

​ 当GICC_CTLR.CBPR位设置为1时,用于在GIC上处理第group 1中断可支持中断分组。

​ PS:imx6ull 上BPR的最小值为2

10.7.2.4 Interrupt Acknowledge Register, GICC_IAR

​ [12:10] CPUID对于多处理器中的SGI,此字段标识请求中断的处理器。 它返回发出请求的CPU interface的编号,例如,值为3表示该请求是通过对CPU interface 3上的GICD_SGIR的写操作生成的。对于所有其他中断,此字段为RAZ。

​ [9:0]中断ID中断ID。

10.7.2.5 Interrupt Register, GICC_EOIR

​ [12:10] CPUID在多处理器实现中,如果写入引用SGI,则此字段包含来自相应GICC_IAR访问的CPUID值。 在所有其他情况下,此字段为SBZ。

​ [9:0] EOIINTID来自相应GICC_IAR访问的中断ID值。

第十一章:GPIO中断

第十一章:GPIO中断

11. GPIO中断

1.1 GPIO中断介绍(通用的概念)

​ 假设你现在正在写作业,突然电话响起,你需要停下写作业接电话,挂电话后继续写作业。突然由人按门铃,你需要先去开门,然后继续回来写作业。电话和门铃打断了写作业,能中断写作业的事情有很多,比如身体不舒服,口渴等。被打断后怎么做?身体不舒服就停下写作业休息一会,身体好了继续写作业。口渴就停下写作业喝水,喝完水继续写作业。如果你正在接一个很重要的电话,突然门铃响了,这是会优先处理其中一件事,比如先让按门铃的人等一下,挂电话后再去开门,或者先挂电话,等开门后再打电话过去。这就存在一个中断优先级的问题。

​ 当有事件产生,处理事件之前我们需要记住现在作业写到第几页了,或者在作业上记一个标记,然后取处理事件,电话铃响了需要到放电话的地方去,门铃响了需要到门口去,口渴需要到放饮水机地方去,也就是说,不同的突发事件需要到不同的地方去处理。

​ 嵌入式系统中也有类似的情况。CPU在运行过程中,也会被各种异常打断。这些异常

​ ① 指令未定义

​ ② 指令、数据访问有问题

​ ③ SWI(软中断)

​ ④ 快中断

​ ⑤ 中断

​ 中断也属于一种异常,导致中断发生的中断源有很多,比如:

​ ① 按键

​ ② 定时器

​ ③ ADC转换完成

​ ④ UART发生完数据、接收数据

​ ⑤ 等等

​ 这些众多的中断源,汇集中中断管理器,由中断管理器选择优先级最高的中断并通知CPU。CPU会根据中断的类型到跳转到不同的地址处理中断。发生中断后,CPU并不是随便跳到一个地址处理中断,而是根据异常向量表,跳转到对应的地址处理中断。

1.2.1 GPIO中断

​ GPIO中断,指有GPIO模块产生的中断,有边沿触发中断或者电平翻转中断。GPIO模块能检测到引脚上的值是0还是1,并能通过外部拓展将电平从变为1或是从1变到0。CPU接收外部的中断请求,并进行处理,其实是一个被动接受的过程,这样的好处是己能保证主任务的执行效率,又能及时获取外部请求,从而处理重要的设备请求中断。

​ 当GPIO模块检测到管脚电平变化且满足中断触发条件,就会触发中断,CPU会跳转到中断处理地址进行中断处理,为了避免破坏主任务数据,CPU会处理保存当前相关寄存器(保存现场)并进入中断服务函数,执行完中断服务函数后,CPU会恢复相关寄存器(恢复现场),回到主任务继续执行程序。

​ 程序发生GPIO中断后会根据异常向量表强制跳转到0x18(IRQ中断地址)。如下图:

​ 异常向量表并不总是从0地址开始,IMX6ULL可以设置vector base寄存器,指定向量表在其他位置,比如设置 vector base 为 0x80000000,指定为 DDR 的某个地 址。但是表中的各个异常向量的偏移地址,是固定的:复位向量偏移地址是 0,中断是 0x18。

​ 本次实验使用GPIO中断方式实现按键控制LED亮灭,并通过串口把中断ID打印出来。

​ 中断控制器和CP15协处理器

​ 操作系统中,中断系统是很重要的一部分。有了中断系统,才不用一直轮询是否有事件发生,系统效率得以提高。中断系统一般分为三个部分:模块、中断管理器和处理器。模块通常有寄存器设置是否使能中断和中断触发方式。中断控制器可以管理中断优先级等。处理器则设置寄存器响应中断。

​ 如上图所示,硬件中断信号发送GIC(Generic Interrupt Controller),GIC产生一个FIQ或IRQ信号给CPU。GPIO模块、UART模块均能产生硬件中断。在初始化中断时,要初始化GIC中断控制器,如果时GPIO中断则还要设置GPIO模块内相关的寄存器,如果时串口中断则还要设置UART模块内相关的寄存器。

1.2 GIC中断控制器介绍

1.2.1 IMX6ULL GIC中断控制器

​ IMX6ULL是Cortex-A7内核,采用GIC V2(Generic Interrupt Controller)中断控制器。在这里只简单的介绍一下GIC,具体可以参考arm文档。

​ GIC的主要作用可以归结为接受硬件中断信号,并进行简单的处理,按照一定的设置策略,分给对应的CPU处理。如下图:

​ ARM内核只提供了四个信号给GIC汇报中断情况:VIRQ(虚拟快速IRQ)、VFIQ(虚拟快速FIQ)、IRQ、FIQ。VIRQ、VFIQ是针对虚拟化,剩下就是IRQ和FIQ。GPIO中断属于IRQ中断,所以在本次实验中GIC上报IRQ信号给ARM内核。

​ 接下来看一下GIC内部过程,如下图:

​ 中断源分为SPI(Shared Peripheral Interrupt)、PPI(Private Peripheral Interrupt)、SGI request(Software-generated Interrupt)。外部中断都属于SPI中断源。

​ GIC控制器包括分发器(Distributor)和CPU接口端(CPU interface)。

​ 分发器(Distributor)主要完成对整个中断控制器使能,设置中断优先级,设置中断触发方式,决定每个中断信号发送到哪一个具体的CPU上执行。

​ CPU接口端(CPU interface)主要完成使能和发送一个具体的中断信号到特定的CPU上,确认中断已被CPU接受、处理以及处理完成,设置CPU能接受中断的优先级以及基于级别的中断抢占。

​ 中断信号先到分发器,根据设定CPU,发送到CPU对应的interface上,在这里判断是否优先级足够高,能否抢断或打断当前的终端处理,如果可以,CPU interface就会发送一个物理的signa到CPU的IRQ线上,CPU接收到中断信号,转到中断处理模式进行处理。

1.2.2 IMX6ULL GIC中断寄存器

​ GIC寄存器分为Distributor register和CPU interface register。寄存器数目较多,这里介绍本次实验中需要我们设置的寄存器。

1.2.2.1 GICC_IAR寄存器

​ GICC_IAR寄存器属于CPU interface register,作用是:保存中断ID,读取GICC_IAR寄存器可以获得中断ID,这个过程可以当作对中断的确认。

1.2.2.2 GICC_EOIR寄存器

​ GICC_EOIR寄存器属于CPU interface register,作用是:中断完成时,向GICC_EOIR写入中断ID,表示IRQ处理结束。

1.2.3 CP15协处理器

1.2.3.1 CP15协处理器介绍

​ 在基于ARM的嵌入式系统中,存储系统通常是系统控制协处理器CP15完成的。ARM处理器使用协处理器指令MCR和MRC来读写寄存器,控制cache、MMU、配置时钟(在bootloader时钟初始化时会用到)等。CP15包含16个32位寄存器,编号为0~15。

​ 在本次实验中,需要设置的寄存器有:SCTLR(System Control Register)寄存器,VBAR(Vector Base Address)寄存器。

1.2.3.2 SCTLR(System Control Register)寄存器

​ 设置SCTLR寄存器可以控制cache、MMU等。

​ Bit[13]: 异常向量表地址设置位。我们设置为0,默认0x00000000地址,可以通过设置vector base寄存器映射到设置地址。

​ Bit[12]、Bit[2]: 指令cache、数据cache使能位。刚上电时,CPU还不能管理cache,指令cache可关闭也可不关闭,但数据cache一定要关闭,否争可能导致刚开始的代码里,去读取数据时到cache里读取,而这时候RAM数据还没有cache过来,导致数据预取错误。

​ Bit[11]: 分支预测使能位。分支预测技术是用来提高执行流水线指令效率。在本次实验中关闭分支预测技术。

​ Bit[1]: 字节对齐设置位。打开字节对齐,可以提高CPU访问效率,但会损失一部分内存空间。在本次实验中 CPU并不会做太多复杂的工作,所以关闭字节对齐。

​ Bit[0]: MMU使能位。上电后系统没有配置MMU,所以要先关掉MMU。

​ MRC p15, 0, < Rt >, c1, c0, 0: 把SCTLR寄存器的值读到ARM寄存器Rt中。

​ MRC p15, 0, < Rt >, c1, c0, 0: 把ARM寄存器Rt的值写入SCTLR寄存器。

1.2.3.3 VBAR(Vector Base Address)寄存器

​ 设置VBAR寄存器,可以设置异常向量表的映射地址。如果不把异常向量表的映射地址告诉CPU,在发生异常时,CPU就找不到异常向量表,就无法处理异常。

​ MRC p15, 0, < Rt >, c12, c0, 0: 把VBAR寄存器的值读到ARM寄存器Rt中。

​ MRC p15, 0, < Rt >, c12, c0, 0: 把ARM寄存器Rt的值写入VBAR寄存器。

1.3 IMX6ULL的GPIO中断寄存器介绍

1.3.1 GPIO interrupt configuration register1 (GPIOx_ICR1)

​ GPIO中断配置寄存器1

​ ICRn[1:0]决定中断类型:

​ 00 低电平触发

​ 01 高电平触发

​ 10 上升沿触发

​ 11 下降沿触发

​ ICR0~ICR15对应GPIO interrupt 0-15

1.3.2 GPIO interrupt configuration register2 (GPIOx_ICR2)

​ GPIO中断配置寄存器2

​ 与GPIOx_ICR1类似

​ ICR0~ICR15对应GPIO interrupt 16-31

1.3.3 GPIO interrupt mask register (GPIOx_IMR)

​ GPIO中断屏蔽寄存器

​ Bit[n]对应interrupt n

​ 0 interrupt n屏蔽

​ 1 interrupt n 打开

1.3.4 GPIO interrupt status register (GPIOx_ISR)

​ GPIO中断状态寄存器

​ 中断状态位-当在GPIO输入上检测到有效状态(由相应的ICR位确定)时,该寄存器的位n置为有效(高电平有效)。该寄存器的值与GPIO_IMR中的值无关。

​ 当检测到活动状态时,相应的位将保持置位状态,直到被软件清除为止。通过将1写入相应的位位置来清除状态标志。

1.3.5 GPIO edge select register (GPIOx_EDGE_SEL)

​ GPIO中断边沿选择寄存器

​ 设置GPIO_EDGE_SEL [n]时,GPIO会忽略ICR [n]设置,同时检测对应输入信号的上升沿和下降沿。

1.4 按键中断程序编程示例一

1.4.1 管脚设置和查询中断号

​ 从上面的电路图可见KEY1接在GPIO5_1(SNVS_TAMPER1 pad,ALT5)上,KEY4接在GPIO4_14(NAND_CE1_B pad,ALT5)上。使用IOMUXC_SetPinMux设置这两个引脚为GPIO模式。如何获取这两个GPIO的中断号呢?查阅数据手册的chapter3,CORTEX A7interrupts章节,这两个GPIO的中断号如下表所示。对应到GIC的SPI中断号需要在此编号基础上加上32,所以KEY1对应的GIC interrupt ID为(74 + 32 = 106),KEY2对应的GIC interrupt ID为(72 + 32 = 104)。

1.4.2 GIC控制器基地址的获取方法

​ 直接查数据手册 Table 2-1. System memory map

​ 可以知道gic的基地址是0xA0000

​ 对于gic控制器还有另一种方法,通过 CP15查询:

​ mrc p15, 4, r0, c15, c0, 0

​ 将gic的基地址通过mrc指令读取到r0寄存器。

1.4.3 GIC的初始化

​ 通过CP15获取GIC的基地址,读取GICD_TYPER寄存器获得中断的数目,往GICD_ ICENABLERn寄存器写入0xFFFFFFFF禁用所有的SGI,PPI和SPI。通过GICC_PMR设置优先级等级,设置为0xF8;将GICC_BPR设置为2,这允许各个优先级进行抢占。 最后使能group0的distributor和CPU interface。

​ 代码在**裸机Git仓库 NoosProgramProject/(11_GPIO中断/008_exception/gic.c)**目录内:

void gic_init(void)
{
	u32 i, irq_num;

	GIC_Type *gic = get_gic_base();

	/* the maximum number of interrupt IDs that the GIC supports */
	irq_num = (gic->D_TYPER & 0x1F) + 1;

	/* On POR, all SPI is in group 0, level-sensitive and using 1-N model */
	
	/* Disable all PPI, SGI and SPI */
	for (i = 0; i < irq_num; i++)
	  gic->D_ICENABLER[i] = 0xFFFFFFFFUL;

	/* The priority mask level for the CPU interface. If the priority of an 
	 * interrupt is higher than the value indicated by this field, 
	 * the interface signals the interrupt to the processor.
	 */
	gic->C_PMR = (0xFFUL << (8 - 5)) & 0xFFUL;

	/* No subpriority, all priority level allows preemption */
	gic->C_BPR = 7 - 5;
	
	/* Enables the forwarding of pending interrupts from the Distributor to the CPU interfaces.
	 * Enable group0 distribution
	 */
	gic->D_CTLR = 1UL;
	
	/* Enables the signaling of interrupts by the CPU interface to the connected processor
	 * Enable group0 signaling 
	 */
	gic->C_CTLR = 1UL;
}

1.4.4 中断异常处理汇编部分

​ 在异常向量表偏移为0x18的地方将pc设置为IRQ_Handler标号的位置,跳转到IRQ_Handler标号位置执行,处理器处于中断模式,lr_irq保存了被中断模式中的下一条即将执行的指令的地址,将lr减去4,将r0-r12和lr保存在栈上,用bl指令调用C函数handle_irq_c,C函数返回来后将r0-r12从栈上弹出,栈上的lr弹出到PC,并将SPSR拷贝到CPSR,返回被打断的指令继续执行。在reset handler里需要设置好irq模式的栈,这样在中断模式里才可以调用C函数,同时调用cpsie i打开中断。使用如下两条指令设置异常向量的基地址

ldr r0, =_vector_table
	mcr p15, 0, r0, c12, c0, 0  /* set VBAR, Vector Base Address Register*/

​ 汇编部分代码如下所示代码如下**裸机Git仓库 NoosProgramProject/(11_GPIO中断/008_exception/**008_exception\start.S)文件:

.text
.global  _start, _vector_table
_start:
_vector_table:
	ldr 	pc, =Reset_Handler			 /* Reset				   */
	ldr 	pc, =Undefined_Handler		 /* Undefined instructions */
	ldr 	pc, =SVC_Handler			 /* Supervisor Call 	   */
	b halt//ldr 	pc, =PrefAbort_Handler		 /* Prefetch abort		   */
	b halt//ldr 	pc, =DataAbort_Handler		 /* Data abort			   */
	.word	0							 /* RESERVED			   */
	ldr 	pc, =IRQ_Handler			 /* IRQ interrupt		   */
	b halt//ldr 	pc, =FIQ_Handler			 /* FIQ interrupt		   */
………
.align 2
IRQ_Handler:
	/* 执行到这里之前:
	 * 1. lr_irq保存有被中断模式中的下一条即将执行的指令的地址
 * 2. SPSR_irq保存有被中断模式的CPSR
	 * 3. CPSR中的M4-M0被设置为10010, 进入到irq模式
	 * 4. 跳到0x18的地方执行程序 
	 */

	/* 保存现场 */
	/* 在irq异常处理函数中有可能会修改r0-r12, 所以先保存 */
	/* lr-4是异常处理完后的返回地址, 也要保存 */
	sub lr, lr, #4
	stmdb sp!, {r0-r12, lr}  
	
	/* 处理irq异常 */
	bl handle_irq_c
	
	/* 恢复现场 */
	ldmia sp!, {r0-r12, pc}^  /* ^会把spsr_irq的值恢复到cpsr里 */	
.align 2
Reset_Handler:
	/* Reset SCTlr Settings */
	mrc 	p15, 0, r0, c1, c0, 0	  /* read SCTRL, Read CP15 System Control register		*/
	bic 	r0,  r0, #(0x1 << 13)	  /* Clear V bit 13 to use normal exception vectors  	*/
	bic 	r0,  r0, #(0x1 << 12)	  /* Clear I bit 12 to disable I Cache					*/
	bic 	r0,  r0, #(0x1 <<  2)	  /* Clear C bit  2 to disable D Cache					*/
	bic 	r0,  r0, #(0x1 << 2)	  /* Clear A bit  1 to disable strict alignment 		*/
	bic 	r0,  r0, #(0x1 << 11)	  /* Clear Z bit 11 to disable branch prediction		*/
	bic 	r0,  r0, #0x1			  /* Clear M bit  0 to disable MMU						*/
	mcr 	p15, 0, r0, c1, c0, 0	  /* write SCTRL, Write to CP15 System Control register	*/

    cps     #0x1B                /* Enter undef mode                */
    ldr     sp, =0x80300000     /* Set up undef mode stack      */

    cps     #0x12                /* Enter irq mode                */
    ldr     sp, =0x80400000     /* Set up irq mode stack      */

    cps     #0x13                /* Enter Supervisor mode         */
    ldr     sp, =0x80200000     /* Set up Supervisor Mode stack  */

	ldr r0, =_vector_table
	mcr p15, 0, r0, c12, c0, 0  /* set VBAR, Vector Base Address Register*/
	//mrc p15, 0, r0, c12, c0, 0  //read VBAR

	bl clean_bss

	bl system_init
	cpsie	i					 /* Unmask interrupts			  */

	bl main

halt:
	b  halt


clean_bss:
	/* 清除BSS段 */
	ldr r1, =__bss_start
	ldr r2, =__bss_end
	mov r3, #0
clean:
	cmp r1, r2
	strlt r3, [r1]
	add r1, r1, #4
	blt clean
	
	mov pc, lr

1.4.5 中断异常处理C函数部分

​ 获取到gic的基地址后,读取GICC_IAR获得中断号,根据中断号调用对应中断号的irq_handler函数,该函数是用户通过request_irq注册的中断处理函数,然后往GICC_EOIR写入中断号清除掉中断。

​ 代码在裸机Git仓库 NoosProgramProject/(11_GPIO中断/008_exception\gic.c)

void handle_irq_c(void)
{
	int nr;

	GIC_Type *gic = get_gic_base();
	/* The processor reads GICC_IAR to obtain the interrupt ID of the
	 * signaled interrupt. This read acts as an acknowledge for the interrupt
	 */
	nr = gic-> C_IAR;
	printf("irq %d is happened\r\n", nr);
irq_table[nr].irq_handler(nr, irq_table[nr].param);

	/* write GICC_EOIR inform the CPU interface that it has completed 
	 * the processing of the specified interrupt 
	 */
	gic->C_EOIR = nr;
}

1.4.6 GPIO中断初始化和安装中断处理程序

​ 对于KEY1,将GPIO5_01通过EDGE_SEL设置成双边沿触发,通过IMR对应位设置为1打开中断,为了防止误触发将ISR对应位写1清除掉中断。然后调用request_irq注册对应中断的中断处理函数,对于GPIO5_01是key_gpio5_handle_irq,中断处理函数里根据按键按下和松开分别在串口打印,并且按下时绿灯点亮,松开时绿灯熄灭,并且往ISR对应位写1清掉中断,否则会一直触发中断。对于KEY2,和KEY1类似,按下和松开只会在串口进行打印。

​ 代码在裸机Git仓库 NoosProgramProject/(11_GPIO中断/008_exception\main.c)

void key_gpio5_handle_irq(void)
{
	/* read GPIO5_DR to get GPIO5_IO01 status*/
	if((GPIO5->DR >> 1 ) & 0x1) {
		printf("key 1 is release\r\n");
		/* led off, set GPIO5_DR to configure GPIO5_IO03 output 1 */
		GPIO5->DR |= (1<<3); //led on
	} else {
		printf("key 1 is press\r\n");
		/* led on, set GPIO5_DR to configure GPIO5_IO03 output 0 */
		GPIO5->DR &= ~(1<<3); //led off
	}
	/* write 1 to clear GPIO5_IO03 interrput status*/
	GPIO5->ISR |= (1 << 1);
}
void key_gpio4_handle_irq(void)
{
	/* read GPIO4_DR to get GPIO4_IO014 status*/
	if((GPIO4->DR >> 14 ) & 0x1)
		printf("key 2 is release\r\n");
	else
		printf("key 2 is press\r\n");
	/* write 1 to clear GPIO4_IO014 interrput status*/
	GPIO4->ISR |= (1 << 14);
}

void key_irq_init(void)
{
	/* if set detects any edge on the corresponding input signal*/
	GPIO5->EDGE_SEL |= (1 << 1);
	/* if set 1, unmasked, Interrupt n is enabled */
	GPIO5->IMR |= (1 << 1);
	/* clear interrupt first to avoid unexpected event */
	GPIO5->ISR |= (1 << 1);

	GPIO4->EDGE_SEL |= (1 << 14);
	GPIO4->IMR |= (1 << 14);
	GPIO4->ISR |= (1 << 14);

	request_irq(GPIO5_Combined_0_15_IRQn, (irq_handler_t)key_gpio5_handle_irq, NULL);
	request_irq(GPIO4_Combined_0_15_IRQn, (irq_handler_t)key_gpio4_handle_irq, NULL);
}

1.4.7 特定中断号的中断使能和禁止

​ 以中断号调用gic_enable_irq,对应的中断在GIC中打开,通过往GICD_ISENABLERn对应的位写入1打开。以中断号调用gic_ disable _irq,对应的中断在GIC中关闭,通过往GICD_ICENABLERn对应的位写入1关闭。

​ 代码裸机Git仓库 NoosProgramProject/(11_GPIO中断/008_exception/gic.c)

void gic_enable_irq(IRQn_Type nr)
{
	GIC_Type *gic = get_gic_base();

	/* The GICD_ISENABLERs provide a Set-enable bit for each interrupt supported by the GIC.
	 * Writing 1 to a Set-enable bit enables forwarding of the corresponding interrupt from the
	 * Distributor to the CPU interfaces. Reading a bit identifies whether the interrupt is enabled.
	 */
	gic->D_ISENABLER[nr >> 5] = (uint32_t)(1UL << (nr & 0x1FUL));

}

void gic_disable_irq(IRQn_Type nr)
{
	GIC_Type *gic = get_gic_base();

	/* The GICD_ICENABLERs provide a Clear-enable bit for each interrupt supported by the
	 * GIC. Writing 1 to a Clear-enable bit disables forwarding of the corresponding interrupt from
     * the Distributor to the CPU interfaces. Reading a bit identifies whether the interrupt is enabled. 
	 */
	gic->D_ICENABLER[nr >> 5] = (uint32_t)(1UL << (nr & 0x1FUL));
}

1.4.8 主函数调用

​ 调用system_init_irq_table初始化中断跳转表,key_irq_init初始化按键中断的GPIO配置和注册中断处理函数,通过gic_init初始化GIC控制器,最后通过gic_enable_irq使能按键对应GIC中断号使用的中断。

​ 代码裸机Git仓库 NoosProgramProject/(11_GPIO中断/008_exception/main.c)

void system_init()
{
	init_pins();
	led_gpio_init();
	led_ctl(0);//turn off led
	boot_clk_gate_init();
	boot_clk_init();
	uart1_init();
	puts("hello world\r\n");
	system_init_irq_table();
	key_irq_init();
	gic_init();
	gic_enable_irq(GPIO5_Combined_0_15_IRQn);
	gic_enable_irq(GPIO4_Combined_0_15_IRQn);
}

1.4.9 参考章节《4-1.4编译程序》编译程序

​ 进入 **裸机Git仓库 NoosProgramProject/(11_GPIO中断/008_exception)**源码目录

1.4.10 参考章节《4-1.4映像文件烧写、运行》烧写、运行程序

​ 此时观察串口打印

​ 按下KEY1,绿灯点亮,松开,绿灯熄灭,同时串口会打印按下松开的信息。按下或者松开KEY2,串口会打印出KEY2按下松开的信息。串口打印如下所示:

1.5 按键中断编程示例二

1.5.1 按键中断程序编程

​ 此节代码在**裸机Git仓库 NoosProgramProject/(11_GPIO中断/011_gpio_eint)**目录内。

1.5.1.1 编写start.S

1.编写异常向量表

.text 	
.global  _start, _vector_table 	
_start: 	
_vector_table: 		
	ldr pc, =Reset_Handler			     
    /* Reset				   */ 		
    b halt							
    /* Undefined instructions */ 		
    b halt			 				 
    /* Supervisor Call 	      */ 		
    b halt		 					 
    /* Prefetch abort		  */ 		
    b halt		 					 
    /* Data abort			  */ 		
    .word	0						
    /* RESERVED			      */ 		
    ldr pc, =IRQ_Handler			     
    /* IRQ interrupt		  */ 		
    b halt			 				 
    /* FIQ interrupt		  */  	
.align 2 
IRQ_Handler: 		
	b halt  	
.align 2 	
    Reset_Handler: 		
	b halt  	
halt: 		
	b halt

​ 上电后,程序从_start地址开始执行,05行代码对应的是0x00地址,06行代码对应的是0x4,依次类推,11行代码对应的是0x18,与11.1.2章节介绍的异常向量表对应。在编程时,通常在异常向量表中放一条跳转指令,跳转去执行更复杂的操作。比如在IRQ_Handler函数中需要保存现场,等处理完异常后又需要恢复现场。

​ 程序在0x00地址通过ldr指令把Reset Handler的地址赋给pc,CPU跳转到Reset Handler运行,发生中断时,CPU跳转到IRQ_Handler运行。

2.编写复位中断函数

	Reset_Handler:
		/* Reset SCTlr Settings */
		mrc 	p15, 0, r0, c1, c0, 0	  	/* read SCTRL, Read CP15 System Control register	*/
		bic 	r0,  r0, #(0x1 << 13)	  	/* Clear V bit 13 to use normal exception vectors  	*/
		bic 	r0,  r0, #(0x1 << 12)	  	/* Clear I bit 12 to disable I Cache					*/
		bic 	r0,  r0, #(0x1 <<  2)	  	/* Clear C bit  2 to disable D Cache				*/
		bic 	r0,  r0, #(0x1 << 1)	  	  	/* Clear A bit  1 to disable strict alignment 		*/
		bic 	r0,  r0, #(0x1 << 11)	  	/* Clear Z bit 11 to disable branch prediction		*/
		bic 	r0,  r0, #0x1			  	/* Clear M bit  0 to disable MMU					*/
		mcr 	p15, 0, r0, c1, c0, 0	  	/* write SCTRL, Write to CP15 System Control register	*/

		cps     #0x1B				/* Enter undef mode               */
		ldr     sp, =0x80300000     	/* Set up undef mode stack      	*/

		cps     #0x12				/* Enter irq mode                	*/
		ldr     sp, =0x80400000	    /* Set up irq mode stack      		*/

		cps     #0x13				/* Enter Supervisor mode         	*/
		ldr     sp, =0x80200000     	/* Set up Supervisor Mode stack  	*/

		ldr r0, =_vector_table
		mcr p15, 0, r0, c12, c0, 0  		/* set VBAR, Vector Base Address Register*/
		//mrc p15, 0, r0, c12, c0, 0  		//read VBAR

		bl clean_bss
		
		bl system_init
		cpsie	i					 /* Unmask interrupts			  	*/

		bl main

	halt:
		b  halt


	clean_bss:
		/* 清除BSS段 */
		ldr r1, =__bss_start
		ldr r2, =__bss_end
		mov r3, #0
	clean:
		cmp r1, r2
		strlt r3, [r1]
		add r1, r1, #4
		blt clean
		
		mov pc, lr

​ 在Reset_Handler中,需要完成关闭Icache、Dcache、MMU等操作。通过CPS指令改变处理器状态,比如cps #0x1B进入undef mode,然后设置undef mode状态下的栈地址。CPS #0x12进入irq mode,设置irq mode状态下的栈地址。CPS #0x13进入Supervisor mode,设置Supervisor mode状态下的栈地址。设置不同模式下栈地址的目的是在调用C函数时,总有一些寄存器的值需要保存下来,如果直接跳转到子函数里去执行,很有可能就被破坏,因为子函数可能也会用到这些寄存器。

3.编写IRQ服务函数

IRQ_Handler: 	
/* 执行到这里之前: 	 
* 1. lr_irq保存有被中断模式中的下一条即将执行的指令的地址 	 
* 2. SPSR_irq保存有被中断模式的CPSR 	 
* 3. CPSR中的M4-M0被设置为10010, 进入到irq模式 	 
* 4. 跳到0x18的地方执行程序  	 */  	
/* 保存现场 */ 	
/* 在irq异常处理函数中有可能会修改r0-r12, 所以先保存 */ 	
/* lr-4是异常处理完后的返回地址, 也要保存 */ 	
sub lr, lr, #4 	
stmdb sp!, {r0-r12, lr}   	 	

/* 处理irq异常 */ 	
bl handle_irq_c 	 	

/* 恢复现场 */ 	
ldmia sp!, {r0-r12, pc}^  /* ^会把spsr_irq的值恢复到cpsr里 */	

​ 在IRQ_Handler中,需要保存现场,因为在handle_irq_c函数中可能会有修改r0-r12寄存器。等异常处理完后,再恢复现场。保存现场、恢复现场都离不开栈,保存现场需要把寄存器的值一个个放入栈中,恢复现场则从栈中一个个读取寄存器的值。所以再Reset_Handler中提前设置IRQ mode下的栈地址,当然,在IRQ_Handler中在设置栈也可以,看个人习惯吧。

1.5.1.2 编写interrupt.c

​ 1.初始化GIC、使能中断并设置中断触发方式

void key_exit_init(void)
	{
		GPIO5_IMR								= (volatile unsigned int *)(0x20AC014);
		GPIO5_EDGE_SEL							= (volatile unsigned int *)(0x20AC01C);
		GPIO5_ISR								= (volatile unsigned int *)(0x20AC018);
		GPIO5_DR                                = (volatile unsigned int *)(0x20AC000);
		
		GPIO4_IMR								= (volatile unsigned int *)(0x20A8014);
		GPIO4_EDGE_SEL							= (volatile unsigned int *)(0x20A801C);
		GPIO4_ISR								= (volatile unsigned int *)(0x20A8018);
		GPIO4_DR								= (volatile unsigned int *)(0x20A8000);

		gic_init();
		gic_enable_irq(GPIO5_Combined_0_15_IRQn);
		gic_enable_irq(GPIO4_Combined_0_15_IRQn);

		/* 设置GPIOx_EDGE_SEL寄存器
		 * GPIO_EDGE_SEL bit is set, then a rising edge or falling edge in the corresponding
		 *      signal generates an interrupt.
		 * GPIO5_EDGE_SEL  0x20AC01C
		 * bit[1] = 0b1
		 * GPIO4_EDGE_SEL  0x20A801C
		 * bit[14] = 0b1
		 */
		*GPIO5_EDGE_SEL |= (1<<1);
		*GPIO4_EDGE_SEL |= (1<<14);

		/* 设置GPIOx_IMR寄存器
		 * GPIO_IMR contains masking bits for each interrupt line.
		 * GPIO5_IMR  0x20AC014
		 * bit[1] = 0b1
		 * GPIO4_IMR  0x20A8014
		 * bit[14] = 0b1
		 */
		*GPIO5_IMR |= (1<<1);
		*GPIO4_IMR |= (1<<14);
	}

​ 在key_exit_init函数中,首先调用gic_init函数对GIC中断控制器初始化,然后调用gic_enable_irq函数允许GPIO5_00~GPIO5_15和GPIO4_00~GPIO4_15管脚中断,这两个函数均根据官方SDK修改,具体的实现就是设置GIC寄存器,在这里不在详细分析。

​ 设置GPIOx_EDGE_SEL选择双边沿触发中断,再设置GPIOx_IMR寄存器,使能GPIO5_1(key1)、GPIO4_14(key2)管脚中断。

​ 2、编写中断服务C语言中断服务函数

void handle_irq_c(void)
	{
		int nr;

		GIC_Type *gic = get_gic_base();
		/* The processor reads GICC_IAR to obtain the interrupt ID of the
		 * signaled interrupt. This read acts as an acknowledge for the interrupt
		 */
		nr = gic->C_IAR;
		printf("irq %d is happened\r\n", nr);

		switch(nr)
		{
			case GPIO5_Combined_0_15_IRQn:
			{
				/* read GPIO5_DR to get GPIO5_IO01 status*/
				if((*GPIO5_DR >> 1 ) & 0x1) {
					printf("key 1 is release\r\n");
					/* led off, set GPIO5_DR to configure GPIO5_IO03 output 1 */
					led_ctl(0);
				} else {
					printf("key 1 is press\r\n");
					/* led on, set GPIO5_DR to configure GPIO5_IO03 output 0 */
					led_ctl(1);
				}
				/* write 1 to clear GPIO5_IO03 interrput status*/
				*GPIO5_ISR |= (1 << 1);
				break;
			}
			
			case GPIO4_Combined_0_15_IRQn:
			{
				/* read GPIO4_DR to get GPIO4_IO014 status*/
				if((*GPIO4_DR >> 14 ) & 0x1)
				{
					printf("key 2 is release\r\n");
					led_ctl(0);
				}
				else
				{
					printf("key 2 is press\r\n");
					led_ctl(1);
				}
				/* write 1 to clear GPIO4_IO014 interrput status*/
				*GPIO4_ISR |= (1 << 14);
				break;
			}

			default:
				break;
		}

		/* write GICC_EOIR inform the CPU interface that it has completed 
		 * the processing of the specified interrupt 
		 */
		gic->C_EOIR = nr;
	}

​ 在handle_irq_c函数中,通过读取GICC_IAR寄存器确定中断ID,得到中断ID后再判断此时管脚的电平状态,最后通过串口把中断ID打印出来,中断处理完成后不要忘了设置GPIOx_ISR寄存器,清除中断标志位,注意这是GPIO模块内的中断标志位寄存器,然后把中断ID写入GICC_EOIR寄存器,表示中断处理完成,这里时GIC中断控制器内的寄存器,清中断标志位一定要清除干净,注意分清楚GPIO内的中断标志位和GIC中断控制器内的中断标志寄存器。

1.5.2 上机实验

1.5.2.1 修改官方SDK文件

#include "gic.h"
#include "my_printf.h"

GIC_Type * get_gic_base(void)
{
	GIC_Type *dst;

	__asm volatile ("mrc p15, 4, %0, c15, c0, 0" : "=r" (dst)); 

	return dst;
}

void gic_init(void)
{
	u32 i, irq_num;

	GIC_Type *gic = get_gic_base();

	/* the maximum number of interrupt IDs that the GIC supports */
	irq_num = (gic->D_TYPER & 0x1F) + 1;

	/* On POR, all SPI is in group 0, level-sensitive and using 1-N model */
	
	/* Disable all PPI, SGI and SPI */
	for (i = 0; i < irq_num; i++)
	  gic->D_ICENABLER[i] = 0xFFFFFFFFUL;

	/* The priority mask level for the CPU interface. If the priority of an 
	 * interrupt is higher than the value indicated by this field, 
	 * the interface signals the interrupt to the processor.
	 */
	gic->C_PMR = (0xFFUL << (8 - 5)) & 0xFFUL;
	
	/* No subpriority, all priority level allows preemption */
	gic->C_BPR = 7 - 5;
	
	/* Enables the forwarding of pending interrupts from the Distributor to the CPU interfaces.
	 * Enable group0 distribution
	 */
	gic->D_CTLR = 1UL;
	
	/* Enables the signaling of interrupts by the CPU interface to the connected processor
	 * Enable group0 signaling 
	 */
	gic->C_CTLR = 1UL;
}

void gic_enable_irq(IRQn_Type nr)
{
	GIC_Type *gic = get_gic_base();

	/* The GICD_ISENABLERs provide a Set-enable bit for each interrupt supported by the GIC.
	 * Writing 1 to a Set-enable bit enables forwarding of the corresponding interrupt from the
	 * Distributor to the CPU interfaces. Reading a bit identifies whether the interrupt is enabled.
	 */
	gic->D_ISENABLER[nr >> 5] = (uint32_t)(1UL << (nr & 0x1FUL));

}

void gic_disable_irq(IRQn_Type nr)
{
	GIC_Type *gic = get_gic_base();

	/* The GICD_ICENABLERs provide a Clear-enable bit for each interrupt supported by the
	 * GIC. Writing 1 to a Clear-enable bit disables forwarding of the corresponding interrupt from
	 * the Distributor to the CPU interfaces. Reading a bit identifies whether the interrupt is enabled. 
	 */
	gic->D_ICENABLER[nr >> 5] = (uint32_t)(1UL << (nr & 0x1FUL));
}

​ gic_init函数作用是初始化GIC控制器

​ gic_enable_irq函数作用是使能GPIO管脚中断

​ gic_disable_irq函数作用是屏蔽GPIO管脚中断

​ get_gic_base函数作用是得到GIC寄存器地址,通过GIC寄存器地址就可以访问GIC内部的寄存器。

1.5.2.2 修改Makefile

PREFIX=arm-linux-gnueabihf-
CC=$(PREFIX)gcc
LD=$(PREFIX)ld
AR=$(PREFIX)ar
OBJCOPY=$(PREFIX)objcopy
OBJDUMP=$(PREFIX)objdump

INCLUDEDIR 	:= $(shell pwd)/include
CFLAGS 		:= -Wall
CPPFLAGS   	:= -nostdinc -fno-builtin -I$(INCLUDEDIR)
LDFLAGS         := -L /usr/arm/gcc-linaro-6.2.1-2016.11-x86_64_arm-linux-gnueabihf/lib/gcc/arm-linux-gnueabihf/6.2.1 -lgcc
objs :=  start.o main.o led.o key.o interrupt.o uart.o eabi_compat.o my_printf.o gic.o

TARGET := eint

$(TARGET).img : $(objs)
	$(LD) -T imx6ull.lds -o $(TARGET).elf $^ $(LDFLAGS)
	$(OBJCOPY) -O binary -S $(TARGET).elf  $(TARGET).bin
	$(OBJDUMP) -D -m arm  $(TARGET).elf  > $(TARGET).dis	
	./tools/mkimage -n ./tools/imximage.cfg.cfgtmp -T imximage -e 0x80100000 -d $(TARGET).bin $(TARGET).imx
	dd if=/dev/zero of=1k.bin bs=1024 count=1
	cat 1k.bin $(TARGET).imx > $(TARGET).img

%.o:%.c
	${CC} $(CPPFLAGS) $(CFLAGS) -c -o $@ $<

%.o:%.S
	${CC} $(CPPFLAGS) $(CFLAGS) -c -o $@ $<

clean:
	rm -f $(TARGET).dis  $(TARGET).bin $(TARGET).elf $(TARGET).imx $(TARGET).img *.o

1.5.3 参考章节《4-1.4编译程序》编译程序

​ 进入裸机Git仓库 NoosProgramProject/(11_GPIO中断/011_gpio_eint) 源码目录进行编译。

1.5.3.1 参考章节《4-1.4映像文件烧写、运行》烧写、运行程序

​ 此时观察串口打印

​ 分别按下松开key1、key2,中断ID打印出来,对应的Led也亮灭,在MCIMX6Y2.h文件中查询GPIO5_Combined_0_15_IRQn、GPIO4_Combined_0_15_IRQn,分别对应106、104,实验成功。

第十二章:定时器编程

第十二章:定时器编程

12. 定时器编程

​ 参考资料:

12.1 GPT定时器介绍

​ GPT具有32位递增计数器。可以将外部引脚上的事件通过定时器计数器捕获到寄存器中。触发事件可以为上升沿或下降沿。当定时器达到设定的值时,GPT还可以在输出引脚上产生事件,并产生中断。GPT具有12位预分频器,该分频器可以对多个时钟源的时钟进行分频。GPT框图如下:

​ 特性

12.1.1 时钟源选择

Clock name Clock Root Description
ipg_clk ipg_clk_root Peripheral clock
ipg_clk_32k ckil_sync_clk_root Low-frequency reference clock (32 kHz)
ipg_clk_highfreq perclk_clk_root High-frequency reference clock
ipg_clk_s ipg_clk_root Peripheral access clock

​ 上表是GPT模块会使用到的时钟,ipg_clk提供了Peripheral的时钟,ipg_clk_perclk提供了gpt控制器的时钟,ipg_clk_32k提供低频参考时钟,ipg_clk_highfreq提供了高频参考时钟,ipg_clk_s提供了访问控制器寄存器所需的时钟。

​ 从上图可以看出,可以从4个时钟源中选择输入到预分频器的时钟。分别为:高频参考时钟(ipg_clk_highfreq),低频参考时钟(ipg_clk_32k),外围时钟(ipg_clk)和外部时钟(GPT_CLK)或者晶体振荡器时钟(ipg_clk_24M)。由于外部时钟(GPT_CLK)或者晶体振荡器时钟(ipg_clk_24M)只能选择一个,所以总共是四个。由于这里暂时不关注lower power mode的内容,实验里将ipg_clk作为预分频器的时钟源,其它的在这里不关注。ipg_clk怎么设置呢?需要查看时钟树。

​ 如上图所示,PERCLK_CLK_ROOT可以来源于IPG_CLK_ROOT和OSC,后续实验选择了IPG_CLK_ROOT,经过CSCMR1[PERCLK_PODF](实验里设置为0,对应分频值1)分频,IPG_CLK_ROOT来源于AHB_CLK_ROOT分频CBCDR[IPG_PODF] (实验里设置为1,对应分频2),AHB_CLK_ROOT实验里设置来源于SYS PLL PFD2分频CBCDR[AHB_PODF](实验里设置为2,对应分频3),SYS PLL PFD2的大小为396M。所以PERCLK_CLK_ROOT的大小为66M。

​ PERCLK_CLK_ROOT = 396M / 3 / 2 = 66M

12.1.2 时钟源选择的操作流程

​ GPT_CR寄存器中的CLKSRC字段选择时钟源。CLKSRC字段仅在禁用GPT(EN = 0)后才能更改该值。

​ 更改时钟源时要遵循的软件顺序为:

1.通过在GPT_CR寄存器中设置EN = 0来禁用GPT。

2.禁用GPT中断寄存器(GPT_IR)。

3.将输出模式配置为未连接/断开连接—往GPT_CR中的OM1,OM2,OM3写0。

4.禁用输入捕获模式-往GPT_CR的IM1和IM2中写入零

5.在GPT_CR寄存器中将时钟源CLKSRC更改为所需的值。

6.将GPT_CR寄存器中的SWR位置1。

7.清除GPT状态寄存器(GPT_SR)(该寄存器是往响应位写1清0)。

8.在GPT_CR寄存器中设置ENMOD = 1,以使GPT计数器为0x00000000。

9.在GPT_CR寄存器中启用GPT(EN = 1)。

10.启用GPT中断寄存器(GPT_IR)

12.1.3 GPT的计数模式

① 重新启动计数模式(Restart mode)

​ 在重启模式下(可通过GPT控制寄存器GPT_CR选择),当计数器达到比较值时,计数器将复位并从0x00000000重新开始计数。重新启动功能仅与比较通道1相关联。对通道1的比较寄存器的任何写操作都将复位GPT计数器。这样做是为了避免在进行计数时将比较值从较高的值更改为较低的值时可能丢失比较事件。对于其他两个比较通道,当发生比较事件时,计数器不会复位。

② 自由运行模式(free-run mode)

​ 在自由运行模式下,当所有三个通道发生比较事件时,计数器不会复位;而是,计数器继续计数直到0xffffffff,然后翻转(变为0x00000000)。

12.1.4 GPT的操作

​ 通用定时器(GPT)具有一个计数器(GPT_CNT),该计数器是32位递增计数器,在由软件启用该计数器后(EN = 1)开始计数。

​ •如果禁用了GPT计时器(EN = 0),则主计数器和预分频器计数器将冻结其当前计数值。当EN位置1且计数器再次使能时,ENMOD位确定GPT counter的值。

​ •如果将ENMOD位置1,则当启用GPT(EN = 1)时,主计数器和预分频器计数器值将重置为0。

​ •如果将ENMOD位设置为0,则当再次启用GPT(EN = 1)时,主计数器和预分频器计数器将从其冻结值重新开始计数。

​ •如果将GPT编程为在低功耗模式(STOP / WAIT)下被禁用,则当GPT进入低功耗模式时,主计数器和预分频器计数器将冻结在其当前计数值。当GPT退出低功耗模式时,无论ENMOD位值如何,主计数器和预分频器计数器都将从其冻结值开始计数。请注意,处理器可以随时读取GPT_CNT,并且两个输入捕获通道都使用相同的计数器(GPT_CNT)。

​ •硬件复位将所有GPT寄存器复位为各自的复位值。除输出比较寄存器(OCR1,OCR2,OCR3)以外的所有寄存器的值均为0x0。比较寄存器复位为0xFFFF_FFFF。

​ •软件复位(GPT_CR控制寄存器中的SWR位)将复位所有寄存器位,除了EN,ENMOD,STOPEN,WAITEN和DBGEN位。这些位的状态不受软件复位的影响。请注意,禁用GPT时可以进行软件复位操作。

12.1.5 GPT的输入捕获

​ 有两个输入捕获通道,每个输入捕获通道都有一个专用的捕获引脚,捕获寄存器和输入边沿检测/选择逻辑。每个输入捕获功能都有一个状态标记位,并且可以向处理器发出中断服务请求。当输入捕获引脚上发生选定的边沿转换时,GPT_CNT的内容被捕捉到相应的捕捉寄存器中,并设置适当的中断状态标志。如果检测到转换(如果在中断寄存器中)相应的使能位置1,则可以生成中断请求。可以将捕获设置为发生在输入引脚的上升沿,下降沿,上升沿和下降沿,或者禁用捕获。事件与选择运行计数器的时钟同步。只有那些在上一个记录的转换之后至少一个时钟周期(选择运行计数器的时钟源)发生的转换才能保证触发捕获事件。输入跳变的锁存最多可能有一个时钟周期的不确定性。可以随时读取输入捕获寄存器,而不会影响它们的值。具体时序图如下所示:

12.1.6 GPT的输出比较

​ 三个输出比较通道使用与输入捕捉通道相同的计数器(GPT_CNT)。当输出比较寄存器的值与GPT_CNT中的值匹配时,将输出比较状态标志置1,并产生中断(如果在中断寄存器中设置了相应的位)。因此,根据模式位,输出比较定时器的引脚将被置位(set),清除(clear),翻转(toggle),没有影响,或在一个输入时钟周期内提供低电平有效脉冲(受焊盘允许的最大频率的限制)。

​ 还有一个“强制比较(forced-compare)”功能,允许软件在需要时生成比较事件,不需要计数器值等于比较值的条件。强制比较的结果所采取的操作与发生输出比较匹配时的操作相同,不同之处在于不设置状态标记位并且不会产生中断。强制比较的通道在写入force-compare位后立即采取设置的措施。这些位是自动清除的,读的话一直零。下图是输出比较时的时序图:

12.1.7 GPT的中断

​ GPT可以产生6种不同的中断。如果选定的用于运行计数器的时钟可用,则可以在低功耗和调试模式下生成所有中断。

​ •翻转中断

​ 当GPT计数器达到0xffffffff,然后重新设置为0x00000000并继续计数时,将产生翻转中断。翻转中断通过GPT_IR寄存器中的ROVIE位来使能。相关的状态位是GPT_SR寄存器中的ROV位。

​ •输入捕获中断1、2

​ 捕获事件发生后,相应的输入捕获通道会产生一个中断。“捕获事件”中断通过IF2IE和IF1IE位(在GPT_IR寄存器中)使能;相应的状态位是IF2和IF1(在GPT_SR寄存器中)。由于捕获事件而导致的计数器值的捕获不受挂起的捕获中断的影响。当发生捕获事件时,无论是否已处理捕获通道的中断,捕获寄存器都会更新新的捕获到计数器值。

​ •输出比较中断1、2、3

​ 比较事件发生后,相应的输出比较通道会产生一个中断。“比较事件”中断由OF3IE,OF2IE和OF1IE位(在GPT_IR寄存器中)使能;相应的状态位是OF3,OF2和OF1(在GPT_SR寄存器中)。 “强制比较(Force compare)”不会产生中断。

​ 还存在一条cumulative中断线,每当上述任何中断发生时,它就会被置为有效。cumulative中断线没有相关的使能或状态位。

12.2 GPT寄存器介绍

12.2.1 GPT Control Register (GPTx_CR)

​ GPT控制寄存器

​ bit31-29 分别为FO3-1,写0没有影响,写1导致相应的输出引脚状态变化,OFn标记位不会设置

​ bit28-26,bit25-23,bit22-20 分别为OM3,OM2和OM1,为000时与输出引脚断开,001表示翻转引脚状态,010时表示引脚清0,011时引脚置位,1xx时产生一个低脉冲

​ bit19-18,bit17-16,分比为IM2和IM1,00时表示捕获功能关闭,01时捕获上升沿,10时捕获下降沿,11时同时捕获上升和下降沿

​ bit15 SWR软件复位

​ bit10 EN_24M,硬件复位时,复位 EN_24M位,软件复位时不影响EN_24M位

​ bit9 FRR 0时为restart模式,1时为free-run模式

​ bit8-6,BLKSRC,时钟源选择位,000时表示与时钟源断开,001时Peripheral Clock (ipg_clk),010时为High Frequency Reference Clock (ipg_clk_highfreq),011 时为External Clock,100 时为Low Frequency Reference Clock (ipg_clk_32k),101时为Crystal oscillator as Reference Clock (ipg_clk_24M)

​ bit5 STOPEN,stop mode时GPT是否使能

​ bit4 DOZEEN,doze mode时GPT是否使能

​ bit3 WAITEN,wait mode时GPT是否使能

​ bit2 DBGEN,debug mode时GPT是否使能

​ bit1 ENMOD,为0时关闭GPT时计数器时保持原有值,为1时关闭GPT时计数器值复位为0

​ bit0 EN,GPT使能位

12.2.2 GPT Prescaler Register (GPTx_PR)

​ GPT预分频寄存器

​ bit15-12,PRESCALER24M,选择24M crystal 时钟时的预分频值

​ bit12-0,选择其它时钟源时的预分频值

12.2.3 GPT Status Register (GPTx_SR)

​ GPT状态寄存器

​ 状态寄存器包含状态位,指示计数器翻转,或者输入通道和输出通道产生相应的事件

​ bit5,ROV,翻转标记位

​ bit4-3,IF2和IF1,输入通道捕获事件标记位

​ bit2-0,OF3-1,输出通道比较事件标记位

12.2.4 GPT Interrupt Register (GPTx_IR)

​ GPT中断寄存器

​ 翻转,输入和输出通道事件的中断使能位,与状态寄存器的位对应。

12.2.5 GPT Output Compare Register 1~3 (GPTx_OCR1~3)

​ GPT输出比较寄存器

​ GPTx_OCR1-3,总共3个输出比较寄存器,当计数器达到输出比较寄存器的值时,将在相应通道上产生事件,restart mode时,写入channael1 的比较寄存器会复位GPT计数器,写寄存器的值在一个时钟周期后生效,读寄存器的值会立即返回。

12.2.6 GPT Input Capture Register 1~2 (GPTx_ICR1~2)

​ GPT输入捕获寄存器

​ GPTx_ICR1-2,两个输入捕获寄存器,只读寄存器,用于保存相应输入捕获通道上一次捕获事件发生时计数器中的值。

12.2.7 GPT Counter Register (GPTx_CNT)

​ GPT计数器寄存器

​ 只读寄存器,GPT计数器的值,读不影响计数过程

12.3 GPT查询方式延时代码详解与测试

12.3.1 代码分析

​ 通过gpt_poll_init和gpt_poll_restart两个函数来实现,gpt_poll_init函数首先对GPT进行软件复位,设置gpt为restart模式,时钟源选择Peripheral Clock (ipg_clk)为66M,预分频值设置为0(即预分频值为1)。

​ gpt_poll_init代码在裸机Git仓库 NoosProgramProject/(12_定时器编程/009_timer_gpt_poll/gpt.c)

void gpt_poll_init(GPT_Type *base)
{
	/* bit15 SWR, Software reset*/
	base->CR |= (1 << 15);
	/* Wait reset finished. */
	while((base->CR >> 15) & 0x1) {
	}

	/*
	 *bit10: Enable 24 MHz clock input from crystal
	 *bit9: 0 restart mode, 1 free-run mode:set 0
	 *bit8-6: Clock Source select :001 Peripheral Clock (ipg_clk)
	 *bit5: GPT Stop Mode enable
	 *bit3: GPT Wait Mode enable.
	 *bit1: GPT Enable Mode
	 */	
	base->CR = (1 << 6) | (1 << 5) | (1 << 3) | (1 << 1);

	/*
	 *bit15-bit12:PRESCALER24M
	 *bit11-0:PRESCALER
	 */
	base->PR = 0;
}

​ gpt_poll_restart函数根据通道设置和延时时间设置输出比较寄存器,为了防止状态寄存器已经设置,先清除一下状态寄存器对应的位,再使能中断寄存器对应的位,然后使能GPT计数器,等待compare flag设置,设置后先关闭GPT计数器,禁止中断寄存器对应的位,最后往状态寄存器相应的位写1清除掉状态位。

​ 代码在裸机Git仓库 NoosProgramProject/(12_定时器编程/009_timer_gpt_poll/gpt.c)

void gpt_poll_restart(GPT_Type *base, enum gpt_comp_channel chan, unsigned int us)
{
	base->OCR[chan] =  USEC_TO_COUNT(us);
	/* write 1 to clear int status to avoid unexpected compare event*/
	base->SR |= (1 << chan);
	/* enable interrupt*/
	base->IR |= (1 << chan);
	/* gpt enable*/
	base->CR |= (1 << 0);
	/*wait for compare flag set*/
	while(!((base->SR >> chan) & 0x1)) 
		;
	/* gpt disable*/
	base->CR &= ~(1 << 0);
	/* disable interrupt*/
	base->IR &= ~(1 << chan);
	/* write 1 to clear int status*/
	base->SR |= (1 << chan);
}

​ 主函数

​ 使用gpt_poll_init初始化GPT1,不断调用gpt_poll_restart(GPT1, OUT_COMP1, 1000000);延时1s,并且依次点亮和关闭绿灯。

​ 代码在裸机Git仓库 NoosProgramProject/(12_定时器编程/009_timer_gpt_poll/main.c):

	gpt_poll_init(GPT1);
	while(1) {
		gpt_poll_restart(GPT1, OUT_COMP1, 1000000);
		GPIO5->DR &= ~(1<<3);//led on
		printf("led is on\r\n");
		gpt_poll_restart(GPT1, OUT_COMP1, 1000000);
		GPIO5->DR |= (1<<3);//led off
		printf("led is off\r\n");
	}

12.3.2 参考章节《4-1.4编译程序》编译程序

​ 进入 裸机Git仓库 NoosProgramProject/(12_定时器编程/009_timer_gpt_poll) 源码目录

12.3.3 参考章节《4-1.4映像文件烧写、运行》烧写、运行程序

​ 此时观察串口信息

​ 绿灯不断闪烁,每隔1s翻转状态。串口输出如下:

12.4 GPT中断方式延时代码详解与测试

12.4.1 GPT1中断号的确定

​ 查询数据手册chapter 3的Table3-1中断号表,gpt1如下所示:

​ gic中断号需要再这个序号基础上加上32,所以gpt1的gic中断号为55+32=87。

12.4.2 代码分析

12.4.2.1 通过gpt_init函数初始化gpt

​ 首先对GPT进行软件复位,设置gpt为restart模式,时钟源选择Peripheral Clock (ipg_clk)为66M,预分频值设置为0(即预分频值为1),输出比较寄存器根据延时值设置。

​ gpt_init代码在裸机Git仓库 NoosProgramProject/(12_定时器编程/009_timer_gpt_int/gpt.c)

/* assume use ipc clk which is 66MHz, 1us against to 66 count */
#define USEC_TO_COUNT(us) (us * 66 - 1)

void gpt_init(GPT_Type *base, enum gpt_comp_channel chan, int us)
{
	/* bit15 SWR, Software reset*/
	base->CR |= (1 << 15);
	/* Wait reset finished. */
	while((base->CR >> 15) & 0x1) {
	}

	/*
	 *bit10: Enable 24 MHz clock input from crystal
	 *bit9: 0 restart mode, 1 free-run mode:set 0
	 *bit8-6: Clock Source select :001 Peripheral Clock (ipg_clk)
	 *bit5: GPT Stop Mode enable
	 *bit3: GPT Wait Mode enable.
	 *bit1: GPT Enable Mode
	 */	
	base->CR = (1 << 6) | (1 << 5) | (1 << 3) | (1 << 1);

	/*
	 *bit15-bit12:PRESCALER24M
	 *bit11-0:PRESCALER
	 */
	base->PR = 0;

	/* GPTx_OCR1  bit31-0: Compare Value
	 * When the counter value equals the COMP bit field value, 
	 * a compare event is generated on Output Compare Channel 1.
	 */
	base->OCR[chan] = USEC_TO_COUNT(us);
}

12.4.2.2 gpt中断使能函数

​ 根据通道使能中断寄存器对应的位.

​ 代码在裸机Git仓库 NoosProgramProject/(12_定时器编程/09_timer_gpt_int/gpt.c)

void gpt_enable_interrupt(GPT_Type *base, enum gpt_interrupt_bit bit, int on)
{
	if (on)
		base->IR |= (1 << bit);
	else
		base->IR &= ~(1 << bit);
}

12.4.2.3 gpt运行使能函数

​ 设置控制寄存器的运行位。

​ 代码在裸机Git仓库 NoosProgramProject/(12_定时器编程/009_timer_gpt_int/gpt.c)

void gpt_run(GPT_Type *base, int on)
{
	/* bit0: GPT Enable */
	if (on)
		base->CR |= (1 << 0);
	else
		base->CR &= ~(1 << 0);
}

12.4.2.4 中断处理函数

​ 中断处理函数里首先清除GPT1状态寄存器对应的位,然后每发生一次中断翻转绿灯的状态。

​ 代码在在裸机Git仓库 NoosProgramProject/(12_定时器编程/009_timer_gpt_int/main.c)

void GPT1_COMP1_handle_irq(void)
{
	static int on = 1;

	printf("GPT1 comp0 interrupt happened\r\n");
	/* 
	 * bit0: OF1 Output Compare 1 Flag
	 * write 1 clear it */
	GPT1->SR |= 1;

	/* read GPIO5_DR to get GPIO5_IO01 status*/
	if(on) {
		/* led off, set GPIO5_DR to configure GPIO5_IO03 output 1 */
		GPIO5->DR |= (1<<3); //led off
		on = 0;
	} else {
		/* led on, set GPIO5_DR to configure GPIO5_IO03 output 0 */
		GPIO5->DR &= ~(1<<3); //led on
		on = 1;
}

12.4.2.5 主函数的设置

​ 通过gpt_init初始化GPT1计数器,延时时间设置为1s,注册中断处理函数,使能gic的GPT1_IRQn的中断,使能GPT1的输出通道1的中断,最后运行GPT1计数器。

​ 代码在裸机Git仓库 NoosProgramProject/(12_定时器编程/009_timer_gpt_int/main.c)

	gpt_init(GPT1, OUT_COMP1, 1000000);// set 1s
	request_irq(GPT1_IRQn, (irq_handler_t)GPT1_COMP1_handle_irq, NULL);
	gic_enable_irq(GPT1_IRQn);
	gpt_enable_interrupt(GPT1, IR_OF1IE, 1);
	gpt_run(GPT1, 1);

12.4.3 参考章节《4-1.4编译程序》编译程序

​ 进入裸机Git仓库 NoosProgramProject/(12_定时器编程/009_timer_gpt_int) 源码目录

12.4.4 参考章节《4-1.4映像文件烧写、运行》烧写、运行程序

​ 此时观察串口及开发板led灯现象

​ 绿灯不断闪烁,每隔1s翻转状态

​ 串口输出:

12.5 EPIT定时器介绍

​ EPIT是一个32位的计时器,能够在处理器很少干预的情况下以固定的时间间隔提供精确的中断。软件使能后,EPIT就开始计数。框图如下:

12.5.1 EPIT特性

​ EPIT具有以下主要特性:

​ •具有时钟源选择的32位递减计数器

​ •12位预分频器,用于对输入时钟频率分频

​ •可即时编程的计数器值

​ •可以设置在低功耗和调试模式下处于计数状态

​ •计数器达到比较值时产生中断

​ 时钟源的选择与GPT类似,后续实验时钟源也选择ipg_clk作为时钟源,ipg_clk设置的频率为66M。

12.5.2 操作模式

​ EPIT可以设置为set-and-forget或free-running模式。使用EPIT_CR [RLD]选择所需的模式。

① set-and-forget模式

​ 要选择这种操作模式,将控制寄存器(EPIT_CR)中的RLD位置1。 在这种模式下,计数器从加载寄存器(EPIT_LR)获取值;它不能直接从数据总线写入。每当计数器达到零时,EPIT_LR中的值就会加载到计数器中,然后将此值减到零。初始化计数器的话,不是等待计数达到零才设置,需要设置EPIT计数器覆盖使能位(EPIT_CR [IOVW]),并将所需的初始化值写入EPIT_LR。

② free-runnning模式

选择此操作模式的话,需要清除RLD位。在这种模式下,计数器从0000 0000h翻转到FFFF FFFFh,而无需从加载寄存器重新加载。翻转后,计数器继续递减计数。初始化计数器的话,需要设置EPIT计数器覆盖使能位(EPIT_CR [IOVW]),并将所需的初始化值写入EPIT_LR。

12.5.3 操作过程

​ EPIT具有单个32位递减计数器,当软件使能该模块时,该计数器开始计数。计数器的起始值从EPIT加载寄存器中加载,处理器可以随时将其写入。比较寄存器中的值确定中断发生的时间。当禁用EPIT(EN = 0)时,主计数器和预分频器计数器会将其计数冻结为当前计数值。当重新启用EPIT(EN = 1)时,ENMOD位(可读可写位)决定计数器的值:

​ •如果设置了ENMOD,则主计数器从加载寄存器加载值(如果RLD = 1)或者 FFFF FFFFh(如果RLD = 0),并且预分频计数器复位(000h)。

​ •如果清除了ENMOD,则主计数器和预分频器计数器均从其冻结值重新开始计数。

​ 如果将EPIT编程为在低功耗模式(STOP / WAIT)下被禁用,则当EPIT进入低功耗模式时,主计数器和预分频器计数器都冻结在其当前计数值。当EPIT退出低功耗模式时,无论ENMOD位如何,主计数器和预分频器计数器都将从其冻结值开始计数。硬件复位会将所有EPIT寄存器复位为各自的复位值。除控制寄存器中的EN,ENMOD,STOPEN和WAITEN位外,软件复位将其它位复位为各自的复位值。这些位的状态不受软件复位的影响。即使禁用了EPIT,也可以进行软件复位操作。

12.5.4 比较事件

​ 当EPIT_EPITCMPR的编程值与EPIT_EPITCNR中的值匹配时,将设置比较状态标志,并且如果控制寄存器中的OCIEN位置1,则会产生中断。

​ 根据控制寄存器中输出模式(OM)位的设置,比较输出引脚可以设置为置位,清除,切换或完全不受影响。如果在翻转时需要中断(当计数器值达到0x0000_0000并加载新值时),则应将比较寄存器值设置为等于set-and-forget模式中的加载寄存器值,或在free-running模式下等于0xFFFF_FFFF。下图显示了比较事件和中断的时序。

​ 计数器值覆盖

​ 可以在任何时候将EPIT计数器值设置为所需要的值。操作方法是,设置控制寄存器中的IOVW位,然后将所需要的值写入到加载寄存器。如果EPIT正在运行,则计数器将从覆盖值继续计数。

12.6 EPIT寄存器介绍

12.6.1 Control register (EPITx_CR)

​ EPIT控制寄存器

​ bit25-24,CLKSRC,选择时钟源,00与时钟源断开,01选择Peripheral clk,10选择Hign-frequency参考时钟,11选择low-frequency参考时钟

​ bit23-22,OM,输出引脚的模式,00与输出引脚断开,01翻转引脚状态,10清0引脚状态,11置位引脚状态

​ bit21,STOPEN,stop mode时是否关闭EPIT

​ bit19,WAITEN,wait mode时是否关闭EPIT

​ bit18,DBGEN,debug mode时是否使能EPIT

​ bit17,IOVW,写加载寄存器是否覆盖计数器的值

​ bit16,SWR,软件复位

​ bit15-4,PRESCALER,预分频计数器

​ bit3,RLD,计数器计数到0时是翻转到0xFFFF_FFFF,还是从加载寄存器加载

​ bit2,OCIEN,比较中断是否使能

​ bit1,ENMOD,0时从上次关闭时的计数值继续计数,为1时,如果RLD为1的话,从加载计数器开始计数,否则从0xFFFF_FFFF开始计数

​ bit0,EN,是否使能EPIT

12.6.2 Status register (EPITx_SR)

​ EPIT状态寄存器

​ 状态寄存器只有一个状态位,用来表示输出比较事件是否发生,写1清除掉该位。

​ bit0,OCIF,比较事件是否发生

12.6.3 Load register (EPITx_LR)

​ EPIT加载寄存器

​ 如果EPIT_CR的RLD置位的话,当EPIT计数器计数到0时,会将EPIT_LR值加载到计数器中。如果IOVW位设置的话,往该寄存器写值会同时覆盖掉计数器的值。这个覆盖特性与RLD是否设置无关。

​ 加载计数的值

12.6.3 Compare register (EPITx_CMPR)

​ EPIT比较寄存器

​ 比较寄存器用来决定什么时候产生比较事件。

​ 比较寄存器,当计数器等于这个值时产生比较中断。

12.6.4 Counter register (EPITx_CNR)

​ EPIT计数器

​ EPIT计数器的值,可以在任何时候读,不会影响计数流程,这是个只读寄存器。如果控制寄存器的IOVW位设置的话,当往加载寄存器EPIT_IR写值时会覆盖当前的计数值。

12.7 EPIT查询实现延时代码详解

12.7.1 代码分析

12.7.1.1 epit_poll_init函数

​ 通过epit_poll_init和epit_poll_restart两个函数来实现,epit_poll_init函数首先对EPIT进行软件复位,设置EPIT为set-and-forget模式,设置enable mode,时钟源选择Peripheral Clock (ipg_clk)为66M,预分频值设置为0(即预分频值为1),使能overwrite,比较寄存器CMPR设置为0,最后使能OCIEN,打开输出比较中断。

​ epit_poll_init代码在裸机Git仓库 NoosProgramProject/(12_定时器编程/009_timer_epit_poll/epit.c)

void epit_poll_init(EPIT_Type *base)
{
	base->CR = 0;

	/* software reset  
	 * bit16
	 */
	base->CR |= (1 << 16);
	/* wait for software reset self clear*/
	while((base->CR) & (1 << 16))
		;

	/*
	 * EPIT_CR
	 * bit21 stopen; bit19 waiten; bit18 debugen
	 * bit17 overwrite enable; bit3 reload
	 * bit2 compare interrupt enable; bit1 enable mode
	 */
	base->CR |= (1 << 21) | (1 << 19) | (1 << 17) | (1 << 3) | (1 << 1);

	/*
	 * EPIT_CR
	 * bit25-24: 00 off, 01 peripheral clock(ipg clk), 10 high, 11 low
	 * bit15-4: prescaler value, divide by n+1
	 */
	base->CR &= ~((0x3 << 24) | (0xFFF << 4));
	base->CR |= (1 << 24);

	/* EPIT_CMPR: compare register */
	base->CMPR = 0;
	/* EPIT_LR: load register , assue use ipc clk 66MHz*/
	//base->LR = USEC_TO_COUNT(us);

	/*	EPIT_CR bit2 OCIEN compare interrupt enable */
	base->CR |= (1 << 2);
}

12.7.1.2 epit_poll_restart函数

​ epit_poll_restart函数首先关闭首先关闭EPIT,然后根据延时时间设置加载寄存器LR,为了防止状态寄存器已经设置,先清除一下状态寄存器OCIF位,再使能EPIT计数器,等待状态寄存器OCIF位设置,设置后清除一下状态寄存器OCIF位。

​ 代码在裸机Git仓库 NoosProgramProject/(12_定时器编程/009_timer_epit_poll/epit.c):

void epit_poll_restart(EPIT_Type *base, unsigned int us)
{
	epit_run(base, 0);
	/* EPIT_LR: load register , assue use ipc clk 66MHz*/
	base->LR = USEC_TO_COUNT(us);
	/* write 1 clear it, avoid it happened before */
	EPIT1->SR |= (1 << 0);
	epit_run(base, 1);
	/* wait compare event happened*/
	while(!(EPIT1->SR & 0x1))
	/* write 1 clear it */
	EPIT1->SR |= (1 << 0);
}

12.7.1.3 epit_run函数

​ epit_run函数用来使能和关闭EPIT。

​ 代码在裸机Git仓库 NoosProgramProject/(12_定时器编程/009_timer_epit_poll/epit.c)

void epit_run(EPIT_Type *base, int on)
{
	/*  EPIT_CR bit0 EN */
	if (on)
		base->CR |= (1 << 0);
	else
		base->CR &= ~(1 << 0);
}

12.7.1.4 主函数

​ 使用epit_poll_init初始化EPIT1,不断调用epit_poll_restart(EPIT1, 1000000)延时1s,并且依次点亮和关闭绿灯。

​ 代码在裸机Git仓库 NoosProgramProject/(12_定时器编程/009_timer_epit_poll/main.c):

epit_poll_init(EPIT1);
while(1) {
    epit_poll_restart(EPIT1, 1000000);
    GPIO5->DR &= ~(1<<3); //led on
    printf("led is on\r\n");
    epit_poll_restart(EPIT1, 1000000);
    GPIO5->DR |= (1<<3); //led off
    printf("led is off\r\n");
}

12.7.2 参考章节《4-1.4编译程序》编译程序

​ 进入 裸机Git仓库 NoosProgramProject/(12_定时器编程/009_timer_epit_poll) 源码目录

12.7.3 参考章节《4-1.4映像文件烧写、运行》烧写、运行程序

​ 此时观察串口及开发板led灯现象

​ 绿灯不断闪烁,每隔1s翻转状态

​ 串口输出不断打印”led is on”和“led is off”,如下:

12.8 EPIT中断实现延时代码详解

12.8.1 EPIT1中断号的确定

​ 查询数据手册chapter 3的Table3-1中断号表,EPIT1如下所示:

​ gic中断号需要再这个序号基础上加上32,所以EPIT1的gic中断号为56+32=88。

12.8.1 代码分析

12.8.1.1 初始化EPIT1

​ 通过epit_init函数初始化EPIT1,首先对EPIT进行软件复位,设置EPIT为set-and-forget模式,设置enable mode,时钟源选择Peripheral Clock (ipg_clk)为66M,预分频值设置为0(即预分频值为1),比较寄存器设置为0,加载寄存器根据延时值设置。

​ epit_init代码在在裸机Git仓库 NoosProgramProject/(12_定时器编程/009_timer_epit_int/epit.c):

/* assume use ipc clk which is 66MHz, 1us against to 66 count */
#define USEC_TO_COUNT(us) (us * 66 - 1)

void epit_init(EPIT_Type *base, unsigned int us)
{
	base->CR = 0;

	/* software reset  
	 * bit16
	 */
	base->CR |= (1 << 16);
	/* wait for software reset self clear*/
	while((base->CR) & (1 << 16))
		;

	/*
	 * EPIT_CR
	 * bit21 stopen; bit19 waiten; bit18 debugen
	 * bit17 overwrite enable; bit3 reload
	 * bit2 compare interrupt enable; bit1 enable mode
	 */
	base->CR |= (1 << 21) | (1 << 19) | (1 << 3) | (1 << 1);

	/*
	 * EPIT_CR
	 * bit25-24: 00 off, 01 peripheral clock(ipg clk), 10 high, 11 low
	 * bit15-4: prescaler value, divide by n+1
	 */
	base->CR &= ~((0x3 << 24) | (0xFFF << 4));
	base->CR |= (1 << 24);

	/* EPIT_CMPR: compare register */
	base->CMPR = 0;
	/* EPIT_LR: load register , assue use ipc clk 66MHz*/
	base->LR = USEC_TO_COUNT(us);
}

12.8.1.2 打开或者关闭比较中断

​ epit_enable_interrupt函数用来打开或者关闭比较中断。

​ 代码在在**裸机Git仓库 NoosProgramProject/(12_定时器编程/**009_timer_epit_int/epit.c):

void epit_enable_interrupt(EPIT_Type *base, int on)
{
	/*  EPIT_CR bit2 OCIEN compare interrupt enable */
	if (on)
		base->CR |= (1 << 2);
	else
		base->CR &= ~(1 << 2);
}

12.8.1.3 EPIT运行使能函数

​ 设置或者关闭控制寄存器的运行位。

​ 代码在裸机Git仓库 NoosProgramProject/(12_定时器编程/009_timer_epit_int/epit.c):

void epit_run(EPIT_Type *base, int on)
{
	/*  EPIT_CR bit0 EN */
	if (on)
		base->CR |= (1 << 0);
	else
		base->CR &= ~(1 << 0);
}

12.8.1.4 中断处理函数

​ EPIT1_handle_irq首先写1清除中断状态位,并且翻转绿灯的状态。

​ 代码在裸机Git仓库 NoosProgramProject/(12_定时器编程/009_timer_epit_int/main.c):

void EPIT1_handle_irq(void)
{
	static int on = 1;

	printf("EPIT1 interrupt happened\r\n");
	/* write 1 clear it */
	 EPIT1->SR |= (1 << 0);

	/* read GPIO5_DR to get GPIO5_IO01 status*/
	if(on) {
		/* led off, set GPIO5_DR to configure GPIO5_IO03 output 1 */
		GPIO5->DR |= (1<<3); //led on
		on = 0;
	} else {
		/* led on, set GPIO5_DR to configure GPIO5_IO03 output 0 */
		GPIO5->DR &= ~(1<<3); //led off
		on = 1;
	}
}

12.8.1.5 主函数

​ 主函数里调用epit_init(EPIT1, 1000000)设置延时时间为1s,注册中断处理函数,设置中断处理函数为EPIT1_handle_irq,gic_enable_irq(EPIT1_IRQn)打开EPIT1对应的gic使能位,调用epit_enable_interrupt(EPIT1, 1)打开EPIT的比较中断使能位,最后调用epit_run(EPIT1, 1)使能EPIT开始计数。

​ 代码在裸机Git仓库 NoosProgramProject/(12_定时器编程/009_timer_epit_int/main.c):

epit_init(EPIT1, 1000000);// set 1s
request_irq(EPIT1_IRQn, (irq_handler_t)EPIT1_handle_irq, NULL);
gic_enable_irq(EPIT1_IRQn);
epit_enable_interrupt(EPIT1, 1);
epit_run(EPIT1, 1);

12.8.1.6 参考章节《4-1.4编译程序》编译程序

​ 进入裸机Git仓库 NoosProgramProject/(12_定时器编程/009_timer_epit_int) 源码目录

12.8.1.7 参考章节《3-1.4映像文件烧写、运行》烧写、运行程序

​ 此时观察串口及开发板led灯现象

​ 绿灯不断闪烁,每隔1s翻转状态

​ 串口输出如下:

第十三章 EMMC编程(未完整校对)

第十三章 EMMC编程(未完整校对)

13. EMMC编程

​ 参考资料

https://linux.codingbelief.com/zh/storage/flash_memory/emmc/

​ 资料光盘: 00_UserManual\参考资料\EMMC编程\JESD84-B50-1eMMCStandard.pdf

1.1 EMMC介绍

1.1.1 EMMC简介

​ eMMC (Embedded Multi Media Card)是MMC协会订立的嵌入式存储器标准规格,主要针对手机、数码相机、平板电脑等产品。eMMC主要是为了简化手机内存储器的设计,节省电路板的面积。eMMC是将NAND Flash、控制器和MMC标准接口封装到一块芯片上,如下图所示。

1.1.2 EMMC硬件连接

​ 如下图所示,CPU和eMMC设备通过CLK、CMD、DATA[0-7]和RST信号脚互联。接口信号描述如下。

​ CLK:MMC总线的时钟线,由主机提供的信号。当时钟有效时才能传输命令和数据。在SDR模式下,时钟的上升沿为数据的采样时间。在DDR模式下,时钟的上下沿均进行数据的采样。不同的总线模式时钟的速率是不一样的。

​ CMD:双向传输的命令线,用于主机和设备的数据通信。当主机发送命令之后,设备会给主机应答,应答信号通过CMD线返回到主机。

​ RST:复位信号线。

​ DATA[0..7]:eMMC的双向数据总线,用于主机和设备之间的数据通信。DATA线是时分复用的,要么数据从主机传输到eMMC设备,要么从eMMC设备传输到主机。当用户上电或者复位的时候,仅能用DATA0传输数据。同时,用户根据自己的需要配置数据总线的位数。当用户选择4位时,eMMC设备配置DATA1-3的内部上拉,如果用户选择的是8位,那么同理会配置DATA1-7的上拉。

1.1.3 eMMC总线速度

​ eMMC5.0协议规定了5种总线数据模式如下表所示。兼容MMC模式、高速SDR模式和高速DDR模式可以兼容协议4.5版本之前的协议。

模式名 数据速率模式 IO电压 总线位宽 时钟速率 最大数据传输速率
兼容MMC模式 SDR 3/1.8/1.2V 1、4、8bit 0~26MHz 26MHz
高速SDR模式 SDR 3/1.8/1.2V 1、4、8bit 0~52MHz 52MHz
高速DDR模式 DDR 3/1.8/12V 4、8bit 0~52MHz 104MHz
HS200模式 SDR 1.8/12V 4、8bit 0~200MHz 200MHz
HS400模式 DDR 1.8/12V 8bit 0~200MHz 400MHz

1.1.4 eMMC总线协议

​ eMMC总线可以挂载一个主设备和多个eMMC设备。总线上的所有通讯都由主机发起,主机一次只能与一个eMMC设备通讯。

​ 系统在上电后,主机会给所有eMMC设备分配地址。当主机需要和某一个eMMC设备通讯时,会先根据RCA选中该eMMC设备,只有被选中的 eMMC设备才会应答主机的命令。

​ eMMC的通信是由单个或多个块组成的。它们分别是命令、应答、数据块,如下图所示。

​ 读取eMMC数据的数据流如下图所示。如果主机发送的是读单个块的命令,那么eMMC设备只会发送一个块的数据,并且不用发送停止命令。

​ 写入eMMC数据过程如下图所示。

​ 如下图所示通信中除了带数据的命令,也有不带数据和不带应答的命令。

1.1.5 命令(CMD)介绍

1.1.5.1 (1) 命令类型

​ CMD有4中类型,分别为:不带应答的广播命令(br);带有应答的广播命令(bcr);点对点无数据传输(ac);点对点有数据传输(adtc)。

1.1.5.2 (2) 命令格式

​ 如下图所示CMD是一个由起始位、传输位、命令索引、命令参数、CRC和结束位组成,一共有48位。

起始位 传输位 命令索引 命令参数 CRC 结束位
位置 47 46 [45:40] [39:8] [7:1] 0
“0” “1” x x x “1”

​ 起始位固定为0。传输位表示数据传输的方向,1代表主机到eMMC设备。命令索引用占用了6个位,取值范围为0~63。命令参数是根据每个命令的功能要求提供相应的参数值。CRC7是起始位、传输位、命令索引、命令参数的内容的校验值。结束位固定为1。

1.1.5.3 (3) 命令分类

​ eMMC将56个命令分成如下表的12个class,每个class代表一类功能,包含所有命令的一个子集。设备支持哪些class,可以通过CSD寄存器的CSD[95:84]来查询,每一个位代表一个class,当某一位为1时代表支持该位对应class。

命令类别 描述 备注
class 0 basic 基本命令
class 1 Obsolete 废弃
class 2 block read 块数据读相关命令,包括设置块长度、读取单块、读取多块
class 3 obsolete 废弃
class 4 block write 块数据写相关命令
class 5 erase 擦除操作
class 6 write protection 设置写保护
class 7 Lock device 锁或者解锁设备
class 8 Application-specific 特定应用命令
class 9 I/O mode 写寄存器,设置系统进入中断模式
class 10 security protocols 连续传输数据块
class 11 reserved 预留

1.1.6 应答(Response)介绍

​ 所有应答都是eMMC设备接收到主机的命令后在CMD信号线上发送,而应答的内容取决于应答的类型。如下图所示应答格式由起始位、传输位、数据位、CRC位和结束位组成。

​ eMMC协议中有6种应答类型,分别是R1、R1b、R2、R3、R4和R5。

​ R1应答总长为48bit,其中[45:40]代表应答的命令索引,[39:8]表示设备的状态以及错误标记位。R1b的结构是在R1的基础上增加了一个Busy信号。

起始位 传输位 命令索引 设备状态 CRC 结束位
位置 47 46 [45:40] [39:8] [7:1] 0
位宽 1 1 6 32 7 1
“0” “0” x x CRC “1”

​ 设备状态的含义如下:

​ R2应答主要返回设备CID或CSD寄存器的内容,CID寄存器分别对应CMD2和CMD10,CSD寄存器则是对应CMD9。

起始位 传输位 检查位 数据 CRC 结束位
位置 135 134 [133:128] [127:8] [7:1] 0
位宽 1 1 6 120 7 1
“0” “0” “111111” x CRC “1”

​ R3应答主要返回设备ORC寄存器的内容,只有下发CMD1时,设备才回复R3应答。

起始位 传输位 检查位 ORC寄存器 检查位 结束位
位置 47 46 [45:40] [39:8] [7:1] 0
位宽 1 1 6 32 7 1
“0” “0” “111111” x “1111111” “1”

​ R4应答主要用于写入和读出某个寄存器其中一个字节的数据。只有主机下发CMD39时,设备应答R4

起始位 传输位 命令39 参数 CRC 结束位
位置 47 46 [45:40] [39:8] [7:1] 0
位宽 1 1 6 32 7 1
“0” “0” “100111” X “1111111” “1”

​ 参数对应内容如下表所示。

[39:8]参数 相对地址 状态 寄存器地址 寄存器内容
位宽 16 1 7 8

​ R5作为中断请求应答,其应答结构如下。

起始位 传输位 命令39 参数 CRC 结束位
位置 47 46 [45:40] [39:8] [7:1] 0
位宽 1 1 6 32 7 1
“0” “0” “100111” X “1111111” “1”

​ 参数对应内容如下表所示。

[39:8]参数 相对地址 中断数据
位宽 16 16

1.1.7 eMMC工作模式

​ 如下表所示,EMMC有5种工作模式:开机模式、识别模式、中断模式、数据传输模式和无效模式

模式 描述
开机模式(Boot mode) 上电后,eMMC设备若收到带有0xF0F0F0F0参数的CMD0时,如果eMMC设备支持开机模式则进入开始模式,否则进入识别模式。
识别模式(Card identification) 上电后,开机模式结束或者不支持开机模式,eMMC设备就会进入测模式,并等待主机下发CMD3设置相对地址。
中断模式(Interrupt mode) 在此模式中不能进行数据传输操作,只能发出中断服务请求。
数据传输模式(Data transfer mode) 当主机为eMMC设备配置完RCA后,就会进入数据传输模式。
无效模式(Inactive mode) 当eMMC设备电压不符规定时就会进入此模式,也可以通过下发CMD1命令使eMMC设备进入无效模式

​ eMMC设备运行时,根据不同模式跳转到不同的工作状态,如下表所示。

设备状态 操作模式 总线模式
Inactive State 非活动模式 开漏
Pre-Idle State 启动模式
Pre-Boot State
Idle State 设备识别模式
Ready State
Identification State
Stand-by State 数据传输模式 上下拉
Sleep State
Transfer State
Bus-Test State
Sending-data State
Programming State
Disconnect State
Boot State 启动模式
Wait-IRQ State 中断模式 开漏

1.1.8 eMMC寄存器

​ 如下表所示,总共有6种。它们可以得到设备的相关内容以及设置工作时的控制对象,在读写数据前的步骤操作相对应的寄存器实现。因此协议中明确定义所用寄存器的含义。

名称 大小(Byte) 描述
CID 16 设备识别寄存器
RCA 2 相关设备地址
DSR 2 驱动等级寄存器
CSD 16 存储设备相关信息
OCR 4 操作状态寄存器
EXT_CSD 512 扩展设备寄存器

1.1.9 eMMC初始化过程

​ eMMC设备的正常工作,必须完成初始化过程。在初始化过程中,主机会对EMMC设备进行识别,确定设备工作的电压范围和使用模式,向设备分配相对地址(RCA,一个系统可以支持多个eMMC设备)。设备初始化过程如下图所示。

​ 电源上电后,eMMC设备进入空闲(Idle State)。虽然进入了空闲状态,但是设备的上电复位过程不一定完成,这时需要读取OCR的Busy位来判断设备的上电复位过程是否完成。

​ 在空闲状态中只有CMD1和CMD58是合法命令。主机通过发送CMD1,读取eMMC设备的OCR寄存器,并进行电压匹配和通过BUSY位判断复位是否完成。当BUSY为1时,代表eMMC设备初始化完毕进入Ready State。

​ 在Ready State中,主机下发CMD2命令后会收到CID寄存器的值,此时eMMC设备进入Identification State。接着主机发送CMD3命令,为eMMC设备配置RCA。配置完成后eMMC设备进入standby state。此时初始化过程结束。

1.1.10 数据传输模式

​ 在数据传输模式下可实现对EMMC设备进行读写,擦除,总线测试等操作。此模式工作状态图如下所示。

1.2 IMX6ULL EMMC寄存器介绍

1.2.1 IMX6ULL USDHC模块介绍

​ IMX6ULL共2个独立的USDHC,主要特型如下:

​ 1) 兼容MMC系统规范4.2/4.3/4.4/4.41/4.5版本;

​ 2) 时钟频率最高可达208MHz;

​ 3) 支持1位/4位/8位的SD、SDIO和MMC模式;

​ 4) 在SDR(单数据速率)模式下使用4条并行数据线对SDIO卡进行高达832Mbps的数据传输;

​ 5) 在DDR(双数据速率)模式下使用4条并行数据线对SDXC卡进行高达400Mbps的数据传输;

​ 6) 在SDXC卡模式下使用4条并行数据线对SDXC卡进行高达832Mbps的数据传输;

​ 7) 在SDXC卡模式下使用4条并行数据传输高达400Mbps的数据传输(双数据传输)支持单块/多块读写,支持1~4096字节的块大小,支持写操作的写保护开关,支持同步和异步中止,支持块间隙数据传输期间的暂停,支持SDIO读取等待和暂停恢复操作,支持自动CMD12 对于多块传输,主机可以在数据传输进行时启动非数据传输命令。

​ 以上只是列举了部分的特性,详细的特征描述可以查看芯片手册《Chapter 58 Ultra Secured Digital Host Controller (uSDHC)》。

1.2.2 IMX6ULL USDHC寄存器介绍

​ IMX6ULL有两路USDHC,每一路各有29个寄存器,只要学会一路寄存器的使用,即可掌握另一路的使用。

​ USDHC的寄存器如下图所示,从图中可得知两路USDHC的寄存器各分配到两段连续的地址空间中。

​ 接下来介绍我们实验要用到的寄存器:

1.2.2.1 (1)uSDHC2_BLK_ATT

​ uSDHC2_BLK_ATT寄存器为块的读写服务。BLKSIZE[12:0]用于设置读写块的大小,最大可以设置为4096字节,如果设置为0时表示没有数据传输。BLKCNT用于配置读写块的数量,最大可配置为65535个块。当在多个块读写时,每读写完一个块BLKCNT自动减一直到0停止。

1.2.2.2 (2)uSDHC2_CMD_ARG

​ uSDHC2_CMD_ARG是命令的参数寄存器,保存要下发命令的参数。

1.2.2.3(3)uSDHC2_CMD_XFR_TYP

​ uSDHC2_CMD_XFR_TYP是命令传输类型寄存器。用于配置传输的命令编号、命令类型、数据传输、应答类型和校验使能等。

1.2.2.4 (4)uSDHC2_CMD_RSP0-3

​ uSDHC2_CMD_RSP0-3是32位的命令应答寄存器。如下表所示系统根据应答类型,将eMMC设备应答的数据分别保存在相应的命令应答寄存器中。

![](第十三章 EMMC编程.assets/13_EMMC_Program_image019.png)

1.2.2.5(5)uSDHC2_DATA_BUFF_ACC_PORT

​ uSDHC2_DATA_BUFF_ACC_PORT是一个32位的数据缓存接口,这个寄存器是内部数据缓存器的一个读写口。通过这个数据缓存接口,就可以读取到数据缓存的数据。

![](第十三章 EMMC编程.assets/13_EMMC_Program_image020.png)

1.2.2.6(6)uSDHC2_PRES_SETATE

​ uSDHC2_PRES_SETATE是USDHC的状态寄存器。状态寄存器体现USDHC的命令传输、数据读写、时钟等的状态。我们主要关注CIHB、CDIHB、SDSTB、BREN、BWEN位。具体使用方法编程阶段会一一介绍。

![](第十三章 EMMC编程.assets/13_EMMC_Program_image021.png)

1.2.2.7 (7)uSDHC2_PROT_CTRL

​ uSDHC2_PROT_CTRL是端口控制寄存器,主要是控制数据位宽、大小端配置等。

![](第十三章 EMMC编程.assets/13_EMMC_Program_image022.png)

1.2.2.8 (8)uSDHC2_SYS_CTRL

​ uSDHC2_SYS_CTRL是系统控制寄存器,用于软件复位、时钟分频配置和超时时间等。

![](第十三章 EMMC编程.assets/13_EMMC_Program_image023.png)

1.2.2.9(9)uSDHC2_INT_STATUS

​ uSDHC2_INT_STATUS是中断状态寄存器,命令传输完成、读写完成和传输错误都会在中断体现。

![](第十三章 EMMC编程.assets/13_EMMC_Program_image024.png)

![](第十三章 EMMC编程.assets/13_EMMC_Program_image025.png)

1.2.2.10(10)uSDHC2_INT_SIGNAL_EN

​ 通过配置这个寄存器使能中断的状态。将这个寄存器某位置为1时则uSDHC2_INT_STATUS对应的状态使能。反则屏蔽该状态。

![](第十三章 EMMC编程.assets/13_EMMC_Program_image026.png)

![](第十三章 EMMC编程.assets/13_EMMC_Program_image027.png)

1.2.2.11 (11)uSDHC2_WTMK_LVL

​ 这个是读写水位寄存器。当缓存器的数据大于WR_WML或者RD_WML时,中断状态寄存器中的读写状态置1。

![](第十三章 EMMC编程.assets/13_EMMC_Program_image028.png)

1.2.2.12 (12)uSDHC2_MIX_CTRL

​ uSDHC2_MIX_CTRL是混合控制寄存器。

![](第十三章 EMMC编程.assets/13_EMMC_Program_image029.png)

1.3 EMMC编程

1.3.1 eMMC引脚配置

​ 前两节介绍了eMMC的协议和IMX6ULL USDHC相关寄存器,接下来开始讲解EMMC驱动设计。编写驱动首先要配置引脚。为了正确配置引脚我们需要了解的是硬件的连接方式和相关引脚的配置寄存器。

​ (1)确定USDHC引脚

​ 从下图可知,eMMC设备的引脚连接到IMX6-NAND的ALE、nRE、nWE和DATA0-7引脚上。查看IMX6UUL数据手册可知所使用的是USDHC2。

![](第十三章 EMMC编程.assets/13_EMMC_Program_image030.png)

![](第十三章 EMMC编程.assets/13_EMMC_Program_image031.png)

​ (2)USDHC2相关引脚寄存器配置

1.3.1.1 步骤1:配置USDHC2相关引脚的复用功能

​ 在手册《Chapter4 External Signals and Pin Multiplexing》中,可以找到USDHC2引脚所对应的复用模式,如下表所示。从表中可知USDHC2的相关引脚的复用模式应设置为ALT1模式。

image-20220111194240136

![](第十三章 EMMC编程.assets/13_EMMC_Program_image033.png)

![](第十三章 EMMC编程.assets/13_EMMC_Program_image034.png)

​ 由上图可知IOMUXC_SW_MUX_CTL_PAD_NAND_ALE[MUX_MODE]应设为0x01。

![](第十三章 EMMC编程.assets/13_EMMC_Program_image035.png)

![](第十三章 EMMC编程.assets/13_EMMC_Program_image036.png)

由上图可知IOMUXC_SW_MUX_CTL_PAD_NAND_RE_B[MUX_MODE]应设为0x01。

![](第十三章 EMMC编程.assets/13_EMMC_Program_image037.png)

![](第十三章 EMMC编程.assets/13_EMMC_Program_image038.png)

由上图可知IOMUXC_SW_MUX_CTL_PAD_NAND_WE_B[MUX_MODE]应设为0x01。

![](第十三章 EMMC编程.assets/13_EMMC_Program_image039.png)

![](第十三章 EMMC编程.assets/13_EMMC_Program_image040.png)

![](第十三章 EMMC编程.assets/13_EMMC_Program_image041.png)

![](第十三章 EMMC编程.assets/13_EMMC_Program_image042.png)

![](第十三章 EMMC编程.assets/13_EMMC_Program_image043.png)

![](第十三章 EMMC编程.assets/13_EMMC_Program_image044.png)

![](第十三章 EMMC编程.assets/13_EMMC_Program_image045.png)

由于DATA0-7都是ALT1所以IOMUXC_SW_MUX_CTL_PAD_NAND_DATA0、IOMUXC_SW_MUX_CTL_PAD_NAND_DATA1、IOMUXC_SW_MUX_CTL_PAD_NAND_DATA2、IOMUXC_SW_MUX_CTL_PAD_NAND_DATA3IOMUXC_SW_MUX_CTL_PAD_NAND_DATA4、IOMUXC_SW_MUX_CTL_PAD_NAND_DATA5、IOMUXC_SW_MUX_CTL_PAD_NAND_DATA6、IOMUXC_SW_MUX_CTL_PAD_NAND_DATA7均配置为0x01。

具体代码如下:

61 	// 配置引脚复用功能
62 	IOMUXC_SW_MUX_CTL_PAD_NAND_RE_B  = (volatile unsigned int *)(0x20E0178);	
63 	IOMUXC_SW_MUX_CTL_PAD_NAND_WE_B  = (volatile unsigned int *)(0x20E017C);		
64 	IOMUXC_SW_MUX_CTL_PAD_NAND_DATA0 = (volatile unsigned int *)(0x20E0180);
65 	IOMUXC_SW_MUX_CTL_PAD_NAND_DATA1 = (volatile unsigned int *)(0x20E0184);
66 	IOMUXC_SW_MUX_CTL_PAD_NAND_DATA2 = (volatile unsigned int *)(0x20E0188);
67 	IOMUXC_SW_MUX_CTL_PAD_NAND_DATA3 = (volatile unsigned int *)(0x20E018C);
68 	IOMUXC_SW_MUX_CTL_PAD_NAND_DATA4 = (volatile unsigned int *)(0x20E0190);
69 	IOMUXC_SW_MUX_CTL_PAD_NAND_DATA5 = (volatile unsigned int *)(0x20E0194);
70 	IOMUXC_SW_MUX_CTL_PAD_NAND_DATA6 = (volatile unsigned int *)(0x20E0198);
71 	IOMUXC_SW_MUX_CTL_PAD_NAND_DATA7 = (volatile unsigned int *)(0x20E019C);
72 	IOMUXC_SW_MUX_CTL_PAD_NAND_ALE   = (volatile unsigned int *)(0x20E01A0);	

73 	
74 	*IOMUXC_SW_MUX_CTL_PAD_NAND_RE_B  = 0x1U;
75 	*IOMUXC_SW_MUX_CTL_PAD_NAND_WE_B  = 0x1U;
76 	*IOMUXC_SW_MUX_CTL_PAD_NAND_DATA0 = 0x1U;
77 	*IOMUXC_SW_MUX_CTL_PAD_NAND_DATA1 = 0x1U;
78 	*IOMUXC_SW_MUX_CTL_PAD_NAND_DATA2 = 0x1U;
79 	*IOMUXC_SW_MUX_CTL_PAD_NAND_DATA3 = 0x1U;
80 	*IOMUXC_SW_MUX_CTL_PAD_NAND_DATA4 = 0x1U;
81 	*IOMUXC_SW_MUX_CTL_PAD_NAND_DATA5 = 0x1U;
82 	*IOMUXC_SW_MUX_CTL_PAD_NAND_DATA6 = 0x1U;
83 	*IOMUXC_SW_MUX_CTL_PAD_NAND_DATA7 = 0x1U;
84 	*IOMUXC_SW_MUX_CTL_PAD_NAND_ALE   = 0x1U;

1.3.1.2 步骤2:设置USDHC2相关引脚的输入源:

如下图IMX6中的功能模块(如:UART、USDHC)可能存在1个或多个可配置引脚,所以在配置完GPIO模式后还要设置输入源选择寄存器。

![](第十三章 EMMC编程.assets/13_EMMC_Program_image046.png)

由下图可知IOMUXC_USDHC2_CLK_SELECT_INPUT [DAISY]应设为0x2。

![](第十三章 EMMC编程.assets/13_EMMC_Program_image047.png)

![](第十三章 EMMC编程.assets/13_EMMC_Program_image048.png)

由下图可知IOMUXC_USDHC2_CMD_SELECT_INPUT [DAISY]应设为0x2。

![](第十三章 EMMC编程.assets/13_EMMC_Program_image049.png)

由下图可知IOMUXC_USDHC2_DATA0_SELECT_INPUT [DAISY]应设为0x2。

![](第十三章 EMMC编程.assets/13_EMMC_Program_image050.png)

由下图可知IOMUXC_USDHC2_DATA1_SELECT_INPUT [DAISY]应设为0x2。

![](第十三章 EMMC编程.assets/13_EMMC_Program_image051.png)

![](第十三章 EMMC编程.assets/13_EMMC_Program_image052.png)

由下图可知IOMUXC_USDHC2_DATA2_SELECT_INPUT [DAISY]应设为0x1。

![](第十三章 EMMC编程.assets/13_EMMC_Program_image053.png)

由下图可知IOMUXC_USDHC2_DATA3_SELECT_INPUT [DAISY]应设为0x2。

![](第十三章 EMMC编程.assets/13_EMMC_Program_image054.png)

USDHC2_DATA4-7的DAISY都0x2,所以IOMUXC_USDHC2_DATA4_SELECT_INPUT [DAISY]、IOMUXC_USDHC2_DATA5_SELECT_INPUT [DAISY]、IOMUXC_USDHC2_DATA6_SELECT_INPUT [DAISY]、IOMUXC_USDHC2_DATA7_SELECT_INPUT [DAISY]应设为0x2。

![](第十三章 EMMC编程.assets/13_EMMC_Program_image055.png)

代码如下:

86 	// 配置引脚的输入源
87 	IOMUXC_USDHC2_CLK_SELECT_INPUT    = (volatile unsigned int *)(0x020E0670);
88 	IOMUXC_USDHC2_CMD_SELECT_INPUT    = (volatile unsigned int *)(0x020E0678);
89 	IOMUXC_USDHC2_DATA0_SELECT_INPUT  = (volatile unsigned int *)(0x020E067C);
90 	IOMUXC_USDHC2_DATA1_SELECT_INPUT  = (volatile unsigned int *)(0x020E0680);
91 	IOMUXC_USDHC2_DATA2_SELECT_INPUT  = (volatile unsigned int *)(0x020E0684);
92 	IOMUXC_USDHC2_DATA3_SELECT_INPUT  = (volatile unsigned int *)(0x020E0688);
93 	IOMUXC_USDHC2_DATA4_SELECT_INPUT  = (volatile unsigned int *)(0x020E068C);
94 	IOMUXC_USDHC2_DATA5_SELECT_INPUT  = (volatile unsigned int *)(0x020E0690);
95 	IOMUXC_USDHC2_DATA6_SELECT_INPUT  = (volatile unsigned int *)(0x020E0694);
96 	IOMUXC_USDHC2_DATA7_SELECT_INPUT  = (volatile unsigned int *)(0x020E0698);
97 	
98 	*IOMUXC_USDHC2_CLK_SELECT_INPUT    = 0x2U;
99 	*IOMUXC_USDHC2_CMD_SELECT_INPUT    = 0x2U;
100 	*IOMUXC_USDHC2_DATA0_SELECT_INPUT  = 0x2U;
101 	*IOMUXC_USDHC2_DATA1_SELECT_INPUT  = 0x2U;
102	 *IOMUXC_USDHC2_DATA2_SELECT_INPUT  = 0x1U;
103 	*IOMUXC_USDHC2_DATA3_SELECT_INPUT  = 0x2U;
104 	*IOMUXC_USDHC2_DATA4_SELECT_INPUT  = 0x1U;
105 	*IOMUXC_USDHC2_DATA5_SELECT_INPUT  = 0x1U;
106 	*IOMUXC_USDHC2_DATA6_SELECT_INPUT  = 0x1U;
107 	*IOMUXC_USDHC2_DATA7_SELECT_INPUT  = 0x1U;

1.3.1.3 步骤3:设置USDHC2相关引脚的输入输出参数

![](第十三章 EMMC编程.assets/13_EMMC_Program_image056.png)

![](第十三章 EMMC编程.assets/13_EMMC_Program_image057.png)

相关引脚的输入输出参数配置如上图红圈所示,所有引脚配置值为0x17059。

相关代码如下:

109	// 配置引脚的输入输出参数
110 	IOMUXC_SW_PAD_CTL_PAD_NAND_RE_B   = (volatile unsigned int *)(0x020E0404U);
111 	IOMUXC_SW_PAD_CTL_PAD_NAND_WE_B   = (volatile unsigned int *)(0x020E0408U);
112 	IOMUXC_SW_PAD_CTL_PAD_NAND_DATA00 = (volatile unsigned int *)(0x020E040CU);
113 	IOMUXC_SW_PAD_CTL_PAD_NAND_DATA01 = (volatile unsigned int *)(0x020E0410U);
114 	IOMUXC_SW_PAD_CTL_PAD_NAND_DATA02 = (volatile unsigned int *)(0x020E0414U);
115 	IOMUXC_SW_PAD_CTL_PAD_NAND_DATA03 = (volatile unsigned int *)(0x020E0418U);
116 	IOMUXC_SW_PAD_CTL_PAD_NAND_DATA04 = (volatile unsigned int *)(0x020E041CU);
117 	IOMUXC_SW_PAD_CTL_PAD_NAND_DATA05 = (volatile unsigned int *)(0x020E0420U);
118 	IOMUXC_SW_PAD_CTL_PAD_NAND_DATA06 = (volatile unsigned int *)(0x020E0424U);
119 	IOMUXC_SW_PAD_CTL_PAD_NAND_DATA07 = (volatile unsigned int *)(0x020E0428U);
120 	
121 	*IOMUXC_SW_PAD_CTL_PAD_NAND_RE_B   = 0x17059; 
122 	*IOMUXC_SW_PAD_CTL_PAD_NAND_WE_B   = 0x17059; 
123 	*IOMUXC_SW_PAD_CTL_PAD_NAND_DATA00 = 0x17059; 
124 	*IOMUXC_SW_PAD_CTL_PAD_NAND_DATA01 = 0x17059; 
125 	*IOMUXC_SW_PAD_CTL_PAD_NAND_DATA02 = 0x17059; 
126 	*IOMUXC_SW_PAD_CTL_PAD_NAND_DATA03 = 0x17059; 
127 	*IOMUXC_SW_PAD_CTL_PAD_NAND_DATA04 = 0x17059; 
128 	*IOMUXC_SW_PAD_CTL_PAD_NAND_DATA05 = 0x17059; 
129 	*IOMUXC_SW_PAD_CTL_PAD_NAND_DATA06 = 0x17059; 
130 	*IOMUXC_SW_PAD_CTL_PAD_NAND_DATA07 = 0x17059;

1.3.2 时钟配置

配置完引脚后,我们还需要设置时钟源。为了正确设置USDHC2的时钟需要解决两个问题。第一是EMMC设备支持什么时钟速率,第二USDHC2的时钟是从哪里来。

1.3.2.1 步骤1:获得eMMC设备的时钟速率。

查看核心板所用的eMMC设备手册《MTFC4GACAJC》得知,MTFC4GACAJC 支持HS200、HS400、SDR和DDR模式,最高时钟频率为52MHz。那么上电时MTFC4GACAJC 支持的时钟频率是多少呢?在MTFC4GACAJC手册的EXCSD表中HS_TIMING得知其始化值为00h。

![](第十三章 EMMC编程.assets/13_EMMC_Program_image058.png)

这个值代表什么意思呢?我们通过《eMMC5.0Spec_JESD84-B50》得知,HS_TIMING高字节代表驱动能力,低字节代表时钟速率。HS_TIMING为0时,代表时钟速率为兼容模式,即时钟速率范围为0~26MHz。本实验我们选择USDHC2时钟速率为400KHz。

![](第十三章 EMMC编程.assets/13_EMMC_Program_image059.png)

![](第十三章 EMMC编程.assets/13_EMMC_Program_image060.png)

11.3.2.2 步骤2:USDHC2的时钟配置

在芯片应用手册《Chapter 18 Clock Controller Module》的时钟树图中可以看到USDHC2时钟源是PLL2的PFD0或PFD2。同时我们也可以看出,使能USDHC2时钟所要配置的寄存器为CSCDR1[USDHC2_POPF]、CSCMR1[USDHC2_CLK_SEL]和CCGR6[CG2]。

![](第十三章 EMMC编程.assets/13_EMMC_Program_image061.png)

如下图,USDHC2_CLK_SEL为0时时钟源是PLL2_PFD2,否则是PLL2_PFD0。本实验选择PLL2_PFD2,所以CSCMR1[USDHC2_CLK_SEL]使用默认值即可。

![](第十三章 EMMC编程.assets/13_EMMC_Program_image062.png)

![](第十三章 EMMC编程.assets/13_EMMC_Program_image063.png)

如下图USDHC2_POPF的值代表分频系数,范围为1~8。本实验采用2分频所以CSCDR1[USDHC2_POPF]选择默认值0x1即可。

![](第十三章 EMMC编程.assets/13_EMMC_Program_image064.png)

![](第十三章 EMMC编程.assets/13_EMMC_Program_image065.png)

![](第十三章 EMMC编程.assets/13_EMMC_Program_image066.png)

![](第十三章 EMMC编程.assets/13_EMMC_Program_image067.png)

由上图CCM_CCGR6寄存器可知,CCM_CCGR6[CG2] 的默认值为11,11表示该模块时钟使能(参考《4.3.2 CCM用于设置是否向GPIO模块提供时钟》),所以这里采用初始值即可。

由时钟树图我们可知道此时USDHC2_CLK_ROOT = PLL2_PDF2 / 2。通过查询手册得知PLL2_PDF2初始值396MHz。USDHC2_CLK_ROOT的频率则为198MHz。

为了实现400KHz的时钟频率,我们还要配置USDHC2_SYS_CTRL寄存器中的SDCLKFS[15:8]和DVS[7:4],

![](第十三章 EMMC编程.assets/13_EMMC_Program_image068.png)

![](第十三章 EMMC编程.assets/13_EMMC_Program_image069.png)

![](第十三章 EMMC编程.assets/13_EMMC_Program_image070.png)

​ SDCLKFS是uSDHC时钟预分频系数,当在SDR模式时,分频值为1、2、4、8、16、32、64、128、256。当在

​ DDR模式时,分频值为SDR模式时的一倍。

​ DVS是uSDHC时钟分频系数,范围从1到16。

​ USDHC2_CLK的速率公式存在两种它们分别为:

​ SDR模式: USDHC2_CLK = USDHC2_CLK_ROOT / (SDCLKFS * DVS)

​ DDR模式: USDHC2_CLK = USDHC2_CLK_ROOT / (SDCLKFS * 2* DVS)

​ 本实验我们的uSDHC_CLK设定为400KHz,eMMC上电后工作在SDR模式中,经过计算,选择SDCLKFS设置为40h,DVS设置为3。通过公式计算实际uSDHC_CLK频率为396KHz。

​ 相关代码如下:

192     /* 设置时钟为400KHz. */
193     //Single Data Rate mode,默认使用PLL2_PFD2 = 396Mhz, usdhc2_clk_root = 396Mhz / 2 =  198Mhz
194     //usdhc2_clk = 	usdhc2_clk_root / (prescaler * divisor)
195 	//SDCLKFS = 0x40 = 128分频
196 	//DVS = 0x03 = 4分频
197 	//usdhc2_clk = 198Mhz/(128*4) = 0.387Mhz
198 	usdhc2_reg->SYS_CTRL &= ~((0xFF00) | (0xF0));
199 	usdhc2_reg->SYS_CTRL |= (0x0040U << 8) | (0x0003U << 4);
200 	
201 	/* 等待时钟稳定 */
202 	while (!(usdhc2_reg->PRES_STATE & 0x8U))
203     {
204 		
205     }	

第198行代码:将SYS_CTRL的SDCLKFS和DVS位置0。

第199行代码:将SYS_CTRL的SDCLKFS和DVS位分别设置为0x40和0x03。

第202行代码:设置完时钟后,需要检验PRES_STATE寄存器的SDSTB位,如果该位为1代表时钟稳定可以往下操作,否则继续等待。

![](第十三章 EMMC编程.assets/13_EMMC_Program_image071.png)

![](第十三章 EMMC编程.assets/13_EMMC_Program_image072.png)

详细代码参考01_emmc目录下的代码。

1.3.3 USDHC2软复位

引脚和时钟配置好后,接下来的工作是初始化USDHC2。

通过设置USDHC2_SYS_CTRL的RSTA位使EMMC控制器复位,复位完成时RSTA位硬件清零。通过下图可知RSTA的掩码为0x1000000。

![](第十三章 EMMC编程.assets/13_EMMC_Program_image073.png)

![](第十三章 EMMC编程.assets/13_EMMC_Program_image074.png)

相关代码如下,详细代码参考01_emmc目录下的mmc.c。

209 /**********************************************************************
210  * 函数名称: USDHC_Reset
211  * 功能描述: usdhc复位
212  * 输入参数: unsigned int mask    :
213  *            unsigned int timeout :超时时间
214  * 输出参数: unsigned char
215  * 返 回 值: 0为复位成功,1为复位失败。
216  * 修改日期        版本号     修改人	      修改内容
217  * -----------------------------------------------
218  * 2020/02/28	     V1.0	  LJZ	           创建
219  ***********************************************************************/
220 static unsigned char USDHC_Reset(unsigned int mask, unsigned int timeout)
221 {
222     usdhc2_reg->SYS_CTRL |= mask;
223 	
224     while ((usdhc2_reg->SYS_CTRL & mask) != 0U)
225     {
226         if (timeout == 0U)
227         {
228             break;
229         }
230         timeout--;
231     }
232 
233     return ((!timeout) ? 0 : 1);
234 }
133 
134 /**********************************************************************
135  * 函数名称: USDHC_Init
136  * 功能描述: usdhc初始化
137  * 输入参数: 无
138  * 输出参数: 无
139  * 返 回 值: 无
140  * 修改日期        版本号     修改人	      修改内容
141  * -----------------------------------------------
142  * 2020/02/28	     V1.0	  LJZ	           创建
143  ***********************************************************************/
144 void USDHC_Init(void)
145 {
146 	CCM_CCGR6            = (volatile unsigned int *)(0x20C4080);
147 	
148 	Emmc_PinConfig();
149 	
150 	/* 使能USDHC时钟 */
151 	*CCM_CCGR6 = ((*CCM_CCGR6) & ~(3U << 0x04)) | (((unsigned int)3U) << 0x04);
152 	
153 	/* 复位 USDHC   */
154 	if( USDHC_Reset(0x1000000U, 100U) == 1 )
155 	{
156 		/* 复位成功 */
157 		printf("Reset Success\n\r");
158 	}
159 	else
160 	{
161 		/* 复位失败 */
162 		printf("Reset false\n\r");
163 		return;
164 	}
165    .
166    .
167  .
207 }    

1.3.4 初始化eMMC

1.3.4.1 步骤0:编写发送CMD函数

发送命令我们要做哪些事呢?首先要告诉uSDHC2的命令号、命令类型、应答类型和命令参数等。然后就要判断uSDHC2的状态信息,如:命令发送完成、错误标记等。那么我们就要对uSDHC2_PRES_STATE、uSDHC2_MIX_CTRL和uSDHC2_CMD_XFR_TYP寄存器进行操作。

为了方便函数调用,我们将命令号、命令类型和应答数据和标志位的掩码描述为一个结构体,如下代码所示。

47 typedef enum 
48 {
49 	ResponseTypeNone = 0U,
50 	ResponseTypeR1   = 1U,
51 	ResponseTypeR1b  = 2U,
52 	ResponseTypeR2   = 3U,
53 	ResponseTypeR3   = 4U,
54 	ResponseTypeR4	 = 5U,
55 	ResponseTypeR5   = 6U,
56 	ResponseTypeR5b  = 7U,
57 	ResponseTypeR6   = 8U,
58 	ResponseTypeR7   = 9U
59 }USDHC_Respones_Type;
60 
61 typedef struct
62 {
63 	unsigned int  index;
64 	unsigned int  arg;
65 	unsigned int  response[4];
66 	USDHC_Respones_Type response_type;
67 	unsigned int  mix_ctrl;
68 	unsigned int  xfr_typ;
69 }USDHC_Command;

​ USDHC_Respones_Type是用枚举来描述应答的类型。USDHC_Command结构体中index是命令的编号,arg是命令的参数,response数据是应答的数据,response_type是应答的类型,mix_ctrl是设置MIX_CTRL寄存器的值,xfr_typ_mark是设置CMD_XFR_TYP寄存器的值。

​ 函数的参数已经准备好了,现在开始编写发送命令的函数。在发送CMD之前要判断命令和数据操作是否可以执行。这就要检查uSDHC2_PRES_STATE的状态寄存器的CIHB和CDIHB位。如下图所示当CIHB和CDIHB为1时代表不能发送命令和数据,直到这两个位为0才可以发送CMD。

![](第十三章 EMMC编程.assets/13_EMMC_Program_image075.png)

![](第十三章 EMMC编程.assets/13_EMMC_Program_image076.png)

​ 代码如下:

248 	/* 等待命令发送完成 */
249 	// CIHB掩码:0x01
250 	while( usdhc2_reg->PRES_STATE & 0x01U )
251 	{
252 	}
253 	
254 	/* 等待数据完成 */
255 	// CDIHB掩码:0x02
256 	while( usdhc2_reg->PRES_STATE & 0x02U )
257 	{
258 	}

​ 第248和258行代码分别是对CIHB(0x1U)和CDIHB(0x2U)位进行判断,如果这两个位为1继续等待,直到CIHB和CDIHB位为0退出循环。

​ 确认可以发送CMD时,根据命令的类型配置uSDHC2_MIX_CTRL的DDR_EN(DDR模式)、DTDSEL(数据传输方向,1为读操作,0为写操作)、BCEN(块计数使能)和MSBSEL(单块或多块操作,0为单个块操作,1为多个块操作)位。

![](第十三章 EMMC编程.assets/13_EMMC_Program_image077.png)

![](第十三章 EMMC编程.assets/13_EMMC_Program_image078.png)

代码如下:

262 	/* 配置传输方式 */
263 	// BCEN掩码: 0x02U
264 	// DDR_EN掩码:0x08U
265 	// DTDSEL掩码:0x10U
266 	// MSBSEL掩码:0x20U
267 	usdhc2_reg->MIX_CTRL &= ~(0x20U | 0x2U | 0x10U | 0x8U );
268     usdhc2_reg->MIX_CTRL |= ((command->mix_ctrl) & (0x20U | 0x2U | 0x10U | 0x8U ));	

DDR_EN掩码为0x8U,DTDSEL的掩码为0x10,BCEN的掩码为0x2U,MSBSEL的掩码为0x20U。

第267行代码:将uSDHC2_MIX_CTRL的DDR_EN、DTDSEL、BCEN、MSBSEL设置为0。

第268行代码:将uSDHC2_MIX_CTRL设置为mix_ctrl的值。

配置完传输方式后,将命令参数写入uSDHC2_CMD_ARG寄存器。

270     usdhc2_reg->CMD_ARG = command->arg;

最后根据命令的要求配置uSDHC2_CMD_XFR_TYP寄存器的命令索引、应答类型、命令CRC校验使能、命令检查使能和数据传输使能。

![](第十三章 EMMC编程.assets/13_EMMC_Program_image079.png)

![](第十三章 EMMC编程.assets/13_EMMC_Program_image080.png)

代码如下:

272 	/* 配置命令参数 */
273 	// CDMINX掩码:0x3F000000U
274 	// DPSEL掩码:0x200000U
275 	// CICEN掩码:0x100000U
276 	// CCCEN掩码:0x80000U
277 	// PSPTYP[1:0]掩码:0x30000U
278 	usdhc2_reg->CMD_XFR_TYP &= ~(0x3F000000U | 0x200000U | 0x100000U | 0x80000U | 0x30000U);
279 
280     usdhc2_reg->CMD_XFR_TYP = (((command->index << 24U) & 0x3F000000U) | 
281 	                           ((command->xfr_typ) &(0x3F000000U | 0x200000U | 0x100000U | 0x80000U | 0x30000U))); 

​ CMDINX、DPSEL、CMDTYP、CICEN、CCCEN和RSPTYP对应掩码值分别为0x3F000000U、0x200000U、0x100000U、0x80000U和0x30000U。

​ 第278行代码:将CMD_XFR_TYP寄存器的CMDINX、DPSEL、CMDTYP、CICEN、CCCEN和RSPTYP位清零。

​ 第280行代码:设置CMD_XFR_TYP寄存器命令编号和设置相应的标记位。

​ CMD_XFR_TYP配置完成后uSDHC2就会开始传输命令。此时可以通过查询uSDHC2_INT_STATUS的CC位判断传输是否完成。

![](第十三章 EMMC编程.assets/13_EMMC_Program_image081.png)

​ 具体代码如下:

282    /* 等待命令发送 */
283 	// CC位掩码:0x01
284 	while( !(usdhc2_reg->INT_STATUS & 0x1U ) )
285 	{
286 	}
287 	
288 	/* 清中断标记位 */
289 	// CC位掩码:0x01
290 	usdhc2_reg->INT_STATUS &= 0x01U ;

​ 第284-286行代码:检查INT_STATUS寄存器的CC位是否为1,如果为1则进行后续操作,否则继续等待直到命令发送完毕。

​ 第290行代码:清除INT_STATUS寄存器的CC位。

​ 最后根据应答类型读取应答寄存器的值。IMX6将应答数据 的CRC部分去掉,其余部分保存到 uSDHC2_CMD_RSP0-3寄存器中。如下表所示,除R2应答是120bit,其他的应答都是32位并存储在uSDHC2_CMD_RSP0或uSDHC2_CMD_RSP3。

![](第十三章 EMMC编程.assets/13_EMMC_Program_image082.png)

代码如下:

295 /**********************************************************************
296  * 函数名称: USDHC_ReadResponse
297  * 功能描述: 读取Response
298  * 输入参数: USDHC_Command* command :USDHC命令结构体
299  * 输出参数: 无
300  * 返 回 值: 无
301  * 修改日期        版本号     修改人	      修改内容
302  * -----------------------------------------------
303  * 2020/02/28	     V1.0	  LJZ	           创建
304  ***********************************************************************/
305 void USDHC_ReadResponse(USDHC_Command *command)
306 {
307 	unsigned int i;
308 	
309 	if( command->response_type != ResponseTypeNone )
310 	{
311 		command->response[0] = usdhc2_reg->CMD_RSP0;
312 		
313 		if( command->response_type == ResponseTypeR2 )
314 		{
315 			command->response[1] = usdhc2_reg->CMD_RSP1;
316 			command->response[2] = usdhc2_reg->CMD_RSP2;
317 			command->response[3] = usdhc2_reg->CMD_RSP3;
318 
319 			i = 4;
320 		
321 			do
322 			{
323 				command->response[i - 1] <<= 8;
324 				if (i > 1)
325 				{
326 					command->response[i - 1] |= ((command->response[i - 2] & 0xFF000000U) >> 24U);
327 				}
328 			} while (i--);
329 		}
330 	}
331 }

第313-329代码:为了后续处理方便应答类型为R2时我们将uSDHC2_CMD_RSP0-3的数据左以7位。

1.3.4.2 步骤1:eMMC初始化

发送命令的函数已经准备好了,现在我们编写程序发送第一个命令CMD0。在编程序之前先了解CMD0的信息。从下表可知,CMD0是无应答广播命令,根据参数的不同eMMC进入不同的状态。

类型 参数 应答
无应答广播 0x00000000
无应答广播 0xF0F0F0F0
- 0xFFFFFFFA

本步骤中需要eMMC进入IDLE状态进行复位,所以参数的值为0x00000000。(这里应该怎么说)MIX_CTRL的值应为0x0。CMD_XFR_TYP也设置为0。应答类型为无应答。代码如下:

16 	command.index = 0;
17 	command.arg = 0;
18 	command.mix_ctrl = 0;
19 	command.xfr_typ = 0;
20 	command.response_type = ResponseTypeNone;
21 	
22 	//发送CMD0
23 	USDHC_SendCommand(&command);

根据13.1.9的状态图,此时需要发送CMD1读取eMMC的OCR寄存器,通过第31位判断eMMC是否复位完毕。CMD1的描述如下:

类型 参数 应答类型
有应答广播 0 R3

相关代码如下:

25 	for( i = 0; i < 10; i++ )
26 	{
27 		//发送CMD1
28 		//设置响应类型 R3
29 		//答复长度为48bit,RSPTYP[1:0] = 0b10 = 2
30 		command.index = 1;
31 		command.arg = 0;
32 		command.mix_ctrl = 0;
33 		command.xfr_typ = 2 << 16;
34 		command.response_type = ResponseTypeR3;
35 		
36 		USDHC_SendCommand(&command);
37 		
38 		// 等待EMMC复位完成
39 		// 在OCR中第31为表示EMMC设备是否准备好处理数据,1为准备好,0为忙
40 		if ( command.response[0] & (1U << 31U) )
41 		{
42 			printf("MMC is Ready ok \n\r");
43 			break;
44 		}
45 		else
46 		{
47 			printf("MMC is Busy.\n\r");
48 		}
49 	}

1.3.4.3 参考章节《4-1.4编译程序》编译程序

进入 cd 01_emmc 源码目录

1.3.4.4 参考章节《4-1.4映像文件烧写、运行》烧写、运行程序

此时观察串口现象

![](第十三章 EMMC编程.assets/13_EMMC_Program_image083.png)

1.3.4.5 步骤2:读取CID信息

​ eMMC初始化完成后进入Ready状态。在Ready状态下需要发送CMD2命令,使eMMC进入Identification状态。如下表所示CMD2是有应答广播命令,参数可以是任意值,应答类型为R2。

类型 参数 应答类型
有应答广播 任意值 R2

​ 本步骤中参数设置为0,应答类型设置为ResponseTypeR2。CMD_XFR_TYP的命令CRC检测使能位CCCEN设置为1,应答类型RSPTYP[1:0]设置为1,对应的值为1<<16|1<<19。代码如下:

57         //发送CMD2
58 		//设置响应类型 R2
59 		//答复长度为136bit,RSPTYP[1:0] = 0b01 = 1
60 		command.index = 2;
61 		command.arg = 0;
62 		command.mix_ctrl = 0;
63 		command.xfr_typ = 1 << 16 | 1 << 19;
64 		command.response_type = ResponseTypeR2;
65 		
66 		USDHC_SendCommand(&command);

​ CMD2发送成功后,主机会收到CID寄存器的值。这里打印CID寄存器的ManufacturerID、CBX、ApplicationID和Product name信息,代码如下:

68 		printf("ManufacturerID: 0x%x \n\r", (unsigned char)((command.response[3U] & 
0xFF000000U) >> 24U));
69 		printf("CBX: 0x%x \n\r", (unsigned char)((command.response[3U] & 0x00FF0000U) >> 16U));
70 		printf("ApplicationID: 0x%x \n\r", (unsigned char)((command.response[3U] & 0x0000FF00U) >> 8U));
71 		printf("Product version: 0x%x \n\r", (unsigned char)((command.response[1U] & 0xFF000000U) >> 24U));

​ 详细代码参考02_emmc目录中的代码。接着我们上机验证,实验结果如图。

![](第十三章 EMMC编程.assets/13_EMMC_Program_image084.png)

1.3.4.6 步骤3:配置RCA

​ 成功进入识别模式后,向eMMC发送CMD3配置RCA,使eMMC进入Standby状态。如下表所示,CMD3参数高16位是RCA,低16位为任意值,应答类型为R1。

类型 参数 应答类型
点对点无数据传输 [31:16]RCA,[15:0]任意值 R1

​ 实验中参数的RCA设为1,低16位为0,即赋值为1<<16即可,应答类型设置为ResponseTypeR1。CMD_XFR_TYP的命令编号检测使能位CICEN设置为1,CRC检测使能位CCCEN设置为1,应答类型RSPTYP[1:0]设置为2,CMD_XFR_TYP赋值为2<<16|1<<19|1<<20。代码如下:

73 		// Send CMD3 设置设备相对地址
74 		command.index = 3;
75 		command.arg = 1 << 16;
76 		command.mix_ctrl = 0;
77 		command.xfr_typ = 2 << 16 | 1 << 19 | 1 << 20;
78 		command.response_type = ResponseTypeR1;

​ 命令发送成功后会收到类型为R1的应答。从《13.1.6应答(Response)介绍》中可知,它是关于设备状态的信息,通过检查对应的位,判断设备处于什么状态和出现哪些错误。这里我们检查CRC错误COM_CRC_ERROR和非法命令ILLEGAL_COMMAND着两个错误位即可。代码如下:

82 		// COM_CRC_ERROR:(1U << 23U)
83 		// ILLEGAL_COMMAND:(1U << 22U)
84 		if (!(command.response[0] & ((1U << 23U) | (1U << 22U))))
85 		{
86 			printf("Emmc in Stand-by State\n\r");
87 		}
88 		else
89 		{
90 			printf("SDMMC_R1 Error\n\r");
91 			return 0;
92 		}

1.3.4.7 步骤4:读取CSD和EXCSD

​ 在Standby状态下,通过发送CMD9命令读取CSD信息,CMD9格式如下表所示。

类型 参数 应答类型
点对点无数据传输 [31:16]RCA,[15:0]任意值 R2

​ CMD9参数高16位是RCA,低16位为任意值,应答类型为R2。本实验RCA设为1。应答类型设置为ResponseTypeR2。CMD_XFR_TYP的CRC检测使能位CCCEN设置为1,应答类型RSPTYP[1:0]设置为1, CMD_XFR_TYP的值应为1<<16|1<<19。命令发送成功后会返回CSD信息,这里我们只显示CSD结构版本信息。具体代码如下:

94 		// Send CMD9 读取CSD信息
95 		command.index = 9;
96 		command.arg = 1 << 16;
97 		command.mix_ctrl = 0;
98 		command.xfr_typ = 1 << 16 | 1 << 19;
99 		command.response_type = ResponseTypeR2;
100 		
101 		USDHC_SendCommand(&command);
102 		
103 		printf("CSD structure: %x \n\r", (unsigned char)((command.response[3U] & 0xC0000000U) >> 30U));

​ 因为EXCSD有512字节组成,所以需要通过数据总线传输。在Standby状态下还不能传输数据,需要要跳转到transfer状态下。此时根据《13-1.2 数据传输模式》的状态图可以得知,需要CMD7切换到transfer状态。CMD7的描述如下表所示。

类型 参数 应答类型
点对点无数据传输 [31:16]RCA,[15:0]任意值 R1/R1b

​ CMD7参数高16位是RCA,低16位为任意值,应答类型为R1或R1b。这里参数的RCA设为1。应答类型设置为ResponseTypeR1。CMD_XFR_TYP的命令编号检测使能位CICEN设置为1,CRC检测使能位CCCEN设置为1,应答类型RSPTYP[1:0]设置为2,CMD_XFR_TYP赋值为2<<16|1<<19|1<<20。代码如下:

105 		// Send CMD7 进入tranfaer状态
106 		command.index = 7;
107 		command.arg = 1 << 16;
108 		command.mix_ctrl = 0;
109 		command.xfr_typ = 2 << 16 | 1 << 19 | 1 << 20;
110 		command.response_type = ResponseTypeR1;
111 		
112 		USDHC_SendCommand(&command);

![](第十三章 EMMC编程.assets/13_EMMC_Program_image085.png)

代码如下:

114       if (!(command.response[0] & ((1U << 23U) | (1U << 22U))))
115 		{
116 			printf("Emmc in Transfer State\n\r");
117 		}
118 		else
119 		{
120 			printf("SDMMC_R1 Error\n\r");
121 			return 0;
122 		}

第114行代码:查询设备状态的第22位和23位,如果这两位为0则表示CMD7操作成功。

进入Transfer状态后,发送CMD8读取EXCSD。CMD8信息如下表所示。

类型 参数 应答类型
点对点有数据传输 任意值 R1

CMD8参数设为0。应答类型设置为ResponseTypeR1。CMD_XFR_TYP的命令编号检测使能位CICEN设置为1,CRC检测使能位CCCEN设置为1,应答类型RSPTYP[1:0]设置为2,CMD_XFR_TYP赋值为2<<16|1<<19|1<<20。MIX_CTRL寄存器的数据传输方向DTDSEL位设置位1,其掩码值应为1<<4。相应代码在下面第385到387行代码中。

366 /**********************************************************************
367  * 函数名称: MMC_ReadExCsd
368  * 功能描述: 读ECsd
369  * 输入参数: 无
370  * 输出参数: 无
371  * 返 回 值: 无
372  * 修改日期        版本号     修改人	      修改内容
373  * -----------------------------------------------
374  * 2020/02/28	     V1.0	  LJZ	           创建
375  ***********************************************************************/
376 void MMC_ReadExCsd(void)
377 {
378 	USDHC_Command command;
379 	unsigned char data[512];
380 	
381 	command.index = 8;
382 	command.arg = 0;
383 	//
384 	//
385 	command.mix_ctrl = 1 << 4 | 0x1 << 1;
386 	command.xfr_typ = 2 << 16 | 1 << 19 | 1 << 20 | 1 << 21;
387 	command.response_type = ResponseTypeR1;
388 	
389 	usdhc2_reg->BLK_ATT =  512 | 1 << 16;
390 
391 	USDHC_SendCommand(&command);
392 	
393 	USDHC_ReadWordData((unsigned int*)data);
394 	
395 	printf("Card type: 0x%x \n\r", data[196U]);
396 	printf("DataBusWidth: 0x%x\n\r", data[183U]);
397 	printf("CSD structure: %x\n\r", data[194U]);
398 	printf("Extended CSD revision: %x\n\r", data[192U]);
399 }

第389行代码:设置BLK_ATT寄存器,将每块数据设置为512字节,块计数为1。

第393行代码:调用USDHC_ReadWordData读取缓存中的数据,此函数接下里介绍。

第395-398行代码:打印CSD信息。

下面讲解USDHC_ReadWordData的函数,相关代码如下:

332 /**********************************************************************
333  * 函数名称: USDHC_ReadWordData
334  * 功能描述: 读取数据
335  * 输入参数: unsigned int* data :数据存储地址
336  * 输出参数: 无
337  * 返 回 值: 无
338  * 修改日期        版本号     修改人	      修改内容
339  * -----------------------------------------------
340  * 2020/02/28	     V1.0	  LJZ	           创建
341  ***********************************************************************/
342 void USDHC_ReadWordData(unsigned int* data)
343 {
344 	unsigned int i;
345 	
346 	printf("Waitting Read buffer ready\n\r");
347 	//
348 	//
349 	// BRR的掩码:0x1<<5
350 	while (!( usdhc2_reg->INT_STATUS & (1 << 5) ))
351 	{
352 		
353 	}
354 	
355 	printf("Read buffer has readied\n\r");
356 	usdhc2_reg->INT_STATUS &= (1 << 5);
357 	
358 	for(i=0; i<512/4; i++)
359 	{
360 		data[i] = usdhc2_reg->DATA_BUFF_ACC_PORT;
361 	}
362 	
363 	printf("Read buffer complete\n\r");
364 }

第350行代码,检测INT_STATUS寄存器的BRR位是否为1,判断缓存是否可读。

第358-361行代码,读取512节的数据。

最后打印EXCSD的Card type、DataBusWidth、CSD Structure和EXCsd Version的信息验证程序的正确性。代码如下:

395 	printf("Card type: 0x%x \n\r", data[196U]);
396 	printf("DataBusWidth: 0x%x\n\r", data[183U]);
397 	printf("CSD structure: %x\n\r", data[194U]);
398 	printf("Extended CSD revision: %x\n\r", data[192U]);

接着我们参考《4.4.4 编译程序》与《3.4 映像文件烧写、运行》,上机验证。结果如图。

![](第十三章 EMMC编程.assets/13_EMMC_Program_image086.png)

1.3.4.8 步骤5:配置位宽

接下来我们要将eMMC的数据位宽设置为4bit。相关代码如下:

402 /**********************************************************************
403  * 函数名称: MMC_SetBusWidth
404  * 功能描述: 设置数据总线宽度
405  * 输入参数: 无
406  * 输出参数: 无
407  * 返 回 值: 无
408  * 修改日期        版本号     修改人	      修改内容
409  * -----------------------------------------------
410  * 2020/02/28	     V1.0	  LJZ	           创建
411  ***********************************************************************/
412 void MMC_SetBusWidth(void)
413 {
414 	USDHC_Command command;
415 	
416 	command.index = 6;
417 	command.arg = (3<<24) | (183<<16) | (1<<8) | (0<<0);
418 	command.mix_ctrl = 0;
419 	command.xfr_typ = 3 << 16 | 1 << 19;
420 	command.response_type = ResponseTypeR1b;
421 	
422 	USDHC_SendCommand(&command);
423 	
424 	while(1)
425 	{
426 		command.index = 13;
427 		command.arg = 1U << 16;
428 		command.mix_ctrl = 0;
429 		command.xfr_typ = 2 << 16 | 1 << 19 | 1 << 20;
430 		command.response_type = ResponseTypeR1;
431 
432 		USDHC_SendCommand(&command);
433 	
434 		//
435 		if ( command.response[0] & (1U << 8U) )
436 		{
437 			printf("SetBusWidth Complete. \n\r");
438 			break;
439 		}
440 		else
441 		{
442 			printf("Emmc is Busy. \n\r");
443 		}	
444 	}
445 }

CMD6描述如下表所示。

类型 参数 应答类型
点对点无数据传输 [31:26] 0, [25:24] Access, [23:16] Index, [15:8] Value, [7:3] Set to 0, [2:0] Cmd Set R1b

CMD6参数中第26到31位为0。Access是代表什么呢?查阅《eMMC5.0Spec_JESD84-B50》手册得到下图。当Access为3时,写EXCSD某一个字节,所以这里Access赋值为3。

![](第十三章 EMMC编程.assets/13_EMMC_Program_image087-1641905989040.png)

Index是指EXCSD的字节编号,总线位宽在EXCSD的第183字节中,所以Index设置为183。

![](第十三章 EMMC编程.assets/13_EMMC_Program_image088.png)

在《eMMC5.0Spec_JESD84-B50》中查到BUS_WIDTH数值对应的位宽模式如下图所示。现在我们设置是SDR的4bit数据总线,所以Value应设置为1。

![](第十三章 EMMC编程.assets/13_EMMC_Program_image089.png)

CMD6参数应赋值为(3<<24) | (183<<16) | (1<<8) | (0<<0)。应答类型设置为ResponseTypeR1。CMD_XFR_TYP寄存器的命令编号检测使能位CICEN设置为1,CRC校验使能位设为1,应答类型RSPTYP[1:0]设置为2,对应掩码值应为2<<16|1<<19|1<<20。相应代码在第416至420行中。

在Transfer状态中,只能通过CMD13来读取ORC值,并通过ORC值的第31位判断eMMC是否处理完成。相应的代码在424至444行。

修改eMMC数据总线位宽后,也需要设置IMX6 USDHC2的数据总线位宽。相关代码如下:

447 /**********************************************************************
448  * 函数名称: USDHC_SetBusWidth
449  * 功能描述: 设置USDHC的数据总线宽度
450  * 输入参数: 无
451  * 输出参数: 无
452  * 返 回 值: 无
453  * 修改日期        版本号     修改人	      修改内容
454  * -----------------------------------------------
455  * 2020/02/28	     V1.0	  LJZ	           创建
456  ***********************************************************************/
457 void USDHC_SetBusWidth(void)
458 {
459 	usdhc2_reg->PROT_CTRL &= ~(6U << 1);
460 	usdhc2_reg->PROT_CTRL |= 1U<<1;
461 }

我们通过USDHC2_PROT_CTRL寄存器的DTW[1:0]修改USDHC2的数据总线位宽。如下图所示,DTW[1:0]应设置为0x1。

![](第十三章 EMMC编程.assets/13_EMMC_Program_image090.png)

第459行代码:将USDHC2_PROT_CTRL寄存器的DTW[1:0]清0。

第460行代码:将USDHC2_PROT_CTRL寄存器的DTW[1:0]赋值为0x01。

详细代码请参考03_emmc目录下的main.c、mmc.c和mmc.h。

1.3.5 读写实验

接下来开始读写实验,在读写操作之前要设置Block的大小。设置Block的大小使用CMD16来完成。CMD16描述如下:

类型 参数 应答类型
点对点无数据传输 [31:26] Block长度 R1

设置Block的函数代码如下:

463 /**********************************************************************
464  * 函数名称: MMC_SetBlockSize
465  * 功能描述: 设置MMC的Block大小
466  * 输入参数: 无
467  * 输出参数: 无
468  * 返 回 值: 无
469  * 修改日期        版本号     修改人	      修改内容
470  * -----------------------------------------------
471  * 2020/02/28	     V1.0	  LJZ	           创建
472  ***********************************************************************/
473 void MMC_SetBlockSize(void)
474 {
475 	USDHC_Command command;
476 	
477 	command.index = 16;
478 	command.arg = 512;
479 	command.mix_ctrl = 0;
480 	command.xfr_typ = 2 << 16 | 1 << 19;
481 	command.response_type = ResponseTypeR1;
482 
483 	USDHC_SendCommand(&command);
484 	
485 	//COM_CRC_ERROR:1U << 23U
486 	//ILLEGAL_COMMAND:1U << 22U
487 	if (!(command.response[0] & ((1U << 23U) | (1U << 22U))))
488 	{
489 		printf("Emmc Set Blocksize Ok\n\r");
490 	}
491 	else
492 	{
493 		printf("Emmc Set Blocksize Error\n\r");
494 	}
495 }

第477-481行代码: 设置CMD16的参数为512,CMD_XFR_TYP寄存器的CRC校验使能位CCCEN设置为1,应答类型RSPTYP[1:0]设置为2,对应掩码值应为2<<16|1<<19。

第487行代码:检查R1应答中的CRC错误位COM_CRC_ERROR和非法命令ILLEGAL_COMMAND错误位。

读数据的函数在读EXCSD的时候已经编写完成,这里添加一个写数据的函数,相关代码如下:

497 /**********************************************************************
498  * 函数名称: MMC_WriteBlock
499  * 功能描述: 写一个BLOCK数据到EMMC
500  * 输入参数: unsigned int address: 地址
501  *            const unsigned int* data:   数据存储地址
502  * 输出参数: 无
503  * 返 回 值: 无
504  * 修改日期        版本号     修改人	      修改内容
505  * -----------------------------------------------
506  * 2020/02/28	     V1.0	  LJZ	           创建
507  ***********************************************************************/
508 void MMC_WriteBlock(unsigned int address, const unsigned int* data)
509 {
510 	USDHC_Command command;
511 	unsigned int i;
512 	
513 	command.index = 24;
514 	command.arg = address;
515 	command.mix_ctrl = 0;
516 	command.xfr_typ = 2 << 16 | 1 << 19 | 1 << 20 | 1 << 21;
517 	command.response_type = ResponseTypeR1;
518 	
519 	usdhc2_reg->BLK_ATT =  512 | 1 << 16;
520 
521 	USDHC_SendCommand(&command);
522 	
523 	//COM_CRC_ERROR:1U << 23U
524 	//ILLEGAL_COMMAND:1U << 22U
525 	if (!(command.response[0] & ((1U << 23U) | (1U << 22U))))
526 	{
527 		printf("CMD24 Success\n\r");
528 		
529 		for(i=0; i<512/4; i++)
530 		{
531 			usdhc2_reg->DATA_BUFF_ACC_PORT = data[i];
532 		}
533 	}
534 	else
535 	{
536 		printf("CMD24 Error\n\r");
537 	}
538 }

第513-517行代码:CMD24参数设为地址值。应答类型设置为ResponseTypeR1。CMD_XFR_TYP寄存器的命令编号检测使能位CICEN设置为1,CRC校验使能位设为1,数据传输选择位DPSEL为1,应答类型RSPTYP[1:0]设置为2,CMD_XFR_TYP寄存器值应为2 << 16 | 1<<19 |1<<20 | 1<<21。

这里还要添加一个读Block数据函数,其代码为:

540 /**********************************************************************
541  * 函数名称: MMC_ReadBlock
542  * 功能描述: 读一个BLOCK数据
543  * 输入参数: unsigned int address: 地址
544  *            unsigned int* data:   数据存储地址
545  * 输出参数: 无
546  * 返 回 值: 无
547  * 修改日期        版本号     修改人	      修改内容
548  * -----------------------------------------------
549  * 2020/02/28	     V1.0	  LJZ	           创建
550  ***********************************************************************/
551 
552 void MMC_ReadBlock(unsigned int address, unsigned int* data)
553 {
554 	USDHC_Command command;
555 	unsigned int i;
556 	
557 	command.index = 17;
558 	command.arg = address;
559 	
560 	
561 	command.mix_ctrl = 0x1 << 4 | 0x1 << 1;
562 	command.xfr_typ = 2 << 16 | 1 << 19 | 1 << 20 | 1 << 21;
563 	command.response_type = ResponseTypeR1;
564 	
565 	usdhc2_reg->BLK_ATT =  512 | 1 << 16;
566 
567 	USDHC_SendCommand(&command);
568 	
569 	// COM_CRC_ERROR:1U << 23U
570 	//ILLEGAL_COMMAND:1U << 22U
571 	if (!(command.response[0] & ((1U << 23U) | (1U << 22U))))
572 	{
573 		printf("CMD17 Success\n\r");
574 	}
575 	else
576 	{
577 		printf("CMD17 Error\n\r");
578 	}
579 	
580 	USDHC_ReadWordData(data);
581 }

第561-563行代码:CMD17参数设为地址值。应答类型设置为ResponseTypeR1。CMD_XFR_TYP寄存器的命令编号检测使能位CICEN设置为1,CRC校验使能位设为1,数据传输选择位DPSEL为1,应答类型RSPTYP[1:0]设置为2,CMD_XFR_TYP寄存器值应为2 << 16 | 1<<19 |1<<20 | 1<<21。MIX_CTRL寄存器的数据传输方向DTDSEL位设为1。

在主函数上分别调用这几个函数,相关代码如下:

152 		//设置EMMC的Block大小
153 		MMC_SetBlockSize();
154 		
155 		for(i=0; i<512/4; i++)
156 		{
157 			WriteData[i] = i;
158 		}
159 		//写一个Block数据
160 		MMC_WriteBlock(0,WriteData);
161 		//读数据
162 		MMC_ReadBlock(0,ReadData);
163 		
164 		for(i=0; i<512/4; i++)
165 		{
166 			printf("Data[%d]: %d\n\r", i, ReadData[i]);
167 		}

1.3.6 参考章节《4-1.4编译程序》编译程序

进入 cd 04_emmc 源码目录进行编译。

1.3.7 参考章节《4-1.4映像文件烧写、运行》烧写、运行程序

此时查看串口输出信息

详细代码请参考04_emmc目录下的main.c、mmc.c和mmc.h。

第十四章:TF卡编程

第十四章:TF卡编程

14. TF卡编程

14.1 背景知识

​ 多媒体存储卡(英语:Multimedia Card),又译MMC卡,是一种快闪记忆卡标准。在1997年由西门子及闪迪共同开发,技术基于东芝的NAND快闪记忆技术,因此较早期基于Intel NOR快闪记忆技术的存储卡(例如CF卡)更细小。MMC卡大小与一张邮票差不多,约24mm x 32mm x 1.5mm。MMC卡原本使用1bit串联接口,但较新的标准则容许同时发送4 bit或8 bits的数据。近年MMC卡技术已差不多完全被SD卡所代替;但由于MMC卡仍可被兼容SD卡的设备所读取,因此仍有其作用。这项技术一个公开标准,所有愿意改进它或者为它开发产品的公司都可使用。

​ Secure Digital,缩写为SD,全名为Secure Digital Memory Card,中文翻译为安全数字卡,为一种存储卡,被广泛地于便携式设备上使用,例如数字相机、个人数码助理和多媒体播放器等。SD卡是东芝在MMC卡技术中加入加密技术硬件而成,由于MMC卡可能会较易让用户复制数字音乐,东芝便加入这些技术希望令音乐业界安心。SD卡的技术是建基于MultiMedia卡格式上。SD卡有比较高的数据发送速度,而且不断更新标准。大部分SD卡的侧面设有写保护控制,以避免一些数据意外地写入,而少部分的SD卡甚至支持数字版权管理的技术。SD卡的大小为32mm×24mm×2.1mm,但官方标准亦有记载“薄版”1.4mm厚度,与MMC卡相同。

​ Micro SD卡原本称为TF卡(T-Flash卡或TransFlash),由摩托罗拉与闪迪共同研发,在2004年推出。不过闪迪无法自行将它推广普及化,前期仅有摩托罗拉的手机支持TransFlash。为了能将销路完全拓展,闪迪于是将TransFlash规格并入SD协会,成为SD家族产品之一,造就了当前使用最广泛的手机存储卡;而重命名为microSD的原因是因为被SD协会(SDA)采纳。TransFlash纳入SD协会后,仅有在实体规格的电性接触端(印刷电路板的金属端子,俗称金手指)缩短一点,其他部分完全沿用完全兼容。另一些被SD协会采纳的存储卡包括miniSD和SD卡。这类存储卡主要于移动电话使用,但因其拥有体积极小的优点,随着不断提升的容量,它慢慢开始于GPS设备、便携式音乐播放器和一些闪存盘中使用。microSD的体积为 15 毫米 x 11 毫米 x 1 毫米 - 约为SD卡的1/4,相当于手指甲盖的大小,它亦能够以转接器来接驳于SD卡或Memory Stick插槽中使用。

​ eMMC (Embedded Multi Media Card)是MMC协会订立、主要针对手机或平板电脑等产品的内嵌式存储器标准规格。eMMC在封装中集成了一个控制器,提供标准接口并管理闪存,使得手机厂商就能专注于产品开发的其它部分,并缩短向市场推出产品的时间。该架构标准将MMC组件(闪存加控制器)放入一个小的球栅数组封装(BGA)中,是一种主要用于印刷电路板的嵌入式非易失性存储器系统。eMMC与MMC的其他版本有明显的不同,因为eMMC不是用户可随意移动的卡,而是永久性的电路板附件。

​ SD插口的用途不止是插存储卡。还支持很多SDIO接口的外设,像GPS接收器,Wi-Fi或蓝牙适配器,调制解调器,局域网适配器,条码读取器,FM无线电,电视接收器,射频识别读取器,或者数字相机等等采用SD标准接口的设备。SDIO和SD卡规范间的一个重要区别是增加了低速标准。SDIO卡只需要SPI和1位SD传输模式。低速卡的目标应用是以最小的硬件开支支持低速I/ O能力。低速卡支持类似调制解调器、条码扫描仪和GPS接受器等应用。对“组合”卡(存储器+ SDIO)而言,全速和4位操作对卡内存储器和SDIO部分都是强制要求的。

14.2 通用的概念和协议介绍

参考的协议文档:

JEDEC的JESD84-B451 或者 JESD84-B51,可以在JEDEC网站上注册一个用户进行下载。

SD Card Association 的三个文档:

Physical_Layer_Simplified_Specification_Ver6.00,

SD Host_Controller_Simplified_Specification_Ver3.00,

SDIO可以参考SDIO_Simplified_Specification_Ver3.00。

这几个文档都可以直接在SD Card Association的网站上下载。

参考资料:芯片手册《Chapter 58 : Ultra Secured Digital Host Controller (uSDHC)》。

14.2.1 系统特性

14.2.1.1 SD系统特性

​ (1)SDSC,Standard Capacity SD Memory Card,最大到2GB

​ (2)SDHC,High Capacity SD Memory Card,2GB到32GB

​ (3)SDXC, Extended Capacity SD Memory Card ,32GB到2TB

​ (1)高电压SD卡,电压范围2.7-3.6V

​ (2)UHS-II存储卡,电压范围VDD1:2.7-3.6V,VDD2:1.70V-1.95V

14.2.1.2 eMMC系统特性

​ eMMC设备定义了一种非直接存储访问的机制。通过一个特别的控制器来实现存储的访问。非直接存储访问的优点是在不用主机软件的的支持下,可以在设备中执行几种存储管理任务。这样可以简化主机的闪存管理层。eMMC支持一下特性。

image-20220112141205032

​ (1)时钟频率范围0~200MHz

​ (2)三种数据总线宽度模式:1bit(默认),4bit,8bit

14.2.2 总线模式

14.2.2.1 SD卡总线速度模式

Bus speed mode for UHS-I Card

image-20220112141322603

Default Speed Mode 3.3V signaling, Frequency up to 25MHz, up to 12.5MB/sec

High Speed Mode 3.3V signaling, Frequency up to 50MHz, up to 25MB/sec

SDR12 UHS-I 1.8V signaling, Frequency up to 25MHz, up to 12.5MB/sec

SDR25 UHS-I 1.8V signaling, Frequency up to 50MHz, up to 25MB/sec

SDR50 UHS-I 1.8V signaling, Frequency up to 100MHz, up to 50MB/sec

SDR104 UHS-I 1.8V signaling, Frequency up to 208MHz, up to 104MB/sec

DDR50 UHS-I 1.8V signaling, Frequency up to 50MHz, up to 50MB/sec

UHS156 UHS-II RCLK Frequency Range 26MHz~52MHz, up to 1.56Gbps per lane

UHS624 UHS-II RCLK Frequency Range 26MHz~52MHz, up to 6.24Gbps per lane

开发板SD卡槽的电压为3.3v,所以SD卡的模式只能设置为default speed或者High speed模式。

14.2.2.2 eMMC总线速度模式

image-20220112141353206

开发板eMMC的电压为3.3v,所以只能设置成default speed或者high speed模式。

14.2.3 SD卡和eMMC寄存器

14.2.3.1 sd卡的构成

image-20220112141429981

​ 上图显示了标准SD卡的形状和内部的构成。通过VDD、两个GND、CLK、CMD和DAT0~DAT3总共9个外部引脚供获取电源和与主机进行通信。内部由卡接口控制器、寄存器、上电检测模块、存储介质接口和存储介质等构成。卡接口控制器主要对内部的存储核心进行控制和管理,通过接收用户对其发送的命令来进行控制和设置,并根据命令做出响应,执行数据读写等操作。寄存器提供卡的状态信息和反映卡的运行状态。存储介质主要由具有存储功能的flash块组成。

14.2.3.2 sd卡的寄存器

名称 宽度 描述 实现
CID 128 Card identification number,设备识别代码 强制
RCA 16 Relative Card Address,相对设备地址 强制
DSR 16 Driver Stage Register, 可选
CSD 128 Card Specific Data,设备特定数据 强制
SCR 64 SD Configuration Register,SD配置寄存器 强制
OCR 32 Operation Condition Register,运行条件寄存器 强制
SSR 512 SD status,SD状态 强制
CSR 32 Card status,Card状态 强制

14.2.3.3 MMC的构成

image-20220112141532242

​ 从上图可以看出,eMMC是把Nand flash存储阵列和Device Controller模块封装在一起。Device Controller负责flash阵列的管理,提供标准接口与主机进行通信。这样主机软件不用处理繁杂的Nand Flash的兼容性和坏块管理问题,简化软件设计,加快产品上市时间。

14.2.3.4 eMMC的寄存器

image-20220112141550937

名称 宽度(字节) 描述 实现
CID 16 Device Identification number,设备识别代码 强制
RCA 2 Relative Device Address,相对设备地址,在初始化过程中由主机动态分配 强制
DSR 2 Driver Stage Register,用来配置设备的驱动能力 可选
CSD 16 Device Specific Data,设备特定数据,包含设备的操作条件 强制
OCR 4 Operations Conditions Register,运行条件寄存器,特定的广播命令来识别设备的电压类型 强制
EXT_CSD 512 Extended Device Specific Data,扩展的设备特定数据,包含设备的能力和选择的模式,v4.0开始存在 强制

14.2.4 命令(COMMAND)和响应(RESPONSE)

14.2.4.1 命令类型

image-20220112141618129

​ 上图是命令的格式,每个命令令牌都以一个开始位(“ 0”)开头,并以一个结束位(“ 1”)结束。总长度为48个位。每个令牌均受CRC位保护,因此可以检测到传输错误,检测到错误后可以重发命令。

SD和eMMC都是4种类型:

image-20220112141644353

​ 上图中前一个命令CMD线上只传输了command;后一个命令CMD线上主机给卡发送command后,卡给主机回应了一个response;这两个命令都是只用到了CMD线,没有用到DAT线。如果命令参数里没有指定RCA,那么前一个命令就是就是bc类型,后一个命令就是bcr类型。如果后一个命令的参数里指定了RCA,那么就是ac类型。

image-20220112141707356

​ 上图显示了多块读操作,属于adtc类型,这种类型既要在命令参数里指定RCA,CMD线上传输的command后会有response,并且会使用DAT线传输数据。对于读操作,数据由卡发往主机。

image-20220112141712729

​ 上图显示了多块写操作,与读操作类似,属于adtc类型,不过数据由主机发往卡。

14.2.4.2 响应类型

响应通过CMD线发送。

对于SD卡和eMMC卡都有:

image-20220112141745166

上图显示了SD卡的响应格式,对于SD卡还有:

image-20220112141756135

上图显示了eMMC卡的响应的格式,对于eMMC卡还有:

14.3 IMX6ULL的uSDHC控制器

14.3.1 uSDHC控制器特性介绍:

​ (1)4线SDR(Single Data Rate)模式SDIO卡数据传输速率最高到832Mbps

​ (2)4线DDR(Dual Data Rate)模式SDIO卡数据传输速率最高到400Mbps

​ (3)4线SDR(Single Data Rate)模式SDXC卡数据传输速率最高到832Mbps

​ (4)4线DDR(Dual Data Rate)模式SDXC卡数据传输速率最高到400Mbps

​ (5)8线SDR(Single Data Rate)模式MMC卡数据传输速率最高到416Mbps

​ (6)8线DDR(Dual Data Rate)模式MMC卡数据传输速率最高到832Mbps

14.3.2 数据传输模式

uSDHC可以选择如下模式进行数据传输:

14.3.3 引脚概述

image-20220112142021189

上图是100sdk_imx6ull开发板的SD卡原理图,使用了uSDHC1控制器。

image-20220112142025607

上图是100sdk_imx6ull开发板eMMC的原理图,接在uSDHC2控制器上。

uSDHC有14个I/O管脚,分别是

14.3.4 时钟

Clock name Clock Root Description
hclk ahb_clk_root AHB bus clock
ipg_clk ipg_clk_root Peripheral clock
ipg_clk_perclk usdhc_clk_root Base clock
ipg_clk_s ipg_clk_root Peripheral access clock for register accesses

​ 上表是uSDHC的时钟源,hclk提供了AHB总线的时钟,ipg_clk提供了Peripheral的时钟,ipg_clk_perclk提供了uSDHC控制器的时钟,ipg_clk_s提供了访问控制器寄存器的时钟,这些时钟如何配置呢?需要根据时钟树进行配置。

image-20220112142122012

​ 上图是时钟树的片段,来自于imx6ull reference manual的chapter18 Clock Controller Module章节的18.3 CCM Clock Tree。可以看出CCM_CSCMR1[USDHCn_CLK_SEL]选择时钟源,CSCDRn[USDHC1_PODF]来控制分频,最终产生USDHCn_CLK_ROOT时钟。寄存器说明如下:

CCM_CSCMR1寄存器片段

image-20220112142130658

CSCDR1[USDHC1_PODF]

image-20220112142135423

​ 从上图可以看出,USDHC1和USDHC2的时钟可以来自PLL2PFD0或者PLL2PFD2,由CCM_CSCMR1来控制,然后由CSCDR1[USDHCn_PODF]来控制分频(1,2,…,8)。默认设置使用PLL2PFD0,PLL2PFD0设置为396M,如果USDHCn_PODF都设置为1的话,即对PLL2PFD2进行二分频,这样USDHC1_CLK_ROOT和USDHC1_CLK_ROOT都为198M。

​ Card_clk是由peripheral source clock分成两个阶段产生的,其实就是两个通过两个分频器产生的。

image-20220112142205025

​ 第一个分频是由uSDHCx_SYS_CTRL[DVS]控制产生DIV,它的值可以等于Base,ase/2, Base/3, ..., 或者 Base/16。

​ 第二个分频是由uSDHCx_SYS_CTRL[SDCLKFS]控制产生内部工作时钟card_clk。

​ 在SDR模式和DDR模式时,CLK是不同的:SDR模式时,CLK等于内部工作时钟card_clk;DDR模式时,CLK等于card_clk/2。

​ Base是来自USDHCx_CLK_ROOT,如果USDHCx_CLK_ROOT为198M的话,就要由uSDHCx_SYS_CTRL[DVS]和uSDHCx_SYS_CTRL[SDCLKFS]分频产生400K,25M,26M,50M,52M等时钟。

14.3.5 ADMA引擎

​ SD主机控制器标准里定义了一种新的DMA传输算法,该算法被称为ADMA(Advanced DMA)。竟然是advanced,是不是之前还有个算法呢?的确是有的,称为SDMA(Simple DMA)。

​ 对于SDMA,当页边界(page boundary)到达时,会产生DMA中断,然后新的地址需要再次写入到DMA System Address (uSDHCx_DS_ADDR)寄存器。由于每个页边界都会产生中断,SDMA算法会导致性能瓶颈。比如对于一个主机控制器,页边界是512KB,那就意味着每次中断里最多传输512KB的数据,并且一次传输的数据不能跨越512KB的边界,然后需要重新对DMA System addreass进行编程。

​ 为了能达到更高的传输速度,ADMA采用了分散聚集DMA算法(scatter gather algorithm)。在ADMA传输前,主机驱动可以事先在系统内存里准备好一个描述符表(descriptor table),然后将描述符表的地址写入ADMA System Address (uSDHCx_ADMA_SYS_ADDR)寄存器。主机会根据描述符表里的内容进行传输,这减少了DMA的中断频率,在描述符表里指向的内容传输完成前就不用打断CPU,自然地提高了传输的速度。

​ ADMA也有两种,一个是ADMA1,另一个是ADMA2。ADMA1需要内存里传输数据的地址4KB对齐。ADMA2改进了这个限制,传输的数据可以在内存里任何位置,传输的大小也可以设置。主机控制标准第三个版本里对ADMA1已经不支持了,后面说到ADMA都是指ADMA2。

image-20220112142231919

(图来源于PartA2_SD Host_Controller_Simplified_Specification_Ver3.00 page14)

上图是ADMA2的框图,主机驱动在系统内存里创建描述符表。每个描述符行(也就是一个执行单元)包括三个域地址(address),长度(length)和属性(attribute)。属性指定了每个描述符行对应的操作。ADMA2不使用ADMA System Address寄存器,而是使用ADMA System Address寄存器指向描述符表。然后ADMA2会从描述表里逐行提取描述符行,依次执行。

上图是一个ADMA2编程的示例。主机驱动里描述符表每行都表示一段内存里连续的空间,分别是内存里这段区间的起始地址,长度和属性。ADMA2会逐行解析描述符,依次传输每段连续内存空间。

对描述符的编程有3个要求:

(1)最小的地址单元是4字节;

(2)每个描述符行描述的地址长度低于64KB;

(3)总长度(Length1+length2+…+lengthn)等于块大小的倍数。

Block count寄存器限制最大传输65535个块,如果ADMA2操作不大于65535个块,block count可以使用。总长度大小需要等于块大小(block size)与块数目(block count)的乘积。

image-20220112142239359

描述符表

image-20220112142245267

上图是32-bit地址描述符表,一个描述符行需要64字节空间。属性值用来控制传输,Nop表明直接跳过这行描述符,取下一个描述符。Rsv操作与NOP相同。Tran操作表明传输以这行描述里32-bit地址为起始地址,长度为16-bit length指定的内存空间。Link用来指定另一个描述符表,可见描述之间也可以向链表那样串起来。属性里的int为1的话表示操作完该描述行后产生DMA中断。End表明描述符结束了,这是最后一个要传输的描述符。Valid为1表明改行描述符是有效的,为0的话产生ADMA error,并且停止ADMA传输。

14.3.6 寄存器介绍

SD Host_Controller_Simplified_Specification_Ver3.00里的标准的SD主机控制器寄存器:

image-20220112142257497

下表示IMX6ULL的寄存器表格:

Absolute address (hex) Register name Width (in bits) Access Reset value Section/ page
219_0000 DMA System Address (uSDHC1_DS_ADDR) 32 R/W 0000_0000h 58.8.1/4014
219_0004 Block Attributes (uSDHC1_BLK_ATT) 32 R/W 0000_0000h 58.8.2/4015
219_0008 Command Argument (uSDHC1_CMD_ARG) 32 R/W 0000_0000h 58.8.3/4017
219_000C Command Transfer Type (uSDHC1_CMD_XFR_TYP) 32 R/W 0000_0000h 58.8.4/4017
219_0010 Command Response0 (uSDHC1_CMD_RSP0) 32 R 0000_0000h 58.8.5/4021
219_0014 Command Response1 (uSDHC1_CMD_RSP1) 32 R 0000_0000h 58.8.6/4021
219_0018 Command Response2 (uSDHC1_CMD_RSP2) 32 R 0000_0000h 58.8.7/4022
219_001C Command Response3 (uSDHC1_CMD_RSP3) 32 R 0000_0000h 58.8.8/4022
219_0020 Data Buffer Access Port (uSDHC1_DATA_BUFF_ACC_PORT) 32 R/W 0000_0000h 58.8.9/4024
219_0024 Present State (uSDHC1_PRES_STATE) 32 R 0000_8080h 58.8.10/ 4024
219_0028 Protocol Control (uSDHC1_PROT_CTRL) 32 R/W 0880_0020h 58.8.11/ 4030
219_002C System Control (uSDHC1_SYS_CTRL) 32 R/W 8080_800Fh 58.8.12/ 4035
219_0030 Interrupt Status (uSDHC1_INT_STATUS) 32 w1c 0000_0000h 58.8.13/ 4038
219_0034 Interrupt Status Enable (uSDHC1_INT_STATUS_EN) 32 R/W 0000_0000h 58.8.14/ 4044
219_0038 Interrupt Signal Enable (uSDHC1_INT_SIGNAL_EN) 32 R/W 0000_0000h 58.8.15/ 4047
219_003C Auto CMD12 Error Status (uSDHC1_AUTOCMD12_ERR_STATUS) 32 R 0000_0000h 58.8.16/ 4050
219_0040 Host Controller Capabilities (uSDHC1_HOST_CTRL_CAP) 32 R 07F3_B407h 58.8.17/ 4053
219_0044 Watermark Level (uSDHC1_WTMK_LVL) 32 R/W 0810_0810h 58.8.18/ 4056
219_0048 Mixer Control (uSDHC1_MIX_CTRL) 32 R/W 8000_0000h 58.8.19/ 4057
219_0050 Force Event (uSDHC1_FORCE_EVENT) 32 W (always reads 0) 0000_0000h 58.8.20/ 4059
219_0054 ADMA Error Status Register (uSDHC1_ADMA_ERR_STATUS) 32 R 0000_0000h 58.8.21/ 4062
219_0058 ADMA System Address (uSDHC1_ADMA_SYS_ADDR) 32 R/W 0000_0000h 58.8.22/ 4064
219_0060 DLL (Delay Line) Control (uSDHC1_DLL_CTRL) 32 R/W 0000_0200h 58.8.23/ 4065
219_0064 DLL Status (uSDHC1_DLL_STATUS) 32 R 0000_0000h 58.8.24/ 4067
219_0068 CLK Tuning Control and Status (uSDHC1_CLK_TUNE_CTRL_STATUS) 32 R/W 0000_0000h 58.8.25/ 4068
219_00C0 Vendor Specific Register (uSDHC1_VEND_SPEC) 32 R/W 2000_7809h 58.8.26/ 4070
219_00C4 MMC Boot Register (uSDHC1_MMC_BOOT) 32 R/W 0000_0000h 58.8.27/ 4073
219_00C8 Vendor Specific 2 Register (uSDHC1_VEND_SPEC2) 32 R/W 0000_0006h 58.8.28/ 4074
219_00CC Tuning Control Register (uSDHC1_TUNING_CTRL) 32 R/W 0021_2800h 58.8.29/ 4076

将两个表格进行对比发现有很多寄存器是类似,甚至寄存器里的很多bit位都是类似的,但是uSDHC与标准SD主机控制器寄存器也有一些差异。

14.3.6.1 DMA System Address (uSDHCx_DS_ADDR)

SDMA传输时的物理内存的地址,用ADMA2时不是设置这个寄存器。

image-20220112142313267

14.3.6.2 Block Attributes (uSDHCx_BLK_ATT)

用来设置块的数目和大小。

image-20220112142340224

BLKCNT当前传输的块数目。

BLKSIZE传输的块的大小。

14.3.6.3 Command Argument (uSDHCx_CMD_ARG)

SD/MMC命令的参数。

image-20220112142404487

14.3.6.4 Command Transfer Type (uSDHCx_CMD_XFR_TYP)

用来控制数据传输的操作。

image-20220112142415095

Multi/Single Block Select Block Count Enable Block Count Function
0 Don't Care Don't Care Single Transfer
1 0 Don't Care Infinite Transfer
1 1 Positive Number Multiple Transfer
1 1 Zero No Data Transfer
Response Type Index Check Enable CRC Check Enable Name of Response Type
00 0 0 No Response
01 0 1 R2
10 0 0 R3,R4
10 1 1 R1,R5,R6
11 1 1 R1b,R5b

CMDINDEX command index命令的编号

CMDTYPE command type命令类型,大部分都是设置为0,normal command

DPSEL Data Present Select是否包含数据,为1表示会用到数据线,有两种情况,分别为

CICEN command index check enable为1表示检查response里的index

CCCEN command crc check enable 检查response里的CRC

RSPTYP response type response的类型00表示不需要响应,01表示响应长度是136,10表示长度是48,11表示长度是48并且需要检测忙信号

14.3.6.5 Command Response0 (uSDHCx_CMD_RSP0)

image-20220112142444186

14.3.6.6 Command Response1 (uSDHCx_CMD_RSP1)

image-20220112142451825

14.3.6.7 Command Response2 (uSDHCx_CMD_RSP2)

image-20220112142458607

14.3.6.8 Command Response3 (uSDHCx_CMD_RSP3)

image-20220112142504119

以上四个寄存器分别是从卡发送命令后得到的响应

Response Type Meaning of Response Response Field Response Register
R1,R1b (normal response) Card Status R[39:8] CMDRSP0
R1b (Auto CMD12 response) Card Status for Auto CMD12 R[39:8] CMDRSP3
R2 (CID, CSD register) CID/CSD register [127:8] R[127:8] {CMDRSP3[23:0], CMDRSP2, CMDRSP1, CMDRSP0}
R3 (OCR register) OCR register for memory R[39:8] CMDRSP0
R4 (OCR register) OCR register for I/O etc. R[39:8] CMDRSP0
R5, R5b SDIO response R[39:8] CMDRSP0
R6 (Publish RCA) New Published RCA[31:16] and card status[15:0] R[39:9] CMDRSP0

上图是响应类型与response寄存器的对应关系(来自reference manual P4022)

14.3.6.9 Data Buffer Access Port(uSDHCx_DATA_BUFF_ACC_PORT)

使用外部DMA时会用到,使用ADMA2时,该寄存器一直是0,不用管。

image-20220112142534276

14.3.6.10 Present State (uSDHCx_PRES_STATE)

只读寄存器,

image-20220112142541528

​ DLSL[7:0] data line signal level DATA引脚电平状态,可以用来调试以及检测DATA引脚从错误中恢复的情况。可以检测DATA0引脚电平来反映是否正忙。

​ CLSL CMD line Signal Level CMD引脚的电平状态,可以用来调试以及检测CMD引脚从错误中恢复的情况。

​ WPSPL Write Protect Swith Pin Level 写保护开关引脚电平。反映卡槽WP引脚的状态

​ CDPL Card Detect Pin Level 这个位反映卡槽CD_B引脚的状态,不过与CD_B脚的状态相反,uSDHC不对去抖。

​ CINST Card Inserted 卡插入状态。反映卡是否插入,uSDHC会对该信号去抖动。

​ TSCD Tape Select Change Done。反映延时设置,和tuning有关。

​ RTR Re-Tuning Request 采样时钟是否需要重新tuning,只有SD3.0 SDR104模式会用到。

​ BREN Buffer READ ENABLE 读缓冲区使能,非DMA传输时才会用到。

​ BWEN Buffer Write ENABLE 写缓冲区使能,非DMA传输时才会用到。

​ RTA READ Transfer active 正在进行读操作

​ WTA Write Transfer Active 正在进行写操作

​ SDOFF SDCLOCK Gated Off Internally SD时钟是否关闭

​ PEROFF IPG_PERCLK Gated Off Internally IPG_PERCLK时钟是否关闭

​ HCKOFF HCLK Gated Off Internally HCLK时钟是否关闭

​ IPGOFF IPG_CLK Gated Off Internally IPG_CLK时钟是否关闭

​ SDSTB SD时钟是否稳定,在修改时钟时,需要等待时钟稳定。

​ DLA Data Line Active Data线是否在使用。

​ CDIHB Command Inhabit(Data)当data线正在使用或者读传输正在进行,该位置位。

​ CIHB Command Inhabit 当为0时,表明CMD线不在使用,uSDHC可以发送SD/MMC命令。

14.3.6.11 Protocol Control (uSDHCx_PROT_CTRL)

image-20220112142627014

NON_EXACT_BK_READ 当前块是否是需要读的块,只要SDIO用到。

BURST_ELN_EN

WECRM Wakeup Event Enable On SD Card Removal 移除卡时知否产生唤醒事件

WECINS Wakeup Event Enable On SD Card Insert 插入卡时是否产生唤醒事件

WECINT Wakeup Event Enable On Card Interrupt 不使能时,时钟使能的情况下才能产生卡中断

RD_DONE_NO_8CLK 使用快间隙停止传输时才需要关心这个位

IABG interrupt At Block GAP 仅SDIO 4bit模式使用到,多块传输时是否需要检测中断。

RWCLT READ wait control 读等待控制,仅SDIO模式使用到。

CREQ continue request 继续传输,恢复使用块间隙停止传输的传输

DMASEL DMA select 选择DMA的模式00 不使用DMA或者使用SDMA,01 ADMA1, 10 ADMA2

CDSS Card Detect Signal Selection 选择卡检测信号的来源

CDTL Card Detect Test Level 卡检测测试电平

EMODE Endian Mode 大小端模式选择,一般选择小端

D3CD Data3 as Card Detection Pin data3引脚作为插拔卡检测

DTW Data Transfer Width 数据传输的宽度

LCTL LED控制

14.3.6.12 System Control (uSDHCx_SYS_CTRL)

image-20220112142639489

RSTT Reset Tuning 复位tuning 电路

INITA Initialization Active 置位时,发送80个SDCLK

RSTD Software Reset for DATA Line数据线软复位,数据模块和DMA模块复位。SW等待自清除。

RSTC Software Reset for CMD line 命令电路模块复位,软件等待自清除。

RSTA Software Reset for ALL 复位整个控制器模块,除了卡检测模块

IPP_RST_N 如果卡支持这个特性的话,硬件复位时直接发给卡的引脚

DTOCV Data Timeout Counter Value 数据超时计数器

SDCLKFS SDCLK Frequency Select SDCLK第二阶段分频选择

DVS divisorSDCLK第一阶段分频

14.3.6.13 Interrupt Status (uSDHCx_INT_STATUS)

​ 当Normal Interrupt Signal Enable使能相应的位才能产生相应的中断。所有的位都是写1清除,写0保持不变。一次可以清除多个位。

image-20220112142650887

image-20220112142709227

Command Complete Command Timeout Error Meaning of the Status
0 0 X
X 1 Response not received within 64 SDCLK cycles
1 0 Response received
Transfer Complete Data Timeout Error Meaning of the Status
0 0 X
0 1 Timeout occurred during transfer
1 X Data Transfer Complete
Command Complete Command Timeout Error Meaning of the Status
0 0 No error
0 1 Response Timeout Error
1 0 Response CRC Error
1 1 CMD line conflict

DMAE DMAError DMA 错误

TNE TuningERROR tuning错误

AC12E Auto CMD12 ERROR Auto CMD12错误

DEBE Data End Bit ERROR 数据结束位错误

DCE Data CRC ERROR 数据CRC错误

DTOE data timeout ERROR 数据超时错误

CIE Command Index Error 命令序号错误

CEBE Command End Bit ERROR 命令结束位错误

CCE Command CRC error 命令crc校验错误

CTOE Command timeout error 命令超时错误

TP tuning pass tuning通过(只有SD3.0 SDR104模式使用)

RTE Re-Tuning Event 重新Tuning事件(只有SD3.0 SDR104模式使用)

CINT card interrupt 卡中断

CRM card remove卡移除

CINS card insert 卡插入

BRR Buffer Read Ready 读缓冲准备好

BWR Buffer Read Ready 写缓冲准备好

DINT DMA interrupt DMA中断

BGE block gap event 块间隙事件

TC Transfer Complete 传输完成

CC Command Complete 命令完成

14.3.6.14 Interrupt Status Enable (uSDHCx_INT_STATUS_EN)

image-20220112142737457

image-20220112142740821

对应的位置位,Interrupt Status对应的位才会在相应的事件产生时置位。如果uSDHCx_INT_STATUS_EN对应的位清0,那么Interrupt Status对应的位也会清除

14.3.6.15 Interrupt Signal Enable (uSDHCx_INT_SIGNAL_EN)

对应的位设置的话,相应的事件发生时才会在中断线上产生中断信号

image-20220112142801615

14.3.6.16 Auto CMD12 Error Status (uSDHCx_AUTOCMD12_ERR_STATUS)

image-20220112142807960

Auto CMD12 CRC Error Auto CMD12 Timeout Error Type of Error
0 0 No Error
0 1 Response Timeout Error
1 0 Response CRC Error
1 1 CMD line conflict

SMP_CLK_SEL Sample Clock Select 采样时钟选择

EXECUTE_TUNING execute tuning 开始tuning

CNIBAC12E Command not Issued By Auto CMD12 Error Auto CMD12错误导致未发送命令

AC12IE Auto CMD12/23 Index Error Auto CMD12响应命令序号错误

AC12CE Auto CMD12/23 CRC Error Auto CMD12响应CRC错误

AC12EBE Auto CMD12/23 End Bit Error Auto CMD12结束位错误

AC12TOE Auto CMD12/23 Timeout Error Auto CMD12超时错误

AC12NE Auto CMD12 Not Executed Auto CMD12没有执行

14.3.6.17 Host Controller Capabilities (uSDHCx_HOST_CTRL_CAP)

主机控制器的功能指示寄存器,不会因为软件复位而变化

image-20220112142823623

image-20220112142827981

VS18 Voltage Support 1.8v 是否支持1.8v

VS30 Voltage Support 3.0v 是否支持3.0v

VS33 Voltage Support 3.3v 是否支持3.3v

SRS Suspend/Resume support 是否支持suspend/resume

DMAS DMA support是否支持DMA

HSS high speed support是否支持HS模式

ADMAS ADMA support是否支持ADMA

MBL Max Block Length 最大支持的块长度

RETURNIGN MODE retuning模式

USE_TUNING_SDR50 使用SDR50模式tuning

TIMER_COUNT_RETUNING

DDR50_SUPPORT 是否支持DDR50

SDR104_SUPPORT 是否支持SDR104

SDR50_SUPPORT 是否支持SDR50

14.3.6.18 Watermark Level (uSDHCx_WTMK_LVL)

image-20220112142845425

14.3.6.19 Mixer Control (uSDHCx_MIX_CTRL)

image-20220112142848599

FBCLK_SEL FeedBack clock Source Selection 只要SD3.0 SDR104模式才能使用到

AUTO_TUNE_EN使能自动tune

SMP_CLK_SEL Tuned clk或者fixed clk用来采样data/cmd

EXE_TUNE Execute Tuning (只有SD3.0 SDR104模式使用到)

AC23EN Auto CMD23 Enable Auto CMD23使能

NIBBLE_POS DDR 4bit模式会用到

MSBSEL 单块多个块选择

DTDSEL Data transfer Direction Select 数据传输方向选择

DDR_EN Dual Data rate Mode selection DDR模式选择

AC12EN Auto CMD12 Enable Auto CMD12使能

BCEN Block Count Enable 块数目使能

DMAEN DMA Enable DMA使能

14.3.6.20 Force Event (uSDHCx_FORCE_EVENT)

image-20220112142905813

14.3.6.21 ADMA Error Status Register(uSDHCx_ADMA_ERR_STATUS)

当ADMA错误中断产生时,ADMA Error Status Register保存ADMA状态,ADMA System Address register保存发生错误是描述符所在的地址

ST_STOP ADMA System Address register对应的前一个描述符传输发生错误

ST_FDS ADMA System Address register当前的描述符传输发生错误

ST_CADR change ADDRESS 递增描述符的指针,不会发生错误

ST_TRF ADMA System Address register对应的前一个描述符传输发生错误

ADMA Error State Coding

D01-D00 ADMA Error State (when error has occurred) Contents of ADMA System Address Register
00 ST_STOP (Stop DMA) Holds the address of the next executable Descriptor command
01 ST_FDS (Fetch Descriptor) Holds the valid Descriptor address
10 ST_CADR (Change Address) No ADMA Error is generated
11 ST_TFR (Transfer Data) Holds the address of the next executable Descriptor command

image-20220112142915422

ADMADCE ADMA Descriptor Error ADMA描述符错误,如果描述符有错误该位置位,在编写ADMA描述符生成的程序时很有用

ADMALME ADMA Length Mismatch Error ADMA长度不匹配错误,对应两种情况,如果BLOCK COUNT ENABLE设置了,但是描述符指示的总长度和BLOCK COUNT与BLOCK LENGTH的乘积不匹配;还有一种情况是总长度不能被BLOCK LENGTH整除

ADMAAES ADMA Error State ADMA出错状态。

14.3.6.22 ADMA System Address (uSDHCx_ADMA_SYS_ADDR)

进行ADMA传输的物理地址,其实是ADMA描述符对应的物理地址。

image-20220112142922547

14.3.6.23 DLL (Delay Line) Control (uSDHCx_DLL_CTRL)

14.3.6.24 DLL Status (uSDHCx_DLL_STATUS)

14.3.6.25 CLK Tuning Control and Status (uSDHCx_CLK_TUNE_CTRL_STATUS)

上面三个寄存器是用来tuning的,对于default speed模式和high speed模式使用不到。

14.3.6.26 Vendor Specific Register (uSDHCx_VEND_SPEC)

使用ADMA是,EXT_DMA_EN需要置0

14.3.6.27 MMC Boot Register (uSDHCx_MMC_BOOT)

image-20220112142955603

BOOT_CLK_CNT 块间隙停止传输自动模式的块数目

DISABLE_TIME_OUT 为0使能超时检测,为1时不检测

AUTO_SABG_EN boot期间,使能块间隙停止传输功能

BOOT_EN fastboot启动模式是否使能

BOOT_MODE 0表示normal boot,1表示Alternative boot

BOOT_ACK boot期间是否要ACK

DTOCVACK BOOT ack超时数器

14.3.6.28 Vendor Specific 2 Register (uSDHCx_VEND_SPEC2)

主要是SDR模式和tuning模式相关的寄存器。

image-20220112143020040

14.3.6.29 Tuning Control Register (uSDHCx_TUNING_CTRL)

对于default speed模式和high speed模式使用不到。

14.3.7 命令(CMD)发送和响应(Response)接收的基本操作

14.3.7.1 协议文档里发送命令的流程

注意:14.3.7.1-14.3.7.3来自SD Host_Controller_Simplified_Specification_Ver3.00,需要对照着协议文档里寄存器的介绍来看,因为imx6ull寄存器的位置和命名与协议文档有差异。

image-20220112143043372

(来自PartA2_SD Host_Controller_Simplified_Specification_Ver3.00的figure3-11)

(1)检查Present State寄存器的Command Inhabit(CMD)。重复检查直到Command Inhabit(CMD)变成0。也就是说Command Inhabit(CMD)为1的话,主机不会发送Command。

(2)如果主机发送的command需要用到busy信号的话,转到(3);如果不需要检查的话,转到(5);

(3)如果发送的是abort command,转到(5);如果不是abort command,转到(4)。

(4)检查Present State寄存器的Command Inhabit(DAT)。重复检查直到Command Inhabit(DAT)变成0。

(5)设置除command寄存器的其它寄存器。

(6)设置command寄存器。

注意:写command寄存器的高字节后,主机控制器就会发送command给卡。

(7)执行command complete sequence(后面有说明)。

14.3.7.2 协议文档里命令完成时的操作

(来自PartA2_SD Host_Controller_Simplified_Specification_Ver3.00的figure3-12)

image-20220112143222038

(1)如果在Transfer Mode寄存器的Response Interrupt Disable设置为1(response check使能),转到(4);否则等待Command Complete中断。如果发生了Command Complete中断,转到(2)。

(2)往Normal Interrupt Status寄存器的Command Complete写1,清除掉该bit。

(3)读Response寄存器,获得发送命令需要关注的信息。

(4)判断发送的命令是否使用到了Transfer Complete中断。如果使用的话,转到(5),否则转到(7)。

(5)等待Transfer Complete中断,如果该中断发生,转到(6)。

(6)往Normal Interrupt Status寄存器的Transfer Complete写1,清除掉该bit。

(7)检查Response data中是否有错误,如果没有转到(8);如果有错误转到(9)。

(8)返回状态No ERROR。

(9)返回状态Response Contents Error。

14.3.7.3 协议文档里使用DAT线通过ADMA传输数据

(来自PartA2_SD Host_Controller_Simplified_Specification_Ver3.00的figure3-15)

image-20220112143230507

(1)在系统主内存里创建ADMA描述符表。

(2)将ADMA描述符表的地址写入ADMA System Address寄存器。

(3)将要发送数据的一个块的长度写入Block Size寄存器。

(4)将要发送的数据的块的数目写入Block Count寄存器。

(5)将命令的参数写入到Argument寄存器。

(6)设置Transfer Mode寄存器,主机驱动需要设置Multi/Single Block Select,Block Count Enable,Data Transfer Direction,Auto CMD12 Enable和DMA enable。

(7)设置Command寄存器,设置后主机控制器会发送命令给卡。

(8)如果Response check使能,转到(11),否则等待Command Complete中断,如果Command Complete为1,转到(9)。

(9)往Normal Interrupt Status寄存器的Command Complete写1,清除掉该bit。

(10)读Response寄存器获得发送命令需要关注的信息。

(11)等待Transfer Complete中断和ADAM Error中断。

(12)如果Transfer Complete设置为1,转到(13);如果ADAM Error设置为1,转到(14)。

(13)往Normal Interrupt Status寄存器的Transfer Complete写1,清除掉该bit。

(14)往Error Interrupt Status寄存器的ADMA Error Interrupt Status写1,清除掉该bit。

(15)发送abort命令中止ADMA操作,

14.3.7.4 发送不用DATA线命令

注意:14.3.7.4-14.3.7.6根据SD Host_Controller_Simplified_Specification_Ver3.00协议文档对imx6ull的uSDHC控制器进行编程,需要对照着SD Host_Controller_Simplified_Specification_Ver3.00文档里的寄存器与imx6ull的寄存器一起看。

1等待CIHB清0

2根据命令设置CMDTYP, DPSEL, CICEN, CCCEN, RSTTYP, DTDSEL

3将命令参数写CMDARG寄存器

4写XFERTYP寄存器,开始发送命令

5等待INT_STATUS命令完成位CC设置,如果出错,报告错误

6写1清除CC和错误

14.3.7.5 发送使用到DATA线的命令

1等待CIHB清0

2等待CDIHB清0

3设置ADMA描述符

4设置ADMA_SYS_ADDR为ADMA描述符的地址

5设置BLK_ATT寄存器里块大小和块长度

6PROT_CTRL寄存器里DMA方式选择ADMA2

7MIX_CTRL寄存器里设置DTDSEL数据方向,使能DMAEN

8将命令参数写CMDARG寄存器

9写XFERTYP寄存器,开始发送命令

10等待INT_STATUS命令完成位CC设置,如果出错,报告错误

11写1清除CC和错误

12等待INT_STATUS传输完成位TC,如果出错,报告错误

13写1清除TC和错误

14.3.7.6 读写单个/多个块的流程

1等待CIHB清0

2等待CDIHB清0

3设置ADMA描述符

4设置ADMA_SYS_ADDR为ADMA描述符的地址

5设置BLK_ATT寄存器里块大小和块长度

6PROT_CTRL寄存器里DMA方式选择ADMA2

7MIX_CTRL寄存器里设置DTDSEL数据方向,使能DMAEN,读写多个块的话,还要设置块8计数使能BCEN,选择多个块MSBSEL,使能AUTO CMD12 AC12EN

9将读写块的首个块号写CMDARG寄存器

10写XFERTYP寄存器,开始发送命令

11等待INT_STATUS命令完成位CC设置,如果出错,报告错误

12写1清除CC和错误

13等待INT_STATUS传输完成位TC,如果出错,报告错误

14写1清除TC和错误

14.3.7.7 SD卡的初始化和识别过程 (只考虑Version2.0 以上的卡)

1 发送CMD0

2 对于SDHC卡,必须发送CMD8

3 只有Version2.0及以上的卡才对CMD8进行响应,legacy cards和非SD卡不会对CMD8响应

4 发送ACMD41获取OCR,如果参数为0的话,可以查询卡支持的电压范围

5 1S内循环发送ACMD41,如果主机支持HCS,参数里的HCS一定要设置为1,直到响应的big31位置1才能退出,响应里包含CCS,CCS为0的话表明是SDSC卡,为1的话表明是SDHC或者SDXC

6 如果需要切换到1.8v电压的话,发送ACMD41,参数里的S18R要设置,如果响应里的S18A为1,表示可以切换到1.8v,发送CMD11进行电压切换

7 发送CMD2获得CID

8 发送CMD3,参数为指定的RCA,不能为0

9 发送CMD9,参数为RCA,获得CSD

10 发送CMD7,参数为RCA,选中卡

11 发送ACMD51,参数为RCA,获得SCR

12 发送ACMD6,参数为指定的总线位宽,参数0表示1bit,参数2表示4bit

13 CMD6 function group 1设置卡的speed mode

14.3.7.8 eMMC卡的初始化和识别过程(翻译自JESD84-B51 的附录A.6)

a. 上电

1 上电,电压范围(2.7~3.6v)

2 设置时钟为400k或者更低

3 等待1ms,等待不少于74个时钟周期

4 发送CMD0复位总线

5 发送CMD1,参数为(0x00FF8000或者0x00000080)

6 收到R3响应

7 如果OCR 的busy位为0,重复5和6

8 从R3响应中可以知道设备是支持High Voltage还是Dual Voltage;如果响应是0x80FF8000说明只支持High Voltage,如果响应是0x80FF8080说明是Dual Voltage设备

9 如果R3响应是别的数的话,表明不兼容该设备(因为电压不兼容,设备会将自己置于inactive状态,不会响应),这种情况下,主机会关闭总线电源下电,开始进行自恢复

低电压上电

如果主机支持低电压的话,进入如下操作切换到低电压,否则直接跳到16

10 如果主机支持低电压,并且设备支持Dual Voltage,主机给总线下电

11 主机给总线上电,电压是低电压(1.70~1.95V)

12 等待1ms,等待不少于74个时钟周期

13 发送CMD1,参数为0x00000080

14 读取R3类型响应,值应该为0x80FF8080

15 如果OCR的busy位为0,重复13和14

b. CID读取和分配RCA

16 发送CMD2

17 收到R2类型的响应,获得设备的CID

18 发送CMD3,指定一个大于1的数作为卡的地址

读取CSD和主机适配

19 发送CMD9

20 收到R2类型的响应,获得设备的CSD

21 根据需要,按照CSD信息调整主机的一些参数。如果SPEC_VERS表明是V4.0及以上,表明是high speed设备并且支持SWITCH和SEND_EXT_CSD命令。否则设备是一个旧的MMC设备。无论设备的类型,最大支持的时钟频率可以设置到TRAN_SPEED域

切换到High Speed高速模式:

总线初始化完成后,如果卡支持high speed模式的话,可以切换到high speed模式

22 发送CMD7,参数为指定的RCA,设备进入到tran state

23 发送CMD8(SEND_EXT_CSD),从EXT_CSD,主机可以知道设备支持的power class,选择一个更宽的总线宽度(看步骤26-37)

24 发送CMD6,参数为0x03B9_0100,就是设置EXT_CSD的HS_TIMING为0x1

24.1 设备会进入到忙状态,响应为R1,知道busy状态清除

24.2 设备退出busy状态后,设备的high speed timing配置成功

25 调整时钟频率(0~26/52MHz)

改变数据总线宽度:

a. 总线测试流程

26 发送CMD19

27 发送数据,所有的数据线上都会传输特定的数据,具体数据参考协议文档

27.1 对于8条数据线,数据(MSB到LSB)是0x0000_0000_0000_AA55

27.2 对于4条数据线,数据(MSB到LSB)是0x0000_AA55

27.3 对于1条数据线,数据是0x80

28 发送之前等待NCR个时钟周期

29 发送CMD14,从所有有效的数据线收到数据

29.1 对于8条数据线,收到8字节数据

29.2 对于4条数据线,收到4字节数据

29.3 对于1条数据线,收到1字节数据

30 和步骤27中的数据XNOR(异或后取反)操作

31 根据如下对结果进行MASK操作

31.1 对于8条数据线,数据(MSB到LSB)是0x0000_0000_0000_FFFF

31.2 对于4条数据线,数据(MSB到LSB)是0x0000_FFFF

31.3 对于1条数据线,数据是0xC0

32 所有的结果需要是0,其它结果表明设备的连接有问题,这种情形下,主机需要下电并进行恢复操作

b 电源和数据宽度选择

33 选择需要工作的总线宽度

34 如果power class和选择宽度与默认的power class不匹配,发送CMD6,写入EXT_CSD的POWER_CLASS为响应的power class

35 发送CMD6后设备进入忙状态,等待设备退出忙状态

36 发送CMD6,写入EXT_CSD的BUS_WIDTH为响应的总线宽度,4bit对应的参数为0x03B7_0100,8bit对应的参数为0x03B7_0200

14.3.8 eMMC分区介绍

image-20220112143422049

​ 如上图所示,存储设备的默认区域包括一个用于存储数据的User Data Area,两个可能的用于引导的Boot Area Partitions和一个防止重放攻击的Replay Protected Memory Block Area(RPMB)。存储配置最初(在执行任何分区操作之前)由User Data AreaPartition和RPMB和Boot Area Partitions(其尺寸和技术参数由存储制造商定义)组成。

​ 因此,存储块区域可以分类如下:

​ 每个General Purpose Area Partition都可以通过enhanced or extended technological features来实现(例如更高的可靠性),以与默认存储介质区分。如果设备支持enhanced storage media features,那么boot和RPMB也默认支持。

​ boot和RPMB分区的大小和属性由存储制造商定义(只读),General Purpose Area Partitions的大小和属性只能由主机在设备生命周期中进行一次编程(只支持一次可编程)。一个可能的配置如下:

image-20220112143458999

​ 主机对General Purpose Area Partition和Enhanced User Data Area的配置可能会影响先前存储的数据(它们将被销毁)和设备初始化时间。特别是,由于内部控制器需要执行操作来设置主机指定的配置,因此配置之后的上电的初始化时间可能会超过规范定义的最大初始化时间。

​ 注意:这里的分区与磁盘里的分区有相同的地方,也有不同。比如两个boot分区和rpmb分区,user data area分区是默认就有的。这里是不需要什么分区表的,他们的大小和范围都在CSD或者EXT_CSD寄存器里定义。如果要使用General Purpose Area Partition就需要对eMMC卡进行配置(只能配置一次)。此外,我们还能像对硬盘进行分区操作那样对user data area进行类似的分区,这里需要使用到分区表,分区后可以安装文件系统。

14.4 代码详解及测试

​ 目的是将第五章编译好的led.imx烧录到TF卡和eMMC里,并且在烧录前后读出两个扇区的值并且再串口上打印出来。

​ 注意:不是直接可以烧录led.imx文件,而是将led.imx转化成一个头文件led.imx.h,头文件里是一个大的unsigned char数组led_imx_image,数组里的内容是由led.imx文件内容转化而来的。然后将led_imx_image这个数组里的内容分别写到TF卡的起始处和eMMC的boot分区起始处。

14.4.1 管脚设置

image-20220112143657344

​ 上图是100sdk_imx6ull开发板的SD卡原理图,使用了uSDHC1控制器。

image-20220112143701287

​ 上图是100sdk_imx6ull开发板eMMC的原理图,接在uSDHC2控制器上。

​ 根据电路设计,对于TF卡将管脚USDHC1_CMD,USDHC1_CLK,USDHC1_DATA0-3,USDHC1_CD_B设置成对应的功能,对于eMMC将管脚USDHC2_CMD,USDHC2_CLK,USDHC2_RESET_B,USDHC2_DATA0~3设置成对应的功能。直接调用IOMUXC_SetPinMux和IOMUXC_SetPinConfig进行设置。

14.4.2 时钟的设置

​ 通过前面的分析可知,USDHCx_CLK_ROOT为198M的话,就要由uSDHCx_SYS_CTRL[DVS]和uSDHCx_SYS_CTRL[SDCLKFS]分频产生400K,25M,26M,50M,52M等时钟。直接设置这两个寄存器对应的分频系数产生相应的时钟即可。

14.4.3 构造发送给CMD_XFR_TYP寄存器的高16bit

​ 根据命令的序号、响应的长度、是否需要命令的序号检测、是否需要CRC校验和是否用到DATA线进行数据传输来设置CMD_XFR_TYP寄存器的高16bit。

代码目录在裸机Git仓库 NoosProgramProject/(14_TF卡编程/006_sd/sd.h)

/*
 * CMDx:  normal command,
 * ACMDx: app command,
 * SCMDx: sd command,
 * MCMDx: mmc command
 * R1 R1b R3 R6 R7 48bit; 
 * R2 136bit
 * bit1-0(bit17-16) RSPTYP 00 no response; 01 lengeh 136; 02 length 48; 03 length 48 and check busy
 * bit3(bit19) CCCEN command crc check enable 
 * bit4(bit20) CICEN Command index check enable
 * bit5(bit21) DPSEL data present select
 */
#define CMD0	0x0000	/* GO_IDLE_STATE	no response */
#define CMD1	0x0102	/* SEND_OP_COND	R3 */
#define CMD2	0x0209	/* ALL_SEND_CID	R2 */
#define CMD3	0x031a	/* SET_RELATIVE_ADDR (R6/R1) */
#define ACMD6	0x061a	/* SET_BUS_WIDTH R1	*/
#define ACMD23	0x171a	/* SET_WR_BLK_ERASE_CNT	R1 */
#define SCMD6	0x063a	/* SWITCH_BUSWIDTH	R1 */
#define MCMD6	0x061b	/* SET_EXT_CSD R1B */
#define CMD7	0x071b	/* SELECT_CARD R1B */
#define CMD8	0x081a	/* SEND_IF_COND	R7 */
#define MCMD8	0x083a	/* GET_EXT_CSD R1 */
#define CMD9	0x0909	/* GET_THE_CSD R2 */
#define CMD11	0x0b1a	/* SWITCH VOLTAGE R1 */
#define CMD13	0x0d1a	/* SEND_STATUS R1 */
#define ACMD13	0x0d3a	/* SEND_STATUS R1 */
#define CMD16	0x101a	/* SET_BLOCKLEN	R1 */
#define CMD17	0x113a	/* READ_SINGLE_BLOCK R1 */
#define CMD18	0x123a	/* READ_MULTIPLE_BLOCK R1 for SD R7 */
#define SCMD19  0x133a  /* SEND_TUNING R1 */
#define MCMD21  0x153a  /* SEND_TUNING R1 */
#define CMD24	0x183a	/* WRITE_BLOCK R1 */
#define CMD25	0x193a	/* WRITE_MULTIPLE_BLOCK	R1 */
#define CMD32	0x201a	/* ERASE_WR_BLK_START R1 */
#define CMD33	0x211a	/* ERASE_WR_BLK_END	R1 */
#define CMD35	0x231a	/* ERASE_GROUP_START R1 */
#define CMD36	0x241a	/* ERASE_GROUP_END	R1 */
#define CMD38	0x261b	/* ERASE R1B */
#define ACMD41	0x2902	/* SD_SEND_OP_COND R3 */
#define ACMD42	0x2a1b	/* LOCK_UNLOCK R1B */
#define ACMD51	0x333a	/* SEND_SCR	R1 */
#define CMD55	0x371a	/* APP_CMD	R1 */

14.4.4 发送未使用到DATA线的命令的函数

​ 这类命令直接通过CMD线发送命令,响应也是通过CMD线由设备发送给主机。先判断PRES_STATE寄存器的CIHB置0,确保CMD线不在使用。直接将命令类型赋给CMD_XFR_TYP寄存器,命令的参数赋给CMD_ARG,赋值完后,需要等待命令执行完成。

​ 代码代码目录在裸机Git仓库 NoosProgramProject/(14_TF卡编程/006_sd/sd.c)

static int USDHC_SendCommand(USDHC_Type *base, u32 command, u32 argument)
{
	/* Wait until command/data bus out of busy status. */
	while (base->PRES_STATE & USDHC_PRES_STATE_CIHB_MASK) {
	}
  
	/* config the command xfertype and argument */
	base->CMD_ARG = argument;
	base->CMD_XFR_TYP = command << 16;

	return USDHC_WaitCommandDone(base);
}

​ 等待命令执行完成函数主要功能是读取INT_STATUS寄存器,等待命令执行完成位(CC)设置,同时检查命令执行出错的位是否设置,出错的位包括命令超时错误、命令CRC错误、命令结束位错误和命令序号错误。如果出错的话调用USDHC_Dump_All函数dump出uSDHC所有的寄存器的值便于调试。最后向相应的位写1,清除掉中断标记位。

​ 代码目录在裸机Git仓库 NoosProgramProject/(14_TF卡编程/ 006_sd/sd.c):

static int USDHC_WaitCommandDone(USDHC_Type *base)
{
	int error = 0;
	uint32_t interruptStatus = 0U;

	/* Wait command complete or USDHC encounters error. */
	while (!(base->INT_STATUS & (USDHC_INT_STATUS_CC_MASK | kUSDHC_CommandErrorFlag))) {
	}

	interruptStatus = base->INT_STATUS;
	if ((interruptStatus & kUSDHC_CommandErrorFlag) != 0U)
	{
		printf("cmd errror, CMD is 0x%x, INT_STATUS is 0x%x\r\n", base->CMD_XFR_TYP, interruptStatus);
		USDHC_Dump_All(base);
		error = -1;
	}

	USDHC_ClearInterruptStatusFlags(
			base, (kUSDHC_CommandCompleteFlag | kUSDHC_CommandErrorFlag | kUSDHC_TuningErrorFlag));

	return error;
}

14.4.5 ADMA描述符的设置函数

​ 每个描述符行(也就是一个执行单元)包括三个域地址(address),长度(length)和属性(attribute)。属性指定了每个描述符行对应的操作。ADMA2不使用DMA System Address寄存器,而是使用ADMA System Address寄存器指向描述符表。

​ 函数有两个参数,分别为保存描述符的地址和地址的长度。首先计算需要多少行描述符,然后设置每个描述符行的地址、长度和属性。地址域按照每个描述符行的所能操作最大长度依次递增,长度域除了最后一个都是描述符行所能操作的最大长度,最后一个描述符行的长度域为最后剩下的长度,属性域每行都得设置tran和VALID,其中最后一个还要设置END,表示描述符行结束了。描述符存放在g_adma2_table[]数组里。

​ 代码目录在裸机Git仓库 NoosProgramProject/(14_TF卡编程/006_sd/sd.c):

static int USDHC_CreateDescTable(u8 *data, u32 len)
{
	int i;
	u32 dma_len, entries;

	entries = len / USDHC_ADMA2_DESCRIPTOR_MAX_LENGTH_PER_ENTRY;
	if ((len % USDHC_ADMA2_DESCRIPTOR_MAX_LENGTH_PER_ENTRY) != 0U)
		entries++;

	for (i = 0; i < entries; i++)
	{
		if (len > USDHC_ADMA2_DESCRIPTOR_MAX_LENGTH_PER_ENTRY)
		{
			dma_len = USDHC_ADMA2_DESCRIPTOR_MAX_LENGTH_PER_ENTRY;
			len -= USDHC_ADMA2_DESCRIPTOR_MAX_LENGTH_PER_ENTRY;
		}
		else
		{
			dma_len = len;
		}

		/* Each descriptor for ADMA2 is 64-bit in length */
		g_adma2_tablbe[i].address = (uint32_t *)data;
		g_adma2_tablbe[i].attribute = (dma_len << USDHC_ADMA2_DESCRIPTOR_LENGTH_SHIFT);
		g_adma2_tablbe[i].attribute |= kUSDHC_Adma2DescriptorTypeTransfer;
		data += dma_len;
	}
	g_adma2_tablbe[entries - 1].attribute |= kUSDHC_Adma2DescriptorEndFlag;
	/*for (i = 0; i < entries; i++) {
	  printf("g_adma2_tablbe[i] address is 0x%x, attribute is 0x%x\r\n", 
	  g_adma2_tablbe[i].address, g_adma2_tablbe[i].attribute);
	  }
	  */

	return 0;
}

14.4.6 发送使用到DATA线的命令的函数

​ 对于这类命令涉及到操作data线。先判断PRES_STATE寄存器的CIHB和CDIHB置0,确保CMD线和DATA线不在使用。命令和响应还是通过CMD线传输。那么数据是如何在主机和设备之间传输呢?通过ADMA传输。写CMD_XFR_TYP寄存器启动发送命令之前,需要设置ADMA。数据传输有三要素,源、目的和长度。长度该如何确定呢?通过设置BLK_ATT寄存器的BLKCNT和BLKSIZE来决定。然后根据长度和传输的地址设置好ADMA描述符。将ADMA描述符的地址赋值给ADMA_SYS_ADDR寄存器。PROT_CTRL寄存器的ADMA传输方式选择ADMA2。这里假设数据的传输只用到从设备传输到主机。数据的传输方向由MIX_CTRL的DTDSEL来设置,为1表明由设备传输到主机。最后将命令的参数赋给CMD_ARG,将命令类型赋给CMD_XFR_TYP寄存器,然后先等待命令传输完成,再等待数据传输完成。等待命令传输完成还是使用USDHC_WaitCommandDone函数,等待数据传输完成使用USDHC_WaitDataDone函数。

​ 等待数据传输完成USDHC_WaitDataDone函数主要是读取INT_STATUS寄存器,等待传输完成位(TC)设置,同时检查传输出错的位是否设置,出错的位包括数据传输超时错误、数据CRC校验错误、数据结束位错误、Auto CMD12错误和DMA错误。如果出错的话调用USDHC_Dump_All函数dump出uSDHC所有的寄存器的值便于调试。主要观察INT_STATUS和ADMA_ERR_STATUS寄存器的值。最后向相应的位写1,清除掉中断标记位。代码在裸机Git仓库 NoosProgramProject/(14_TF卡编程/006_sd/sd.c):

static int USDHC_WaitDataDone(USDHC_Type *base)
{
	int error = 0;
	uint32_t interruptStatus = 0U;

	/* Wait command complete or USDHC encounters error. */
	while (!(base->INT_STATUS & (kUSDHC_DataCompleteFlag | kUSDHC_DataErrorFlag | kUSDHC_DmaErrorFlag))) {
	}

	interruptStatus = base->INT_STATUS;

	if ((interruptStatus & (kUSDHC_DataErrorFlag | kUSDHC_DmaErrorFlag)) != 0U) {
		printf("data or ADMA errror, CMD is 0x%x, INT_STATUS is 0x%x\r\n", base->CMD_XFR_TYP, interruptStatus);
		USDHC_Dump_All(base);
		error = -1;
	}

	USDHC_ClearInterruptStatusFlags(
			base, (kUSDHC_DataCompleteFlag | kUSDHC_DataErrorFlag | kUSDHC_DmaErrorFlag));

	return error;
}

14.4.7 读扇区的函数

​ 先确保PRES_STATE寄存器的CIHB和CDIHB置0,保证CMD线和data线不在使用。根据要读缓冲区的地址和长度创建ADMA描述符,并将ADMA描述符的地址赋值给ADMA_SYS_ADDR寄存器。根据长度设置寄存器的BLKCNT和BLKSIZE。PROT_CTRL寄存器的DMA方式选择ADMA2。对于读一个扇区,设置MIX_CTRL的DTDSEL为1,表示是读,并且使能DMAEN。对于读多个扇区,还要使能MIX_CTRL的MSBSEL,表示是多个扇区,BCEN,表示启用块计数器,和AC12EN,这样数据传输结束后主机自动发送CMD12结束传输。最后将起始传输的块号设置到CMD_ARG,将CMD17(表示单个块读)或者CMD18(表示多个块读)设置到CMD_XFR_TYP启动传输。然后分别等待命令传输完成和数据传输完成。结束时发送cmd13查询状态,看读的过程是否有错误发生。代码在裸机Git仓库 NoosProgramProject/(14_TF卡编程/006_sd/sd.c)

int sd_read_blocks(USDHC_Type *base, uint8_t *buffer, uint32_t startBlock, uint32_t blockCount)
{
	int err = 0;

	/* Wait until command/data bus out of busy status. */
	while (base->PRES_STATE & USDHC_PRES_STATE_CIHB_MASK) {
	}
	while (base->PRES_STATE & USDHC_PRES_STATE_CDIHB_MASK) {
	}
	/* set ADMA descriptor */
	USDHC_CreateDescTable(buffer, blockCount * 512);

	/* When use ADMA, disable simple DMA */
	base->DS_ADDR = 0U;
	base->ADMA_SYS_ADDR = (u32) g_adma2_tablbe;

	/* config data block size/block count */
	base->BLK_ATT = (USDHC_BLK_ATT_BLKSIZE(512) | USDHC_BLK_ATT_BLKCNT(blockCount));

	/* disable the external DMA if support */
	base->VEND_SPEC &= ~USDHC_VEND_SPEC_EXT_DMA_EN_MASK;
	/* select DMA mode and config the burst length */
	base->PROT_CTRL &= ~(USDHC_PROT_CTRL_DMASEL_MASK | USDHC_PROT_CTRL_BURST_LEN_EN_MASK);
	base->PROT_CTRL |=
		USDHC_PROT_CTRL_DMASEL(kUSDHC_DmaModeAdma2) | USDHC_PROT_CTRL_BURST_LEN_EN(kUSDHC_EnBurstLenForINCR);

	if (blockCount == 1) {
		/* single block read*/
		/* direction:read, enable DMA */
		base->MIX_CTRL &= ~(USDHC_MIX_CTRL_MSBSEL_MASK | USDHC_MIX_CTRL_BCEN_MASK | USDHC_MIX_CTRL_DTDSEL_MASK |
				USDHC_MIX_CTRL_AC12EN_MASK | USDHC_MIX_CTRL_DMAEN_MASK);
		base->MIX_CTRL |= USDHC_MIX_CTRL_DTDSEL_MASK | USDHC_MIX_CTRL_DMAEN_MASK;

		/* config the command xfertype and argument */
		base->CMD_ARG = startBlock;
		base->CMD_XFR_TYP = CMD17 << 16;
	} else {
		/* multi block read */
		/* block count enable, Multiblcok, direction:read, enable DMA */
		base->MIX_CTRL &= ~(USDHC_MIX_CTRL_MSBSEL_MASK | USDHC_MIX_CTRL_BCEN_MASK | USDHC_MIX_CTRL_DTDSEL_MASK |
				USDHC_MIX_CTRL_AC12EN_MASK | USDHC_MIX_CTRL_DMAEN_MASK);
		base->MIX_CTRL |= USDHC_MIX_CTRL_MSBSEL_MASK | USDHC_MIX_CTRL_BCEN_MASK | 
			USDHC_MIX_CTRL_DTDSEL_MASK | USDHC_MIX_CTRL_AC12EN_MASK | USDHC_MIX_CTRL_DMAEN_MASK;

		/* config the command xfertype and argument */
		base->CMD_ARG = startBlock;
		base->CMD_XFR_TYP = CMD18 << 16;
	}

	err = USDHC_WaitCommandDone(base);
	if (err < 0)
		return err;

	err =  USDHC_WaitDataDone(base);
	if (err < 0)
		return err;

	err = SD_WaitReadWriteComplete(base);
	if (err & 0xc0200000) {
		err = -1;
		printf("%s read error\r\n", __func__);
	}

	return err;
}

14.4.8 写扇区的函数

​ 与读扇区函数类似,区别是MIX_CTRL的DTDSEL为0,表示是写。发送命令时,CMD24表示单个块的写,CMD25表示多个块的写。结束时发送cmd13查询状态,看写的过程是否有错误发生。

​ 代码在裸机Git仓库 NoosProgramProject/(14_TF卡编程/006_sd/sd.c)

int sd_write_blocks(USDHC_Type *base, uint8_t *buffer, uint32_t startBlock, uint32_t blockCount)
{
	int err = 0;

	/* Wait until command/data bus out of busy status. */
	while (base->PRES_STATE & USDHC_PRES_STATE_CIHB_MASK) {
	}
	while (base->PRES_STATE & USDHC_PRES_STATE_CDIHB_MASK) {
	}
	/* set ADMA descriptor */
	USDHC_CreateDescTable(buffer, blockCount * 512);

	/* When use ADMA, disable simple DMA */
	base->DS_ADDR = 0U;
	base->ADMA_SYS_ADDR = (u32) g_adma2_tablbe;

	/* config data block size/block count */
	base->BLK_ATT = (USDHC_BLK_ATT_BLKSIZE(512) | USDHC_BLK_ATT_BLKCNT(blockCount));

	/* disable the external DMA if support */
	base->VEND_SPEC &= ~USDHC_VEND_SPEC_EXT_DMA_EN_MASK;
	/* select DMA mode and config the burst length */
	base->PROT_CTRL &= ~(USDHC_PROT_CTRL_DMASEL_MASK | USDHC_PROT_CTRL_BURST_LEN_EN_MASK);
	base->PROT_CTRL |=
		USDHC_PROT_CTRL_DMASEL(kUSDHC_DmaModeAdma2) | USDHC_PROT_CTRL_BURST_LEN_EN(kUSDHC_EnBurstLenForINCR);

	if (blockCount == 1) {
		/* single block wrtie*/
		/* direction:read, enable DMA */
		base->MIX_CTRL &= ~(USDHC_MIX_CTRL_MSBSEL_MASK | USDHC_MIX_CTRL_BCEN_MASK | USDHC_MIX_CTRL_DTDSEL_MASK |
				USDHC_MIX_CTRL_AC12EN_MASK | USDHC_MIX_CTRL_DMAEN_MASK);
		base->MIX_CTRL |= USDHC_MIX_CTRL_DMAEN_MASK;

		/* config the command xfertype and argument */
		base->CMD_ARG = startBlock;
		base->CMD_XFR_TYP = CMD24 << 16;
	} else {
		/* multi block write */
		/* block count enable, Multiblcok, direction:read, enable DMA */
		base->MIX_CTRL &= ~(USDHC_MIX_CTRL_MSBSEL_MASK | USDHC_MIX_CTRL_BCEN_MASK | USDHC_MIX_CTRL_DTDSEL_MASK |
				USDHC_MIX_CTRL_AC12EN_MASK | USDHC_MIX_CTRL_DMAEN_MASK);
		base->MIX_CTRL |= USDHC_MIX_CTRL_MSBSEL_MASK | USDHC_MIX_CTRL_BCEN_MASK |
			USDHC_MIX_CTRL_AC12EN_MASK | USDHC_MIX_CTRL_DMAEN_MASK;

		/* config the command xfertype and argument */
		base->CMD_ARG = startBlock;
		base->CMD_XFR_TYP = CMD25 << 16;
	}

	err = USDHC_WaitCommandDone(base);
	if (err < 0)
		return err;

	err =  USDHC_WaitDataDone(base);
	if (err < 0)
		return err;

	err = SD_WaitReadWriteComplete(base);
	if (err & 0xe4000000) {
		err = -1;
		printf("%s write error\r\n", __func__);
	}

	return err;
}

14.4.9 TF卡初始化函数

​ 发送命令的函数准备好后,直接根据协议发送命令,解析响应和数据得到卡的状态和信息。代码在裸机Git仓库 NoosProgramProject/(14_TF卡编程/006_sd/sd.c):

int sd_init(USDHC_Type *base)
{
	int err;
	u32 retries, acmd41arg = 0, resp[4], raw_scr[2];

	USDHC_Init(base);
	CardInsertDetect(base);

	/* set DATA bus width */
	USDHC_SetDataBusWidth(base, kUSDHC_DataBusWidth1Bit);
	/*set card freq to 400KHZ*/
	g_sd_card.busClock_Hz = USDHC_SetSdClock(base, 198000000U, SDMMC_CLOCK_400KHZ);
	/* send card active */
	USDHC_SetCardActive(base, 100U);
	/* Get host capability. ignore,just decision form spec HOST_CTRL_CAP*/ 

	/* CMD0 - GO_IDLE_STATE software reset and set into idle */
	err = USDHC_SendCommand(base, CMD0, 0x0);
	if (err < 0)
		return -1;

	/* verify card interface operating condition. */
	for (retries = 0; retries < 10; retries++) {
		/* CMD8 (physical layer spec Ver2.0 is mandatory) */		
		err = USDHC_SendCommand(base, CMD8, 0x01aa);
		if (err == 0)
			break;
	}

	if (err == 0) {
		/* SDHC or SDXC card */
		acmd41arg |= kSD_OcrHostCapacitySupportFlag;
	} else {
		/* SDSC card */
		err = USDHC_SendCommand(base, CMD0, 0x0);
		if (err !=  0)
			return -1;
	}

	acmd41arg |= (kSD_OcrVdd32_33Flag | kSD_OcrVdd33_34Flag);
	for(retries = 0; retries < 5000; retries++) {
		/* rca = 0 since card is in IDLE state */
		err = USDHC_SendCommand(base, CMD55, 0x0);
		if (err < 0)
			return -1;

		/* ACMD41 to query OCR */
		err = USDHC_SendCommand(base, ACMD41, acmd41arg);
		if (err < 0)
			return -1;

		if (base->CMD_RSP0 & kSD_OcrPowerUpBusyFlag) {
			g_sd_card.ocr = base->CMD_RSP0;
			printf("ocr is 0x%x\r\n", g_sd_card.ocr);
			break;
		}
	}

	if (retries >= 1000 ) {
		printf("HandShakeOperationConditionFailed\r\n");
		return -1;
	}

	/* check 1.8V support */
	if (g_sd_card.ocr & kSD_OcrSwitch18AcceptFlag) {
		printf("support 1.8v\r\n");
	}
	// our board just support 3.3v, ignore it
	//SD_SwitchVoltage(card))

	/* get CID number */
	err = USDHC_SendCommand(base, CMD2, 0x0);
	if (err < 0)
		return -1;	
	USDHC_GetResponse(base, resp);
	memcpy(g_sd_card.rawCid, resp, sizeof(resp));
	SD_DecodeCid(&g_sd_card, g_sd_card.rawCid);

	/* publish a new relative card address(RCA) */
	err = USDHC_SendCommand(base, CMD3, 0x0);
	if (err < 0)
		return -1;
	g_sd_card.relativeAddress = base->CMD_RSP0 >> 16;
	printf("relative address is 0x%x\r\n", g_sd_card.relativeAddress);

	/* get CID number */
	err = USDHC_SendCommand(base, CMD9, g_sd_card.relativeAddress << 16);
	if (err < 0)
		return -1;	
	USDHC_GetResponse(base, resp);
	memcpy(g_sd_card.rawCsd, resp, sizeof(resp));
	SD_DecodeCsd(&g_sd_card, g_sd_card.rawCsd);
	printf("card is %s", g_sd_card.csd.csdStructure ? "SDHC/SDXC" : "SDSC");
	printf("card block count is %d\r\n", g_sd_card.blockCount);
	printf("card sector size is %d\r\n", g_sd_card.blockSize);
	printf("card command class is 0x%x\r\n", g_sd_card.csd.cardCommandClass);

	/* CMD7: SelectCard */
	err = USDHC_SendCommand(base, CMD7, g_sd_card.relativeAddress << 16);
	if (err < 0)
		return err;

	/* ACMD51: Read SCR */
	err = USDHC_SendCommand(base, CMD55, g_sd_card.relativeAddress << 16);
	if (err < 0)
		return err;
	err = USDHC_SendCommand_with_data(base, ACMD51, 0, raw_scr, 8);
	if (err < 0)
		return err;
	raw_scr[0] = SWAP_32(raw_scr[0]);
	raw_scr[1] = SWAP_32(raw_scr[1]);
	memcpy(g_sd_card.rawScr, raw_scr, sizeof(raw_scr));
	SD_DecodeScr(&g_sd_card, g_sd_card.rawScr);

	printf("scr[0] is 0x%x, scr[1] is 0x%x\r\n", raw_scr[0], raw_scr[1]);
	printf("sd specification is 0x%x\r\n", g_sd_card.scr.sdSpecification);
	if ((g_sd_card.scr.sdBusWidths & 0x4U) == 0) {
		printf("The card can not support 4bit width");
		return -1;
	}
	/* speed class control cmd */
	if ((g_sd_card.scr.commandSupport & 0x01U) == 0)
	{
		printf("The card can not support Speed Class Control (CMD20)\r\n");
	}
	/* set block count cmd */
	if ((g_sd_card.scr.commandSupport & 0x02U) == 0)
	{
		printf("The card can not support Support SetBlockCountCmd (CMD23)\r\n");
	}

	/* Set to max frequency in non-high speed mode. */
	g_sd_card.busClock_Hz = USDHC_SetSdClock(base, 198000000U, SD_CLOCK_25MHZ);

	/* Set to 4-bit data bus mode. */
	/* set card to 4 bit width*/
	err = USDHC_SendCommand(base, CMD55, g_sd_card.relativeAddress << 16);
	if (err < 0)
		return err;	
	err =  USDHC_SendCommand(base, ACMD6, 2);
	if (err < 0)
		return err;
	/* set host to 4 bit width*/
	base->PROT_CTRL = ((base->PROT_CTRL & ~USDHC_PROT_CTRL_DTW_MASK) | USDHC_PROT_CTRL_DTW(1));

	/* set block size to 512 : CMD16 SET_BLOCKLEN */
	err = USDHC_SendCommand(base, CMD16, FSL_SDMMC_DEFAULT_BLOCK_SIZE);
	if (err < 0)
		return err;	

	/* select high speed successful, switch clock to 50M */
	if (SD_SelectBusTiming(base) == 0)
		g_sd_card.busClock_Hz = USDHC_SetSdClock(base, 198000000U, SD_CLOCK_50MHZ);
		//g_sd_card.busClock_Hz = USDHC_SetSdClock(base, 396000000U, SD_CLOCK_50MHZ);
	//printf("clock is %d, base->SYS_CTRL is 0x%x\r\n", g_sd_card.busClock_Hz, base->SYS_CTRL);
	printf("sd init sccessful\r\n");
	
	return 0;
}

14.4.10 TF卡主函数

​ 将led.imx转换成十六进制的数据,并且保存在led.imx.h文件里。从第二个块读取两个块数据并且打印,再将led_imx_image的1024偏移位置开始的数据写入到TF卡,最后再将从第二个块读取两个块数据并且打印,查看读出的数据和写入的数据是否相同。

​ 代码在裸机Git仓库 NoosProgramProject/(14_TF卡编程/006_sd/sd.c)

printf("start test sd\r\n");
			sd_init(USDHC1);

			memset(sd_read_buf, 0, sizeof(sd_read_buf));
			sd_read_blocks(USDHC1, sd_read_buf, 2, 2);

			printf("read 2 sectors from 2nd sector\r\n");
			for(i = 0; i < 1024; i++) {
				printf("%02x ", sd_read_buf[i]);
				if((i + 1)%16 == 0)
					printf("\r\n");
			}

			printf("burn led.imx to sd\r\n");
			sd_write_blocks(USDHC1, led_imx_image + 1024, 2, (sizeof(led_imx_image) - 1024) >> 9);
			printf("please set dip switch to SD boot, and push reset, then green led blink \r\n");

			memset(sd_read_buf, 0, sizeof(sd_read_buf));
			sd_read_blocks(USDHC1, sd_read_buf, 2, 2);

			printf("read 2 sectors from 2nd sector after burn led_imx_image:\r\n");
			for(i = 0; i < 1024; i++) {
				printf("%02x ", sd_read_buf[i]);
				if((i + 1)%16 == 0)
					printf("\r\n");
			}			
			printf("please set dip switch to SD boot, and push reset, then green led blink \r\n");		

对于eMMC,引脚的设置和时钟的设置与TF卡类似,发送命令和读写扇区的函数也类似,和TF卡共用。主要有以下几个不同:

14.4.11 eMMC切换boot partition函数

Extended CSD register

PARTITION_CONFIG[179]

image-20220112144324646

image-20220112144330003

当PARTITION_ACCESS为0时对应user partition,为1时对应boot1 partition,为2时对应boot2 partition。

image-20220112144334889

CMD6的格式

Bit31-26 设置为0

Bit25-24 为访问模式 access mode

Bit23-16 为寄存器序号

Bit15-8 为设置的值

Bit7-3 设置为0

Bit2-0 cmd set

image-20220112144339189

00时切换command set

01 根据value域中的1,置位指定字节中对应的位

10 根据value域中的1,清除指定字节中对应的位

11 将value写入到指定的字节

所以由user partition切换到boot1 partition时,设置的值为

(1<< 24) | (179 << 16) | (1 << 8) | 0

由boot1 partition切换回user partition时,设置的值为

(2<<24) | (179 << 16) | (1 << 8) | 0

代码在裸机Git仓库 NoosProgramProject/(14_TF卡编程/006_sd/sd.c)

static status_t MMC_SetExtendedCsdConfig(USDHC_Type *base, mmc_extended_csd_access_mode_t access_mode, uint8_t index, uint8_t value)
{
	int err;

	err = USDHC_SendCommand(base, MCMD6, (access_mode << 24) | (index << 16) | (value << 8));
	if (err < 0)
		return -1;

	return 0;

}
static status_t MMC_SelectPartition(USDHC_Type *base, mmc_extended_csd_access_mode_t access_mode, mmc_access_partition_t part_number)
{
	int err;

	err = MMC_SetExtendedCsdConfig(base, access_mode, 179,  part_number);
	if (err < 0)
		return -1;

	err = SD_WaitReadWriteComplete(base);
	if (err & (1 << 7)) {
		printf("failed select partition\r\n");
		return -1;
	}

	return 0;
}
int MMC_SelectBoot1Partition(USDHC_Type *base)
{
	return	MMC_SelectPartition(base, kMMC_ExtendedCsdAccessModeSetBits, 1);
}

14.4.12 eMMC初始化函数

根据协议发送命令,解析响应和获得数据得到卡的状态和信息。

代码在裸机Git仓库 NoosProgramProject/(14_TF卡编程/006_sd/sd.c)

int mmc_init(USDHC_Type *base)
{
	int err;
	u32 retries, resp[4], maxBusClock_Hz, bus_width = 4;

	USDHC_Init(base);

	/* set DATA bus width */
	USDHC_SetDataBusWidth(base, kUSDHC_DataBusWidth1Bit);
	/*set card freq to 400KHZ*/
	g_mmc_card.busClock_Hz = USDHC_SetSdClock(base, 198000000U, SDMMC_CLOCK_400KHZ);
	/* send card active */
	USDHC_SetCardActive(base, 100U);
	/* Get host capability. ignore,just decision form spec HOST_CTRL_CAP*/ 

	/* CMD0 - GO_IDLE_STATE software reset the bus and set into idle */
	err = USDHC_SendCommand(base, CMD0, 0x0);
	if (err < 0)
		return -1;

    /* Hand-shaking with card to validata the voltage range Host first sending its expected
       information.*/
    err = USDHC_SendCommand(base, CMD1, 0x0);
	if (err < 0)
		return -1;
	g_mmc_card.ocr = base->CMD_RSP0;
	g_mmc_card.ocr |= 2 << 29;/* set access mode to sector mode */
	for (retries = 0; retries < 1000; retries++) {		
		err = USDHC_SendCommand(base, CMD1, 0x0);
		if (err < 0)
			return -1;

		if ((base->CMD_RSP0 & MMC_OCR_BUSY_MASK))
			break;
	}

	if (retries >= 1000 ) {
		printf("HandShakeOperationConditionFailed\r\n");
		return -1;
	}
	g_mmc_card.ocr = base->CMD_RSP0;
	printf("ocr is 0x%x\r\n", g_mmc_card.ocr);

	/* switch the host voltage which the card can support */
	/* ignore switch voltage, our board just support 3.3v */


	/* CMD2 Get card CID */
	err = USDHC_SendCommand(base, CMD2, 0x0);
	if (err < 0)
		return -1;
	USDHC_GetResponse(base, resp);
	memcpy(g_mmc_card.rawCid, resp, sizeof(resp));
	MMC_DecodeCid(&g_mmc_card, g_mmc_card.rawCid);

	/* Send CMD3 with a chosen relative address, with value greater than 1 */
	g_mmc_card.relativeAddress = 2;
	err = USDHC_SendCommand(base, CMD3, g_mmc_card.relativeAddress << 16);
	if (err < 0)
		return -1;

	/* CMD9 Get the CSD register content */
	err = USDHC_SendCommand(base, CMD9, g_mmc_card.relativeAddress << 16);
	if (err < 0)
		return -1;
	USDHC_GetResponse(base, resp);
	memcpy(g_mmc_card.rawCsd, resp, sizeof(resp));
	MMC_DecodeCsd(&g_mmc_card, g_mmc_card.rawCsd);

	/* Set to maximum speed in normal mode. */
	/*used to calculate the max speed in normal
    mode not high speed mode.
    For cards supporting version 4.0, 4.1, and 4.2 of the specification, the value shall be 20MHz(0x2A).
    For cards supporting version 4.3, the value shall be 26 MHz (0x32H). In High speed mode, the max
    frequency is decided by CARD_TYPE in Extended CSD. */
	printf("csd tran speed is 0x%x\r\n", g_mmc_card.csd.transferSpeed);
	if (g_mmc_card.csd.transferSpeed == 0x32)
		maxBusClock_Hz = 26000000;
	else if ( g_mmc_card.csd.transferSpeed == 0x2A )
		maxBusClock_Hz = 20000000;

	g_mmc_card.busClock_Hz = USDHC_SetSdClock(base, 198000000U, maxBusClock_Hz);

    /* Send CMD7 with the card's relative address to place the card in transfer state. Puts current selected card in
    transfer state. */
	err = USDHC_SendCommand(base, CMD7, g_mmc_card.relativeAddress << 16);
	if (err < 0)
		return err;

	/* Get Extended CSD register content. */
	err = USDHC_SendCommand_with_data(base, MCMD8, 0, g_mmc_card.rawExtendedCsd, 512);
	if (err < 0)
		return err;
	MMC_DecodeExtendedCsd(&g_mmc_card, g_mmc_card.rawExtendedCsd);
	printf("g_mmc_card.extendedCsd.sectorCount is %d\r\n", g_mmc_card.extendedCsd.sectorCount);

	/* set block size to 512 : CMD16 SET_BLOCKLEN */
	err = USDHC_SendCommand(base, CMD16, FSL_SDMMC_DEFAULT_BLOCK_SIZE);
	if (err < 0)
		return err;	

    /* switch to host support speed mode, then switch MMC data bus width and select power class */
	/* select bus width */
	err = MMC_SetBusWidth(base, bus_width);
	if (err < 0)
		return -1;
	printf("bus width is %d\r\n", bus_width);

	/* switch to high speed mode */
	err = MMC_SwitchBusMode(base, kMMC_HighSpeedTiming);
	if (err < 0)
		return err;
	g_mmc_card.busClock_Hz = USDHC_SetSdClock(base, 198000000U, MMC_CLOCK_52MHZ);

	printf("init MMC sucessful\r\n");

	return 0;
}

14.4.13 eMMC主函数

​ 为了可以烧录到eMMC上执行led的测试程序,需要将partition切换到boot1 partition,烧写完后再切换回user partition。切到boot1 partition后,从第二个块读取两个块数据并且打印,再将led_imx_image的1024偏移位置开始的数据写入到eMMC的boot1 partition,再将从第二个块读取两个块数据并且打印,查看读出的数据和写入的数据是否相同,然后切换回user partition。

​ 代码在裸机Git仓库 NoosProgramProject/(14_TF卡编程/006_sd/sd.c)

   ```c

mmc_init(USDHC2); MMC_SelectBoot1Partition(USDHC2);

	memset(sd_read_buf, 0, sizeof(sd_read_buf));
	sd_read_blocks(USDHC2, sd_read_buf, 2, 2);

	printf("read 2 sectors from eMMC boot1 partition 2nd sector\r\n");
	for(i = 0; i < 1024; i++) {
		printf("%02x ", sd_read_buf[i]);
		if((i + 1)%16 == 0)
			printf("\r\n");
	}

	/* wirte led_imx_image to eMMC boot1 partition */
	sd_write_blocks(USDHC2, led_imx_image + 1024, 2, (sizeof(led_imx_image) - 1024) >> 9);
	printf("please set dip switch to eMMC boot, and push reset, then green led blink\r\n");

	printf("read 2 sectors from eMMC boot1 part 2nd sector after burn led_imx_image\r\n");
	memset(sd_read_buf, 0, sizeof(sd_read_buf));
	sd_read_blocks(USDHC2, sd_read_buf, 2, 2);
	MMC_ExitBoot1Partition(USDHC2);

	for(i = 0; i < 1024; i++) {
		printf("%02x ", sd_read_buf[i]);
		if((i + 1)%16 == 0)
			printf("\r\n");
	}	
   ```

14.4.14 参考章节《4-1.4编译程序》编译程序

进入 **裸机Git仓库 NoosProgramProject/(14_TF卡编程/**009_timer_epit_poll) 源码目录。

14.4.15 参考章节《3-1.4映像文件烧写、运行》烧写、运行程序

14.4.16 测试SD卡

1.需要先将编译好的 sd.imx裸机程序烧写至emmc存储器内。

2.插入TF卡,并设置启动方式为emmc启动。

3.打开开发板串口终端,并开启开发板电源。

4.在打印的串口终端下输入1,来选择测试SD卡,

image-20220112144530783

之后在弹出的对话框里输入字符 c 用以继续操作。

image-20220112144536176

程序读取烧录前从第2扇(从0计)区开始的两个扇区,烧录led.imx,读出烧录后的从第2扇(从0计)区开始的两个扇区。烧写完成后会提示 please set dip switch to SD boot, and push reset, then green led blink 此时设置开发板为SD卡启动方式,打开电源,即可看到绿色的LED灯在闪烁。

14.4.17 测试EMMC

1.需要先将编译好的 sd.imx裸机程序烧写至TF卡内。

2.插入TF卡,并设置启动方式为SD卡启动。

3.打开开发板串口终端,并开启开发板电源。

4.在打印的串口终端下输入2,来选择测试EMMC,

image-20220112144557698

程序读取boot1 partition烧录前从第2扇(从0计)区开始的两个扇区,烧录led.imx,读出烧录后的从第2扇(从0计)区开始的两个扇区。烧写完成后会有如下图所示的打印信息,此时移除TF并设置启动方式为EMMC启动,观察绿色LED灯是否在闪烁。

image-20220112144605048

第十五章:LCD编程

第十五章:LCD编程

15. LCD编程

15.1 LCD硬件原理

15.2.1 LCD硬件工作原理简介

image-20220112144941418

​ 假设上图是一个LCD屏幕,屏幕中一个一个密密麻麻的黑点称之为像素点,每一行有若干个点,试想下有一个电子枪,电子枪位于某一个像素点的背后,然后向这个像素发射红,绿,蓝三种原色,这三种颜色不同比例的组合成任意一种颜色。电子枪在像素点的背后,一边移动一边发出各种颜色的光,电子枪从左往右移动,到右边边缘之后就跳到下一行的行首,继续从左往右移动,如此往复,一直移动到屏幕右下角的像素点,最后就跳回原点。

​ 问题1:电子枪如何移动?

​ 答: 有一条像素时钟信号线(DCLK),连接屏幕,每来一个像素时钟信号(DCLK),电子枪就移动一个像素。

​ 问题2:电子枪打出的颜色该如何确定?

​ 答:有三组红,绿,蓝信号线(RGB),连接屏幕,由这三组信号线(RGB)确定颜色

​ 问题3:电子枪移动到LCD屏幕右边边缘时,如何得知需要跳到下一行的行首?

​ 答:有一条水平同步信号线(HSYNC),连接屏幕,当接收到水平同步信号(HSYNC),电子枪就跳到下一行的最左边

​ 问题4:电子枪如何得知需要跳到原点?

​ 答:有一条垂直同步信号线(VSYNC),连接屏幕,当接收到垂直同步信号线(VSYNC),电子枪就由屏幕右下脚跳到左上角(原点)

​ 问题5:电子枪如何得知三组信号线(RGB)确定的颜色就是它是需要的呢?

​ 答:有一条RGB数据使能信号线(DE),连接屏幕,当接收到数据使能信号线(DE),电子枪就知道这时由这三组信号线(RGB)确定的颜色是有效的,可以发射到该像素点。

​ 下图是开发板,LCD控制器,LCD屏幕的框图

image-20220112145025755

​ 之前提到的像素时钟(DCLK), 三组红,绿,蓝信号线(RGB),水平同步信号线(HSYNC),垂直同步信号线(VSYNC),RGB数据使能信号线(DE)都是从LCD控制器发出的,只要开发板支持LCD显示,他肯定就会有一个LCD控制器。

​ 问题6:RGB三组信号线上的数据从何而来?

image-20220112145046895

​ 上图是RGB数据来源框图,内存中划出一部分区域,这块区域成为Framebuffer,在这个Framebuffer里面我们会构造好每一个颜色所对应的像素,Framebuffer中的值会被LCD控制器读出来,通过RGB三组线传给电子枪,电子枪再它转换成红绿蓝三种颜色打到屏幕上,在屏幕上的每一个像素,在我们的Frambuffer里面都有一个对应存储空间,里面存有屏幕上对应像素的颜色。

​ 我们的LCD控制器会周而复始的从Framebuffer中取出一个像素的颜色值,发给电子枪,同时需要和DCLK,VSYNC,HSYNC,DE这些信号配合好。

15.2.2 RGB接口的LCD硬件连接信号

​ 本次实验编程的屏幕属于RGB接口的显示屏,RGB接口的显示屏至少具备以下信号:

(1)像素时钟信号(DCLK)

​ 像素时钟信号,用于同步LCD上的DE,VS,HS,RGB信号线。

(2)RGB数据信号(R[0:7] ,G[0:7],B[0:7])

​ 三组信号线组成,分别代表R(红色),G(绿色),B(蓝色),这三组信号中的每一组都会有8根信号,三组共同组成24根线来控制颜色数据。

(3)RGB数据使能信号(DE)

​ RGB接口的 LCD 有两种驱动模式DE 模式和 HV 模式, 在HV模式下,需要用到HS与VS同伴RGB数据,在DE模式下,则只需要DE信号同伴RGB数据,但是一般做LCD显示程序,都会兼容两种模式,所以一般都要将数据使能信号(DE),垂直同步信号(HS),水平同步信号(VS)一起使用。

(4)水平同步信号,

​ 电路中常用HS或HSYNC表示,详细说明下一小节会说明。

(5)垂直同步信号(帧同步或场同步)

​ 电路中常用VS或VSYNC表示,相信说明下一小节会说明。

(6)LCD背光电源控制信号

​ 一般是由普通GPIO控制(利用高低电平控制背光),背光就是在在LCD显示屏的背部一大串的灯珠,用它们来照亮屏幕。

​ 例如100ASK_IMX6ULL开发板的LCD接口定义,就包含了上面所述的几种信号类型:

image-20220112145210913

15.2.3 TFT材质液晶屏接口简介(7寸1024600TN-RGB)

​ 嵌入式一般都采用TFT材质的液晶屏,如遇到别的材质的屏幕,操作方法也是雷同,可能稍微有些差异,针对差异去做修改即可,7寸1024600TN-RGB液晶屏幕接口引脚如下图,一些关键的引脚做了注释。

image-20220112145308136

15.2.4 LCD关键特性

①信号时序与信号的极性

​ 接下来我们查看下100ASK_7.0寸LCD手册时序图

image-20220112145317135

​ 从最小的像素开始分析,电子枪每次在CLK下降沿,如上图所示,该LCD在像素时钟下降沿采集数据,从数据线上得到数据,发射到显示屏上,然后移动到下一个位置。Dn0-Dn7上的数据来源就是前面介绍的FrameBuffer。就这样从一行的最左边,一直移动到一行的最右边,完成了一行的显示,假设为x。

​ 当打完一行的最后一个数据后,就会收到Hsync行同步信号,如上图可知该LCD的HSD有效脉冲为低脉冲,根据时序图,一个HSD周期可以大致分为五部分组成:thp、thb、thd、thf。thpw称为脉冲宽度,这个时间不能太短,太短电子枪可能识别不到。电子枪正确识别到thpw后,会从最右端移动最左端,这个移动的时间就是thb,称之为移动时间。thfp表示显示完最右像素,再过多久HSD才来,thd为数据有效区,th为打完一行所需要的时间。

image-20220112145335485

​ 同理,当电子枪一行一行的从上面移动到最下面时,VSD垂直同步信号,如上图可知该LCD的VSD有效脉冲为低脉冲。然后就让电子枪移动回最上边。VSD中的tvpw是脉冲宽度,tvb是移动时间,tvfp表示显示完最下一行像素,再过多久VSD才来,tvd为数据有效区,tv为打完一帧所需要的时间。假设一共有y行,则LCD的分辨率就是x*y。

image-20220112145341913

​ RGB数据有效信号(DEN),高电平表示数据有效。

根据以上信息大致了解几个关键信号的时序和极性,后面章节会详细介绍。

image-20220112145347564

​ 再根据上图,我们就可以确定像素时钟是51.2Mhz。

②RGB数据的存放形式

​ 前面的LCD硬件接口,R0-R7、G0-G7、B0-B7,每个像素是占据3*8=24位的,即硬件上LCD的BPP是确定的。虽然硬件引脚连接是固定的,但我们使用的时候,可以根据实际情况进行取舍,比如我们的IMX6ULL开发板,可以让他支持不同的像素格式,ARGB888,ARGB555,RGB565等等,

​ 本实验支持ARGB888和ARGB555。

ARGB888:每个像素就占据32位数据,其中最高字节A表示灰度透明度其余RGB数据8+8+8=24BPP。

ARGB555:每个像素就占据16位数据,其中最高位A表示灰度透明度其余RGB数据5+5+5=15BPP。

15.2 IMX6ULL LCD控制器操作及寄存器

15.2.1 LCD控制器模块介绍

​ IMX6ULL的LCD控制器名称为elcdif(增强型LCD接口)主要特性如下:

​ a.支持MPU模式,针对显示屏内部有显存的显示屏;

​ b.支持DOTCLK模式,针对RGB接口使用,本实验就是此模式;

​ c.VSYNC模式,针对高速数据传输(行场信号);

​ d. 8/16/18/24/32 bit 的bpp数据都支持,取决于IO的复用设置及寄存器配置;

​ e.MPU模式,VSYNC模式,DOTCLK模式,都具有配置的时序参数;

image-20220112145424746

​ 上图是IMX6ULL的LCD控制器框图,AXI是一种总线协议,通过此总线将显存中的RGB数据写入到FIFO,经过FIFO过度,到达LCD接口,LCD控制器分两个时钟域,一个是外设总线时钟域,一个是LCD像素时钟域,前者是用于让LCD控制器正常工作的时钟,后者是控制电子枪移动速度的时钟。Read_Data操作工作在MPU模式,我们采用的是DCLK模式,因此不予考虑。

​ 以上只是介绍了部分,如需要更加详细的了解需要查看IMX6ull芯片手册《Chapter 34

​ Enhanced LCD Interface (eLCDIF)》

15.2.2 LCD控制器寄存器简介

image-20220112145449271

​ 上图是我们将要使用到的寄存器,接着将会大致讲解下使用到的寄存器,更加详细说明会在后续的LCD控制编程实验中提及。

15.2.2.1 ①LCDIF_CTRL寄存器:

image-20220112145500174

SFTRST:软复位,用于修改像素时钟后,进行复位同步时钟;

BYPASS_COUNT:DOTCLK和DVI modes都需要设置为1;

DOTCLK_MODE:设置为1,进入DOTCLK模式;

LCD_DATABUS_WIDTH:RGB数据总线,跟进数据总线宽度设置;

WORD_LENGTH:输入的RBG数据格式,即多少位表示一个像素;

MASTER:LCD控制器主机模式设置;

DATA_FORMAT_16_BIT:当设置为16BPP的时候需要设置该位

15.2.2.2 ②LCDIF_CTRL1寄存器:

image-20220112145520234

BYTE_PACKING_FORMAT:用于表示4字节RGB数据中,那几个字节是属于有有效数据,因为其中有一个字节表示A(灰度,透明度)

15.2.2.3 ③LCDIF_TRANSFER_COUNT寄存器:

image-20220112145543927

V_COUNT:表示垂直方向上的像素个数,即分辨率中的x;

H_COUNT:表示水平方向上的像素个数,即分辨率中的y;

15.2.2.4 ④LCDIF_VDCTRL0寄存器

image-20220112145551594

VSYNC_PULSE_WIDTH:垂直同步信号的宽度;

VSYNC_PULSE_WIDTH_UNIT:根据不同模式下的计算时钟方式来决定垂直同步信号宽度;

VSYNC_PERIOD_UNIT:根据不同模式下的计算时钟方式来发垂直同步信号;

ENABLE_POL:DE数据有效信号的极性,即有效电平极性;

DOTCLK_POL:像素时钟信号的极性,即有效电平极性;

HSYNC_POL:水平同步信号的极性,即有效电平极性;

VSYNC_POL:垂直同步信号的极性,即有效电平极性;

ENABLE_PRESENT:在DOTCLK模式下,是否会硬件产生ENABLE使能数据信号;

VSYNC_OEB:VSYNC设置为输出还是输入模式,我们选择输出模式;

15.2.2.5 ⑤LCDIF_VDCTRL1寄存器

image-20220112145558654

VSYNC_PERIOD:两个垂直同步信号之间的总数,即垂直方向同步信号的总周期;

15.2.2.6 ⑥LCDIF_VDCTRL2寄存器

image-20220112145602359

HSYNC_PULSE_WIDTH:水平同步信号脉冲宽度;

HSYNC_PERIOD:两个水平同步信号之间的总数,即水平方向同步信号的总周期

15.2.2.7 ⑦LCDIF_VDCTRL3寄存器

image-20220112145625980

HRIZONTAL_WAIT_CNT:水平方向上的等待像素个数;

VERTICAL_WAIT_CNT:垂直方向上的等待像素个数;

15.2.2.8 ⑧LCDIF_VDCTRL4寄存器

image-20220112145632883

SYNC_SIGNALS_ON:工作在DOTCLK模式下,需要设置为1;

DOTCLK_H_VALID_DATA_CNT:DOTCLK模式下,水平方向上的有效像素点个数,即分辨率的y;

15.2.2.9 ⑨LCDIF_CUR_BUF寄存器

image-20220112145636601

ADDR:通过LCD控制器,发送的当前帧地址;

15.2.2.10 ⑩LCDIF_NEXT_BUF寄存器

image-20220112145651586

ADDR:通过LCD控制器,发送的下一帧地址;

15.3 编程_框架与准备

​ 本节文档对应的视频是《第003节_LCD编程_框架与准备_P》。

15.3.3 功能目的

​ 我们最终的目的是在LCD显示屏上画线、画圆和写字,此外还需要一个测试程序提供操作菜单,调用画线、画圆和写字操作,这些终究其核心是画点,我们需要实现画点才能实现其他功能,但是画点前也要让我们的LCD控制器正常工作起来才能实现它,最终总结:先让LCD控制器正常工作(配置寄存器),再编写画点的函数。

15.3.4 编程框架

​ 接着我们就需要实现画点,在实现画点之前想两个问题:

​ ①有两款尺寸大小的LCD显示屏,如何快速的在两个lcd上切换?

​ ②有两款不同的CPU都需要显示同一款LCD显示屏,如何快速的在两个cpu上切换?

​ 为了让程序更加好扩展,下面介绍“面向对象编程”的概念

​ 我们发现LCD显示屏虽然不同尺寸,参数不同,但是它们终究是LCD显示屏,我们可以把他们归一类,当需要使用某款LCD显示屏的参数时就提供该款的参数,其他的LCD显示屏参数不管他不就可以了吗?

​ 同理不同的CPU虽然LCD控制器地址不同,操作也不同,但是它们终究是LCD控制器,我们可以把他们归一类,当确定使用某个LCD控制器的时候就用这个LCD控制器的操作,其他的LCD控制器不管他。

​ 下图是LCD编程的框架,尽可能的“高内聚低耦合”,即类的内聚性是否高,耦合度是否低。目的是使程序模块的可重用性、移植性大大增强。通常程序结构中各模块的内聚程度越高,模块间的耦合程度就越低

image-20220112145758887

​ 根据不同的LCD控制器特性,来设置不同的LCD控制器,对于我们开发板,就是imx6ull_con.c,假如希望在其它 开发板上也实现LCD显示,只需添加相应的代码文件xxx_con即可。

​ 根据不同的LCD屏幕特性,来编写不同的LCD屏幕参数,对于我们的开发板,就是lcd_7_0.c,假如希望这个开发板支持别的LCD屏幕,只需添加相应的代码文件lcd_xxx.c即可。

15.4 编程_抽象出重要结构体

​ 本节代码在裸机Git仓库 NoosProgramProject/(15_LCD编程/01_simple_test/lcd_manager.h) 与**裸机Git仓库 NoosProgramProject/(15_LCD编程/01_simple_test/lcd_controller_manager.h)**头文件中,本节文档对应的视频是《第004节_LCD编程_抽象出重要结构体_P》。

15.4.1 抽象出LCD屏幕的结构体

​ 建立一个lcd_manager.h,将任意LCD都共有的参数(引脚的极性、时序、数据的格式bpp、分辨率等)使用面向对象的思维方式,将这些封装成结构体放在lcd_manager.h中

enum {
		NORMAL = 0,
		INVERT = 1,
	};

	/* NORMAL : 正常
	* INVERT : 取反
	*/
	typedef struct pins_polarity {
		int de;    /* normal: 高电平使能数据 */
		int vclk;  /* normal: 在下降沿获取数据 */
		int hsync; /* normal:高脉冲 */
		int vsync; /* normal:高脉冲  */
	}pins_polarity, *p_pins_polarity;

	typedef struct time_sequence {
		/* 垂直方向 */
		int tvp; /*  vysnc脉冲宽度 */
		int tvb; /*上边黑框 , Vertical Back porch */
		int tvf; /*下边黑框, Vertical Front porch */
	
		/* 水平方向 */
		int thp; /* hsync脉冲宽度 */
		int thb; /* 左边黑框 ,Horizontal Back porch */
		int thf; /* 右边黑框,Horizontal Front porch */

		int vclk;
	}time_sequence, *p_time_sequence;


	typedef struct lcd_params {
	
		char *name;
		
		/*引脚极性参数*/
		pins_polarity pins_pol;
	
		/*时序参数*/
		time_sequence time_seq;
	
		/*分辨率*/
		int xres;
		int yres;
		int bpp;
	
		/*显存*/
		unsigned int fb_base;
	
	}lcd_params, *p_lcd_params;

​ 以后就使用lcd_params结构体来表示lcd参数 ,通过register_lcd函数注册某款LCD屏幕参数到一个lcd_params结构体数组,然后通过select_lcd函数在lcd_params结构体数组中选中指定的LCD屏幕参数保存起来,提供给其他函数用。

15.4.2 抽象出LCD控制器的结构体

​ 建立一个lcd_controller_manager.h,将任意LCD控制器都共有的函数(初始化函数,使能函数等)使用面向对象的思维方式,将这些封装成结构体放在lcd_controller_manager.h中

typedef struct lcd_controller{
	char* name;
	void (*init)(p_lcd_params plcdparams);
	void(*enable)(void);
	void(*disable)(void);
}lcd_controller, *p_lcd_controller;

​ 以后就使用lcd_controller结构体来表示lcd控制器。

​ 建立一个lcd_controller_manager.c,提供一系列的LCD控制器的管理函数,用这些管理函数,通过register_lcd_controller函数注册新的LCD控制器到lcd_controller的结构体数组中,然后通过select_lcd_controller函数在lcd_controller结构体数组中选中指定的LCD控制器,提供给其他函数用,最终用户再调用光宇LCD控制器的操作时,就会通过选中的lcd_controller的结构体访问到对应的某款LCD控制器的函数。

15.5 编程_LCD控制器

​ 本节代码在**裸机Git仓库 NoosProgramProject/(15_LCD编程/01_simple_test/imx6ull_con.c)**源文件中, 本节文档对应的视频是《第005节_LCD编程_LCD控制器_P》。

15.5.1 LCD控制器相关引脚复用配置

image-20220112145945035

​ 根据前面硬件接口章节15.1.3和上图,我们知道要设置这30个引脚,设置引脚按两步走,第一步设置引脚复用功能,第二部设置引脚的硬件属性。查看相应的寄存器得知,复用功能寄存器均设置为0即可,接着硬件属性根据章节《4-1.3 GPIO操作方法》的内容,均设置为0xB9即可。

15.5.2 LCD控制器像素时钟配置

​ 根据IMX6ULL芯片手册的Chapter 18 Clock Controller Module (CCM),我们就可以设置像素时钟为我们需要的51.2Mhz

15.5.2.1 ①确定PLL

image-20220112150005692

​ 由上图可知LCD控制器的时钟来源是PLL5(video pll)

image-20220112150039275

​ 根据上图可得知VIDEO pll的公式,但是为了方便计算,我们不需要后面的小数运算即Video PLL output frequency(PLL5)= Fref * (DIV_SELECT + 0)

15.5.2.2 ②确定PLL后的分频系数

image-20220112150053310

​ 根据上图可知PLL5出来后经过两级分频,即PLL5_MAIN_CLK = PLL5 / POST_DIV_SELECT / VIDEO_DIV

15.5.2.3 ③PLL分频后进入LCDIF控制器前的分频系数

image-20220112150121312

​ 根据上图可知PLL5分频后到LCDIF控制器也有两级分频,即LCDIF1_CLK_ROOT = PLL5_MAIN_CLK /LCDIF1_PRED / LCDIF1_PODF,根据上面三个内容,我们可以采用以下这个配置来达到像素时钟51.2Mhz,DIV_SELECT = 32;NUM = 0;DENOM = 0;POST_DIV_SELECT = 1,VIDEO_DIV = 1LCDIF1_PRED = 3;LCDIF1_PODF = 5

​ 带入时钟公式得24*(32+0)/1/1/3/5 ≈ 51.2Mhz。下面开始编程:

15.5.3 LCD控制器时钟编程

15.5.3.1 ①取消小数分配器

  CCM_ANALOG->PLL_VIDEO_NUM   = 0; 	 
  CCM_ANALOG->PLL_VIDEO_DENOM = 0;

​ 清零表示取消小数分配器

15.5.3.2 ②设置CCM_ANALOG_PLL_VIDEOn寄存器

image-20220112150211473

CCM_ANALOG->PLL_VIDEO =  (2 << 19) | (1 << 13) | (32<< 0);

​ 设置PLL5使能,倍频为32倍,1分频(即不分频),选择外部24M晶振为时钟源,至此PLL5分频后为24 * 32 / 1

15.5.3.3 ③设置CCM_ANALOG_MISC2n

image-20220112150247815

默认就为1分频,所以无需设置,至此PLL5分频后为24 * 32 / 1 / 1 = 768Mhz

15.5.3.4 ④设置CCM_CSCDR2

image-20220112150300024

设置选中对应的时钟源,预分频系数为3,得768 / 3 = 256Mhz

	CCM->CSCDR2 &= ~(7 << 15); 	   
	CCM->CSCDR2 |=  (2 << 15);  
	CCM->CSCDR2 &= ~(7 << 12); 	 
	CCM->CSCDR2 |=  (2 << 12); 						 
 	CCM->CSCDR2 &= ~(7 << 9);

15.5.3.5 ⑤设置CCM_CBCMR

image-20220112150322222

	CCM->CBCMR &= ~(7 << 23);					 
	CCM->CBCMR |=	4 << 23;	/*[25:23] :4 : 表示5分频*/

设置预分频系数为5,最终得到51.2Mhz

15.5.3.6 ⑥重新同步时钟

image-20220112150341565

/* 重新设置时钟后,需要软复位LCD控制器,让LCD控制器像素时钟同步*/
	 LCDIF->CTRL  = 1<<31;   

  /*软复位需要花费好几个时钟周期,这里需要一些时间等待*/
	 delay(100);

  /*同步像素时钟结束*/
	LCDIF->CTRL  = 0<<31; /* 取消复位 */

15.5.4 LCD控制器像素格式配置

15.5.4.1 ①设置LCDIF_CTRLn寄存器

image-20220112150402916

	LCDIF->CTRL |= (1 << 19) | (1 << 17) |(3 << 10) | (bpp_mode << 8) | (1 << 5) ;

		/* [3]当bpp为16时,数据格式为ARGB555*/
	 	if(plcdparams->bpp == 16)
	 	{
	 		LCDIF->CTRL |= 1<<3;
	 	}

15.5.4.2 ②设置LCDIF_CTRL1n寄存器

image-20220112150501256

if(plcdparams->bpp == 24 || plcdparams->bpp == 32)
	 {		
	 		LCDIF->CTRL1 &= ~(0xf << 16); 
		 	LCDIF->CTRL1 |=  (0x7 << 16); 
	 } 

表示ARGB传输格式模式下,传输24位无压缩数据,A通道不用传输),当我们选用16bpp即ARGB555时,不需要设置此位。

15.5.5 LCD控制器时序配置及极性配置

15.5.5.1 ①设置LCDIF_TRANSFER_COUNT寄存器

image-20220112150610501

    ```c

LCDIF->TRANSFER_COUNT = (plcdparams->yres << 16) | (plcdparams->xres << 0); ```

15.5.5.2 ②设置LCDIF_VDCTRL0n寄存器

image-20220112150638122

LCDIF->VDCTRL0 = (1 << 28)|( plcdparams->pins_pol.vsync << 27)
						|( plcdparams->pins_pol.hsync << 26)
						|( plcdparams->pins_pol.vclk << 25)
						|(plcdparams->pins_pol.de << 24)
						|(1 << 21)|(1 << 20)|( plcdparams->time_seq.tvp << 0);

15.5.5.3 ③配置LCDIF_VDCTRL1寄存器

image-20220112150655412

LCDIF->VDCTRL1 = plcdparams->time_seq.tvb + plcdparams->time_seq.tvp + plcdparams->yres + plcdparams->time_seq.tvf;  

设置垂直方向的总周期:上黑框tvb+垂直同步脉冲tvp+垂直有效高度yres+下黑框tvf

15.5.5.4 ④配置LCDIF_VDCTRL2寄存器

image-20220112150744101

LCDIF->VDCTRL2 = (plcdparams->time_seq.thp << 18) | (plcdparams->time_seq.thb +  plcdparams->time_seq.thp + plcdparams->xres + plcdparams->time_seq.thf);

[18:31] : 水平同步信号脉冲宽度

[17: 0] : 水平方向总周期

设置水平方向的总周期:左黑框thb+水平同步脉冲thp+水平有效高度xres+右黑框thf

15.5.5.5 ⑤配置LCDIF_VDCTRL3寄存器

image-20220112150809660

LCDIF->VDCTRL3 = ((plcdparams->time_seq.thb + plcdparams->time_seq.thp) << 16) | (plcdparams->time_seq.tvb + plcdparams->time_seq.tvp);

设置ELCDIF的VDCTRL3寄存器

[27:16] :水平方向上的等待时钟数 =thb + thp

[15:0] : 垂直方向上的等待时钟数 = tvb + tvp

15.5.5.6 ⑥设置LCDIF_VDCTRL4寄存器

image-20220112150833971

LCDIF->VDCTRL4 = (1<<18) | (plcdparams->xres);

设置ELCDIF的VDCTRL4寄存器

[18] : 使用VSHYNC、HSYNC、DOTCLK模式此为置1

17:0: 水平方向的宽度

15.5.6 设置显存

image-20220112150949262

LCDIF->CUR_BUF  =  plcdparams->fb_base;
LCDIF->NEXT_BUF =  plcdparams->fb_base;

CUR_BUF : 当前显存地址

NEXT_BUF : 下一帧显存地址

方便运算,都设置为同一个显存地址

15.5.7 编程_LCD设置

本节代码在**裸机Git仓库 NoosProgramProject/(15_LCD编程/01_simple_test/lcd_7_0.c)**源文件中 ,本节文档对应的视频是《第006节_LCD编程_LCD设置_P》

15.5.8 添加LCD屏幕名称

.name = "lcd_7.0",

先给我们本次实验的LCD屏幕参数一个名称,可以根据名称去选择我们需要的LCD屏幕参数。

15.5.9 极性设置

LCDIF_VDCTRL0n寄存器极性设置位

image-20220112151048189

VSYNC_POL与HSYNC_POL:0表示低电平有效,1表示高电平有效

DOTCLK_POL:0表示上升沿有效,1表示下降沿有效

ENABLE_POL:0表示低电平有效,1表示高电平有效

​ 接着根据15-1.4关键特性章节中,我们已经知道像素时钟DCLK是下降沿时获取数据,水平同步信号HSD和垂直同步信号VSD都是低电平有效,数据使能信号DEN是高电平表示数据有效,接着我们只要在lcd_7_0.c文件中设定成对应的标记用来识别即可,1)数据使能信号de设置为1, 2)像素时钟vclk极性设置为1 ,3)水平同步信号HSD和垂直同步信号VSD都设置为0。

enum {
	NORMAL = 0,
	INVERT = 1,
};

根据枚举内容,填入对应的参数即可

.pins_pol = {
	.de    = INVERT,	/* normal: 低电平表示使能输出 */
	.vclk  = INVERT,		/* normal: 在上升降沿获取数据*/
	.hsync = NORMAL,    	/* normal: 低脉冲*/
	.vsync = NORMAL, 	/* normal: 低脉冲*/
},

15.5.10 时序设置

image-20220112151141164

​ 观察上图中红色框框内容,接着我们只需要把上面数值往lcd_7_0.c中定义的lcd_7_0_params中填入对应的值即可

​ 1)Thpw:结构体中的tvb = 20

​ 2)Thb:结构体中的thb = 140

​ 3)Thfp:结构体中的thf = 160

​ 4)THA:结构体中的tvp = 1024

​ 5)Tvpw:结构体中的tvp = 3

​ 6)Tvb:结构体中的thp = 20

​ 7)Tvfp:结构体中的tvf = 12

​ 8)Tvd:结构体中的yres = 600

.time_seq = {
	/* 垂直方向 */
	.tvp=	3, /* vysnc脉冲宽度 */
	.tvb=	20,  /* 上边黑框, Vertical Back porch */
	.tvf=	12,  /* 下边黑框, Vertical Front porch */

	/* 水平方向 */
	.thp=	20, /* hsync脉冲宽度 */
	.thb=	140,  /* 左边黑框, Horizontal Back porch */
	.thf=	160,  /* 右边黑框, Horizontal Front porch */

	.vclk=	51.2,  /* MHz */
},
.xres = 1024,
.yres = 600

15.5.11 显存地址设置

image-20220112151220548

​ 根据上图imx_6ull可以挂接最大2GB的内存,可实际我们使用的100ask_imx_6ull开发板只挂接了512M的内存,因此我们可选的内存地址范围是0x80000000~0xa0000000。我们的裸机实验的链接地址是0x80100000,我们裸机实验程序不大,最多算它0x900000(实际没有这么多),那么我们显存可以用范围就变成0x81000000~0xa0000000,再计算我们需要的显存大小10246004=2457600≈2.4MB。

​ 总结:0x81000000~0xa0000000范围内,任意满足2.4MB空间的内存都可以。

​ 本次实验选取的显存是0x99000000。

15.6 编程_简单测试

​ 简单测试编程实现LCD全屏顺序显示红,绿,蓝三种颜色。

​ 本节代码在**裸机Git仓库 NoosProgramProject/(15_LCD编程/01_simple_test)**工程文件中,同时本节文档对应的视频是《第007节_LCD编程_简单测试_P》。

15.6.1 初始化LCD

	/*添加LCD屏幕参数*/		
	lcd_7_0_add();
	/*添加LCD控制器*/		
	lcd_contoller_add();

​ 使用lcd_manager.c和lcd_controller_manager.c中的管理函数,添加LCD屏幕参数和LCD控制器到各自的结构体数组中。

	/*选择Imx6ull的LCD控制器*/		
	select_lcd_controller("Imx6ull");	
	/*选择LCD屏幕参数*/		
	select_lcd("lcd_7.0");

​ 使用lcd_manager.c和lcd_controller_manager.c中的管理函数,从各自的结构体数组中选择指定名称的的LCD屏幕参数和LCD控制器。

lcd_controller_init(g_p_lcd_selected);

​ 最后通过lcd_controller_init函数将选择的LCD参数传入到被选中的LCD控制器中,调用该控制器的初始化函数进行初始化。

15.6.2 使能LCD

​ g_p_lcd_controller_selected变量就是被选LCD控制器的函数指针集合,接着调用对应的函数指针即可

	void lcd_controller_enable(void)
	{
		if (g_p_lcd_controller_selected)
		{
			g_p_lcd_controller_selected->enable();
		}
	}

​ g_p_lcd_controller_selected变量就是被选LCD控制器的函数指针集合,现在我们已经成功的选中LCD控制器,接着调用lcd_controller_enable函数,它将负责调用g_p_lcd_controller_selected结构体中的使能函数。即是被选中的LCD控制器使能函数。

static void Imx6ull_lcd_controller_enable(void)
{	
	LCDIF->CTRL |= 1<<0; /* 使能6ULL的LCD控制器 */
}

​ 由于我们选定是imx6ull的LCD控制器,所以g_p_lcd_controller_selected->enable()调用的就是我们使用的Imx6ull_lcd_controller_enable函数。

15.6.3 获取LCD参数

​ 在执行我们的简单测试之前,还需要让我们的测试程序知道现在用的lcd关键参数,实现各种颜色的显示。

	void get_lcd_params(unsigned int *fb_base, int *xres, int *yres, int *bpp)
	{
		*fb_base = g_p_lcd_selected->fb_base;
		*xres = g_p_lcd_selected->xres;
		*yres = g_p_lcd_selected->yres;
		*bpp = g_p_lcd_selected->bpp;
	}

​ 获取显存地址fb_base,获取分辨率xres和yres,获取bpp即每个像素点所占的位数。

15.6.4 往framebuffer中写数据

​ 为了支持16bpp或32bpp,我们需要分两个分支实现往framebuffer中写数据

		p = (unsigned short *)fb_base;
		for (x = 0; x < xres; x++)
			for (y = 0; y < yres; y++)
				*p++ = 0x7c00;

		/* green */
		p = (unsigned short *)fb_base;
		for (x = 0; x < xres; x++)
			for (y = 0; y < yres; y++)
				*p++ = 0x3E0;

		/* blue */
		p = (unsigned short *)fb_base;
		for (x = 0; x < xres; x++)
			for (y = 0; y < yres; y++)
				*p++ = 0x1f;

​ 当获取的lcd参数bpp等于16时走上面分支

​ 当bpp为16时,我们lcd控制设定的颜色格式是ARGB555,也就是ARRRRRGGGGGBBBBB,红绿蓝各占5位,最高一位为灰度。

		p2 = (unsigned int *)fb_base;
			for (x = 0; x < xres; x++)
			for (y = 0; y < yres; y++)
			*p2++ = 0xff0000;

		/*green*/ 
		p2 = (unsigned int *)fb_base;
		for (x = 0; x < xres; x++)
			for (y = 0; y < yres; y++)
				*p2++ = 0x00ff00;

		/*blue*/ 
		p2 = (unsigned int *)fb_base;
		for (x = 0; x < xres; x++)
			for (y = 0; y < yres; y++)
				*p2++ = 0x0000ff;

​ 当获取的lcd参数bpp等于32时走上面分支,当bpp为32时,我们lcd控制设定的颜色格式是ARGB888,也就是AAAAAAAARRRRRRRRGGGGGGGGBBBBBBBB,红绿蓝各占8位,最高字节表示灰度。

15.7 编程_画点线圆

​ 本节代码在**裸机Git仓库 NoosProgramProject/(15_LCD编程/02_dot_line_circle)**工程文件中,同时本节文档对应的视频是《第008节_LCD编程_画点线圆_P》。

15.7.1 实现画点

①一个点(x,y)在FB中的位置如图

image-20220112151751170

可以得出其计算公式:(x,y)像素起始地址=fb_base+(xres*bpp/8)y + xbpp/8

②新建立一个frambuffer.c文件,由于新建立的frambuffer.c没有LCD的资源参数,所以需要编写一个调用

	void fb_get_lcd_params(void)
	{
		get_lcd_params(&fb_base, &xres, &yres, &bpp);
	}

③在获取LCD资源参数之后,当bpp是32位的时候,可以直接往对应显存位置填颜色值,但是当bpp等于16位时候,我们就需要一些转换,让32位颜色数据变成16位颜色数据,由于16bpp我们使用的是ARG555,因此实际每一个颜色只有5位。我们需要先将32位中的rgb分离出来,再通过移位指令移到ARG555格式对应的红绿蓝位置。

int r = (rgb >> 16)& 0xff;
	int g = (rgb >> 8) & 0xff;
	int b = rgb & 0xff;

	/* argb555 */
	r = r >> 3;
	g = g >> 3;
	b = b >> 3;
	return ((r<<10) | (g<<5) | (b));

④编写画点函数,提供x坐标,y坐标,和颜色,实现往显存对应的位置填写颜色数据即可,对于16PP,每个像素只占据16位(2字节),因此采用unsigned short类型;对于32PP,每个像素只占据32位(4字节),因此采用unsigned int类型;

void fb_put_pixel(int x, int y, unsigned int color)
	{
		unsigned short *pw;  /* 16bpp */
		unsigned int   *pdw; /* 32bpp */
	
		unsigned int pixel_base = fb_base + (xres * bpp / 8) * y + x * bpp / 8;
	
		switch (bpp)
		{
			case 16:
				pw = (unsigned short *) pixel_base;
				*pw = convert32bppto16bpp(color);
			break;
			case 32:
				pdw = (unsigned int *) pixel_base;
				*pdw = color;
				break;
		}
	}

15.7.2 实现画线

​ 画线的具体原理不是我们的主要内容,我们直接百度“C语言 LCD 画线”可以得到相关的实现代码,比如这篇博客:http://blog.csdn.net/p1126500468/article/details/50428613

​ 新建一个geometry.c,复制博客中代码,替换里面的描点显示函数即可

​ 最后在主函数测试程序里,加上画线的测试代码:

fb_get_lcd_params();
	
	delay(100000);

	draw_line(0, 0, xres - 1, 0, 0xff0000);
	delay(100000);
	draw_line(xres - 1, 0, xres - 1, yres - 1, 0xffff00);
	delay(100000);
	draw_line(0, yres - 1, xres - 1, yres - 1, 0xff00aa);
	delay(100000);
	draw_line(0, 0, 0, yres - 1, 0xff00ef);
	delay(100000);
	draw_line(0, 0, xres - 1, yres - 1, 0xff4500);
	delay(100000);
	draw_line(xres - 1, 0, 0, yres - 1, 0xff0780);

​ 我们为划线函数提供对应的参数起点坐标,末点坐标,颜色就可以实现。

15.7.3 实现画圆

​ 画圆的具体原理不是我们的主要内容,我们直接百度“C语言 LCD 画圆”可以得到相关的实现代码,比如这篇博客:http://blog.csdn.net/p1126500468/article/details/50428613

在geometry.c中添加,博客中代码,替换里面的描点显示函数即可

image-20220112151929285

​ 我们只需提供圆心的坐标,圆的半径,和圆的颜色即可实现。最后在主函数测试程序里,加上画圆画线的测试代码:

draw_circle(xres/2, yres/2, yres/4, 0xff00);

15.8 编程_显示文字

​ 本节代码在**裸机Git仓库 NoosProgramProject/(15_LCD编程/03_font_test)**工程文件中,同时本节文档对应的视频是《第009节_LCD编程_显示文字_P》。

15.8.1 获取LCD参数

​ 新建一个font.c,由于新建文件中没有LCD参数,因此也需要获取LCD参数,根据这些LCD参数显示我们需要显示的字符。

void font_init(void)
{
	get_lcd_params(&fb_base, &xres, &yres, &bpp);
}

15.8.2 编写单个字符显示函数

​ 文字也是由点构成的,一个个点组成的点阵,宏观的来看,就是文字。可以参考Linux内核源码中的相关操作,在内核中搜索“font”,打开font_8x16.c,可以看到里面的A字符内容如下:

	/* 65 0x41 'A' */
	0x00, /* 00000000 */
	0x00, /* 00000000 */
	0x10, /* 00010000 */
	0x38, /* 00111000 */
	0x6c, /* 01101100 */
	0xc6, /* 11000110 */
	0xc6, /* 11000110 */
	0xfe, /* 11111110 */
	0xc6, /* 11000110 */
	0xc6, /* 11000110 */
	0xc6, /* 11000110 */
	0xc6, /* 11000110 */
	0x00, /* 00000000 */
	0x00, /* 00000000 */
	0x00, /* 00000000 */
	0x00, /* 00000000 */

​ 根据这些数据,在一个8*16的区域里,将为1的点显示出来,为0的则不显示,最终将呈现一个字母“A”。根据字母的点阵在LCD上描画文字,需要的步骤如下:

​ a. 根据带显示的字符的ascii码在fontdata_8x16中得到点阵数据

​ b. 根据点阵来设置对应象素的颜色

​ c. 根据点阵的某位决定是否描颜色

void fb_print_char(int x, int y, char c, unsigned int color)
	{
		int i, j;
		
		/* 根据c的ascii码在fontdata_8x16中得到点阵数据 */
		const unsigned char *dots = &fontdata_8x16[c * 16];
	
		unsigned char data;
		int bit;
	
		/* 根据点阵来设置对应象素的颜色 */
		for (j = y; j < y+16; j++)
		{
			data = *dots++;
			bit = 7;
			for (i = x; i < x+8; i++)
		
				/* 根据点阵的某位决定是否描颜色 */
				if (data & (1<<bit))
					fb_put_pixel(i, j, color);
				bit--;
			}
		}
	}

​ 在font_8x16.c里面,每个字符占据16位,因此想要根据ascii码找到对应的点阵数据,需要对应的乘16,再取地址,得到该字符的首地址

​ 再根据每个点阵数据每位是否为1,来调用描点函数fb_put_pixel()。这样,依次显示16个点阵数据,获得字符图。

15.8.3 编写实现字符串显示函数

​ 显示字符串,那么就需要在每显示完一个字符后,x方向加8个像素,同时考虑是否超出屏幕(xres)显示范围进行换行处理(y+16)

	void fb_print_string(int x, int y, char* str, unsigned int color)
	{
		int i = 0, j;
		
		while (str[i])
		{
			if (str[i] == '\n')
				y = y+16;
			else if (str[i] == '\r')
				x = 0;
	
			else
			{
				fb_print_char(x, y, str[i], color);
				x = x+8;
				if (x >= xres) /* 换行 */
				{
					x = 0;
					y = y+16;
				}
			}
			i++;
		}
	}

​ 最后在在主函数里,加上显示字符串的函数,传入希望显示的字符串即可。

fb_print_string(10, 10, "www.100ask.net\n\r100ask.taobao.com", 0xff00);

15.8.4 参考章节《4-1.4编译程序》编译程序

​ 进入 裸机Git仓库 NoosProgramProject/(15_LCD编程/03_font_test) 源码目录进行编译。

15.8.5 参考章节《3-1.4映像文件烧写、运行》烧写、运行程序

​ 此时可以看到屏幕先显示RGB三原色 之后显示点 线  和圆,最后显示字体

第十六章:I2C编程

第十六章:I2C编程

16. I2C编程

​ I2C(Inter-Integrated Circuit BUS)是I2C BUS简称,中文为集成电路总线,是目前应用最广泛的总线之一,和IMX6ULL有些相关的是,刚好该总线是NXP前身的PHILIPS设计。当前仍然是应用最广泛的总线协议之一。

16.1 I2C协议

16.1.1 概述

​ I2C是一种串行通信总线,使用多主从架构,最初设计师为了让主板、嵌入式系统或手机用以连接低速周边设备而发展而来。在小数据量场合使用,有传输距离短,任意时刻只能有一个主机等特性。严格意义上讲,I2C应该是软硬件结合体,所以我们将分物理层和协议层来介绍该总线。(总线结构如下图)

image-20220112152634031

​ 对于I2C通信的过程,韦老师有个形象的说法:

​ 传输数据,我们需要发数据,从主设备发送到从设备上去,也需要把数据从从设备传送到主设备上去,数据涉及到双向传输。

​ 举个例子:

image-20220112152634031

​ 体育老师:可以把球发给学生,也可以把球从学生中接过来。

​ ① 发球:

​ a. 老师说:注意了(start)

​ b. 老师对A学生说,我要球发给你(地址)。

​ c. 老师就把球发出去了(传输)。

​ d. A收到球之后,应该告诉老师一声(回应)。

​ e. 老师说下课(停止)

​ ② 接球:

​ a. 老师说注意了(start),

​ b. 老师说:B把球发给我(地址)

​ c. B就把球发给老师(传输)

​ d. 老师收到球之后,给B说一声,表示收到球了(回应)。

​ e. 老师说下课(停止)

我们就使用这个简单的例子,来解释一下IIC的传输协议。

16.1.2 物理层

​ **特性1:**半双工(非全双工)

​ 两条总线线路:

SDA(串行数据线): 主芯片通过一根SDA线既可以把数据发给从设备,也可以从SDA上读取数据,连接SDA线的引脚里面必然有两个引脚(发送引脚/接受引脚),具体可以参考下图device端I2Cn_SDA(output/input)。

​ **SCL(**串行时钟线):**同SDA的引脚电路结构一致,引脚的输出驱动与输入缓冲连在一起。其中输出为漏极开路的场效应管、输入缓冲为一只高输入阻抗的同相器。这样结构有如下特性:

​ a. 由于 SDA、SCL 为漏极开路结构,借助于外部的上拉电阻实现了信号的“线与”逻辑;

​ b. 引脚在输出信号的同时还将引脚上的电平进行检测,检测是否与刚才输出一致。为 “时钟同步”和“总线仲裁”提供硬件基础。

​ SDA和CLK连接线上连有两个上拉电阻,当总线空闲时,两根线均为高电平。连到总线上的任一器件输出的低电平,都将使总线的信号变低。(物理层连接如下图所示)

image-20220112152634031

​ **特性2:**地址和角色可配置

​ 每个连接到总线的器件都可以通过唯一的地址和其它器件通信,主机/从机角色和地址可配置,主机可以作为主机发送器和主机接收器。

特性3:多主机

​ IIC是真正的多主机总线,( IIC可以在通讯过程中,改变主机),如果两个或更多的主机同时请求总线,可以通过冲突检测和仲裁防止总线数据被破坏。

​ **特性4:**传输速率

​ 传输速率在标准模式下可以达到100kb/s,快速模式下可以达到400kb/s。

​ **特性5:**负载和距离

​ 节点的最大数量受限于地址空间以及总线电容决定。另外总电容也限制了实际通信距离只有几米。

16.1.2 协议层

(1)数据有效性

​ I2C协议的数据有效性是靠时钟来保证的,在时钟的高电平周期内,SDA线上的数据必须保持稳定。数据线仅可以在时钟SCL为低电平时改变。

image-20220112152634031

(2)起始和结束条件

**起始条件:**当SCL为高电平的时候,SDA线上由高到低的跳变被定义为起始条件。

**结束条件:**当SCL为高电平的时候,SDA线上由低到高的跳变被定义为停止条件,要注意起始和终止信号都是由主机发出的,连接到I2C总线上的器件,若具有I2C总线的硬件接口,则很容易检测到起始和终止信号。

image-20220112152634031

​ 总线在起始条件之后,视为忙状态,在停止条件之后被视为空闲状态。

(3)应答

​ 每当主机向从机发送完一个字节的数据,主机总是需要等待从机给出一个应答信号,以确认从机是否成功接收到了数据,从机应答主机所需要的时钟仍是主机提供的,应答出现在每一次主机完成8个数据位传输后紧跟着的时钟周期,低电平0表示应答,1表示非应答

(4)数据帧格式

​ SDA线上每个字节必须是8位长,传输几个字节每个transfer是不限制的,每个字节后面必须跟一个ACK。数据首先用最高有效位(MSB)传输。

image-20220112152634031

16.2 IMX6ULL的I2C控制器操作与寄存器介绍

​ IMX6ULL的I2C提供了标准I2C从服务器和主服务器的功能,I2C是设计与标准的NXP I2C总线协议兼容,所以上面的通用知识完全可以应用在IMX6ULL I2C的编程和控制。

​ 掌握IMX6ULL I2C控制器的使用,重点要熟悉IMX6ULL I2C的寄存器的操作。

​ 重点介绍寄存器之前,我们先来看一下IMX6ULL的I2C控制器的框图,对IMX6ULL I2C的结构和特性有一个初步认识。

image-20220112152634031

IMX6ULL的I2C控制器有如下额外特性:

​ ① 多主机运行。

​ ② 64种不同的串行时钟频率之一的软件可编程性。

​ ③ 软件可选择的应答位。

​ ④ 中断驱动,逐字节数据传输。

​ ⑤ 仲裁丢失中断与自动模式切换从主到从。

​ ⑥ 启动和停止信号生成/检测。

​ ⑦ 重复启动信号生成。

​ ⑧ 应答位生成和检测。

​ ⑨ 总线忙检测。

​ 另外的IMX6ULL 的I2C也支持两种模式:标准模式和快速模式,标准模式下I2C数据传输速率最高是100Kbits/s,在快速模式下数据传输速率最高为400Kbits/s。

​ 通过上面的介绍对IMX6ULL的I2C控制器有了整体认识,下面结合I2C 框图对重点寄存器进行介绍。

16.2.1 I2C Memory Map

I2C包含5个16-bit 的寄存器

注意:寄存器在偏移量0x0002/0x0006/0x000A/0x000E作为保留位。

可以看到I2C1的入口地址为21A_0000,这个我们重点关注,后面做实验,编程会使用到。

image-20220112152634031

16.2.2 Register

​ 我们先把这些寄存器的用途做个介绍,后面在编程框架那里会结合位图来讲解。下面这部分也可以先跳过,回头当表来查。

16.2.2.1 I2C Address Register (I2Cx_IADR)地址寄存器,偏移量为0h。

​ a. bit15-8为保留位即只读为0。

​ b. bit7-1(ADR)位是I2C作为从机时的地址。从属模式是I2C的默认模式,这个地址是作为从机的相应地址,不能被软件复位。

16.2.2.2 I2C Frequency Divider Register (I2Cx_IFDR)分频寄存器,偏移量为4h。

​ a. bit15-6位为保留位,即只读为0。

​ b. bit5-1 位为I2C时钟频率

​ 注意:该值在传输过程中不应该改变,但是可以在之前改变。

​ I2C_IFDR提供了一个可编程的预分频器,用于时钟配置以进行比特率选择,寄存器不会被软件重置。

寄存器IC位设置计算方法如下:

​ I2C的时钟源来源于IPG_CLK_ROOT=66Mhz

PLL2 = 528 MHz

PLL2_PFD2 = 528 MHz

IPG_CLK_ROOT = (PLL2_PFD2 / ahb_podf )/ ipg_podf = (528 MHz/4)/2 = 66Mhz

PER_CLK_ROOT = IPG_CLK_ROOT/perclk_podf = 66 MHz/1 = 66 MHz

设置I2C的波特率为100K, 因此当分频值=66000000/100000=660.

参考Table 31-3. I2C_IFDR Register Field Values 中只有640对应的0x15最接近

image-20220112152634031

即寄存器IFDR的IC位设置为0X15。

16.2.2.3 I2C Control Register (I2Cx_I2CR)控制寄存器,偏移量为8h

​ a. bit 15-8为保留位,即只读为0。

​ b. bit 7 为I2C使能位。(0 disable,1 enable)

​ c. bit 6 为I2C中断使能位。(0 disable,1 enable)

​ d. bit 5 为主/从模式选择位(0 slave mode,1 master mode )

​ e. bit 4 为传输方向模式选择位 (0 receive mode,1 transmit mode)

​ f. bit 3为应答使能位 (0 ACK , 1 NO ACK)

​ g. bit 2 重复开始信号(0 no repeat start,1 Generates repeat start)

​ h. bit 0 保留位

16.2.2.4 I2C Status Register (I2Cx_I2SR)状态寄存器,偏移量为Ch

​ a. bit 15-8为保留位即只读为0。

​ b. bit 7 数据传输状态位(0 传输中,1 传输完成)

​ c. bit 6 I2C地址是否为从标识(0 不表示,1 是从机地址)

​ d. bit 5 I2C总线忙状态标识位(0 空闲,1 忙 )

​ e. bit 4 仲裁丢失位 (0 正常,1 仲裁丢失)

​ f. bit 3 保留位

​ g. bit 2从机读写标识位 (0 slave接收,主向从写 , 1 slave发送 主向从读)

​ h. bit 1 I2C中断(0无中断等待, 1有中断等待)

​ i. bit 0 应答信号标识位(0检测到ACK, 1检测到NO ACK)

6.2.2.5 I2C Data I/O Register (I2Cx_I2DR)数据寄存器,偏移量为10h

​ a. bit 15-8为保留位即只读为0。

​ b. bit 7-1 数据字节

​ 注意:在主接收模式下,读取数据寄存器允许发生读取并初始化下一个字节被接收。在从模式下,相同功能需要编址后生效。

​ 低8位为有效数据位,发送数据时将数据写到这个寄存器中,如果要接收时直接读取该寄存器中的数据。

16.3 AP3216C操作方法

16.3.1 AP3216C简介

​ AP3216C 模块的核心就是这个芯片本身。这颗芯片集成了光强传感器(ALS: Ambient Light Sensor),接近传感器(PS: Proximity Sensor),还有一个红外LED(IR LED)。这个芯片设计的用途是给手机之类的使用,比如:返回当前环境光强以便调整屏幕亮度;用户接听电话时,将手机放置在耳边后,自动关闭屏幕避免用户误触碰。该芯片通过I2C接口作为slave与主控制器相连,支持中断。

16.3.2 AP3216C特性

​ ① 接口:I2C

​ ② 速率:FS mode 可达400kbit/s

​ ③ 模式:ALS/PS+IR/ALS+PS+IR/PD/ALS once/SW Reset/PS+IR once/ALS+PS+IR once。

​ ④ 内置温度补偿电路。

​ ⑤ 工作温度:-30℃ to +80℃

​ ⑥ ALS

​ a. (0-65536)16位有效的线性输出

​ b. 4量程动态范围可选择

​ c. 防闪烁(50/60 HZ)

​ d. 高敏感性@黑玻璃

​ e. 窗口损失补偿

​ ⑦ PS

​ a. (0-1023)10位有效线性输出

​ b. 4增益

​ c. 高环境光抑制

​ d. 串扰补偿

​ ⑧ 封装大小:4.1mm x 2.4mm x 1.35mm

16.3.3 AP3216C结构图

​ 下面是AP3216C的结构图以及典型电路应用图

image-20220112152634031

image-20220112152634031

​ 从图上看AP3216C结构简单,应用方便。本来模块我们其实也不用太深究原理,所以重点我们放在对典型I2C从设备的应用上。

16.3.4 AP3216C寄存器及使用

​ a. I2C从设备地址

​ 从设备地址有7bit ,一个读/写位应该由主设备附加到从设备地址以正确地与设备通信。

​ AP3216的地址是0X1E。

​ b. 系统寄存器表

​ 同其他所有从设备一样,AP3216C有它的内部寄存器让我们来设置和操作。表格如下所示。可以知道。次级地址0x00为系统配置寄存器,并且提供了后面7bit组合的工作模式供选择。

image-20220112152634031

​ 后面会参考这张表格对AP3216C进行编程并读取数据。

16.4 I2C控制器编程_框架

​ 之前的所有铺垫都是为了实现I2C通讯,所以怎么用代码实现也是尤为关键的一个问题。本节源码目录在裸机Git仓库 NoosProgramProject/(16_I2C编程\001_example_i2c_ap3216c_led_show\i2c.c和

裸机Git仓库 NoosProgramProject/(16_I2C编程\001_example_i2c_ap3216c_led_show\i2c.h

​ 首先,需要定义I2C寄存器的入口地址,各个寄存器绝对地址,我们代码仅使用I2C1,故仅先定义I2C1相关寄存器。

 /*
  *The I2C contains five 16-bit registers.
  */
 /*  绝对地址  寄存器名    位宽(bit)权限   复位值   章节/页
  * 21A_0000  (I2C1_IADR)  16       R/W  0000h   31.7.1/1463
  * 21A_0004  (I2C1_IFDR)  16       R/W  0000h   31.7.2/1463
  * 21A_0008  (I2C1_I2CR)  16       R/W  0000h   31.7.3/1465
  * 21A_000C  (I2C1_I2SR)  16       R/W  0081h  31.7.4/1466
  * 21A_0010  (I2C1_I2DR)  16       R/W  0000h  31.7.5/1468
  */
 #define I2C1_BASE_ADDR            (0x21A0000u)
 /* I2C1 Base address */
 #define I2C1                       ((I2C_REGISTERS *)I2C1_BASE_ADDR)

​ 有了上面的地址,定义如下寄存器结构体,这样在使用时,我们仅需要把入口地址定义成如下结构,那么就可以通用I2C1-IC25,在使用时传入相应的入口地址即可。

 /* 寄存器地址的宏结构体定义,此种方式仅定义入口地址即可 */
 /* all registers address is Base address + xh offset*/
 typedef struct tagRegisters{
   volatile uint16_t IADR;                /*I2C Address Register, offset: 0x0 */
            uint8_t ReservedIADR[2];
   volatile uint16_t IFDR;                /*I2C Frequency Divider Register, offset: 0x4 */
            uint8_t ReservedIFDR[2];
   volatile uint16_t I2CR;                /*I2C Control Register, offset: 0x8 */
            uint8_t ReservedI2CR[2];
   volatile uint16_t I2SR;                /*I2C Status Register, offset: 0xC */
            uint8_t ReservedI2SR[2];
   volatile uint16_t I2DR;                /*I2C Data I/O Register, offset: 0x10 */
 } I2C_REGISTERS;

​ 有了基本的信息,我们也要有相应的操作,主要有读和写两个操作码。

 typedef enum enI2C_OPCODE
 {
     I2C_WRITE = 0,            /* 主机向从机写数据 */
     I2C_READ  = 1,  		    /* 主机从从机读数据 */
     I2C_DONOTHING_BULL
 } I2C_OPCODE;

​ 然后需要有个分配和传输承载信息角色的载体,我们仅以做master为例,如果做通用的结构体,还可以加上角色等信息。而master的主传输结构体包括目标以下信息:

 typedef struct tagI2cTransfer
 {
     uint8_t  ucSlaveAddress;      	 /* 7位从机地址 */
     uint32_t ulOpcode  ; 		     /* 操作码*/
     uint32_t ulSubAddress;          /* 目标寄存器地址 */
     uint8_t  ulSubAddressLen;    	 /* 寄存器地址长度 */
     volatile uint32_t ulLenth;  	     /* 数据长度 */
     uint8_t *volatile pbuf;    	     /* 数据*/
} I2C_TRANSFER;

​ 有了这些寄存器的基本信息、操作方式、传输结构,下面来实现具体的函数和功能。

16.4.1 i2c_init

void i2c_init(I2C_REGISTERS *I2C_BASE);81 void i2c_init(I2C_REGISTERS *I2C_BASE);

​ 通过传入I2C1的入口地址,完成初始化。

​ 因为不使用中断服务函数模式,初始化代码非常简单,我们仅需要将I2CR(bit7)(位图如下)

image-20220112152634031

​ 置0后写入IFDR为0x15(位图如下),设置波特率为100k。(具体计算方法参考16.2 IMX6ULL的I2C控制器操作与寄存器介绍中关于分频寄存器),然后再使能I2C,将I2CR(bit7)置1 。

image-20220112152634031

代码如下:

     I2C_BASE->I2CR &= ~(1 << 7);
     I2C_BASE->IFDR = 0x15;
     I2C_BASE->I2CR |= (1<<7);

16.4.2 i2c_transfer

函数原型如下:

uint8_t i2c_transfer(I2C_REGISTERS *I2C_BASE, I2C_TRANSFER *transfer);

参数是I2C控制器入口地址以及传输结构体,作为通用公共传输函数,负责公共流程

下面是函数流程图。

image-20220112152634031

清除标识位是bit-4 IAL 仲裁位,bit-1 IIF 中断标志位都置位,位图如图所示:

image-20220112152634031

代码如下:

I2C_BASE->I2SR &= ~((1 << 1) | (1 << 4));

等待传输完成需要判断上面的ICF位:

while(!((I2C_BASE->I2SR >> 7) & 0X1)){};

16.4.3 i2c_start

​ start需要先判断I2C是否忙,然后设置发送模式,最后将slave地址通过数据寄存器发出去,函数声明如下:

uint8_t i2c_start(I2C_REGISTERS *I2C_BASE, uint8_t ucSlaveAddr, uint32_t ulOpcode);

​ 首先,判断忙是上面的IBB位

if(I2C_BASE->I2SR & (1 << 5))

​ 设置发送模式MSTA设置为1即master,MTX设置为1即发送模式,位图如下:

image-20220112152634031

I2C_BASE->I2CR |= (1 << 5) | (1 << 4);

​ 接下来将slave地址放到数据寄存器中发送,最后一位是操作码,位图如下:

63  I2C_BASE->I2DR = ((uint32_t)ucSlaveAddr << 1) | ((I2C_READ == ulOpcode)? 1 : 0);

16.4.4 i2c_check

​ check中传入的是I2SR寄存器中的值,代码如下:

uint8_t i2c_check(I2C_REGISTERS *I2C_BASE, uint32_t status);

​ 判断仲裁位即SR寄存器bit4,如果置位,需要清除仲裁位,并重启I2C。如果没有收到从机的应答信号即SR寄存器bit0是否为1,如果没有返回状态码NAK。代码如下:

uint8_t i2c_check(I2C_REGISTERS *I2C_BASE, uint32_t status);
 	/* 检查是否发生仲裁丢失错误 */
 	if(status & (1<<4))
 	{
 		I2C_BASE->I2SR &= ~(1<<4);	   /* 清除仲裁丢失错误位 */
 
 		I2C_BASE->I2CR &= ~(1 << 7);	   /* 先关闭I2C */
 		I2C_BASE->I2CR |= (1 << 7);	   /* 重新打开I2C */
 		return I2C_ARBITRATIONLOST;
 	} 
 	else if(status & (1 << 0))     	       /* 没有接收到从机的应答信号 */
 	{
 		return I2C_NAK;		           /* 返回NAK(No acknowledge) */
 	}
 	return I2C_OK;

16.4.5 i2c_write

​ 写函数完成清除标志位,设置模式为发送,把buf中的数据写入I2DR数据寄存器中,等待传输完成(判断的是I2SR bit1 IIF),然后清除该位。然后检查ACK,最后再清除标识位。代码如下:

void i2c_write(I2C_REGISTERS *I2C_BASE, const uint8_t *pbuf, uint32_t len)
 {
 	/* 等待传输完成 */
 	while(!(I2C_BASE->I2SR & (1 << 7))); 
 	
 	I2C_BASE->I2SR &= ~(1 << 1); 	        /* 清除标志位 */
 	I2C_BASE->I2CR |= 1 << 4;		       /* 发送数据 */
 	while(len--)
 	{
 		I2C_BASE->I2DR = *pbuf++; 	   /* 将buf中的数据写入到I2DR寄存器 */
 		
 		while(!(I2C_BASE->I2SR & (1 << 1))); 	            /* 等待传输完成 */	
 		I2C_BASE->I2SR &= ~(1 << 1);                         /* 清除标志位 */
 
 		/* 检查ACK */
 		if(i2c_check(I2C_BASE, I2C_BASE->I2SR))
 			break;
 	}
 	
 	I2C_BASE->I2SR &= ~(1 << 1);
 	i2c_stop(I2C_BASE); 	                                 /* 发送停止信号 */

 }

16.4.6 i2c_read

​ 读函数具体流程如下,值得注意两个点,一个是假读是为了启动用的,有兴趣的小伙伴可以移步https://community.nxp.com/thread/378405 查看官方解释。另外如果仅需要读一个字节的话,需要发送一个NACK信号。即将I2CR bit3 TXAK置1。其他寄存器和操作我们在上面已经都讲解过。代码如下:

void i2c_read(I2C_REGISTERS *I2C_BASE, uint8_t *pbuf, uint32_t len)
{
	volatile uint8_t dummy = 0;
	dummy++; 	/* 防止编译报错 */
	/* 等待传输完成 */
	while(!(I2C_BASE->I2SR & (1 << 7))); 
	
	I2C_BASE->I2SR &= ~(1 << 1); 				/* 清除中断挂起位 */
	I2C_BASE->I2CR &= ~((1 << 4) | (1 << 3));	/* 接收数据 */
	
	/* 如果只接收一个字节数据的话发送NACK信号 */
	if(len == 1)
        I2C_BASE->I2CR |= (1 << 3);
	dummy = I2C_BASE->I2DR; /* 假读 */
	while(len--)
	{
		while(!(I2C_BASE->I2SR & (1 << 1))); 	/* 等待传输完成 */	
		I2C_BASE->I2SR &= ~(1 << 1);			/* 清除标志位 */
	 	if(len == 0)
        {
        	i2c_stop(I2C_BASE); 			/* 发送停止信号 */
        }
        if(len == 1)
        {
            I2C_BASE->I2CR |= (1 << 3);
        }
		*pbuf++ = I2C_BASE->I2DR;
	}
}

16.4.7 I2C读写标准流程

下面是是I2C的读写标准流程,可能实际使用时根据需求略有异同。

写寄存器的标准流程如下图:

image-20220112152634031

① 1.Master发起START

② 2.Master发送I2C addr(7bit)和w操作0(1bit),等待ACK

③ 3.Slave发送ACK

④ 4.Master发送reg addr(8bit),等待ACK

⑤ Slave发送ACK

⑥ Master发送data(8bit),即要写入寄存器中的数据,等待ACK

⑦ Slave发送ACK

⑧ 第6步和第7步可以重复多次,即顺序写多个寄存器

⑨ Master发起STOP

读寄存器的标准流程如下图:

image-20220112152634031

① Master发送I2C addr(7bit)和w操作1(1bit),等待ACK

② Slave发送ACK

③ Master发送reg addr(8bit),等待ACK

④ Slave发送ACK

⑤ Master发起RESTART

⑥ Master发送I2C addr(7bit)和r操作1(1bit),等待ACK

⑦ Slave发送ACK

⑧ Slave发送data(8bit),即寄存器里的值

⑨ Master发送ACK

⑩ 第8步和第9步可以重复多次,即顺序读多个寄存器

⑪ Master发送NO ACK表示读取完成,从机也不用发送ACK

⑫ Master发送STOP

1.5 I2C控制器编程_中断

I2C控制器中和中断有关的寄存器如下:

**IIEN:**I2C_I2CR(bit6)(0 disable I2C interrupt ,1 enable I2C interrupt)

**IIF:**I2C_I2SR (bit1) (0 No I2C interrupt pending ,1 An interrupt is pending)

上面分别是中断使能位,中断状态位。

编码中在传送完1byte后可以通过判断IIF状态来确认ACK和传输完成。当然作为从机时,收到自己的地址后也可以进入中断。

16.6 AP3216C编程

AP3216C的编程基于I2C初始化后,write/read功能之上。

代码路径在裸机Git仓库 NoosProgramProject/(16_I2C编程\001_example_i2c_ap3216c_led_show\ap3216c.c)裸机Git仓库 NoosProgramProject/(16_I2C编程\001_example_i2c_ap3216c_led_show\ap3216c.h)

16.6.1 AP3216C初始化IO

参考100ASK_IMX6ULL原理图如下:

image-20220112152634031

AP3216C接口简单,仅将PIN2和PIN8分别连接到I2C1_SCL和I2C1_SDA。而另外的一端从下图可知,I2C1_SCL使用的UART4_RXD,I2C1_SDA使用的UART4_TXD这两个IO。

image-20220112152634031

16.6.2 初始化AP3216C

通过《1.3-1.3.4 AP3216C寄存器及使用》 的系统寄存器表格可知,0X00这个寄存器是模式控制寄存器,用来设置AP3216C的工作模式。

初始化流程如下:

① 复位(设置0X00寄存器为0X04)

② 设置工作模式(如0X03,开启ALS+PS+IR,其他模式请参考16.3中系统寄存器表格)

③ 设置中断(可选)

代码如下:

 	ret = i2c_write_one_byte(AP3216C_ADDR, AP3216C_SYSTEMCONG, 0X4);
 	mdelay(10);						/* AP33216C复位至少10ms */
 	ret = i2c_write_one_byte(AP3216C_ADDR, AP3216C_SYSTEMCONG, 0X3);

​ 判断是否初始化成功,就读一下刚写入的data是否为0X3,代码如下:

data = i2c_read_one_byte(AP3216C_ADDR, AP3216C_SYSTEMCONG);	

​ i2c_write_one_byte和i2c_read_one_byte就是刚才 i2c_transfer函数入参的填写过程,这里不多介绍。详细可以参考i2c.c中具体实现。

16.6.3 AP3216C数据读取

​ 重点介绍一下下面的读数据操作,函数原型如下:

	void ap3216c_read_data(uint16_t *ir, uint16_t *ps, uint16_t *als)

​ 入参和出参很简单,分别是这三个寄存器的值。

​ 寄存器AP3216C_IRDATALOW到AP3216C_PSDATAHIGH一共6个寄存器,将这个6个寄存器的值顺序读取,分别放到ir,ps,als变量中。

void ap3216c_read_data(uint16_t *ir, uint16_t *ps, uint16_t *als)
{
    uint8_t buf[6];
    uint8_t i;
    for(i = 0; i < 6; i++)	/* 循环读取所有传感器数据 */
    {
        buf[i] = i2c_read_one_byte(AP3216C_ADDR, AP3216C_IRDATALOW + i);	
    }
    if(buf[0] & 0X80)      /* IR_OF位为1,则数据无效 */
	   {
		   *ir = 0;			
	   }
	   else 				    /* 读取IR传感器的数据   */
    {
		   *ir = ((uint16_t)buf[1] << 2) | (buf[0] & 0X03); 	
	   }
						   /* 读取ALS传感器的数据 */  
	   *als = ((uint16_t)buf[3] << 8) | buf[2];	
	
    if(buf[4] & 0x40)	    /* IR_OF位为1,则数据无效*/
	   {
		   *ps = 0;    	
	   }										
	   else 			       /* 读取PS传感器的数据    */	
	   {
		   *ps = ((uint16_t)(buf[5] & 0X3F) << 4) | (buf[4] & 0X0F); 
    }
	   return;
}

​ 从0X0A~0X0F这6个寄存器就是数据寄存器,保存着ALS、PS和IR这三个传感器获取到的数据值。如果同时打开ALS、PS和IR的读取间隔最少要112.5ms,其他情况时间间隔,如下图所示:

image-20220112152634031

16.7 AP3216C上机实验

​ 通过下面的两个例程总结本章对于AP3216C通过I2C总线将数据发送给IMX6ULL并使用不同输出方法显示。实验程序结构如下:

image-20220112152634031

​ ① 实验中的显示或输出设备

​ ② 通信方式

​ ③ 设备

​ ④ 应用

16.7.1 AP3216C实验一

​ 代码在裸机Git仓库 NoosProgramProject/(16_I2C编程\001_example_i2c_ap3216c_led_show)

​ 通过上述实验程序结构图中1-3步骤后,应用通过如下方式获取数据,并显示结果。

 while(!ret)
     {   
         /*环境光强度(ALS)、接近距离(PS)和红外线强度(IR)*/
         ap3216c_read_data(&ir,&ps,&als);
         /*调整光强和接近距离可以控制LED亮灭*/
         if (als>100 || ps >1000)
         {
             led_ctl(1);
         }
         else
         {
             led_ctl(0);
         }
     }

16.7.2 参考章节《4-1.4编译程序》编译程序

​ 进入 裸机Git仓库 NoosProgramProject/(16_I2C编程\001_example_i2c_ap3216c_led_show) 源码目录进行编译。

16.7.3 参考章节《3-1.4映像文件烧写、运行》烧写、运行程序

​ 初始化成功后会绿灯闪烁一下后关闭,LED用来判断ALS或者PS寄存器值的是否超过阈值,如果超过绿灯亮起,否则常暗。

​ 下图常暗:

image-20220112152634031

​ 下图调整板子入光或者接近传感器后绿灯亮起:

image-20220112152634031

16.7.4 AP3216C实验二

​ 代码在裸机Git仓库 NoosProgramProject/(16_I2C编程\002_example_i2c_ap3216c_printf_show)

​ 通过上述实验程序结构图中1-3步骤后,应用通过如下方式获取数据,并显示结果。

    while(1)
     {   
         delay(100000);
         /*环境光强度(ALS)、接近距离(PS)和红外线强度(IR)*/
         ap3216c_read_data(&ir,&ps,&als);
         
         printf("ir=%d ps=%d als=%d\n\r",ir,ps,als);
     }

16.7.5 参考章节《4-1.4编译程序》编译程序

进入 **裸机Git仓库 NoosProgramProject/(16_I2C编程\002_example_i2c_ap3216c_printf_show)**源码目录进行编译。

16.7.6 参考章节《3-1.4映像文件烧写、运行》烧写、运行程序

实验结果如图所示:

image-20220112152634031

第十八章:SPI编程(未完整校对)

第十八章:SPI编程(未完整校对)

18. SPI编程

18.1 SPI接口简介

​ SPI(Serial Peripheral Interface)接口是全双工的同步串行通讯总线,支持通过多个不同的片选信号来连接多个外设。SPI接口通常由四根线组成,分别是提供时钟的SCLK,提供数据输出的MOSI,提供数据输入的MISO和提供片选信号的CS。同一时刻只能有一个SPI设备处于工作状态。为了适配不同的外设 ,SPI支持通过寄存器来配置片选信号和时钟信号的极性和相位。(imx6ull支持ecspi,即增强配置型spi,这里为了与其他兼容,统一用spi来称呼)。

18.1.1 SPI硬件连接

​ SPI支持slave和master两种模式,作为APU来说,多数情况下是作为master来使用的。在master模式下,通过不同的片选引脚ssn来连接多个不同的设备。下图为MASTER模式下的SPI单线通讯模式框图,只对设备进行写操作。

image-20220107143401855

18.1.2 SPI通讯数据格式

​ 在master模式下,ss、sclk和mosi作为信号输出接口,MISO作为信号输入接口。通过SS片选信号使能外部SPI设备,SCLK同步数据传输。MOSI和MISO信号在SCLK上升沿变化,在下降沿锁存数据。SPI的具体通讯格式如下图所示(默认高位在前,低位在后),输出数据为0xD2,输入数据为0x66。

image-20220107143430735

​ SPI支持不同的SPI时钟和CS片选相位和极性设置,通过设置POL和PHA值的不同来设置相对相位和极性。POL:表示SPICLK的初始电平,0为电平,1为高电平 ;CHA:表示相位,即第一个还是第二个时钟沿采样数据,0为第一个时钟沿,1为第二个时钟沿。具体如下表所示:

POL PHA 模式 含义
0 0 0 初始电平为低电平,在第一个时钟沿采样数据
0 1 1 初始电平为低电平,在第二个时钟沿采样数据
1 0 2 初始电平为高电平,在第一个时钟沿采样数据
1 1 3 初始电平为高电平,在第二个时钟沿采样数据

​ 实际时钟和相位关系如下图所示,我们常用的是模式0和模式3,因为它们都是在上升沿采样数据,不用去在乎时钟的初始电平是什么,只要在上升沿采集数据就行。极性选什么?格式选什么?通常去参考外接的模块的芯片手册。

image-20220107162721035

18.2 IMX6ULL的SPI控制器操作与寄存器介绍

18.2.1 SPI控制器介绍

IMX6ULL的SPI控制器ECSPI(Enhanced Configurable Serial Peripheral Interface) 为全双工同步四线串行通讯模块,有4路独立的控制器。主要特性如下:

SPI控制器内部包含位移寄存器、接收和发送FIFO、控制寄存器等相关寄存器。SPI控制器时钟来源于外部,有低频和参考时钟源两个来源。通过时钟发生器提供给状态机进行SPI控制器的状态控制,控制器内部框图如下图所示。

image-20220107163027163

18.2.2 SPI控制器初始化流程

SPI控制器在使用时,需要首先进行初始化。具体的初始化流程如下:

  1. 清除CONFREG寄存器的EN位来复位模块
  2. 使能SPI时钟,具体在CCM模块中进行设置
  3. 配置控制寄存器,并使能ECSPI_CONFREG寄存器的EN位来让模块工作
  4. 配置SPI引脚,具体在IOMUX中设置
  5. 根据外部SPI设备规格信息来配置SPI寄存器

在MASTER模式下,对SPI的操作可以参考下图所示的流程进行设置

image-20220107163102420

18.2.3 SPI控制器寄存器介绍

IMX6ULL有4路SPI控制器,掌握其中一路SPI控制器即可。其他几路SPI控制器都类似,只需要修改寄存器基址和IO复用管脚即可。这里以SPI3为例进行说明,SPI3的相关寄存器地址如下图所示,其中红框内的寄存器为我们本次实验使用到的寄存器。

image-20220107163123688

此次实验用到的寄存器具体功能如下:

18.2.3.1 ECSPIx_RXDATA

该寄存器用于保存数据传输中从外部设备接收到的外部数据,是只读寄存器。该寄存器与接收数据FIFO的顶端数据相同,只允许四个字节的读操作。当模块被禁止的时候,读取到的是0。

image-20220107163227986

18.2.3.2 ECSPIX_TXDATA

该寄存器用于保存数据传输中发送给外部设备的数据,是只写寄存器。该寄存器与发送数据FIFO有关,只允许四个字节的写操作。当模块被禁止的时候,写入的数据将被忽略。

image-20220107163245837

18.2.3.3 ECSPIX_CONREG

该寄存器用于设置SPI控制器的操作模式,包括时钟频率,相关控制方式和数据传输长度等。

image-20220107163253051

18.2.3.4 ECSPIX_CONFIGREG

该寄存器用于配置每个不同SPI片选通道的控制方式,包括SPI时钟和片选信号的模式相位和极性等。

image-20220107163353864

18.2.3.5 ECSPIX_STATREG

该寄存器只有低8位有效,用于指示SPI控制器的操作状态,包括发送和接收FIFO的状态等信息。如果模块被禁止的话,读取到的值为0x00000003。

image-20220107163402129

18.2.3.6 ECSPIX_TESTREG

该寄存器提供了一个测试方式,通过软件在SPI控制器内部将接收和发送区域连接起来。通过该寄存器,将接收和发送连接起来了,可以用于测试。不会影响到设备的输出,同时外部的输入将会被忽略。

image-20220107163431517

18.2.4 SPI控制器介绍

为了使用SPI控制器,我们需要初始化对应的IO引脚为spi3引脚。PAD的功能设置框图如下图所示,需要设置引脚工作模式、引脚上下拉和工作速度等。

image-20220107163542616

18.2.4.1 初始化SPI3引脚

初始化引脚为SPI3,涉及到的引脚模式配置的寄存器如下图所示。

image-20220107163559253

这四组引脚涉及到的具体寄存器如下

  1. IOMUXC_SW_MUX_CTL_PAD_UART2_TX_DATA

该寄存器用于设置SPI3的SS0片选引脚,由于需要设置为SPI3_SS0模式(如果要求CS片选有效和时钟有效间隔较大,可以将该引脚设置为普通GPIO来进行拉高拉低操作来代替默认片选功能,此时应将该引脚设置成GPIO模式)。

image-20220107163605899

  1. IOMUXC_SW_MUX_CTL_PAD_UART2_RX_DATA

该寄存器用于设置SPI3的SCLK引脚,需要设置为SPI3_SCLK模式。

image-20220107163611126

  1. IOMUXC_SW_MUX_CTL_PAD_UART2_CTS_B

该寄存器用于设置SPI3的MOSI引脚,需要设置为SPI3_MOSI模式。

image-20220107163616139

  1. IOMUXC_SW_MUX_CTL_PAD_UART2_RTS_B

该寄存器用于设置SPI3的MISO引脚,需要设置为SPI3_MISO模式。

image-20220107163623707

初始化引脚为SPI3之后,需要设置引脚的上下拉和工作速度等,涉及到的SW_CTL_PAD下图所示。

18.2.4.2 初始化SPI3工作速率

image-20220107163718602

这四组引脚涉及到的具体寄存器如下

  1. IOMUXC_SW_PAD_CTL_PAD_UART2_TX_DATA

该寄存器用于设置引脚UART2_TX_DATA上下拉、速度和驱动能力等,具体如下图所示。

image-20220107163724691

  1. IOMUXC_SW_PAD_CTL_PAD_UART2_RX_DATA

该寄存器用于设置引脚UART2_RX_DATA上下拉、速度等,具体如下图所示。

image-20220107163732738

  1. IOMUXC_SW_PAD_CTL_PAD_UART2_CTS_B

该寄存器用于设置引脚UART2_CTS_B上下拉、速度等,具体如下图所示。

image-20220107163737567

  1. IOMUXC_SW_PAD_CTL_PAD_UART2_RTS_B

该寄存器用于设置引脚UART2_RTS_B上下拉、速度等,具体如下图所示。

image-20220107163741931

18.3 ICM-20608-G操作方法

18.3.1 ICM-20608-G介绍

ICM-20608-G采用LGA16 封装(大小为330.75m),6轴姿态传感器,包括3轴加速度和3轴角速度,具有如下特性:

ICM-20608-G内部框图如下所示:

image-20220107163934560

18.3.2 ICM-20608-G操作方法

ICM-20608-G支持SPI和I2C去操作,这里选择SPI接口进行控制。具体连接方式如下图所示,通过SPI接口连接到imx6ull上,使用imx6ull的spi3接口去读写ICM-206068-G设备。

image-20220107163953919

18.3.3 ICM-20608-G相关寄存器

ICM-20608-G内部有多个寄存器,可以通过spi接口来选择不同寄存器地址来实现读取设备ID、温度、加速度和加速度等多个信息,也可以通过SPI接口来设置ICM-20608-G的相关工作方式。相关寄存器地址具体如下图所示,其中红框里的是此次实验涉及到的寄存器。

image-20220107164000427

相关寄存器的含义具体如下:

  1. Sample rate divider

该寄存器用于设置采样频率,该值用于产生采样频率来控制传感器数据输出和FIFO采样率。

  1. Configuration

该寄存器用于设置设备的工作模式,包括FIFO、加速度、温度和加速度输出等。

  1. Gyroscope configuration

该寄存器用于配置加速度传感器,包括量程选择、是否自测等信息

  1. Accelerometer configuration

该寄存器用于配置加速度传感器,包括量程选择、是否自测等。

  1. Accelerometer configuration2

该寄存器也同时用于配置加速度传感器,包括采样速度等

  1. Low power mode configuration

该寄存器用于设置ICM-20608-G是否处于低功耗模式以及唤醒频率

  1. Fifo enable

该寄存器用于设置数据存储FIFO是否进行使用,为1表示使用,为0表示禁止FIFO。

  1. Power management1

该寄存器用于设置ICM-20608-G时钟来源、电源模式、以及是否清楚全部寄存器到默认状态等

  1. Power management2

该寄存器用于设置是否使能相关传感器、包括加速度和角速度传感器

  1. Who am i

该寄存器存储设备ID,ICM-20608-G的默认值为0xAF。

  1. Accelerometer measurements

该寄存器存储角速度的测量结果,一共6个地址,每两个地址为一组X/Y/Z轴角速度的高低字节,具体内容如下图所示。

  1. Temperature measurements

该寄存器用于存储温度测量结果,由高低两个字节构成。

  1. Gyroscope measurement

该寄存器存储加速度的测量结果,一共6个地址,每两个地址为一组X/Y/Z轴加速度的高低字节,具体内容如下图所示。

18.4 SPI控制器编程

板卡通过spi3接口连接到ICM-20608-G,因此这里针对spi3进行相关编程说明,其他的spi接口可通过修改寄存器地址和初始化相关引脚来实现。

在这里,我们首先进行SPI控制器的编程。要想实现SP控制器的顺利使用,我们首先需要将对应的SPI控制器引脚初始化为SPI功能引脚,然后设置SPI控制器。SPI控制器的设置包括时钟的配置、传输数据长度、工作模式和电平极性等。SPI控制的具体部分的设置可以参考以下几个部分。

18.4.1 SPI控制器引脚设置

我们使用的是SPI3控制器,连接到ICM-20608-G的引脚如下图红框处所示。

image-20220107164100677

18.4.1.1 设置引脚的工作模式

我们首先设置引脚的工作模式,然后设置引脚上下拉和驱动能力等

  1. 设置SPI3的SS0片选引脚,针对ICM-20608-G,我们将该引脚设置成GPIO,通过拉高拉低来进行片选操作。因此需要将IOMUX_SW_MUX_CTL_PAD_UART2_TX_DATA设置为ALT5模式,作为GPIO进行使用。SPI3的SS0引脚对应的GPIO为GPIO1_20,具体地址如下所示。

image-20220107164214439

  1. 设置SPI3的SCLK引脚,需要将IOMUXC_SW_MUX_CTL_PAD_UART2_RX_DATA设置为ALT8模式。

  2. 设置SPI3的MOSI引脚,需要将IOMUXC_SW_MUX_CTL_PAD_UART2_CTS_B设置为ALT8模式。

  3. 设置SPI3的MISO引脚,需要将IOMUXC_SW_MUX_CTL_PAD_UART2_RTS_B设置为ALT8模式。

接下来通过寄存器SW_PAD_CTL_PAD来设置引脚上下拉和驱动能力等,我们使用默认值,不进行处理。

18.4.1.2 引脚初始化代码

SPI3引脚初始化代码如下所示,将CS片选引脚设置成ALT5模式,其他设置成ALT8模式。

这部分的代码在**裸机Git仓库 NoosProgramProject/18_SPI编程(001_spi_init/spi.c)**中第87行~第96行。

        //引脚初始化

        iomuxc_sw_set(UART2_TX_BASE,5);//设置为GPIO作为片选来进行使用。GPIO1_IO20

        GPIO1_GDIR = (volatile unsigned int *)(0x209C000 + 0x4);

        GPIO1_DR = (volatile unsigned int *)(0x209C000);

        *GPIO1_GDIR |= (1<<20);//设置为输出

        *GPIO1_DR |= (1<<20);    

        iomuxc_sw_set(UART2_RX_BASE,8);

        iomuxc_sw_set(UART2_RTS_BASE,8);

        iomuxc_sw_set(UART2_CTS_BASE,8);

18.4.2 SPI控制器时钟设置

我们需要设置SPI控制器时钟来源并进行使能,然后通过CONREG寄存器设置实际提供给SPI控制器的时钟频率。

SPI控制器的时钟来源如下图所示,起始于PLL3,经过分频之后提供给SPI控制器进行使用。PLL3频率为480MHz,经过8分频之后提供60MHz的时钟给SPI控制器进行使用。

image-20220107164924325

18.4.2.1 设置SPI时钟
  1. 选择SPI控制器的时钟来源并进行使能

具体对应的寄存器为CCM_CSCDR2和CCM_CCGR1。CCM_CSCDR2涉及到SPI控制器的相关设置如下图所示,我们选择默认值。时钟来源于PLL3,分频倍数为1,因此提供给SPI控制器的时钟即为60MHz。

image-20220107164933264

  1. 使能SPI控制器时钟

在选择时钟来源之后,我们需要使能SPI控制器时钟,对应的寄存器CCM_CCGR1相关设置位如下图红框所示。

image-20220107164941528

image-20220107164946111

CG的设置参考值如下图所示,我们将bit5:4的值设置为2’b11,在所有模式下SPI控制器时钟都进行使能(除STOP模式之外)。因为所有的外设时钟都在文件《imximage.cfg.cfgtmp》进行了初始化,所有我们不需要进行设置,。

image-20220107164954834

  1. 设置SPI工作时钟

为了与外部设备时钟相匹配,我们将实际的SPI时钟SCLK设置为4MHz。涉及到的寄存器为步骤1的CONREG寄存器Bit15:12和Bit11:8,计算公式如下所示:

image-20220107165210889

根据上述公式,为了将SPI控制器频率设置为4MHz,我们需要将bit15:11设置为14即可,计算结果如下

image-20220107165229108

因此,我们需要在原先CONREG寄存器值的基础上,设置bit115:11为14,即将CONREG寄存器值或上14<<11。

18.4.2.2 SPI时钟设置相关代码

SPI时钟设置相关代码如下所示,这部分的代码在**裸机Git仓库 NoosProgramProject/18_SPI编程(001_spi_init/spi.c)**第71行~第85行。

  /*设置时钟相关的*/

       /* 

        从RM手册chapter18中,我们得知时钟来源为PLL3

        1、pll3_sw_clk_sel为0,则选择pll3;为1则选择ccm_pll3_bys,时钟  默认选择pll3 。输出pll3_sw_clk给spi进行使用 输出给spi的时钟为480M/8=60Mhz

75      2、我们需要使能spi的时钟进行使用,通过CCM_CCGR1的bit5:2来进行设置 这部分在制作.imx文件的时候初始化,可以不处理

        3、计算时钟频率 CONREG寄存器

           bit15:12 div_1 

           bit11:8      div_2

        最终提供给spip的时钟为

        60M/(div+1)*(2^div_2))

        假设我们要使用的时钟是4M

        则我们设置bit15:12 = 15即可 60M/4 = 15Mhz 

        */

        uc_num->CONREG &= ~(0xf<<12|0xf<<8);//清除原先的时钟频率设置

        uc_num->CONREG |= (14<<12); //设置clk = 60/(14+1) = 4M

18.4.3 SPI控制器控制和配置设置

在这里,我们将SPI控制器作为master来连接外部设备,按照如下流程来对SPI控制器编程进行初始化,具体相关设置如下所示

18.4.3.1 步骤1、配置CONREG寄存器

CONREG寄存器相关设置如下图所示:

image-20220107165337713

具体值的含义如下:

根据以上描述,我们需要先将寄存器CONREG值清零,然后设置该寄存器,设置为master模式,每次传输为一个字节,使能立即传输并使能模块,具体对应的值为:

(7<<20)|(1<<4)|(1<<3)|(1<<0)
18.4.3.2 步骤2、配置CONFIGREG寄存器

CONFIGREG寄存器相关设置如下图所示:

image-20220107165549664

具体值的含义如下:

根据以上描述,我们将CONFIGREG寄存器值设置为0。

18.4.3 步骤3、配置SPI引脚

具体在IOMUX中设置(引脚已在上一小节初始化,这里不再赘述)。

SPI初始化相关代码如下所示,,这部分的代码在**裸机Git仓库 NoosProgramProject/18_SPI编程(001_spi_init/spi.c)**第40行~第70行。

/*

           1、清除CONREG寄存器的EN位 来复位模块

           2、在ccm中使能spi时钟

           3、配置control register,然后设置CONREG的EN位来使spi模块退出复位

           4、配置spi对应的IOMUX引脚

           5、根据外部spi设备规格来合适的配置spi寄存器

           

        */

           printf("spi 初始化开始\n\r") ; 

 

       /**/

        uc_num->CONREG = 0;// clear all bits

        /*

           bit0:使能SPI

           bit3:写入TXDATA之后,立即发送

           bit4:设置通道0为master mode

           bit31:20 设置burst length ,7表示为8bits,一个字节

        */

        uc_num->CONREG |= (7<<20)|(1<<4)|(1<<3)|(1<<0);

        /*   CONFIGREG采用默认设置

           *

           *bit0               PHA=0

           *bit7:4      sclk高电平有效

           *bit11:8     通道片选信号,当SMC =1的时候,无效(当前处于SMC=1模式)

           *bit15:12    POL=0

           *bit19:16    数据线空闲为高电平

           *bit23:20    sclk空闲为低电平

          *bit28:24    设置消息长度 ,该产品不进行使用

           *

        */

       uc_num->CONFIGREG = 0;//   

 

18.4.4 SPI控制器TESTREG寄存器设置

TESTREG寄存器存储提供了在控制器内部将接收和发送连接起来的方式,可以用于查看接收和发送FIFO的内容。

image-20220107165850731

TESTREG寄存器具体设置如下图所示,其中最高位为1表示进入测试模式,即接收和发送区域内部连接成回环模式。Bit14:8表示接收FIFO的深度,Bit6:0表示发送FIFO的深度。

image-20220107165854896

可以通过将bit32设置为1,然后进行数据发送并统计发送的数量,之后读取接收fifo的内容和数量进行对比,从而测试SPI控制器是否正常。

18.4.5 SPI控制器TXDATA和RXDATA寄存器

这两个寄存器分别用于发送和接收数据,可以通过将值写入TXDATA来发送,通过读取RXDATA来获取设备值。

发送和接收的代码如下所示,这部分的代码在程序文件《spi.c》中spi_writeread函数。

while(!(spi_num->STATREG&(1<<0)));//如果FIFO时空的话,则填充数据以开始下一次发送

 spi_num->TXDATA = uc_txdata;

 

 while(!(spi_num->STATREG&(1<<3)));//等待接收数据完成,当为1的时候表示有接收数据存在,可以进行读取

return spi_num->RXDATA;

18.5 ICM-20608-G编程

通过SPI接口连接ICM-20608-G,ICM-20608-G的操作要求如下:

在SPI控制器初始化完成之后,我们可以对ICM-20608-G进行使用设置。ICM-20608-G内部包含有多个寄存器,我们可以通过SPI对不同的地址进行操作读写操作来实现。

ICM-20608-G的SPI传输的地址和数据格式如下图所示,可以看到地址最高位MSB表示读写操作,为1表示独操作,为0表示写操作。

image-20220107165933856

为了使用ICM-20608-G,我们需要进行初始化。首先需要使传感器退出复位模式(设备在刚上电的时候,处于复位模式)。之后,我们需要设置传感器的采样率、加速度和角速度传感器的量程、加速度和角速度的相关滤波器以及是否使用低功耗模式。当我们设置完成之后,我们使能传感器。之后,就可以读取相关传感器数据了。具体的ICM-20608-G操作过程如以下几部分所述。

18.5.1 ICM-20608-G配置初始化

18.5.1.1采样率设置寄存器

该寄存器地址为0x19,用于设置采样率。AMPLE_RATE = INTERNAL_SAMPLE_RATE / (1 + SMPLRT_DIV) Where INTERNAL_SAMPLE_RATE = 1kHz。具体如下图所示,我们选择采用了为1K,因此该寄存器值设定为0.

image-20220107170304335

18.5.1.2 配置寄存器

该寄存器地址为0x1A,可以设置FIFO的模式、外部FSYNC引脚功能以及DLPF配置,具体如下图所示。我们将低通滤波器设定为250Hz,其余采用默认,因此该寄存器值为0x00。

image-20220107170311178

低通滤波器DLPF_CFG的设定值范围如下所示,我们选择20Hz。

image-20220107170316913

18.5.1.3 角速度传感器设置

该寄存器地址为0x1B,可以设置角速度传感器自测和量程范围以及低通滤波器的范围,具体如下图所示。我们选择量程范围为±250dps,其余使用默认值,因此该寄存器值设定为0x00。

image-20220107170324625

18.5.1.4 加速度传感器设置

该寄存器地址为0x1C,可以设置加速度传感器自测和量程范围以及低通滤波器的范围,具体如下图所示。我们选择量程范围为±2g,其余使用默认值,因此该寄存器值设定为0x00。

image-20220107170329812

18.5.1.5 加速度传感器设置2

该寄存器地址为0x1D,可以设置加速度传感器采样和低通滤波器的范围,具体如下图所示。我们选择低通滤波器为218.1Hz,其余使用默认值,因此该寄存器值设定为0x00。

image-20220107170335479

18.5.1.6 低功耗模式设置

该寄存器地址为0x1E,可以设置低功耗模式和低功耗模式下唤醒频率等,具体如下图所示。我们不需要低功耗模模式,因此将该寄存器设置为0,关闭低功耗模式。

image-20220107170340614

18.5.1.7 FIFO使能设置,

该寄存器地址为0x23,用于使能传感器的FIFO,具体如下图所示。由于我们不需要FIFO,因此我们将该寄存器值设定为0,关闭所有FIFO。

image-20220107170346118

18.5.1.8 电源管理设置1

该寄存器地址为0x6B,可以控制设备复位、传感器休眠等,具体如下图所示。在最开始的时候,为了安全,在初始化ICM-20608-G的时候,我们首先将该寄存器设置为0x80来复位整个芯片(在复位完成之后,该值自动清零)。等复位完成之后 ,我们将该值设置为0x01,表示禁止传感器休眠,使能温度传感器并自动选择合适的时钟源。

image-20220107170351662

18.5.1.9 电源管理设置2

该寄存器地址为0x6C,可以设置传感器使能等,具体如下图所示。我们使用所有的传感器,因此将该值设置为0x00。

image-20220107170357124

ICM20608-G相关初始化代码如下所示,这部分的代码在**裸机Git仓库 NoosProgramProject/18_SPI编程(003_spi_read_icm20608_id)裸机Git仓库 NoosProgramProject/18_SPI编/004_read_sensor_data)**程序文件《icm20608g.c》的icm20608g_init函数中。

icm20608_write_addr(ICM20608G_PWR_MGMT_1,0x80);//设备复位

       icm20608g_write_addr(ICM20608G_PWR_MGMT_1,0x01);//设备退出复位

       icm20608g_write_addr(ICM20608G_SMPLRT_DIV,0x00);//采样率默认1K

       icm20608g_write_addr(ICM20608G_CONFIG, 0x00);//禁止FIFO

       icm20608g_write_addr(ICM20608G_GYRO_CONFIG,0x00);//使用默认量程和低通滤波器

       icm20608g_write_addr(ICM20608G_ACC_CONFIG,0x00);//使用默认量程

       icm20608g_write_addr(ICM20608G_ACC_CONFIG2,0x00);//使用默认低通滤波器

       icm20608g_write_addr(ICM20608G_LP_MODE_CFG,0x00);//关闭低功耗模式

       icm20608g_write_addr(ICM20608G_FIFO_EN,0x00);//禁止传感器FIFO

       icm20608g_write_addr(ICM20608G_PWR_MGMT_2,0x00);//使能传感器

18.5.2读取ICM-20608-G的设备ID

设备ID,该寄存器地址为0x75,可以读取WHO_AM_I信息。WHO_AM_I是一个8位设备ID,该值默认为0xAF,具体如下图所示。

image-20220107170437872

读取设备ID相关的代码如下所示,这部分的代码在**裸机Git仓库 NoosProgramProject/18_SPI编程/003_spi_read_icm20608_id)裸机Git仓库 NoosProgramProject/18_SPI编程/004_read_sensor_data)**程序文件《icm20608g.c》的icm20608g_init函数第62行~第69行。

  //读取设备id并对比,如果不等于0xaf,则退出初始化

        uc_dev_id = icm20608g_read_addr(ICM20608G_WHO_AM_I);

        printf("read icm20608g id is 0x%x\n\r",uc_dev_id);

        if(uc_dev_id!=0xAF)

        {
           printf("read id fail\n\r");
           return -1;
        }

18.5.3 读取ICM-20608-G的温度信息

温度传感器数据,该值分为高低两个字节,分别对应寄存器地址0x41和0x42。对应计算公司为Temp_degC = (Temp_out-RoomTemp_Offset)/Temp_sensitivity)+25degC。具体如下图所示

image-20220107170506715

读取温度相关的代码如下所示,这部分的代码在**裸机Git仓库 NoosProgramProject/18_SPI编程/004_read_sensor_data/icm20608g.c)**的icm20608g_read_temp函数。

        icm20608g_read_len(0x41,uc_buf,2);
        icm20608g_get.temp_adc = (signed short)((uc_buf[0]<<8)|uc_buf[1]);
 

18.5.4 读取ICM-20608-G的加速度信息

ICM-20608-G支持加速度测量,对应地址为从寄存器地址59到64。其中59和60地址对应X轴加速度高低字节,61和62地址对应Y轴加速度高低字节,63和64地址对应Z轴高低字节。高低字节拼接之后进行处理即可。具体如下图所示

image-20220107170528695

读取加速度相关的代码如下所示,这部分的代码在**裸机Git仓库 NoosProgramProject/18_SPI编程/004_read_sensor_data/icm20608g.c)**的icm20608g_read_acc函数。

        icm20608g_read_len(0x3b,uc_buf,6);

       icm20608g_get.acc_x_adc = (signed short)((uc_buf[0]<<8)|uc_buf[1]);

        icm20608g_get.acc_y_adc = (signed short)((uc_buf[2]<<8)|uc_buf[3]);

        icm20608g_get.acc_z_adc = (signed short)((uc_buf[4]<<8)|uc_buf[5]);

18.5.5 读取ICM-20608-G的角速度信息

ICM-20608-G支持角速度测量,对应地址为从寄存器地址67到72。其中67和68地址对应X轴角速度高低字节,69和70地址对应Y轴角速度高低字节,71和72地址对应Z轴角速度高低字节。高低字节拼接之后进行处理即可。具体如下图所示

image-20220107170545413

读取角速度相关的代码如下所示,这部分的代码在**裸机Git仓库 NoosProgramProject/18_SPI编程/004_read_sensor_data/icm20608g.c)**的icm20608g_read_gyro函数。

        icm20608g_read_len(0x43,uc_buf,6);

        icm20608g_get.gyro_x_adc = (signed short)((uc_buf[0]<<8)|uc_buf[1]);

        icm20608g_get.gyro_y_adc = (signed short)((uc_buf[2]<<8)|uc_buf[3]);

        icm20608g_get.gyro_z_adc = (signed short)((uc_buf[4]<<8)|uc_buf[5]);

18.6 上机实验

18.6.1 SPI控制器初始化测试(无示波器可不进行观察试验)

执行函数spi_init(ESCPI3_BASE);即可初始化SPI控制器,通过spi_writeread(0x54)执行向外数据的输出,可以通过测试控制器的输出时钟SCLK来测试控制器是否初始化成功。

这部分的试验代码在裸机Git仓库 NoosProgramProject/18_SPI编程/001_spi_init/main.c),具体代码如下所示:

 int main()

 { 

  unsigned char uc_cnt;

  spi_init(ESCPI3_BASE);

  return 0;

 }
18.6.1.1 参考章节《4-1.4编译程序》编译程序

进入**裸机Git仓库 NoosProgramProject/18_SPI编程/001_spi_init)**源码目录进行编译。

18.6.1.2 参考章节《3-1.4映像文件烧写、运行》烧写、运行程序

实验结果如图所示:

18.6.2 SPI控制器回环测试

该实验主要用于测试SPI控制器本身功能,通过将收发连接到一块来进行测试。

首先实现该函数在**裸机Git仓库 NoosProgramProject/18_SPI编程/002_spi_loopback/spi.c)中spi_test_rw函数,然后在裸机Git仓库 NoosProgramProject/18_SPI编程/002_spi_loopback/main.c)**中进行调用.

首先设置SPI控制器进入回环模式,然后开始写入数据.每次写入之后就读出数据,循环20次。读写完成之后将写入和读出的数据进行比较,如果一致则表示回环模式测试成功。在最后成功完成测试的时候需要退出循环模式。

spi_test_rw函数代码在**裸机Git仓库 NoosProgramProject/18_SPI编程/002_spi_loopback/spi.c)**中,具体如下所示:

//设置进入loop模式,进行测试

        spi_num->TESTREG = (1<<31);

        printf("spi进入回环测试模式\n\r");     

        //造数

        for(uc_cnt=0;uc_cnt<20;uc_cnt++)

        {

           uc_buf_write[uc_cnt] = 0x20+uc_cnt;

        }

        //进行读写测试

        for(uc_cnt=0;uc_cnt<20;uc_cnt++)

        {

           printf("write_cnt %d\t",uc_cnt);   

           spi_writeread(spi_num,uc_buf_write[uc_cnt]);

           printf("write %d\t",uc_buf_write[uc_cnt]);   

           uc_buf_read[uc_cnt]=spi_writeread(ESCPI3_BASE,0xff);  

           printf("read %d\n\r",uc_buf_read[uc_cnt]);  

        }

        //进行数据对比

        for(uc_cnt=0;uc_cnt<20;uc_cnt++)

        {

           if(uc_buf_read[uc_cnt]!=uc_buf_write[uc_cnt])

           {/*表示回环测试失败,存在问题*/

               printf("!!!spi回环测试失败\n\r");            

               return -1;

          }

        }

        printf("@@@spi回环测试成功\n\r");

        printf("spi退出回环测试模式\n\r");

           //exit loopback mode

        spi_num->TESTREG = 0;

之后在裸机Git仓库NoosProgramProject/18_SPI编程/002_spi_loopback/main.c)中调用spi_test_rw实现回环测试,调用代码如下所示:

18.6.2.1 参考章节《4-1.4编译程序》编译程序

进入 **裸机Git仓库 NoosProgramProject/18_SPI编程/002_spi_loopback)**源码目录进行编译。

18.6.2. 参考章节《3-1.4映像文件烧写、运行》烧写、运行程序

测试结果记录如下,可以看到读取和写入的数据相同

image-20220107170631856

18.6.3 读取ICM-20608-G的设备ID

该实验用于测试读取与ICM-20608-G的SPI通讯接口功能正常与否。如果能够正确的读取到设备ID,则表示SPI接口通讯正常。

裸机Git仓库 NoosProgramProject/18_SPI编程/003_spi_read_icm20608_id/main.c)中调用icm20608g_init实现ICM-20608-G的初始化和ID的读取,调用代码如下所示:

 int main()

 {       

        unsigned char uc_cnt;
        icm20608g_init();//初始化传感器ICM-20608-G  
        return 0;

 }
18.6.3.1 参考章节《4-1.4编译程序》编译程序

进入**裸机Git仓库 NoosProgramProject/18_SPI编程/003_spi_read_icm20608_id/)**源码目录进行编译。

18.6.3.4 参考章节《3-1.4映像文件烧写、运行》烧写、运行程序

测试结果如下所示,可以看到读取到的ID是0xAF,与ICM-20608-G的ID一致。

image-20220107171628819

18.6.4 读取ICM-20608-G的传感器信息

通过读取寄存器的值读取传感器信息,包括温度、三轴角速度和加速度值。

裸机Git仓库 NoosProgramProject/18_SPI编程/004_read_sensor_data/main.c)中调用icm20608g_read_acc、icm20608g_read_ac和icm20608g_read_temp函数来实现传感器数值读取,调用代码如下所示:

int main()

 {       

        unsigned char uc_cnt;

        icm20608g_init();//初始化传感器ICM-20608-G  

        for(uc_cnt=0;uc_cnt<1;uc_cnt++)

       {

           icm20608g_read_acc();

           icm20608g_read_gyro();

           icm20608g_read_temp();

        }


       return 0;

 }
18.6.4.1 参考章节《4-1.4编译程序》编译程序

进入 **裸机Git仓库 NoosProgramProject/18_SPI编程/004_read_sensor_data)**源码目录进行编译。

18.6.4.2 参考章节《3-1.4映像文件烧写、运行》烧写、运行程序

测试结果记录如下

image-20220107170705702