linux/drivers/media/pci/cobalt/cobalt-alsa-pcm.c
Hans Verkuil 85756a069c [media] cobalt: add new driver
The cobalt device is a PCIe card with 4 HDMI inputs (adv7604) and a
connector that can be used to hook up an adv7511 transmitter or an
adv7842 receiver daughterboard.

This device is used within Cisco but is sadly not available outside
of Cisco. Nevertheless it is a very interesting driver that can serve
as an example of how to support HDMI hardware and how to use the popular
adv devices.

Signed-off-by: Hans Verkuil <hans.verkuil@cisco.com>
Signed-off-by: Mauro Carvalho Chehab <mchehab@osg.samsung.com>
2015-05-20 13:44:01 -03:00

604 lines
14 KiB
C

/*
* ALSA PCM device for the
* ALSA interface to cobalt PCM capture streams
*
* Copyright 2014-2015 Cisco Systems, Inc. and/or its affiliates.
* All rights reserved.
*
* This program is free software; you may redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 2 of the License.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/vmalloc.h>
#include <linux/delay.h>
#include <media/v4l2-device.h>
#include <sound/core.h>
#include <sound/pcm.h>
#include "cobalt-driver.h"
#include "cobalt-alsa.h"
#include "cobalt-alsa-pcm.h"
static unsigned int pcm_debug;
module_param(pcm_debug, int, 0644);
MODULE_PARM_DESC(pcm_debug, "enable debug messages for pcm");
#define dprintk(fmt, arg...) \
do { \
if (pcm_debug) \
pr_info("cobalt-alsa-pcm %s: " fmt, __func__, ##arg); \
} while (0)
static struct snd_pcm_hardware snd_cobalt_hdmi_capture = {
.info = SNDRV_PCM_INFO_BLOCK_TRANSFER |
SNDRV_PCM_INFO_MMAP |
SNDRV_PCM_INFO_INTERLEAVED |
SNDRV_PCM_INFO_MMAP_VALID,
.formats = SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S32_LE,
.rates = SNDRV_PCM_RATE_48000,
.rate_min = 48000,
.rate_max = 48000,
.channels_min = 1,
.channels_max = 8,
.buffer_bytes_max = 4 * 240 * 8 * 4, /* 5 ms of data */
.period_bytes_min = 1920, /* 1 sample = 8 * 4 bytes */
.period_bytes_max = 240 * 8 * 4, /* 5 ms of 8 channel data */
.periods_min = 1,
.periods_max = 4,
};
static struct snd_pcm_hardware snd_cobalt_playback = {
.info = SNDRV_PCM_INFO_BLOCK_TRANSFER |
SNDRV_PCM_INFO_MMAP |
SNDRV_PCM_INFO_INTERLEAVED |
SNDRV_PCM_INFO_MMAP_VALID,
.formats = SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S32_LE,
.rates = SNDRV_PCM_RATE_48000,
.rate_min = 48000,
.rate_max = 48000,
.channels_min = 1,
.channels_max = 8,
.buffer_bytes_max = 4 * 240 * 8 * 4, /* 5 ms of data */
.period_bytes_min = 1920, /* 1 sample = 8 * 4 bytes */
.period_bytes_max = 240 * 8 * 4, /* 5 ms of 8 channel data */
.periods_min = 1,
.periods_max = 4,
};
static void sample_cpy(u8 *dst, const u8 *src, u32 len, bool is_s32)
{
static const unsigned map[8] = { 0, 1, 5, 4, 2, 3, 6, 7 };
unsigned idx = 0;
while (len >= (is_s32 ? 4 : 2)) {
unsigned offset = map[idx] * 4;
u32 val = src[offset + 1] + (src[offset + 2] << 8) +
(src[offset + 3] << 16);
if (is_s32) {
*dst++ = 0;
*dst++ = val & 0xff;
}
*dst++ = (val >> 8) & 0xff;
*dst++ = (val >> 16) & 0xff;
len -= is_s32 ? 4 : 2;
idx++;
}
}
static void cobalt_alsa_announce_pcm_data(struct snd_cobalt_card *cobsc,
u8 *pcm_data,
size_t skip,
size_t samples)
{
struct snd_pcm_substream *substream;
struct snd_pcm_runtime *runtime;
unsigned long flags;
unsigned int oldptr;
unsigned int stride;
int length = samples;
int period_elapsed = 0;
bool is_s32;
dprintk("cobalt alsa announce ptr=%p data=%p num_bytes=%zd\n", cobsc,
pcm_data, samples);
substream = cobsc->capture_pcm_substream;
if (substream == NULL) {
dprintk("substream was NULL\n");
return;
}
runtime = substream->runtime;
if (runtime == NULL) {
dprintk("runtime was NULL\n");
return;
}
is_s32 = runtime->format == SNDRV_PCM_FORMAT_S32_LE;
stride = runtime->frame_bits >> 3;
if (stride == 0) {
dprintk("stride is zero\n");
return;
}
if (length == 0) {
dprintk("%s: length was zero\n", __func__);
return;
}
if (runtime->dma_area == NULL) {
dprintk("dma area was NULL - ignoring\n");
return;
}
oldptr = cobsc->hwptr_done_capture;
if (oldptr + length >= runtime->buffer_size) {
unsigned int cnt = runtime->buffer_size - oldptr;
unsigned i;
for (i = 0; i < cnt; i++)
sample_cpy(runtime->dma_area + (oldptr + i) * stride,
pcm_data + i * skip,
stride, is_s32);
for (i = cnt; i < length; i++)
sample_cpy(runtime->dma_area + (i - cnt) * stride,
pcm_data + i * skip, stride, is_s32);
} else {
unsigned i;
for (i = 0; i < length; i++)
sample_cpy(runtime->dma_area + (oldptr + i) * stride,
pcm_data + i * skip,
stride, is_s32);
}
snd_pcm_stream_lock_irqsave(substream, flags);
cobsc->hwptr_done_capture += length;
if (cobsc->hwptr_done_capture >=
runtime->buffer_size)
cobsc->hwptr_done_capture -=
runtime->buffer_size;
cobsc->capture_transfer_done += length;
if (cobsc->capture_transfer_done >=
runtime->period_size) {
cobsc->capture_transfer_done -=
runtime->period_size;
period_elapsed = 1;
}
snd_pcm_stream_unlock_irqrestore(substream, flags);
if (period_elapsed)
snd_pcm_period_elapsed(substream);
}
static int alsa_fnc(struct vb2_buffer *vb, void *priv)
{
struct cobalt_stream *s = priv;
unsigned char *p = vb2_plane_vaddr(vb, 0);
int i;
if (pcm_debug) {
pr_info("alsa: ");
for (i = 0; i < 8 * 4; i++) {
if (!(i & 3))
pr_cont(" ");
pr_cont("%02x", p[i]);
}
pr_cont("\n");
}
cobalt_alsa_announce_pcm_data(s->alsa,
vb2_plane_vaddr(vb, 0),
8 * 4,
vb2_get_plane_payload(vb, 0) / (8 * 4));
return 0;
}
static int snd_cobalt_pcm_capture_open(struct snd_pcm_substream *substream)
{
struct snd_pcm_runtime *runtime = substream->runtime;
struct snd_cobalt_card *cobsc = snd_pcm_substream_chip(substream);
struct cobalt_stream *s = cobsc->s;
runtime->hw = snd_cobalt_hdmi_capture;
snd_pcm_hw_constraint_integer(runtime, SNDRV_PCM_HW_PARAM_PERIODS);
cobsc->capture_pcm_substream = substream;
runtime->private_data = s;
cobsc->alsa_record_cnt++;
if (cobsc->alsa_record_cnt == 1) {
int rc;
rc = vb2_thread_start(&s->q, alsa_fnc, s, s->vdev.name);
if (rc) {
cobsc->alsa_record_cnt--;
return rc;
}
}
return 0;
}
static int snd_cobalt_pcm_capture_close(struct snd_pcm_substream *substream)
{
struct snd_cobalt_card *cobsc = snd_pcm_substream_chip(substream);
struct cobalt_stream *s = cobsc->s;
cobsc->alsa_record_cnt--;
if (cobsc->alsa_record_cnt == 0)
vb2_thread_stop(&s->q);
return 0;
}
static int snd_cobalt_pcm_ioctl(struct snd_pcm_substream *substream,
unsigned int cmd, void *arg)
{
return snd_pcm_lib_ioctl(substream, cmd, arg);
}
static int snd_pcm_alloc_vmalloc_buffer(struct snd_pcm_substream *subs,
size_t size)
{
struct snd_pcm_runtime *runtime = subs->runtime;
dprintk("Allocating vbuffer\n");
if (runtime->dma_area) {
if (runtime->dma_bytes > size)
return 0;
vfree(runtime->dma_area);
}
runtime->dma_area = vmalloc(size);
if (!runtime->dma_area)
return -ENOMEM;
runtime->dma_bytes = size;
return 0;
}
static int snd_cobalt_pcm_hw_params(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params)
{
dprintk("%s called\n", __func__);
return snd_pcm_alloc_vmalloc_buffer(substream,
params_buffer_bytes(params));
}
static int snd_cobalt_pcm_hw_free(struct snd_pcm_substream *substream)
{
if (substream->runtime->dma_area) {
dprintk("freeing pcm capture region\n");
vfree(substream->runtime->dma_area);
substream->runtime->dma_area = NULL;
}
return 0;
}
static int snd_cobalt_pcm_prepare(struct snd_pcm_substream *substream)
{
struct snd_cobalt_card *cobsc = snd_pcm_substream_chip(substream);
cobsc->hwptr_done_capture = 0;
cobsc->capture_transfer_done = 0;
return 0;
}
static int snd_cobalt_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
{
switch (cmd) {
case SNDRV_PCM_TRIGGER_START:
case SNDRV_PCM_TRIGGER_STOP:
return 0;
default:
return -EINVAL;
}
return 0;
}
static
snd_pcm_uframes_t snd_cobalt_pcm_pointer(struct snd_pcm_substream *substream)
{
snd_pcm_uframes_t hwptr_done;
struct snd_cobalt_card *cobsc = snd_pcm_substream_chip(substream);
hwptr_done = cobsc->hwptr_done_capture;
return hwptr_done;
}
static void pb_sample_cpy(u8 *dst, const u8 *src, u32 len, bool is_s32)
{
static const unsigned map[8] = { 0, 1, 5, 4, 2, 3, 6, 7 };
unsigned idx = 0;
while (len >= (is_s32 ? 4 : 2)) {
unsigned offset = map[idx] * 4;
u8 *out = dst + offset;
*out++ = 0;
if (is_s32) {
src++;
*out++ = *src++;
} else {
*out++ = 0;
}
*out++ = *src++;
*out = *src++;
len -= is_s32 ? 4 : 2;
idx++;
}
}
static void cobalt_alsa_pb_pcm_data(struct snd_cobalt_card *cobsc,
u8 *pcm_data,
size_t skip,
size_t samples)
{
struct snd_pcm_substream *substream;
struct snd_pcm_runtime *runtime;
unsigned long flags;
unsigned int pos;
unsigned int stride;
bool is_s32;
unsigned i;
dprintk("cobalt alsa pb ptr=%p data=%p samples=%zd\n", cobsc,
pcm_data, samples);
substream = cobsc->playback_pcm_substream;
if (substream == NULL) {
dprintk("substream was NULL\n");
return;
}
runtime = substream->runtime;
if (runtime == NULL) {
dprintk("runtime was NULL\n");
return;
}
is_s32 = runtime->format == SNDRV_PCM_FORMAT_S32_LE;
stride = runtime->frame_bits >> 3;
if (stride == 0) {
dprintk("stride is zero\n");
return;
}
if (samples == 0) {
dprintk("%s: samples was zero\n", __func__);
return;
}
if (runtime->dma_area == NULL) {
dprintk("dma area was NULL - ignoring\n");
return;
}
pos = cobsc->pb_pos % cobsc->pb_size;
for (i = 0; i < cobsc->pb_count / (8 * 4); i++)
pb_sample_cpy(pcm_data + i * skip,
runtime->dma_area + pos + i * stride,
stride, is_s32);
snd_pcm_stream_lock_irqsave(substream, flags);
cobsc->pb_pos += i * stride;
snd_pcm_stream_unlock_irqrestore(substream, flags);
if (cobsc->pb_pos % cobsc->pb_count == 0)
snd_pcm_period_elapsed(substream);
}
static int alsa_pb_fnc(struct vb2_buffer *vb, void *priv)
{
struct cobalt_stream *s = priv;
if (s->alsa->alsa_pb_channel)
cobalt_alsa_pb_pcm_data(s->alsa,
vb2_plane_vaddr(vb, 0),
8 * 4,
vb2_get_plane_payload(vb, 0) / (8 * 4));
return 0;
}
static int snd_cobalt_pcm_playback_open(struct snd_pcm_substream *substream)
{
struct snd_cobalt_card *cobsc = snd_pcm_substream_chip(substream);
struct snd_pcm_runtime *runtime = substream->runtime;
struct cobalt_stream *s = cobsc->s;
runtime->hw = snd_cobalt_playback;
snd_pcm_hw_constraint_integer(runtime, SNDRV_PCM_HW_PARAM_PERIODS);
cobsc->playback_pcm_substream = substream;
runtime->private_data = s;
cobsc->alsa_playback_cnt++;
if (cobsc->alsa_playback_cnt == 1) {
int rc;
rc = vb2_thread_start(&s->q, alsa_pb_fnc, s, s->vdev.name);
if (rc) {
cobsc->alsa_playback_cnt--;
return rc;
}
}
return 0;
}
static int snd_cobalt_pcm_playback_close(struct snd_pcm_substream *substream)
{
struct snd_cobalt_card *cobsc = snd_pcm_substream_chip(substream);
struct cobalt_stream *s = cobsc->s;
cobsc->alsa_playback_cnt--;
if (cobsc->alsa_playback_cnt == 0)
vb2_thread_stop(&s->q);
return 0;
}
static int snd_cobalt_pcm_pb_prepare(struct snd_pcm_substream *substream)
{
struct snd_cobalt_card *cobsc = snd_pcm_substream_chip(substream);
cobsc->pb_size = snd_pcm_lib_buffer_bytes(substream);
cobsc->pb_count = snd_pcm_lib_period_bytes(substream);
cobsc->pb_pos = 0;
return 0;
}
static int snd_cobalt_pcm_pb_trigger(struct snd_pcm_substream *substream,
int cmd)
{
struct snd_cobalt_card *cobsc = snd_pcm_substream_chip(substream);
switch (cmd) {
case SNDRV_PCM_TRIGGER_START:
if (cobsc->alsa_pb_channel)
return -EBUSY;
cobsc->alsa_pb_channel = true;
return 0;
case SNDRV_PCM_TRIGGER_STOP:
cobsc->alsa_pb_channel = false;
return 0;
default:
return -EINVAL;
}
}
static
snd_pcm_uframes_t snd_cobalt_pcm_pb_pointer(struct snd_pcm_substream *substream)
{
struct snd_cobalt_card *cobsc = snd_pcm_substream_chip(substream);
size_t ptr;
ptr = cobsc->pb_pos;
return bytes_to_frames(substream->runtime, ptr) %
substream->runtime->buffer_size;
}
static struct page *snd_pcm_get_vmalloc_page(struct snd_pcm_substream *subs,
unsigned long offset)
{
void *pageptr = subs->runtime->dma_area + offset;
return vmalloc_to_page(pageptr);
}
static struct snd_pcm_ops snd_cobalt_pcm_capture_ops = {
.open = snd_cobalt_pcm_capture_open,
.close = snd_cobalt_pcm_capture_close,
.ioctl = snd_cobalt_pcm_ioctl,
.hw_params = snd_cobalt_pcm_hw_params,
.hw_free = snd_cobalt_pcm_hw_free,
.prepare = snd_cobalt_pcm_prepare,
.trigger = snd_cobalt_pcm_trigger,
.pointer = snd_cobalt_pcm_pointer,
.page = snd_pcm_get_vmalloc_page,
};
static struct snd_pcm_ops snd_cobalt_pcm_playback_ops = {
.open = snd_cobalt_pcm_playback_open,
.close = snd_cobalt_pcm_playback_close,
.ioctl = snd_cobalt_pcm_ioctl,
.hw_params = snd_cobalt_pcm_hw_params,
.hw_free = snd_cobalt_pcm_hw_free,
.prepare = snd_cobalt_pcm_pb_prepare,
.trigger = snd_cobalt_pcm_pb_trigger,
.pointer = snd_cobalt_pcm_pb_pointer,
.page = snd_pcm_get_vmalloc_page,
};
int snd_cobalt_pcm_create(struct snd_cobalt_card *cobsc)
{
struct snd_pcm *sp;
struct snd_card *sc = cobsc->sc;
struct cobalt_stream *s = cobsc->s;
struct cobalt *cobalt = s->cobalt;
int ret;
s->q.gfp_flags |= __GFP_ZERO;
if (!s->is_output) {
cobalt_s_bit_sysctrl(cobalt,
COBALT_SYS_CTRL_AUDIO_IPP_RESETN_BIT(s->video_channel),
0);
mdelay(2);
cobalt_s_bit_sysctrl(cobalt,
COBALT_SYS_CTRL_AUDIO_IPP_RESETN_BIT(s->video_channel),
1);
mdelay(1);
ret = snd_pcm_new(sc, "Cobalt PCM-In HDMI",
0, /* PCM device 0, the only one for this card */
0, /* 0 playback substreams */
1, /* 1 capture substream */
&sp);
if (ret) {
cobalt_err("snd_cobalt_pcm_create() failed for input with err %d\n",
ret);
goto err_exit;
}
snd_pcm_set_ops(sp, SNDRV_PCM_STREAM_CAPTURE,
&snd_cobalt_pcm_capture_ops);
sp->info_flags = 0;
sp->private_data = cobsc;
strlcpy(sp->name, "cobalt", sizeof(sp->name));
} else {
cobalt_s_bit_sysctrl(cobalt,
COBALT_SYS_CTRL_AUDIO_OPP_RESETN_BIT, 0);
mdelay(2);
cobalt_s_bit_sysctrl(cobalt,
COBALT_SYS_CTRL_AUDIO_OPP_RESETN_BIT, 1);
mdelay(1);
ret = snd_pcm_new(sc, "Cobalt PCM-Out HDMI",
0, /* PCM device 0, the only one for this card */
1, /* 0 playback substreams */
0, /* 1 capture substream */
&sp);
if (ret) {
cobalt_err("snd_cobalt_pcm_create() failed for output with err %d\n",
ret);
goto err_exit;
}
snd_pcm_set_ops(sp, SNDRV_PCM_STREAM_PLAYBACK,
&snd_cobalt_pcm_playback_ops);
sp->info_flags = 0;
sp->private_data = cobsc;
strlcpy(sp->name, "cobalt", sizeof(sp->name));
}
return 0;
err_exit:
return ret;
}