基于S5PC100裸机程序之SPI(上)
作者:杨老师,华清远见嵌入式学院讲师。
SPI作为应用最为广泛的通信总线协议之一,开发人员应当掌握,本章将介绍SPI总线协议的基本理论,以及S5PC100的SPI总线控制器的操作方法。
1. SPI总线协议理论
1.1 协议简介
SPI是英文Serial Peripheral Interface的缩写,该协议是由美国摩托罗拉公司推出的一种同步串行传输规范,首先由摩托罗拉公司在其MC68HCXX系列处理器上定义,后主要应用在 EEPROM、FLASH、实时时钟、AD转换器,还有数字信号处理器和数字信号解码器之间。
SPI是一种高速的全双工、同步的通信总线,并且在芯片的引脚上只占用四根线,节约了芯片的引脚,同时为PCB的布局上节省空间,提供方便,正是出于这种简单易用的特性,现在越来越多的芯片集成了这种通信协议。
1.2 协议内容
SPI有4个引脚:CS(从器件选择线)、SDO(串行数据输出线)、SDI(串行数据输入线)和SPICLK(同步串行时钟线)。
SPI的通信原理很简单,它以主从方式工作,这种模式通常有一个主设备和一个或多个从设备,需要至少4根线,事实上3根也可以(单向传输时)。也是所有基于SPI的设备共有的,这些脚的定义如下:
(1)SDO(MOSI)——主设备数据输出,从设备数据输入。
(2)SDI(MISO)——主设备数据输入,从设备数据输出。
(3)SPICLK——时钟信号,由主设备产生。
(4)CS——从设备使能信号,由主设备控制。
其中CS是控制芯片是否被选中的,也就是说只有片选信号为预先规定的使能信号时(高电位或低电位),对此芯片的操作才有效。这就使在同一总线上连接多个SPI设备成为可能。其中总线协议时序如图1所示。
图1 SPI总线协议时序
接下来就是负责通信的3根线了。通信是通过数据交换完成的,这里先要知道SPI是串行通信协议,也就是说数据是一位一位地传输的。这就是SPICLK时钟线存在的原因,由SPICLK提供时钟脉冲,SDO、SDI则基于此脉冲完成数据传输。数据输出通过 SDO线,数据在时钟上升沿或下降沿时改变,在紧接着的下降沿或上升沿被读取,完成一位数据传输,输入也使用同样原理。这样,在至少8次时钟信号的改变 (上沿和下沿为一次)后,就可以完成8位数据的传输。
要注意的是,SPICLK信号线只由主设备控制,从设备不能控制信号线。同样在一个基于SPI的设备中,至少有一个主控设备。这样传输的特点:这样的传输方式有一个优点,即其与普通的串行通信不同,普通的串行通信一次连续传送至少8位数据,而SPI允许数据一位一位地传送,甚至允许暂停,因为SPICLK时钟线由主控设备控制,当没有时钟跳变时,从设备不采集或传送数据。也就是说,主设备通过对SPICLK时钟线的控制可以完成对通信的控制。SPI还是一个数据交换协议:因为SPI的数据输入和输出线独立,所以允许同时完成数据的输入和输出。不同的SPI设备的实现方式不尽相同,主要是数据改变和采集的时间不同,在时钟信号上沿或下沿采集有不同定义。
在点对点的通信中,SPI接口不需要进行寻址操作,且为全双工通信,显得简单高效。在多个从设备的系统中,每个从设备需要独立地使能信号,在硬件上要比I2C总线控制稍微复杂一些。
注意:
SPI的一个缺点是没有指定的流控制,没有应答机制确认是否接收到数据。
SPI 控制器为了和外设进行数据交换,根据外设工作要求,其输出串行同步时钟极性和相位可以进行配置,时钟极性(CPOL)对传输协议没有重大影响。如果 CPOL=0,串行同步时钟的空闲状态为低电平;如果CPOL=1,串行同步时钟的空闲状态为高电平。时钟相位(CPHA)能够配置用于选择两种不同的传输协议之一进行数据传输。如果CPHA=0,在串行同步时钟的第一个跳变沿(上升或下降)数据被采样,如图2所示;如果CPHA=1,在串行同步时钟的第二个跳变沿(上升或下降)数据被采样,如图3所示。SPI主控制器和与之通信的外设时钟相位和极性应该一致,另外,上面提到这些特性将在寄存器中具体实现。
图2 CPHA=0时的情况
图3 CPHA=1时的情况
2 SPI控制器详解
2.1 S5PC100的SPI控制器简介
S5PC100包含了两套8位、16位、32位移位寄存器用于收发。在SPI传输数据时,数据的发送及接收数据是同步的,该总线控制器支持摩托罗拉串行外设接口。
下面是该控制器的特性:
全双工通信方式。
8位、16位、32位移位寄存器。
3时钟源供应。
支持8位、16位、32位总线接口。
支持摩托罗拉SPI协议。
支持两个独立的传输及接收FIFO。
支持主机模式及从机模式。
无法传送条件下接收。
Tx/Rx频率最大支持50MHz。
2.2 时钟源控制
每个SPI都能获得不同的3个时钟源,用户可以根据自己的需要来进行配置,需要设置CLK_CFG这个寄存器,后面将会介绍。
时钟控制器如图4所示。
图4 时钟控制器
2.3 寄存器详解
如表-1所示为SPI配置寄存器。
表1 SPI配置寄存器
MODE_CFGn | 位 | 描述 | 复位值 |
CH_WIDTH | [30:29] | 00 = 字节 01 = 半字 10 = 字 11 = 保留 | 0 |
TRAILING_CNT | [28:19] | 接收FIFO中最后写入字节的个数 | 0 |
BUS_WIDTH | [18:17] | 00 = 字节 01 = 半字 10 = 字 11 = 保留 | 0 |
如表2所示为时钟配置寄存器。
表2 时钟配置寄存器
MODE_CFGn | 位 | 描述 | 复位值 |
CH_WIDTH | [30:29] | 00 = 字节 01 = 半字 10 = 字 11 = 保留 | 0 |
TRAILING_CNT | [28:19] | 接收FIFO中最后写入字节的个数 | 0 |
BUS_WIDTH | [18:17] | 00 = 字节 01 = 半字 10 = 字 11 = 保留 | 0 |
如表3所示为SPI模式配置寄存器。
表3 SPI模式配置寄存器
CLK_CFGn | 位 | 描述 | 复位值 |
SPI_CLKSEL | [10:9] | 时钟源选择 00 = PCLK 01 = SCLK_SPI_48 10 = SCLK_SPI 11 = 保留 | 0 |
ENCLK | [8] | 时钟使能 0 = 禁止 1 = 使能 | 0 |
SPI_SCALER | [7:0] | SPI时钟分频值 SPI 时钟输出 =时钟源 /(2 x(预分值 +1)) | 0 |
如表4所示为SPI数据发送寄存器。
表4 SPI数据发送寄存器
SPI_TX_DATAn | 位 | 描述 | 复位值 |
TX_DATA | [31:0] | 该寄存器包含了所要发送的数据 | 0 |
表5所示为SPI数据接收寄存器。
表5 SPI数据接收寄存器
SPI_RX_DATAn | 位 | 描述 | 复位值 |
RX_DATA | [31:0] | 该寄存器包含了所要接收的数据 | 0 |
如表6所示为SPI状态寄存器。
表6 SPI状态寄存器
SPI_STATUSn | 位 | 描述 | 复位值 |
TX_DONE | [21] | 0 = 其他情况 1 = 发送移位寄存器准备 | 0 |
RX_FIFO_LVL | [19:13] | RX FIFO 0 ~ 64 字节 | 0 |
TX_FIFO_LVL | [12:6] | TX FIFO 0 ~ 64 字节 | 0 |
RX_OVERRUN | [5] | Rx Fifo 溢出错误 0 =无误 1 = 溢出错 | 0 |
RX_UNDERRUN | [4] | 0 = 无误 1 = 数据缺失 | 0 |
TX_OVERRUN | [3] | Tx Fifo 溢出错误 0 =无误 1 = 溢出错 | 0 |
TX_UNDERRUN | [2] | 0 = 无误 1 = 数据缺失 | 0 |
3 SPI接口应用示例
这里将介绍一种通过SPI通信的Flash,该芯片是M24PXX,如图5所示为该芯片的原理图,每根接线的意义已经清楚地标识出来了。
图5 M25PXX原理图
这一款芯片内部集成了12条指令,包括了通用的读、写、配置等命令,还有一个内置的状态寄存器,可以通过该寄存器获取芯片当前状态。
基于S5PC100裸机程序之SPI(下)
3 SPI接口应用示例
这里将介绍一种通过SPI通信的Flash,该芯片是M24PXX,如图5所示为该芯片的原理图,每根接线的意义已经清楚地标识出来了。
图5 M25PXX原理图
这一款芯片内部集成了12条指令,包括了通用的读、写、配置等命令,还有一个内置的状态寄存器,可以通过该寄存器获取芯片当前状态。
如表7所示为M5DPXX芯片指令集。
表7 M25PXX芯片指令集
Instrucion | Description | One-byle Instruction Code | Address Bytes | Dummy Bytes | Date Bytes | |
WREN | 写使能 | 0000 0110 | 06h | 0 | 0 | 0 |
WRDI | 写禁止 | 0000 0100 | 04h | 0 | 0 | 0 |
RDID | 读取ID | 1001 1111 | 9Fh | 0 | 0 | 1 to 3 |
RDSR | 读状态寄存器 | 0000 0101 | 05h | 0 | 0 | 1 to ∞ |
WRSR | 写状态寄存器 | 0000 0001 | 01h | 0 | 0 | 1 |
READ | 字节数据读取 | 0000 0011 | 03h | 3 | 0 | 1 to ∞ |
FAST_READ | 高速的字节读取 | 0000 1011 | 0Bh | 3 | 1 | 1 to ∞ |
PP | 页编程 | 0000 0010 | 02h | 3 | 0 | 1 to 256 |
SE | 扇区擦除 | 1101 1000 | D8h | 3 | 0 | 0 |
BE | 块擦除 | 1100 0111 | C7h | 0 | 0 | 0 |
DP | 深度擦除 | 1011 1001 | B9h | 0 | 0 | 0 |
RES | 从睡眠模式唤醒以及读出电特性 | 1010 1011 | ABh | 0 | 3 | 1 to ∞ |
唤醒 | 0 | 0 | 0 |
有了上文的知识做铺垫,现在我们先来看一下SPI控制器的基本编程模型:
(1)设置时钟源并配置分频值等参数。
(2)软复位后,并设置SPI配置寄存器(SPI CONFIGURATION REGISTER)。
(3)设置模式寄存器。
(4)设置从机选择寄存器。
(5)收发数据。
根据以上信息,这里分成若干模块来逐一实现。
1.相关寄存器结构体定义及宏定义。
/*SPI总线控制器寄存器定义*/
typedef struct {
unsigned int CHCFG;
unsigned int CLKCFG;
unsigned int MODECFG;
unsigned int SLAVESEL;
unsigned int INTEN;
unsigned int STATUS;
unsigned int TXDATA;
unsigned int RXDATA;
unsigned int PACKETCNT;
unsigned int PENDINGCLR;
unsigned int SWAPCFG;
unsigned int FBCLK;
}spi;
#define SPI0 ( * (volatile spi *)0XEC300000 )
/* Flash opcodes. */
#define OPCODE_WREN 0x06 /*写使能*/
#define OPCODE_WRDA 0x04 /*写禁止*/
#define OPCODE_RDSR 0x05 /*读状态寄存器*/
#define OPCODE_WRSR 0x01 /*写状态寄存器*/
#define OPCODE_NORM_READ 0x03 /*低频率的数据读取*/
#define OPCODE_FAST_READ 0x0b /*高频率的数据读取*/
#define OPCODE_PP 0x02 /*页编程*/
#define OPCODE_BE_4K 0x20 /* Erase 4KiB block */
#define OPCODE_BE_32K 0x52 /* Erase 32KiB block */
#define OPCODE_CHIP_ERASE 0xc7 /*擦除整块芯片*/
#define OPCODE_SE 0xd8 /*扇区擦除*/
#define OPCODE_RDID 0x9f /*读ID */
/*状态寄存器*/
#define SR_WIP 1 /*写状态中*/
#define SR_WEL 2 /*写保护锁*/
2.延时函数,使能芯片及禁用芯片的实现如下。
void delay(int times)
{
volatile int i,j;
for (j = 0; j < times; j++){
for (i = 0; i < 100000; i++);
i = i + 1;
}
}
void disable_chip(void)
{
/* disable chip*/
SPI0.SLAVESEL |= 0x1;
delay(1);
}
void enable_chip(void)
{
/* enable chip*/
SPI0.SLAVESEL &= ~0x1;
delay(1);
}
3.软件复位的代码如下
void soft_reset(void)
{
SPI0.CHCFG |= 0x1 << 5;
delay(1);
SPI0.CHCFG &= ~(0x1 << 5);
}
4.接收(字节读)的实现。
在实现读字节功能的时候,第一次发送读指令后,紧接着芯片就会回送数据,这时芯片会自动累加地址,因此需要人为控制所读数据的范围,读时序如图6所示。
图6 读时序
void receive(unsigned char *buf, int len)
{
int i;
SPI0.CHCFG &= ~0x1; // disable Tx
SPI0.CHCFG |= 0x1 << 1; // enable Rx
delay(1);
for (i = 0; i < len; i++){
buf[i] = SPI0.RXDATA;
//S5PC100 SPI支持在FiFo为空时,读缓存产生SPI时钟,否则需要发送数据产生时钟
delay(1);
}
SPI0.CHCFG &= ~(0x1 << 1);
}
5.发送(写页面)的实现。
在发出写指令后,要紧跟着发出第一个数据要存放的地址,这个时候再顺序写入数据,和读的时候一样,芯片会自动累加地址,芯片的页面为256Bytes,写时序如图7所示。
图7 写时序
关键代码如下。
void transfer(unsigned char *data, int len)
{
int i;
SPI0.CHCFG &= ~(0x1 << 1);
SPI0.CHCFG = SPI0.CHCFG | 0x1; // enable Tx and disable Rx
delay(1);
for (i = 0; i < len; i++){
SPI0.TXDATA = data[i];
while( !(SPI0.STATUS & (0x1 << 21)) );
delay(1);
}
SPI0.CHCFG &= ~0x1;
}
6.擦除芯片相关操作如下。
这块芯片提供了两种擦除方式,第一种是分扇区来进行擦除,重点在于选好指定的地址,然后进行擦除,结合如图8所示的时序图。
图8 擦除扇区时序图
第二种是整片芯片擦除,如图9所示为整片芯片的擦除时的时序情况。
图9 整片芯片擦除时序图
关键代码如下。
/*擦除扇区*/
void erase_sector(int addr)
{
unsigned char buf[4];
buf[0] = OPCODE_SE;
buf[1] = addr >> 16;
buf[2] = addr >> 8;
buf[3] = addr;
enable_chip();
transfer(buf, 4);
disable_chip();
}
/*擦除芯片*/
void erase_chip()
{
unsigned char buf[4];
buf[0] = OPCODE_CHIP_ERASE;
enable_chip();
transfer(buf, 1);
disable_chip();
}
7.解析状态。
判断芯片工作是否结束,如图10所示为状态寄存器读时序。
图10 状态寄存器读时序
关键代码如下。
void wait_till_write_finished()
{
unsigned char buf[1];
enable_chip();
buf[0] = OPCODE_RDSR;
transfer(buf, 1);
while(1) {
receive(buf, 1);
if(buf[0] & SR_WIP) {
// printf( "Write is still in progress\n" );
}
else {
printf( "Write is finished.\n" );
break;
}
}
disable_chip();
}
8.读芯片ID。
读ID时序如图11所示。
图11 读ID时序
关键代码如下。
void read_ID(void)
{
unsigned char buf[3];
int i;
buf[0] = OPCODE_RDID;
soft_reset();
enable_chip();
transfer(buf, 1);
receive(buf, 3);
disable_chip();
printf("MI = %x\tMT = %x\tMC = %x\t\n", buf[0], buf[1], buf[2]);
}
9.主程序
int main()
{
unsigned char buf[10] = "home\n";
unsigned char data[10] = "morning\n";
uart0_init();
printf("aaaaa \n");
cfg_gpio(); //配置SPI IO功能
set_clk(); //使能SPI控制器的时钟
cfg_spi0(); //配置SPI0控制器
while(1)
{
read_ID(); //读出SPI Flash的ID号
write_spi(buf, 4, 0); //向目标芯片0地址写入4个字节数据
read_spi(data, 4, 0); //从目标芯片0地址读出4个字节数据
printf("read from spi :%s", data);
}
return 0;
}