c3a68dfd19
In soc_dma_set_request() we try to set a bit in a uint64_t, but we
do it with "1 << ch->num", which can't set any bits past 31;
any use for a channel number of 32 or more would fail due to
integer overflow.
This doesn't happen in practice for our current use of this code,
because the worst case is when we call soc_dma_init() with an
argument of 32 for the number of channels, and QEMU builds with
-fwrapv so the shift into the sign bit is well-defined. However,
it's obviously not the intended behaviour of the code.
Add casts to force the shift to be done as 64-bit arithmetic,
allowing up to 64 channels.
Found by Linux Verification Center (linuxtesting.org) with SVACE.
Fixes: afbb5194d4
("Handle on-chip DMA controllers in one place, convert OMAP DMA to use it.")
Signed-off-by: Anastasia Belova <abelova@astralinux.ru>
Message-id: 20240409115301.21829-1-abelova@astralinux.ru
[PMM: Edit commit message to clarify that this doesn't actually
bite us in our current usage of this code.]
Reviewed-by: Peter Maydell <peter.maydell@linaro.org>
Signed-off-by: Peter Maydell <peter.maydell@linaro.org>
362 lines
11 KiB
C
362 lines
11 KiB
C
/*
|
|
* 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/osdep.h"
|
|
#include "qemu/error-report.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[];
|
|
};
|
|
|
|
static void soc_dma_ch_schedule(struct soc_dma_ch_s *ch, int delay_bytes)
|
|
{
|
|
int64_t now = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL);
|
|
struct dma_s *dma = (struct dma_s *) ch->dma;
|
|
|
|
timer_mod(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 |= (uint64_t)1 << ch->num;
|
|
else
|
|
dma->ch_enable_mask &= ~((uint64_t)1 << ch->num);
|
|
|
|
if (level != ch->enable) {
|
|
soc_dma_ch_freq_update(dma);
|
|
ch->enable = level;
|
|
|
|
if (!ch->enable)
|
|
timer_del(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 = timer_new_ns(QEMU_CLOCK_VIRTUAL, 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) {
|
|
error_report("%s: FIFO at %"PRIx64
|
|
" collides with RAM region at %"PRIx64
|
|
"-%"PRIx64, __func__,
|
|
virt_base, entry->addr,
|
|
(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) {
|
|
error_report("%s: FIFO at %"PRIx64
|
|
" collides FIFO at %"PRIx64,
|
|
__func__, virt_base, 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)) {
|
|
error_report("%s: RAM at %"PRIx64 "-%"PRIx64
|
|
" collides with RAM region at %"PRIx64
|
|
"-%"PRIx64, __func__,
|
|
virt_base, virt_base + size,
|
|
entry->addr, 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) {
|
|
error_report("%s: RAM at %"PRIx64 "-%"PRIx64
|
|
" collides with FIFO at %"PRIx64,
|
|
__func__, virt_base, virt_base + size,
|
|
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 */
|