为了让CSR867x的开发更容易,现与思度科技联合推出CSR867x学习板【淘宝链接:思度科技CSR开发板】。
技术交流QQ群号:743434463
开发板会员QQ群号:725398389(凭订单号入群,赠PPT、项目源码、视频教程)
——————————正文分割线———————————–
#1. 引言
I2C Master接口是通用MCU的常见接口,CSR8675支持1路I2C master接口。
#2. 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通信序列如下:
上图中有几个关键点需要注意:
多字节写传输:
与单字节传输相比,多字节传输只需重复步骤6-7,直到所有数据发送完毕。这里有一个问题,不同MCU支持的单次多字节写的最大传输字节数并不相同,取决于驱动代码的实现方式。
尽量不要在单次多字节写时传输过多数据。因为多字节写传输是一个完整的时序,当传输数据过多时,这个时序会持续较长时间,可能影响到重要中断的响应。
多字节读传输:
与单字节读传输相比,多字节读传输需重复步骤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通信的方法:
##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,因此配置如下:
这里注意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通信,需完成如下动作:
###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,在读取大量数据时很有用处。有机会可以尝试一下用法并分享给大家。