使用lib_i2c配置I2C Master
XMOS lib_i2c库提供了软件定义的、符合工业标准的I2C总线解决方案。lib_i2c允许您创建I2C主机或从机设备组件,这些设备利用XMOS GPIO端口实现通信。
I2C是一种双线串行总线,有明确的协议定义用来连接设备。它包含一条时钟线和一条数据线,两条线均由外部上拉电阻拉高,并由I2C驱动器驱动为低电平。
在I2C Fast-mode下,XMOS lib_i2c 库支持速率高达400Kbit/s的主设备和从设备的通信。
本应用笔记演示了如何使用lib_i2c主库与X-Power的AC101立体声音频编解码器配合工作。该应用利用单个逻辑核心运行应用程序,并根据需要调用I2C主设备库的函数。

软件要求
此示例通过验证,可在XTC 15.1.4或更高版本中正常工作。也可能在xTIMEcomposer 14或其他版本中正常工作。
依赖库:
- lib_i2c - I2C通信库
- lib_logging - 日志记录库
为确保依赖库能被正确链接和编译,请按照使用Makefile构建工具-XCOMMON系统中的要求放置依赖库。
硬件要求
-
基于XCORE-200或XCORE-AI系列的XMOS芯片平台。
-
X-Power的AC101立体声音频编解码器
如何使用 I2C 主设备
配置 Makefile
为了在应用程序使用 lib_i2c库,您需要将 lib_i2c lib_logging 添加到 Makefile (通常在以 app_ 开头的目录下):
USED_MODULES = .. lib_i2c lib_logging ..
lib_logging 中包含一个debug_printf 函数,这是 C 标准库 printf 函数的一个更快但更受限制的版本。
日志库可在编译时配置,要启用打印,必须将以下内容添加到XCC编译标志中:
XCC_FLAGS = .. -DDEBUG_PRINT_ENABLE=1 ..
使用依赖库中的头文件
I2C库函数定义在i2c.h头文件中,日志功能是由debug_print.h头文件提供的。要使用这个库,必须在代码中包含两个头文 件。
#include "i2c.h"
#include "debug_print.h"
扩展 i2c 客户端接口
为了初始化AC101音频编解码芯片,我们扩展了i2c.h中的客户端接口 i2c_master_if。在XC语言中,这类似于面向对象的继承概念。
然后,我们扩展了两个用于AC101芯片的方法:
AC101_init(): 初始化AC101芯片AC101_hp_vol_ctrl(): 控制AC101的耳机输出音量
extends client interface i2c_master_if:{
inline void AC101_init(client interface i2c_master_if i2c){}
inline void AC101_hp_vol_ctrl(client interface i2c_master_if i2c, int db_vol){}
}
这样,AC101模块的实现细节被封装在AC101_i2c_extends.h内,外部模块只需要关注通用的i2c_master_if接口。
在这个示例中,我们将AC101_init和AC101_hp_vol_ctrl方法定义为内联函数,并在.h文件中实现它。这样做使得代码更加简洁,并且AC101的初始化与音量控制不太可能被多个模块反复调用。如果您需要将其扩展成一个更复杂的驱动或硬件抽象层,那么最好采用传统的.h和.c分离接口和实现的方式。
使用 i2c 写入数据
现在,我们将实现AC101_init和AC101_hp_vol_ctrl这两个方法。在此我们将使用i2c.h头文件中提供的write_reg16_addr8()函数,该函数通过I2C总线向8位寄存器地址写入16位数据。
如果您的I2C从属设备采用不同的数据写入格式,请参考i2c.h头文件提供的其他接口选择。现在,让我们先来实现AC101_init()
inline void AC101_init(client interface i2c_master_if i2c)
{
debug_printf("AC101_init\n");
const init_i2c_struct_t AC101_init_data[] =
{
AC101_INIT_DATA // AC101 的初始化数据
};
for (size_t i = 0; i < sizeof(AC101_init_data) / sizeof(AC101_init_data[0]); i++)
{
debug_printf("write addr: 0x%x data: 0x%4x\n", AC101_init_data[i].reg_addr, AC101_init_data[i].data);
i2c.write_reg16_addr8(AC101_ADDR, AC101_init_data[i].reg_addr, AC101_init_data[i].data); // 通过 I2C 写入初始化数据
}
}
为提升代码的可读性,我们将AC101的I2C地址以及初始化使用的寄存器数据,通过宏定义,并为初始化数据定义一个结构体:
#include "AC101_init_data.h"
#define AC101_ADDR (0X34 >> 1)
typedef struct _init_i2c_struct
{
uint8_t reg_addr; // 寄存器地址
uint16_t data; // 数据
} init_i2c_struct_t;
我们将需要配置的寄存器数据放在 AC101_init_data.h中。当需要为特定芯片配置寄存器时,该芯片的制造商通常会提供.h头文件和.c源文件,其中包含该芯片常用的初始化寄存器值。
AC101_init_data.h
#ifndef __AC101_INIT_DATA_H__
#define __AC101_INIT_DATA_H__
/**
* @def AC101_INIT_DATA
* @brief AC101编解码器的初始化寄存器值
*/
#define AC101_INIT_DATA \
{0x01, 0x015F},\
{0x02, 0x8175},\
{0x03, 0xAA08},\
{0x04, 0x800C},\
{0x05, 0x800C},\
{0x06, 0x7000},\
{0x10, 0x88B0},\
{0x11, 0xC100},\
{0x12, 0xC000},\
{0x13, 0x2200},\
{0x40, 0x8000},\
{0x41, 0xBBA0},\
{0x45, 0x0010},\
{0x48, 0x8000},\
{0x4C, 0x8800},\
{0x50, 0xBFC0},\
{0x51, 0x0008},\
{0x52, 0x55C4},\
{0x53, 0xFF80},\
{0x54, 0x0002},\
{0x56, 0x3BF1},\
{0x58, 0xE01B},\
{0x82, 0x2000},\
{0x83, 0x2000},\
{0xB4, 0x00C0},
#endif
现在,我们来实现AC101_hp_vol_ctrl():
inline void AC101_hp_vol_ctrl(client interface i2c_master_if i2c, int db_vol)
{
int max_vol = 0x3F;
uint16_t reg_data = 0x3801; // 整个寄存器的值
max_vol += db_vol;
reg_data |= max_vol << 4; //4~9位为耳机音量控制,范围为0~-62dB
i2c.write_reg16_addr8(AC101_ADDR, 0x56, reg_data); // 通过 I2C 写入音量值
}
在应用程序中使用i2c
要在应用程序中使用I2C接口及AC101模块,我们需要包含以下头文件:
#include <platform.h> //包含XMOS平台相关定义
#include <xs1.h> //包含XMOS xCORE相关定义
#include "i2c.h" //包含I2C主机接口及通用函数
#include "timer.h" //包含XMOS延时相关函数
#include AC101_i2c_extends.h //我们实现的AC101 I2C扩展接口
之后,我们调用AC101_init()函数初始化AC101芯片,并在之前添加一个100ms的延时以确保AC101启动成功,然后设置AC101的耳机音量输出为-6dB
void my_app(client i2c_master_if i2c)
{
delay_milliseconds(100);
i2c.AC101_init();
i2c.AC101_hp_vol_ctrl(-6);
}
在这里,我们通过i2c对象调用AC101_i2c_extends.h中定义的AC101专用方法来实现对AC101芯片的初始化和音量控制。i2c对象实现了i2c_master_if接口,它可以是应用程序中其他任务实现的I2C主机对象,也可以是我们自己实现的I2C 主机对象。
接下来,我们需要定义I2C总线的SDA和SCL信号连接的端口引脚,并在main()中定义I2C的硬件,并使用我们刚刚写的my_app()方法。使用1bit port与使用4bit port 或 8bit port的实现方式不同,此处提供两种示例:
- 1bit PORT
- 4bit PORT
我们需要用两个1bit端口配置SDA和SCL管脚,用于标准I2C通信。
port p_sda = XS1_PORT_1E;
port p_scl = XS1_PORT_1F;
要在应用程序中使用my_app(),我们需要声明一个结构为i2c_master_if的接口数组,以声明i2c的所有客户端。接下来我们在par中初始化了I2C主设备,然后调用my_app()函数使用I2C通信。
int main(void)
{
i2c_master_if i2c[1];
par
{
i2c_master(i2c, 1, p_scl, p_sda, 100/*kbit/s*/);
my_app(i2c[0]);
}
return 0;
}
我们使用一个4bit端口配置SDA和SCL管脚,用于标准I2C通信。
port p_i2c = XS1_PORT_4C;
要在应用程序中使用my_app(),我们需要声明一个结构为i2c_master_if的接口数组,以声明i2c的所有客户端。接下来我们在par中初始化了I2C主设备,其中第5和第6个参数是SDA和SCL在4bit PORT中的位置,然后调用my_app()函数使用I2C通信。
int main(void)
{
i2c_master_if i2c[1];
par
{
i2c_master_single_port(i2c, 1, p_i2c, 100/*kbit/s*/, 1/*SCL*/, 2/*SDA*/, 0);
my_app(i2c[0]);
}
return 0;
}
扩展阅读
对于更多实例与应用,您可以参考源码库文件中lib_i2c/examples中的代码示例,示例中包含了其他lib_i2c中的其他API用例。