串口通信
文章目录
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; // 配置波特率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)。这两条线都是漏极开路或者集电极开路结构,使用时需要外加上拉电阻,可以挂载多个设备(如下图),每个设备都有属于自己的地址,主机通过选择不同的地址来选择不同的设备。
(此处因为博主对模、数电的知识还未彻底掌握,所以就不介绍漏极开路和集电极开路了,总而言之,开漏输出只能输出低,或者关闭输出,因此开漏输出总是要配一个上拉电阻使用。)
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是一种异步传输接口,不需要时钟线,通过起始位和停止位及波特率进行数据识别。
异步是指:发送方发出数据后,不等接收方发回响应,接着发送下个数据包的通讯方式。
如图: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保持发送状态,直到数据发送完成。