本文主要针对在IMX6Q平台上实现的XFM10621六麦克阵列驱动做了一些介绍和说明,因为科大讯飞没有一个可参考的通用驱动,所以自己就在IMX6Q上实现了一下,相信可以给很多感兴趣和有需求的朋友作为参考,具体的驱动源码可以参看附件部分。
1. 环境介绍
硬件平台 | IMX6Q |
Android版本 | 5.1 |
Linux版本 | 3.14.52 |
麦克阵列模块 | 讯飞XFM10621六麦阵列 |
2. Linux框架简介
音频驱动主要涉及Linux ASoc框架,ASoc基于ALSA框架,由Codec驱动,Platform驱动和Machine驱动组成。Codec驱动负责编解码以及音频硬件的实际控制,Platform驱动主要负责CPU和音频模块的通信接口的控制,Machine驱动协调Codec驱动和Platform驱动对用户层提供声卡接口。
下表描述了我们驱动源码的位置:
驱动 | 源码位置 |
Codec | kernel_imx/sound/soc/codecs/ifly-dmic.c |
Platform | kernel_imx/sound/soc/fsl/fsl_ssi.c |
Machine | kernel_imx/sound/soc/fsl/fsl_dmic.c |
DTS | kernel_imx/arch/arm/boot/dts/imx6qdl-sabresd.dtsi |
3. codec驱动
我们的Codec驱动主要职责是处理唤醒中断,对用户层上报唤醒信息及唤醒角度,向ASoc框架注册Codec驱动。
以I2C驱动为入口,在I2C驱动probe函数中主要做的事情包含:
-
初始化codec驱动数据结构;
-
解析设备树获取中断引脚的gpio;
-
向Linux输入子系统注册一个输入设备,用来上报唤醒信息;
-
向Linux ASoc框架注册一个Codec驱动。
主要数据结构及描述如下:
struct ifly_dmic_data {
int irq; //通过gpio转化来的中断号
int angle; //用来记录唤醒角度
spinlock_t lock; //中断上半部分保护数据的自旋锁
struct timespec stamp; //被唤醒的时间戳
struct work_struct work; //中断下半部分的工作队列
struct snd_soc_codec *codec; //指向codec数据结构
struct i2c_client *client; //指向i2c客户端
struct input_dev *input_dev; //指向输入设备
};
注册codec驱动时提供的dai信息如下:
static struct snd_soc_dai_driver dmic_dai = {
.name = "dmic-hifi",
.capture = {
.stream_name = "Capture",
.channels_min = 2,
.channels_max = 2,
.rates = SNDRV_PCM_RATE_16000,
.formats = SNDRV_PCM_FMTBIT_S16_LE,
},
};
在codec驱动的probe函数中会:
(1)在sysfs中创建接口供用户层获取唤醒角度;
(2)保存codec设备指针;
(3)注册唤醒中断;
(4)初始化中断下半部分的工作队列,并使能中断。
唤醒中断被触发后,上半部分会根据上次的时间戳过滤掉抖动,接着调度下半部分的工作队列,在中断下半部分会点亮对应唤醒角度的led灯,并保存唤醒的角度,然后上报KEY_RECORD键值告知用户层可以获取录音数据了。
4. platform驱动
platform驱动是飞思卡尔写的,主要负责dma和i2s接口的音频数据传输,每个硬件平台都有针对自己平台的platform驱动。写codec驱动和machine驱动还是需要分析下这边方便定位问题。
5. machine驱动
machine驱动以平台虚拟总线驱动的形式注册到系统,其probe函数的主要工作如下:
(1)解析设备树获取codec信息;
(2)配置imx6q的audmux控制器设置总线复用通路;
(3)向Linux ASoc框架注册一个声卡设备。
machine驱动向ASoc注册声卡的时候也需要提供dai信息用来粘合platform驱动和codec驱动。
6. 附件
codec驱动源码:
/* sound/soc/codecs/ifly-dmic.c * Created by Neo * 2017/7/17 * */
#include <linux/i2c.h> #include <linux/slab.h> #include <linux/module.h> #include <linux/delay.h> #include <sound/core.h> #include <sound/pcm.h> #include <sound/soc.h> #include <sound/soc-dapm.h> #include <linux/gpio.h> #include <linux/interrupt.h> #include <linux/workqueue.h> #include <linux/of_gpio.h> #include <linux/time.h> #include <linux/spinlock.h> #include <linux/fs.h> #include <linux/input.h>
struct ifly_dmic_data { int irq; int angle; spinlock_t lock; struct timespec stamp; struct work_struct work; struct snd_soc_codec *codec; struct i2c_client *client; struct input_dev *input_dev; };
static struct snd_soc_dai_driver dmic_dai = { .name = "dmic-hifi", .capture = { .stream_name = "Capture", .channels_min = 2, .channels_max = 2, .rates = SNDRV_PCM_RATE_16000, .formats = SNDRV_PCM_FMTBIT_S16_LE, }, };
static unsigned int ifly_reg_read(struct i2c_client *client, unsigned int reg) { unsigned char buf[4] = {0}; unsigned int ret = 0; struct i2c_msg msgs[] = { { .addr = client->addr, .flags = 0, .len = 1, .buf = (unsigned char *)®, }, { .addr = client->addr, .flags = I2C_M_RD, .len = 4, .buf = buf, }, };
i2c_transfer(client->adapter, msgs, sizeof(msgs)/sizeof(msgs[0])); ret = (buf[0] | (buf[1] << 8) | (buf[2] << 16) | (buf[3] << 24));
return ret; }
static int ifly_reg_write(struct i2c_client *client, unsigned int reg, unsigned int value) { unsigned char buf[5] = {0}; struct i2c_msg msgs[] = { { .addr = client->addr, .flags = 0, .len = 5, .buf = buf, }, };
buf[0] = reg & 0xff; buf[1] = value & 0xff; buf[2] = (value >> 8) & 0xff; buf[3] = (value >> 16) & 0xff; buf[4] = (value >> 24) & 0xff;
return i2c_transfer(client->adapter, msgs, sizeof(msgs)/sizeof(msgs[0])); }
static int ifly_dmic_get_wakeup_angle(struct i2c_client *client) { int ret = 0;
ret = ifly_reg_write(client, 0x00, 0x1000); mdelay(200); ret = ifly_reg_read(client, 0x00); ret = ifly_reg_read(client, 0x01);
return ret; }
static struct snd_soc_codec_driver soc_dmic;
//#define WAKEUP_LED_KERNEL void ifly_dmic_work(struct work_struct *work) { #ifdef WAKEUP_LED_KERNEL struct file *filp = (struct file*)-ENOENT; mm_segment_t oldfs; char *led_class = "/sys/class/leds"; char *led_color = "green"; char *led_path = NULL; char *led_seq[] = {"01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"}; #endif struct ifly_dmic_data *data = container_of(work, typeof(*data), work); int angle = 0;
angle = ifly_dmic_get_wakeup_angle(data->client); dev_warn(data->codec->dev, "ifly-dmic wakeup (%d)\n", angle);
if ((angle <= 360) && (angle >= 0)) { angle %= 360; #ifdef WAKEUP_LED_KERNEL oldfs = get_fs(); set_fs(KERNEL_DS); led_path = kasprintf(GFP_KERNEL, "%s/lp55231-%s-%s/brightness", led_class, led_seq[angle/30], led_color); if (led_path) { filp = filp_open(led_path, O_RDWR, S_IRUSR | S_IWUSR); if (!IS_ERR_OR_NULL(filp)) { generic_file_llseek(filp, 0, SEEK_SET); if (vfs_write(filp, "255", 3, &filp->f_pos) != 3) { dev_err(data->codec->dev, "set led[%s] failed\n", led_seq[angle/30]); } filp_close(filp, NULL); filp = (struct file*)-ENOENT; } kfree(led_path); led_path = NULL; } if ((angle/30) != (data->angle/30)) { led_path = kasprintf(GFP_KERNEL, "%s/lp55231-%s-%s/brightness", led_class, led_seq[data->angle/30], led_color); if (led_path) { filp = filp_open(led_path, O_RDWR, S_IRUSR | S_IWUSR); if (!IS_ERR_OR_NULL(filp)) { generic_file_llseek(filp, 0, SEEK_SET); if (vfs_write(filp, "0", 1, &filp->f_pos) != 1) { dev_err(data->codec->dev, "clear led[%s] failed\n", led_seq[data->angle/30]); } filp_close(filp, NULL); filp = (struct file*)-ENOENT; } kfree(led_path); led_path = NULL; } } set_fs(oldfs); #endif data->angle = angle; input_report_key(data->input_dev, KEY_RECORD, 1); input_sync(data->input_dev); mdelay(100); input_report_key(data->input_dev, KEY_RECORD, 0); input_sync(data->input_dev); } }
static irqreturn_t ifly_dmic_isr(int irq, void *p) { struct timespec now; unsigned long flags = 0; struct ifly_dmic_data *data = p;
spin_lock_irqsave(&data->lock, flags); getnstimeofday(&now); if ((timespec_to_ns(&now) - timespec_to_ns(&data->stamp)) >= 100000000) { schedule_work(&data->work); } getnstimeofday(&data->stamp); spin_unlock_irqrestore(&data->lock, flags);
return IRQ_HANDLED; }
static ssize_t angle_show(struct device *dev, struct device_attribute *attr, char *buf) { struct ifly_dmic_data *data = dev_get_drvdata(dev);
sprintf(buf, "%d\n", data->angle); return (strlen(buf) > 0) ? (strlen(buf) + 1) : 0; }
static DEVICE_ATTR(angle, 0444, angle_show, NULL);
static int ifly_dmic_probe(struct snd_soc_codec *codec) { int ret = 0; struct ifly_dmic_data *data = NULL;
data = dev_get_drvdata(codec->dev); if (data) { device_create_file(codec->dev, &dev_attr_angle); data->codec = codec; spin_lock_init(&data->lock); request_irq(data->irq, ifly_dmic_isr, IRQF_TRIGGER_FALLING | IRQF_SHARED, "ifly-irq", data); INIT_WORK(&data->work, ifly_dmic_work); enable_irq_wake(data->irq); }
return ret; }
static int ifly_dmic_remove(struct snd_soc_codec *codec) { int ret = 0; struct ifly_dmic_data *data = NULL;
data = dev_get_drvdata(codec->dev); if (data) { disable_irq(data->irq); cancel_work_sync(&data->work); free_irq(data->irq, data); data->codec = NULL; }
return ret; }
static int dmic_dev_probe(struct i2c_client *client, const struct i2c_device_id *id) { int gpio = -1; int error = 0; struct ifly_dmic_data *data = NULL; struct input_dev *input_dev = NULL;
memset(&soc_dmic, 0, sizeof(soc_dmic)); soc_dmic.probe = ifly_dmic_probe; soc_dmic.remove = ifly_dmic_remove;
gpio = of_get_named_gpio(client->dev.of_node, "gpio-irq", 0); if (!gpio_is_valid(gpio)) { dev_warn(&client->dev, "invalid gpio-irq(%d)!!!\n", gpio); return -EINVAL; }
data = kzalloc(sizeof(*data), GFP_KERNEL); if (!data) { dev_err(&client->dev, "alloc private data failed!\n"); error = -ENOMEM; goto err_alloc_data; } input_dev = devm_input_allocate_device(&client->dev); if (!input_dev) { dev_err(&client->dev, "alloc input device failed!\n"); error = -ENOMEM; goto err_alloc_input; }
input_dev->name = "ifly-dmic"; input_dev->id.bustype = BUS_I2C; input_dev->dev.parent = &client->dev;
__set_bit(EV_KEY, input_dev->evbit); __set_bit(KEY_RECORD, input_dev->keybit);
data->irq = gpio_to_irq(gpio); data->client = client; data->input_dev = input_dev; dev_set_drvdata(&client->dev, data);
error = input_register_device(data->input_dev); if (error) { dev_err(&client->dev, "register input device failed!\n"); goto err_register_input; }
return snd_soc_register_codec(&client->dev, &soc_dmic, &dmic_dai, 1);
err_register_input: input_free_device(input_dev); data->input_dev = NULL; err_alloc_input: kfree(data); data = NULL; err_alloc_data: return error; }
static int dmic_dev_remove(struct i2c_client *client) { struct ifly_dmic_data *data = NULL;
data = dev_get_drvdata(&client->dev);
input_unregister_device(data->input_dev); input_free_device(data->input_dev); data->input_dev = NULL; snd_soc_unregister_codec(&client->dev);
kfree(data); data = NULL;
return 0; }
static const struct i2c_device_id dmic_id[] = { {"ifly-dmic", 0}, {}, };
MODULE_DEVICE_TABLE(i2c, dmic_id);
static const struct of_device_id dmic_dt_ids[] = { { .compatible = "fsl,ifly-dmic", }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, dmic_dt_ids);
static struct i2c_driver dmic_driver = { .driver = { .name = "ifly-dmic", .owner = THIS_MODULE, .of_match_table = dmic_dt_ids, }, .probe = dmic_dev_probe, .remove = dmic_dev_remove, .id_table = dmic_id, };
module_i2c_driver(dmic_driver);
MODULE_DESCRIPTION("iFly DMIC driver"); MODULE_AUTHOR("Neo <neo.hou@qq.com>"); MODULE_LICENSE("GPL"); |
machine驱动源码:
/* sound/soc/fsl/fsl_dmic.c * Neo neo.hou@qq.com * */
#include <linux/init.h> #include <linux/module.h> #include <linux/platform_device.h> #include <linux/of.h> #include <linux/of_platform.h> #include <sound/soc.h>
#include "imx-audmux.h"
#define DAI_NAME_SIZE 32
struct fsl_dmic_data { struct snd_soc_dai_link dai; struct snd_soc_card card; struct platform_device *codec_dev; char codec_dai_name[DAI_NAME_SIZE]; char platform_name[DAI_NAME_SIZE]; unsigned int clk_frequency; };
static int fsl_dmic_audmux_config(struct platform_device *pdev) { struct device_node *np = pdev->dev.of_node; int int_port, ext_port; int ret;
ret = of_property_read_u32(np, "mux-int-port", &int_port); if (ret) { dev_err(&pdev->dev, "mux-int-port missing or invalid\n"); return ret; } ret = of_property_read_u32(np, "mux-ext-port", &ext_port); if (ret) { dev_err(&pdev->dev, "mux-ext-port missing or invalid\n"); return ret; }
/* * The port numbering in the hardware manual starts at 1, while * the audmux API expects it starts at 0. */ int_port--; ext_port--; ret = imx_audmux_v2_configure_port(int_port, IMX_AUDMUX_V2_PTCR_SYN | IMX_AUDMUX_V2_PTCR_RFSEL(ext_port | 0x08) | IMX_AUDMUX_V2_PTCR_RCSEL(ext_port | 0x08) | IMX_AUDMUX_V2_PTCR_TFSEL(ext_port | 0x08) | IMX_AUDMUX_V2_PTCR_TCSEL(ext_port | 0x08) | IMX_AUDMUX_V2_PTCR_TFSDIR | IMX_AUDMUX_V2_PTCR_TCLKDIR | IMX_AUDMUX_V2_PTCR_RFSDIR | IMX_AUDMUX_V2_PTCR_RCLKDIR, IMX_AUDMUX_V2_PDCR_RXDSEL(ext_port)); if (ret) { dev_err(&pdev->dev, "audmux internal port setup failed\n"); return ret; }
ret = imx_audmux_v2_configure_port(ext_port, 0, IMX_AUDMUX_V2_PDCR_RXD(int_port)); if (ret) { dev_err(&pdev->dev, "audmux external port setup failed\n"); return ret; }
return 0; }
static int asoc_dmic_probe(struct platform_device *pdev) { int ret = 0; struct fsl_dmic_data *data = NULL; struct device_node *cpu_np = NULL; struct device_node *codec_np = NULL;
cpu_np = of_parse_phandle(pdev->dev.of_node, "cpu-dai", 0); if (!cpu_np) { dev_err(&pdev->dev, "cpu dai phandle missing or invalid\n"); ret = -EINVAL; goto fail; } codec_np = of_parse_phandle(pdev->dev.of_node, "audio-codec", 0); if (!codec_np) { dev_err(&pdev->dev, "audio codec phandle missing or invalid\n"); ret = -EINVAL; goto fail; }
if (strstr(cpu_np->name, "ssi")) { ret = fsl_dmic_audmux_config(pdev); if (ret) goto fail; }
data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL); if (!data) { ret = -ENOMEM; goto fail; }
data->dai.name = "HiFi"; data->dai.stream_name = "HiFi"; data->dai.codec_dai_name = "dmic-hifi"; data->dai.codec_of_node = codec_np; data->dai.cpu_of_node = cpu_np; data->dai.platform_of_node = cpu_np; data->dai.dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF | SND_SOC_DAIFMT_CBM_CFM;
data->card.dai_link = &data->dai; data->card.num_links = 1; data->card.dev = &pdev->dev;
ret = snd_soc_of_parse_card_name(&data->card, "model"); if (ret) goto err_parse_card_name;
platform_set_drvdata(pdev, &data->card); snd_soc_card_set_drvdata(&data->card, data);
ret = devm_snd_soc_register_card(&pdev->dev, &data->card); if (ret) { dev_err(&pdev->dev, "snd_soc_register_card failed (%d)\n", ret); goto err_register_card; }
goto fail; err_register_card: platform_device_unregister(data->codec_dev); err_register_codec: err_parse_card_name: devm_kfree(&pdev->dev, data); fail: if (cpu_np) of_node_put(cpu_np);
return ret; }
static int asoc_dmic_remove(struct platform_device *pdev) { struct snd_soc_card *card = platform_get_drvdata(pdev); struct fsl_dmic_data *data = snd_soc_card_get_drvdata(card);
platform_device_unregister(data->codec_dev); devm_kfree(&pdev->dev, data); }
static const struct of_device_id fsl_dmic_of_match[] = { { .compatible = "fsl,ifly-dmic", }, { } }; MODULE_DEVICE_TABLE(of, fsl_dmic_of_match);
static struct platform_driver asoc_dmic_driver = { .driver = { .name = "fsl-dmic", .owner = THIS_MODULE, .of_match_table = fsl_dmic_of_match, }, .probe = asoc_dmic_probe, .remove = asoc_dmic_remove, };
static int __init asoc_dmic_init(void) { return platform_driver_register(&asoc_dmic_driver); }
static void __exit asoc_dmic_exit(void) { platform_driver_unregister(&asoc_dmic_driver); }
module_init(asoc_dmic_init); module_exit(asoc_dmic_exit); MODULE_AUTHOR("Neo <neo.hou@qq.com>"); MODULE_DESCRIPTION("Freescale DMIC Machine Driver"); MODULE_LICENSE("GPL"); |
DTS源码:
dmic_codec: ifly-dmic@47 {
compatible = "fsl,ifly-dmic";
reg = <0x47>;
gpio-irq = <&gpio5 20 1>;
};
sound-dmic {
compatible = "fsl,ifly-dmic";
model = "fsl-audio-dmic";
cpu-dai = <&ssi2>;
audio-codec = <&dmic_codec>;
mux-int-port = <2>;
mux-ext-port = <3>;
};