嵌入式Linux——SPI总线(3):2440SPI控制器控制OLED和flash

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/W1107101310/article/details/80360025

简介:

    本文主要讲解使用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<len;i++){
		flash_write_cmd(buf[i]);
	}
	
	spi_set_CS(1);

	spi_flash_wait_when_busy();	
}

/* 读数据 */
void spi_flash_read(unsigned int addr,unsigned char *buf,int len)
{
	int i;

	spi_set_CS(0);
	flash_write_cmd(0x03);
	flash_write_addr(addr);

	for(i=0;i<len;i++){
		buf[i] = flash_read_byte();
	}
	
	spi_set_CS(1);

}
    讲到这里就讲完了,谢谢。

    我将详细的代码放到了:2440SPI控制器控制OLED和FLASH


猜你喜欢

转载自blog.csdn.net/W1107101310/article/details/80360025