/*************************************************************************** * __________ __ ___. * Open \______ \ ____ ____ | | _\_ |__ _______ ___ * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ * \/ \/ \/ \/ \/ * $Id$ * * Copyright (C) 2014 by Cástor Muñoz * * 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 * of the License, or (at your option) any later version. * * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY * KIND, either express or implied. * ****************************************************************************/ #include "config.h" #include #include "system.h" #include "pl080.h" #include "panic.h" /* * ARM PrimeCell PL080 Multiple Master DMA controller */ /*#define PANIC_DEBUG*/ #ifdef PANIC_DEBUG void dmac_ch_panicf(const char *fn, struct dmac_ch* ch) { char *err = NULL; if (!ch) err = "NULL channel"; else if (!ch->dmac) err = "NULL ch->dmac"; else if (ch->dmac->ch_l[ch->prio] != ch) err = "not initialized channel"; if (err) panicf("%s(): <%d> %s", fn, ch ? (int)ch->prio : -1, err); } #define PANIC_DEBUG_CHANNEL(ch) dmac_ch_panicf(__func__,(ch)) #else #define PANIC_DEBUG_CHANNEL(ch) {} #endif /* task helpers */ static inline struct dmac_tsk *dmac_ch_tsk_by_idx( struct dmac_ch *ch, uint32_t idx) { return ch->tskbuf + (idx & ch->tskbuf_mask); } #define CH_TSK_TOP(ch) dmac_ch_tsk_by_idx((ch), (ch)->tasks_queued) #define CH_TSK_TAIL(ch) dmac_ch_tsk_by_idx((ch), (ch)->tasks_done) static inline bool dmac_ch_task_queue_empty(struct dmac_ch *ch) { return (ch->tasks_done == ch->tasks_queued); } /* enable/disable DMA controller */ static inline void dmac_hw_enable(struct dmac *dmac) { DMACCONFIG(dmac->baddr) |= DMACCONFIG_E_BIT; } static inline void dmac_hw_disable(struct dmac *dmac) { DMACCONFIG(dmac->baddr) &= ~DMACCONFIG_E_BIT; } /* enable/disable DMA channel */ static inline void dmac_ch_enable(struct dmac_ch *ch) { DMACCxCONFIG(ch->baddr) |= DMACCxCONFIG_E_BIT; } static inline void dmac_ch_disable(struct dmac_ch *ch) { uint32_t baddr = ch->baddr; /* Disable the channel, clears the FIFO after completing current AHB transfer */ DMACCxCONFIG(baddr) &= ~DMACCxCONFIG_E_BIT; /* Wait for it to go inactive */ while (DMACCxCONFIG(baddr) & DMACCxCONFIG_A_BIT); } #if 0 static void dmac_ch_halt(struct dmac_ch *ch) { uint32_t baddr = ch->baddr; /* Halt the channel, ignores subsequent DMA requests, the contents of the FIFO are drained */ DMACCxCONFIG(baddr) |= DMACCxCONFIG_H_BIT; /* Wait for it to go inactive */ while (DMACCxCONFIG(baddr) & DMACCxCONFIG_A_BIT); /* Disable channel and restore Halt bit */ DMACCxCONFIG(baddr) &= ~(DMACCxCONFIG_H_BIT | DMACCxCONFIG_E_BIT); } #endif /* launch next task in queue */ static void ICODE_ATTR dmac_ch_run(struct dmac_ch *ch) { struct dmac *dmac = ch->dmac; if (!dmac->ch_run_status) dmac_hw_enable(dmac); dmac->ch_run_status |= (1 << ch->prio); /* Clear any pending interrupts leftover from previous operation */ /*DMACINTTCCLR(dmac->baddr) = (1 << ch->prio);*/ /* not needed */ /* copy whole LLI to HW registers */ *DMACCxLLI(ch->baddr) = *(CH_TSK_TAIL(ch)->start_lli); dmac_ch_enable(ch); } static void ICODE_ATTR dmac_ch_abort(struct dmac_ch* ch) { struct dmac *dmac = ch->dmac; dmac_ch_disable(ch); /* Clear any pending interrupt */ DMACINTTCCLR(dmac->baddr) = (1 << ch->prio); dmac->ch_run_status &= ~(1 << ch->prio); if (!dmac->ch_run_status) dmac_hw_disable(dmac); } /* ISR */ static inline void dmac_ch_callback(struct dmac_ch *ch) { PANIC_DEBUG_CHANNEL(ch); /* backup current task cb_data */ void *cb_data = CH_TSK_TAIL(ch)->cb_data; /* mark current task as finished (resources can be reused) */ ch->tasks_done++; /* launch next DMA task */ if (ch->queue_mode == QUEUE_NORMAL) if (!dmac_ch_task_queue_empty(ch)) dmac_ch_run(ch); /* run user callback, new tasks could be launched/queued here */ if (ch->cb_fn) ch->cb_fn(cb_data); /* disable DMA channel if there are no running tasks */ if (dmac_ch_task_queue_empty(ch)) dmac_ch_abort(ch); } void ICODE_ATTR dmac_callback(struct dmac *dmac) { #ifdef PANIC_DEBUG if (!dmac) panicf("dmac_callback(): NULL dmac"); #endif unsigned int ch_n; uint32_t baddr = dmac->baddr; uint32_t intsts = DMACINTSTS(baddr); /* Lowest channel index is serviced first */ for (ch_n = 0; ch_n < DMAC_CH_COUNT; ch_n++) { if ((intsts & (1 << ch_n))) { if (DMACINTERRSTS(baddr) & (1 << ch_n)) panicf("DMA ch%d: HW error", ch_n); /* clear terminal count interrupt */ DMACINTTCCLR(baddr) = (1 << ch_n); dmac_ch_callback(dmac->ch_l[ch_n]); } } } /* * API */ void dmac_open(struct dmac *dmac) { uint32_t baddr = dmac->baddr; int ch_n; dmac_hw_enable(dmac); DMACCONFIG(baddr) = ((dmac->m1 & DMACCONFIG_M1_MSK) << DMACCONFIG_M1_POS) | ((dmac->m2 & DMACCONFIG_M2_MSK) << DMACCONFIG_M2_POS); for (ch_n = 0; ch_n < DMAC_CH_COUNT; ch_n++) { DMACCxCONFIG(DMAC_CH_BASE(baddr, ch_n)) = 0; /* disable channel */ dmac->ch_l[ch_n] = NULL; } dmac->ch_run_status = 0; /* clear channel interrupts */ DMACINTTCCLR(baddr) = 0xff; DMACINTERRCLR(baddr) = 0xff; dmac_hw_disable(dmac); } void dmac_ch_init(struct dmac_ch *ch, struct dmac_ch_cfg *cfg) { #ifdef PANIC_DEBUG if (!ch) panicf("%s(): NULL channel", __func__); else if (!ch->dmac) panicf("%s(): NULL ch->dmac", __func__); else if (ch->dmac->ch_l[ch->prio]) panicf("%s(): channel %d already initilized", __func__, ch->prio); uint32_t align_mask = (1 << MIN(cfg->swidth, cfg->dwidth)) - 1; if (ch->cfg->lli_xfer_max_count & align_mask) panicf("%s(): bad bus width: sw=%u dw=%u max_cnt=%u", __func__, cfg->swidth, cfg->dwidth, ch->cfg->lli_xfer_max_count); #endif struct dmac *dmac = ch->dmac; int ch_n = ch->prio; dmac->ch_l[ch_n] = ch; ch->baddr = DMAC_CH_BASE(dmac->baddr, ch_n); ch->llibuf_top = ch->llibuf; ch->tasks_queued = 0; ch->tasks_done = 0; ch->cfg = cfg; ch->control = ((cfg->sbsize & DMACCxCONTROL_SBSIZE_MSK) << DMACCxCONTROL_SBSIZE_POS) | ((cfg->dbsize & DMACCxCONTROL_DBSIZE_MSK) << DMACCxCONTROL_DBSIZE_POS) | ((cfg->swidth & DMACCxCONTROL_SWIDTH_MSK) << DMACCxCONTROL_SWIDTH_POS) | ((cfg->dwidth & DMACCxCONTROL_DWIDTH_MSK) << DMACCxCONTROL_DWIDTH_POS) | ((cfg->sbus & DMACCxCONTROL_S_MSK) << DMACCxCONTROL_S_POS) | ((cfg->dbus & DMACCxCONTROL_D_MSK) << DMACCxCONTROL_D_POS) | ((cfg->sinc & DMACCxCONTROL_SI_MSK) << DMACCxCONTROL_SI_POS) | ((cfg->dinc & DMACCxCONTROL_DI_MSK) << DMACCxCONTROL_DI_POS) | ((cfg->prot & DMACCxCONTROL_PROT_MSK) << DMACCxCONTROL_PROT_POS); /* flow control notes: * - currently only master modes are supported (FLOWCNTRL_x_DMA). * - must use DMAC_PERI_NONE when srcperi and/or dstperi are memory. */ uint32_t flowcntrl = (((cfg->srcperi != DMAC_PERI_NONE) << 1) | (cfg->dstperi != DMAC_PERI_NONE)) << DMACCxCONFIG_FLOWCNTRL_POS; DMACCxCONFIG(ch->baddr) = ((cfg->srcperi & DMACCxCONFIG_SRCPERI_MSK) << DMACCxCONFIG_SRCPERI_POS) | ((cfg->dstperi & DMACCxCONFIG_DESTPERI_MSK) << DMACCxCONFIG_DESTPERI_POS) | flowcntrl | DMACCxCONFIG_IE_BIT | DMACCxCONFIG_ITC_BIT; } void dmac_ch_lock_int(struct dmac_ch *ch) { PANIC_DEBUG_CHANNEL(ch); int flags = disable_irq_save(); DMACCxCONFIG(ch->baddr) &= ~DMACCxCONFIG_ITC_BIT; restore_irq(flags); } void dmac_ch_unlock_int(struct dmac_ch *ch) { PANIC_DEBUG_CHANNEL(ch); int flags = disable_irq_save(); DMACCxCONFIG(ch->baddr) |= DMACCxCONFIG_ITC_BIT; restore_irq(flags); } /* 1D->2D DMA transfers: * * srcaddr: aaaaaaaaaaabbbbbbbbbbbccccccc * <- size -> * <- width -><- width -><- r -> * * dstaddr: aaaaaaaaaaa..... * dstaddr + stride: bbbbbbbbbbb..... * dstaddr + 2*stride: ccccccc......... * <- stride -> * <- width -> * * 1D->1D DMA transfers: * * If 'width'=='stride', uses 'lli_xfer_max_count' for LLI count. * * Queue modes: * * QUEUE_NORMAL: each task in the queue is launched using a new * DMA transfer once previous task is finished. * * QUEUE_LINK: when a task is queued, it is linked with the last * queued task, creating a single continuous DMA transfer. New * tasks must be queued while the channel is running, otherwise * the continuous DMA transfer will be broken. * * Misc notes: * * Arguments 'size', 'width' and 'stride' are in bytes. * * Maximum supported 'width' depends on bus 'swidth' size, it is: * maximum width = DMAC_LLI_MAX_COUNT << swidth * * User must supply 'srcaddr', 'dstaddr', 'width', 'size', 'stride' * and 'lli_xfer_max_count' aligned to configured source and * destination bus widths, otherwise transfers will be internally * aligned by DMA hardware. */ #define LLI_COUNT(lli) ((lli)->control & DMACCxCONTROL_COUNT_MSK) #define LNK2LLI(link) ((struct dmac_lli*) ((link) & ~3)) static inline void drain_write_buffer(void) { asm volatile ( "mcr p15, 0, %0, c7, c10, 4\n" : : "r"(0)); } static inline void clean_dcache_line(void volatile *addr) { asm volatile ( "mcr p15, 0, %0, c7, c10, 1\n" /* clean d-cache line by MVA */ : : "r"((uint32_t)addr & ~(CACHEALIGN_SIZE - 1))); } void ICODE_ATTR dmac_ch_queue_2d( struct dmac_ch *ch, void *srcaddr, void *dstaddr, size_t size, size_t width, size_t stride, void *cb_data) { #ifdef PANIC_DEBUG PANIC_DEBUG_CHANNEL(ch); uint32_t align = (1 << MIN(ch->cfg->swidth, ch->cfg->dwidth)) - 1; if (((uint32_t)srcaddr | (uint32_t)dstaddr | size | width | stride) & align) panicf("dmac_ch_queue_2d(): %d,%p,%p,%u,%u,%u: bad alignment?", ch->prio, srcaddr, dstaddr, size, width, stride); #endif struct dmac_tsk *tsk; unsigned int srcinc, dstinc; uint32_t control, llibuf_idx; struct dmac_lli volatile *lli, *next_lli; /* get and fill new task */ tsk = CH_TSK_TOP(ch); tsk->start_lli = ch->llibuf_top; tsk->size = size; tsk->cb_data = cb_data; /* use maximum LLI transfer count for 1D->1D transfers */ if (width == stride) width = stride = ch->cfg->lli_xfer_max_count << ch->cfg->swidth; srcinc = (ch->cfg->sinc) ? stride : 0; dstinc = (ch->cfg->dinc) ? width : 0; size >>= ch->cfg->swidth; width >>= ch->cfg->swidth; /* fill LLI circular buffer */ control = ch->control | width; lli = ch->llibuf_top; llibuf_idx = lli - ch->llibuf; while (1) { llibuf_idx = (llibuf_idx + 1) & ch->llibuf_mask; next_lli = ch->llibuf + llibuf_idx; lli->srcaddr = srcaddr; lli->dstaddr = dstaddr; if (size <= width) break; lli->link = (uint32_t)next_lli | ch->llibuf_bus; lli->control = control; srcaddr += srcinc; dstaddr += dstinc; size -= width; /* clean dcache after completing a line */ if (((uint32_t)next_lli & (CACHEALIGN_SIZE - 1)) == 0) clean_dcache_line(lli); lli = next_lli; } /* last lli, enable terminal count interrupt */ lli->link = 0; lli->control = ch->control | size | DMACCxCONTROL_I_BIT; clean_dcache_line(lli); drain_write_buffer(); tsk->end_lli = lli; /* previous code is not protected against IRQs, it is fine to enter the DMA interrupt handler while an application is queuing a task, but the aplication must be protected when doing concurrent queueing. */ int flags = disable_irq_save(); ch->llibuf_top = next_lli; /* queue new task, launch it if it is the only queued task */ if (ch->tasks_done == ch->tasks_queued++) { dmac_ch_run(ch); } else if (ch->queue_mode == QUEUE_LINK) { uint32_t baddr = ch->baddr; uint32_t link, hw_link; link = (uint32_t)tsk->start_lli | ch->llibuf_bus; hw_link = DMACCxLINK(baddr); /* if it is a direct HW link, do it ASAP */ if (!hw_link) { DMACCxLINK(baddr) = link; /* check if the link was successful */ link = DMACCxLINK(baddr); /* dummy read for delay */ if (!(DMACCxCONFIG(baddr) & DMACCxCONFIG_E_BIT)) panicf("DMA ch%d: link error", ch->prio); } /* Locate the LLI where the new task must be linked. Link it even if it was a direct HW link, dmac_ch_get_info() needs it. */ lli = dmac_ch_tsk_by_idx(ch, ch->tasks_queued-2)->end_lli; lli->link = link; clean_dcache_line(lli); drain_write_buffer(); /* If the updated LLI was loaded by the HW while it was being modified, verify that the HW link is correct. */ if (LNK2LLI(hw_link) == lli) { uint32_t cur_hw_link = DMACCxLINK(baddr); if ((cur_hw_link != hw_link) && (cur_hw_link != link)) DMACCxLINK(baddr) = link; } } restore_irq(flags); } void dmac_ch_stop(struct dmac_ch* ch) { PANIC_DEBUG_CHANNEL(ch); int flags = disable_irq_save(); dmac_ch_abort(ch); ch->tasks_done = ch->tasks_queued; /* clear queue */ restore_irq(flags); } bool dmac_ch_running(struct dmac_ch *ch) { PANIC_DEBUG_CHANNEL(ch); int flags = disable_irq_save(); bool running = !dmac_ch_task_queue_empty(ch); restore_irq(flags); return running; } /* returns source or destination address of the actual LLI transfer, remaining bytes for current task, and total remaining bytes */ void *dmac_ch_get_info(struct dmac_ch *ch, size_t *bytes, size_t *t_bytes) { PANIC_DEBUG_CHANNEL(ch); void *cur_addr = NULL; size_t count = 0, t_count = 0; int flags = disable_irq_save(); if (!dmac_ch_task_queue_empty(ch)) { struct dmac_lli volatile *cur_lli; struct dmac_tsk *tsk; uint32_t cur_task; /* index */ uint32_t baddr = ch->baddr; /* Read DMA transfer progress: * * The recommended procedure (stop channel -> read progress -> * relaunch channel) is problematic for real time transfers, * specially when fast sample rates are combined with small * pheripheral FIFOs. * * An experimental method is used, it is based on the results * observed when reading the LLI registers at the instant they * are being updated by the HW (using s5l8702, 2:1 CPU/AHB * clock ratio): * - SRCADDR may return erroneous/corrupted data * - LINK and COUNT always returns valid data * - it seems that HW internally updates LINK and COUNT * 'atomically', this means that reading twice using the * sequence LINK1->COUNT1->LINK2->COUNT2: * if LINK1 == LINK2 then COUNT1 is consistent with LINK * if LINK1 <> LINK2 then COUNT2 is consistent with LINK2 */ uint32_t link, link2, control, control2; /* HW read sequence */ link = DMACCxLINK(baddr); control = DMACCxCONTROL(baddr); link2 = DMACCxLINK(baddr); control2 = DMACCxCONTROL(baddr); if (link != link2) { link = link2; control = control2; } count = control & DMACCxCONTROL_COUNT_MSK; /* HW count */ cur_task = ch->tasks_done; /* In QUEUE_LINK mode, when the task has just finished and is * waiting to enter the interrupt handler, the readed HW data * may correspont to the next linked task. Check it and update * real cur_task accordly. */ struct dmac_lli *next_start_lli = LNK2LLI( dmac_ch_tsk_by_idx(ch, cur_task)->end_lli->link); if (next_start_lli && (next_start_lli->link == link)) cur_task++; tsk = dmac_ch_tsk_by_idx(ch, cur_task); /* get previous to next LLI in the circular buffer */ cur_lli = (link) ? ch->llibuf + (ch->llibuf_mask & (LNK2LLI(link) - ch->llibuf - 1)) : tsk->end_lli; /* Calculate current address, choose destination address when * dest increment is set (usually MEMMEM or PERIMEM transfers), * otherwise use source address (usually MEMPERI transfers). */ void *start_addr; if (ch->control & (1 << DMACCxCONTROL_DI_POS)) { cur_addr = cur_lli->dstaddr; start_addr = tsk->start_lli->dstaddr; } else { cur_addr = cur_lli->srcaddr; start_addr = tsk->start_lli->srcaddr; } cur_addr += (LLI_COUNT(cur_lli) - count) << ch->cfg->swidth; /* calculate bytes for current task */ count = tsk->size - (cur_addr - start_addr); /* count bytes for the remaining tasks */ if (t_bytes) while (++cur_task != ch->tasks_queued) t_count += dmac_ch_tsk_by_idx(ch, cur_task)->size; } restore_irq(flags); if (bytes) *bytes = count; if (t_bytes) *t_bytes = count + t_count; return cur_addr; }