diff options
Diffstat (limited to 'sound/hda/hdac_stream.c')
-rw-r--r-- | sound/hda/hdac_stream.c | 473 |
1 files changed, 473 insertions, 0 deletions
diff --git a/sound/hda/hdac_stream.c b/sound/hda/hdac_stream.c new file mode 100644 index 00000000000000..0515377823ca72 --- /dev/null +++ b/sound/hda/hdac_stream.c @@ -0,0 +1,473 @@ +/* + * HD-audio stream operations + */ + +#include <linux/kernel.h> +#include <linux/delay.h> +#include <linux/export.h> +#include <sound/core.h> +#include <sound/pcm.h> +#include <sound/hdaudio.h> +#include <sound/hda_register.h> + +/* initialize each stream (aka device) + * assign the starting bdl address to each stream (device) + * and initialize + */ +void snd_hdac_stream_init(struct hdac_bus *bus, struct hdac_stream *azx_dev, + int idx, int direction, int tag) +{ + azx_dev->bus = bus; + if (bus->posbuf.area) + azx_dev->posbuf = (u32 __iomem *)(bus->posbuf.area + idx * 8); + /* offset: SDI0=0x80, SDI1=0xa0, ... SDO3=0x160 */ + azx_dev->sd_addr = bus->remap_addr + (0x20 * idx + 0x80); + /* int mask: SDI0=0x01, SDI1=0x02, ... SDO3=0x80 */ + azx_dev->sd_int_sta_mask = 1 << idx; + azx_dev->index = idx; + azx_dev->direction = direction; + azx_dev->stream_tag = tag; +} +EXPORT_SYMBOL_GPL(snd_hdac_stream_init); + +/* start a stream */ +void snd_hdac_stream_start(struct hdac_stream *azx_dev, bool fresh_start) +{ + struct hdac_bus *bus = azx_dev->bus; + + azx_dev->start_wallclk = azx_readl(bus, WALLCLK); + if (!fresh_start) + azx_dev->start_wallclk -= azx_dev->period_wallclk; + + /* enable SIE */ + azx_writel(bus, INTCTL, azx_readl(bus, INTCTL) | (1 << azx_dev->index)); + /* set DMA start and interrupt mask */ + azx_sd_writeb(bus, azx_dev, SD_CTL, + azx_sd_readb(bus, azx_dev, SD_CTL) | + SD_CTL_DMA_START | SD_INT_MASK); + azx_dev->running = true; +} +EXPORT_SYMBOL_GPL(snd_hdac_stream_start); + +/* stop DMA */ +void snd_hdac_stream_clear(struct hdac_stream *azx_dev) +{ + struct hdac_bus *bus = azx_dev->bus; + + azx_sd_writeb(bus, azx_dev, SD_CTL, + azx_sd_readb(bus, azx_dev, SD_CTL) & + ~(SD_CTL_DMA_START | SD_INT_MASK)); + azx_sd_writeb(bus, azx_dev, SD_STS, SD_INT_MASK); /* to be sure */ + azx_dev->running = false; +} +EXPORT_SYMBOL_GPL(snd_hdac_stream_clear); + +/* stop a stream */ +void snd_hdac_stream_stop(struct hdac_stream *azx_dev) +{ + struct hdac_bus *bus = azx_dev->bus; + + snd_hdac_stream_clear(azx_dev); + /* disable SIE */ + azx_writel(bus, INTCTL, + azx_readl(bus, INTCTL) & ~(1 << azx_dev->index)); +} +EXPORT_SYMBOL_GPL(snd_hdac_stream_stop); + +/* reset stream */ +void snd_hdac_stream_reset(struct hdac_stream *azx_dev) +{ + struct hdac_bus *bus = azx_dev->bus; + unsigned char val; + int timeout; + + snd_hdac_stream_clear(azx_dev); + + azx_sd_writeb(bus, azx_dev, SD_CTL, + azx_sd_readb(bus, azx_dev, SD_CTL) | + SD_CTL_STREAM_RESET); + udelay(3); + timeout = 300; + do { + val = azx_sd_readb(bus, azx_dev, SD_CTL) & SD_CTL_STREAM_RESET; + if (val) + break; + } while (--timeout); + val &= ~SD_CTL_STREAM_RESET; + azx_sd_writeb(bus, azx_dev, SD_CTL, val); + udelay(3); + + timeout = 300; + /* waiting for hardware to report that the stream is out of reset */ + do { + val = azx_sd_readb(bus, azx_dev, SD_CTL) & SD_CTL_STREAM_RESET; + if (!val) + break; + } while (--timeout); + + /* reset first position - may not be synced with hw at this time */ + if (azx_dev->posbuf) + *azx_dev->posbuf = 0; +} +EXPORT_SYMBOL_GPL(snd_hdac_stream_reset); + +/* + * set up the SD for streaming + */ +int snd_hdac_stream_setup(struct hdac_stream *azx_dev) +{ + struct hdac_bus *bus = azx_dev->bus; + struct snd_pcm_runtime *runtime = azx_dev->substream->runtime; + unsigned int val; + + /* make sure the run bit is zero for SD */ + snd_hdac_stream_clear(azx_dev); + /* program the stream_tag */ + val = azx_sd_readl(bus, azx_dev, SD_CTL); + val = (val & ~SD_CTL_STREAM_TAG_MASK) | + (azx_dev->stream_tag << SD_CTL_STREAM_TAG_SHIFT); + if (!bus->snoop) + val |= SD_CTL_TRAFFIC_PRIO; + azx_sd_writel(bus, azx_dev, SD_CTL, val); + + /* program the length of samples in cyclic buffer */ + azx_sd_writel(bus, azx_dev, SD_CBL, azx_dev->bufsize); + + /* program the stream format */ + /* this value needs to be the same as the one programmed */ + azx_sd_writew(bus, azx_dev, SD_FORMAT, azx_dev->format_val); + + /* program the stream LVI (last valid index) of the BDL */ + azx_sd_writew(bus, azx_dev, SD_LVI, azx_dev->frags - 1); + + /* program the BDL address */ + /* lower BDL address */ + azx_sd_writel(bus, azx_dev, SD_BDLPL, (u32)azx_dev->bdl.addr); + /* upper BDL address */ + azx_sd_writel(bus, azx_dev, SD_BDLPU, + upper_32_bits(azx_dev->bdl.addr)); + + /* enable the position buffer */ + if (bus->use_posbuf) { + if (!(azx_readl(bus, DPLBASE) & AZX_DPLBASE_ENABLE)) + azx_writel(bus, DPLBASE, + (u32)bus->posbuf.addr | AZX_DPLBASE_ENABLE); + } + + /* set the interrupt enable bits in the descriptor control register */ + azx_sd_writel(bus, azx_dev, SD_CTL, + azx_sd_readl(bus, azx_dev, SD_CTL) | SD_INT_MASK); + + if (azx_dev->direction == SNDRV_PCM_STREAM_PLAYBACK) + azx_dev->fifo_size = + azx_sd_readw(bus, azx_dev, SD_FIFOSIZE) + 1; + else + azx_dev->fifo_size = 0; + + /* when LPIB delay correction gives a small negative value, + * we ignore it; currently set the threshold statically to + * 64 frames + */ + if (runtime->period_size > 64) + azx_dev->delay_negative_threshold = + -frames_to_bytes(runtime, 64); + else + azx_dev->delay_negative_threshold = 0; + + /* wallclk has 24Mhz clock source */ + azx_dev->period_wallclk = (((runtime->period_size * 24000) / + runtime->rate) * 1000); + + return 0; +} +EXPORT_SYMBOL_GPL(snd_hdac_stream_setup); + +void snd_hdac_stream_cleanup(struct hdac_stream *azx_dev) +{ + struct hdac_bus *bus = azx_dev->bus; + + azx_sd_writel(bus, azx_dev, SD_BDLPL, 0); + azx_sd_writel(bus, azx_dev, SD_BDLPU, 0); + azx_sd_writel(bus, azx_dev, SD_CTL, 0); + azx_dev->bufsize = 0; + azx_dev->period_bytes = 0; + azx_dev->format_val = 0; +} +EXPORT_SYMBOL_GPL(snd_hdac_stream_cleanup); + +/* assign a stream for the PCM */ +struct hdac_stream *snd_hdac_stream_assign(struct hdac_bus *bus, + struct snd_pcm_substream *substream) +{ + struct hdac_stream *azx_dev; + struct hdac_stream *res = NULL; + + /* make a non-zero unique key for the substream */ + int key = (substream->pcm->device << 16) | (substream->number << 2) | + (substream->stream + 1); + + list_for_each_entry(azx_dev, &bus->stream_list, list) { + if (azx_dev->direction != substream->stream) + continue; + if (azx_dev->opened) + continue; + if (azx_dev->assigned_key == key) { + res = azx_dev; + break; + } + if (!res || bus->reverse_assign) + res = azx_dev; + } + if (res) { + spin_lock_irq(&bus->reg_lock); + res->opened = 1; + res->running = 0; + res->assigned_key = key; + res->substream = substream; + spin_lock_irq(&bus->reg_lock); + } + return res; +} +EXPORT_SYMBOL_GPL(snd_hdac_stream_assign); + +/* release the assigned stream */ +void snd_hdac_stream_release(struct hdac_stream *azx_dev) +{ + struct hdac_bus *bus = azx_dev->bus; + + spin_lock_irq(&bus->reg_lock); + azx_dev->opened = 0; + azx_dev->running = 0; + azx_dev->substream = NULL; + spin_lock_irq(&bus->reg_lock); +} +EXPORT_SYMBOL_GPL(snd_hdac_stream_release); + +/* + * set up a BDL entry + */ +static int setup_bdle(struct hdac_bus *bus, + struct snd_dma_buffer *dmab, + struct hdac_stream *azx_dev, u32 **bdlp, + int ofs, int size, int with_ioc) +{ + u32 *bdl = *bdlp; + + while (size > 0) { + dma_addr_t addr; + int chunk; + + if (azx_dev->frags >= AZX_MAX_BDL_ENTRIES) + return -EINVAL; + + addr = snd_sgbuf_get_addr(dmab, ofs); + /* program the address field of the BDL entry */ + bdl[0] = cpu_to_le32((u32)addr); + bdl[1] = cpu_to_le32(upper_32_bits(addr)); + /* program the size field of the BDL entry */ + chunk = snd_sgbuf_get_chunk_size(dmab, ofs, size); + /* one BDLE cannot cross 4K boundary on CTHDA chips */ + if (bus->align_bdle_4k) { + u32 remain = 0x1000 - (ofs & 0xfff); + + if (chunk > remain) + chunk = remain; + } + bdl[2] = cpu_to_le32(chunk); + /* program the IOC to enable interrupt + * only when the whole fragment is processed + */ + size -= chunk; + bdl[3] = (size || !with_ioc) ? 0 : cpu_to_le32(0x01); + bdl += 4; + azx_dev->frags++; + ofs += chunk; + } + *bdlp = bdl; + return ofs; +} + +/* + * set up BDL entries + */ +int snd_hdac_stream_setup_periods(struct hdac_stream *azx_dev) +{ + struct hdac_bus *bus = azx_dev->bus; + struct snd_pcm_substream *substream = azx_dev->substream; + struct snd_pcm_runtime *runtime = substream->runtime; + u32 *bdl; + int i, ofs, periods, period_bytes; + int pos_adj, pos_align; + + /* reset BDL address */ + azx_sd_writel(bus, azx_dev, SD_BDLPL, 0); + azx_sd_writel(bus, azx_dev, SD_BDLPU, 0); + + period_bytes = azx_dev->period_bytes; + periods = azx_dev->bufsize / period_bytes; + + /* program the initial BDL entries */ + bdl = (u32 *)azx_dev->bdl.area; + ofs = 0; + azx_dev->frags = 0; + + pos_adj = bus->bdl_pos_adj; + if (!azx_dev->no_period_wakeup && pos_adj > 0) { + pos_align = pos_adj; + pos_adj = (pos_adj * runtime->rate + 47999) / 48000; + if (!pos_adj) + pos_adj = pos_align; + else + pos_adj = ((pos_adj + pos_align - 1) / pos_align) * + pos_align; + pos_adj = frames_to_bytes(runtime, pos_adj); + if (pos_adj >= period_bytes) { + dev_warn(bus->dev, "Too big adjustment %d\n", + pos_adj); + pos_adj = 0; + } else { + ofs = setup_bdle(bus, snd_pcm_get_dma_buf(substream), + azx_dev, + &bdl, ofs, pos_adj, true); + if (ofs < 0) + goto error; + } + } else + pos_adj = 0; + + for (i = 0; i < periods; i++) { + if (i == periods - 1 && pos_adj) + ofs = setup_bdle(bus, snd_pcm_get_dma_buf(substream), + azx_dev, &bdl, ofs, + period_bytes - pos_adj, 0); + else + ofs = setup_bdle(bus, snd_pcm_get_dma_buf(substream), + azx_dev, &bdl, ofs, + period_bytes, + !azx_dev->no_period_wakeup); + if (ofs < 0) + goto error; + } + return 0; + + error: + dev_err(bus->dev, "Too many BDL entries: buffer=%d, period=%d\n", + azx_dev->bufsize, period_bytes); + return -EINVAL; +} +EXPORT_SYMBOL_GPL(snd_hdac_stream_setup_periods); + +static cycle_t azx_cc_read(const struct cyclecounter *cc) +{ + struct hdac_stream *azx_dev = container_of(cc, struct hdac_stream, cc); + + return azx_readl(azx_dev->bus, WALLCLK); +} + +static void azx_timecounter_init(struct hdac_stream *azx_dev, + bool force, cycle_t last) +{ + struct timecounter *tc = &azx_dev->tc; + struct cyclecounter *cc = &azx_dev->cc; + u64 nsec; + + cc->read = azx_cc_read; + cc->mask = CLOCKSOURCE_MASK(32); + + /* + * Converting from 24 MHz to ns means applying a 125/3 factor. + * To avoid any saturation issues in intermediate operations, + * the 125 factor is applied first. The division is applied + * last after reading the timecounter value. + * Applying the 1/3 factor as part of the multiplication + * requires at least 20 bits for a decent precision, however + * overflows occur after about 4 hours or less, not a option. + */ + + cc->mult = 125; /* saturation after 195 years */ + cc->shift = 0; + + nsec = 0; /* audio time is elapsed time since trigger */ + timecounter_init(tc, cc, nsec); + if (force) { + /* + * force timecounter to use predefined value, + * used for synchronized starts + */ + tc->cycle_last = last; + } +} + +void snd_hdac_stream_timecounter_init(struct hdac_stream *azx_dev, + unsigned int streams) +{ + struct hdac_bus *bus = azx_dev->bus; + struct snd_pcm_runtime *runtime = azx_dev->substream->runtime; + struct hdac_stream *s; + bool inited = false; + cycle_t cycle_last = 0; + int i = 0; + + list_for_each_entry(s, &bus->stream_list, list) { + if (streams & (1 << i)) { + azx_timecounter_init(s, inited, cycle_last); + if (!inited) { + inited = true; + cycle_last = s->tc.cycle_last; + } + } + i++; + } + + snd_pcm_gettime(runtime, &runtime->trigger_tstamp); + runtime->trigger_tstamp_latched = true; +} +EXPORT_SYMBOL_GPL(snd_hdac_stream_timecounter_init); + +void snd_hdac_stream_sync_trigger(struct hdac_stream *azx_dev, bool set, + unsigned int streams, unsigned int reg) +{ + struct hdac_bus *bus = azx_dev->bus; + unsigned int mask = ~streams; + + if (!set) + streams = 0; + if (!reg) + reg = AZX_REG_SSYNC; + _azx_write(l, bus, reg, (_azx_read(l, bus, reg) & mask) | streams); +} +EXPORT_SYMBOL_GPL(snd_hdac_stream_sync_trigger); + +/* wait until all FIFOs get ready */ +void snd_hdac_stream_sync(struct hdac_stream *azx_dev, bool start, + unsigned int streams) +{ + struct hdac_bus *bus = azx_dev->bus; + int i, nwait, timeout; + struct hdac_stream *s; + + for (timeout = 5000; timeout; timeout--) { + nwait = 0; + i = 0; + list_for_each_entry(s, &bus->stream_list, list) { + if (streams & (1 << i)) { + if (start) { + /* check FIFO gets ready */ + if (!(azx_sd_readb(bus, s, SD_STS) & + SD_STS_FIFO_READY)) + nwait++; + } else { + /* check RUN bit is cleared */ + if (azx_sd_readb(bus, s, SD_CTL) & + SD_CTL_DMA_START) + nwait++; + } + } + i++; + } + if (!nwait) + break; + cpu_relax(); + } +} +EXPORT_SYMBOL_GPL(snd_hdac_stream_sync); |