367 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			C
		
	
	
	
			
		
		
	
	
			367 lines
		
	
	
		
			12 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-common.h"
 | |
| #include "qemu/timer.h"
 | |
| #include "hw/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 */
 |