串口通信SPI、UART、I2C

串口通信

文章目录

1.SPI

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

首先讲讲同步的概念,

串口通信SPI、UART、I2C

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

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

1.四条数据线的介绍:

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

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

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

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

2.数据传输:

串口通信SPI、UART、I2C

​ 正如上图中,主机通过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; // 配置波特率4MHz
    U1CSR &= ~0xA0;               // 配置为SPI模式且为SPI主机
    U1GCR &= ~0xC0;               //空闲时SCLK处于低电平、上升沿数据接受、下降沿数据发送
    U1GCR |= 0x20;                // MSB(高字节)先传送
}
 
void SPI_Master_Receive()
{
    P1_4 = 0;              // SSN下降沿,SPI从机活跃,开始收发数据
    U1DBUF = 0x33;         // 向数据缓存寄存器发送数据
    while(!(U1CSR&0x02));  // 等待数据传送完成,即发送完成标志位置1
    U1CSR &= 0xFD;         // 清除发送完成标志位,即发送完成标志位置0
    temp = 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;
      }
      else
        LED1 = 0;
      halMcuWaitMs(300);
    }
    
}

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

#include <ioCC2530.h>
 
#define LED2          P1_1
 
unsigned 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: MISO
    P1SEL |= 0xF0;                 // 配置P1_4、P1_5、P1_6、P1_7为外设功能
    U1BAUD = 0x00; U1GCR |= 0x11;  // 配置波特率4MHz
    U1CSR &= ~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;
      }
      else
        LED2 = 0;
    }
}

2.I2C

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

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

串口通信SPI、UART、I2C

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;		//使能外设IO
	GPIOB->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;//应答结束,时钟输出0
	return 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,则数据位为1
      	else IIC_SDA=0; //不为1,数据位为0
        txd<<=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,反之为0
		delay_us(1); 
    }					 
    if (!ack)
        IIC_NAck();//不产生ACK应答
    else
        IIC_Ack(); //产生ACK应答
    return receive;
}

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

3.UART通信

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

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

串口通信SPI、UART、I2C

​ 如图: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   GPIOA9
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA9
  GPIO_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;//PA10
  GPIO_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 ;//抢占优先级3
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;		//子优先级3
	NVIC_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); //初始化串口1
  USART_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);	 //串口初始化为115200
 	while(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);   
		}
	}	 
 }

nfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
uart_init(115200); //串口初始化为115200
while(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);
}
}
}


​		上述代码中。“USART_SendData(USART1, USART_RX_BUF[t])”表示向串口1发送数据,下一句代码则是判段数据是否发送完成,如果发送完成则会返回set,未完成则用while保持发送状态,直到数据发送完成。
上一篇:MySQL的安装及配置


下一篇:Centos8.4 二进制安装配置Mysql5.7.34