简介:
本文主要讲解使用2440中的SPI控制器来控制SPI传输命令和数据,以实现对OLED和FLASH的控制。同时本文会分为两部分,第一部分主要介绍2440的SPI控制器。而第二部分我们将结合代码,看是如何实现对OLED和FLASH的控制。
所用开发板:JZ2440 V3
所用OLED 屏幕:韦东山老师淘宝所用屏幕
所用OLED 驱动芯片:SSD1306
FLASH:W25Q16DV
声明:
我在前面两篇文章:嵌入式Linux——SPI总线(1):2440裸机GPIO模拟SPI驱动OLED和嵌入式Linux——SPI总线(2):2440裸机GPIO模拟SPI控制FLASH中已经讲解了对OLED和FLASH的设置。而前面两篇文章的重点就是OLED手册和flash手册的说明,因此,我在这篇文章中将主要讲解2440中SPI控制器实现收发函数的步骤,而关于OLED和flash部分的知识将简略介绍。
第一部分:2440SPI控制器
S3C2440A的串行外设接口(SPI)可以作为串行数据传输的接口。S3C2440A有两个SPI接口,每个接口有两个8位的移位寄存器分别用来传送和接收数据。当一个SPI接口在传输时,数据即发送(串行移出)又接收(串行移入)。8位的串行数据的传输频率是由他相应的控制寄存器设置的。如果你仅仅想要传送,输出数据可以忽略,同样如果你仅仅只是想接收。你应该发送1 。
SPI传输中有4个I/O引脚:时钟SCK (SPICLK0,1), 主机输入MISO (SPIMISO0,1), 主机输出MOSI(SPIMOSI0,1)和片选/SS (nSS0,1)。
SPI操作:
使用SPI接口,S3C2440A可以同时与一个外设发送/接收8位数据。而串行时钟线与用于发送和接收信息的数据线同步。当SPI为主机时,可以通过给SPPREn寄存器设置一个合适的值来设置传输频率。而当SPI为从机时,另一个主机控制时钟。当程序员向SPTDATn寄存器写入数据时,SPI发送/接收操作就开始了。通常,片选nSS要在向SPIDATn写数据前拉低。
SPI传输格式:
S3C2440A支持4种不同的传输格式,如下图。
在这里我先讲解一下这四种不同格式的传输有什么区别。
首先我们先讲CPOL=0与CPOL=1的区别。当CPOL=0时,时钟平时将处于低电平,他的第一个时钟沿一定是上升沿,而当完成传输后时钟会回到低电平状态。而当CPOL=1时,时钟平时将处于高电平,他的第一个时钟沿一定是下降沿,而当完成传输后时钟会回到高电平状态。
下面我们讲CPHA=0与CPHA=1的区别。当CPHA=0时,SPI在第一个时钟沿时触发数据的发送或读取。而当CPHA=1时,SPI在第二个时钟沿时触发数据的发送或读取。
而Format A与Format B的区别是,Format A是当这个时钟周期传完,而下一个时钟周期还没有到来前,在MISO线上当前位电平应该与上一时钟周期的最高有效位电平保持一致。而Format B是当这个时钟周期传完,而下一个时钟周期还没有到来前,在MISO线上当前位电平应该与下一时钟周期的最低有效位电平保持一致。如下图:
下面就是SPI寄存器:
SPI控制寄存器:
寄存器
地址
R/W
描述
重启值
SPCON0
0x59000000
R/W
SPI通道0控制寄存器
0x00
SPCON1
0x59000020
R/W
SPI通道1控制寄存器
0x00
SPCONn
位
描述
初始值
SPI模式选择(SMOD)
[6:5]
决定SPTDAT怎样读写:
00 = 轮询 01 = 中断模式
10 = DMA模式 11 = 保留
00
时钟使能(ENSCK)
[4]
决定你是否想要使能CSK(只能是主机)
0 = 不使能 1 = 使能
0
主机/从机选择(MSTR)
[3]
决定想要的模式(主机或者从机)
0 = 从机 1 = 主机
主要:在从机模式时,由主机来设置初始化发送接收时间
0
时钟极性选择(CPOL)
[2]
决定一个高电平或低电平时钟
0 = 高电平 1 = 低电平
0
时钟相位选择(CPHA)
[1]
从两个不同的传输模式中选出一个
0 = 模式A 1 = 模式B
0
传输自动Garbage数据模式使能(TAGD)
[0]
决定是否请求接收数据
0 = 正常模式 1 = 传输自动Garbage数据模式
注意:在正常模式,如果你仅仅只是想接收数据,你应该传输0xff
0
SPI状态寄存器:
寄存器
地址
R/W
描述
重启值
SPSTA0
0x59000004
R
SPI通道0状态寄存器
0x01
SPSTA1
0x59000024
R
SPI通道1状态寄存器
0x01
SPSTAn
位
描述
初始值
保留
[7:3]
数据冲突错误标志(DCOL)
[2]
当一个传输正在进行时,如果SPTDATn被写入或者SPRDATNT被读,将使该位置1。通过读SPSTAn清零
0 = 没有检测到 1 = 检测到冲突错误
0
多主机错误标志(MULF)
[1]
当片选nSS信号选中时,SPI被配置为主机该位就被设置为1,SPPINn的ENMUL位就检测到多主机错误。MULF通过读SPSTAn清零
0 = 没有检测到 1 = 检测到错误
0
传输就绪标志位(REDY)
[0]
该位表示SPTDATn或者SPRDATn准备去发送或者接收。通过读SPSTAn清零
0 = 没有就绪 1 = 就绪
1
SPI波特率预分频寄存器:
寄存器
地址
R/W
描述
重启值
SPPRE0
0x5900000C
R/W
SPI通道0波特率预分频寄存器
0x00
SPPRE1
0x5900002C
R/W
SPI通道1波特率预分频寄存器
0x00
SPPREn
位
描述
初始值
预分频值
[7:0]
决定SPI时钟速率
波特率 = PCLK / 2 / (预分频值+ 1)
0x00
注意:波特率应该小于25MHz
SPI发送数据寄存器:
寄存器
地址
R/W
描述
重启值
SPTDAT0
0x59000010
R/W
SPI通道0发送数据寄存器
0x00
SPTDAT1
0x59000030
R/W
SPI通道1发送数据寄存器
0x00
SPTDATn
位
描述
初始值
发送数据寄存器
[7:0]
包含发送到SPI通道中的数据
0x00
SPI接收数据寄存器:
寄存器
地址
R/W
描述
重启值
SPRDAT0
0x59000014
R
SPI通道0接收数据寄存器
0xff
SPRDAT1
0x59000034
R
SPI通道1接收数据寄存器
0xff
SPRDATn
位
描述
初始值
接收数据寄存器
[7:0]
包含从SPI通道中接收的数据
0xff
讲解完这些我们就要按着2440手册SPI中所讲的步骤来写SPI模块了:
1. 设置波特率预分频寄存器(SPPREn)。
2. 设置SPCONn来配置合适的SPI模块
3. 向SPTDATn寄存器写10次0xff 来初始化MMC或者SD卡
4. 设置GPIO引脚,他们将拉低nSS来激活MMC或者SD卡
5. 传送数据 :检查传输就绪标志(REDY=1)的状态,然后将数据写入SPTDATn
6. 接收数据(1):SPCONn的TAGD为正常模式(normal mode)
7. 写0xff到SPTDATn寄存器,然后确定REDY位被设置,最后就可以从读缓存中读数据了
8. 接收数据(2):SPCONn的TAGD为Tx auto garbage data mode
9. 然后确定REDY位被设置,最后就可以从读缓存中读数据了
10. 拉高片选引脚
第二部分:结合代码讲解SPI控制OLED和FLASH
我们首先先完成设置波特率预分频寄存器(SPPREn)和设置SPCONn来配置合适的SPI模块。因为这两步实现的是对SPI控制器的初始化,代码为:
static void spi_cnt_init(void)
{
/*
*第一步:设置波特率
*oled 最大速率为10MHz,而flash最大速率为104MHz,
*所以选OLED的10MHz为波特率,PCLK为50MHz
*由于Baud rate = PCLK / 2 / (Prescaler value + 1)
*所以Prescaler为2
*/
SPPRE0 = 2;
SPPRE1 = 2;
/*
*第二步:设置spi控制寄存器SPCON
*[6:5] :00 轮询模式
*[4] :1 使能时钟
*[3] :1 主机模式
*[2] :0 选择默认高电平
*[1] :0 默认模式A
*[0] :0 正常模式
*/
SPCON0 = (1<<4) | (1<<3);
SPCON1 = (1<<4) | (1<<3);
}
从上面的代码可以看出,波特率预分频系数为2,所以波特率为8.3MHz。我们设置的SPI为主机,并设置传输方式为轮询。而SPI的传输格式为CPOL=0,CPHA=0,即时钟平时将处于低电平,他的第一个时钟沿一定是上升沿,而当完成传输后时钟会回到低电平状态。SPI在第一个时钟沿时触发数据的发送或读取。同时我们还设置为正常模式。
由于我们没有使用MMC和SD卡,所以我们不用步骤3,接着我们按步骤4操作,即对GPIO进行初始化:
static void spi_gpio_init(void)
{
/* GPF1 OLED_CS0 output */
GPFCON &= ~(0x3<<(1*2));
GPFCON |= (0x1<<(1*2));
GPFDAT |= (0x1<<1);
/* GPG2 FLASH_CS0 output
* GPG4 OLED_D/C output
* GPG5 SPI_MISO
* GPG6 SPI_MOSI
* GPG7 SPI_CLK
*/
GPGCON &= ~((0x3<<(2*2)) | (0x3<<(4*2)) | (0x3<<(5*2)) | (0x3<<(6*2)) | (0x3<<(7*2)));
GPGCON |= ((0x1<<(2*2)) | (0x1<<(4*2)) | (0x3<<(5*2)) | (0x3<<(6*2)) | (0x3<<(7*2)));
GPGDAT |= (0x1<<2);
}
完成上面这些准备工作,我们将在下面开始开始做数据收发的工作。我们先看步骤5,传送数据 :检查传输就绪标志(REDY=1)的状态,然后将数据写入SPTDATn。而它对应的函数为:
void spi_send_byte(unsigned char val)
{
while(!(SPSTA1 & 1)); //检验REDY=1
SPTDAT1 = val;
}
如上面SPI状态寄存器所示:REDY表示SPTDATn或者SPRDATn准备去发送或者接收。通过读SPSTAn清零。当REDY = 0时表示没有就绪 当REDY =1时表示就绪。当SPI准备就绪后,
数据写入SPI数据传输寄存器SPTDAT1。
由于我们在上面SPI控制寄存器中配置SPI为正常模式,所以我们按步骤6和步骤7来接收数据,即接收数据(1):SPCONn的TAGD为正常模式(normal mode),写0xff到SPTDATn寄存器,然后确定REDY位被设置,最后就可以从读缓存中读数据了。而接收函数为:
unsigned char spi_recv_byte(void)
{
SPTDAT1 = 0xff;
while(!(SPSTA1 & 1)); //检验REDY=1
return SPRDAT1;
}
好了,现在我们就讲完SPI控制器的设置,以及收发函数的编写。下面我们就要将他们运用到OLED和flash上去。我们先看在OLED上的运用。
SPI在OLED上的运用:
由于我们的spi并没有对片选引脚/CS的控制,所以我们要自己编写对片选引脚/CS控制。同时又由于在OLED中要使用数据/命令引脚的高低电平来区分我们所发送的是命令还是数据。所以我们要再定义两个引脚:
/* 数据/命令引脚 */
void spi_set_DC(char val)
{
if(val)
GPGDAT |= (0x1<<4);
else
GPGDAT &= ~(0x1<<4);
}
/* 片选引脚 */
static void spi_set_CS(char val)
{
if(val)
GPFDAT |= (0x1<<1);
else
GPFDAT &= ~(0x1<<1);
}
下面我们要完成的是对数据和命令的写函数,因为操作OLED其实就是对OLED进行写命令和写数据操作而已。下面我们看
写命令和写数据函数:
/* 写命令函数 */
void spi_oled_write_cmd(unsigned char cmd)
{
spi_set_DC(0);
spi_set_CS(0);
spi_send_byte(cmd);
spi_set_CS(1);
spi_set_DC(0);
}
/* 写数据函数 */
void spi_oled_write_dat(unsigned char dat)
{
spi_set_DC(1);
spi_set_CS(0);
spi_send_byte(dat);
spi_set_CS(1);
spi_set_DC(1);
}
而写完写数据和写命令函数后,我们就可以对OLED进行操作了,如下面的
初始化OLED函数,设置OLED显示位置函数,显示单字符函数,显示字符串函数以及清屏函数。
/* 初始化OLED函数 */
void oled_init(void)
{
spi_oled_write_cmd(0xae);//--display off
spi_oled_write_cmd(0x00);//---set low column address
spi_oled_write_cmd(0x10);//---set high column address
spi_oled_write_cmd(0x40);//--set start line address
spi_oled_write_cmd(0xb0);//--set page address
spi_oled_write_cmd(0x81); // contract control
spi_oled_write_cmd(0xff);//--128
spi_oled_write_cmd(0xa1);//set segment remap
spi_oled_write_cmd(0xa6);//--normal / reverse
spi_oled_write_cmd(0xa8);//--set multiplex ratio(1 to 64)
spi_oled_write_cmd(0x3f);//--1/32 duty
spi_oled_write_cmd(0xc8);//com scan direction
spi_oled_write_cmd(0xd3);//-set display offset
spi_oled_write_cmd(0x00);//
spi_oled_write_cmd(0xd5);//set osc division
spi_oled_write_cmd(0x80);//
spi_oled_write_cmd(0xd8);//set area color mode off
spi_oled_write_cmd(0x05);//
spi_oled_write_cmd(0xd9);//set pre-charge period
spi_oled_write_cmd(0xf1);//
spi_oled_write_cmd(0xda);//set com pin configuartion
spi_oled_write_cmd(0x12);//
spi_oled_write_cmd(0xdb);//set vcomh
spi_oled_write_cmd(0x30);//
spi_oled_write_cmd(0x8d);//set charge pump enable
spi_oled_write_cmd(0x14);//
oled_set_pageAddr_mode();
oled_clear();
spi_oled_write_cmd(0xaf);//--turn on oled panel
}
/* 设置OLED显示位置函数 */
void oled_set_pos(int page,int col)
{
spi_oled_write_cmd(0xb0+page);
spi_oled_write_cmd(col&0x0f);
spi_oled_write_cmd(((col&0xf0)>>4)|0x10);
}
/* 显示单字符函数 */
static void oled_put_char(int page,int col,char c)
{
int i;
/* 得到字模 */
const unsigned char *val = F8X16[c-' '];
/* 发送OLED */
oled_set_pos(page,col);
/* 发送8字节数据 */
for(i=0;i<8;i++){
spi_oled_write_dat(val[i]);
}
/* 发送OLED */
oled_set_pos(page+1,col);
/* 发送8字节数据 */
for(i=0;i<8;i++){
spi_oled_write_dat(val[i+8]);
}
}
/* 显示字符串函数 */
void oled_print_string(int page,int col,char *str)
{
int i = 0;
while(str[i]){
oled_put_char(page,col,str[i]);
col += 8;
if(col > 127){
col = 0;
page += 2;
}
i++;
}
}
/* 清屏函数 */
void oled_clear(void)
{
int page,col;
for(page=0;page<8;page++){
oled_set_pos(page,0);
for(col=0;col<128;col++){
spi_oled_write_dat(0);
}
}
}
讲完在OLED中的运用,我们现在讲解在flash中运用spi控制器的收发函数。
flash中的运用:
同样,由于spi没有对片选引脚的控制,所以我们要在这里加对片选的控制,
片选函数为:
static void spi_set_CS(char val)
{
if(val)
GPGDAT |= (0x1<<2);
else
GPGDAT &= ~(0x1<<2);
}
下面我们来完成写
命令函数,其实写命令函数就是我们的SPI发送函数,这里只是调用一下就可以了:
static void flash_write_cmd(unsigned char cmd)
{
spi_send_byte(cmd);
}
而由于我们的flash内存为16Mbit,所以我们需要用24位的地址来访问内存中的任意位置,而写地址就要连续的使用三次SPI发送数据,所以
发地址函数为:
static void flash_write_addr(unsigned int addr)
{
spi_send_byte((addr>>16)&0xff);
spi_send_byte((addr>>8)&0xff);
spi_send_byte(addr&0xff);
}
而
flash接收函数与SPI的接收函数一样,所以这里也只是调用一下:
unsigned char flash_read_byte(void)
{
return spi_recv_byte();
}
有了上面的函数我们就可以按着
操作flash的步骤写flash了:
1. 去保护
a. 写保护
b. 写寄存器
2. 擦除扇区(擦除指定地址内存)
3. 写页(即页编程,向指定的位置 写入信息)
4. 读数据(从指定的位置读信息)
我们先完成步骤1:
/* 写使能 */
static void spi_flash_write_enable(int enable)
{
if(enable){
spi_set_CS(0);
flash_write_cmd(0x06);
spi_set_CS(1);
}else{
spi_set_CS(0);
flash_write_cmd(0x04);
spi_set_CS(1);
}
}
/* 读状态寄存器1 */
static unsigned char spi_flash_read_status_Reg1(void)
{
unsigned char val = 0;
spi_set_CS(0);
flash_write_cmd(0x05);
val = flash_read_byte();
spi_set_CS(1);
return val;
}
/* 读状态寄存器2 */
static unsigned char spi_flash_read_status_Reg2(void)
{
unsigned char val = 0;
spi_set_CS(0);
flash_write_cmd(0x35);
val = flash_read_byte();
spi_set_CS(1);
return val;
}
/* 等待就绪 */
static void spi_flash_wait_when_busy(void)
{
while(spi_flash_read_status_Reg1() & 1);
}
/* 写状态寄存器 */
static void spi_flash_write_status_Reg(unsigned char reg1,unsigned char reg2)
{
spi_flash_write_enable(1);
spi_set_CS(0);
flash_write_cmd(0x01);
flash_write_cmd(reg1);
flash_write_cmd(reg2);
spi_set_CS(1);
spi_flash_wait_when_busy();
}
/* 去状态寄存器保护 */
static void spi_flash_clear_protect_for_status_Reg(void)
{
unsigned char reg1,reg2;
reg1 = spi_flash_read_status_Reg1();
reg2 = spi_flash_read_status_Reg2();
reg1 &= ~(1<<7);
reg2 &= ~(1<<0);
spi_flash_write_status_Reg(reg1,reg2);
spi_flash_wait_when_busy();
}
/* 去内存保护 */
static void spi_flash_clear_protect_for_data(void)
{
unsigned char reg1,reg2;
reg1 = spi_flash_read_status_Reg1();
reg2 = spi_flash_read_status_Reg2();
reg1 &= ~(7<<2);
reg2 &= ~(1<<6);
spi_flash_write_status_Reg(reg1,reg2);
spi_flash_wait_when_busy();
}
然后是
擦除扇区,页编程和读数据:
/* 擦除扇区,erase 4K */
void spi_flash_erase_sector(unsigned int addr)
{
spi_flash_write_enable(1);
spi_set_CS(0);
flash_write_cmd(0x20);
flash_write_addr(addr);
spi_set_CS(1);
spi_flash_wait_when_busy();
}
/* 页编程 */
void spi_flash_program(unsigned int addr,unsigned char *buf,int len)
{
int i;
spi_flash_write_enable(1);
spi_set_CS(0);
flash_write_cmd(0x02);
flash_write_addr(addr);
for(i=0;i
讲到这里就讲完了,谢谢。
我将详细的代码放到了:2440SPI控制器控制OLED和FLASH