Zephyr I2S驱动模型

本文介绍Zephyr I2S驱动接口定义,使用和实现接口说明。

概述 链接到标题

Zephyr在i2s.h中定义了统一的i2s驱动接口,zephyr i2s的驱动模型提供了如下接口功能:

  • I2S配置操作 i2s_configure i2s_config_get
  • 读写操作 i2s_read i2s_buf_read i2s_write i2s_buf_write
  • 控制操作 i2s_trigger 目前i2s的接口已经属于稳定接口,可以放心使用。

接口 链接到标题

枚举和定义 链接到标题

typedef uint8_t i2s_fmt_t i2s数据格式标准由以下三种分类的按位或组成

  • I2S数据格式标准 I2S_FMT_DATA_FORMAT_I2S: 标准I2S数据格式 I2S_FMT_DATA_FORMAT_PCM_SHORT:PCM短帧同步模式 I2S_FMT_DATA_FORMAT_PCM_LONG: PCM长帧同步模式 I2S_FMT_DATA_FORMAT_LEFT_JUSTIFIED: 左对齐数据格式 I2S_FMT_DATA_FORMAT_RIGHT_JUSTIFIED:右对齐数据格式

  • I2S数据大小端 I2S_FMT_DATA_ORDER_MSB: 大端 I2S_FMT_DATA_ORDER_LSB: 小段 I2S_FMT_DATA_ORDER_INV: 默认是大端, INV就是小端

  • I2S时钟格式配置 I2S_FMT_BIT_CLK_INV: 反转位时钟 I2S_FMT_FRAME_CLK_INV: 反转帧时钟 NF表示“正常帧”,IF表示“反转帧”;NB表示“正常位时钟”,IB表示“反转位时钟”,以下自由组合: I2S_FMT_CLK_NF_NB I2S_FMT_CLK_NF_IB I2S_FMT_CLK_IF_NB I2S_FMT_CLK_IF_IB

typedef uint8_t i2s_opt_t I2S的选项 I2S_OPT_BIT_CLK_CONT: 连续运行位时钟 I2S_OPT_BIT_CLK_GATED: 仅在发送数据时运行位时钟 I2S_OPT_BIT_CLK_MASTER: I2S驱动程序是位时钟主机 I2S_OPT_BIT_CLK_SLAVE: I2S驱动程序是位时钟从机 I2S_OPT_FRAME_CLK_MASTER:I2S驱动程序是帧时钟主机 I2S_OPT_FRAME_CLK_SLAVE:I2S驱动程序是帧时钟从机 I2S_OPT_LOOPBACK: 回环模式,RX输入将内部连接到TX输出,主要用于测试 I2S_OPT_PINGPONG: 乒乓模式,通常用于音频流式播放,TX有两个缓存区,一个用于缓冲数据,一个用于播放。目前Zephyr内I2S驱动均不支持该模式

enum i2s_dir I2S_DIR_RX 接收方向 I2S_DIR_TX 发送方向 I2S_DIR_BOTH 双向, 目前大多数I2S驱动的API实现都没有支持BOTH

enum i2s_trigger_cmd I2S_TRIGGER_START: 开始数据传输 I2S_TRIGGER_STOP: 在当前slab块传输完后停止数据传输,并清空缓冲区 I2S_TRIGGER_DRAIN:在所有已缓存的slab块传输完后停止传输。 I2S_TRIGGER_DROP: 立即停止传输并丢弃所有数据 I2S_TRIGGER_PREPARE: 在传输出错的情况下使用该命令恢复I2S状态

结构体 链接到标题

struct i2s_config

struct i2s_config {
	uint8_t word_size;    //采样位数,8,16,24,32
	uint8_t channels;    //音频通道数
	i2s_fmt_t format;    //音频数据格式
	i2s_opt_t options;    //I2S控制器的操作模式
	uint32_t frame_clk_freq;    //采样率
	struct k_mem_slab *mem_slab;    //slab内存池
	size_t block_size;            //slab每个内存块的字节数
	int32_t timeout;            //传输超时时间ms, 0表示不等待立即返回,SYS_FOREVER_MS表示一直等待
};

函数 链接到标题

int i2s_configure(const struct device *dev, enum i2s_dir dir, const struct i2s_config *cfg) 对I2S进行配置,可以分别对RX/TX或是两者同时配置,配置时会指定采样率, 采样bit数,读写阻塞时间等,具体可以参考struct i2s_config 参数 dev 是指向I2S设备的指针 dir 指定要配置的I2S数据流方向 cfg 是一个指向struct i2s_config类型的指针,用于配置I2S总线的参数

返回值 0表示成功,负值表示失败。

const struct i2s_config *i2s_config_get(const struct device *dev, enum i2s_dir dir) 获取I2S的配置信息 参数 dev 是指向I2S设备的指针 dir 指定要获取配置的I2S数据流方向

返回值 指向i2s配置。

**int i2s_read(const struct device *dev, void *mem_block, size_t size) 读取i2s数据,I2S接口接收到的数据存储在RX队列中,该队列由rx_mem_slab(由i2s_configure配置)预先分配的内存块组成。RX内存块的所有权将传递给用户,用户读取完数据后负责对其进行释放。如果RX队列中没有数据,函数将按照i2s_config配置的超时时间进行阻塞等待。

参数 dev 是指向I2S设备的指针 mem_block 输出装有I2S数据内存的指针,该内存由i2s驱动分配,由用户释放 size 有效数据大小,以字节为单位

返回值 0表示成功,负值表示失败。

**int i2s_buf_read(const struct device *dev, void buf, size_t size)i2s_read功能类似,区别是:读取i2s数据到外部buf,该函数会从RX队列中删除一个内存块,并将数据拷贝到buf中,然后自动释放掉这个内存块。 注意:要由用户保证buf的大小比i2s使用的slab内存块大. 参数 dev 是指向I2S设备的指针 buf 用于保存读出数据的内存,由用户分配管理 size 读出数据的大小,以字节为单位

返回值 0表示成功,负值表示失败。

**int i2s_write(const struct device dev, void mem_block, size_t size) 发送i2s数据,用户从tx_mem_slab(该slab在i2s_configure是配置)预分配的内存块,将数据放入该内存块传入发送。i2s_write在所有数据传输完成后释放该内存块。如果I2S TX队列不空闲,函数将按照i2s_config配置的超时时间进行阻塞等待

参数 dev 是指向I2S设备的指针 mem_block 装有I2S数据内存的指针,该内存由用户分配,由驱动释放 size 有效数据大小,以字节为单位

返回值 0表示成功,负值表示失败。

**int i2s_buf_write(const struct device dev, void buf, size_t size)i2s_write功能类似,区别是:写buf内的数据到i2s,该函数会从TX队列中分配一个内存块,并将buf的数据拷贝到内存块内,数据发送完后自动释放掉这个内存块。 参数 dev 是指向I2S设备的指针 buf 发送数据的内存指针,由用户分配管理 size 发送数据的大小,以字节为单位

返回值 0表示成功,负值表示失败。

*int i2s_trigger(const struct device dev, enum i2s_dir dir, enum i2s_trigger_cmd cmd) 发送I2S触发命令,控制I2S的启动停止等 参数 dev 是指向I2S设备的指针 dir 指定要配置的I2S数据流方向 cmdenum i2s_trigger_cmd定义的触发命令

使用示例 链接到标题

Zephyr中I2S的操作比较简单,以mm_feather为例

1.在设备树中启用i2s接口 链接到标题

mm_feather使用rt1062 soc,I2S在设备树中叫做sai, 在设备树中添加如下代码启用sai1

i2s_rxtx: &sai1 {
	status = "okay";
};

2.配置prj.conf中开启对I2S的支持 链接到标题

CONFIG_I2S=y
CONFIG_I2S_MCUX_SAI=y

3.示例代码,从I2S读取数据再播放出去 链接到标题

//I2S配置参数
#define SAMPLE_FREQUENCY    16000
#define SAMPLE_BIT_WIDTH    16
#define BYTES_PER_SAMPLE    sizeof(int16_t)
#define NUMBER_OF_CHANNELS  2
#define TIMEOUT             1000

//Slab大小定义
//每个block保存100ms的PCM数据
#define SAMPLES_PER_BLOCK   ((SAMPLE_FREQUENCY / 10) * NUMBER_OF_CHANNELS)
#define INITIAL_BLOCKS      2

#define BLOCK_SIZE  (BYTES_PER_SAMPLE * SAMPLES_PER_BLOCK)
#define BLOCK_COUNT (INITIAL_BLOCKS + 2)

static K_MEM_SLAB_DEFINE(mem_slab, BLOCK_SIZE, BLOCK_COUNT, 4);

//方法设备树的i2s_rxtx节点
char *dev_name = DT_NAME(i2s_rxtx);
struct device *dev = device_get_binding(dev_name);

//配置的数据
struct i2s_config config;
config.word_size = SAMPLE_BIT_WIDTH;
config.channels = NUMBER_OF_CHANNELS;
config.format = I2S_FMT_DATA_FORMAT_I2S;
config.options = I2S_OPT_BIT_CLK_MASTER | I2S_OPT_FRAME_CLK_MASTER;
config.frame_clk_freq = SAMPLE_FREQUENCY;
config.mem_slab = &mem_slab;
config.block_size = BLOCK_SIZE;
config.timeout = TIMEOUT;    //读写等待时间为1秒
//分别对RX/TX进行配置
i2s_configure(dev, I2S_DIR_RX, &config);
i2s_configure(dev, I2S_DIR_TX, &config);

//启动
i2s_trigger(i2s_dev_rx, I2S_DIR_RX, I2S_TRIGGER_START);
i2s_trigger(i2s_dev_tx, I2S_DIR_TX, I2S_TRIGGER_START);

while (1) {
	void *mem_block;
	uint32_t block_size;
	int ret;
    //读取数据
	ret = i2s_read(dev, &mem_block, &block_size);
	if (ret < 0) {
        shell_print(shell, "Failed to read data: %d\n", ret);
        break;
    }

    //写入数据
    ret = i2s_write(dev, mem_block, block_size);
    if (ret < 0) {
        shell_print(shell, "Failed to write data: %d\n", ret);
        break;
    }

    if(stoped){
        break;
    }
}
//停止
i2s_trigger(i2s_dev_rx, I2S_DIR_RX, I2S_TRIGGER_STOP);
i2s_trigger(i2s_dev_tx, I2S_DIR_TX, I2S_TRIGGER_STOP);

驱动实现接口 链接到标题

和Zephyr其它驱动一样,I2S驱动的实现者只需要将i2s.h中规定好的API实现,并进行注册,就可以通过I2S接口进行访问。参考Zephyr驱动模型实现方式

接口简要 链接到标题

I2S的驱动实现接口定义在i2s.h中,如下

__subsystem struct i2s_driver_api {
    //配置I2S
	int (*configure)(const struct device *dev, enum i2s_dir dir,
			 const struct i2s_config *cfg);
    //获取配置信息
	const struct i2s_config *(*config_get)(const struct device *dev,
				  enum i2s_dir dir);
    //读出I2S数据
	int (*read)(const struct device *dev, void **mem_block, size_t *size);
    //向I2S写入数据
	int (*write)(const struct device *dev, void *mem_block, size_t size);
    //发送Trigger命令
	int (*trigger)(const struct device *dev, enum i2s_dir dir,
		       enum i2s_trigger_cmd cmd);
};

实现驱动时,完成以上函数指针原型的API,然后创建struct i2s_driver_api变量,再通过DEVICE_DT_INST_DEFINE进行注册,就完成了I2S驱动的添加。 对于配置中的i2s_fmt_ti2s_opt_t还有触发控制的enum i2s_trigger_cmd驱动可以根据自身情况和硬件限制有选择的进行支持。

内存管理要求 链接到标题

由于I2S的接口定义需要在用户和驱动之间传递内存块,slab内存块由用户和驱动共同管理,因此驱动实现时需要考虑内存块的管理,下图是一个内存管理的原型 对于发送:由用户分配slab内存块,填充好发送数据后将内存块送到驱动,驱动维护一个TX Queue,当Queue还有空间时内存块被加入到TX Queue中,如果没有空间,write的实现将要求阻塞,直到硬件将数据发送后TX Queue有空间为止。 对于接收:当硬件有数据产生时,会从slab中分配内存块,数据装入后将内存块加入到RX Queue,用户读取数据时直接冲RX Queue中取走内存块,用户读完数据后,将内存块释放回slab. RX Queue为空时用户将等待,当RX Queue满时,硬件仍然会产生数据,具体的操作行为由驱动实现者来定义,通常I2S是产生的数据具有时间顺序,因此会做成丢弃老数据的动作

状态机 链接到标题

i2s.h中定义了I2S的接口状态

enum i2s_state {
	I2S_STATE_NOT_READY,    //接口还没有被配置
	I2S_STATE_READY,        //i2s接口已经被配置或初始化,但还没有开始发送或接收数据
	I2S_STATE_RUNNING,       //i2s接口正在发送或接收数据
	I2S_STATE_STOPPING,    //i2s接口正在清空发送列队
	I2S_STATE_ERROR,        //表示i2s接口发生了错误,如缓冲区溢出或欠流
};

接口定义有描述接口动作和状态机的关系,因此我们通常会按照该状态机来实现驱动,参考如下图

参考 链接到标题

https://docs.zephyrproject.org/3.3.0/hardware/peripherals/audio/i2s.html