satip-axe/kernel/sound/stm/core.c

812 lines
20 KiB
C

/*
* Core routines for STMicroelectronics' SoCs audio drivers
*
* Copyright (c) 2005-2011 STMicroelectronics Limited
*
* Author: Pawel Moll <pawel.moll@st.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
*/
#include <linux/init.h>
#include <linux/io.h>
#include <linux/mm.h>
#include <linux/platform_device.h>
#include <linux/bpa2.h>
#include <linux/stm/stm-dma.h>
#include <sound/core.h>
#include <sound/pcm.h>
#include <sound/info.h>
#include <sound/pcm_params.h>
#include <sound/asoundef.h>
#include "common.h"
int snd_stm_debug_level;
module_param_named(debug, snd_stm_debug_level, int, S_IRUGO | S_IWUSR);
static int snd_stm_card_index = -1; /* First available index */
module_param_named(index, snd_stm_card_index, int, 0444);
MODULE_PARM_DESC(index, "Index value for STMicroelectronics audio subsystem "
"card.");
static char *snd_stm_card_id;
module_param_named(id, snd_stm_card_id, charp, 0444);
MODULE_PARM_DESC(id, "ID string for STMicroelectronics audio subsystem card.");
/*
* ALSA card management
*/
static struct snd_card *snd_stm_card;
static int snd_stm_card_registered;
int snd_stm_card_register(void)
{
int result;
const char *soc_type = get_cpu_subtype(cpu_data);
BUG_ON(!soc_type);
BUG_ON(!snd_stm_card);
BUG_ON(snd_stm_card_registered);
if (!snd_stm_card_id)
strlcpy(snd_stm_card->id, soc_type, sizeof(snd_stm_card->id));
strlcpy(snd_stm_card->driver, soc_type, sizeof(snd_stm_card->driver));
snprintf(snd_stm_card->shortname, sizeof(snd_stm_card->shortname),
"%s audio subsystem", soc_type);
if (cpu_data->cut_minor < 0)
snprintf(snd_stm_card->longname, sizeof(snd_stm_card->longname),
"STMicroelectronics %s cut %d.x SOC audio "
"subsystem", soc_type, cpu_data->cut_major);
else
snprintf(snd_stm_card->longname, sizeof(snd_stm_card->longname),
"STMicroelectronics %s cut %d.%d SOC audio "
"subsystem", soc_type, cpu_data->cut_major,
cpu_data->cut_minor);
result = snd_card_register(snd_stm_card);
if (result == 0)
snd_stm_card_registered = 1;
return result;
}
EXPORT_SYMBOL(snd_stm_card_register);
int snd_stm_card_is_registered(void)
{
BUG_ON(!snd_stm_card);
return snd_stm_card_registered;
}
EXPORT_SYMBOL(snd_stm_card_is_registered);
struct snd_card *snd_stm_card_get(void)
{
BUG_ON(!snd_stm_card);
return snd_stm_card;
}
EXPORT_SYMBOL(snd_stm_card_get);
/*
* Resources management
*/
int snd_stm_memory_request(struct platform_device *pdev,
struct resource **mem_region, void **base_address)
{
struct resource *resource;
resource = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!resource) {
snd_stm_printe("Failed to"
" platform_get_resource(IORESOURCE_MEM)!\n");
return -ENODEV;
}
*mem_region = request_mem_region(resource->start,
resource->end - resource->start + 1, pdev->name);
if (!*mem_region) {
snd_stm_printe("Failed request_mem_region(0x%08x,"
" 0x%08x, '%s')!\n", resource->start,
resource->end - resource->start + 1,
pdev->name);
return -EBUSY;
}
snd_stm_printd(0, "Memory region: 0x%08x-0x%08x\n",
(*mem_region)->start, (*mem_region)->end);
*base_address = ioremap(resource->start,
resource->end - resource->start + 1);
if (!*base_address) {
release_resource(*mem_region);
snd_stm_printe("Failed ioremap!\n");
return -EINVAL;
}
snd_stm_printd(0, "Base address is 0x%p.\n", *base_address);
return 0;
}
EXPORT_SYMBOL(snd_stm_memory_request);
void snd_stm_memory_release(struct resource *mem_region,
void *base_address)
{
iounmap(base_address);
release_resource(mem_region);
}
EXPORT_SYMBOL(snd_stm_memory_release);
int snd_stm_irq_request(struct platform_device *pdev,
unsigned int *irq, irq_handler_t handler, void *dev_id)
{
struct resource *resource;
int result;
resource = platform_get_resource(pdev, IORESOURCE_IRQ, 0);
if (!resource) {
snd_stm_printe("Failed to "
"platform_get_resource(IORESOURCE_IRQ)!\n");
return -ENODEV;
}
snd_stm_printd(0, "IRQ: %u\n", resource->start);
*irq = resource->start;
result = request_irq(*irq, handler, IRQF_DISABLED, pdev->name, dev_id);
if (result != 0) {
snd_stm_printe("Failed request_irq!\n");
return -EINVAL;
}
/* request_irq() enables the interrupt immediately; as it is
* lethal in concurrent audio environment, we want to have
* it disabled for most of the time... */
disable_irq(*irq);
return 0;
}
int snd_stm_fdma_request(struct platform_device *pdev, int *channel)
{
static const char *fdmac_id[] = { STM_DMAC_ID, NULL };
static const char *fdma_cap_lb[] = { STM_DMA_CAP_LOW_BW, NULL };
static const char *fdma_cap_hb[] = { STM_DMA_CAP_HIGH_BW, NULL };
*channel = request_dma_bycap(fdmac_id, fdma_cap_lb, pdev->name);
if (*channel < 0) {
*channel = request_dma_bycap(fdmac_id, fdma_cap_hb, pdev->name);
if (*channel < 0) {
snd_stm_printe("Failed to request_dma_bycap()==%d!\n",
*channel);
return -ENODEV;
}
}
snd_stm_printd(0, "FDMA channel: %d\n", *channel);
return 0;
}
/*
* ALSA procfs additional entries
*/
static struct snd_info_entry *snd_stm_info_root;
int snd_stm_info_create(void)
{
int result = 0;
snd_stm_info_root = snd_info_create_module_entry(THIS_MODULE,
"stm", NULL);
if (snd_stm_info_root) {
snd_stm_info_root->mode = S_IFDIR | S_IRUGO | S_IXUGO;
if (snd_info_register(snd_stm_info_root) < 0) {
result = -EINVAL;
snd_info_free_entry(snd_stm_info_root);
}
} else {
result = -ENOMEM;
}
return result;
}
void snd_stm_info_dispose(void)
{
if (snd_stm_info_root)
snd_info_free_entry(snd_stm_info_root);
}
int snd_stm_info_register(struct snd_info_entry **entry,
const char *name,
void (read)(struct snd_info_entry *, struct snd_info_buffer *),
void *private_data)
{
int result = 0;
/* Skip the "snd_" prefix, if bus_id has been simply given */
if (strncmp(name, "snd_", 4) == 0)
name += 4;
*entry = snd_info_create_module_entry(THIS_MODULE, name,
snd_stm_info_root);
if (*entry) {
(*entry)->c.text.read = read;
(*entry)->private_data = private_data;
if (snd_info_register(*entry) < 0) {
result = -EINVAL;
snd_info_free_entry(*entry);
}
} else {
result = -EINVAL;
}
return result;
}
EXPORT_SYMBOL(snd_stm_info_register);
void snd_stm_info_unregister(struct snd_info_entry *entry)
{
if (entry)
snd_info_free_entry(entry);
}
EXPORT_SYMBOL(snd_stm_info_unregister);
/*
* PCM buffer memory management
*/
struct snd_stm_buffer {
struct snd_pcm *pcm;
struct bpa2_part *bpa2_part;
int allocated;
struct snd_pcm_substream *substream;
snd_stm_magic_field;
};
#if defined(CONFIG_BPA2)
static char *bpa2_part = CONFIG_SND_STM_BPA2_PARTITION_NAME;
#else
static char *bpa2_part = "";
#endif
module_param(bpa2_part, charp, S_IRUGO);
struct snd_stm_buffer *snd_stm_buffer_create(struct snd_pcm *pcm,
struct device *device, int prealloc_size)
{
struct snd_stm_buffer *buffer;
snd_stm_printd(1, "snd_stm_buffer_init(pcm=%p, prealloc_size=%d)\n",
pcm, prealloc_size);
BUG_ON(!pcm);
buffer = kzalloc(sizeof(*buffer), GFP_KERNEL);
if (!buffer) {
snd_stm_printe("Can't allocate memory for a buffer "
"description!\n");
return NULL;
}
snd_stm_magic_set(buffer);
buffer->pcm = pcm;
#if defined(CONFIG_BPA2)
buffer->bpa2_part = bpa2_find_part(bpa2_part);
if (buffer->bpa2_part) {
snd_stm_printd(0, "Using BPA2 partition '%s'...\n", bpa2_part);
return buffer;
}
buffer->bpa2_part = bpa2_find_part("bigphysarea");
if (buffer->bpa2_part) {
snd_stm_printd(0, "Using legacy 'bigphysarea' BPA2 "
"partition...\n");
return buffer;
}
#endif
if (snd_pcm_lib_preallocate_pages_for_all(pcm,
SNDRV_DMA_TYPE_DEV, device,
prealloc_size, prealloc_size) == 0) {
snd_stm_printd(0, "Using pcm_lib's preallocated buffer "
"(%d bytes)...\n", prealloc_size);
return buffer;
}
snd_stm_printe("Can't provide any memory for buffers!\n");
kfree(buffer);
return NULL;
}
void snd_stm_buffer_dispose(struct snd_stm_buffer *buffer)
{
snd_stm_printd(1, "snd_stm_buffer_dispose(buffer=%p)\n", buffer);
BUG_ON(!buffer);
BUG_ON(!snd_stm_magic_valid(buffer));
BUG_ON(buffer->allocated);
/* snd_pcm_lib__preallocate*-ed buffer is freed automagically */
snd_stm_magic_clear(buffer);
kfree(buffer);
}
int snd_stm_buffer_is_allocated(struct snd_stm_buffer *buffer)
{
snd_stm_printd(1, "snd_stm_buffer_is_allocated(buffer=%p)\n",
buffer);
BUG_ON(!buffer);
BUG_ON(!snd_stm_magic_valid(buffer));
return buffer->allocated;
}
int snd_stm_buffer_alloc(struct snd_stm_buffer *buffer,
struct snd_pcm_substream *substream, int size)
{
snd_stm_printd(1, "snd_stm_buffer_alloc(buffer=%p, substream=%p, "
"size=%d)\n", buffer, substream, size);
BUG_ON(!buffer);
BUG_ON(!snd_stm_magic_valid(buffer));
BUG_ON(buffer->allocated);
BUG_ON(size <= 0);
if (buffer->bpa2_part) {
#if defined(CONFIG_BPA2)
struct snd_pcm_runtime *runtime = substream->runtime;
int pages = (size + PAGE_SIZE - 1) / PAGE_SIZE;
runtime->dma_addr = bpa2_alloc_pages(buffer->bpa2_part, pages,
0, GFP_KERNEL);
if (runtime->dma_addr == 0) {
snd_stm_printe("Can't get %d pages from BPA2!\n",
pages);
return -ENOMEM;
}
runtime->dma_bytes = size;
runtime->dma_area = ioremap_nocache(runtime->dma_addr, size);
#else
snd_BUG();
#endif
} else {
if (snd_pcm_lib_malloc_pages(substream, size) < 0) {
snd_stm_printe("Can't allocate pages using pcm_lib!\n");
return -ENOMEM;
}
/* runtime->dma_* are set by snd_pcm_lib_malloc_pages()
* (by snd_pcm_set_runtime_buffer() to be more specific.) */
}
snd_stm_printd(1, "Allocated memory: dma_addr=0x%08x, dma_area=0x%p, "
"dma_bytes=%u\n", substream->runtime->dma_addr,
substream->runtime->dma_area,
substream->runtime->dma_bytes);
buffer->substream = substream;
buffer->allocated = 1;
return 0;
}
void snd_stm_buffer_free(struct snd_stm_buffer *buffer)
{
struct snd_pcm_runtime *runtime;
snd_stm_printd(1, "snd_stm_buffer_free(buffer=%p)\n", buffer);
BUG_ON(!buffer);
BUG_ON(!snd_stm_magic_valid(buffer));
BUG_ON(!buffer->allocated);
runtime = buffer->substream->runtime;
snd_stm_printd(1, "Freeing dma_addr=0x%08x, dma_area=0x%p, "
"dma_bytes=%u\n", runtime->dma_addr,
runtime->dma_area, runtime->dma_bytes);
if (buffer->bpa2_part) {
#if defined(CONFIG_BPA2)
iounmap(runtime->dma_area);
bpa2_free_pages(buffer->bpa2_part, runtime->dma_addr);
runtime->dma_area = NULL;
runtime->dma_addr = 0;
runtime->dma_bytes = 0;
#else
snd_BUG();
#endif
} else {
snd_pcm_lib_free_pages(buffer->substream);
/* runtime->dma_* are cleared by snd_pcm_lib_free_pages()
* (by snd_pcm_set_runtime_buffer() to be more specific.) */
}
buffer->allocated = 0;
buffer->substream = NULL;
}
static int snd_stm_buffer_mmap_fault(struct vm_area_struct *area,
struct vm_fault *vmf)
{
/* No VMA expanding here! */
return VM_FAULT_SIGBUS;
}
static struct vm_operations_struct snd_stm_buffer_mmap_vm_ops = {
.open = snd_pcm_mmap_data_open,
.close = snd_pcm_mmap_data_close,
.fault = snd_stm_buffer_mmap_fault,
};
int snd_stm_buffer_mmap(struct snd_pcm_substream *substream,
struct vm_area_struct *area)
{
struct snd_pcm_runtime *runtime = substream->runtime;
unsigned long map_offset = area->vm_pgoff << PAGE_SHIFT;
unsigned long phys_addr = runtime->dma_addr + map_offset;
unsigned long map_size = area->vm_end - area->vm_start;
unsigned long phys_size = runtime->dma_bytes + PAGE_SIZE -
runtime->dma_bytes % PAGE_SIZE;
snd_stm_printd(1, "snd_stm_buffer_mmap(substream=%p, area=%p)\n",
substream, area);
snd_stm_printd(1, "Mmaping %lu bytes starting from 0x%08lx "
"(dma_addr=0x%08x, dma_size=%u, vm_pgoff=%lu, "
"vm_start=0x%lx, vm_end=0x%lx)...\n", map_size,
phys_addr, runtime->dma_addr, runtime->dma_bytes,
area->vm_pgoff, area->vm_start, area->vm_end);
if (map_size > phys_size) {
snd_stm_printe("Trying to perform mmap larger than buffer!\n");
return -EINVAL;
}
area->vm_ops = &snd_stm_buffer_mmap_vm_ops;
area->vm_private_data = substream;
area->vm_flags |= VM_RESERVED;
area->vm_page_prot = pgprot_noncached(area->vm_page_prot);
if (remap_pfn_range(area, area->vm_start, phys_addr >> PAGE_SHIFT,
map_size, area->vm_page_prot) != 0) {
snd_stm_printe("Can't remap buffer!\n");
return -EAGAIN;
}
/* Must be called implicitly here... */
snd_pcm_mmap_data_open(area);
return 0;
}
/*
* Common ALSA parameters constraints
*/
/*
#define FIXED_TRANSFER_BYTES max_transfer_bytes > 16 ? 16 : max_transfer_bytes
#define FIXED_TRANSFER_BYTES max_transfer_bytes
*/
#if defined(FIXED_TRANSFER_BYTES)
int snd_stm_pcm_transfer_bytes(unsigned int bytes_per_frame,
unsigned int max_transfer_bytes)
{
int transfer_bytes = FIXED_TRANSFER_BYTES;
snd_stm_printd(1, "snd_stm_pcm_transfer_bytes(bytes_per_frame=%u, "
"max_transfer_bytes=%u) = %u (FIXED)\n",
bytes_per_frame, max_transfer_bytes, transfer_bytes);
return transfer_bytes;
}
int snd_stm_pcm_hw_constraint_transfer_bytes(struct snd_pcm_runtime *runtime,
unsigned int max_transfer_bytes)
{
return snd_pcm_hw_constraint_step(runtime, 0,
SNDRV_PCM_HW_PARAM_PERIOD_BYTES,
snd_stm_pcm_transfer_bytes(0, max_transfer_bytes));
}
#else
int snd_stm_pcm_transfer_bytes(unsigned int bytes_per_frame,
unsigned int max_transfer_bytes)
{
unsigned int transfer_bytes;
for (transfer_bytes = bytes_per_frame;
transfer_bytes * 2 < max_transfer_bytes;
transfer_bytes *= 2)
;
snd_stm_printd(2, "snd_stm_pcm_transfer_bytes(bytes_per_frame=%u, "
"max_transfer_bytes=%u) = %u\n", bytes_per_frame,
max_transfer_bytes, transfer_bytes);
return transfer_bytes;
}
static int snd_stm_pcm_hw_rule_transfer_bytes(struct snd_pcm_hw_params *params,
struct snd_pcm_hw_rule *rule)
{
int changed = 0;
unsigned int max_transfer_bytes = (unsigned int)rule->private;
struct snd_interval *period_bytes = hw_param_interval(params,
SNDRV_PCM_HW_PARAM_PERIOD_BYTES);
struct snd_interval *frame_bits = hw_param_interval(params,
SNDRV_PCM_HW_PARAM_FRAME_BITS);
unsigned int transfer_bytes, n;
transfer_bytes = snd_stm_pcm_transfer_bytes(frame_bits->min / 8,
max_transfer_bytes);
n = period_bytes->min % transfer_bytes;
if (n != 0 || period_bytes->openmin) {
period_bytes->min += transfer_bytes - n;
changed = 1;
}
transfer_bytes = snd_stm_pcm_transfer_bytes(frame_bits->max / 8,
max_transfer_bytes);
n = period_bytes->max % transfer_bytes;
if (n != 0 || period_bytes->openmax) {
period_bytes->max -= n;
changed = 1;
}
if (snd_interval_checkempty(period_bytes)) {
period_bytes->empty = 1;
return -EINVAL;
}
return changed;
}
int snd_stm_pcm_hw_constraint_transfer_bytes(struct snd_pcm_runtime *runtime,
unsigned int max_transfer_bytes)
{
return snd_pcm_hw_rule_add(runtime, 0, SNDRV_PCM_HW_PARAM_PERIOD_BYTES,
snd_stm_pcm_hw_rule_transfer_bytes,
(void *)max_transfer_bytes,
SNDRV_PCM_HW_PARAM_PERIOD_BYTES,
SNDRV_PCM_HW_PARAM_FRAME_BITS, -1);
}
#endif
/*
* Common ALSA controls routines
*/
int snd_stm_ctl_boolean_info(struct snd_kcontrol *kcontrol,
struct snd_ctl_elem_info *uinfo)
{
uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN;
uinfo->count = 1;
uinfo->value.integer.min = 0;
uinfo->value.integer.max = 1;
return 0;
}
EXPORT_SYMBOL(snd_stm_ctl_boolean_info);
int snd_stm_ctl_iec958_info(struct snd_kcontrol *kcontrol,
struct snd_ctl_elem_info *uinfo)
{
uinfo->type = SNDRV_CTL_ELEM_TYPE_IEC958;
uinfo->count = 1;
return 0;
}
int snd_stm_ctl_iec958_mask_get_con(struct snd_kcontrol *kcontrol,
struct snd_ctl_elem_value *ucontrol)
{
ucontrol->value.iec958.status[0] = IEC958_AES0_PROFESSIONAL |
IEC958_AES0_NONAUDIO |
IEC958_AES0_CON_NOT_COPYRIGHT |
IEC958_AES0_CON_EMPHASIS |
IEC958_AES0_CON_MODE;
ucontrol->value.iec958.status[1] = IEC958_AES1_CON_CATEGORY |
IEC958_AES1_CON_ORIGINAL;
ucontrol->value.iec958.status[2] = IEC958_AES2_CON_SOURCE |
IEC958_AES2_CON_CHANNEL;
ucontrol->value.iec958.status[3] = IEC958_AES3_CON_FS |
IEC958_AES3_CON_CLOCK;
ucontrol->value.iec958.status[4] = IEC958_AES4_CON_MAX_WORDLEN_24 |
IEC958_AES4_CON_WORDLEN;
return 0;
}
int snd_stm_ctl_iec958_mask_get_pro(struct snd_kcontrol *kcontrol,
struct snd_ctl_elem_value *ucontrol)
{
ucontrol->value.iec958.status[0] = IEC958_AES0_PROFESSIONAL |
IEC958_AES0_NONAUDIO |
IEC958_AES0_PRO_EMPHASIS |
IEC958_AES0_PRO_FREQ_UNLOCKED |
IEC958_AES0_PRO_FS;
ucontrol->value.iec958.status[1] = IEC958_AES1_PRO_MODE |
IEC958_AES1_PRO_USERBITS;
ucontrol->value.iec958.status[2] = IEC958_AES2_PRO_SBITS |
IEC958_AES2_PRO_WORDLEN;
return 0;
}
int snd_stm_iec958_cmp(const struct snd_aes_iec958 *a,
const struct snd_aes_iec958 *b)
{
int result;
BUG_ON(!a);
BUG_ON(!b);
result = memcmp(a->status, b->status, sizeof(a->status));
if (result == 0)
result = memcmp(a->subcode, b->subcode, sizeof(a->subcode));
if (result == 0)
result = memcmp(a->dig_subframe, b->dig_subframe,
sizeof(a->dig_subframe));
return result;
}
/*
* Debug features
*/
/* Memory dump function */
void snd_stm_hex_dump(void *data, int size)
{
unsigned char *buffer = data;
char line[57];
int i;
for (i = 0; i < size; i++) {
if (i % 16 == 0)
sprintf(line, "%p", data + i);
sprintf(line + 8 + ((i % 16) * 3), " %02x", *buffer++);
if (i % 16 == 15 || i == size - 1)
printk(KERN_DEBUG "%s\n", line);
}
}
/* IEC958 structure dump */
void snd_stm_iec958_dump(const struct snd_aes_iec958 *vuc)
{
int i;
char line[54];
const unsigned char *data;
printk(KERN_DEBUG " "
"0 1 2 3 4 5 6 7 8 9\n");
data = vuc->status;
for (i = 0; i < 24; i++) {
if (i % 10 == 0)
sprintf(line, "%p status %02d:",
(unsigned char *)vuc + i, i);
sprintf(line + 22 + ((i % 10) * 3), " %02x", *data++);
if (i % 10 == 9 || i == 23)
printk(KERN_DEBUG "%s\n", line);
}
data = vuc->subcode;
for (i = 0; i < 147; i++) {
if (i % 10 == 0)
sprintf(line, "%p subcode %03d:",
(unsigned char *)vuc +
offsetof(struct snd_aes_iec958,
dig_subframe) + i, i);
sprintf(line + 22 + ((i % 10) * 3), " %02x", *data++);
if (i % 10 == 9 || i == 146)
printk(KERN_DEBUG "%s\n", line);
}
printk(KERN_DEBUG "%p dig_subframe: %02x %02x %02x %02x\n",
(unsigned char *)vuc +
offsetof(struct snd_aes_iec958, dig_subframe),
vuc->dig_subframe[0], vuc->dig_subframe[1],
vuc->dig_subframe[2], vuc->dig_subframe[3]);
}
/*
* Core initialization
*/
static int __init snd_stm_core_init(void)
{
int result;
snd_stm_printd(0, "%s()\n", __func__);
result = snd_card_create(snd_stm_card_index, snd_stm_card_id,
THIS_MODULE, 0, &snd_stm_card);
if (result != 0) {
snd_stm_printe("Failed to create ALSA card!\n");
goto error_card_create;
}
result = snd_stm_info_create();
if (result != 0) {
snd_stm_printe("Procfs info creation failed!\n");
goto error_info;
}
result = snd_stm_conv_init();
if (result != 0) {
snd_stm_printe("Converters infrastructure initialization"
" failed!\n");
goto error_conv;
}
return result;
error_conv:
snd_stm_info_dispose();
error_info:
snd_card_free(snd_stm_card);
error_card_create:
return result;
}
static void __exit snd_stm_core_exit(void)
{
snd_stm_printd(0, "%s()\n", __func__);
snd_stm_conv_exit();
snd_stm_info_dispose();
snd_card_free(snd_stm_card);
}
MODULE_AUTHOR("Pawel Moll <pawel.moll@st.com>");
MODULE_DESCRIPTION("STMicroelectronics System-on-Chips' audio core driver");
MODULE_LICENSE("GPL");
module_init(snd_stm_core_init);
module_exit(snd_stm_core_exit);