本文介绍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
10struct 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数据流方向cmd
由enum i2s_trigger_cmd
定义的触发命令
使用示例
Zephyr中I2S的操作比较简单,以mm_feather为例
1.在设备树中启用i2s接口
mm_feather使用rt1062 soc,I2S在设备树中叫做sai, 在设备树中添加如下代码启用sai11
2
3i2s_rxtx: &sai1 {
status = "okay";
};
2.配置prj.conf中开启对I2S的支持
1 | CONFIG_I2S=y |
3.示例代码,从I2S读取数据再播放出去
1 | //I2S配置参数 |
驱动实现接口
和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_t
和i2s_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
7enum 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