IoT嵌入式--数据接口
“在嵌入式软硬件设计中,常常会碰到模块、芯片之间的数据交互接口问题,接下来我将会带大家一起梳理常用的一些接口的软硬件实现原理。”01—I2C接口I2C总线是由Philips公司开发的一种...
“ 在嵌入式软硬件设计中,常常会碰到模块、芯片之间的数据交互接口问题,接下来我将会带大家一起梳理常用的一些接口的软硬件实现原理。”
01
—
I2C接口
I2C总线是由Philips公司开发的一种简单、双向二线制同步串行总线。它只需要两根线即可在连接于总线上的器件之间传送信息。
1、SDA(串行数据线)和SCL(串行时钟线)都是双向I/O线,接口电路为开漏输出。
2、需通过上拉电阻接电源VCC.当总线空闲时.两根线都是高电平,连接总线的外同器件都是CMOS器件,输出级也是开漏电路。

3、在总线上消耗的电流很小,因此,总线上扩展的器件数量主要由电容负载来决定,因为每个器件的总线接口都有一定的等效电容。而线路中电容会影响总线传输速度。当电容过大时,有可能造成传输错误。所以,其负载能力为400pF,因此可以估算出总线允许长度和所接器件数量。
4、连接到相同总线上的IC数量只受总线最大电容的限制,串行的8位双向数据传输位速率在标准模式下可达100Kbit/s,快速模式下可达400Kbit/s,高速模式下可达3.4Mbit/s。
I2C总线数据相关规定:
1、数据位的有效性规定
I2C总线进行数据传送时,时钟信号为高电平期间,数据线上的数据必须保持稳定,只有在时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化

SCL处于高电平的时候,SDA用来传输数据,必须保持电平稳定
如果SDA处于低电平,则表示传输数据0;SDA处于高电平,则表示传输数据1
如果要产生数据即SDA需要变化时,只能在SCL处于低电平的时候
2、起始和终止信号
SCL线为高电平期间,SDA线由高电平向低电平的变化表示起始信号;SCL线为高电平期间,SDA线由低电平向高电平的变化表示终止信号。

a、信号的发起和终止,由主机发起,在起始信号产生后,总线就处于被占用的状态;在终止信号产生后,总线就处于空闲状态。
b、处于高电平的时候,SDA由高->低的跳变, 表示发起传输,产生起始信号S,如果SDA低->高跳变,表示终止传输,产生终止信号p
c、在中间,可以正常传输数据
d、连接到I2C总线上的器件,若具有I2C总线的硬件接口,则很容易检测到起始和终止信号。
e、接收器件收到一个完整的数据字节后,有可能需要完成一些其它工作,如处理内部中断服务等,可能无法立刻接收下一个字节,这时接收器件可以将SCL线拉成低电平,从而使主机处于等待状态【只有SCL处于高电平的时候,主机才会去匹配SDA的电平,接收下一个bit】。直到接收器件准备好接收下一个字节时,再释放SCL线使之为高电平,从而使数据传送可以继续进行。
3. 数据传送格式
1)字节传送与应答
每一个字节必须保证是8位长度。数据传送时,先传送最高位(MSB),每一个被传送的字节后面都必须跟随一位应答位(即一帧共有9位)

主机每次发起通信后,第一个字节,往往是从设备的地址(7位)+RW(寻址信号)
a、由于某种原因从机不对主机寻址信号应答时(如从机正在进行实时性的处理工作而无法接收总线上的数据),它必须将数据线置于高电平,而由主机产生一个终止信号以结束总线的数据传送。
b、 如果从机对主机进行了应答,但在数据传送一段时间后无法继续接收更多的数据时,从机可以通过对无法接收的第一个数据字节的“非应答”(拉高SDA)通知主机,主机则应发出终止信号以结束数据的继续传送。
c、当主机接收数据时,它收到最后一个数据字节后,必须向从机发出一个结束传送的信号。这个信号是由对从机的“非应答”(拉高SDA)来实现的。然后,从机释放SDA线,以允许主机产生终止信号。
2)数据帧格式
I2C总线上传送的数据信号是广义的,即包括地址信号,又包括真正的数据信号。
在起始信号后必须传送一个从机的地址(7位),第8位数数据的传送方向位(R/T),用“0”表示主机发送数据(T),“1”表示主机接收数据(R)。每次数据传送总是由主机产生的终止信号结束。在总线的一次数据传送过程中,可以有以下几种组合方式:
a) 主机向从机发送数据:

注1:有阴影部分表示数据由主机向从机传送,无阴影部分则表示数据由从机向主机传送。
A表示应答, 非表示非应答(高电平)。S表示起始信号,P表示终止信号。
注2:I2C总线协议有明确的规定:采用7位的寻址字节(寻址字节是起始信号后的第一个字节)
b) 主机从从机中读取数据

c)主机从从机中既有读数据也有写数据

4、I2C发送与接收数据时序图
注:红色框代表主机拥有SDA控制权,绿色框代表从机拥有SDA控制权
a、主机发送数据

b、主机读取数据

上面这四点是关于I2C总线的一些时序规定以及数据协议,原理不是很复杂,用起来也比较简单,现在很多的MCU也集成了I2C接口,可以直接调用API来使用,当然对于没有集成I2C接口的MCU我们也可以通过模拟来进行曲线救国。下面是一段I2C模拟程序,大家可以试试看。
//软件模拟的方式
void IIC_Delay()
{
// u8 cnt=20;
u8 cnt=19;
while(cnt--);
}
/*
* 函数名:IIC_Start
* 描述 :产生IIC起始信号
* 输入 :无
* 输出 :无
*/
u8 IIC_Start(void) //SCL处于高电平,SDA由高跳变到低
{
IIC_SDA_1;
IIC_SCL_1;
IIC_Delay(); //SCL时钟周期,最低4.7uS
IIC_SDA_0; //START:when CLK is high,DATA change form high to low
IIC_Delay();
IIC_SCL_0; //钳住I2C总线,准备发送或接收数据
IIC_Delay();
return 1;
}
/*
* 函数名:IIC_Stop
* 描述 :产生IIC停止信号
* 输入 :无
* 输出 :无
*/
void IIC_Stop(void) //SCL处于高电平,SDA低跳变到高
{
IIC_SCL_0;
IIC_Delay();
IIC_SDA_0;//STOP:when CLK is high DATA change form low to high
IIC_Delay();
IIC_SCL_1;
IIC_Delay();
IIC_SDA_1;//发送I2C总线结束信号
IIC_Delay();
}
/*
* 函数名:IIC_Wait_Ack
* 描述 :等待应答信号到来
* 输入 :无
* 输出 :返回值:1,接收应答失败,0,接收应答成功
*/
u8 IIC_Wait_Ack(void)
{
u16 ucErrTime=0;
IIC_SDA_1;
IIC_Delay();
IIC_SCL_1;
IIC_Delay();
while(READ_SDA)
{
ucErrTime++;
if(ucErrTime>100)
{
IIC_Stop();
return 0;
}
}
IIC_SCL_0;//时钟输出0
return 1;
}
/*
* 函数名:IIC_Ack
* 描述 :产生ACK应答
* 输入 :无
* 输出 :无
*/
void IIC_Ack(void)
{
IIC_SCL_0;
IIC_Delay();
IIC_SDA_0;
IIC_Delay();
IIC_SCL_1;
IIC_Delay();
IIC_SCL_0;
IIC_Delay();
}
/*
* 函数名:IIC_NAck
* 描述 :不产生ACK应答
* 输入 :无
* 输出 :无
*/
void IIC_NAck(void)
{
IIC_SCL_0;
IIC_Delay();
IIC_SDA_1;
IIC_Delay();
IIC_SCL_1;
IIC_Delay();
IIC_SCL_0;
IIC_Delay();
}
/*
* 函数名:IIC_Send_Byte
* 描述 :IIC发送一个字节
* 输入 :发送的字节
* 输出 :无
*/
void IIC_Send_Byte(u8 DAT) //在响应SDA期间,SCL要拉低,响应完以后,SCL拉高
{
u8 t;
for(t=0;t<8;t++)
{
IIC_SCL_0;
IIC_Delay();
if(DAT&0x80)
IIC_SDA_1;
else
IIC_SDA_0;
DAT<<=1;
// IIC_Delay();
IIC_SCL_1;
IIC_Delay();
}
IIC_SCL_0;
}
/*
* 函数名:IIC_Read_Byte
* 描述 :读1个字节,ack=1时,发送ACK,ack=0,发送nACK
* 输入 :ack=1,0
* 输出 :ACK,nACK
*/
u8 IIC_Read_Byte(void)
{
u8 i,receive=0;
IIC_SDA_1 ;
// IIC_SCL_1;
for(i=0;i<8;i++ )
{
receive<<=1;
IIC_SCL_0;
IIC_Delay();
IIC_SCL_1;
IIC_Delay();
if(READ_SDA) receive|=0x01;
IIC_SCL_0;
}
return receive;
}
//写入1字节数据
void IIC_Write_One_Byte(u8 DeviceAddress,u8 WriteAddress, u8 SendByte)
{
IIC_Start();
IIC_Send_Byte( DeviceAddress & 0xFE); //写器件地址 //地址的最低位是R/W选择 R/W=1是读命令,R/W=0是写命令
IIC_Wait_Ack();
IIC_Send_Byte((WriteAddress) & 0xFF); //设置低起始地址
IIC_Wait_Ack();
IIC_Send_Byte(SendByte); //写数据
IIC_Wait_Ack();
IIC_Stop();
}
//读出1字节数据
u8 IIC_Read_One_Byte(u8 DeviceAddress,u8 ReadAddress)
{
u8 temp;
IIC_Start();
IIC_Send_Byte((DeviceAddress & 0xFE)); //写器件地址
IIC_Wait_Ack();
IIC_Send_Byte((ReadAddress) & 0xFF); //设置低起始地址
IIC_Wait_Ack();
IIC_Start();
IIC_Send_Byte((DeviceAddress & 0xFE)|0x01); //读器件地址 0XA1
IIC_Wait_Ack();
temp = IIC_Read_Byte();
// IIC_NAck();
IIC_Stop();
return temp;
}
关于I2C使用以及调试的问题:
1、I2C在低功耗应用中要谨慎使用上拉电阻,可能会造成漏电流
2、I2C在调试过程中,如果发现逻辑分析仪上显示的波形正常,起始位、停止位、数据位都正常,但是依然没有应答,需要用示波器来抓下SDA和SCL的波形,有时候会因为模拟的问题,导致主机和从机之间互相拉电流,从而导致波形不正常。
3、在使用硬件I2C接口的时候也要特别小心,因为硬件I2C的延时特别小,很容出现锁死的情况
4、模拟I2C只能用在低速数据传输的场景,毕竟模拟中很多地方都需要延时。
02
—
SPI接口
SPI 是由摩托罗拉(Motorola)公司开发的全双工同步串行总线,是微处理控制单元(MCU)和外围设备之间进行通信的同步串行端口。主要应用在EEPROM、Flash、实时时钟(RTC)、数模转换器(ADC)、网络控制器、MCU、数字信号处理器(DSP)以及数字信号解码器之间。SPI 系统可直接与各个厂家生产的多种标准外围器件直接接口,一般使用4 条线:串行时钟线SCK、主机输人/从机输出数据线MISO、主机输出/从机输人数据线MOSI 和低电平有效的从机选择线CS。
在讨论SPI 数据传输时,必须明确以下两位的特点及功能:
(1) CPOL: 时钟极性控制位。该位决定了SPI总线空闲时SCK 时钟线的电平状态。
CPL=0,当SPI总线空闲时,SCK 时钟线为低电平。
CPL=1,当SPI总线空闲时,SCK 时钟线为高电平。
(2) CPHA: 时钟相位控制位。该位决定了SPI总线上数据的采样位置。
CPHA=0,SPI总线在时钟线的第1个跳变沿处采样数据。
CPHA= 1,SPI总线在时钟线的第2个跳变沿处采样数据。
下图为SPI FLASH的一个SPI连接图。

同I2C接口一样,很多MCU已经集成了SPI接口,也有很多低成本的MCU没有这一接口,同样可以通过模拟的方式来实现:
时序详解:
CPOL:时钟极性选择,为0时SPI总线空闲为低电平,为1时SPI总线空闲为高电平
CPHA:时钟相位选择,为0时在SCK第一个跳变沿采样,为1时在SCK第二个跳变沿采样
工作方式1:
当CPHA=0、CPOL=0时SPI总线工作在方式1。MISO引脚上的数据在第一个SPSCK沿跳变之前已经上线了,而为了保证正确传输,MOSI引脚的MSB位必须与SPSCK的第一个边沿同步,在SPI传输过程中,首先将数据上线,然后在同步时钟信号的上升沿时,SPI的接收方捕捉位信号,在时钟信号的一个周期结束时(下降沿),下一位数据信号上线,再重复上述过程,直到一个字节的8位信号传输结束。
工作方式2:
当CPHA=0、CPOL=1时SPI总线工作在方式2。与前者唯一不同之处只是在同步时钟信号的下降沿时捕捉位信号,上升沿时下一位数据上线。
工作方式3:
当CPHA=1、CPOL=0时SPI总线工作在方式3。MISO引脚和MOSI引脚上的数据的MSB位必须与SPSCK的第一个边沿同步,在SPI传输过程中,在同步时钟信号周期开始时(上升沿)数据上线,然后在同步时钟信号的下降沿时,SPI的接收方捕捉位信号,在时钟信号的一个周期结束时(上升沿),下一位数据信号上线,再重复上述过程,直到一个字节的8位信号传输结束。
工作方式4:
当CPHA=1、CPOL=1时SPI总线工作在方式4。与前者唯一不同之处只是在同步时钟信号的上升沿时捕捉位信号,下降沿时下一位数据上线
//当CPHA=0、CPOL=0时SPI总线工作在方式1
uint8_t SPI_FLASH_SendByte(uint8_t Data)
{
int i = 0;
unsigned char RecvByte = 0;
for (i = 0; i < 8; i++)
{
if ((Data & 0x80) != 0) MOSI_IO = 1;
else MOSI_IO = 0;
Data <<= 1;
//-发送-
SCK_IO = 0;
RecvByte <<= 1;
if (MISO_IO != 0)
RecvByte |= 0x01;
//-接收-
SCK_IO = 1;
}
return RecvByte;
}
//当CPHA=1、CPOL=0时SPI总线工作在方式3
uint8_t SPI_FLASH_SendByte(uint8_t Data)
{
int i = 0;
unsigned char RecvByte = 0;
for (i = 0; i < 8; i++)
{
if ((Data & 0x80) != 0) MOSI_IO = 1;
else MOSI_IO = 0;
Data <<= 1;
//-发送-
SCK_IO = 1;
RecvByte <<= 1;
if (MISO_IO != 0)
RecvByte |= 0x01;
//-接收-
SCK_IO = 0;
}
return RecvByte;
}
//当CPHA=0、CPOL=1时SPI总线工作在方式2
uint8_t SPI_FLASH_SendByte(uint8_t Data)
{
int i = 0;
unsigned char RecvByte = 0;
for (i = 0; i < 8; i++)
{
//-发送-
SCK_IO = 1;
if ((Data & 0x80) != 0) MOSI_IO = 1;
else MOSI_IO = 0;
Data <<= 1;
//-接收-
SCK_IO = 0;
RecvByte <<= 1;
if (MISO_IO != 0)
RecvByte |= 0x01;
SCK_IO = 1;
}
return RecvByte;
}
//当CPHA=1、CPOL=1时SPI总线工作在方式4
uint8_t SPI_FLASH_SendByte(uint8_t byte)
{
u8 i,Temp=0;
for(i=0;i<8;i++) // 循环8次
{
SCK_IO = 0; //拉低时钟
if(byte&0x80) MOSI_IO = 1; //若最到位为高,则输出高
else MOSI_IO = 0; //若最到位为低,则输出低
byte <<= 1; // 低一位移位到最高位
SCK_IO = 1; //拉高时钟
Temp <<= 1; //数据左移
if(MISO_IO) Temp++; //若从从机接收到高电平,数据自加一
SCK_IO = 0; //拉低时钟
}
return (Temp); //返回数据
}
03
—
UART接口
通用异步收发传输器(Universal Asynchronous Receiver/Transmitter),通常称作UART。它将要传输的资料在串行通信与并行通信之间加以转换。作为把并行输入信号转成串行输出信号的芯片,UART通常被集成于其他通讯接口的连接上。
UART作为异步串口通信协议的一种,工作原理是将传输数据的每个字符一位接一位地传输。其中各位的意义如下:

起始位:先发出一个逻辑”0”的信号,表示传输字符的开始。
数据位:紧接着起始位之后。资料位的个数可以是4、5、6、7、8等,构成一个字符。通常采用ASCII码。从最低位开始传送,靠时钟定位。
奇偶校验位:资料位加上这一位后,使得“1”的位数应为偶数(偶校验)或奇数(奇校验),以此来校验资料传送的正确性。
停止位:它是一个字符数据的结束标志。可以是1位、1.5位、2位的高电平。由于数据是在传输线上定时的,并且每一个设备有其自己的时钟,很可能在通信中两台设备间出现了小小的不同步。因此停止位不仅仅是表示传输的结束,并且提供计算机校正时钟同步的机会。适用于停止位的位数越多,不同时钟同步的容忍程度越大,但是数据传输率同时也越慢。
空闲位:处于逻辑“1”状态,表示当前线路上没有资料传送。
波特率:是衡量资料传送速率的指标。表示每秒钟传送的符号数(symbol)。一个符号代表的信息量(比特数)与符号的阶数有关。例如传输使用256阶符号,每8bit代表一个符号,资料传送速率为120字符/秒,则波特率就是120baud,比特率是120*8=960bit/s。这两者的概念很容易搞错。
UART是通用异步收发器(异步串行通信口)的英文缩写,它包括了RS232、RS449、RS423、RS422和RS485等接口标准规范和总线标准规范,即UART是异步串行通信口的总称。而RS232、RS449、RS423、RS422和RS485等,是对应各种异步串行通信口的接口标准和总线标准,它规定了通信口的电气特性、传输速率、连接特性和接口的机械特性等内容。
在IoT应用中比较常用的是RS232,RS485,TTL,主要的表现电气特性以及连接特性上的不同,这三种接口之间我们也常常进行转换,也有专门的IC来实现这一转换功能。比如TTL和RS232之间的转换IC max232

TTL和RS485之间的互相转换

同样现在基本上所有的MCU都集成了UART接口,当然对于极少数的一些低成本的MCU或者是串口数比较少的MCU,我们还是可以用模拟的方式来进行UART通信,下面的实例代码采用的是延时方法,
void VirtualUartWByte(u8 dat) //利用虚拟串口发送一个字节。
{
u8 i=8;
VTXD=0; //起始位
delay_us(tbaud);
while(i--)
{
VTXD=dat&0x01;
delay_us(tbaud);
dat=dat>>1;
}
VTXD=1; //停止位
delay_us(tbaud);
}
u8 VirtualUartRByte() //利用虚拟串口发送一个字节。
{
u8 i=8;
u8 output=0;
delay_us(tbaud*1.5); //起始位
while(i--)
{
output>>=1;
if(VRXD) output|=0x80;
delay_us(tbaud);
}
return output;
}
当然大家也可以尝试使用中断的方法来实现,原理都一样
void TIM2_IRQHandler(void)
{
/******* receive 1 byte *******/
temp0=rx_buff&0x03;
rx_buff>>=1;
if(rxd1)rx_buff|=0x80;
temp1=rx_buff&0x38;
if(rx_start==0)
{
rx_int=0;
if(temp0==0x01)
{
if(temp1==0x30 || temp1==0x18 || temp1==0x28 || temp1==0x38)
return;
else
{
rx_start=1;
rxcnt=8;
rxbits=8;
return;
}
}
}
else
{
rxcnt--;
if(rxcnt==0)
{
rxdata>>=1;
if(temp1==0x30 || temp1==0x18 || temp1==0x28 || temp1==0x38)
rxdata|=0x80;
rxbits--;
if(rxbits==0)
{
rx_int=1;
rx_start=0;
rxbits=8;
rxcnt=8;
}
}
}
/******* send 1 byte *******/
if(txbits!=0)
{
if(txdata&0x01)
txd1=1;
else
txd1=0;
txcnt--;
if(txcnt==0)
{
txbits=8;
txdata>>=1;
}
}
}
但是实际应用中还是建议大家尽量使用硬件串口,模拟串口无论是延时还是中断都会极大的影响系统的运行。如果中断的优先级比较低,还会导致串口输出不正常,但是可以作为一个调试手段来打印一些debug信息。
04
—
miniPCIe接口
MINI PCI-E 是基于PCI-E 总线的接口,MINI PCI 是基于 PCI 总线的接口,两种接口在电气性能上不同,外形不同,不可混用,且每种接口都有相对应的元器件,弄错了是插不上的。比如4G模块EC20 Mini PCIe-C模块接口引脚分配为:

听说关注公众号的都是大牛

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐

所有评论(0)