通信协议
文章目录
- 通信协议
-
- 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); }} }