AC97 is a bus for sound usage. It enables for a AC97 AC-Link to link one controller to 0 to 4 AC97 codecs.
The goal of this new implementation is to implement a device/driver model for AC97, with an automatic scan of the bus and automatic discovery of AC97 codec devices.
Signed-off-by: Robert Jarzmik robert.jarzmik@free.fr --- include/sound/ac97/codec.h | 98 ++++++++++++ include/sound/ac97/compat.h | 21 +++ include/sound/ac97/controller.h | 39 +++++ sound/ac97/Kconfig | 9 ++ sound/ac97/Makefile | 5 + sound/ac97/ac97_core.h | 10 ++ sound/ac97/bus.c | 330 ++++++++++++++++++++++++++++++++++++++++ sound/ac97/codec.c | 15 ++ sound/ac97/snd_ac97_compat.c | 104 +++++++++++++ 9 files changed, 631 insertions(+) create mode 100644 include/sound/ac97/codec.h create mode 100644 include/sound/ac97/compat.h create mode 100644 include/sound/ac97/controller.h create mode 100644 sound/ac97/Kconfig create mode 100644 sound/ac97/Makefile create mode 100644 sound/ac97/ac97_core.h create mode 100644 sound/ac97/bus.c create mode 100644 sound/ac97/codec.c create mode 100644 sound/ac97/snd_ac97_compat.c
diff --git a/include/sound/ac97/codec.h b/include/sound/ac97/codec.h new file mode 100644 index 000000000000..4b8b3e570892 --- /dev/null +++ b/include/sound/ac97/codec.h @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2016 Robert Jarzmik robert.jarzmik@free.fr + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + */ +#ifndef AC97_CODEC_H +#define AC97_CODEC_H + +#include <linux/device.h> + +#define AC97_ID(vendor_id1, vendor_id2) \ + (((vendor_id1 & 0xffff) << 16) | (vendor_id2 & 0xffff)) +#define AC97_DRIVER_ID(vendor_id1, vendor_id2, mask_id1, mask_id2, _data) \ + { .id = ((vendor_id1 & 0xffff) << 16) | (vendor_id2 & 0xffff), \ + .mask = ((mask_id1 & 0xffff) << 16) | (mask_id2 & 0xffff), \ + .data = _data } + +#define to_ac97_device(d) container_of(d, struct ac97_codec_device, dev) +#define to_ac97_driver(d) container_of(d, struct ac97_codec_driver, driver) + +struct ac97_controller; + +/** + * struct ac97_id - matches a codec device and driver on an ac97 bus + * @id: The significant bits if the codec vendor ID1 and ID2 + * @mask: Bitmask specifying which bits of the id field are significant when + * matching. A driver binds to a device when : + * ((vendorID1 << 8 | vendorID2) & (mask_id1 << 8 | mask_id2)) == id. + * @data: Private data used by the driver. + */ +struct ac97_id { + unsigned int id; + unsigned int mask; + void *data; +}; + +/** + * ac97_codec_device - a ac97 codec + * @dev: the code device + * @vendor_id: the vendor_id of the codec, as sensed on the AC-link + * @num: the codec number, 0 is primary, 1 is first slave, etc ... + * @ac97_ctrl: ac97 digital controller on the same AC-link + * + * This is the device instanciated for each codec living on a AC-link. There are + * normally 0 to 4 codec devices per AC-link, and all of them are controlled by + * an AC97 digital controller. + */ +struct ac97_codec_device { + struct device dev; /* Must stay first member */ + unsigned int vendor_id; + unsigned int num; + struct list_head list; + struct ac97_controller *ac97_ctrl; +}; + +/** + * ac97_codec_driver - a ac97 codec driver + * @driver: the device driver structure + * @probe: the function called when a ac97_codec_device is matched + * @remove: the function called when the device is unbound/removed + * @suspend: suspend function (might be NULL) + * @resume: resume function (might be NULL) + * @shutdown: shutdown function (might be NULL) + * @id_table: ac97 vendor_id match table, { } member terminated + */ +struct ac97_codec_driver { + struct device_driver driver; + int (*probe)(struct ac97_codec_device *); + int (*remove)(struct ac97_codec_device *); + int (*suspend)(struct ac97_codec_device *); + int (*resume)(struct ac97_codec_device *); + void (*shutdown)(struct ac97_codec_device *); + struct ac97_id *id_table; +}; + +int ac97_codec_driver_register(struct ac97_codec_driver *drv); +void ac97_codec_driver_unregister(struct ac97_codec_driver *drv); + +static inline struct device * +ac97_codec_dev2dev(const struct ac97_codec_device *adev) +{ + return (struct device *)(adev); +} + +static inline void *ac97_get_drvdata(const struct ac97_codec_device *adev) +{ + return dev_get_drvdata(ac97_codec_dev2dev(adev)); +} + +static inline void ac97_set_drvdata(const struct ac97_codec_device *adev, + void *data) +{ + dev_set_drvdata(ac97_codec_dev2dev(adev), data); +} + +#endif diff --git a/include/sound/ac97/compat.h b/include/sound/ac97/compat.h new file mode 100644 index 000000000000..bf611f572f2d --- /dev/null +++ b/include/sound/ac97/compat.h @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2016 Robert Jarzmik robert.jarzmik@free.fr + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This file is for backward compatibility with snd_ac97 structure and its + * multiple usages, such as the snd_ac97_bus and snd_ac97_build_ops. + * + */ +#ifndef AC97_COMPAT_H +#define AC97_COMPAT_H + +#include <sound/ac97_codec.h> +#include <sound/soc.h> + +struct snd_ac97 *compat_alloc_snd_ac97_codec(struct snd_soc_codec *codec); +void compat_release_snd_ac97_codec(struct snd_ac97 *ac97); + +#endif diff --git a/include/sound/ac97/controller.h b/include/sound/ac97/controller.h new file mode 100644 index 000000000000..f1e5e645f5ef --- /dev/null +++ b/include/sound/ac97/controller.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2016 Robert Jarzmik robert.jarzmik@free.fr + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + */ +#ifndef AC97_CONTROLLER_H +#define AC97_CONTROLLER_H + +#include <linux/list.h> + +struct device; +struct ac97_codec_device; + +struct ac97_controller_ops { + void (*reset)(struct ac97_codec_device *ac97); + void (*warm_reset)(struct ac97_codec_device *ac97); + int (*write)(struct ac97_codec_device *ac97, unsigned short reg, + unsigned short val); + int (*read)(struct ac97_codec_device *ac97, unsigned short reg); + void (*wait)(struct ac97_codec_device *ac97); + void (*init)(struct ac97_codec_device *ac97); +}; + +struct ac97_controller { + const struct ac97_controller_ops *ops; + struct list_head controllers; + struct device *dev; + int bus_idx; + int bound_codecs; + struct list_head codecs; +}; + +int ac97_digital_controller_register(const struct ac97_controller_ops *ops, + struct device *dev); +int ac97_digital_controller_unregister(const struct device *dev); + +#endif diff --git a/sound/ac97/Kconfig b/sound/ac97/Kconfig new file mode 100644 index 000000000000..fd2c2d031e62 --- /dev/null +++ b/sound/ac97/Kconfig @@ -0,0 +1,9 @@ +# +# PCI configuration +# + +config AC97 + bool "AC97 bus" + help + Say Y here if you want to have AC97 devices, which are sound oriented + devices around an AC-Link. diff --git a/sound/ac97/Makefile b/sound/ac97/Makefile new file mode 100644 index 000000000000..5575909d46e2 --- /dev/null +++ b/sound/ac97/Makefile @@ -0,0 +1,5 @@ +# +# make for AC97 bus drivers +# + +obj-y += bus.o codec.o snd_ac97_compat.o diff --git a/sound/ac97/ac97_core.h b/sound/ac97/ac97_core.h new file mode 100644 index 000000000000..db6e27288357 --- /dev/null +++ b/sound/ac97/ac97_core.h @@ -0,0 +1,10 @@ +/* + * Copyright (C) 2016 Robert Jarzmik robert.jarzmik@free.fr + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + */ + +unsigned int ac97_bus_scan_one(struct ac97_controller *ac97, + int codec_num); diff --git a/sound/ac97/bus.c b/sound/ac97/bus.c new file mode 100644 index 000000000000..f9bf5632d4aa --- /dev/null +++ b/sound/ac97/bus.c @@ -0,0 +1,330 @@ +/* + * Copyright (C) 2016 Robert Jarzmik robert.jarzmik@free.fr + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + */ + +#include <linux/module.h> +#include <linux/device.h> +#include <linux/list.h> +#include <linux/mutex.h> +#include <linux/pm.h> +#include <linux/slab.h> +#include <sound/ac97/codec.h> +#include <sound/ac97/controller.h> +#include <sound/ac97/regs.h> + +#define AC97_BUS_MAX_CODECS 4 + +/* + * Protects ac97_controllers and each ac97_controller structure. + */ +static DEFINE_MUTEX(ac97_controllers_mutex); +static LIST_HEAD(ac97_controllers); +static int ac97_bus_idx; + +struct bus_type ac97_bus_type; + +static struct ac97_codec_device * +ac97_codec_find(struct ac97_controller *ac97_ctrl, int codec_num) +{ + struct ac97_codec_device *codec; + + list_for_each_entry(codec, &ac97_ctrl->codecs, list) + if (codec->num == codec_num) + return codec; + + return NULL; +} + +static void ac97_codec_release(struct device *dev) +{ + struct ac97_codec_device *codec; + + codec = container_of(dev, struct ac97_codec_device, dev); + list_del(&codec->list); + kfree(codec); +} + +static int ac97_codec_add(struct ac97_controller *ac97_ctrl, int idx, + unsigned int vendor_id) +{ + struct ac97_codec_device *codec; + char *codec_name; + int ret; + + codec = kzalloc(sizeof(*codec), GFP_KERNEL); + if (!codec) + return -ENOMEM; + + codec->vendor_id = vendor_id; + codec->dev.release = ac97_codec_release; + codec->dev.bus = &ac97_bus_type; + codec->dev.parent = ac97_ctrl->dev; + codec->num = idx; + codec->ac97_ctrl = ac97_ctrl; + INIT_LIST_HEAD(&codec->list); + list_move_tail(&codec->list, &ac97_ctrl->codecs); + + codec_name = kasprintf(GFP_KERNEL, "%s:%d", dev_name(ac97_ctrl->dev), + idx); + codec->dev.init_name = codec_name; + + ret = device_register(&codec->dev); + kfree(codec_name); + + return ret; +} + +unsigned int ac97_bus_scan_one(struct ac97_controller *ac97, + int codec_num) +{ + struct ac97_codec_device codec; + unsigned short vid1, vid2; + int ret; + + codec.dev = *ac97->dev; + codec.num = codec_num; + ret = ac97->ops->read(&codec, AC97_VENDOR_ID1); + vid1 = (ret & 0xffff); + if (ret < 0) + return 0; + + ret = ac97->ops->read(&codec, AC97_VENDOR_ID2); + vid2 = (ret & 0xffff); + if (ret < 0) + return 0; + + dev_dbg(&codec.dev, "%s(codec_num=%d): vendor_id=0x%08x\n", + __func__, codec_num, AC97_ID(vid1, vid2)); + return AC97_ID(vid1, vid2); +} + +static int ac97_bus_scan(struct ac97_controller *ac97_ctrl) +{ + int ret, i; + unsigned int vendor_id; + + for (i = 0; i < AC97_BUS_MAX_CODECS; i++) { + if (ac97_codec_find(ac97_ctrl, i)) + continue; + vendor_id = ac97_bus_scan_one(ac97_ctrl, i); + if (!vendor_id) + continue; + + ret = ac97_codec_add(ac97_ctrl, i, vendor_id); + if (ret < 0) + return ret; + } + return 0; +} + +void ac97_rescan_all_controllers(void) +{ + struct ac97_controller *ac97_ctrl; + int ret; + + mutex_lock(&ac97_controllers_mutex); + list_for_each_entry(ac97_ctrl, &ac97_controllers, controllers) { + ret = ac97_bus_scan(ac97_ctrl); + if (ret) + dev_warn(ac97_ctrl->dev, "scan failed: %d\n", ret); + } + mutex_unlock(&ac97_controllers_mutex); +} +EXPORT_SYMBOL(ac97_rescan_all_controllers); + +static int ac97_bus_reset(struct ac97_controller *ac97_ctrl) +{ + struct ac97_codec_device codec; + + memset(&codec, 0, sizeof(codec)); + codec.dev = *ac97_ctrl->dev; + + ac97_ctrl->ops->reset(&codec); + return 0; +} + +/** + * ac97_codec_driver_register - register an AC97 codec driver + * @dev: AC97 driver codec to register + * + * Register an AC97 codec driver to the ac97 bus driver, aka. the AC97 digital + * controller. + * + * Returns 0 on success or error code + */ +int ac97_codec_driver_register(struct ac97_codec_driver *drv) +{ + int ret; + + drv->driver.bus = &ac97_bus_type; + + ret = driver_register(&drv->driver); + if (!ret) + ac97_rescan_all_controllers(); + + return ret; +} +EXPORT_SYMBOL(ac97_codec_driver_register); + +/** + * ac97_codec_driver_unregister - unregister an AC97 codec driver + * @dev: AC97 codec driver to unregister + * + * Unregister a previously registered ac97 codec driver. + */ +void ac97_codec_driver_unregister(struct ac97_codec_driver *drv) +{ + driver_unregister(&drv->driver); +} +EXPORT_SYMBOL(ac97_codec_driver_unregister); + +static int ac97_dc_codecs_unregister(struct ac97_controller *ac97_ctrl) +{ + struct ac97_codec_device *codec, *tmp; + + list_for_each_entry_safe(codec, tmp, &ac97_ctrl->codecs, list) + put_device(&codec->dev); + + return 0; +} + +/** + * ac97_digital_controller_register - register an ac97 controller + * @ops: the ac97 bus operations + * @dev: the device providing the ac97 DC function + * + * Register a digital controller which can control up to 4 ac97 codecs. This is + * the controller side of the AC97 AC-link, while the slave side are the codecs. + * + * Returns a positive bus index upon success, negative value upon error + */ +int ac97_digital_controller_register(const struct ac97_controller_ops *ops, + struct device *dev) +{ + struct ac97_controller *ac97_ctrl; + + ac97_ctrl = kzalloc(sizeof(*ac97_ctrl), GFP_KERNEL); + if (!ac97_ctrl) + return -ENOMEM; + + mutex_lock(&ac97_controllers_mutex); + ac97_ctrl->ops = ops; + ac97_ctrl->bus_idx = ac97_bus_idx++; + ac97_ctrl->dev = dev; + INIT_LIST_HEAD(&ac97_ctrl->codecs); + list_add(&ac97_ctrl->controllers, &ac97_controllers); + mutex_unlock(&ac97_controllers_mutex); + + ac97_bus_reset(ac97_ctrl); + ac97_bus_scan(ac97_ctrl); + + return ac97_ctrl->bus_idx; +} +EXPORT_SYMBOL(ac97_digital_controller_register); + +/** + * ac97_digital_controller_unregister - unregister an ac97 controller + * @dev: the device previously provided to ac97_digital_controller_register() + * + * Returns 0 on success, negative upon error + */ +int ac97_digital_controller_unregister(const struct device *dev) +{ + struct ac97_controller *ac97_ctrl, *tmp; + int ret = -ENODEV; + + mutex_lock(&ac97_controllers_mutex); + list_for_each_entry_safe(ac97_ctrl, tmp, &ac97_controllers, + controllers) { + if (ac97_ctrl->dev != dev) + continue; + if (ac97_ctrl->bound_codecs) + ret = -EBUSY; + else + ret = ac97_dc_codecs_unregister(ac97_ctrl); + if (!ret) + list_del(&ac97_ctrl->controllers); + } + + mutex_unlock(&ac97_controllers_mutex); + return ret; +} +EXPORT_SYMBOL(ac97_digital_controller_unregister); + +static const struct dev_pm_ops ac97_pm = { + .suspend = pm_generic_suspend, + .resume = pm_generic_resume, + .freeze = pm_generic_freeze, + .thaw = pm_generic_thaw, + .poweroff = pm_generic_poweroff, + .restore = pm_generic_restore, +}; + +static ssize_t vendor_id_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct ac97_codec_device *codec = to_ac97_device(dev); + + return sprintf(buf, "%08x", codec->vendor_id); +} + +static struct device_attribute ac97_dev_attrs[] = { + __ATTR_RO(vendor_id), + __ATTR_NULL, +}; + +int ac97_bus_match(struct device *dev, struct device_driver *drv) +{ + struct ac97_codec_device *adev = to_ac97_device(dev); + struct ac97_codec_driver *adrv = to_ac97_driver(drv); + struct ac97_id *id = adrv->id_table; + + if (adev->vendor_id == 0x0 || adev->vendor_id == 0xffffffff) + return false; + + do { + if ((id->id & id->mask) == (adev->vendor_id & id->mask)) + return true; + } while (++id->id); + + return false; +} + +static int ac97_bus_probe(struct device *dev) +{ + struct ac97_codec_device *adev = to_ac97_device(dev); + struct ac97_codec_driver *adrv = to_ac97_driver(dev->driver); + + return adrv->probe(adev); +} + +static int ac97_bus_remove(struct device *dev) +{ + struct ac97_codec_device *adev = to_ac97_device(dev); + struct ac97_codec_driver *adrv = to_ac97_driver(dev->driver); + + return adrv->remove(adev); +} + +struct bus_type ac97_bus_type = { + .name = "ac97", + .dev_attrs = ac97_dev_attrs, + .match = ac97_bus_match, + .pm = &ac97_pm, + .probe = ac97_bus_probe, + .remove = ac97_bus_remove, +}; +EXPORT_SYMBOL(ac97_bus_type); + +static int __init ac97_bus_init(void) +{ + return bus_register(&ac97_bus_type); +} +subsys_initcall(ac97_bus_init); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Robert Jarzmik robert.jarzmik@free.fr"); diff --git a/sound/ac97/codec.c b/sound/ac97/codec.c new file mode 100644 index 000000000000..a835f03744bf --- /dev/null +++ b/sound/ac97/codec.c @@ -0,0 +1,15 @@ +/* + * Copyright (C) 2016 Robert Jarzmik robert.jarzmik@free.fr + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + */ + +#include <sound/ac97_codec.h> +#include <sound/ac97/codec.h> +#include <sound/ac97/controller.h> +#include <linux/device.h> +#include <linux/slab.h> +#include <sound/soc.h> /* For compat_ac97_* */ + diff --git a/sound/ac97/snd_ac97_compat.c b/sound/ac97/snd_ac97_compat.c new file mode 100644 index 000000000000..7e2f01c96fc9 --- /dev/null +++ b/sound/ac97/snd_ac97_compat.c @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2016 Robert Jarzmik robert.jarzmik@free.fr + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + */ + +#include <linux/list.h> +#include <linux/slab.h> +#include <sound/ac97/codec.h> +#include <sound/ac97/controller.h> +#include <sound/soc.h> + +#include "ac97_core.h" + +static void compat_ac97_reset(struct snd_ac97 *ac97) +{ + struct ac97_codec_device *adev = to_ac97_device(ac97->private_data); + struct ac97_controller *actrl = adev->ac97_ctrl; + + if (actrl->ops->reset) + actrl->ops->reset(adev); +} + +static void compat_ac97_warm_reset(struct snd_ac97 *ac97) +{ + struct ac97_codec_device *adev = to_ac97_device(ac97->private_data); + struct ac97_controller *actrl = adev->ac97_ctrl; + + if (actrl->ops->warm_reset) + actrl->ops->warm_reset(adev); +} + +static void compat_ac97_write(struct snd_ac97 *ac97, unsigned short reg, + unsigned short val) +{ + struct ac97_codec_device *adev = to_ac97_device(ac97->private_data); + struct ac97_controller *actrl = adev->ac97_ctrl; + + actrl->ops->write(adev, reg, val); +} + +static unsigned short compat_ac97_read(struct snd_ac97 *ac97, + unsigned short reg) +{ + struct ac97_codec_device *adev = to_ac97_device(ac97->private_data); + struct ac97_controller *actrl = adev->ac97_ctrl; + + return actrl->ops->read(adev, reg); +} + +static struct snd_ac97_bus_ops compat_snd_ac97_bus_ops = { + .reset = compat_ac97_reset, + .warm_reset = compat_ac97_warm_reset, + .write = compat_ac97_write, + .read = compat_ac97_read, +}; + +static struct snd_ac97_bus compat_soc_ac97_bus = { + .ops = &compat_snd_ac97_bus_ops, +}; + +struct snd_ac97 *compat_alloc_snd_ac97_codec(struct snd_soc_codec *codec) +{ + struct snd_ac97 *ac97; + + ac97 = kzalloc(sizeof(struct snd_ac97), GFP_KERNEL); + if (ac97 == NULL) + return ERR_PTR(-ENOMEM); + + ac97->dev = *codec->dev; + ac97->private_data = codec->dev; + ac97->bus = &compat_soc_ac97_bus; + return ac97; +} +EXPORT_SYMBOL_GPL(compat_alloc_snd_ac97_codec); + +void compat_release_snd_ac97_codec(struct snd_ac97 *ac97) +{ + kfree(ac97); +} +EXPORT_SYMBOL_GPL(compat_release_snd_ac97_codec); + +int snd_ac97_reset(struct snd_ac97 *ac97, bool try_warm, unsigned int id, + unsigned int id_mask) +{ + struct ac97_codec_device *adev = to_ac97_device(ac97->private_data); + struct ac97_controller *actrl = adev->ac97_ctrl; + + if (try_warm) { + compat_ac97_warm_reset(ac97); + if (ac97_bus_scan_one(actrl, adev->num) == adev->vendor_id) + return 1; + } + + compat_ac97_reset(ac97); + compat_ac97_warm_reset(ac97); + if (ac97_bus_scan_one(actrl, adev->num) == adev->vendor_id) + return 0; + + return -ENODEV; +} +EXPORT_SYMBOL_GPL(snd_ac97_reset);