当前位置: 代码迷 >> 综合 >> 通信SPI、UART、I2C
  详细解决方案

通信SPI、UART、I2C

热度:92   发布时间:2023-11-24 05:45:19.0

通信协议

文章目录

  • 通信协议
    • 1.SPI
      • 1.四条数据线的介绍:
      • 2.数据传输:
      • 3.时钟极性和时钟相位
      • 4.优缺点:
      • 5.代码讲解SPI:
    • 2.I2C
      • 1.一般操作:
      • 2.开始和结束条件:
      • 3.优缺点:
      • 4.代码讲解I2C:
    • 3.UART通信
      • 1.数据格式:
      • 2.优缺点:
      • 3.代码讲解UART

1.SPI

? ??SPI(Serial Peripheral Interface,串行外设接口),是Motorola公司提出的一种同步串行数据传输标准,主要应用在 EEPROM,FLASH,实时时钟,AD转换器,还有数字信号处理器和数字信号解码器之间。

首先讲讲同步的概念,

在这里插入图片描述

? ?? 上图中,左边的为主机(Master),右边的为从机(Slave)。SPI接口经常被称为4线串行总线,以主从方式输出,正如上图中,主、从机由四条数据线相连。

? ?? 同步是指:发送方发出数据后,等接收方发回响应以后才发下一个数据包的通讯方式。

1.四条数据线的介绍:

(1).SCLK为串行时钟,用来同步数据传输,由主机输出;

(2).MOSI为主机输出从机输入数据线,通常优先传输MSB;

(3).MISO为主机输入从机输出数据线,通常优先传输LSB;

(4).SS为片选线,低电平有效,由主机输出,简而言之是通过选定片选线来选定从机。

2.数据传输:

在这里插入图片描述

? ??正如上图中,主机通过MOSI线发送1位数据给从机接收,从机则通过MISO发送一位数据给主机接收(通过移位寄存器实现),主、从二者形成循环,当寄存器中的内容全部移除时,相当于完成了两个移位寄存器之间内容的交换。

3.时钟极性和时钟相位

? ?? 时钟极性(CPOL或UCCKPL),时钟相位(CPHA或UCCKPH)。

时钟极性:时钟空闲时所处的极性。

时钟相位:设置读取数据和发送数据的时钟沿(上升、下降沿同时也)。

4.优缺点:

优:

? ??(1).支持全双工(即主、从数据可同时进行数据传输,互不干扰),且未定义速度限制,一般的实现通常能达到甚至超过10 Mbps。

? ??(2).操作相对简单

? ??(3).数据的传输效率较高

缺:

??? (1).需要占用主机较多的I/O口线,没个从机都需要一根。

? ??(2).只支持单个主机

5.代码讲解SPI:

主机:

SPI主模式:
?? 当寄存器 UxBUF 写入字节后,SPI 主模式字节传送就开始了。USART 使用波特率发生器生
成 SCK 串行时钟,而且传送发送寄存器提供的字节到输出引脚 MOSI。与此同时,接收寄
存器从输入引脚 MISO 获取收到的字节。

#include <ioCC2540.h>
#include "hal_cc8051.h"#define LED1 P1_0
unsigned char temp = 0;  // 数据收发缓存void SPI_Master_Init()
{
    CLKCONCMD = 0x80;while (CLKCONSTA != 0x80);      // 系统时钟配置为32MHz// SPI主机模式配置PERCFG |= 0x02;               // 使用USART1的I/O的备用位置2// P1_4: SSN, P1_5: SCK, P1_6: MOSI, P1_7: MISO P1SEL |= 0xE0;                // 配置P1_5、P1_6、P1_7为外设功能 P1SEL &= ~0x10;               // 配置P1_4为通用I/O口(SSN)P1DIR |= 0x10;                // 配置SSN引脚为输出引脚U1BAUD = 0x00; U1GCR |= 0x11; // 配置波特率4MHzU1CSR &= ~0xA0;               // 配置为SPI模式且为SPI主机U1GCR &= ~0xC0;               //空闲时SCLK处于低电平、上升沿数据接受、下降沿数据发送U1GCR |= 0x20;                // MSB(高字节)先传送
}void SPI_Master_Receive()
{
    P1_4 = 0;              // SSN下降沿,SPI从机活跃,开始收发数据U1DBUF = 0x33;         // 向数据缓存寄存器发送数据while(!(U1CSR&0x02));  // 等待数据传送完成,即发送完成标志位置1U1CSR &= 0xFD;         // 清除发送完成标志位,即发送完成标志位置0temp = U1DBUF;         // 从数据缓存寄存器接受数据P1_4 = 1;              // SSN上升沿,SPI从机不活跃,不接收数据
}void P1_Init()
{
    P1DIR |= 0x01;LED1 = 0;
}void main()
{
    SPI_Master_Init();P1_Init();for(;;) {
    SPI_Master_Receive();if( temp == 0x11 ){
    LED1 = 1;}elseLED1 = 0;halMcuWaitMs(300);}}

SPI从模式:(上升沿还是下降沿触发可编程控制)
SSN 的下降沿,SPI 从模式活跃,在 MOSI 输入上接收数据,在 MOSI 输出上输出数据。
SSN 的上升沿,SPI 从模式不活跃,不接收数据。

#include <ioCC2530.h>#define LED2 P1_1unsigned char temp=0;   // 数据接受缓存void SPI_Slave_Init()
{
    CLKCONCMD = 0x80; while(CLKCONSTA != 0x80);  // 系统时钟配置为32MHz // SPI从机模式配置PERCFG |= 0x02;                // 使用USART1的I/O的备用位置2// P1_4: SSN、P1_5: SCK、P1_6: MOSI、P1_7: MISOP1SEL |= 0xF0;                 // 配置P1_4、P1_5、P1_6、P1_7为外设功能U1BAUD = 0x00; U1GCR |= 0x11;  // 配置波特率4MHzU1CSR &= ~0x80; U1CSR |= 0x20; // 配置为SPI模式且为SPI从机U1GCR &= ~0xC0;                //空闲时SCLK处于低电平、上升沿数据接受、下降沿数据发送U1GCR |= 0x20;                 // MSB(高字节)先传送
}void SPI_Slave_Receive()
{
    while (!(U1CSR&0x04)); // 等待数据接受完成(即接受完成标志置1)U1CSR &= 0xFB;         // 清除接受完成标志(即接受完成标志置0)temp = U1DBUF;         // 从数据缓存寄存器读取数据,赋值给temp
}void P1_Init()
{
    P1DIR |= 0x02;LED2 = 0;
}void main()
{
    P1_Init();SPI_Slave_Init();for(;;) {
    U1DBUF = 0x11;SPI_Slave_Receive();if( temp == 0x33 ){
    LED2 = 1;}elseLED2 = 0;}
}

2.I2C

? ??I2C包括时钟线(SCL)和数据线(SDA)。这两条线都是漏极开路或者集电极开路结构,使用时需要外加上拉电阻,可以挂载多个设备(如下图),每个设备都有属于自己的地址,主机通过选择不同的地址来选择不同的设备。

(此处因为博主对模、数电的知识还未彻底掌握,所以就不介绍漏极开路和集电极开路了,总而言之,开漏输出只能输出低,或者关闭输出,因此开漏输出总是要配一个上拉电阻使用。)

在这里插入图片描述

1.一般操作:

? 主机给从机发送数据

? ??1.发送开始条件START和从机地址(地址的8位传送完毕后,成功配置地址的Slave设备必须发送“ACK”。否则否则一定时间之后Master视为超时,将放弃数据传送,发送“Stop”。);

? ?? 2.发送数据(当写数据的时候,Master每发送完8个数据位,Slave设备如果还有空间接受下一个字节应该回答“ACK”,Slave设备如果没有空间接受更多的字节应该回答“NACK”,Master当收到“NACK”或者一定时间之后没收到任何数据将视为超时,此时Master放弃数据传送,发送“Stop”。);

? ?? 3.发送停止条件STOP结束。

? 主机从从机读取数据

? ?? 1.发送开始条件START和从机地址(地址的8位传送完毕后,成功配置地址的Slave设备必须发送“ACK”。否则否则一定时间之后Master视为超时,将放弃数据传送,发送“Stop”。));

? ?? 2.发送要读取的地址(当读数据的时候,Slave设备每发送完8个数据位,如果Master希望继续读下一个字节,Master应该回答“ACK”以提示Slave准备下一个数据,如果Master不希望读取更多字节,Master应该回答“NACK”以提示Slave设备准备接收Stop信号。);

? ?? 3.读取数据;

? ??4.发送停止条件STOP结束。

2.开始和结束条件:

? ??当SCL保持为高电平时,SDA从高电平变成低电平,即为START。

? ??当SCL保持为低电平时,SDA从低电平变成高电平,即为STOP。

? ??当读取数据时,发送完发送开始条件START和从机地址后,不发送STOP,则可以重复开始读取数据。 数据传输时先传MSB。接收者在每个字节后的第9个时钟周期将SDA保持低电平进行确认数据接收成功;而在第9个时钟周期将SDA保持高电平表示数据传输出错,或者主机不再想接收数据。

3.优缺点:

? 优点:
? ??(1).只使用两条信号线;
? ??(2).支持多主机多从机(理论上最大主设备数无限制,最大从机数为127);
? ??(3).有应答机制。
? 缺点:
? ??(1).速率比SPI慢。

4.代码讲解I2C:

? ?? 首先当然是定义头文件,其中SDA_IN和SDA_OUT分别为设置输入、输出模式。

#ifndef __MYIIC_H
#define __MYIIC_H
#include "sys.h"//IO输入输出方向设置,操作CRL寄存器
#define SDA_IN() {
      GPIOB->CRL&=0X0FFFFFFF;GPIOB->CRL|=(u32)8<<28;}
#define SDA_OUT() {
      GPIOB->CRL&=0X0FFFFFFF;GPIOB->CRL|=(u32)3<<28;}//设置IIC数据线和时钟线的引脚
#define IIC_SCL PBout(6) //SCL
#define IIC_SDA PBout(7) //SDA 
#define READ_SDA PBin(7) //????SDA //IIC操作函数
void IIC_Init(void);                //初始化IIC的IO口 
void IIC_Start(void);				//发送IIC开始信号
void IIC_Stop(void);	  			//发送IIC停止信号
void IIC_Send_Byte(u8 txd);	//发送一个字节数据 
u8 IIC_Read_Byte(unsigned char ack);//IIC读取一个字节
u8 IIC_Wait_Ack(void); 	//IIC等待应答信号 
void IIC_Ack(void);					//IIC产生应答信号
void IIC_NAck(void);			//IIC产生非应答信号 
#endif

I2C数据的发送读取如下:


#include "myiic.h"
#include "delay.h"
//IIC初始化
void IIC_Init(void)
{
    					     RCC->APB2ENR|=1<<3;		//使能外设IOGPIOB->CRL&=0X00FFFFFF;	//PB6.7清零GPIOB->CRL|=0X33000000; //PB6.7推挽输出GPIOB->ODR|=3<<6;     	//PB6.7 输出高
}
//产生I2C起始信号
//I2C起始信号产生的条件为:SCL为高电平时,SDA变为低电平
void IIC_Start(void)
{
    SDA_OUT();     	//设置SDA为输出模式IIC_SDA=1;	  	//设置初始状态都为高电平 IIC_SCL=1;delay_us(4);IIC_SDA=0;		//起始信号,SDA由高变低delay_us(4);IIC_SCL=0; 		//钳住I2C总线,准备发送或接收数据
}	 //产生I2C停止信号
//产生停止信号的条件为:SCL为高电平时,SDA由低变高
void IIC_Stop(void)
{
    SDA_OUT();//SDA设置为输出IIC_SCL=0;IIC_SDA=0;//起始都是低电平delay_us(4);IIC_SCL=1; //SCL变为高电平IIC_SDA=1;//SDA由低电平转变为高电平产生停止信号delay_us(4);							   	
}//I2C主设备传输一个数据完成后,从设备产生应答信号,主设备等待应答信号到来
//产生条件:SCL为高电平期间,SDA时钟保持低电平。
//返回值:1,接收应答失败;0,接收应答成功
u8 IIC_Wait_Ack(void)
{
    u8 ucErrTime=0;SDA_IN();      			 //SDA设置为输入IIC_SDA=1;delay_us(1);	 //刚开始都为高电平IIC_SCL=1;delay_us(1);	 while(READ_SDA)		//读取数据线SDA的电平状态,如果持续低电平,则不会产生IIC_Stop信号,返回0{
    ucErrTime++;if(ucErrTime>250){
    IIC_Stop();//如果在SCL高电平期间,SDA信号线产生了一定时间的高电平则认为应答失败return 1;}}IIC_SCL=0;//应答结束,时钟输出0return 0;  
} //产生ACK应答信号
//产生条件为:SCL为高电平期间,SDA始终保持低电平
void IIC_Ack(void)
{
    IIC_SCL=0;SDA_OUT();IIC_SDA=0;delay_us(2);IIC_SCL=1;delay_us(2);IIC_SCL=0;
}
//产生非应答信号
//产生条件为:SCL为高电平期间,SDA也出现了高电平 
void IIC_NAck(void)
{
    IIC_SCL=0;SDA_OUT();IIC_SDA=1;delay_us(2);IIC_SCL=1;delay_us(2);IIC_SCL=0;
}					 				     
//IIC发送一个字节 
//发送条件为:SCL为低电平期间准备好数据,SCL为高电平期间保持数据
void IIC_Send_Byte(u8 txd)
{
                            u8 t;   SDA_OUT(); 	  //SDA设置为输出IIC_SCL=0;//拉低时钟准备数据for(t=0;t<8;t++){
                  if((txd&0x80)>>7) //从数据的最高位开始传输IIC_SDA=1;	//如果为1,则数据位为1else IIC_SDA=0; //不为1,数据位为0txd<<=1; 	  //逐个传输delay_us(2);   IIC_SCL=1;delay_us(2); IIC_SCL=0;	delay_us(2);}	 
} 	    
//读一个字节,ack=1时,发送ACK,ack=0,发送nACK
//读取条件为:SCL为高电平期间,读取SDA的电平状态
u8 IIC_Read_Byte(unsigned char ack)
{
    unsigned char i,receive=0;SDA_IN();//设置SDA为输入for(i=0;i<8;i++ ) //逐个读8位{
    IIC_SCL=0; delay_us(2);IIC_SCL=1; //SCL为高电平receive<<=1; //逐个移动数据位if(READ_SDA)receive++;   //如果SDA为高,则相应的数据为+1,反之为0delay_us(1); }					 if (!ack)IIC_NAck();//不产生ACK应答elseIIC_Ack(); //产生ACK应答return receive;
}

? 正如介绍的那般,在对I2C初始化以后,就设定start()与stop()函数,进行信号收发的开始、结束(通过调整SDA、SCL的状态实现),再设定ACK应答函数,实现“stop”信号的发送(即结束信号收发,同样也是调整SDA、SCL来实现)。

3.UART通信

? ?? UART是一种异步传输接口,不需要时钟线,通过起始位和停止位及波特率进行数据识别。

? ?? 异步是指:发送方发出数据后,不等接收方发回响应,接着发送下个数据包的通讯方式。

在这里插入图片描述

? ?? 如图:RX(接收数据)、TX(发出数据),一个设备的TX需要与另一个设备的RX相连,同样的一个设备的RX要与另一个设备的TX相连,完成数据的接收与发送。

1.数据格式:

(1)起始位
??数据线空闲状态为高电平,要发送数据时将其拉低一个时钟周期表示起始位。
(2)数据位
??使用校验位时,数据位可以有5~8位,一般为8位(保证ASCII值的正确性),如果不使用校验位,数据位可以达9位。
(3)校验位
??奇偶校验,保证包括校验位和数据位在内的所有位中1的个数为奇数或偶数。
(4)停止位
??为了表示数据包的结束,发送端需要将信号线从低电平变为高电平,并至少保持2个时钟周期。

2.优缺点:

优点:

??(1).只使用两个信号线

??(2).不需要时钟信号

缺点:

? ??传输速率比较低。

3.代码讲解UART

以下用一段正点原子32的代码来讲解UART通信:

u8 USART_RX_BUF[USART_REC_LEN];     //接收缓冲,最大USART_REC_LEN个字节.
//接收状态
//bit15, 接收完成标志
//bit14, 接收到0x0d
//bit13~0, 接收到的有效字节数目
u16 USART_RX_STA=0;       //接收状态标记 void uart_init(u32 bound){
    //GPIO端口设置GPIO_InitTypeDef GPIO_InitStructure;USART_InitTypeDef USART_InitStructure;NVIC_InitTypeDef NVIC_InitStructure;RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE);	//使能USART1,GPIOA时钟//USART1_TX GPIOA9GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA9GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;	//复用推挽输出GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA9//USART1_RX GPIOA10初始化GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;//PA10GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.10 //Usart1 NVIC 配置NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//抢占优先级3NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;		//子优先级3NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;			//IRQ通道使能NVIC_Init(&NVIC_InitStructure);	//根据指定的参数初始化VIC寄存器//USART 初始化设置USART_InitStructure.USART_BaudRate = bound;//串口波特率USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;	//收发模式USART_Init(USART1, &USART_InitStructure); //初始化串口1USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启串口接受中断USART_Cmd(USART1, ENABLE);                    //使能串口1 }

? 还是依照32的代码规则,将管脚、中断等先初始化,之后设定需要的波特率(一般为9600或115200)、接收的字节数、设定的数据收发的停止位(因为在一个字节的时间内,收发端的时钟不会相差太大,但是当收发数据多了之后,它们的差距会越来越大,所以,每传输8位数据之后,使用停止位做一次时钟同步,那么收发端的时钟差距被限定在一个区间内,不会造成数据读取错乱)、有无奇偶校验位等。

接下来就是数据的收发了。

数据的接收如下:

void USART1_IRQHandler(void)                	//串口1中断服务程序{
    u8 Res;if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)  //接收中断(接收到的数据必须是0x0d 0x0a结尾){
    Res =USART_ReceiveData(USART1);	//读取接收到的数据if((USART_RX_STA&0x8000)==0)//接收未完成{
    if(USART_RX_STA&0x4000)//如果已经接收到了0x0d{
    if(Res!=0x0a)USART_RX_STA=0;//如果在接收到0x0d之后,没有紧接着就接收到0z0a,那么就是接收错误,重新开始else USART_RX_STA|=0x8000;	//接收完成了 }else //还没收到0X0D{
    	if(Res==0x0d)USART_RX_STA|=0x4000;else{
    USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ;USART_RX_STA++;if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;//接收数据错误,重新开始接收 }		 }}   		 } 

? ?? 正如上述代码中“if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) ”,此句代码就是识别第一、二位数据是否是0x0d和0x0a(即接收是否开始,因为上面定义以0x0d为起始点,以0x0a结尾),当接收到0x0d、0x0a后,32就会把数据存储起来,一直到“ (USART_RX_STA&0x8000)==0 ”才结束接收。

? ?? 其中,USART_RX_STA为判断信号是否接收结束的变量,USART_RX_STA为0000 0000 0000 0000,第十六位为0则串口数据没有接收完,为1则接收完了(中断里有判断),而0x8000=1000 0000 0000 0000,所以USART_RX_STA只存在两种可能性(接收结束或未接收结束)。

数据的发送如下:

int main(void){
    		u16 t;  u16 len;	u16 times=0;delay_init();	    	 //延时函数初始化 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级uart_init(115200);	 //串口初始化为115200while(1){
    if(USART_RX_STA&0x8000){
    					   len=USART_RX_STA&0x3fff;//得到此次接收到的数据长度printf("\r\n您发送的消息为:\r\n\r\n");for(t=0;t<len;t++){
    USART_SendData(USART1, USART_RX_BUF[t]);//向串口1发送数据while(USART_GetFlagStatus(USART1,USART_FLAG_TC)!=SET);//等待发送结束}printf("\r\n\r\n");//插入换行USART_RX_STA=0;}else{
    times++;if(times%200==0)printf("请输入数据,以回车键结束\n");  delay_ms(10);   }}	 }