可靠的MCU伙伴
Design for Reliability
全国服务咨询热线
0757-8185 9361

SWM32X_LVGL移植说明

SWM32SRET7_LVGL移植说明

1.SWM32SRET7硬件资源介绍

1.1 LCD控制器(LCDC)

1.1.1 概述

本系列LCDC模块操作均相同,使用前需使能LCDC模块时钟。

LCDC模块用于实现MCU与外部LCD的对接,在MCU的控制下,将需要显示的数据通过传送到外部LCD接口(支持SYNC的LCD接口)去显示。

                                      图1.1 MCU和显示模块连接示意图

1.1.2 特性

支持同步LCD接口,节约成本,与其他DBI接口(SPI,Motorola 6800Intel 8080)相比,LCDC可以连接任意无显示控制器和GRAM的低成本显示模块

接口时序可调

输出时钟可配置为空闲时关闭

灵活的色彩格式,支持RGB565 格式LCDC 输出数据宽度16bit

支持最高分辨率1024*768,实际分辨率可以配置

支持横屏和竖屏模式

内置单通道DMA,FIFO深度32*32bit。不需要额外的应用层,LCDC硬件完全能够管理数据读取、RGB输出和信号控制。

1.1.3 灵活的时序和硬件接口

由于其时序和硬件接口的灵活性,LCD-TFT控制器能够驱动多台具有不同分辨率和信号极性的屏幕。

为了驱动LCD-TFT显示器,LTDC利用简单的3.3V信号提供了多达20个信号,包括:

像素时钟LCD_CLK。

数据使能LCD_DE。

同步信号(LCD_HSYNC和LCD_VSYNC)。

像素数据RGB565。

                                               表1.1.1 LCDC接口输出信号

LCD-TFT 信号

说明

LCD_CLK

LCD_CLK用作LCD-TFT的数据有效信号。只有在LCD_CLK上升沿才会显示该数据。

LCD_HSYNC

行同步信号(LCD_HSYNC)管理水平线扫描,作为行显示选通。

LCD_VSYNC

帧同步信号(LCD_VSYNC)管理垂直扫描,作为帧更新选通。

LCD_DE

DE信号向LCD-TFT指示RGB总线中的数据是有效的,并且该数据必须被锁存才能绘制出来。

像素RGB数据

可以对LTDC界面进行配置,使之输出多种色深。它最多可以使用16条数据线RGB565)作为显示接口总线。

通常情况下,显示面板接口还包含其他信号,这些信号不属于表1.1.1中所述LCDC信号的一部分。显示模块要完全发挥作用,这些额外的信号是必需的。LCDC控制器只能驱动1.1.1中所述的信号。

不属于LCDC的信号可以使用GPIO和其他外设进行管理,这可能需要特定的电路。

显示面板通常会嵌入背光单元,背光单元需要额外的背光控制电路和GPIO。

一些显示面板需要复位信号以及串行接口(如I2C或SPI)。这些接口通常用于显示器初始化命令或触摸面板控制。

                             图1.2 典型LCDC显示帧(有效宽度=480像素)

void lcd_rgb_init(void)

{

    LCD_InitStructure LCD_initStruct;

    GPIO_Init(GPIOM, PIN20, 1, 0, 0); //复位

    GPIO_ClrBit(GPIOM, PIN20);

    swm_delay(1);

    GPIO_SetBit(GPIOM, PIN20);

    GPIO_Init(GPIOB, PIN12, 1, 0, 0); //背光控制

    GPIO_SetBit(GPIOB, PIN12);        //点亮背光

    PORT->PORTN_SEL0 = 0xAAAAAAAA; //GPION.0~15   LCD_DATA0~15

    PORT->PORTN_SEL1 = 0xAA;

    LCD_initStruct.Interface = LCD_INTERFACE_RGB;

    LCD_initStruct.HnPixel = LV_HOR_RES_MAX;

    LCD_initStruct.VnPixel = LV_VER_RES_MAX;

    LCD_initStruct.Hfp = 5;

    LCD_initStruct.Hbp = 40;

    LCD_initStruct.Vfp = 8;

    LCD_initStruct.Vbp = 8;

    LCD_initStruct.ClkDiv = LCD_CLKDIV_6;

    LCD_initStruct.ClkAlways = 0;

    LCD_initStruct.SamplEdge = LCD_SAMPLEDGE_FALL;

    LCD_initStruct.HsyncWidth = LCD_HSYNC_1DOTCLK;

    LCD_initStruct.IntEOTEn = 1;

    LCD_Init(LCD, &LCD_initStruct);

}

1.2 SDRAM控制器(SDRAMC)

1.2.1 概述

本系列所有型号SDRAMC模块操作均相同,主要功能在于完成AHB总线和外部SDRAM 之间的数据搬移,使用前需使能SDRAMC模块时钟。模块支持标准AHB总线操作,仅支持WORD级别读写。

1.2.2 特性

仅支持32 位WORD 操作

支持16bit 位宽的SDRAM

支持兼容PC133 标准的SDRAM 颗粒

内部8MB的SDRAM颗粒

                                       图2.1 一种SDRAM芯片的内部结构框图

1.2.3 SDRAM信号线

2.1中第一部分引出的是SDRAM芯片的控制引脚,其说明见表2.1。

                                              表2.1 SDRAM控制引脚说明

信号线

类型

说明

CLK

I

同步时钟信号,所有输入信号都在 CLK 为上升沿的时候被采集

CKE

I

时钟使能信号,禁止时钟信号时 SDRAM 会启动自刷新操作

CS#

I

片选信号,低电平有效

CAS#

I

列地址选通,为低电平时地址线表示的是列地址

RAS#

I

行地址选通,为低电平时地址线表示的是行地址

WE#

I

写入使能,低电平有效

DQM[0:1]

I

数据输入/输出掩码信号,表示 DQ 信号线的有效部分

BA[0:1]

I

Bank 地址输入,选择要控制的 Bank

A[0:12]

I

地址输入

DQ[0:15]

I/O

数据输入输出信号

除了时钟、地址和数据线,控制 SDRAM 还需要很多信号配合,它们具体作用在描述时序图时进行讲解。

1.2.4 控制逻辑

SDRAM 内部的“控制逻辑”指挥着整个系统的运行,外部可通过 CS、WE、CAS、RAS 以及地址线来向控制逻辑输入命令,命令经过“命令器译码器”译码,并将控制参数保存到“模式寄存器中”,控制逻辑依此运行。

SDRAM 包含有“A”以及“BA”两类地址线,A 类地址线是行(Row)与列(Column)共用的地址总线,BA 地址线是独立的用于指定 SDRAM 内部存储阵列号(Bank)。在命令模式下,A 类地址线还用于某些命令输入参数。

要了解 SDRAM 的储存单元寻址以及“A”、“BA”线的具体运用,需要先熟悉它内部存储阵列的结构,见图2.2。

                                    图2.2 SDRAM存储阵列模型

SDRAM 内部包含的存储阵列,可以把它理解成一张表格,数据就填在这张表格上。和表格查找一样,指定一个行地址和列地址,就可以精确地找到目标单元格,这是SDRAM芯片寻址的基本原理。这样的每个单元格被称为存储单元,而这样的表则被称为存储阵列(Bank),目前设计的SDRAM芯片基本上内部都包含有4个这样的Bank,寻址时指定Bank 号以及行地址,然后再指定列地址即可寻找到目标存储单元。SDRAM内部具有多个Bank时的结构见图2.3。

                                      图2.3 SDRAM内有4个Bank时的结构图

SDRAM芯片向外部提供有独立的BA类地址线用于Bank寻址,而行与列则共用A类地址线。

2.1标号4中表示的就是它内部的存储阵列结构,通讯时当RAS线为低电平,则“行地址选通器”被选通,地址线A[12:0]表示的地址会被输入到“行地址译码及锁存器”中,作为存储阵列中选定的行地址,同时地址线BA[1:0]表示的Bank也被锁存,选中了要操作的Bank 号;接着控制CAS线为低电平,“列地址选通器”被选通,地址线A[11:0]表示的地址会被锁存到“列地址译码器”中作为列地址,完成寻址过程。

若是写SDRAM内容,寻址完成后,DQ[15:0]线表示的数据经过图2.1标号5中的输入数据寄存器,然后传输到存储器阵列中,数据被保存;数据输出过程相反。

本型号的 SDRAM 存储阵列的“数据宽度”是16位(即数据线的数量),在与SDRAM进行数据通讯时,16位的数据是同步传输的,但实际应用中我们可能会以8位、16位的宽度存取数据,也就是说16位的数据线并不是所有时候都同时使用的,而且在传输低宽度数据的时候,我们不希望其它数据线表示的数据被录入。如传输8位数据的时候,我们只需DQ[7:0]表示的数据,而DQ[15:8]数据线表示的数据必须忽略,否则会修改非目标存储空间的内容。所以数据输入输出时,还会使用DQM[1:0]线来配合,每根DQM线对应8位数据,如DQM0(LDQM)”为低电平,“DQM1(HDQM)”为高电平时,数据线DQ[7:0]表示的数据有效,而DQ[15:8]表示的数据无效。

1.2.5 SDRAM的命令

控制SDRAM需要用到一系列的命令,见表2.2。各种信号线状态组合产生不同的控制命令。

                                             表2.2 SDRAM命令表

命令禁止

只要CS引脚为高电平,即表示“命令禁止”(COMMAND INHBIT),它用于禁止SDRAM执行新的命令,但它不能停止当前正在执行的命令。

空操作

“空操作”(NO OPERATION),“命令禁止”的反操作,用于选中SDRAM,以便接下来发送命令。

行有效

进行存储单元寻址时,需要先选中要访问的Bank和行,使它处于激活状态。该操作通过“行有效”(ACTIVE)命令实现,见图2.4,发送行有效命令时,RAS线为低电平,同时通过BA线以及A线发送Bank地址和行地址。

                  图2.4 行有效命令时序图

列读写

行地址通过“行有效”命令确定后,就要对列地址进行寻址了。“读命令”(READ)和“写命令”(WRITE)的时序很相似,见图2.5,通过共用的地址线A发送列地址,同时使WE引脚表示读/写方向,WE为低电平时表示写,高电平时表示读。数据读写时,使用DQM线表示有效的DQ数据线。

                                            图2.5 读取命令时序

本型号的SDRAM芯片表示列地址时仅使用A[8:0]线,而A10线用于控制是否“自动预充电”,该线为高电平时使能,低电平时关闭。

预充电

SDRAM的寻址具有独占性,所以在进行完读写操作后,如果要对同一个Bank的另一行进行寻址,就要将原来有效(ACTIVE)的行关闭,重新发送行/列地址。Bank关闭当前工作行,准备打开新行的操作就是预充电(Precharge)。

预充电可以通过独立的命令控制,也可以在每次发送读写命令的同时使用A10”线控制自动进行预充电。实际上,预充电是一种对工作行中所有存储阵列进行数据重写,并对行地址进行复位,以准备新行的工作。

独立的预充电命令时序见图2.6。该命令配合使用A10线控制,若A10为高电平时,所有Bank都预充电;A10为低电平时,使用BA线选择要预充电的Bank。

                                              图2.6 PRECHARGE命令时序

刷新

SDRAM要不断进行刷新(Refresh)才能保留住数据,因此它是DRAM最重要的操作。刷新操作与预充电中重写的操作本质是一样的。

但因为预充电是对一个或所有Bank中的工作行操作,并且不定期,而刷新则是有固定的周期,依次对所有行进行操作,以保证那些久久没被访问的存储单元数据正确。

刷新操作分为两种:“自动刷新”(Auto Refresh)与“自我刷新”(Self Refresh),发送命令后CKE时钟为有效时(低电平),使用自动刷新操作,否则使用自我刷新操作。不论是何种刷新方式,都不需要外部提供行地址信息,因为这是一个内部的自动操作。

对于“自动刷新”,SDRAM内部有一个行地址生成器(也称刷新计数器)用来自动地依次生成行地址,每收到一次命令刷新一行。在刷新过程中,所有Bank都停止工作,而每次刷新所占用的时间为N个时钟周期(视SDRAM型号而定,通常为N=9),刷新结束之后才可进入正常的工作状态,也就是说在这N个时钟期间内,所有工作指令只能等待而无法执行。一次次地按行刷新,刷新完所有行后,将再次对第一行重新进行刷新操作,这个对同一行刷新操作的时间间隔,称为SDRAM的刷新周期,通常为64ms。显然刷新会对SDRAM的性能造成影响,但这是它的DRAM的特性决定的,也是DRAM相对于SRAM取得成本优势的同时所付出的代价。

“自我刷新”则主要用于休眠模式低功耗状态下的数据保存,也就是说即使外部控制器不工作了,SDRAM都能自己确保数据正常。在发出“自我刷新”命令后,将CKE置于无效状态(低电平),就进入自我刷新模式,此时不再依靠外部时钟工作,而是根据SDRAM内部的时钟进行刷新操作。在自我刷新期间除了CKE之外的所有外部信号都是无效的,只有重新使CKE有效才能退出自我刷新模式并进入正常操作状态。

加载模式寄存器

前面提到SDRAM的控制逻辑是根据它的模式寄存器来管理整个系统的,而这个寄存器的参数就是通过“加载模式寄存器”命令(LOAD MODE REGISTER)来配置的。发送该命令时,使用地址线表示要存入模式寄存器的参数OP-Code”,各个地址线表示的参数见图2.7。

                                                       2.7 模式寄存器解析图

模式寄存器的各个参数介绍如下:

Burst Length译为突发长度,下面简称BL。突发是指在同一行中相邻的存储单元连续进行数据传输的方式,连续传输所涉及到存储单元(列)的数量就是突发长度。

上文讲到的读/写操作,都是一次对一个存储单元进行寻址,如果要连续读/写就还要对当前存储单元的下一个单元进行寻址,也就是要不断的发送列地址与读/写命令(行地址不变,所以不用再对行寻址)。虽然由于读/写延迟相同可以让数据的传输在 I/O 端是连续的,但它占用了大量的内存控制资源,在数据进行连续传输时无法输入新的命令,效率很低。

为此,人们开发了突发传输技术,只要指定起始列地址与突发长度,内存就会依次地自动对后面相应数量的存储单元进行读/写操作而不再需要控制器连续地提供列地址。这样,除了第一笔数据的传输需要若干个周期外,其后每个数据只需一个周期的即可获得。其实我们在EERPOM及FLASH读写章节讲解的按页写入就是突发写入,而它们的读取过程都是突发性质的。

非突发连续读取模式:不采用突发传输而是依次单独寻址,此时可等效于 BL=1。虽然也可以让数据连续地传输,但每次都要发送列地址与命令信息,控制资源占用极大。突发连续读取模式:只要指定起始列地址与突发长度,寻址与数据的读取自动进行,而只要控制好两段突发读取命令的间隔周期(与BL相同)即可做到连续的突发传输。而BL的数值,也是不能随便设或在数据进行传输前临时决定。在初始化SDRAM调用LOAD MODE REGISTER命令时就被固定。BL可用的选项是1、2、4、8,常见的设定是4和8。若传输时实际需要数据长度小于设定的BL值,则调用“突发停止”(BURST TERMINATE)命令结束传输。

模式寄存器中的BT位用于设置突发模式,突发模式分为顺序(Sequential)与间隔(Interleaved)两种。在顺序方式中,操作按地址的顺序连续执行,如果是间隔模式,则操作地址是跳跃的。跳跃访问的方式比较乱,不太符合思维习惯,我们一般用顺序模式。顺序访问模式时按照 “0-1-2-3-4-5-6-7”的地址序列访问。

模式寄存器中的CASLatency是指列地址选通延迟,简称CL。在发出读命令(命令同时包含列地址)后,需要等待几个时钟周期数据线DQ才会输出有效数据,这之间的时钟周期就是指CL,CL一般可以设置为2或3个时钟周期,见图2.8。

                                                       图2.8 CL=2CL=3的说明图

CL只是针对读命令时的数据延时,在写命令是不需要这个延时的,发出写命令时可同时发送要写入的数据。

OP Mode指Operating Mode,SDRAM的工作模式。当它被配置为“00”的时候表示工作在正常模式,其它值是测试模式或被保留的设定。实际使用时必须配置成正常模式。

WB 用于配置写操作的突发特性,可选择使用BL设置的突发长度或非突发模式。

1.2.6 SDRAM的初始化流程

最后我们来了解SDRAM的初始化流程。SDRAM并不是上电后立即就可以开始读写数据的,它需要按步骤进行初始化,对存储矩阵进行预充电、刷新并设置模式寄存器,见图2.9。

                                                     图2.9 SDRAM初始化流程

该流程说明如下:

(1)给SDRAM上电,并提供稳定的时钟,至少100us;

(2)发送“空操作”(NOP)命令;

(3)发送“预充电”(PRECHARGE)命令,控制所有Bank进行预充电,并等待tRP时间,tRP表示预充电与其它命令之间的延迟;

(4)发送至少2个“自动刷新”(AUTO REFRESH)命令,每个命令后需等待tRFC时间,tRFC表示自动刷新时间;

(5)发送“加载模式寄存器”(LOAD MODE REGISTER)命令,配置SDRAM的工作参数,并等待 tMRD时间,tMRD表示加载模式寄存器命令与行有行或刷新命令之间的延迟;

(6)初始化流程完毕,可以开始读写数据。

其中tRP、tRFC、tMRD等时间参数跟具体的SDRAM有关,可查阅其数据手册获知,MCU访问时配置需要这些参数。

1.2.7 SDRAM的读写流程

初始化步骤完成,开始读写数据,其时序流程见图2.10及图2.11。

                                                 图2.10 CL=2时,带AUTO PRECHARGE的读时序

                                                    图2.11 AUTO PRECHARGE命令的写时序

读时序和写时序的命令过程很类似,下面我们统一解说:

(1)发送“行有效”(ACTIVE)命令,发送命令的同时包含行地址和Bank地址,然后等待tRCD时间,tRCD表示行有效命令与读/写命令之间的延迟;

(2)发送“读/写”(READ/WRITE)命令,在发送命令的同时发送列地址,完成寻址的地址输入。对于读命令,根据模式寄存器的CL定义,延迟CL个时钟周期后,SDRAM的数据线DQ才输出有效数据,而写命令是没有CL延迟的,主机在发送写命令的同时就可以把要写入的数据用DQ输入到SDRAM中,这是读命令与写命令的时序最主要的区别。图中的读/写命令都通过地址线A10控制自动预充电,SDRAM接收到带预充电要求的读/写命令后,并不会立即预充电,而是等待tWR时间才开始,tWR表示写命令与预充电之间的延迟;

(3)执行“预充电”(auto precharge)命令后,需要等待tRP时间,tRP表示预充电与其它命令之间的延迟;

(4)图中的标号4处的tRAS,表示自刷新周期,即在前一个“行有效”与“预充电”命令之间的时间;

(5)发送第二次“行有效”(ACTIVE)命令准备读写下一个数据,在图中的标号5处的tRC,表示两个行有效命令或两个刷新命令之间的延迟。

其中tRCD、tWR、tRP、tRAS以及tRC等时间参数跟具体的SDRAM有关,可查阅其数据手册获知,MCU访问时配置需要这些参数。

void lcd_memory_init(void)

{

    SDRAM_InitStructure SDRAM_InitStruct;

    PORT->PORTP_SEL0 = 0xAAAAAAAA; //PP0-23 => ADDR0-23

    PORT->PORTP_SEL1 &= ~0x00000F0F;

    PORT->PORTP_SEL1 |= 0x00000A0A;

    PORT->PORTM_SEL0 = 0xAAAAAAAA; //PM0-15 => DATA15-0

    PORT->PORTM_INEN = 0xFFFF;

    //PM16 => OEN,PM17 => WEN,PM18 => NORFL_CSN,PM19 => SDRAM_CSN,PM20 => SRAM_CSN,PM21 => SDRAM_CKE

    PORT->PORTM_SEL1 = 0x888;

    SDRAM_InitStruct.CellSize = SDRAM_CELLSIZE_64Mb;

    SDRAM_InitStruct.CellBank = SDRAM_CELLBANK_4;

    SDRAM_InitStruct.CellWidth = SDRAM_CELLWIDTH_16;

    SDRAM_InitStruct.CASLatency = SDRAM_CASLATENCY_2;

    SDRAM_InitStruct.TimeTMRD = SDRAM_TMRD_3;

    SDRAM_InitStruct.TimeTRRD = SDRAM_TRRD_2;

    SDRAM_InitStruct.TimeTRAS = SDRAM_TRAS_6;

    SDRAM_InitStruct.TimeTRC = SDRAM_TRC_8;

    SDRAM_InitStruct.TimeTRCD = SDRAM_TRCD_3;

    SDRAM_InitStruct.TimeTRP = SDRAM_TRP_3;

    SDRAM_Init(&SDRAM_InitStruct);

}

1.3 SDIO接口(SDIO)

1.3.1 概述

本系列SDIO模块操作均相同,部分型号可能不包含该模块。使用前需使能SDIO模块时钟。SDIO模块控制器支持多媒体卡(MMC)、SD存储卡、SDIO卡等设备,可以使用软件方法或者DMA方法(SDIO模块内部DMA,与芯片DMA模块无关)进行数据传输。

SD卡(Secure Digital Memory Card)在我们生活中已经非常普遍了,控制器对SD卡进行读写通信操作一般有两种通信接口可选,一种是SPI接口,另外一种就是SDIO接口。SDIO 全称是安全数字输入/输出接口,多媒体卡(MMC)、SD卡、SD I/O卡都有SDIO接口。MMC 卡可以说是SD卡的前身,现阶段已经用得很少。SD I/O 卡本身不是用于存储的卡,它是指利用SDIO传输协议的一种外设。比如Wi-Fi Card,它主要是提供Wi-Fi功能,有些Wi-Fi 模块是使用串口或者SPI接口进行通信的,但Wi-Fi SDIO Card是使用SDIO接口进行通信的。并且一般设计SD I/O卡是可以插入到SD的插槽。CE-ATA 是专为轻薄笔记本硬盘设计的硬盘高速通讯接口。随之科技发展,SD卡容量需求越来越大,SD卡发展到现在也是有几个版本的,关于SDIO接口的设备整体概括见图1.3.1。

                        图1.3.1 SDIO接口的设备

关于SD卡和SD I/O部分内容可以在SD协会网站获取到详细的介绍,比如各种SD卡尺寸规则、读写速度标示方法、应用扩展等等信息。

本章内容针对SD卡使用讲解,对于其他类型卡的应用可以参考相关系统规范实现,所以对于控制器中针对其他类型卡的内容可能在本章中简单提及或者被忽略,本章内容不区分SDIO和SD卡这两个概念。即使目前SD协议提供的SD卡规范版本最新是6.0版本,MCU只支持SD卡规范版本2.0,即只支持标准容量SD和高容量SDHC标准卡,不支持超大容量 SDXC 标准卡,所以可以支持的最高卡容量是32GB。

1.3.2 特性

兼容SD 主机控制标准规范2.0

兼容SDIO 卡规范2.0

兼容SD 存储卡规范2.0(Draft 版本)

兼容SD 存储卡安全规范1.01

兼容MMC 规范标准3.31、4.2 和4.3

支持DMA 和非DMA 操作两种模式

支持MMC Plus 和MMC Mobile

卡检测(插入/移除)

可变时钟频率:0~52MHz

支持1 位、4 位、8 位的SD 模式

支持多媒体卡中断模式

4 位SD 模式下,传输速率高达100Mbits/S

8 位SD 模式下,传输速率高达416Mbits/S

支持读写控制,暂停/恢复操作

支持MMC4.3 卡纠错

支持CRC 循环冗余校验

1.3.3 SD卡物理结构

一张SD卡包括有存储单元、存储单元接口、电源检测、卡及接口控制器和接口驱动5个部分,见图1.3.2。存储单元是存储数据部件,存储单元通过存储单元接口与卡控制单元进行数据传输;电源检测单元保证SD卡工作在合适的电压下,如出现掉电或上状态时,它会使控制单元和存储单元接口复位;卡及接口控制单元控制SD卡的运行状态,它包括有8个寄存器;接口驱动器控制SD卡引脚的输入输出。

                图1.3.2 SD卡物理结构

SD卡总共有8个寄存器,用于设定或表示SD卡信息,参考表1.3.1。这些寄存器只能通过对应的命令访问,对SD卡进行控制操作并不是像操作控制器GPIO相关寄存器那样一次读写一个寄存器的,它是通过命令来控制,SDIO定义了64个命令,每个命令都有特殊意义,可以实现某一特定功能,SD卡接收到命令后,根据命令要求对SD卡内部寄存器进行修改,程序控制中只需要发送组合命令就可以实现SD卡的控制以及读写操作。

                                                        表1.3.1 SD卡寄存器

名称

Bit宽度

描述

CID

128

卡识别号(Card identification number):用来识别的卡的个体号码(唯一的)

RCA

16

相对地址(Relative card address):卡的本地系统地址,初始化时,动态地由卡建议,主机核准。

DSR

16

驱动级寄存器(Driver Stage Register):配置卡的输出驱动

CSD

128

卡的特定数据(Card Specific Data):卡的操作条件信息

SCR

64

SD 配置寄存器(SD Configuration Register):SD 卡特殊特性信息

OCR

32

操作条件寄存器(Operation conditions register)

SSR

512

SD 状态(SD Status):SD 卡专有特征的信息

CSR

32

卡状态(Card Status):卡状态信息

每个寄存器位的含义可以参考 SD 简易规格文件《Physical Layer Simplified Specification V2.0》第 5 章内容。

1.3.4 SDIO总线

SD卡使用9-pin接口通信,其中2根电源线、1根卡检测线、1根时钟线、1根命令线和4根数据线,具体说明如下:

DEC:卡检测线,检测是否插入SD卡;

CLK:时钟线,由SDIO主机产生,即由MCU输出;

CMD:命令控制线,SDIO主机通过该线发送命令控制SD卡,如果命令要求SD卡提供应答(响应),SD卡也是通过该线传输应答信息;

D0-3:数据线,传输读写数据;SD卡可将D0拉低表示忙状态;

VDDVSS:电源和地信号。

SDIO不管是从主机控制器向SD卡传输,还是SD卡向主机控制器传输都只CLK时钟线的上升沿为有效。SD卡操作过程会使用两种不同频率的时钟同步数据,一个是识别卡阶段时钟频率FOD,最高为400kHz,另外一个是数据传输模式下时钟频率FPP,默认最高为25MHz,如果通过相关寄存器配置使SDIO工作在高速模式,此时数据传输模式最高频率为50MHz。

SD总线通信是基于命令和数据传输的。通讯由一个起始位(“0”),由一个停止位(“1”)终止。SD通信一般是主机发送一个命令(Command),从设备在接收到命令后作出响(Response),如有需要会有数据(Data)传输参与。

SD总线的基本交互是命令与响应交互,见图1.3.3。

                                                图1.3.3 命令与响应交互

SD数据是以块(Black)形式传输的,SDHC卡数据块长度一般为512字节,数据可以从主机到卡,也可以是从卡到主机。数据块需要CRC位来保证数据传输成功。CRC位由SD卡系统硬件生成。MCU可以控制使用单线或4线传输。1.3.4为主机向SD卡写入数据块操作示意图。

                                                            图1.3.4 多块写入操作

SD数据传输支持单块和多块读写,它们分别对应不同的操作命令,多块写入还需要使用命令来停止整个写入操作。数据写入前需要检测SD卡忙状态,因为SD卡在接收到数据后编程到存储区过程需要一定操作时间。SD卡忙状态通过把D0线拉低表示。

数据块读操作与之类似,只是无需忙状态检测。

使用4数据线传输时,每次传输4bit数据,每根数据线都必须有起始位、终止位以及CRC位,CRC位每根数据线都要分别检查,并把检查结果汇总然后在数据传输完后通过D0线反馈给主机。

SD卡数据包有两种格式,一种是常规数据(8bit宽),它先发低字节再发高字节,而每个字节则是先发高位再发低位,4线传输示意如图1.3.5。

                                                            图1.3.5 8位宽数据包传输

4线同步发送,每根线发送一个字节的其中两个位,数据位在四线顺序排列发送,DAT3 数据线发较高位,DAT0数据线发较低位。

另外一种数据包发送格式是宽位数据包格式,对SD卡而言宽位数据包发送方式是针SD卡SSR(SD状态)寄存器内容发送的,SSR寄存器总共有512bit,在主机发出ACMD13命令后SD卡将SSR寄存器内容通过DAT线发送给主机。宽位数据包格式示意见图1.3.6。

                                                    图1.3.6 宽位数据包传输

SD命令由主机发出,以广播命令和寻址命令为例,广播命令是针对与SD主机总线连接的所有从设备发送的,寻址命令是指定某个地址设备进行命令传输。

SD命令格式固定为48bit,都是通过CMD线连续传输的(数据线不参与),见图1.3.7。

                                                      图1.3.7 SD命令格式

SD命令的组成如下:

起始位和终止位:命令的主体包含在起始位与终止位之间,它们都只包含一个数据位,起始位为0,终止位为 1。

q   传输标志:用于区分传输方向,该位为1时表示命令,方向为主机传输到SD卡,该位为0时表示响应,方向为SD卡传输到主机。

命令主体内容包括命令、地址信息/参数和CRC校验三个部分。

q   命令号:它固定占用6bit,所以总共有64个命令(代号:CMD0~CMD63),每个命令都有特定的用途,部分命令不适用于SD卡操作,只是专门用于MMC卡或者SD I/O卡。

q   地址/参数:每个命令有32bit地址信息/参数用于命令附加内容,例如,广播命令没有地址信息,这32bit用于指定参数,而寻址命令这32bit用于指定目标SD卡的地址。

q   CRC7校验:长度为7bit的校验位用于验证命令传输内容正确性,如果发生外部干扰导致传输数据个别位状态改变将导致校准失败,也意味着命令传输失败,SD卡不执行命令。

SD 命令有 4 种类型:

l 无响应广播命令(bc),发送到所有卡,不返回任务响应;

l 带响应广播命令(bcr),发送到所有卡,同时接收来自所有卡响应;

l 寻址命令(ac),发送到选定卡,DAT 线无数据传输;

l 寻址数据传输命令(adtc),发送到选定卡,DAT线有数据传输。

另外,SD卡主机模块系统旨在为各种应用程序类型提供一个标准接口。在此环境中,需要有特定的客户/应用程序功能。为实现这些功能,在标准中定义了两种类型的通用命令:特定应用命令(ACMD)和常规命令(GEN_CMD)。要使用SD卡制造商特定的ACMD命令如ACMD6,需要在发送该命令之前无发送CMD55命令,告知SD卡接下来的命令为特定应用命令。CMD55命令只对紧接的第一个命令有效,SD卡如果检测到CMD55之后的第一条命令为ACMD则执行其特定应用功能,如果检测发现不是ACMD命令,则执行标准命令。

SD卡系统的命令被分为多个类,每个类支持一种“卡的功能设置”。表1.3.2列举了SD卡部分命令信息,更多详细信息可以参考SD简易规格文件说明,表中填充位和保留位都必须被设置为 0。

虽然没有必须完全记住每个命令详细信息,但越熟悉命令对后面编程理解非常有帮助。

1.3.2 SD部分命令描述

命令序号

类型

参数

响应

缩写

描述

基本命令(Class 0

CMD0

bc

[31:0]填充位

-

GO_IDLE_STATE

复位所有的卡到 idle 状态。

CMD2

bcr

[31:0]填充位

R2

ALL_SEND_CID

通知所有卡通过 CMD 线返回 CID值。

CMD3

bcr

[31:0]填充位

R6

SEND_RELATIVE_ADDR

通知所有卡发布新 RCA。

CMD4

bc

[31:16]DSR[15:0]填充位

-

SET_DSR

编程所有卡的 DSR。

CMD7

ac

[31:16]RCA[15:0]填充位

R1b

SELECT/DESELECT_CARD

选择/取消选择 RCA 地址卡。

CMD8

bcr

[31:12] 保 留 位[11:8]VHS[7:0]检查模式

R7

SEND_IF_COND

发送 SD 卡接口条件,包含主机支持的电压信息,并询问卡是否支持。

CMD9

ac

[31:16]RCA[15:0]填充位

R2

SEND_CSD

选定卡通过CMD线发送CSD内容

CMD10

ac

[31:16]RCA[15:0]填充位

R2

SEND_CID

选定卡通过CMD线发送CID内容

CMD12

ac

[31:0]填充位

R1b

STOP_TRANSMISSION

强制卡停止传输

CMD13

ac

[31:16]RCA[15:0]填充位

R1

SEND_STATUS

选定卡通过CMD线发送它状态寄存

CMD15

ac

[31:16]RCA[15:0]填充位

-

GO_INACTIVE_STATE

使选定卡进入“inactive”状态

面向块的读操作(Class 2)

CMD16

ac

[31:0]块长度

R1

SET_BLOCK_LEN

对于标准SD卡,设置块命令的长度,对于SDHC卡块命令长度固定为512字节。

CMD17

adtc

[31:0]数据地址

R1

READ_SINGLE_BLOCK

对于标准卡,读取 SEL_BLOCK_LEN长度字节的块;对于 SDHC 卡,读取512 字节的块。

CMD18

adtc

[31:0]数据地址

R1

READ_MULTIPLE_BLOCK

连续从SD卡读取数据块,直到被CMD12中断。块长度同CMD17。

面向块的写操作(Class 4)

CMD24

adtc

[31:0]数据地址

R1

WRITE_BLOCK

对于标准卡,写入 SEL_BLOCK_LEN长度字节的块;对于 SDHC卡,写入512字节的块。

CMD25

adtc

[31:0]数据地址

R1

WRITE_MILTIPLE_BLOCK

连续向SD卡写入数据块,直到被CMD12中断。每块长度同CMD17。

CMD27

adtc

[31:0]填充位

R1

PROGRAM_CSD

CSD的可编程位进行编程

擦除命令(Class 5)

CMD32

ac

[31:0]数据地址

R1

ERASE_WR_BLK_START

设置擦除的起始块地址

CMD33

ac

[31:0]数据地址

R1

ERASE_WR_BLK_END

设置擦除的结束块地址

CMD38

ac

[31:0]填充位

R1b

ERASE

擦除预先选定的块

加锁命令(Class 7)

CMD42

adtc

[31:0]保留

R1

LOCK_UNLOCK

加锁/解锁SD卡

特定应用命令(Class 8)

CMD55

ac

[31:16]RCA[15:0]填充位

R1

APP_CMD

指定下个命令为特定应用命令,不是标准命令

CMD56

adtc

[31:1]填充位[0]/写

R1

GEN_CMD

通用命令,或者特定应用命令中,用于传输一个数据块,最低位为 1表示读数据,为 0 表示写数据

SD 卡特定应用命令

ACMD6

ac

[31:2] 填 充 位[1:0]总线宽度

R1

SET_BUS_WIDTH

义 数 据 总 线宽度('00'=1bit, '10'=4bit)。

ACMD13

adtc

[31:0]填充位

R1

SD_STATUS

发送 SD 状态

ACMD41

bcr

[32] 保 留 位[30]HCS(OCR[30]) [29:24]保留位[23:0]VDD电压(OCR[23:0])

R3

SD_SEND_OP_COND

主机要求卡发送它的支持信息(HCS)OCR寄存器内容。

ACMD51

adtc

[31:0]填充位

R1

SEND_SCR

读取配置寄存器 SCR

1.3.5 SDIO响应

响应由SD卡向主机发出,部分命令要求SD卡作出响应,这些响应多用于反馈SD卡的状态。SDIO总共有7个响应类型(代号:R1~R7),其中SD卡没有R4、R5类型响应。特定的命令对应有特定的响应类型,比如当主机发送CMD3命令时,可以得到响应R6。与命令一样,SD卡的响应也是通过CMD线连续传输的。根据响应内容大小可以分为短响应和长响应。短响应是48bit长度,只有R2类型是长响应,其长度为136bit。各个类型响应具体情况如表1.3.3。

除了R3类型之外,其他响应都使用CRC7校验来校验,对于R2类型是使用CID和CSD寄存器内部CRC7。

1.3.3 SD卡响应类型

R1(正常响应命令)


描述

起始位

传输位

命令号

卡状态

CRC7

终止位

Bit

47

46

[45:40]

[39:8]

[7:1]

0

位宽

1

1

6

32

7

1

0

0

x

x

x

1

备注

如果有传输到卡的数据,那么在数据线可能有busy信号

R2(CID,CSD 寄存器)

描述

起始位

传输位

保留

[127:1]

终止位

Bit

135

134

[133:128]

127

0

位宽

1

1

6

x

1

0

0

111111

CID 或者 CSD 寄存器[127:1]位的值

1

备注

CID寄存器内容为CMD2和CMD10响应,CSD寄存器内容为CMD9响应。

R3(OCR 寄存器)

描述

起始位

传输位

保留

OCR寄存器

保留

终止位

Bit

47

46

[45:40]

[39:8]

[7:1]

0

位宽

1

1

6

32

7

1

0

0

111111

x

1111111

1

备注

OCR 寄存器的值作为ACMD41的响应

R6(发布的 RCA 寄存器响应)

描述

起始位

传输位

CMD3

RCA 寄存器

卡状态

CRC7

终止位

Bit

47

46

[45:40]

[39:8]

[7:1]

0

位宽

1

1

6

16

16

7

1

0

0

000011

x

x

x

1

备注

专用于命令CMD3的响应

R7(发布的 RCA 寄存器响应)

描述

起始位

传输位

命令号

保留

接收

电压

检测

模式

CRC7

终止位

Bit

47

46

[45:40]

[39:20]

[19:16]

[15:8]

[7:1]

0

位宽

1

1

6

20

4

8

7

1

0

0

001000

00000h

x

x

x

1

备注

专用于命令CMD8的响应,返回卡支持电压范围和检测模式


1.3.6 SD卡的操作模式及切换

SD卡有多个版本,MCU目前最高支持《Physical Layer Simplified Specification V2.0》定义的SD卡,MCU对SD卡进行数据读写之前需要识别卡的种类:V1.0标准卡、V2.0标准卡、V2.0高容量卡或者不被识别卡。

SD卡系统(包括主机和SD卡)定义了两种操作模式:卡识别模式和数据传输模式。在系统复位后,主机处于卡识别模式,寻找总线上可用的SDIO设备;同时,SD卡也处于卡识别模式,直到被主机识别到,即当SD卡接收到SEND_RCA(CMD3)命令后,SD卡就会进入数据传输模式,而主机在总线上所有卡被识别后也进入数据传输模式。在每个操作模式下,SD卡都有几种状态,参考表1.3.4,通过命令控制实现卡状态的切换。

1.3.4 SD卡状态与操作模式

操作模式

SD卡状态

无效模式(Inactive)

无效状态(Inactive State)

卡识别模式(Card identification mode)

空闲状态(Idle State)

准备状态(Ready State)

识别状态(Identification State)

数据传输模式(Data transfer mode)

待机状态(Stand-by State)

传输状态(Transfer State)

发送数据状态(Sending-data State)

接收数据状态(Receive-data State)

编程状态(Programming State)

断开连接状态(Disconnect State)

在卡识别模式下,主机会复位所有处于“卡识别模式”的SD卡,确认其工作电压范围,识别SD卡类型,并且获取SD卡的相对地址(卡相对地址较短,便于寻址)。在卡识别过程中,要求SD卡工作在识别时钟频率FOD的状态下。卡识别模式下SD卡状态转换如1.3.8。

1.3.8 卡识别模式状态转换图

主机上电后,所有卡处于空闲状态,包括当前处于无效状态的卡。主机也可以发送GO_IDLE_STATE(CMD0)让所有卡软复位从而进入空闲状态,但当前处于无效状态的卡并不会复位。

主机在开始与卡通信前,需要先确定双方在互相支持的电压范围内。SD卡有一个电压支持范围,主机当前电压必须在该范围可能才能与卡正常通信。SEND_IF_COND(CMD8)命令就是用于验证卡接口操作条件的(主要是电压支持)。卡会根据命令的参数来检测操作条件匹配性,如果卡支持主机电压就产生响应,否则不响应。而主机则根据响应内容确定卡的电压匹配性。CMD8是SD卡标准V2.0版本才有的新命令,所以如果主机有接收到响应,可以判断卡为V2.0或更高版本SD卡。

SD_SEND_OP_COND(ACMD41)命令可以识别或拒绝不匹配它的电压范围的卡。ACMD41命令的VDD电压参数用于设置主机支持电压范围,卡响应会返回卡支持的电压范围。对于对CMD8有响应的卡,把ACMD41命令的HCS位设置为1,可以测试卡的容量类型,如果卡响应的CCS位为1说明为高容量SD卡,否则为标准卡。卡在响应ACMD41之后进入准备状态,不响应ACMD41的卡为不可用卡,进入无效状态。ACMD41是应用特定命令,发送该命令之前必须先发CMD55。

ALL_SEND_CID(CMD2)用来控制所有卡返回它们的卡识别号(CID),处于准备状态的卡在发送CID之后就进入识别状态。之后主机就发送SEND_RELATIVE_ADDR(CMD3)命令,让卡自己推荐一个相对地址(RCA)并响应命令。这个RCA是16bit地址,而CID是128bit地址,使用RCA简化通信。卡在接收到CMD3并发出响应后就进入数据传输模式,并处于待机状态,主机在获取所有卡RCA之后也进入数据传输模。

只有SD卡系统处于数据传输模式下才可以进行数据读写操作。数据传输模式下可以将主机SD时钟频率设置为FPP,默认最高为25MHz,频率切换可以通过CMD4命令来实现。数据传输模式下,SD卡状态转换过程见图1.3.9。

1.3.9 数据传授模式卡状态转换

CMD7用来选定和取消指定的卡,卡在待机状态下还不能进行数据通信,因为总线上可能有多个卡都是出于待机状态,必须选择一个RCA地址目标卡使其进入传输状态才可以进行数据通信。同时通过 CMD7 命令也可以让已经被选择的目标卡返回到待机状态。

数据传输模式下的数据通信都是主机和目标卡之间通过寻址命令点对点进行的。卡处于传输状态下可以使用表1.3.2中面向块的读写以及擦除命令对卡进行数据读写、擦除。CMD12可以中断正在进行的数据通信,让卡返回到传输状态。CMD0和CMD15会中止任何数据编程操作,返回卡识别模式,这可能导致卡数据被损坏。

uint32_t sdcard_init(void)

{

    PORT_Init(PORTB, PIN1, PORTB_PIN1_SD_CLK, 0);

    PORT_Init(PORTB, PIN2, PORTB_PIN2_SD_CMD, 1);

    PORT_Init(PORTB, PIN3, PORTB_PIN3_SD_D0,   1);

    PORT_Init(PORTB, PIN4, PORTB_PIN4_SD_D1,   1);

    PORT_Init(PORTB, PIN5, PORTB_PIN5_SD_D2,   1);

    PORT_Init(PORTB, PIN6, PORTB_PIN6_SD_D3,   1);

    return SDIO_Init(30000000);

}

1.4 串行外设接口控制器(SPI)

1.4.1 概述

串行外设接口(SPI)是微控制器和外围IC(如传感器、ADCDAC、移位寄存器、SRAM等)之间使用最广泛的接口之一。SPI是一种同步、全双工、主从式接口。来自主机或从机的数据在时钟上升沿或下降沿同步。主机和从机可以同时传输数据。SPI接口可以是3线式或4线式。

不同型号SPI 数量可能不同。使用前需使能对应SPI 模块时钟。

SPI模块支持SPI模式及SSI模式。SPI模式下支持MASTER模式及SLAVE模式。具备深度为8的FIFO,速率及帧宽度可灵活配置。

1.4.2 特性

全双工串行同步收发

可编程时钟极性和相位

支持MASTER模式和SLAVE模式

MASTER模式下最高传输速度支持主时钟4分频

数据宽度支持4BIT至16BIT

具备深度为8 的接收和发送FIFO

1.4.3 SPI接口

4线SPI器件有四个信号:

l 时钟(SPI CLK, SCLK)

l 片选(CS)

l 主机输出、从机输入(MOSI)

l 主机输入、从机输出(MISO)

产生时钟信号的器件称为主机。主机和从机之间传输的数据与主机产生的时钟同步。同I2C接口相比,SPI器件支持更高的时钟频率。MASTER模式下最高传输速度支持主时钟4分频。

SPI接口只能有一个主机,但可以有一个或多个从机。图1.4.1显示了主机和从机之间的SPI连接。来自主机的片选信号用于选择从机。这通常是一个低电平有效信号,拉高时从机与SPI总线断开连接。当使用多个从机时,主机需要为每个从机提供单独的片选信号。本文中的片选信号始终是低电平有效信号。MOSI和MISO是数据线。MOSI将数据从主机发送到从机,MISO将数据从从机发送到主机。

要开始SPI通信,主机必须发送时钟信号,并通过使能CS信号选择从机。片选通常是低电平有效信号。因此,主机必须在该信号上发送逻辑0以选择从机。SPI是全双工接口,主机和从机可以分别通过MOSI和MISO线路同时发送数据。在SPI通信期间,数据的发送(串行移出到MOSI/SDO总线上)和接收(采样或读入总线(MISO/SDI)上的数据)同时进行。串行时钟沿同步数据的移位和采样。SPI接口允许用户灵活选择时钟的上升沿或下降沿来采样和/或移位数据。

1.4.1 含主机和从机的SPI配置

1.4.4 时钟极性和时钟相位

SPI中,主机可以选择时钟极性和时钟相位。在空闲状态期间,CPOL位设置时钟信号的极性。空闲状态是指传输开始时CS为高电平且在向低电平转变的期间,以及传输结束时CS为低电平且在向高电平转变的期间。CPHA位选择时钟相位。根据CPHA位的状态,使用时钟上升沿或下降沿来采样和/或移位数据。主机必须根据从机的要求选择时钟极性和时钟相位。根据CPOL和CPHA位的选择,有四种SPI模式可用。表1.4.1显示了这4种SPI模式。

1.4.1 通过CPOLCPHA选择SPI模式

SPI模式

CPOL

CPHA

空闲状态下的时钟极性

用于采样和/或移位数据的时钟响应

0

0

0

逻辑低电平

数据在上升沿采样,在下降沿移出

1

0

1

逻辑低电平

数据在下降沿采样,在上升沿移出

2

1

0

逻辑高电平

数据在下降沿采样,在上升沿移出

3

1

1

逻辑高电平

数据在上升沿采样,在下降沿移出

1.4.2至图1.4.5显示了四种SPI模式下的通信示例。在这些示例中,数据显示在MOSI和MISO线上。传输的开始和结束用绿色虚线表示,采样边沿用橙色虚线表示,移位边沿用蓝色虚线表示。请注意,这些图形仅供参考。要成功进行SPI通信,用户须参阅产品数据手册并确保满足器件的时序规格。

1.4.2 SPI模式0,CPOL=0,CPHA=0:CLK空闲状态=低电平,数据在上升沿采样

1.4.3给出了SPI模式1的时序图。在此模式下,时钟极性为0,表示时钟信号的空闲状态为低电平。此模式下的时钟相位为1,表示数据在下降沿采样(由橙色虚线显示),并且数据在时钟信号的上升沿移出(由蓝色虚线显示)。

1.4.3 SPI模式1,CPOL=0,CPHA=1:CLK空闲状态=低电平,数据在下降沿采样

1.4.4给出了SPI模式2的时序图。在此模式下,时钟极性为1,表示时钟信号的空闲状态为高电平。此模式下的时钟相位为0,表示数据在下降沿采样(由橙色虚线显示),并且数据在时钟信号的上升沿移出(由蓝色虚线显示)。

1.4.4 SPI模式2,CPOL=1,CPHA=1:CLK空闲状态=高电平,数据在下降沿采样

1.4.5给出了SPI模式3的时序图。在此模式下,时钟极性为1,表示时钟信号的空闲状态为高电平。此模式下的时钟相位为1,表示数据在上升沿采样(由橙色虚线显示),并且数据在时钟信号的下降沿移出(由蓝色虚线显示)。

1.4.5 SPI模式3,CPOL=1,CPHA=0:CLK空闲状态=高电平,数据在上升沿采样

多个从机可与单个SPI主机一起使用。从机可以采用常规模式连接,见图1.4.6。

1.4.6 多从机SPI配置

在常规模式下,主机需要为每个从机提供单独的片选信号。一旦主机使能(拉低)片选信号,MOSI/MISO线上的时钟和数据便可用于所选的从机。如果使能多个片选信号,则MISO线上的数据会被破坏,因为主机无法识别哪个从机正在传输数据。

从图1.4.6可以看出,随着从机数量的增加,来自主机的片选线的数量也增加。这会快速增加主机需要提供的输入和输出数量,并限制可以使用的从机数量。可以使用其他技术来增加常规模式下的从机数量,例如使用多路复用器产生片选信号。

static void spi_configuration(spi_user_data_t spi)

{

    SPI_InitStructure SPI_initStruct;

    GPIO_Init(GPIOP, PIN22, 1, 0, 0);

    PORT_Init(PORTP, PIN23, FUNMUX1_SPI0_SCLK, 0);

    PORT_Init(PORTP, PIN18, FUNMUX0_SPI0_MOSI, 0);

    PORT_Init(PORTP, PIN19, FUNMUX1_SPI0_MISO, 1);

    SPI_initStruct.clkDiv = SPI_CLKDIV_4;

    SPI_initStruct.FrameFormat = SPI_FORMAT_SPI;

    SPI_initStruct.SampleEdge = SPI_SECOND_EDGE;

    SPI_initStruct.IdleLevel = SPI_HIGH_LEVEL;

    SPI_initStruct.WordSize = 8;

    SPI_initStruct.Master = 1;

    SPI_initStruct.RXHFullIEn = 0;

    SPI_initStruct.TXEmptyIEn = 0;

    SPI_initStruct.TXCompleteIEn = 0;

    SPI_Init(spi->spix, &SPI_initStruct);

    SPI_Open(spi->spix);

}

1.5 I2C总线控制器(I2C)

1.5.1 概述

I2C总线在物理连接上非常简单,分别由SDA(串行数据线)和SCL(串行时钟线)及上拉电阻组成。通信原理是通过对SCL和SDA线高低电平时序的控制,来产生I2C总线协议所需要的信号进行数据的传递。在总线空闲状态时,这两根线一般被上面所接的上拉电阻拉高,保持着高电平。

I2C总线上的每一个设备都可以作为主设备或者从设备,而且每一个设备都会对应一个唯一的地址(可以从I2C器件的数据手册得知),主从设备之间就通过这个地址来确定与哪个器件进行通信,在通常的应用中,我们把CPU带I2C总线接口的模块作为主设备,把挂接在总线上的其他设备都作为从设备。

I2C总线上可挂接的设备数量受总线的最大电容400pF限制,如果所挂接的是相同型号的器件,则还受器件地址位的限制。

I2C总线上的主设备与从设备之间以字节(8位)为单位进行双向的数据传输。

不同型号 I2C 数量可能不同。使用前需使能对应 I2C 模块时钟。

1.5.2 特性

  支持最高 1MHZ 速率主机模式

  支持最高 400KHZ 速率从机模式

  支持 7 位或 10 位地址

  波特率可配置

  支持中断功能

1.5.3 I2C总线协议

I2C协议规定,总线上数据的传输必须以一个起始信号作为开始条件,以一个结束信号作为传输的停止条件。起始和结束信号总是由主设备产生。总线在空闲状态时,SCL和SDA都保持着高电平,当SCL为高电平而SDA由高到低的跳变,表示产生一个起始条件;当SCL为高而SDA由低到高的跳变,表示产生一个 停止条件。在起始条件产生后,总线处于忙状态,由本次数据传输的主从设备独占,其他I2C器件无法访问总线;而在停止条件产生后,本次数据传输的主从设备将释放总线,总线再次处于空闲状态。如图1.5.1所示:

1.5.1

在了解起始条件和停止条件后,我们再来看看在这个过程中数据的传输是如何进行的。前面我们已经提到过,数据传输以字节为单位。主设备在SCL线上产生每个时钟脉冲的过程中将在SDA线上传输一个数据位,当一个字节按数据位从高位到低位的顺序传输完后,紧接着从设备将拉低SDA线,回传给主设备一个应答位, 此时才认为一个字节真正的被传输完成。当然,并不是所有的字节传输都必须有一个应答位,比如:当从设备不能再接收主设备发送的数据时,从设备将回传一个否定应答位。数据传输的过程如图1.5.2所示:

1.5.2

在前面我们还提到过,I2C总线上的每一个设备都对应一个唯一的地址,主从设备之间的数据传输是建立在地址的基础上,也就是说,主设备在传输有效数据之前 要先指定从设备的地址,地址指定的过程和上面数据传输的过程一样,只不过大多数从设备的地址是7位的,然后协议规定再给地址添加一个最低位用来表示接下来 数据传输的方向,0表示主设备向从设备写数据,1表示主设备向从设备读数据。如图1.5.3所示:

1.5.3

1.5.4 I2C总线操作

I2C总线的操作实际就是主从设备之间的读写操作。大致可分为以下三种操作情况:第一,主设备往从设备中写数据。数据传输格式如图1.5.4所示:

1.5.4

第二,主设备从从设备中读数据。数据传输格式如图1.5.5所示:

1.5.5

第三,主设备往从设备中写数据,然后重启起始条件,紧接着从从设备中读取数据;或者是主设备从从设备中读数据,然后重启起始条件,紧接着主设备往从设备中写数据。数据传输格式如图1.5.6所示:

1.5.6

第三种操作在单个主设备系统中,重复的开启起始条件机制要比用STOP终止传输后又再次开启总线更有效率。

1.6 SysTick定时器

1.6.1 为什么要有SysTick定时器

Cortex-M处理器内集成了一个小型的名为SysTick(系统节拍)的定时器,它属于NVIC的一部分,且可以产生SysTick异常(异常类型#15)。SysTick为简单的向下计数的24位计数器,可以使用处理器时钟或外部参考时钟(通常是片上时钟源)。

在现代操作系统中,需要一个周期性的中断来定期触发OS内核,如用于任务管理和上下文切换,处理器也可以在不同时间片内处理不同任务。处理器设计还需要确保运行在非特权等级的应用任务无法禁止该定时器,否则任务可能会禁止SysTick定时器并锁定整个系统。

之所以在处理器内增加一个定时器,是为了提高软件的可移植性。由于所有的Cortex-M处理器都具有相同的SysTick定时器,为一种Cortex-M3/M4微控制器实现的OS也能适用于其他的Cortex-M3/M4微控制器。

若应用中不需要使用OSSysTick定时器可用作简单的定时器外设,用以产生周期性中断,延时或时间测量。

1.6.2 SysTick定时器操作

如表1.6.1所示,SysTick定时器中存在4个寄存器。CMSIS-Core头文件中定义了一个名为SysTick的结构体,方便对这些寄存器的访问。

1.6.1 SysTick寄存器一览

地址

CMSIS-Core符号

寄存器

0xE000E010

SysTick->CTRL

SysTick控制和状态寄存器

0xE000E014

SysTick->LOAD

SysTick重装载值寄存器

0xE000E018

SysTick->VAL

SysTick当前值寄存器

0xE000E01C

SysTick->CALIB

SysTick校准值寄存器

/** \brief   Structure type to access the System Timer (SysTick).

*/

typedef struct

{

  __IO uint32_t CTRL;   /*!< Offset: 0x000 (R/W)   SysTick Control and Status Register */

  __IO uint32_t LOAD;   /*!< Offset: 0x004 (R/W)   SysTick Reload Value Register       */

  __IO uint32_t VAL;   /*!< Offset: 0x008 (R/W)   SysTick Current Value Register      */

  __I   uint32_t CALIB; /*!< Offset: 0x00C (R/ )   SysTick Calibration Register        */

} SysTick_Type;

SysTick内部包含一个24位向下计数器,如图1.6.1所示。它会根据处理器时钟或一个参考时钟信号(在ARM Cortex-M3Cortex-M4技术参考手册中也被称作STCLK)来减小计数。参考时钟信号取决于微控制器的实际设计,有些情况下,它可能会不存在。由于要检测上升沿,参考时钟至少得比处理器时钟慢两倍。

在设置控制和状态寄存器的第0位使能该计数器后,当前值寄存器在每个处理器时钟周期或参考时钟的上升沿都会减小。若计数减至0,它会从重加载寄存器中加载数值并继续运行。

另外一个寄存器为SysTick校准值寄存器。它为软件提供了校准信息。由于CMSIS-Core提供了一个名为SystemCoreClock的软件变量(CMIS1.2及之后版本可用,CMSIS1.1或之前的版本则使用变量SystemFrequency,因此它就未使用SysTick校准值寄存器。系统初始化函数SystemInit()函数设置了该变量,而且每次系统时钟配置改变时都要对其进行更新。这种软件手段比利用SysTick校准值寄存器的硬件方式更灵活。

SysTick寄存器的细节如表2~5所示。

1.6.1 SysTick定时器简单框图

1.6.2 SYSTICK控制和状态寄存器(0xE000E010

名称

类型

复位值

描述

16

COUNTFLAG

RO

0

当SYSTICK定时器计数到0时,该位变为1,读取寄存器或清除计数器当前值时会被清零

2

CLKSOURCE

R/W

0

0=外部参考时钟(STCLK

1=使用内核时钟

1

TICKINT

R/W

0

1=SYSTICK定时器计数减至0时产生异常

0=不产生异常

0

ENABLE

R/W

0

SYSTICK定时器使能

1.6.3 SYSTICK重装载值寄存器(0xE000E014

名称

类型

复位值

描述

23:0

RELOAD

R/W

未定义

定时器计数为0时的重装载值

1.6.4 SYSTICK当前值寄存器(0xE000E018

名称

类型

复位值

描述

23:0

CURRENT

R/Wc

0

读出值为SYSTICK定时器的当前值。写入任何值都会清除寄存器,SYSTICK控制和状态寄存器中的COUNTFLAG也会清零

1.6.5 SYSTICK校准值寄存器(0xE000E01C

名称

类型

复位值

描述

31

NOREF

R

-

1=没有外部参考时钟(STCLK不可用)

0=有外部参考时钟可供使用

30

SKEW

R

-

1=校准值并非精确的10ms

0=校准值准确

23:0

TENMS

R/W

0

10毫秒校准值。芯片设计者应通过Cortex-M3的输入信号提供该数值,若读出为0,则表示校准值不可用

/* SysTick Control / Status Register Definitions */

#define SysTick_CTRL_COUNTFLAG_Pos         16                                             /*!< SysTick CTRL: COUNTFLAG Position */

#define SysTick_CTRL_COUNTFLAG_Msk         (1UL << SysTick_CTRL_COUNTFLAG_Pos)            /*!< SysTick CTRL: COUNTFLAG Mask */

#define SysTick_CTRL_CLKSOURCE_Pos          2                                             /*!< SysTick CTRL: CLKSOURCE Position */

#define SysTick_CTRL_CLKSOURCE_Msk         (1UL << SysTick_CTRL_CLKSOURCE_Pos)            /*!< SysTick CTRL: CLKSOURCE Mask */

#define SysTick_CTRL_TICKINT_Pos            1                                             /*!< SysTick CTRL: TICKINT Position */

#define SysTick_CTRL_TICKINT_Msk           (1UL << SysTick_CTRL_TICKINT_Pos)              /*!< SysTick CTRL: TICKINT Mask */

#define SysTick_CTRL_ENABLE_Pos             0                                             /*!< SysTick CTRL: ENABLE Position */

#define SysTick_CTRL_ENABLE_Msk            (1UL << SysTick_CTRL_ENABLE_Pos)               /*!< SysTick CTRL: ENABLE Mask */

/* SysTick Reload Register Definitions */

#define SysTick_LOAD_RELOAD_Pos             0                                             /*!< SysTick LOAD: RELOAD Position */

#define SysTick_LOAD_RELOAD_Msk            (0xFFFFFFUL << SysTick_LOAD_RELOAD_Pos)        /*!< SysTick LOAD: RELOAD Mask */

/* SysTick Current Register Definitions */

#define SysTick_VAL_CURRENT_Pos             0                                             /*!< SysTick VAL: CURRENT Position */

#define SysTick_VAL_CURRENT_Msk            (0xFFFFFFUL << SysTick_VAL_CURRENT_Pos)        /*!< SysTick VAL: CURRENT Mask */

/* SysTick Calibration Register Definitions */

#define SysTick_CALIB_NOREF_Pos            31                                             /*!< SysTick CALIB: NOREF Position */

#define SysTick_CALIB_NOREF_Msk            (1UL << SysTick_CALIB_NOREF_Pos)               /*!< SysTick CALIB: NOREF Mask */

#define SysTick_CALIB_SKEW_Pos             30                                             /*!< SysTick CALIB: SKEW Position */

#define SysTick_CALIB_SKEW_Msk             (1UL << SysTick_CALIB_SKEW_Pos)                /*!< SysTick CALIB: SKEW Mask */

#define SysTick_CALIB_TENMS_Pos             0                                             /*!< SysTick CALIB: TENMS Position */

#define SysTick_CALIB_TENMS_Msk            (0xFFFFFFUL << SysTick_CALIB_TENMS_Pos)        /*!< SysTick CALIB: TENMS Mask */


1.6.3 使用SysTick定时器

若只想产生周期性的SysTick中断,最简单的方法就是使用CMSIS-Core函数SysTick_Config:

/** \brief   System Tick Configuration

    The function initializes the System Timer and its interrupt, and starts the System Tick Timer.

    Counter is in free running mode to generate periodic interrupts.

    \param [in]   ticks   Number of ticks between two interrupts.

    \return          0   Function succeeded.

    \return          1   Function failed.

    \note     When the variable <b>__Vendor_SysTickConfig</b> is set to 1, then the

    function <b>SysTick_Config</b> is not included. In this case, the file <b><i>device</i>.h</b>

    must contain a vendor-specific implementation of this function.

*/

__STATIC_INLINE uint32_t SysTick_Config(uint32_t ticks)

{

  if ((ticks - 1) > SysTick_LOAD_RELOAD_Msk)   return (1);      /* Reload value impossible */

  SysTick->LOAD   = ticks - 1;                                  /* set reload register */

  NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1);   /* set Priority for Systick Interrupt */

  SysTick->VAL   = 0;                                          /* Load the SysTick Counter Value */

  SysTick->CTRL   = SysTick_CTRL_CLKSOURCE_Msk |

                   SysTick_CTRL_TICKINT_Msk   |

                   SysTick_CTRL_ENABLE_Msk;                    /* Enable SysTick IRQ and SysTick Timer */

  return (0);                                                  /* Function successful */

}

该函数将SysTick中断间隔设置为ticks,使能计数器使用处理器时钟,然后设置SysTick异常为最低优先级。

例如,若要在30MHz的时钟频率下产生1kHz的SysTick异常,则可以使用:

SysTick_Config(SystemCoreClock / 1000);

变量SystemCoreClock应该存放正确的时钟频率数值,也就是30*1000000。另外,只需使用:

SysTick_Config(30000); //30MHz / 1000 = 30000

SysTick_Handler(void)的触发频率就变成了1kHz

SysTick_Config函数的输入参数不满足24位重加载数值寄存器(大于0xFFFFFF),SysTick_Config函数返回1,否则会返回0。

许多情况下,可能会使用参考时钟或者不想使能SysTick中断,那么就不要使用SysTick_Config函数。此时需要直接操作SysTick寄存器,推荐使用下面的流程:

(1) 0写入SysTick->CTRL禁止SysTick定时器。这个操作时可选的。若重用了其他代码,则由于SysTick之前可能已经使能过了,因此本操作是推荐使用的。

(2) 将新的重加载值写入SysTick->LOAD,重加载值应该为周期数减1。

(3) 将任何数值写入SysTick当前值寄存器SysTick->VAL,该寄存器会被清零。

(4) 写入SysTick控制和状态寄存器SysTick->CTRL启动SysTick定时器。

由于SysTick定时器向下计数到0,因此,若要设置SysTick周期为1000,则应该将重加载值(SysTick->LOAD)设置为999

若要在轮询模式中使用SysTick定时器,则可以利用SysTick控制和状态寄存器(SysTick->CTRL)中的计数标志来确定定时器何时变为0。例如,可以将SysTick定时器设置为特定数值,然后等它变为0,并以此实现延时:

    SysTick->CTRL = 0;                        //禁止SysTick

    SysTick->LOAD = 0xFF;                     //计数范围255~0256个周期)

    SysTick->VAL = 0;                         //清除当前值和计数标志

    SysTick->CTRL = 5;                        //使能SysTick定时器并使用处理器时钟

    while ((SysTick->CTRL & 0x00010000) == 0) //等待计数标志置位

        ;

    SysTick->CTRL = 0; //禁止SysTick

若要将SysTick中断用作在一定时间后触发的单发操作,则可以将重加载值减小12个周期,以补偿中断等待时间。例如,要使SysTick定时器在300个时钟周期后执行:

    volatile int SysTickFired;   //全局软件标志,表示SysTickAlarm已执行

    SysTick->CTRL = 0;          //禁止SysTick

    SysTick->LOAD = (300 - 12); //设置重加载值,由于异常等待减去12

    SysTick->VAL = 0;           //清除当前值和计数标志

    SysTickFired = 0;           //将软件标志设为0

    SysTick->CTRL = 0x07;       //使能SysTick,使能SysTick异常且使用处理器时钟

    while (SysTickFired == 0);   //等待SysTick处理将软件标志置位

在单发SysTick处理中,需要禁止SysTick,以防SysTick异常再次产生。若由于所需的处理任务花费的时间太长而导致挂起状态再次置位,则可能还需要清除SysTick的挂起状态:

void SysTick_Handler(void)

{

    SysTick->CTRL = 0x00; //禁止SysTick

    SCB->ICSR |= 1 << 25; //清除SYSTICK挂起位,防止再次挂起

    SysTickFired++;       //更新软件标志,主程序据此可以知道SysTick定时任务已执行

    return;

}

若同时产生了另一个异常,则SysTick异常可能会延迟。

SysTick定时器可用于时间测量。例如,可以用下面的代码测量一个短函数的持续时间:

    unsigned int start_time, stop_time, cycle_count;

    SysTick->CTRL = 0;         //禁止SysTick

    SysTick->LOAD = 0xFFFFFF;   //将重加载值设置为最大

    SysTick->VAL = 0;          //将当前值清为0

    SysTick->CTRL = 0x05;      //使能SysTick,使用处理器时钟

    while (SysTick->VAL != 0); //等待SysTick重加载

    start_time = SysTick->VAL; //获取开始时间

    function();                //执行要测量的函数

    stop_time = SysTick->VAL;   //获取停止时间

    cycle_count = start_time - stop_time;

由于SysTick定时器向下计数,start_time的数值比stop_time要大。可能还需要再时间测量的结尾检查一下count_flag。若count_flag置位时测试的时间大于0XFFFFFF,还得使SysTick异常且在SysTick处理中计算SysTick计数器溢出的次数。时钟周期的总数还要考虑SysTick异常。

SysTick定时器还提供了一个校准值寄存器。若该信息存在,则SysTick->CALIB寄存器的最低24位标志要得到10ms SysTick间隔所需的重加载值。不过,许多微控制器中并没有这个信息,TENMS位域读出为0。CMSIS-Core方案则提供了一个标志频率信息的软件变量,这种方式更加灵活且得到了多数微控制器供应商的支持。

可以利用SysTick校准值寄存器的第31位确定参考时钟是否存在。

1.6.4 其他考虑

在使用SysTick定时器时需要考虑以下几点:

l SysTick定时器中的寄存器只能在特权状态下访问。

l 参考时钟在一些微控制器设计中可能会不存在。

l 若应用中存在嵌入式OSSysTick定时器会被OS使用,因此就不能再被应用任务使用了。

l 当处理器再调试期间暂停时,SysTick定时器会停止计数。

l 根据微控制器的实际设计,SysTick定时器可能会在某些休眠模式中停止计数。

2.文件系统移植

2.1 SFUD移植

2.1.1 SFUD概述

SFUD 是一款开源的串行 SPI Flash 通用驱动库。由于现有市面的串行 Flash 种类居多,各个 Flash 的规格及命令存在差异, SFUD 就是为了解决这些 Flash 的差异现状而设计,让我们的产品能够支持不同品牌及规格的 Flash,提高了涉及到 Flash 功能的软件的可重用性及可扩展性,同时也可以规避 Flash 缺货或停产给产品所带来的风险。

2.1.2 SFUD移植

移植文件位于/sfud/port/sfud_port.c,文件中的sfud_err sfud_spi_port_init(sfud_flash *flash) 方法是库提供的移植方法,在里面完成各个设备 SPI 读写驱动(必选)、重试次数(必选)、重试接口(可选)及 SPI 锁(可选)的配置。更加详细的移植内容,可以参考 demo 中的各个平台的移植文件。

static spi_user_data spi0 = {.spix = SPI0, .cs_gpiox = GPIOP, .cs_gpio_pin = PIN22};

static void spi_configuration(spi_user_data_t spi)

{

    SPI_InitStructure SPI_initStruct;

    GPIO_Init(GPIOP, PIN22, 1, 0, 0);

    PORT_Init(PORTP, PIN23, FUNMUX1_SPI0_SCLK, 0);

    PORT_Init(PORTP, PIN18, FUNMUX0_SPI0_MOSI, 0);

    PORT_Init(PORTP, PIN19, FUNMUX1_SPI0_MISO, 1);

    SPI_initStruct.clkDiv = SPI_CLKDIV_4;

    SPI_initStruct.FrameFormat = SPI_FORMAT_SPI;

    SPI_initStruct.SampleEdge = SPI_SECOND_EDGE;

    SPI_initStruct.IdleLevel = SPI_HIGH_LEVEL;

    SPI_initStruct.WordSize = 8;

    SPI_initStruct.Master = 1;

    SPI_initStruct.RXHFullIEn = 0;

    SPI_initStruct.TXEmptyIEn = 0;

    SPI_initStruct.TXCompleteIEn = 0;

    SPI_Init(spi->spix, &SPI_initStruct);

    SPI_Open(spi->spix);

}

static void spi_lock(const sfud_spi *spi)

{

    __disable_irq();

}

static void spi_unlock(const sfud_spi *spi)

{

    __enable_irq();

}

/**

* SPI write data then read data

*/

static sfud_err spi_write_read(const sfud_spi *spi, const uint8_t *write_buf, size_t write_size, uint8_t *read_buf,

                               size_t read_size)

{

    sfud_err result = SFUD_SUCCESS;

    uint8_t send_data, read_data;

    spi_user_data_t spi_dev = (spi_user_data_t)spi->user_data;

    if (write_size)

    {

        SFUD_ASSERT(write_buf);

    }

    if (read_size)

    {

        SFUD_ASSERT(read_buf);

    }

    GPIO_ClrBit(spi_dev->cs_gpiox, spi_dev->cs_gpio_pin);

    /* 开始读写数据 */

    for (size_t i = 0, retry_times; i < write_size + read_size; i++)

    {

        /* 先写缓冲区中的数据到 SPI 总线,数据写完后,再写 dummy(0xFF) SPI 总线 */

        if (i < write_size)

        {

            send_data = *write_buf++;

        }

        else

        {

            send_data = SFUD_DUMMY_DATA;

        }

        /* 发送数据 */

        retry_times = 1000;

        while (SPI_IsTXFull(spi_dev->spix))

        {

            SFUD_RETRY_PROCESS(NULL, retry_times, result);

        }

        if (result != SFUD_SUCCESS)

        {

            goto exit;

        }

        SPI_Write(spi_dev->spix, send_data);

        /* 接收数据 */

        retry_times = 1000;

        while (SPI_IsRXEmpty(spi_dev->spix))

        {

            SFUD_RETRY_PROCESS(NULL, retry_times, result);

        }

        if (result != SFUD_SUCCESS)

        {

            goto exit;

        }

        read_data = SPI_Read(spi_dev->spix);

        /* 写缓冲区中的数据发完后,再读取 SPI 总线中的数据到读缓冲区 */

        if (i >= write_size)

        {

            *read_buf++ = read_data;

        }

    }

exit:

    GPIO_SetBit(spi_dev->cs_gpiox, spi_dev->cs_gpio_pin);

    return result;

}

2.2 FATFS移植

2.2.1 FATFS概述

FATFS是一个通用的文件系统(FAT/exFAT)模块,用于在小型嵌入式系统中实现FAT文件系统 FatFs 组件的编写遵循ANSI C(C89),完全分离于磁盘 I/O ,因此不依赖于硬件平台。它可以嵌入到资源有限的微控制器中,如 8051, PIC, AVR, ARM, Z80, RX等等,不需要做任何修改。

2.2.2 FATFS移植

因为FATFS模块完全与磁盘I/O层分开,因此需要下面的函数来实现底层物理磁盘的读写与获取当前时间。底层磁盘I/O模块并不是FATFS的一部分,并且必须由用户提供。这些函数一般有5个,在diskio.c里面。

disk_initialize

disk_status

disk_read

disk_write

disk_ioctl

/*-------------------------------------------------------------------*/

/* 设备初始化                                                         */

/*-------------------------------------------------------------------*/

DSTATUS disk_initialize(

    BYTE pdrv /* 物理编号 */

)

{

    DSTATUS status = STA_NOINIT;

    switch (pdrv)

    {

    case DEV_MMC: /* SD CARD */

        if (sdcard_init() == SD_RES_OK)

        {

            status = RES_OK;

        }

        break;

    case DEV_FLASH: /* SPI FLASH */

        if (sfud_init() == SFUD_SUCCESS)

        {

            status = RES_OK;

        }

        status = RES_OK;

        break;

    default:

        break;

    }

    return status;

}

/*-------------------------------------------------------------------*/

/* 获取设备状态                                                       */

/*-------------------------------------------------------------------*/

DSTATUS disk_status(

    BYTE pdrv /* 物理编号 */

)

{

    DSTATUS status = STA_NOINIT;

    switch (pdrv)

    {

    case DEV_MMC: /* SD CARD */

        status &= ~STA_NOINIT;

        break;

    case DEV_FLASH: /* SPI FLASH */

        status &= ~STA_NOINIT;

        break;

    default:

        break;

    }

    return status;

}

/*-------------------------------------------------------------------*/

/* 读扇区:读取扇区内容到指定存储区                                     */

/*-------------------------------------------------------------------*/

DRESULT disk_read(

    BYTE pdrv,    /* 设备物理编号(0..) */

    BYTE *buff,   /* 数据缓存区 */

    DWORD sector, /* 扇区首地址 */

    UINT count    /* 扇区个数(1..128) */

)

{

    DRESULT status = RES_PARERR;

    switch (pdrv)

    {

    case DEV_MMC: /* SD CARD */

        if (count == 1)

        {

            SDIO_BlockRead(sector, (uint32_t *)buff);

        }

        else

        {

            SDIO_MultiBlockRead(sector, count, (uint32_t *)buff);

        }

        status = RES_OK;

        break;

    case DEV_FLASH: /* SPI FLASH */

    {

        sfud_err result = SFUD_SUCCESS;

        sfud_flash *spi_flash = sfud_get_device(SFUD_W25_DEVICE_INDEX);

        result = sfud_read(spi_flash, sector * FLASH_SECTOR_SIZE, count * FLASH_SECTOR_SIZE, buff);

        if (result == SFUD_SUCCESS)

        {

            status = RES_OK;

        }

        else

        {

            status = RES_ERROR;

        }

    }

    break;

    default:

        break;

    }

    return status;

}

DRESULT disk_write(

    BYTE pdrv,        /* 设备物理编号(0..) */

    const BYTE *buff, /* 欲写入数据的缓存区 */

    DWORD sector,     /* 扇区首地址 */

    UINT count        /* 扇区个数(1..128) */

)

{

    DRESULT status = RES_PARERR;

    if (!count)

    {

        return RES_PARERR; /* Check parameter */

    }

    switch (pdrv)

    {

    case DEV_MMC: /* SD CARD */

        if (count == 1)

        {

            SDIO_BlockWrite(sector, (uint32_t *)buff);

        }

        else

        {

            SDIO_MultiBlockWrite(sector, count, (uint32_t *)buff);

        }

        status = RES_OK;

        break;

    case DEV_FLASH: /* SPI FLASH */

    {

        sfud_err result = SFUD_SUCCESS;

        sfud_flash *spi_flash = sfud_get_device(SFUD_W25_DEVICE_INDEX);

        sfud_erase_write(spi_flash, sector * FLASH_SECTOR_SIZE, count * FLASH_SECTOR_SIZE, buff);

        if (result == SFUD_SUCCESS)

        {

            status = RES_OK;

        }

        else

        {

            status = RES_ERROR;

        }

    }

    break;

    default:

        break;

    }

    return status;

}

DRESULT disk_ioctl(

    BYTE pdrv, /* 物理编号 */

    BYTE cmd,   /* 控制指令 */

    void *buff /* 写入或者读取数据地址指针 */

)

{

    DRESULT status = RES_PARERR;

    switch (pdrv)

    {

    case DEV_MMC: /* SD CARD */

        switch (cmd)

        {

        // Get R/W sector size (WORD)

        case GET_SECTOR_SIZE:

            *(WORD *)buff = 512;

            break;

        // Get erase block size in unit of sector (DWORD)

        case GET_BLOCK_SIZE:

            *(DWORD *)buff = SD_cardInfo.CardBlockSize;

            break;

        case GET_SECTOR_COUNT:

            *(DWORD *)buff = SD_cardInfo.CardCapacity / 512;

            break;

        case CTRL_SYNC:

        default:

            break;

        }

        status = RES_OK;

        break;

    case DEV_FLASH: /* SPI FLASH */

        switch (cmd)

        {

        // Get R/W sector size (WORD)

        case GET_SECTOR_SIZE:

            *(WORD *)buff = FLASH_SECTOR_SIZE;

            break;

        // Get erase block size in unit of sector (DWORD)

        case GET_BLOCK_SIZE:

            *(DWORD *)buff = 1;

            break;

        case GET_SECTOR_COUNT:

        {

            sfud_flash *spi_flash = sfud_get_device(SFUD_W25_DEVICE_INDEX);

            *(DWORD *)buff = spi_flash->chip.capacity / FLASH_SECTOR_SIZE;

        }

        break;

        case CTRL_SYNC:

        default:

            break;

        }

        status = RES_OK;

        break;

    default:

        status = RES_PARERR;

    }

    return status;

}

3.LittlevGL移植

3.1 LittlevGL概述

LittlevGL是一个免费的开放源代码图形库,它提供创建嵌入式GUI所需的一切,它具有易于使用的图形元素,精美的视觉效果和低内存占用。强大的构建块按钮,图表,列表,滑块,图像等,带有动画,抗锯齿,不透明度,平滑滚动的高级图形,各种输入设备的触摸板,鼠标,键盘,编码器等,多显示器支持,即同时使用更多的TFT和单色显示器,支持UTF-8编码的多语言,完全可定制的图形元素。

独立于任何微控制器或显示器使用的硬件,可扩展以使用较少的内存(80kB闪存,12 kB RAM),支持操作系统,外部存储器和GPU,但不是必需的,即使使用单帧缓冲区操作,也具有高级图形效果。

C语言编写,以实现最大的兼容性(与C++兼容),模拟器可在没有嵌入式硬件的PC 上启动嵌入式GUI设计,快速GUI设计的教程,示例,主题,在线和离线文档,在 MIT 许可下免费和开源。

LittlevGL 官网:https://littlevgl.com

GitHub 地址:https://github.com/littlevgl

3.2 LittlevGL硬件要求

16、32或64位微控制器或处理器

建议时钟频率大于16MHz

闪存/ ROM:对于非常重要的组件,其大小大于64 kB(建议大于180 kB)

内存:

静态RAM使用量:大约8至16 kB,具体取决于所使用的功能和对象类

堆栈:大于2kB(建议大于4kB)

动态数据(堆):大于4 KB(如果使用多个对象,则建议大于16 kB)。LV_MEM_SIZE 在lv_conf.h中设置

显示缓冲区:大于“水平分辨率”像素(建议大于10× “水平分辨率”)

C99或更高版本的编译器

基本的C(或C ++)知识:指针,结构,回调

3.3 LittlevGL移植

3.3.1 屏幕介绍

开发板板载的是一个RGB接口的屏幕JLT4301A,我们的板子使用的是RGB565接口。屏幕分辩率480*272,显示方向为向。

触摸采用的是 GT911的电容触摸屏。

使用RGB屏,SDRAM是必须的,因为RGB屏需要使用显存,例如480*272的RGB565 屏幕,一个像素占用2字节的显存,总共需要480*272*2=261120 折合255KB的显存,内部RAM很显然是不够用的。那么RGB屏的驱动只需要使能背光、配置LCDC的外设以及显存的地址就可以了然后往屏幕填充内容就是往对应的显存发送数据就可以了。不同 RGB 屏的配置参数可能不一样,显示方向也不一样。

开发板使用的是电容触摸屏,使用I2C进行通信,利用I2C初始化触IC GT911后,我们就可以通过I2C读出触摸的绝对位置,跟屏幕是一一对应的。

3.3.2 移植流程

LittlevGL 的移植过程也非常简单,总结了以下几个步骤

1. 添加库文件到工程

2. 配置屏幕大小以及颜色深度等跟显示相关的参数

3. 分配一个显示缓冲区并实现屏幕填充的接口

4. 实现输入设备接口,读取触摸屏坐标

5. 实现文件系统接口,实现文件的读取写入

6. 提供一个滴答时钟的接口 lv_tick_inc();

7. 完成库的初始化以及接口的初始化 lv_init();lv_port_disp_init();lv_port_indev_init(); lv_port_fs_init;

8. 定期调用任务处理函数,可设置为 5-10ms lv_task_handler();

3.3.3 源码下载

LittlevGL 的源码可以在GitHub 进行下载,https://github.com/littlevgl/lvgl,可以 clone 到本地也可以直接下载压缩包。除了下载源码以外,还可以下载example和drivers。example里面包含了各种应用展示和控件使用示例,drivers 里面包含了一些液晶屏驱动接口示例。

共下载3个文件夹

打开源码文件,里面包含了接口示例文件和配置示例文件,其中src文件夹下面又进行了分类,具体请查看源码。

3.3.4 添加库文件到工程

第一步,复制库文件

复制lvgl文件夹到工程文件夹APP下面。

lvgl\lv_conf_template.h文件复制一份并改名为lv_conf.h。这个文件是lvgl的配置文件,后面再介绍如何修改。

lv_examples文件夹复制到工程文件夹下面,这里面包含了各种应用和基础示例,我们后面需要使用。

第二步,添加库文件到工程

打开MDK工程,添加源码,在lvgl\src文件夹下面有如下文件,我们在MDK里面添加全部文件即可

然后在MDK里面再新建一个lvgl_porting文件夹和一个lvgl_demo文件夹,前者用于添加接口文件,后者用于我们后面添加示例文件,将 lvgl\porting\lv_port_disp_template.clvgl\porting\lv_port_indev_template.clvgl\porting\lv_port_fs_template.c各复制一份,分别改名为lv_port_disp,clv_port_indev.c,lv_port_fs.c添加到MDK的lv_porting文件夹

下面,后面再针对开发板的硬件进行对应的修改。

3.3.5 移植文件的适配

lvgl的源码中包含头文件有完整路径和简单路径两种方式,在MDK里面我们直接使用简单的头文件包含形式。

/*********************

*      INCLUDES

*********************/

#ifdef LV_CONF_INCLUDE_SIMPLE

#include "lv_conf.h"

#else

#include "../../../lv_conf.h"

#endif

我们使用简单的头文件包含,在MDK的宏定义里面添加LV_CONF_INCLUDE_SIMPLE 定义,如果后面还有lvgl的头文件路径是MDK不能识别的,读者根据实际情况修改为编译器能识别的格式,后面不再对此修改做介绍。

修改lv_conf文件,在我们之前复制过来的配置示例文件默认是被注释掉了。我们需要打开他。只需要将开头的#if 0修改为#if 1即可,如果后续的文件中再出现这种开关,读者自行打开即可。

/**

/*

* COPY THIS FILE AS `lv_conf.h` NEXT TO the `lvgl` FOLDER

*/

#if 1 /*Set it to "1" to enable content*/

#ifndef LV_CONF_H

#define LV_CONF_H

/* clang-format off */

#include <stdint.h>

配置屏幕的大小,我们的液晶屏是480*272

/* Maximal horizontal and vertical resolution to support by the library.*/

#define LV_HOR_RES_MAX          (480)

#define LV_VER_RES_MAX          (272)

配置颜色位数,我们使用RGB565模式,16位色

/* Color depth:

* - 1:   1 byte per pixel

* - 8:   RGB233

* - 16: RGB565

* - 32: ARGB8888

*/

#define LV_COLOR_DEPTH     16

/* Swap the 2 bytes of RGB565 color.

* Useful if the display has a 8 bit interface (e.g. SPI)*/

#define LV_COLOR_16_SWAP   0

LVGL内存配置,这里区别与绘图缓冲区,这里配置的是lvgl的控件等使用的内存,需大于2KB,这里可以使用默认的32K配置,也可以将内存定义100K,从而分配更多的内存

/* 1: use custom malloc/free, 0: use the built-in `lv_mem_alloc` and `lv_mem_free` */

#define LV_MEM_CUSTOM      0

#if LV_MEM_CUSTOM == 0

/* Size of the memory used by `lv_mem_alloc` in bytes (>= 2kB)*/

#   define LV_MEM_SIZE    (100 * 1024U)

GPU配置,这里关闭,我们使用LCD中断进行buffer的填充

/* 1: Enable GPU interface*/

#define LV_USE_GPU              0

LOG接口配置,这里暂未使用,读者在使用是可以将其配置为串口等

/*1: Enable the log module*/

#define LV_USE_LOG      0

#if LV_USE_LOG

/* How important log should be added:

* LV_LOG_LEVEL_TRACE       A lot of logs to give detailed information

* LV_LOG_LEVEL_INFO        Log important events

* LV_LOG_LEVEL_WARN        Log if something unwanted happened but didn't cause a problem

* LV_LOG_LEVEL_ERROR       Only critical issue, when the system may fail

* LV_LOG_LEVEL_NONE        Do not log anything

*/

#   define LV_LOG_LEVEL    LV_LOG_LEVEL_WARN

/* 1: Print the log with 'printf';

* 0: user need to register a callback with `lv_log_register_print_cb`*/

#   define LV_LOG_PRINTF   1

#endif   /*LV_USE_LOG*/

3.3.6 配置显示接口

LittlevGL绘制的过程需要有一个缓冲区disp_buf,lvgl内部将位图绘制到这个缓冲区,缓冲区满了以后调用flush_cb接口函数进行屏幕的填充,我们将这部分缓冲区的内容通过LCD中断搬运到液晶屏,当填充完成后,调用lv_disp_flush_ready函数通知库绘制已经完成,可以开始其他绘制过程。

LittlevGL已经提供了一个示例文件,我们上面提到的复制文件也是使用他提供的文件,只需要我们修改几个接口函数即可。

定义lvgl绘制的缓冲区,这里需定义在外部SDRAM定义两个全屏的缓冲区。

static lv_disp_buf_t disp_buf;

#ifdef SWM_USING_SRAM

static lv_color_t lcdbuf_1[LV_HOR_RES_MAX * LV_VER_RES_MAX] __attribute__((at(SRAMM_BASE)))           = {0x00000000};

static lv_color_t lcdbuf_2[LV_HOR_RES_MAX * LV_VER_RES_MAX] __attribute__((at(SRAMM_BASE + 0x3FC00))) = {0x00000000};

#endif

#ifdef SWM_USING_SDRAM

static uint32_t lcdbuf_1[LV_HOR_RES_MAX * LV_VER_RES_MAX / 2] __attribute__((at(SDRAMM_BASE)))           = {0x00000000};

static uint32_t lcdbuf_2[LV_HOR_RES_MAX * LV_VER_RES_MAX / 2] __attribute__((at(SDRAMM_BASE + 0x3FC00))) = {0x00000000};

#endif

    lv_disp_buf_init(&disp_buf, lcdbuf_1, lcdbuf_2, LV_HOR_RES_MAX * LV_VER_RES_MAX); /*Initialize the display buffer*/

定义一个lv_disp_drv_t的变量并初始化

    lv_disp_drv_t disp_drv;      /*Descriptor of a display driver*/

    lv_disp_drv_init(&disp_drv); /*Basic initialization*/

设置缓冲区。

    /*Set a display buffer*/

    disp_drv.buffer = &disp_buf;

设置屏幕填充接口,这里的 disp_fluash 是一个函数,在示例中已经定义,我们直接对其修改就行了。

    /*Used to copy the buffer's content to the display*/

    disp_drv.flush_cb = disp_flush;

注册驱动程序。

    /*Finally register the driver*/

    lv_disp_drv_register(&disp_drv);

修改disp_flush函数以适应我们自己的硬件和屏幕

static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)

{

    LCD->SRCADDR = (uint32_t)disp_drv->buffer->buf_act;

    LCD_Start(LCD);

    /* IMPORTANT!!!

     * Inform the graphics library that you are ready with the flushing*/

    lv_disp_flush_ready(disp_drv);

}

3.3.7 配置输入设备接口

lvgl支持键盘、鼠标、按键、触摸屏作为输入设备,我们这里仅仅使用触摸屏进行输入。

lv_port_indev_template.c中,注册一个触摸屏设备需要以下步骤,我们只需要对触摸屏读取的回调函数进行修改即可。

    /*Register a touchpad input device*/

    lv_indev_drv_init(&indev_drv);

    indev_drv.type = LV_INDEV_TYPE_POINTER;

    indev_drv.read_cb = touchpad_read;

    indev_touchpad = lv_indev_drv_register(&indev_drv);

打开触摸屏读取的回调函数touchpad_read函数

/* Will be called by the library to read the touchpad */

static bool touchpad_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data)

{

    static lv_coord_t last_x = 0;

    static lv_coord_t last_y = 0;

    /*Save the pressed coordinates and the state*/

    if (touchpad_is_pressed())

    {

        touchpad_get_xy(&last_x, &last_y);

        data->state = LV_INDEV_STATE_PR;

    }

    else

    {

        data->state = LV_INDEV_STATE_REL;

    }

    /*Set the last pressed coordinates*/

    data->point.x = last_x;

    data->point.y = last_y;

    /*Return `false` because we are not buffering and no more data to read*/

    return false;

}

touchpad_read函数可以看出,用户只需要完成两个接口函数即可。这两个函数的实现如下:

/*Return true is the touchpad is pressed*/

static bool touchpad_is_pressed(void)

{

    /*Your code comes here*/

    if(tp_dev.sta & 0x80)

    {

        return true;

    }

    return false;

}

/*Get the x and y coordinates if the touchpad is pressed*/

static void touchpad_get_xy(lv_coord_t * x, lv_coord_t * y)

{

    /*Your code comes here*/

    (*x) = tp_dev.x[0];

    (*y) = tp_dev.y[0];

}

3.3.8 配置文件系统接口

文件系统使用FATFS,对接前需要完成文件系统的挂载。

文件系统注册。

void lv_port_fs_init(void)

{

    /*----------------------------------------------------

     * Initialize your storage device and File System

     * -------------------------------------------------*/

    fs_init();

    /*---------------------------------------------------

     * Register the file system interface   in LittlevGL

     *--------------------------------------------------*/

    /* Add a simple drive to open images */

    lv_fs_drv_t fs_drv;

    lv_fs_drv_init(&fs_drv);

    /*Set up fields...*/

    fs_drv.file_size     = sizeof(file_t);

    fs_drv.letter        = 'P';

    fs_drv.open_cb       = fs_open;

    fs_drv.close_cb      = fs_close;

    fs_drv.read_cb       = fs_read;

    fs_drv.write_cb      = fs_write;

    fs_drv.seek_cb       = fs_seek;

    fs_drv.tell_cb       = fs_tell;

    fs_drv.free_space_cb = fs_free;

    fs_drv.size_cb       = fs_size;

    fs_drv.remove_cb     = fs_remove;

    fs_drv.rename_cb     = fs_rename;

    fs_drv.trunc_cb      = fs_trunc;

    fs_drv.rddir_size   = sizeof(dir_t);

    fs_drv.dir_close_cb = fs_dir_close;

    fs_drv.dir_open_cb   = fs_dir_open;

    fs_drv.dir_read_cb   = fs_dir_read;

    lv_fs_drv_register(&fs_drv);

}

3.3.9 配置LittlevGL的嘀嗒心跳时钟接口

LittlevGL的使用需要需要周期性的时钟支持,用户需要定期调用 lv_tick_inc(uint32_t tick_period)函数,我们这里利用滴答定时器的1KHz去调用这个函数。在滴答定时器的中断服务函数中添加lvgl时基函数

uint8_t tick_indev = 0;

void SysTick_Handler_cb(void)

{

    lv_tick_inc(1);

    tick_indev++;

    if (tick_indev > 100)

    {

        tick_indev = 0;

        GT911_Scan();

    }

}

3.3.10 初始化

包含lvgl的初始化以及显示和触摸,文件系统接口的初始化

    lv_init();

    lv_port_disp_init();

    lv_port_indev_init();

    lv_port_fs_init();

我们需要在主循环中周期性调用LittelvGL的任务处理函数。

    while (1 == 1)

    {

        lv_task_handler();

    }

然后就可以进行应用的开发了。

上一篇什么是LVGL