第20课 SPI协议详解及裸机程序开发分析

2020-03-24来源: eefocus关键字:SPI  协议详解  裸机程序

第001节_SPI协议介绍

市面上的开发板很少接有SPI设备,但是SPI协议在工作中经常用到。我们开发了SPI模块,上面有SPI Flash和SPI OLED。OLED就是一块显示器。


我们裸板程序会涉及两部分:


用GPIO模拟SPI

用S3C2440的SPI控制器

我们先介绍下SPI协议,硬件框架如下:

这里写图片描述

SCK:提供时钟

DO:作为数据输出

DI:作为数据输入

CS0/CS1:作为片选

同一时刻只能有一个SPI设备处于工作状态。


假设现在2440传输一个0x56数据给SPI Flash,时序如下:

这里写图片描述

首先CS0先拉低选中SPI Flash,0x56的二进制就是0b0101 0110,因此在每个SCK时钟周期,DO输出对应的电平。 

SPI Flash会在每个时钟周期的上升沿读取D0上的电平。


在SPI协议中,有两个值来确定SPI的模式。 

CPOL:表示SPICLK的初始电平,0为电平,1为高电平 

CPHA:表示相位,即第一个还是第二个时钟沿采样数据,0为第一个时钟沿,1为第二个时钟沿

image.png

我们常用的是模式0和模式3,因为它们都是在上升沿采样数据,不用去在乎时钟的初始电平是什么,只要在上升沿采集数据就行。

极性选什么?格式选什么?通常去参考外接的模块的芯片手册。比如对于OLED,查看它的芯片手册时序部分:
这里写图片描述

SCLK的初始电平我们并不需要关心,只要保证在上升沿采样数据就行。


第002节_使用GPIO实现SPI协议操作OLED

现在开始写代码,使用GPIO实现SPI协议操作。


我们现在想要操作OLED,通过三条线(SCK、DO、CS)与OLED相连,这里没有DI是因为2440只会向OLED传数据而不用接收数据。


我们要用GPIO来实现SOC向OLED写数据,这一层用gpio_spi.c来实现,负责发送数据。


对于OLED,有专门的指令和数据格式,要传输的数据内容,在oled.c这一层来实现,负责组织数据。


因此,我们需要实现以上两个文件。

这里写图片描述

需要实现的函数:先SPI初始化SPIInt(),再初始化OLEDOLEDInit(),最后再显示OLEDPrint()。


新建一个gpio_spi.c文件,实现SPI初始化SPIInt()


void SPIInit(void)

{

    /* 初始化引脚 */

    SPI_GPIO_Init();

}


再具体实现SPI_GPIO_Init()。这里使用GPIO实现SPI协议,电路图如下:

这里写图片描述

 GPF1作为OLED片选引脚,设置为输出;

 GPG2作为FLASH片选引脚,设置为输出;

 GPG4作为OLED的数据(Data)/命令(Command)选择引脚,设置为输出;

 GPG5作为SPI的MISO,设置为输入;

 GPG6作为SPI的MOSI,设置为输出;

 GPG7作为SPI的时钟CLK,设置为输出;


/* 用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文件,以实现初始化OLEDOLEDInit()


void OLEDInit(void)

{

/* 向OLED发命令以初始化 */

}


查阅OLED数据手册SPEC UG-2864TMBEG01.pdf可以得知其初始化流程和参考的初始化代码:


void OLEDInit(void)

{

    /* 向OLED发命令以初始化 */

    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); 

}


因此我们还要先实现OLEDWriteCmd()函数,对于OLED,除了SPI的片选、时钟、数据引脚,还有一个数据/命令切换引脚。

这里写图片描述

这里的D/C即数据(Data)/命令(Command)选择引脚,它为高电平时,OLED即认为收到的是数据;它为低电平时,OLED即认为收到的是命令。


对于OLED,命令由开启/关闭显示、背光亮度等,具体有什么命令,可以查阅OLED的主控芯片手册SSD1306-Revision 1.1 (Charge Pump).pdf,在9 COMMAND TABLE 有相关命令的介绍。


因此,在编写OLEDWriteCmd()时,需要先设置为命令模式:


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); /*  */

}


即:先设置为命令模式,再片选OLED,再传输命令,再恢复成原来的模式和取消片选。


片选函数和模式切换函数都比较简单,设置为对应的高低电平即可:


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()函数,它属于SPI协议,放在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;

    }


}


发送数据要满足SPI的时序要求,参考前面的介绍:

这里写图片描述

先设置CLK为低,然后数据引脚输出数据的最高位,然后CLK为高,在CLK这个上升沿中,OLED就读取了一位数据。接着左移一位,将原来的第7位移动到了第8位,重复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则灭。


每一个字节数据Datax控制每列8个像素,在显存里面存放Data数据。


之后所需的操作就是把数据写到显存里面去,如何写到显存可以拆分成两个问题:


①怎么发地址 

②怎么发数据


OLED主控的手册里介绍了三种地址模式,我们常用的是页地址模式(Page addressing mode (A[1:0]=10xb)),它把显存的64行分为8页,每页对应8行;选中某页后,再选择某列,然后就可以往里面写数据了,每写一个数据,地址就会加1,一直写到最右端的位置,他会自动跳到最左端。


通过命令来实现发送页地址和列地址,其中列地址分为两次发送,先发送低字节,再发送高字节。


假设每个字符数据大小为8x16,假如第一个字符位置为(page,col),相邻的右边就是(page,col+8),写满一行跳至下一行的坐标就是(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++;

    }

}


只要字符数组str[i]有数据,就调用OLEDPutChar(page, col, str[i])在指定位置显示第一个字符,然后位置向右移动一个字符的大小,如果遇到行尾,再进行换行,就这样依次显示完所有字符。


现在开始实现最重要的OLEDPutChar()函数。把一个字符在OLED上显示出来需要以下几个步骤:


a.得到字模


b.发给OLED


字模我们可以从网上搜索相关资料获取到,将字模的数组oled_asc2_8x16[95][16]放在oledfont.c里面,字符从空格开始,因此每次减去一个空格才是我们想要的字符。


如图所示一个字符,先以(page, col)为起点,显示8位数据,再换行,以(page+1, col)为起点显示8位数据。

这里写图片描述

/* page: 0-7

 * col : 0-127

 * 字符: 8x16象素

 */

void OLEDPutChar(int page, int col, char c)

{

    int i = 0;

    /* 得到字模 */

const unsigned

[1] [2] [3] [4]
关键字:SPI  协议详解  裸机程序 编辑:什么鱼 引用地址:http://news.eeworld.com.cn/mcu/ic492478.html 本网站转载的所有的文章、图片、音频视频文件等资料的版权归版权所有人所有,本站采用的非本站原创文章及图片等内容无法一一联系确认版权者。如果本网所选内容的文章作者及编辑认为其作品不宜公开自由传播,或不应无偿使用,请及时通过电子邮件或电话通知我们,以迅速采取适当措施,避免给双方造成不必要的经济损失。

上一篇:第019课 I2C协议详解及裸机程序分析
下一篇:arm的PWM模块脉宽调制及超声波系统设计

关注eeworld公众号 快捷获取更多信息
关注eeworld公众号
快捷获取更多信息
关注eeworld服务号 享受更多官方福利
关注eeworld服务号
享受更多官方福利

推荐阅读

STM32开发笔记84: SX1268驱动程序设计(SPI总线)
单片机型号:STM32L053R8T6本系列开发日志,将详述SX1268驱动程序的整个设计过程,本篇介绍SPI总线驱动程序。一、数据手册1、关键点:全双工SPICPOL=0,CPHA=0从器件写操作:地址字节+数据字节读操作:直接发送地址字节,就可返回一个数据字节NSS在整个帧传输过程保持低电平MISO在NSS为高时为高阻态SCK最大时钟16M2、几个时序图,t10是指从睡眠状态唤醒,NSS下降沿到SCK上升沿的时间,数据手册表明最短时间为100us。3、芯片离开sleep模式的方法可以通过NSS的下降沿使得芯片从sleep模式唤醒。下降沿发生后,芯片内部的稳压器都将打开。芯片开始初始化,然后具备接收第1个SPI命令的能力
发表于 2020-03-08
STM32开发笔记84: SX1268驱动程序设计(SPI总线)
技术文章—轻松实现隔离式SPI通信
监测和控制不同的系统需要能够直接访问传感器和驱动器,最好是从一个中心位置,采用标准化通信方法(例如串行外设接口(SPI))进行访问。SPI是一种同步串行数据总线,帮助设备和中央控制单元之间进行长距离的数据交换。通信操作遵从主从原则,是全双工的。SPI接口包含三行:SDI、SDO和SCK。 SPI通信方法适用的线缆距离不超过10米,通信距离更长时,通常需要用到中继器,这是因为随着线缆增长,其线缆阻抗相应增加,由此导致信号衰减。然后必须再次放大信号。与此同时,线路会获得更高的信噪比(SNR)。可利用ADI公司提供的isoSPI通信接口IC LTC6820 等器件来读取这些信号。 得益于该器件的创新式设计,可以使用
发表于 2020-03-06
技术文章—轻松实现隔离式SPI通信
STM8用SPI交换1Byte数据
发表于 2020-03-05
STM8用SPI交换1Byte数据
ATmega168的SPI发送完寄存器SPIF不置位的问题
利用ATmega168的硬件SPI驱动74HC595来扩展串行接口。把MOSI和SCK设置为输出,然后设置好寄存器。,具体如下:static void vSpi595Init(void){DDRB|=(1<DDRD|=(1<SPCR=(1<<spe)|(1<<mstr)|(1<<spr0)|(1<<spr1); 使能spi主机模式传送速度。<="" p="">}然后调用如下的发送函数static void vSPIMasterTransmit(unsigned char ucData){SS_L();//拉低存储寄存器
发表于 2020-03-04
stm8 io口 spi模拟,可用于RC522
///////////////////////////////////////////////////////////////////////功    能:SPI写数据//输    入: 无// 无返回值///////////////////////////////////////////////////////////////////// void Write_SPI(unsigned char num)    {  unsigned char count=0;     for(count
发表于 2020-03-02
浅谈STM8(3)——SPI通信
STM8的SPI还是挺好用的,也挺简单,起码相比于I2C来说是的。最近因为要调试NRF905,所以就用到了STM8的SPI。因为调试过程中没有遇到什么大的问题,所以在此只对STM8的SPI作简单的介绍。博主只使用了STM8作为主设备的双线单向数据模式,并且没有使用CRC。在使用STM8的SPI时首先需要确认——1.主从关系,并且确认是否使用STM8上的NSS引脚作为主从判据2.SPI时序,四种有差异的时序3.串行数据是MSB在前还是LSB在前以上条件很好地弄清楚后,便可以配置寄存器了。因为主模式下数据收发全由STM8自身控制,所以不需要使用中断。另外端口也无需设置,使用默认状态就可以。需要配置的寄存器只有SPI
发表于 2020-03-02
何立民专栏 单片机及嵌入式宝典

北京航空航天大学教授,20余年来致力于单片机与嵌入式系统推广工作。

电子工程世界版权所有 京ICP证060456号 京ICP备10001474号 电信业务审批[2006]字第258号函 京公海网安备110108001534 Copyright © 2005-2020 EEWORLD.com.cn, Inc. All rights reserved