目录
基本理论知识:
并行通信/串行通信:
异步通信/同步通信:
半双工通信/全双工通信:
UART串口:
I2C串口:
SPI串口:
I2C在单片机中的应用:
软件模拟:
51单片机:
STM32:
硬件I2C:
软件I2C和硬件I2C的区别:
UART在单片机中的应用:
USB转TTL模块:
协议层:
波特率:
通信的起始信号和停止信号:
有效数据:
数据校验:
51单片机串口通信:
STM32串口通信:
SPI在单片机中的应用:
硬件电路:
时序:
软件模拟SPI:
硬件SPI:
基本理论知识:
1、并行通信/串行通信
2、异步通信/同步通信
3、半双工通信/全双工通信
并行通信/串行通信:
并行通信:传输速度快,一次传输8bit,但是通信成本高,需要8个独立的通道,另外不支持长距离传输。用于打印机和扫描仪等设备,例如DB-25接口。
串行通信:传输速度慢,成本低,支持长距离传输,是计算机通信的主要方式,例如DB-接口。
异步通信/同步通信:
异步通信:用于低速设备,会有更高的误码率。
同步通信:用于高速设备传输,同步传输有同步时钟为节拍进行传输数据。
半双工通信/全双工通信:
单工通信:发送机只能给接收机发送数据,不允许从接收机发送给发送机。
半双工通信:发送机和接收机可以相互读写通信,但不能同时读写。
全双工工信:发送机和接收机可以相互读写通信,且能同时读写。
UART串口:
UART(Universal Asynchronous Receiver Transmitter),具有串行通信、异步通信、全双工通信的特点,两线制(TX, RX),传输速度慢,点对点的异步通信,一般用于RJ45 Console、打印机等。
UART工作原理:
发送器UART1从发送端数据总线接收到并行数据,将起始位、校验位和停止位添加到数据帧中,打包成数据包;然后将数据包以串行方式发送给接收器UART2;UART2以预配置的波特率对数据进行采样,将数据包还原成数据帧;最后UART2将数据帧串行转并传输给接收端的数据总线。
I2C串口:
I2C(Inter-Integrated Circuit),具有串行通信、同步通信、半双工通信的特点,两线制(SDA, SCLK),用于监控、存储和数字信号处理器等。
I2C是两线制(SDA, SCLK),通过上拉电阻接到电源线,总线空闲时,SDA,SCLK都保持高电平。
I2C的数据传输过程:
Step1: I2C总线空闲时,上拉电阻使SDA, SCLK处于高电平。
Step2:Master发送start后,将SDA由高电平切换成低电平,然后SCLK线也由高电平切换成低电平。
Step3: Master在发送start后,再发送 slave的地址和读/写的命令,其中write是0,read是1,slave收到地址和读写命令后,向master回复ASK。
Step4:Master收到ASK后,再发送特定寄存器的地址,slave收到后回复master ASK。
Step5:Master再次收到ASK后,再像特定的寄存器发送8bit数据,slave收到数据后回复ASK,重复这一动作直至数据发完。
Step6:Master收到stop,SCLK由低电平切换成高电平,随后SDA也从低电平切换成高电平。
SPI串口:
SPI(Serial Peripheral Interface),具有串行通信、同步通信、全双工通信的特点,四线制(CS, SCLK, MOSI, MISO),传输速度快,时许同步准确,一般用于存储器、数字信号处理、传感器和语音识别等。
I2C在单片机中的应用:
软件模拟:
51单片机:
/*******************************************************************************
* 函 数 名 : iic_start
* 函数功能 : 产生IIC起始信号
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void iic_start(void)
{
IIC_SDA=1;//如果把该条语句放在SCL后面,第二次读写会出现问题
delay_10us(1);
IIC_SCL=1;
delay_10us(1);
IIC_SDA=0; //当SCL为高电平时,SDA由高变为低
delay_10us(1);
IIC_SCL=0;//钳住I2C总线,准备发送或接收数据
delay_10us(1);
}
void iic_start(void)
{
IIC_SDA=1;//如果把该条语句放在SCL后面,第二次读写会出现问题
delay_10us(1);
IIC_SCL=1;
delay_10us(1);
IIC_SDA=0; //当SCL为高电平时,SDA由高变为低
delay_10us(1);
IIC_SCL=0;//钳住I2C总线,准备发送或接收数据
delay_10us(1);
}
/*******************************************************************************
* 函 数 名 : iic_stop
* 函数功能 : 产生IIC停止信号
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void iic_stop(void)
{
IIC_SDA=0;//如果把该条语句放在SCL后面,第二次读写会出现问题
delay_10us(1);
IIC_SCL=1;
delay_10us(1);
IIC_SDA=1; //当SCL为高电平时,SDA由低变为高
delay_10us(1);
}
/*******************************************************************************
* 函 数 名 : iic_ack
* 函数功能 : 产生ACK应答
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void iic_ack(void)
{
IIC_SCL=0;
IIC_SDA=0; //SDA为低电平
delay_10us(1);
IIC_SCL=1;
delay_10us(1);
IIC_SCL=0;
}
/*******************************************************************************
* 函 数 名 : iic_nack
* 函数功能 : 产生NACK非应答
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void iic_nack(void)
{
IIC_SCL=0;
IIC_SDA=1; //SDA为高电平
delay_10us(1);
IIC_SCL=1;
delay_10us(1);
IIC_SCL=0;
}
/*******************************************************************************
* 函 数 名 : iic_wait_ack
* 函数功能 : 等待应答信号到来
* 输 入 : 无
* 输 出 : 1,接收应答失败
0,接收应答成功
*******************************************************************************/
u8 iic_wait_ack(void)
{
u8 time_temp=0;
IIC_SCL=1;
delay_10us(1);
while(IIC_SDA) //等待SDA为低电平
{
time_temp++;
if(time_temp>100)//超时则强制结束IIC通信
{
iic_stop();
return 1;
}
}
IIC_SCL=0;
return 0;
}
/*******************************************************************************
* 函 数 名 : iic_write_byte
* 函数功能 : IIC发送一个字节
* 输 入 : dat:发送一个字节
* 输 出 : 无
*******************************************************************************/
void iic_write_byte(u8 dat)
{
u8 i=0;
IIC_SCL=0;
for(i=0;i<8;i++) //循环8次将一个字节传出,先传高再传低位
{
if((dat&0x80)>0)
IIC_SDA=1;
else
IIC_SDA=0;
dat<<=1;
delay_10us(1);
IIC_SCL=1;
delay_10us(1);
IIC_SCL=0;
delay_10us(1);
}
}
/*******************************************************************************
* 函 数 名 : iic_read_byte
* 函数功能 : IIC读一个字节
* 输 入 : ack=1时,发送ACK,ack=0,发送nACK
* 输 出 : 应答或非应答
*******************************************************************************/
u8 iic_read_byte(u8 ack)
{
u8 i=0,receive=0;
for(i=0;i<8;i++ ) //循环8次将一个字节读出,先读高再传低位
{
IIC_SCL=0;
delay_10us(1);
IIC_SCL=1;
receive<<=1;
if(IIC_SDA)receive++;
delay_10us(1);
}
if (!ack)
iic_nack();
else
iic_ack();
return receive;
}
STM32:
PA4和PA2分别作为SCL和SDA。
void MyI2C_Init(void)
{
//开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_2;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PB10和PB11引脚初始化为开漏输出
/*设置默认电平*/
GPIO_SetBits(GPIOA, GPIO_Pin_4 | GPIO_Pin_2); //设置PB10和PB11引脚初始化后默
}
void MyI2C_W_SCL(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue); //根据BitValue,设置SCL引脚的电平
Delay_us(10); //延时10us,防止时序频率超过要求
}
void MyI2C_W_SDA(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_2, (BitAction)BitValue);
Delay_us(10);
}
uint8_t MyI2C_R_SDA(void)
{
uint8_t BitValue;
BitValue = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_2); //读取SDA电平
Delay_us(10); //延时10us,防止时序频率超过要求
return BitValue; //返回SDA电平
}
void MyI2C_Start(void)
{
MyI2C_W_SCL(1);
MyI2C_W_SDA(1);
MyI2C_W_SDA(0);
MyI2C_W_SCL(0);
}
void MyI2C_Stop(void)
{
MyI2C_W_SDA(0);
MyI2C_W_SCL(1);
MyI2C_W_SDA(1);
}
void MyI2C_SendByte(uint8_t Byte)
{
uint8_t i;
for(i=0;i<8;i++)
{
MyI2C_W_SDA(Byte&0x80>>i);
MyI2C_W_SCL(1);
MyI2C_W_SCL(0);
}
}
uint8_t MyI2C_ReceiveByte(void)
{
uint8_t i,Byte=0x00;
MyI2C_W_SDA(1);
for(i=0;i<8;i++)
{
MyI2C_W_SCL(1);
if (MyI2C_R_SDA() == 1){Byte |= (0x80 >> i);}
MyI2C_W_SCL(0);
}
return Byte;
}
void MyI2C_SendAck(uint8_t AckBit)
{
MyI2C_W_SDA(AckBit);
MyI2C_W_SCL(1);
MyI2C_W_SCL(0);
}
uint8_t MyI2C_ReceiveAck(void)
{
uint8_t AckBit;
MyI2C_W_SDA(1);
MyI2C_W_SCL(1);
AckBit= MyI2C_R_SDA();
MyI2C_W_SCL(0);
return AckBit;
}
硬件I2C:
51单片机中没有硬件I2C的功能,只有STM32中有,这里只讲解STM32中的I2C功能。
通信引脚:
数据逻辑控制:
(3) 以上步骤正常执行并对 ADDR 位清零后,我们往 I2C 的“数据寄存器 DR”写入要发送的数
据,这时 TXE 位会被重置 0,表示数据寄存器非空,I2C 外设通过 SDA 信号线一位位把数据发送
出去后,又会产生 “EV8” 事件,即 TXE 位被置 1,重复这个过程,就可以发送多个字节数据了。
(4) 当我们发送数据完成后,控制 I2C 设备产生一个停止信号 (P),这个时候会产生 EV8_2 事件 ,
SR1 的 TXE 位及 BTF 位都被置 1,表示通讯结束。假如我们使能了 I2C 中断,以上所有事件产生时,都会产生 I2C 中断信号,进入同一个中断服务函数,到 I2C 中断服务程序后,再通过检查寄存器位来判断是哪一个事件。
主接收器:
主接收器接收流程及事件说明如下:
I2C初始化结构体:
typedef struct
{
uint32_t I2C_ClockSpeed; /*!< 设置 SCL 时钟频率,此值要低于 400000*/
uint16_t I2C_Mode; /*!< 指定工作模式,可选 I2C 模式及 SMBUS 模式 */
uint16_t I2C_DutyCycle; /* 指定时钟占空比,可选 low/high = 2:1 及 16:9 模式*/
uint16_t I2C_OwnAddress1; /*!< 指定自身的 I2C 设备地址 */
uint16_t I2C_Ack; /*!< 使能或关闭响应 (一般都要使能) */
uint16_t I2C_AcknowledgedAddress; /*!< 指定地址的长度,可为 7 位及 10 位 */
} I2C_InitTypeDef;
配置完这些结构体成员值,调用库函数 I2C_Init 即可把结构体的配置写入到寄存器中。
说白了,硬件I2C就是帮我们完成了I2C协议的基本时序,但是如何实现通信,需要我们把他进行组合,不断检测标志位,判断通信到了哪一步,然后我们在调用硬件I2C的函数,一般是清楚标志位、发送开始时序,发送结束时序,发送非应答,接收数据(读取SR)、发送数据(写入SR)生成对应的时序,继续通信。
这里可以和DMA模块一起复用,通过DMA,可以不断的发送数据,这里可以粗略的讲解一下,我们知道,当I2C硬件完成发送一个数据之后,相应的标志位会被置位,我们在设置相应DMA触发事件,就可以不断的往SR中搬运数据,实现连续发送数据,读取也是一样。
软件I2C和硬件I2C的区别:
1、前者时序的搭建,需要CPU的参与,不断的改变引脚电平,来满足时序的要求。
2、后者不需要CPU的参与,大大节省了CPU的资源。
3、他们的波形也有些许差别。
软件:
硬件:
仔细观察,可以发现,当SCL拉低的那一瞬间之后,软件的SDA没有立即改变,而硬件控制的SDA则迅速反应,这是因为软件完成时序是通过函数改变引脚电平,这段时间会有点延时。
UART在单片机中的应用:
在单片机使用的串口通讯中,一般只使用 RXD、TXD 以及 GND 三条信号线,直接传输数据信号。
USB转TTL模块:
这里需要用到CH340USB转TTL模块。
CH340是一种USB转串口芯片,常用于单片机与计算机之间的串口通信。它的主要作用是将计算机的USB接口转换为串行通信接口,从而实现计算机与单片机之间的数据传输。
在单片机开发中,通常需要通过串口与计算机进行通信,以实现数据的传输、调试和监控等功能。而现代计算机通常只提供USB接口而不再配备传统的串口接口,因此需要通过USB转串口芯片来连接单片机和计算机。
CH340芯片具有成本低廉、稳定可靠、兼容性好等优点,因此被广泛应用于单片机开发中。它提供了简单易用的串口通信功能,使得开发者可以轻松实现单片机与计算机之间的数据交换。
协议层:
波特率:
通信的起始信号和停止信号:
有效数据:
在数据包的起始位之后紧接着的就是要传输的主体数据内容,也称为有效数据,有效数据的长度常被约定为 5、6、7 或 8 位长。
数据校验:
51单片机串口通信:
串口通信初始化代码可以从STC-ISP中获取:
发送数据:
//串口发送一个字节数据
void UART_SendByte(unsigned char Byte){
SBUF=Byte;
//检测是否完成
while(TI==0);
TI=0;//TI复位
}
接收数据:
这里用的是中断4
void uart() interrupt 4 //串口通信中断函数
{
u8 rec_data;
RI = 0; //清除接收中断标志位
rec_data=SBUF; //存储接收到的数据
}
STM32串口通信:
TX:发送数据输出引脚。
static void NVIC_Configuration(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
/* 嵌套向量中断控制器组选择 */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
/* 配置 USART 为中断源 */
NVIC_InitStructure.NVIC_IRQChannel = DEBUG_USART_IRQ;
/* 抢断优先级为 1 */
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
/* 子优先级为 1 */
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
/* 使能中断 */
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
/* 初始化配置 NVIC */
NVIC_Init(&NVIC_InitStructure);
}
void USART_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
// 打开串口 GPIO 的时钟
DEBUG_USART_GPIO_APBxClkCmd(DEBUG_USART_GPIO_CLK, ENABLE);
// 打开串口外设的时钟
DEBUG_USART_APBxClkCmd(DEBUG_USART_CLK, ENABLE);
将 USART Tx 的 GPIO 配置为推挽复用模式
GPIO_InitStructure.GPIO_Pin = DEBUG_USART_TX_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(DEBUG_USART_TX_GPIO_PORT, &GPIO_InitStructure);
// 将 USART Rx 的 GPIO 配置为浮空输入模式
GPIO_InitStructure.GPIO_Pin = DEBUG_USART_RX_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(DEBUG_USART_RX_GPIO_PORT, &GPIO_InitStructure);
// 配置串口的工作参数
// 配置波特率
USART_InitStructure.USART_BaudRate = DEBUG_USART_BAUDRATE;
// 配置 针数据字长
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
// 配置停止位
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(DEBUG_USARTx, &USART_InitStructure);
// 串口中断优先级配置
NVIC_Configuration();
// 使能串口接收中断
USART_ITConfig(DEBUG_USARTx, USART_IT_RXNE, ENABLE);
// 使能串口
USART_Cmd(DEBUG_USARTx, ENABLE);
}
/**
* 函 数:串口发送一个字节
* 参 数:Byte 要发送的一个字节
* 返 回 值:无
*/
void Serial_SendByte(uint8_t Byte)
{
USART_SendData(USART1, Byte); //将字节数据写入数据寄存器,写入后USART自动生成时序波形
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); //等待发送完成
/*下次写入数据寄存器会自动清除发送完成标志位,故此循环后,无需清除标志位*/
}
/**
* 函 数:USART1中断函数
* 参 数:无
* 返 回 值:无
* 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
* 函数名为预留的指定名称,可以从启动文件复制
* 请确保函数名正确,不能有任何差异,否则中断函数将不能进入
*/
void USART1_IRQHandler(void)
{
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) //判断是否是USART1的接收事件触发的中断
{
Serial_RxData = USART_ReceiveData(USART1); //读取数据寄存器,存放在接收的数据变量
Serial_RxFlag = 1; //置接收标志位变量为1
USART_ClearITPendingBit(USART1, USART_IT_RXNE); //清除USART1的RXNE标志位
//读取数据寄存器会自动清除此标志位
//如果已经读取了数据寄存器,也可以不执行此代码
}
}
SPI在单片机中的应用:
硬件电路:
时序:
软件模拟SPI:
这里由于还是通过CPU直接控制GPIO引脚来模拟时序,对于51和32,思路都是一样,我这里就只列举STM32中软件模拟。
/**
* 函 数:SPI写SS引脚电平
* 参 数:BitValue 协议层传入的当前需要写入SS的电平,范围0~1
* 返 回 值:无
* 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SS为低电平,当BitValue为1时,需要置SS为高电平
*/
void MySPI_W_SS(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue); //根据BitValue,设置SS引脚的电平
}
/**
* 函 数:SPI写SCK引脚电平
* 参 数:BitValue 协议层传入的当前需要写入SCK的电平,范围0~1
* 返 回 值:无
* 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SCK为低电平,当BitValue为1时,需要置SCK为高电平
*/
void MySPI_W_SCK(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue); //根据BitValue,设置SCK引脚的电平
}
/**
* 函 数:SPI写MOSI引脚电平
* 参 数:BitValue 协议层传入的当前需要写入MOSI的电平,范围0~0xFF
* 返 回 值:无
* 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置MOSI为低电平,当BitValue非0时,需要置MOSI为高电平
*/
void MySPI_W_MOSI(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue); //根据BitValue,设置MOSI引脚的电平,BitValue要实现非0即1的特