Zephyr I2S驱动模型

Creative Commons
本作品采用知识共享署名

本文介绍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

1
2
3
4
5
6
7
8
9
10
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

1
2
3
i2s_rxtx: &sai1 {
status = "okay";
};

2.配置prj.conf中开启对I2S的支持

1
2
CONFIG_I2S=y
CONFIG_I2S_MCUX_SAI=y

3.示例代码,从I2S读取数据再播放出去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
//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中,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__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的接口状态

1
2
3
4
5
6
7
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