aboutsummaryrefslogtreecommitdiff
path: root/hw/dma/soc_dma.c
diff options
context:
space:
mode:
Diffstat (limited to 'hw/dma/soc_dma.c')
-rw-r--r--hw/dma/soc_dma.c366
1 files changed, 366 insertions, 0 deletions
diff --git a/hw/dma/soc_dma.c b/hw/dma/soc_dma.c
new file mode 100644
index 0000000000..5e3491d373
--- /dev/null
+++ b/hw/dma/soc_dma.c
@@ -0,0 +1,366 @@
+/*
+ * On-chip DMA controller framework.
+ *
+ * Copyright (C) 2008 Nokia Corporation
+ * Written by Andrzej Zaborowski <andrew@openedhand.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 or
+ * (at your option) version 3 of the License.
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+#include "qemu-common.h"
+#include "qemu/timer.h"
+#include "hw/arm/soc_dma.h"
+
+static void transfer_mem2mem(struct soc_dma_ch_s *ch)
+{
+ memcpy(ch->paddr[0], ch->paddr[1], ch->bytes);
+ ch->paddr[0] += ch->bytes;
+ ch->paddr[1] += ch->bytes;
+}
+
+static void transfer_mem2fifo(struct soc_dma_ch_s *ch)
+{
+ ch->io_fn[1](ch->io_opaque[1], ch->paddr[0], ch->bytes);
+ ch->paddr[0] += ch->bytes;
+}
+
+static void transfer_fifo2mem(struct soc_dma_ch_s *ch)
+{
+ ch->io_fn[0](ch->io_opaque[0], ch->paddr[1], ch->bytes);
+ ch->paddr[1] += ch->bytes;
+}
+
+/* This is further optimisable but isn't very important because often
+ * DMA peripherals forbid this kind of transfers and even when they don't,
+ * oprating systems may not need to use them. */
+static void *fifo_buf;
+static int fifo_size;
+static void transfer_fifo2fifo(struct soc_dma_ch_s *ch)
+{
+ if (ch->bytes > fifo_size)
+ fifo_buf = g_realloc(fifo_buf, fifo_size = ch->bytes);
+
+ /* Implement as transfer_fifo2linear + transfer_linear2fifo. */
+ ch->io_fn[0](ch->io_opaque[0], fifo_buf, ch->bytes);
+ ch->io_fn[1](ch->io_opaque[1], fifo_buf, ch->bytes);
+}
+
+struct dma_s {
+ struct soc_dma_s soc;
+ int chnum;
+ uint64_t ch_enable_mask;
+ int64_t channel_freq;
+ int enabled_count;
+
+ struct memmap_entry_s {
+ enum soc_dma_port_type type;
+ hwaddr addr;
+ union {
+ struct {
+ void *opaque;
+ soc_dma_io_t fn;
+ int out;
+ } fifo;
+ struct {
+ void *base;
+ size_t size;
+ } mem;
+ } u;
+ } *memmap;
+ int memmap_size;
+
+ struct soc_dma_ch_s ch[0];
+};
+
+static void soc_dma_ch_schedule(struct soc_dma_ch_s *ch, int delay_bytes)
+{
+ int64_t now = qemu_get_clock_ns(vm_clock);
+ struct dma_s *dma = (struct dma_s *) ch->dma;
+
+ qemu_mod_timer(ch->timer, now + delay_bytes / dma->channel_freq);
+}
+
+static void soc_dma_ch_run(void *opaque)
+{
+ struct soc_dma_ch_s *ch = (struct soc_dma_ch_s *) opaque;
+
+ ch->running = 1;
+ ch->dma->setup_fn(ch);
+ ch->transfer_fn(ch);
+ ch->running = 0;
+
+ if (ch->enable)
+ soc_dma_ch_schedule(ch, ch->bytes);
+ ch->bytes = 0;
+}
+
+static inline struct memmap_entry_s *soc_dma_lookup(struct dma_s *dma,
+ hwaddr addr)
+{
+ struct memmap_entry_s *lo;
+ int hi;
+
+ lo = dma->memmap;
+ hi = dma->memmap_size;
+
+ while (hi > 1) {
+ hi /= 2;
+ if (lo[hi].addr <= addr)
+ lo += hi;
+ }
+
+ return lo;
+}
+
+static inline enum soc_dma_port_type soc_dma_ch_update_type(
+ struct soc_dma_ch_s *ch, int port)
+{
+ struct dma_s *dma = (struct dma_s *) ch->dma;
+ struct memmap_entry_s *entry = soc_dma_lookup(dma, ch->vaddr[port]);
+
+ if (entry->type == soc_dma_port_fifo) {
+ while (entry < dma->memmap + dma->memmap_size &&
+ entry->u.fifo.out != port)
+ entry ++;
+ if (entry->addr != ch->vaddr[port] || entry->u.fifo.out != port)
+ return soc_dma_port_other;
+
+ if (ch->type[port] != soc_dma_access_const)
+ return soc_dma_port_other;
+
+ ch->io_fn[port] = entry->u.fifo.fn;
+ ch->io_opaque[port] = entry->u.fifo.opaque;
+ return soc_dma_port_fifo;
+ } else if (entry->type == soc_dma_port_mem) {
+ if (entry->addr > ch->vaddr[port] ||
+ entry->addr + entry->u.mem.size <= ch->vaddr[port])
+ return soc_dma_port_other;
+
+ /* TODO: support constant memory address for source port as used for
+ * drawing solid rectangles by PalmOS(R). */
+ if (ch->type[port] != soc_dma_access_const)
+ return soc_dma_port_other;
+
+ ch->paddr[port] = (uint8_t *) entry->u.mem.base +
+ (ch->vaddr[port] - entry->addr);
+ /* TODO: save bytes left to the end of the mapping somewhere so we
+ * can check we're not reading beyond it. */
+ return soc_dma_port_mem;
+ } else
+ return soc_dma_port_other;
+}
+
+void soc_dma_ch_update(struct soc_dma_ch_s *ch)
+{
+ enum soc_dma_port_type src, dst;
+
+ src = soc_dma_ch_update_type(ch, 0);
+ if (src == soc_dma_port_other) {
+ ch->update = 0;
+ ch->transfer_fn = ch->dma->transfer_fn;
+ return;
+ }
+ dst = soc_dma_ch_update_type(ch, 1);
+
+ /* TODO: use src and dst as array indices. */
+ if (src == soc_dma_port_mem && dst == soc_dma_port_mem)
+ ch->transfer_fn = transfer_mem2mem;
+ else if (src == soc_dma_port_mem && dst == soc_dma_port_fifo)
+ ch->transfer_fn = transfer_mem2fifo;
+ else if (src == soc_dma_port_fifo && dst == soc_dma_port_mem)
+ ch->transfer_fn = transfer_fifo2mem;
+ else if (src == soc_dma_port_fifo && dst == soc_dma_port_fifo)
+ ch->transfer_fn = transfer_fifo2fifo;
+ else
+ ch->transfer_fn = ch->dma->transfer_fn;
+
+ ch->update = (dst != soc_dma_port_other);
+}
+
+static void soc_dma_ch_freq_update(struct dma_s *s)
+{
+ if (s->enabled_count)
+ /* We completely ignore channel priorities and stuff */
+ s->channel_freq = s->soc.freq / s->enabled_count;
+ else {
+ /* TODO: Signal that we want to disable the functional clock and let
+ * the platform code decide what to do with it, i.e. check that
+ * auto-idle is enabled in the clock controller and if we are stopping
+ * the clock, do the same with any parent clocks that had only one
+ * user keeping them on and auto-idle enabled. */
+ }
+}
+
+void soc_dma_set_request(struct soc_dma_ch_s *ch, int level)
+{
+ struct dma_s *dma = (struct dma_s *) ch->dma;
+
+ dma->enabled_count += level - ch->enable;
+
+ if (level)
+ dma->ch_enable_mask |= 1 << ch->num;
+ else
+ dma->ch_enable_mask &= ~(1 << ch->num);
+
+ if (level != ch->enable) {
+ soc_dma_ch_freq_update(dma);
+ ch->enable = level;
+
+ if (!ch->enable)
+ qemu_del_timer(ch->timer);
+ else if (!ch->running)
+ soc_dma_ch_run(ch);
+ else
+ soc_dma_ch_schedule(ch, 1);
+ }
+}
+
+void soc_dma_reset(struct soc_dma_s *soc)
+{
+ struct dma_s *s = (struct dma_s *) soc;
+
+ s->soc.drqbmp = 0;
+ s->ch_enable_mask = 0;
+ s->enabled_count = 0;
+ soc_dma_ch_freq_update(s);
+}
+
+/* TODO: take a functional-clock argument */
+struct soc_dma_s *soc_dma_init(int n)
+{
+ int i;
+ struct dma_s *s = g_malloc0(sizeof(*s) + n * sizeof(*s->ch));
+
+ s->chnum = n;
+ s->soc.ch = s->ch;
+ for (i = 0; i < n; i ++) {
+ s->ch[i].dma = &s->soc;
+ s->ch[i].num = i;
+ s->ch[i].timer = qemu_new_timer_ns(vm_clock, soc_dma_ch_run, &s->ch[i]);
+ }
+
+ soc_dma_reset(&s->soc);
+ fifo_size = 0;
+
+ return &s->soc;
+}
+
+void soc_dma_port_add_fifo(struct soc_dma_s *soc, hwaddr virt_base,
+ soc_dma_io_t fn, void *opaque, int out)
+{
+ struct memmap_entry_s *entry;
+ struct dma_s *dma = (struct dma_s *) soc;
+
+ dma->memmap = g_realloc(dma->memmap, sizeof(*entry) *
+ (dma->memmap_size + 1));
+ entry = soc_dma_lookup(dma, virt_base);
+
+ if (dma->memmap_size) {
+ if (entry->type == soc_dma_port_mem) {
+ if (entry->addr <= virt_base &&
+ entry->addr + entry->u.mem.size > virt_base) {
+ fprintf(stderr, "%s: FIFO at " TARGET_FMT_lx
+ " collides with RAM region at " TARGET_FMT_lx
+ "-" TARGET_FMT_lx "\n", __FUNCTION__,
+ (target_ulong) virt_base,
+ (target_ulong) entry->addr, (target_ulong)
+ (entry->addr + entry->u.mem.size));
+ exit(-1);
+ }
+
+ if (entry->addr <= virt_base)
+ entry ++;
+ } else
+ while (entry < dma->memmap + dma->memmap_size &&
+ entry->addr <= virt_base) {
+ if (entry->addr == virt_base && entry->u.fifo.out == out) {
+ fprintf(stderr, "%s: FIFO at " TARGET_FMT_lx
+ " collides FIFO at " TARGET_FMT_lx "\n",
+ __FUNCTION__, (target_ulong) virt_base,
+ (target_ulong) entry->addr);
+ exit(-1);
+ }
+
+ entry ++;
+ }
+
+ memmove(entry + 1, entry,
+ (uint8_t *) (dma->memmap + dma->memmap_size ++) -
+ (uint8_t *) entry);
+ } else
+ dma->memmap_size ++;
+
+ entry->addr = virt_base;
+ entry->type = soc_dma_port_fifo;
+ entry->u.fifo.fn = fn;
+ entry->u.fifo.opaque = opaque;
+ entry->u.fifo.out = out;
+}
+
+void soc_dma_port_add_mem(struct soc_dma_s *soc, uint8_t *phys_base,
+ hwaddr virt_base, size_t size)
+{
+ struct memmap_entry_s *entry;
+ struct dma_s *dma = (struct dma_s *) soc;
+
+ dma->memmap = g_realloc(dma->memmap, sizeof(*entry) *
+ (dma->memmap_size + 1));
+ entry = soc_dma_lookup(dma, virt_base);
+
+ if (dma->memmap_size) {
+ if (entry->type == soc_dma_port_mem) {
+ if ((entry->addr >= virt_base && entry->addr < virt_base + size) ||
+ (entry->addr <= virt_base &&
+ entry->addr + entry->u.mem.size > virt_base)) {
+ fprintf(stderr, "%s: RAM at " TARGET_FMT_lx "-" TARGET_FMT_lx
+ " collides with RAM region at " TARGET_FMT_lx
+ "-" TARGET_FMT_lx "\n", __FUNCTION__,
+ (target_ulong) virt_base,
+ (target_ulong) (virt_base + size),
+ (target_ulong) entry->addr, (target_ulong)
+ (entry->addr + entry->u.mem.size));
+ exit(-1);
+ }
+
+ if (entry->addr <= virt_base)
+ entry ++;
+ } else {
+ if (entry->addr >= virt_base &&
+ entry->addr < virt_base + size) {
+ fprintf(stderr, "%s: RAM at " TARGET_FMT_lx "-" TARGET_FMT_lx
+ " collides with FIFO at " TARGET_FMT_lx
+ "\n", __FUNCTION__,
+ (target_ulong) virt_base,
+ (target_ulong) (virt_base + size),
+ (target_ulong) entry->addr);
+ exit(-1);
+ }
+
+ while (entry < dma->memmap + dma->memmap_size &&
+ entry->addr <= virt_base)
+ entry ++;
+ }
+
+ memmove(entry + 1, entry,
+ (uint8_t *) (dma->memmap + dma->memmap_size ++) -
+ (uint8_t *) entry);
+ } else
+ dma->memmap_size ++;
+
+ entry->addr = virt_base;
+ entry->type = soc_dma_port_mem;
+ entry->u.mem.base = phys_base;
+ entry->u.mem.size = size;
+}
+
+/* TODO: port removal for ports like PCMCIA memory */