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

2019-07-12 20:12发布

简介:     本文主要讲解使用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