一、概念
串口通讯:一般都是一主一从(UART)
总线通讯:N主N从(SPI/IIC/USB CAN总线)
串口通讯可以当成uart,uart就是一主一从,通讯就是一个接一个发;
三个以上的设备要通信的话,设备之间需要两两相连接,很麻烦,所以有了总线通讯;
1、SPI
三线制和四线制;时钟线,片选线等;
主往从发MOSI;从往主发是MISO;
2、I2C协议起源
一种说法是,恩智浦(NXP)半导体公司优化的;飞利浦公司(1960年成立的)恩智浦是2006年独立出去的;
I2C经典协议:可以有多个主机,也可以有多个从机;
只有两根线,一个时钟线一个数据线(SCK和SDA)
硬件层:可以是一主多从,也可以是多主多从;
如果主1和主2同时对从1进行通讯,会出现冲突,
因此提出方案:总线仲裁
二、总线仲裁
1、按位与操作(SDA的线与结构)
SCK对数据进行采样,只在高电平的时候进行采样;比如一个占空比为50%的方波上进行采样;进行仲裁的时候,在高电平的时候分别进行采样,看高电平的时候,SDA如果是高电平,那么如果这个主机发送的不是高电平,就pass这个主机;这就是仲裁机制;
2、总线仲裁两步骤
由多个主设备,先线与产生SDA;然后利用SCK(SCK是选择比较区域的,SDA是和主机电平作比较的)逐位将SDA与主1/主2进行比较,相同数据则满足条件,反之则停止发送
多个主机同时对从机进行数据传送时,产生冲突因而需要选择接收数据的次序;因此要用到我们的总线仲裁方案:对多个主机的电平线线与产生SDA(1-1为1,0-0为0,1-0为0),接着利用SCK(SCK是选择比较区域的,SDA是和主机电平作比较的)逐位将SDA与主1/主2进行比较,相同数据则满足条件,反之则停止发送
总线仲裁的应用:出现多个主机同时与相同的从机进行通讯;
3、总线仲裁的操作
线与:将主1与上主2等等,得到我们的SDA;
比较:让主1和SDA比较,主2和SDA比较…比较的时机是SCK的高电平;
规则:首次出现,主和SDA不相符的,立即退出,将SDA的主动权让给其他符合条件的主机。
三、硬件层
1、SDA、SCK
开漏+上拉
不这样的话,就存在烧坏(短路–大电流–烧坏)的风险
为什么呢?——(设备1为0,设备2为1的时候,就会短路,设备1是1,设备2是0的时候也会短路)
开漏模式下输出高电平——加上拉电阻;
SCK和SDA都各自加一个上拉电阻;设备1和设备2一个为0,一个为1时,电阻大小选择:4.7k,稍微大一点的电阻(因为电压是3.3V,电流是0.702mA,如果电阻是100欧,电流就会是33mA,电流过大了)用4.7k的电阻,这样子I2C运行稳定;如果要让I2C运行提速的话,选择一个稍微小点的电阻,其实电阻这里能影响I2C通讯的速度的;
问题:两个从机同时向主机通讯,主机如何接收?
这个需要IIC协议层去解决;
四、软件层
1、概括
整个通讯协议分两步,主机往从机(写/W),从机往主机(读/R);当主机通过从机地址与某个从机通讯时,则其他从机没有SDA的使用权;
2、写的步骤
1找到从机地址;
2明确和从机通讯的方向:写(主–>从),明确是要往从机进行写操作;
3从机存放数据的地址(0x01/0x02/0x03…)
(pc指向分区的位置)(也就是将从机内部指针指向目标寄存器(内存))
4主->从发数据
3、读
(主机想和0x3f通讯)
从机地址;
读操作;
将PC指针指向03位置;
从机往主机发送数据(读操作)
找到从机的地址,要把这个从机的地址发送到主机,这其实也是一种写操作->在发从机地址和从机内存地址的时候,这两个其实就是写操作;
所以这个读操作需要优化:
1从机地址(0x3f)+写操作;
2从机内存地址(0x03)指针指向(0x03)
3主机给从机发送一个停止信号
4主机又发送从机地址+读操作
5主机开始接收从机的数据(因为操作2中,指针就指向了0x03了)(从当前PC的位置开始读)
123步也就是:找到从机,并且将从机内部指针指向目标寄存器(内存)
4、写
1.主机发一个从机地址(3f)+写操作
2.主机发0x3f的内存地址(02)
0x3f设备将自身的指针指向自己的02寄存器(从机的行为)
3.主机在已知从设备以及从设备的内存寄存器下,开始发送数据
4.主机停止发送数据,并且给出一个停止位
5、读(具体)
本质是从机往主机发送数据,但是前提条件是,指针指向的位置正确,所以主机还是要先往从机发数据,将从设备内部指针指向正确
例如:需求是:主机想读取0x4f中03的数据
1.先将从设备中的指针指向正确的内存地址
(主机发送从设备的地址(0x4f)+写操作
主机发送内存地址(0x4f的02)
从设备0x4f将自己的指针指向0x02寄存器
主机发停止位)
2.从当前指针的位置开始读数据
(主机发一个0x4f + 读操作
从机开始往主机发数据
主机发一个停止位)
以上是读、写的基本的原则
五、市面上常见的I2C设备
1、监测环境的相关传感器
(温度传感器,只负责去测量温度,这个芯片只放了一块内存,就只用来存储外界温度值;这种产品不需要写这个通讯协议,只需要读,而且不需要规定在哪里读,不需要知道内存地址,直接读即可)(通讯协议就可以这样简单设计:开始位,从机地址+读操作,ACK响应,传数据,NACK,STOP)
2、时钟芯片
(专门买个时钟芯片,这种都是用I2C通讯的)
液晶屏驱动芯片/LED驱动芯片(这个驱动芯片涉及到解耦思想)(液晶显示屏驱动芯片,需要yog指令去控制内存;有很多条指令,比如第一条指令是在0x00–0x2f之间读,第二条指令又在另一个地址读,这种设备,就需要根据指令去操作了:开始位,从机地址+写操作,驱动芯片给一个返回值ACK,写从机的指令,接收从机的返回ACK,还想发可以继续,不发的话就是一个STOP;
读数据的话:开始位,从机地址+读操作,ACK,指令,ACK,停止位STOP)
3、EEPROM芯片
(flash,内存相关)
4、ADC/DAC
(模数转换/数模转换)
5、GPIO拓展芯片
六、协议
协议组成:空闲、开始位、停止位、发送应答、接收应答、发送一个字节、接收一个字节
这些协议,是以主机为参考的;
1、组成介绍
空闲:SCK和SDA都为高电平,在这个时间点,都是空闲的;
开始位:SCK=1;SDA由高电平变为低电平;
停止位:SCK=1;SDA由低电平转化为高电平;
出现了一个上升沿;即出现上升沿时,代表通信已经结束了;
发送应答:SCK=1; SDA=0时,代表有应答;
SDA=1时,代表无应答;
发送应答、接收应答,主机和从机都可以
发送字节、接收字节:规则:
主机和从机发送字节,都必须遵循高位先行的规则
10110011
发送一位字节,必须在SCK=0时发;
接收一位字节,必须在SCK=1时收;
主机发第一个数据(1),有一个大的原则:时钟线高电平的时候(也就是SCK=1时要保证SDA的值稳定),数据一定要保持稳定;SCK变成高电平之前,SDA就要变化好;
2、实践代码
①IIC_Init( )初始化
②IIC Write( )-----
1、IIC_Start( )
2、IIC_Addr( )(从机的真实地址+写操作位)
3、IIC_Receive ACK( )
4、IIC_addr( )(外设内部的地址)
5、依旧是ACK
6、IIC_SendData( )
7、IIC_Receive ACK( ) 6和7是真正写入数据
8、IIC_Stop( )
③IIC_Read( )
1-5、从机将内部指针指向对应的寄存器(reg)
6、IIC_Addr( ) (这时候是地址+读操作)
7、IIC_Receive ACK( )
8、IIC_Receive Data( )
9、IIC_Send ACK( ) 8和9一直循环
直到IIC_Send NACK( )
10、IIC_Stop( )
IIC start和stop肯定需要的,接收数据和发送数据肯定是需要的; SendData ReceiveData
还有Send_ACK 和 Receive_ACK
3、IIC分为硬件IIC和软件IIC
硬件IIC有现成的接口:API: HAL IIC xx
软件IIC: 不用外设,只用GPIO,随机找两个引脚,
一个取名SCK,一个取名SDA;
实现方法:两个引脚配置:
SCK:输出->开漏加上拉
SDA:既有主机往从机发,又有从机往主机发;
解决方案:①:不断切换输入/输出模式
②:直接配置成开漏加上拉电阻;输出的时候也是可以进行读操作的;输出模式下不会影响输入功能;正常就是把两个引脚都配置成开漏加上拉;
软件实现的好处就是:引脚可以任意选择,只要配置正确就行;
4、伪代码
IIC_Start( ){
SCK=1;
SDA=1;
SDA=0;
SCK=0;
}
IIC_Stop( ){
SCK=1;
SDA=0;
SDA=1;
SCK=0;
}
IIC_Send ACK( ){
SCK=0;
SDA(0/1);(用hal库中的write函数)
SCK=1;(SCK=1的时候进行校验)
SCK=0;
}
IIC_Receive_ACK( ){(这时候ACK是从机发的)
uint8 ACK;
SDA=1;(1的时候是空闲状态)
//主机放开主动权
SCK=1;(数据必须在SCK=1的时候进行抓取)
//主机等待读取的时机
ACK=SDA( );(HAL库中的reading函数)
SCK=0;(主动拉低时钟线,为下次做准备)
return ACK;
}
IIC_SendByte(unit8_t Data)
原则:发送/接收原则:高位先行
SCK=0的时候,数据跳变
SCK=1的时候,数据要稳定
SCK=0;
SDA=第八位;
SCK=1;
SCK=0;
SDA=第七位;
SCK=1;
需要循环八次-->
真实代码:
SCK=0;
for(i=0;i<8;i++){
if(Data&(0x80>>i)){
SDA=1;
}else{
SDA=0;
}
SCK=1;
SCK=0;
}
IIC_ReceiveByte( ){
//定义data
unit8_t Data=0;
//主机让出使用权
SDA=1;
for(i=0;i<8;i++){
SCK=1;
if(SDA==1){
data |=(0X80....);
}
SCK=0;
}
}
七、编程
设置两个引脚,都配置为开漏+上拉
在工程文件夹中,创建IIC文件夹,在此文件夹中创建IIC.c和IIC.h文件
头文件都要包含;还要记得防止重复定义;
导入IIC.h文件
IIC.h文件
#ifndef IIC_H
#define IIC_H
#include "gpio.h"
#define SDA(x) HAL_GPIO_WritePin(SDA_GPIO_Port,SDA_Pin,x);
#define SCK(x) HAL_GPIO_WritePin(SCK_GPIO_Port,SCK_Pin,x);
#define Read_SDA() HAL_GPIO_ReadPin(SCK_GPIO_Port,SCK_Pin);
void IIC_Init();
void IIC_Start();
void IIC_Stop();
void IIC_SendByte(uint8_t data);
void IIC_ReceiveByte(uint8_t data);
void IIC_Write();
uint8_t IIC_Read(uint8_t addr);
#endif
IIC.c文件
#include "IIC.h"
void IIC_Init() {
//SDA和SCK都是开漏+上拉
SDA(1); //确保SDA线为高
SCK(1); //确保SCK线为高
}
void IIC_Write(){
IIC_Start();
IIC_SendByte(address);
IIC_SendByte(data);
IIC_Stop();
}
uint8_t IIC_Read(uint8_t addr){
uint8_t data=0;
IIC_Start();
IIC_SendByte(addr|0x01);
data=IIC_ReceiveByte(data);
IIC_Stop();
return data;
}
void IIC_Start(){
SCK(1);
HAL_Delay(1);
SDA(1);
HAL_Delay(1);
SDA(0);
HAL_Delay(1);
SCK(0);
HAL_Delay(1);
}
void IIC_Stop(){
SCK(1);
HAL_Delay(1);
SDA(0);
HAL_Delay(1);
SDA(1);
HAL_Delay(1);
SCK(0);
HAL_Delay(1);
}
void IIC_SendByte(uint8_t data){
SCK(0);
for(int i=0;i<8;i++){
if(data & (0x80 >> i)){
SDA(1);
}else{
SDA(0);
}
SCK(1);
HAL_Delay(1);
SCK(0);
}
SDA(1);
SCK(1);
HAL_Delay(1);
if(SDA==0){
}
SCK(0);
}
void IIC_ReceiveByte(uint8_t data){
data=0;
SDA(1);
for(int i=0;i<8;i++){
SCK(1);
if(SDA==1){
data|=(0x80>>i);
}
SCK(0);
}
SDA(0);
SCK(1);
HAL_Delay(1);
SCK(0);
SDA(1);
return data;
}