CSR8675学习笔记:I2C Master通信

为了让CSR867x的开发更容易,现与思度科技联合推出CSR867x学习板【淘宝链接:思度科技CSR开发板】。

技术交流QQ群号:743434463
开发板会员QQ群号:725398389(凭订单号入群,赠PPT、项目源码、视频教程)
——————————正文分割线———————————–

#1. 引言
I2C Master接口是通用MCU的常见接口,CSR8675支持1路I2C master接口。

#2. I2C通信简介
I2C通信是一种应用非常广泛的硬件接口,网上有大量资料,在此不详述。推荐阅读如下:

  • I2C总线官网
  • 个人总结之I2C总线协议
  • 解析I2C通信协议

I2C总线通信协议的基本框架是固定的,用户可以自定义通过此协议传输的数据的含义。很难用同一个驱动代码来适配所有的芯片,因此需要为不同的芯片开发不同的驱动程序。

在开发I2C驱动程序时,需要重点关注芯片的几个特征:

  • 芯片的I2C设备地址
  • 芯片支持的I2C通信速率
  • 芯片支持的I2C传输类型

##2.1. I2C设备地址
常见的I2C设备地址有7bit 、8bit、10bit三种,详细内容参考如下链接:
7位,8位和10位IIC/I2C从地址

##2.2. I2C通信速率
常见的I2C通信速率有100 kbit/s、400 kbit/s两种,对应标准模式(standard mode)和高速模式(fast mode)。这里给出的两种速率是每个模式的上限值,工程上可以根据项目需求、硬件平台灵活设定。

##2.3. I2C传输类型
常见的I2C传输类型有四种:单字节写、单字节读、多字节写、多字节读。这四种传输类型都基于典型的I2C通信序列。

以TI的TAS2557芯片举例(TAS2557数据手册),其典型I2C通信序列如下:
CSR8675学习笔记:I2C Master通信_第1张图片
上图中有几个关键点需要注意:

  • 每个典型序列由起始状态、7bit从设备地址+读写位、8bit寄存器地址、1~N个8bit寄存器值、应答位、停止状态组成
  • 在完整的序列完成前,不应开始新的序列
  • 高位在前、低位在后
  • SDA线上的数据在SCL线上出现上升沿时有效
  • 应答位可由主设备或从设备产生,如主设备设备未收到从设备的应答位,可认为通信终止
  • 8-bit data for register (n+1)的隐含意思是这颗芯片支持顺序I2C寻址

单字节写传输:
CSR8675学习笔记:I2C Master通信_第2张图片

  • 步骤1:主设备发送起始状态
  • 步骤2:主设备发送 (7bit I2C设备地址左移1位 | 0x00)
  • 步骤3:从设备发送应答位
  • 步骤4:主设备发送寄存器地址
  • 步骤5:从设备发送应答位
  • 步骤6:主设备发送1字节数据
  • 步骤7:从设备发送应答位
  • 步骤8:主设备发送停止状态

多字节写传输:
CSR8675学习笔记:I2C Master通信_第3张图片
与单字节传输相比,多字节传输只需重复步骤6-7,直到所有数据发送完毕。这里有一个问题,不同MCU支持的单次多字节写的最大传输字节数并不相同,取决于驱动代码的实现方式。

尽量不要在单次多字节写时传输过多数据。因为多字节写传输是一个完整的时序,当传输数据过多时,这个时序会持续较长时间,可能影响到重要中断的响应。

单字节读传输:
CSR8675学习笔记:I2C Master通信_第4张图片

  • 步骤1:主设备发送起始状态
  • 步骤2:主设备发送 (7bit I2C设备地址左移1位 | 0x00)
  • 步骤3:从设备发送应答位
  • 步骤4:主设备发送寄存器地址
  • 步骤5:从设备发送应答位
  • 步骤6:主设备再次发送起始状态
  • 步骤7:主设备发送 (7bit I2C设备地址左移1位 | 0x01)
  • 步骤8:从设备发送应答位
  • 步骤9:主设备读取1字节数据
  • 步骤10:主设备发送非应答位
  • 步骤11:主设备发送停止状态

多字节读传输:
CSR8675学习笔记:I2C Master通信_第5张图片
与单字节读传输相比,多字节读传输需重复步骤9-10,直到所有数据接收完毕。有一个容易忽视的细节,主设备在读最后一个字节时发送非应答位,其他字节发送应答位。

上述多字节读写传输利用了芯片的一种特性:顺序I2C寻址。

The TAS2557 supports sequential I2C addressing. For write transactions, if a register is issued followed by data for that register and all the remaining registers that follow, a sequential I2C write transaction has taken place. For I2C sequential write transactions, the register issued then serves as the starting point, and the amount of data subsequently transmitted, before a stop or start is transmitted, determines to how many registers are written.

这种顺序I2C寻址的好处是,在读写地址连续的寄存器时,可以省去起始状态、停止状态、I2C设备地址,降低通信开销,缩短芯片配置时间。

#3. CSR8675实现I2C master通信的两种方式
CSR8675有两种实现I2C master通信的方法:

  • VM层API
  • Kalimba DSP

##3.1. VM层API
VM层封装了一个I2C master的API接口函数,可支持上述4中读写序列。

 @brief Allows transfer of data across the i2c interface.
     @param address The device address of the slave for this transfer.
     @param tx      A pointer to the data we wish to transmit. One octet per word.
     @param tx_len  The length of the data we wish to transmit.
     @param rx      A pointer to the memory we wish to read received data into. One octet per word.
     @param rx_len  The length of the data we wish to receive.
     @return The number of bytes acknowledged including the address bytes and the 
                    final data byte (which isn't strictly acknowledged).

uint16 I2cTransfer(uint16 address, const uint8 *tx, 
                   uint16 tx_len, uint8 *rx, uint16 rx_len);

在使用此API前,需要在PSTool工具中指定I2C通信使用的IO口,并确保程序中没有将这两个IO口用作其他用途。参考文档file:///C:/ADK4.1/firmware/assisted/unified/rick/pskeys.html
这里写图片描述
我们的项目将PIO1配置成SCL,将PIO2配置成SDA,fast mode,因此配置如下:

  • PSKEY_I2C_CONFIG: 1
  • PSKEY_I2C_SCL_PIO: 1
  • PSKEY_I2C_SDA_PIO: 2

这里注意CSR8675只有PIO0-PIO15可以用作I2C功能。

###3.1.1. 单字节写

static int8 tas2557_i2c_write_device(
                    uint8_t addr,
                    uint8_t reg,
                    uint8_t value)
{
    int8 nResult = 0;
    uint8_t i2c_data[2]; 
    
    i2c_data[0] = reg; 
    i2c_data[1] = value; 

#ifdef I2C_DEBUG
    DEBUG_2557(("%s R[%x] D[%x]\n", __func__, reg, value));
#endif

    if(I2cTransfer(addr, i2c_data, sizeof(i2c_data), 0, 0))
    {
        nResult = 0;
    }
    else
    {
        DEBUG_2557_ERR(("%s[0x%x] Error %d\n", __func__, addr, nResult));
        nResult = -1;
    }
    
    return nResult;
}

###3.1.2. 多字节写

static int8 tas2557_i2c_bulkwrite_device(
                    uint8_t addr,
                    uint8_t reg,
                    uint8_t *pBuf,
                    uint8_t len)
{
    int8 nResult = 0;
#ifdef I2C_DEBUG
    uint8_t nIdx = 0;
#endif
    uint8 *pBuf1;
    uint8 *pBuf2;

    pBuf1 = PanicUnlessMalloc(len+1);
    *pBuf1 = reg;
    pBuf2 = pBuf1+1;
    memcpy(pBuf2, (const void *)pBuf, len);

#ifdef I2C_DEBUG
    DEBUG_2557(("%s R[%x] L[%d]\n", __func__, reg, len));
    for(nIdx = 0; nIdx < len; nIdx++)
    {
        DEBUG_2557(("D[%x] ", pBuf2[nIdx]));
    }
    DEBUG_2557(("\n"));
#endif

    nResult = I2cTransfer(addr, pBuf1, (len+1), 0, 0);
    
    free(pBuf1);

    if (nResult)
    {
        return 0;
    }
    else
    {
        DEBUG_2557_ERR(("%s[0x%x] Error %d\n", __func__, addr, nResult));
        return -1;
    }
}

###3.1.3. 单字节读

static int8 tas2557_i2c_read_device(
                    uint8_t addr,
                    uint8_t reg,
                    uint8_t *p_value)
{
    int8 nResult = 0;
    uint8 i2c_data = reg; 
    uint8 rev_data = 0; 
    
    nResult = I2cTransfer(addr, &i2c_data, 1, &rev_data, 1);

#ifdef I2C_DEBUG
    DEBUG_2557(("%s R[%x] D[%x]\n", __func__, i2c_data, rev_data));
#endif
    
    if (nResult)
    {
        *p_value = rev_data;
        return 0;
    }
    else
    {
        DEBUG_2557_ERR(("readOneData Fail...\n"));
        return -1;
    }
}

###3.1.4. 多字节读

static int8 tas2557_i2c_bulkread_device(
                uint8_t addr,
                uint8_t reg,
                uint8_t *p_value,
                uint8_t len)
{
    int8 nResult = 0;
#ifdef I2C_DEBUG
    uint8_t nIdx = 0;
#endif
    uint8_t addr_local = reg;
    uint8_t *pBuf;

    pBuf = PanicUnlessMalloc(len);
    
    nResult = I2cTransfer(addr, &addr_local, 1, pBuf, len);

    if (nResult)
    {
        memcpy(p_value, (const void *)pBuf, len);
        
    #ifdef I2C_DEBUG
        DEBUG_2557(("%s R[%x] L[%d]\n", __func__, reg, len));
        for(nIdx = 0; nIdx < len; nIdx++)
        {
            DEBUG_2557(("D[%x] ", pBuf[nIdx]));
        }
        DEBUG_2557(("\n"));
    #endif
    
        free(pBuf);
        return 0;
    }
    else
    {
        free(pBuf);
        DEBUG_2557_ERR(("%s[0x%x] Error %d\n", __func__, addr, nResult));
        return -1;
    }
}

##3.2. Kalimba DSP
ADK里提供了一个i2c.asm库文件,可以用Kalimba DSP实现多个I2C master通信。

为了实现DSP的I2C master通信,需完成如下动作:

  • 修改i2c.asm
  • 配置专用IO
  • 实现典型的4种I2C序列

###3.2.1. 修改i2c.asm
i2c.asm中包含了几个I2C master通信的原子级操作的源码:

  • 发送起始状态
  • 写一个字节
  • 读一个字节
  • 发送停止状态

有了这几个原子级操作,即可为不同芯片定制I2C驱动。

在使用i2c.asm之前,需要将里面每个module的存储位置修改成PM,否则运行VM build是会提示出错。下面以delay_high_clk模块举例,每个模块都要修改。

修改前:
.MODULE $M.i2c.delay_high_clk;
   .CODESEGMENT I2C_HIGH_CLK_PM;

修改后:
.MODULE $M.i2c.delay_high_clk;
   .CODESEGMENT PM;

###3.2.2. 配置专用IO
DSP可控制PIO0-PIO15作为I2C master的硬件接口。

VM和DSP都可以控制PIO0-PIO15,默认VM占用控制权。为了让DSP获得控制权,需调用API将控制权移交给DSP。

DSP获取IO控制权的示例代码如下:

    /* Set PIO1, PIO2 controlled by dsp */
    ret = PioSetKalimbaControl32(0x06, 0x06);
    DEBUG_I2C(("I2C: PioSetKalimbaControl32 = %ld\n", ret));

这里的0x06,二进制表示是b0000_0110,对应PIO1、PIO2。

如果ret返回值不为0,则控制权设置失败,应检查PIO是否被配置成了其他特殊功能。

VM获取IO控制权的示例代码如下:

    /* Set PIO1, PIO2 controlled by dsp */
    ret = PioSetKalimbaControl32(0x06, 0x00);
    DEBUG_I2C(("I2C: PioSetKalimbaControl32 = %ld\n", ret));

在DSP工程内,需要设置PIO1、PIO2与SDA, SCL的对应关系和IO输入输出配置:

   /* PIO2设为SDA,PIO1设为SCL */
   .CONST $i2c.default_pio_mask.SDA                        (1<<2);
   .CONST $i2c.default_pio_mask.SCLK                       (1<<1);

   /* PIO1、PIO2都设为输出 */
   r0 = 0x06; // Set PIO1, PIO2 as output
   M[$PIO_DIR] = r0;

###3.2.3. 单字节写

   /* 发送起始状态 */
   call $i2c.start_bit;

      /* 发送I2C设备地址 | 0x00 */
      r0 = 0x98;   
      call $i2c.send_byte;

      /* 发送寄存器地址 */
      r0 = 0x03;   
      call $i2c.send_byte;

   /* 发送停止状态 */
   call $i2c.stop_bit;

###3.2.4. 单字节读

/* 发送起始状态 */
   call $i2c.start_bit;

      /* 发送I2C设备地址 | 0x00 */
      r0 = 0x98;   
      call $i2c.send_byte;

      /* 发送寄存器地址 */
      r0 = 0x03;   
      call $i2c.send_byte;

   /* 再次发送起始状态 */
   call $i2c.start_bit;
      
      /* 发送I2C设备地址 | 0x01 */
      r0 = 0x99;   
      call $i2c.send_byte;
   
      /* 从SDA线读取1字节数据并发送非应答位 */
      r0 = 0; // not ack
      call $i2c.receive_byte;
   
   /* 发送停止状态 */
   call $i2c.stop_bit;

###3.2.5. 多字节写

   // write 120 bytes
   call $i2c.start_bit;
   
      r0 = 0x98;   
      call $i2c.send_byte;
      
      r0 = 0x08;   
      call $i2c.send_byte;
      
      /* 循环发送120字节 */
      r5 = 120;
      write_loop:
         r0 = 0xaa;
         call $i2c.send_byte;
         r5 = r5 - 1;
         if NZ jump write_loop;
   
   call $i2c.stop_bit;

###3.2.6. 多字节读

// read 120 bytes
   call $i2c.start_bit;
   
      r0 = 0x98;   
      call $i2c.send_byte;
      
      r0 = 0x08;   
      call $i2c.send_byte;
      
   call $i2c.start_bit;
   
      r0 = 0x99;   
      call $i2c.send_byte;
      
      /* 循环读119字节并发送应答位 */
      r5 = 119;
      read_loop:
         r0 = 1; // ack
         call $i2c.receive_byte;
         r5 = r5 - 1;
         if NZ jump read_loop;
   
      /* 读第120字节并发送非应答位 */
      r0 = 0; // not ack
      call $i2c.receive_byte;
      
   call $i2c.stop_bit;

#4. 总结
再简单对比一下VM和DSP实现I2C master的差异:

对象 编程语言 数量 实现方式 调用方式 灵活性 开发难度
VM C语言 1 软件I2C API接口 不能定制
DSP 汇编语言 8 软件I2C 系统消息 可定制序列、I2C通信速率、SCL占空比等

在掌握CSR8675实现I2C master的两种方法后,蓝牙平台的拓展能力有了很大提升。

CSR8675的I2C可以作为stream source,在读取大量数据时很有用处。有机会可以尝试一下用法并分享给大家。

你可能感兴趣的:(蓝牙方案,CSR8670蓝牙芯片软件开发)