版權(quán)聲明:本文為小斑馬學(xué)習(xí)總結(jié)文章,技術(shù)來源于韋東山著作砌们,轉(zhuǎn)載請注明出處杆麸!
一、SPI協(xié)議介紹
市面上的開發(fā)板很少接有SPI設(shè)備浪感,但是SPI協(xié)議在工作中經(jīng)常用到昔头。開發(fā)SPI模塊,上面有SPI Flash和SPI OLED影兽。OLED就是一塊顯示器揭斧。
裸板程序會涉及兩部分:
- 用GPIO模擬SPI
- 用S3C2440的SPI控制器
- SCK:提供時鐘
- DO:作為數(shù)據(jù)輸出
- DI:作為數(shù)據(jù)輸入
- CS0/CS1:作為片選
同一時刻只能有一個SPI設(shè)備處于工作狀態(tài)。
SPI Flash會在每個時鐘周期的上升沿讀取D0上的電平。
在SPI協(xié)議中成艘,有兩個值來確定SPI的模式赏半。
CPOL:表示SPICLK的初始電平,0為電平淆两,1為高電平
CPHA:表示相位断箫,即第一個還是第二個時鐘沿采樣數(shù)據(jù),0為第一個時鐘沿秋冰,1為第二個時鐘沿
CPOL | CPHA | 模式 | 含義 |
---|---|---|---|
0 | 0 | 0 | 初始電平為低電平瑰枫,在第一個時鐘沿采樣數(shù)據(jù) |
0 | 1 | 1 | 初始電平為低電平,在第二個時鐘沿采樣數(shù)據(jù) |
1 | 0 | 2 | 初始電平為高電平丹莲,在第一個時鐘沿采樣數(shù)據(jù) |
1 | 1 | 3 | 初始電平為高電平光坝,在第二個時鐘沿采樣數(shù)據(jù) |
常用的是模式0和模式3,因為它們都是在上升沿采樣數(shù)據(jù)甥材,不用去在乎時鐘的初始電平是什么盯另,只要在上升沿采集數(shù)據(jù)就行。
極性選什么洲赵?格式選什么鸳惯?通常去參考外接的模塊的芯片手冊。比如對于OLED叠萍,查看它的芯片手冊時序部分:
二苛谷、使用GPIO實現(xiàn)SPI協(xié)議操作OLED
現(xiàn)在開始寫代碼辅鲸,使用GPIO實現(xiàn)SPI協(xié)議操作。
現(xiàn)在想要操作OLED腹殿,通過三條線(SCK独悴、DO、CS)與OLED相連锣尉,這里沒有DI是因為2440只會向OLED傳數(shù)據(jù)而不用接收數(shù)據(jù)刻炒。
要用GPIO來實現(xiàn)SOC向OLED寫數(shù)據(jù),這一層用gpio_spi.c來實現(xiàn)自沧,負(fù)責(zé)發(fā)送數(shù)據(jù)坟奥。
對于OLED,有專門的指令和數(shù)據(jù)格式,要傳輸?shù)臄?shù)據(jù)內(nèi)容爱谁,在oled.c這一層來實現(xiàn)晒喷,負(fù)責(zé)組織數(shù)據(jù)。
新建一個gpio_spi.c文件雨效,實現(xiàn)SPI初始化SPIInt()
void SPIInit(void)
{
/* 初始化引腳 */
SPI_GPIO_Init();
}
再具體實現(xiàn)SPI_GPIO_Init()迅涮。這里使用GPIO實現(xiàn)SPI協(xié)議,電路圖如下:GPF1作為OLED片選引腳徽龟,設(shè)置為輸出叮姑;
GPG2作為FLASH片選引腳,設(shè)置為輸出据悔;
GPG4作為OLED的數(shù)據(jù)(Data)/命令(Command)選擇引腳传透,設(shè)置為輸出;
GPG5作為SPI的MISO极颓,設(shè)置為輸入朱盐;
GPG6作為SPI的MOSI,設(shè)置為輸出菠隆;
GPG7作為SPI的時鐘CLK兵琳,設(shè)置為輸出;
/* 用GPIO模擬SPI */
static void SPI_GPIO_Init(void) {
/* GPF1 OLED_CSn output */
GPFCON &= ~(3<<(1*2));
GPFCON |= (1<<(1*2));
GPFDAT |= (1<<1);
/* GPG2 FLASH_CSn output
* GPG4 OLED_DC output
* GPG5 SPIMISO input
* GPG6 SPIMOSI output
* GPG7 SPICLK output
*/
GPGCON &= ~((3<<(2*2)) | (3<<(4*2)) | (3<<(5*2)) | (3<<(6*2)) | (3<<(7*2)));
GPGCON |= ((1<<(2*2)) | (1<<(4*2)) | (1<<(6*2)) | (1<<(7*2)));
GPGDAT |= (1<<2);
}
再新建一個oled.c文件骇径,以實現(xiàn)初始化OLEDOLEDInit()
void OLEDInit(void)
{
/* 向OLED發(fā)命令以初始化 */
}
查閱OLED數(shù)據(jù)手冊SPEC UG-2864TMBEG01.pdf可以得知其初始化流程和參考的初始化代碼:
void OLEDInit(void)
{
/* 向OLED發(fā)命令以初始化 */
OLEDWriteCmd(0xAE); /*display off*/
OLEDWriteCmd(0x00); /*set lower column address*/
OLEDWriteCmd(0x10); /*set higher column address*/
OLEDWriteCmd(0x40); /*set display start line*/
OLEDWriteCmd(0xB0); /*set page address*/
OLEDWriteCmd(0x81); /*contract control*/
OLEDWriteCmd(0x66); /*128*/
OLEDWriteCmd(0xA1); /*set segment remap*/
OLEDWriteCmd(0xA6); /*normal / reverse*/
OLEDWriteCmd(0xA8); /*multiplex ratio*/
OLEDWriteCmd(0x3F); /*duty = 1/64*/
OLEDWriteCmd(0xC8); /*Com scan direction*/
OLEDWriteCmd(0xD3); /*set display offset*/
OLEDWriteCmd(0x00);
OLEDWriteCmd(0xD5); /*set osc division*/
OLEDWriteCmd(0x80);
OLEDWriteCmd(0xD9); /*set pre-charge period*/
OLEDWriteCmd(0x1f);
OLEDWriteCmd(0xDA); /*set COM pins*/
OLEDWriteCmd(0x12);
OLEDWriteCmd(0xdb); /*set vcomh*/
OLEDWriteCmd(0x30);
OLEDWriteCmd(0x8d); /*set charge pump enable*/
OLEDWriteCmd(0x14);
}
因此我們還要先實現(xiàn)OLEDWriteCmd()函數(shù)躯肌,對于OLED,除了SPI的片選破衔、時鐘清女、數(shù)據(jù)引腳,還有一個數(shù)據(jù)/命令切換引腳晰筛。
對于OLED卦方,命令由開啟/關(guān)閉顯示羊瘩、背光亮度等,具體有什么命令,可以查閱OLED的主控芯片手冊SSD1306-Revision 1.1 (Charge Pump).pdf尘吗,在9 COMMAND TABLE 有相關(guān)命令的介紹逝她。
因此,在編寫OLEDWriteCmd()時睬捶,需要先設(shè)置為命令模式
static void OLEDWriteCmd(unsigned char cmd)
{
OLED_Set_DC(0); /* command */
OLED_Set_CS(0); /* select OLED */
SPISendByte(cmd);
OLED_Set_CS(1); /* de-select OLED */
OLED_Set_DC(1); /* */
}
即:先設(shè)置為命令模式黔宛,再片選OLED,再傳輸命令擒贸,再恢復(fù)成原來的模式和取消片選臀晃。
片選函數(shù)和模式切換函數(shù)都比較簡單,設(shè)置為對應(yīng)的高低電平即可:
static void OLED_Set_DC(char val)
{
if (val)
GPGDAT |= (1<<4);
else
GPGDAT &= ~(1<<4);
}
static void OLED_Set_CS(char val)
{
if (val)
GPFDAT |= (1<<1);
else
GPFDAT &= ~(1<<1);
}
還剩下SPISendByte()函數(shù)介劫,它屬于SPI協(xié)議徽惋,放在gpio_spi.c里面:
void SPISendByte(unsigned char val)
{
int i;
for (i = 0; i < 8; i++)
{
SPI_Set_CLK(0);
SPI_Set_DO(val & 0x80);
SPI_Set_CLK(1);
val <<= 1;
}
}
發(fā)送數(shù)據(jù)要滿足SPI的時序要求,參考前面的介紹:先設(shè)置CLK為低座韵,然后數(shù)據(jù)引腳輸出數(shù)據(jù)的最高位险绘,然后CLK為高,在CLK這個上升沿中,OLED就讀取了一位數(shù)據(jù)誉碴。接著左移一位宦棺,將原來的第7位移動到了第8位,重復(fù)8次黔帕,傳輸完成代咸。
再完成SPI_Set_CLK()和SPI_Set_DO():
static void SPI_Set_CLK(char val)
{
if (val)
GPGDAT |= (1<<7);
else
GPGDAT &= ~(1<<7);
}
static void SPI_Set_DO(char val)
{
if (val)
GPGDAT |= (1<<6);
else
GPGDAT &= ~(1<<6);
}
至此,SPI初始化和OLED初始化就基本完成了蹬屹,接下來就是OLED顯示部分侣背。
先了解一下OLED顯示的原理:
OLED長有128個像素,寬有64個像素慨默,每個像素用一位來表示贩耐,為1則亮,為0則滅厦取。
每一個字節(jié)數(shù)據(jù)Datax控制每列8個像素潮太,在顯存里面存放Data數(shù)據(jù)。
之后所需的操作就是把數(shù)據(jù)寫到顯存里面去虾攻,如何寫到顯存可以拆分成兩個問題:
- ①怎么發(fā)地址
- ②怎么發(fā)數(shù)據(jù)
OLED主控的手冊里介紹了三種地址模式铡买,我們常用的是頁地址模式(Page addressing mode (A[1:0]=10xb)),它把顯存的64行分為8頁霎箍,每頁對應(yīng)8行奇钞;選中某頁后,再選擇某列漂坏,然后就可以往里面寫數(shù)據(jù)了景埃,每寫一個數(shù)據(jù)媒至,地址就會加1,一直寫到最右端的位置谷徙,他會自動跳到最左端拒啰。
通過命令來實現(xiàn)發(fā)送頁地址和列地址,其中列地址分為兩次發(fā)送完慧,先發(fā)送低字節(jié)谋旦,再發(fā)送高字節(jié)。
假設(shè)每個字符數(shù)據(jù)大小為8x16屈尼,假如第一個字符位置為(page,col)册着,相鄰的右邊就是(page,col+8),寫滿一行跳至下一行的坐標(biāo)就是(page+2,col)鸿染。
/* page: 0-7
* col : 0-127
* 字符: 8x16象素
*/
void OLEDPrint(int page, int col, char *str)
{
int i = 0;
while (str[i])
{
OLEDPutChar(page, col, str[i]);
col += 8;
if (col > 127)
{
col = 0;
page += 2;
}
i++;
}
}
只要字符數(shù)組str[i]有數(shù)據(jù)指蚜,就調(diào)用OLEDPutChar(page, col, str[i])在指定位置顯示第一個字符乞巧,然后位置向右移動一個字符的大小涨椒,如果遇到行尾,再進(jìn)行換行绽媒,就這樣依次顯示完所有字符蚕冬。
現(xiàn)在開始實現(xiàn)最重要的OLEDPutChar()函數(shù)。把一個字符在OLED上顯示出來需要以下幾個步驟:
- a. 得到字模
- b. 發(fā)給OLED
字模我們可以從網(wǎng)上搜索相關(guān)資料獲取到是辕,將字模的數(shù)組oled_asc2_8x16[95][16]放在oledfont.c里面囤热,字符從空格開始,因此每次減去一個空格才是我們想要的字符获三。
/* page: 0-7
* col : 0-127
* 字符: 8x16象素
*/
void OLEDPutChar(int page, int col, char c)
{
int i = 0;
/* 得到字模 */
const unsigned char *dots = oled_asc2_8x16[c - ' '];
/* 發(fā)給OLED */
OLEDSetPos(page, col);
/* 發(fā)出8字節(jié)數(shù)據(jù) */
for (i = 0; i < 8; i++)
OLEDWriteDat(dots[i]);
OLEDSetPos(page+1, col);
/* 發(fā)出8字節(jié)數(shù)據(jù) */
for (i = 0; i < 8; i++)
OLEDWriteDat(dots[i+8]);
}
顯示一個字符贞谓,就先獲取字模數(shù)據(jù)限佩,接著發(fā)出8字節(jié)數(shù)據(jù),再換行發(fā)出8字節(jié)數(shù)裸弦。
再設(shè)置列:
static void OLEDSetPos(int page, int col)
{
OLEDWriteCmd(0xB0 + page); /* page address */
OLEDWriteCmd(col & 0xf); /* Lower Column Start Address */
OLEDWriteCmd(0x10 + (col >> 4)); /* Lower Higher Start Address */
}
前面提到了OLED主控有三種地址模式砖顷,我們常用的是頁地址模式(Page addressing mode (A[1:0]=10xb))暇矫,雖然這是默認(rèn)的摸索,但還是設(shè)置一下比較好:即先發(fā)送0x20择吊,再設(shè)置A[1:0]=10:
static void OLEDSetPageAddrMode(void)
{
OLEDWriteCmd(0x20);
OLEDWriteCmd(0x02);
}
在顯示中李根,一般都需一個清屏函數(shù)來清空當(dāng)前可能顯示的數(shù)據(jù)。清屏函數(shù)比較簡單几睛,往所有位置里面寫0即可:
static void OLEDClear(void)
{
int page, i;
for (page = 0; page < 8; page ++)
{
OLEDSetPos(page, 0);
for (i = 0; i < 128; i++)
OLEDWriteDat(0);
}
}
再把地址模式OLEDSetPageAddrMode()和清屏函數(shù)OLEDClear()放在SPI_GPIO_Init()里房轿,在Makefile加上gpio_spi.o和oled.o。
最后在主函數(shù)里加上初始化和顯示函數(shù):
三所森、SPI_FLASH編程_讀ID
這節(jié)講解如何使用SPI操作Flash囱持,代碼上進(jìn)行修改,添加一個文件 spi_flash.c 和其頭文件 spi_flash.h 焕济。
先做一個最簡單的spi操作纷妆,讀取Flash的ID, SPIFlashID() 晴弃。
Flash的ID有廠家ID和設(shè)備ID掩幢,分別用pMID和pDID來保存。
void SPIFlashReadID(int *pMID, int *pDID)
{
SPIFlash_Set_CS(0); /* 選中SPI FLASH */
SPISendByte(0x90);
SPIFlashSendAddr(0);
*pMID = SPIRecvByte();
*pDID = SPIRecvByte();
SPIFlash_Set_CS(1);
}
把其中的發(fā)送24地址封裝成了一個函數(shù) SPIFlashSendAddr() :
static void SPIFlashSendAddr(unsigned int addr)
{
SPISendByte(addr >> 16);
SPISendByte(addr >> 8);
SPISendByte(addr & 0xff);
}
依次完成上面的子函數(shù),先是SPI片選谴咸,上一節(jié)的原理圖可以看到SPI Flash的片選是GPG2:
static void SPIFlash_Set_CS(char val)
{
if (val)
GPGDAT |= (1<<2);
else
GPGDAT &= ~(1<<2);
}
SPISendByte() 和前面OLED的是一樣的轮听,就不用寫了,因此就只剩下 SPIRecvByte() 岭佳,放在 gpio_spi.c 里面實現(xiàn):
unsigned char SPIRecvByte(void)
{
int i;
unsigned char val = 0;
for (i = 0; i < 8; i++)
{
val <<= 1;
SPI_Set_CLK(0);
if (SPI_Get_DI())
val |= 1;
SPI_Set_CLK(1);
}
return val;
}
在每個時鐘周期讀取DI引腳上的值血巍,對于SOC就是MISO引腳:
static char SPI_Get_DI(void)
{
if (GPGDAT & (1<<5))
return 1;
else
return 0;
}
至此,讀取Flash的ID基本實現(xiàn)驼唱,最后在主函數(shù)里調(diào)用打印藻茂,分別在串口和OLED上顯示:
SPIFlashReadID(&mid, &pid);
printf("SPI Flash : MID = 0x%02x, PID = 0x%02x\n\r", mid, pid);
sprintf(str, "SPI : %02x, %02x", mid, pid);
OLEDPrint(4,0,str);