在通信协议-SPI小节,我们已经对SPI协议进行了详细的介绍,这里就不在重复赘述。
一、S3C2440上的SPI
1.1 SPI概述
SPI的使用位于S3C2440芯片手册的第23章。S3C2440包含了2个SPI,每个SPI都有2个分别分别用于发送和接收的8位串行移位寄存器。
一个SPI时钟周期,同时发送(串行移除)和接收(串行移入)一位数据,由相应控制寄存器设置指定8位串行数据的输出频率。
如果只希望发送数据,则接收数据可以保持伪位;如果只希望接收数据,则需要发送伪位'1'数据。
S3C2440 SPI特性:
- 支持2个通道SPI;
- 兼容SPI协议(2.11版本);
- 8位发送串行移位寄存器;
- 8位接收串行移位寄存器;
- 8位预分频逻辑;
- 查询、中断和DMA传输模式;
1.2 SPI方块图
S3C2440 SPI相关引脚定义:
SPI | SCLK | MOSI | MISO | SS |
SPI0 | GPE13 | GPE12 | GPE11 | GPG2 |
SPI1 | GPG7 | GPG6 | GPG5 | GPG3 |
二、SPI相关寄存器
2.1 SPI控制寄存器SPCONn
寄存器 | 地址 | R/W | 描述 | 复位值 |
SPCON0 | 0x59000000 | R/W | SPI通道0控制寄存器 | 0x00 |
SPCON1 | 0x59000020 | R/W | SPI通道1控制寄存器 | 0x00 |
寄存器位信息:
SPICONn | 位 | 描述 | 初始状态 |
SMOD | [6:5] |
SPI模式选择,决定如何读/写SPTDAT 00:查询模式 01:中断模式 10:DMA模式 11:保留 |
00 |
ENSCK | [4] |
SCK使能 0:禁止 1:使能 |
0 |
MSTR | [3] |
主/从机选择 0:从机 1:主机 |
0 |
CPOL | [2] |
时钟极性选择,决定时钟是高电平有效还是低电平有效 0:高电平有效 1:低电平有效 |
0 |
CPHA | [1] |
时钟相位选择,和CPOL一起决定采样时刻 0:第一个边沿 1:第二个边沿 |
0 |
TAGD | [0] |
仅接收模式控制 0:正常收发 1:仅接收(此时自动发送任意数据) 在正常模式,如果只想接收数据,需要发送0xFF |
0 |
2.2 SPI状态寄存器SPSTAn
寄存器 | 地址 | R/W | 描述 | 复位值 |
SPSTA0 | 0x59000004 | R | SPI通道0状态寄存器 | 0x01 |
SPSTA1 | 0x59000024 | R | SPI通道1状态寄存器 | 0x01 |
寄存器位信息:
SPSTAn | 位 | 描述 | 初始状态 |
保留 | [7:3] |
保留 |
- |
DCOL | [2] |
数据冲突错误标志。如果当传输正在进行中写了SPTDATn或读了SPRDATn此标志置位,并且可以通过读取SPSTAn清除 0:无错误 1:发生冲突错误 |
0 |
MULF | [1] |
多主SPI错误标志 0:无错误 1:多主SPI错误 |
0 |
REDY | [0] |
收发就绪标志,此位表示SPTDATn或SPRDATn准备好了发送或者接收 0:未就绪 1:Tx/Rx就绪 |
1 |
2.3 SPI引脚控制寄存器SPPINn
寄存器 | 地址 | R/W | 描述 | 复位值 |
SPPIN0 | 0x59000008 | R/W | SPI通道0引脚控制寄存器 | 0x02 |
SPPIN1 | 0x59000028 | R/W | SPI通道1引脚控制寄存器 | 0x02 |
寄存器位信息:
SPPINn | 位 | 描述 | 初始状态 |
保留 | [7:3] |
保留 |
- |
ENMUL | [2] |
多主机错误检测使能。当SPI系统为主机时,nSS引脚被用来作为输入检测多主机错误 0:禁止 1:使能 |
|
保留 | [1] |
保留 |
|
KEEP | [0] |
决定当1字节发送完成后MOSI的控制或释放(主机) 0:释放 1:保持之前的电平 |
2.4 SPI波特率预分频寄存器SPPREn
寄存器 | 地址 | R/W | 描述 | 复位值 |
SPPRE0 | 0x5900000C | R/W | SPI通道0波特率预分频寄存器 | 0x00 |
SPPRE1 | 0x5900002C | R/W | SPI通道1波特率预分频寄存器 | 0x00 |
寄存器位信息:
SPPREn | 位 | 描述 | 初始状态 |
保留 | [7:0] |
决定SPI时钟频率 波特率=PCLK/2/(预分频值+1) |
0xxx |
波特率应低于25MHz。
2.5 SPI发送数据寄存器SPTDATn
寄存器 | 地址 | R/W | 描述 | 复位值 |
SPTDAT0 |
0x59000010 | R/W | SPI通道0发送数据寄存器 | 0x00 |
SPTDAT1 | 0x59000030 | R/W | SPI通道1发送数据寄存器 | 0x00 |
寄存器位信息:
SPTDATn | 位 | 描述 | 初始状态 |
保留 | [7:0] |
包含通过SPI通道要发送的数据 |
0x00 |
2.6 SPI接收数据寄存器SPRDATn
寄存器 | 地址 | R/W | 描述 | 复位值 |
SPRDAT0 |
0x59000014 | R/W | SPI通道0接收数据寄存器 | 0xFF |
SPRDAT1 | 0x59000034 | R/W | SPI通道1接收数据寄存器 | 0xFF |
寄存器位信息:
SPRDATn | 位 | 描述 | 初始状态 |
保留 | [7:0] |
包含通过SPI通道接收到的数据 |
0xFF |
三、读写操作流程
3.1 初始化
3.1.1 IO引脚设置
设置SPIn引脚复用,这里我们以SPI通道1相关引脚为例:
GPGCON &= ~((3<<6) | (3<<10) | (3<<12) | (3 <<14)); /* 清零 */ GPGCON |= ((3<<6) | (3<<10) | (3 <<12) | (3 <<14)); /* 设置为SPI */
3.1.2 设置波特率预分频寄存器(SPPRE1)
由于我们PCLK=50HMz,因此当设置SPPRE1为4时,频率为$frac{50MHz}{2*(4+1)}=5MHz$
SPPRE1 = 4;
注意:设置波特率,需要根据威慑所能接收的范围来设置,比如查阅OLED的芯片手册得知其时钟最小值为100ns,即最大为10MHz。
3.1.3 配置SPCON1
配置查询模式;位[6:5]=00;
配置SCK输出使能;位[4]=1;
配置时钟极性、时钟相位;根据外接SPI设备来设置时钟极性,以及时钟相位,这里我们设置位[3:2]=00;
配置主机模式;位[1]=1;
配置正常收发模式;位[0]=0;
SPCON1 = 0<<5 | 1<<4 | 1<<3 | 0<<2 | 0<< 1 | 0<<0;
初始化代码如下,这里代码包含了通过SPI控制器实现SPI通信,以及通过GPIO模拟SPI通信两种方式:
/************************************************************* * * Function : spi0初始化,GPG7 ~ SCLK GPG6 ~ MOSI GPG5 ~ MISO GPG3 ~ SS * **************************************************************/ void spi_init() { #ifdef GPIO_SPI /* IO配置 */ GPGCON &= ~((3<<6) | (3<<10) | (3<<12) | (3 <<14)); /* 清零 */ GPGCON |= ((1<<6) | (0<<10) | (1<<12) | (1 <<14)); /* 设置GPG5输入 GPG3、6、7为输出 */ #else /* 1. IO配置 */ GPGCON &= ~((3<<6) | (3<<10) | (3<<12) | (3 <<14)); /* 清零 */ GPGCON |= ((1<<6) | (3<<10) | (3 <<12) | (3 <<14)); /* 设置GPG5、6、7为SPI、GPG3为输出 */ /* 2. 设置波特率预分频寄存器 */ SPPRE1 = 4; /* 3. 配置SPI 查询模式 SCK输出使能 时钟极性高电平有效 时钟相位第一个边沿 主机模式 正常收发 */ SPCON1 = (0<<5) | (1<<4) | (1<<3) | (0<<2) | (0<< 1) | (0<<0); #endif GPGDAT |= (1<<3) ; /* 取消片选 */ }
需要注意的是,当采用SPI控制器实现SPI通信时,片选引脚GPG3是需要自己控制片选/取消片选的,这里经过测试发现如果GPG3引脚复用成nSS1是没有效果的。
3.2 发送数据
检查状态寄存器SPSTA1发送就绪标志位(REDY=1),并接着写数据到SPTDAT1;
/************************************************************* * * Function : 主设备发送一个字节 * Input : data 字节数据 * **************************************************************/ void spi_write_byte(u8 data) { int i; /* 选中 */ spi_set_cs(0); #ifdef GPIO_SPI for (i = 0; i < 8; i++){ spi_set_clk(0); spi_set_mosi(data & 0x80); /* 上升沿采样 输出最高位 */ spi_set_clk(1); data <<= 1; } #else /* 等待发送或接收 ready */ while (!(SPSTA1 & 1)); SPTDAT1 = data; #endif /* 取消选中 */ spi_set_cs(1); }
GPIO模拟相关位操作代码:
/************************************************************* * * Function : 片选信号 GPG3引脚 * Input : val 0 选中 1 未选中 * **************************************************************/ void spi_set_cs(char val) { if (val) /* 1 未选中 */ GPGDAT |= (1<<3); else GPGDAT &= ~(1<<3); /* 0 选中 */ } /************************************************************* * * Function : GPIO模拟SPI时 设置时钟信号 * Input : val 0 低电平 1 高电平 * **************************************************************/ void spi_set_clk(u8 val){ if (val) /*1 高电平 */ GPGDAT |= (1<<7); else /*0 低电平 */ GPGDAT &= ~(1<<7); } /************************************************************* * * Function : GPIO模拟SPI时 设置MOSI * Input : val 0 低电平 1 高电平 * **************************************************************/ void spi_set_mosi(u8 val){ if (val) /*1 高电平 */ GPGDAT |= (1<<6); else /*0 低电平 */ GPGDAT &= ~(1<<6); }
3.3 接收数据
正常收发方式(同时收发,TAGD=0):向数据发送寄存器SPTDAT1写0xFF,查询并确认 REDY为1,然后从数据接收寄存器中读取数据;
/************************************************************* * * Function : 主设备接收一个字节 * Input : data 字节数据 * **************************************************************/ u8 spi_read_byte() { u8 val = 0; /* 选中 */ spi_set_cs(0); #ifdef GPIO_SPI /* todo 未实现*/ #else /* 向数据发送寄存器SPTDAT1写0xFF */ SPTDAT1 = 0xff; /* 等待发送或接收 ready */ while (!(SPSTA1 & 1)); val = SPRDAT1; #endif /* 取消选中 */ spi_set_cs(1); }
仅接收方式(TAGD=1):并确认REDY为1,然后从数据接收寄存器中读取数据。读取数据的同时启动一次发送。
四、OLED128x64(SSD1306)
由于Mini2440开发板并没有外接SPI设备,因此我们只能通过开发板引脚外接SPI设备。
这里我们外接一款支持SPI通信的OLED,显示屏尺寸为0.96寸、OLED屏幕内部驱动IC为SSD1306,像素为128*64。
OLED 有机发光二极管,相比于LCD区别在于LCD需要背光,而OLED不需要,因为它是自发光的。
4.1 SSD1606介绍
SD1306是一款带控制器的用于OLED点阵图形显示系统的单片CMOS OLED/PLED驱动器。它由128个SEG(列输出)和64个COM(行输出)组成。该芯片专为共阴极OLED面板设计。
SSD1306内置对比度控制器、显示RAM(GDDRAM)和振荡器,以此减少了外部元件的数量和功耗。
该芯片有256级亮度控制,数据或命令由通用微控制器通过硬件选择的6800/8000系通用并行接口、I2C接口或串行外围接口发送。
该芯片适用于许多小型便携式应用,如手机副显示屏、MP3播放器和计算器等。
使用该芯片可通过硬件电阻连接选中使用三线SPI、四线SPI或IIC,如下图所示:
至于为什么,我们可以在芯片datasheet上找到答案,SSD1306通过BS[2:0]引脚选择通信协议:
当选择不同的通信协议时,其数据引脚和控制引脚也略有差异:
4.2 引脚说明
当SSD1306选定4-wire serial interface接口方式,SPI引脚定义:
- CS:片选信号;连接S3C2440的GPG3引脚;
- DC::命令数据选择引脚;连接S3C2440的GPG10引脚;
- RES:模块复位引脚,低电平有效;连接S3C2440的GPG9引脚;
- D1:MOSI,SPI数据线,主设备输出从设备输入引脚;连接S3C2440的GPG6引脚;
- D0:SCLK,SPI时钟线;连接S3C2440的GPG7引脚;
- VCC:电源正极3.3~5V;
- GND:电源地;
我们需要按照Mini2440的电路原理图去将oled引脚和开发板GPIO引脚连接起来。
4.3 显示原理
SSD1306使用GDDRAM保存要显示的数据,GCDRAN是未映射静态RAM。RAM的大小为128 x 64位,RAM分为8页,从第0页到第7页,用于单色128x64点阵显示。
每个点(像素)使用1位来表示,为1则亮,为0则灭。
当一个数据字节写入GDDRAM时,列被填充(即,由列地址指针指示的整列(8位)被填充)。数据位D0被写入顶行,而数据位D7被写入底行。
4.4 GDDRAM寻址模式
GDRAM有三种寻址模式,页寻址,水平寻址。垂直寻址。一般我们不修改其寻址模式,使用默认的页寻址,但当有数据更合适其它寻址方式时,可以更换寻址方式。
4.4.1 页寻址模式
页寻址模式是器件默认选择的GDDRAM寻址模式,通过“20H,02H”命令可以设置寻址模式为页寻址。
在页寻址模式下,寻址只在一页(PAGEn)内进行,地址指针不会跳到其它页。每次向GDDRAM写入1字节显示数据后,列指针会自动+1。
当128列都寻址完之后,列指针会重新指向SEG0而页指针仍然保持不变。通过页寻址模式我们可以方便地对一个小区域内数据进行修改。
4.4.2 水平寻址
水平寻址模式可以通过指令“20H,00H”来设置。
水平寻址模式下,每次向GDDRAM写入1字节数据后,列地址指针自动+1。
列指针到达结束列之后会被重置到起始列,而页指针将会+1。
页地址指针达到结束页之后,将会自动重置到起始页。水平寻址模式适用于大面积数据写入,例如一帧画面刷新。
4.4.3 垂直寻址
垂直寻址模式可以通过指令“20H,01H”来设置。
垂直寻址模式下,每次向GDDRAM写入1字节数据之后,页地址指针将会自动+1。
页指针到达结束页之后会被重置到0,而列指针将会+1。
列地址指针达到结束页之后,将会自动重置到起始列。
4.5 常用命令
4.5.1 设置对比度
双字节指令:0x81H + A[7:0]
此命令设置显示器的对比度设置。该芯片具有从0x00H到0xFFh的256个对比度步长。这个段输出电流随着对比度阶跃值的增加而增加。
4.5.2 设置正常/反转显示
单字节指令:0xA6H / 0xA7H (正常/反转)
正常为1亮0灭,反转为1灭0亮。
4.5.3 设置寻址方式
单字节指令:0x20H + A[1:0]
A[1:0]为寻址方式,00为水平寻址,01为垂直寻址,02为页寻址。默认为02。
4.5.4 设置起始/结束列地址(21H)
三字节指令:0x21H + A[6:0] + B[6:0] (起始 + 终止)
A、B为需要设置的起始和结束坐标,最高位为无效位,即最高设置坐标为127。
4.5.5 设置起始/结束页地址
三字节指令:0x22H + A[2:0] + B[2:0](起始 + 终止)
A、B为需要设置的起始和结束坐标,仅低三位有效,即最高设置页为7。
注意:该三字节命令指定显示数据RAM的页起始地址和结束地址,该指令仅在垂直寻址和水平寻址模式下才有效。
4.5.6 设置列地址
单字节指令:0x00H / 0x10H (低/高)+ A[3:0]
设置列地址需要发送两次命令,一次是设置列地址低4位,一次是设置列地址高四位。
A为需要设置列的坐标的低/高四位,(00h~0Fh)。
注意:该指令仅在设置页寻址模式下才有效
4.5.7 设置页地址
单字节指令: 0xB0H + A[3:0]
A为需要设置的页,最高为7。
注意:该指令仅在设置页寻址模式下才有效
4.5.8 设置显示开关
单字节指令:0xAEH / 0xAFH
0xAEH:显示关,0xAF:显示开。
4.5.9 Entire Display ON
4.5.10 Set Display Start Line (40h~7Fh)
单字节指令: 0x40 ~0x7F
该命令通过从0到63选择一个值,设置显示起始行寄存器以确定显示RAM的起始地址。值等于0时,RAM行0映射到COM0。当值等于1时,RAM行1被映射到COM0,依此类推。
4.5.11 设置左右反置
单字节指令: 0xA0 /0xA1
0xA0:正常,0xA1左右反置;
4.5.12 设置上下反置
单字节指令: 0xC0 /0xC8
0xC0:正常,0xC8上下反置;
五、oled示例代码
5.1 GPIO初始化
配置 GPG9、GPG10配置为输出,并且初始化SPI1:
/************************************************************* * * Function : 初始化oled使用到的所有引脚、初始化SPI1 * **************************************************************/ void oled_gpio_init() { /* 1. IO配置 GPG9、GPG10配置为输出 */ GPGCON &= ~((3<<18) | (3<<20)); /* 清零 */ GPGCON |= ((1<<18) | (1<<20)); /* 设置为输出 */ GPGDAT |= ((1<<9) | (1<<10)); /* 2. SPI1初始化 */ spi_init(); } /************************************************************* * * Function : oled复位信号 GPG9引脚 * Input : val 0 复位 1 取消复位 * **************************************************************/ void oled_set_res(char val) { if (val) /* 1 取消复位 */ GPGDAT |= (1<<9); else GPGDAT &= ~(1<<9); /* 1 复位 */ } /************************************************************* * * Function : oled数据/命令信号 GPG10引脚 * Input : val 0 命令 1 数据 * **************************************************************/ void oled_set_dc(u8 val) { if (val) /* 1 数据 */ GPGDAT |= (1<<10); else GPGDAT &= ~(1<<10); /* 0 命令*/ }
5.2 写命令
/************************************************************* * * Function : 通过SPI协议写命令 * Input : cmd 命令 * **************************************************************/ void oled_write_cmd(u8 cmd) { /* 写命令 */ oled_set_dc(0); /* 发送数据 */ spi_write_byte(cmd); /* gpio output default is pull up*/ oled_set_dc(1); }
5.3 写数据
/************************************************************* * * Function : 通过SPI协议写数据 * Input : data 数据 * **************************************************************/ void oled_write_data(u8 data) { /* 写数据 */ oled_set_dc(1); /* 发送数据 */ spi_write_byte(data); /* gpio output default is pull up*/ oled_set_dc(1); }
5.4 设置oled坐标
/************************************************************* * * Function : 坐标设定 * Input : x x坐标 * y y坐标 * **************************************************************/ void oled_pos(u8 x,u8 y) { oled_write_cmd(0xB0+y); oled_write_cmd(((x&0xF0)>>4)|0x10); /* 设置列地址高四位 */ oled_write_cmd(x&0x0F); /* 设置列地址低四位 */ }
这里发送了两个命令一个是设置页地址,另一个是设置列地址。
5.5 清屏
/************************************************************* * * Function : 清屏 * **************************************************************/ void oled_clear() { u8 x; u8 y; for(y=0;y<8;y++) { oled_write_cmd(0xB0+y); /* 选择页 */ oled_write_cmd(0x00); /* 设置列地址低四位 */ oled_write_cmd(0x10); /* 设置列地址高四位 */ for(x=0;x<0x80;x++) oled_write_data(0x00); /* 每次清1列 */ } }
5.6 oled初始化
/*************************************************************************************************** * * Function : oled初始化 * **************************************************************************************************/ void oled_init() { /* 复位 */ oled_set_res(0); delay_ms(50); oled_set_res(1); oled_write_cmd(0xAE); /* 显示关 */ oled_write_cmd(0x00); /* 设置列低位地址 */ oled_write_cmd(0x10); /* 设置列高位地址 */ oled_write_cmd(0x40); /* set start line address Set Mapping RAM Display Start Line (0x00~0x3F) */ oled_write_cmd(0X81); /* 设置对比度 */ oled_write_cmd(0xCF); /* 值越大 越亮 */ oled_write_cmd(0xA1); /* 设置列左右反置 0xa0左右反置 0xa1正常 */ oled_write_cmd(0xC8); /* 设置行上下反置 0xc0上下反置 0xc8正常 */ oled_write_cmd(0xA6); /* 设置正常显示 */ oled_write_cmd(0x20); /* 设置页地址模式 (0x00/0x01/0x02) */ oled_write_cmd(0x02); /* 页寻址 */ oled_write_cmd(0x8D); /* 设置电荷磊开关 */ oled_write_cmd(0x14); /* 电荷磊开 */ oled_write_cmd(0xA4); /* 字符显示开关 0xA4:开 0xA5:关 */ oled_write_cmd(0xA6); /* 背景色显示开关 0xA6:关 0xA7:开 */ oled_write_cmd(0xAF); /* 显示开 */ oled_clear(); /* 初始清屏 */ oled_pos(0,0); }
5.7 显示字符
关于字符的显示这里不重复介绍了,具体可以查看Mini2440裸机开发之LCD编程(GB2312、ASCII字库制作)。
5.7.1 一个ASCII字符8行6列
/*************************************************************************************************** * * Function : 写入一组标准ASCII字符串 一个字节占8行6列 * Input : x 设置列地址0~0X7F * y 设置页地址0~7 * str 要显示的字符 * **************************************************************************************************/ void oled_p6x8str(u8 x,u8 y,u8 *str) { u8 i=0; u8 j=0; u8 k=0; while (str[j]!='