在嵌入式软硬件设计中,常常会碰到模块、芯片之间的数据交互接口问题,接下来我将会带大家一起梳理常用的一些接口的软硬件实现原理。

 

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模块接口引脚分配为:

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

 

Logo

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

更多推荐