diff options
Diffstat (limited to 'hw/dma/soc_dma.c')
-rw-r--r-- | hw/dma/soc_dma.c | 366 |
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 */ |