| // Copyright 2017 The Fuchsia Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| #include <assert.h> |
| #include <stdint.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| |
| #include <zircon/compiler.h> |
| #include <zircon/assert.h> |
| #include <zircon/syscalls.h> |
| #include <zircon/threads.h> |
| |
| #include <ddk/binding.h> |
| #include <ddk/device.h> |
| #include <ddk/driver.h> |
| |
| #include <bcm/bcm28xx.h> |
| #include <bcm/dma.h> |
| |
| // clang-format off |
| #if TRACE |
| #define xprintf(fmt...) printf("BCMDMA: "fmt) |
| #else |
| #define xprintf(fmt...) \ |
| do { \ |
| } while (0) |
| #endif |
| |
| #define BCM_DMA_PAGE_SIZE (4096) |
| #define BCM_DMA_NUM_CONTROL_BLOCKS (64) |
| #define BCM_DMA_MAX_CH (14) |
| // clang-format on |
| |
| static bcm_dma_ctrl_regs_t* dma_regs; |
| |
| static int dma_irq_thread(void* arg) { |
| bcm_dma_t* dma = arg; |
| zx_handle_t irq_handle = dma->irq_handle; |
| xprintf("dma interrupt thread started\n"); |
| |
| zx_status_t stat; |
| |
| while (!dma->irq_thrd_stop) { |
| |
| zx_interrupt_complete(irq_handle); |
| stat = zx_interrupt_wait(irq_handle); |
| ZX_DEBUG_ASSERT(stat == ZX_OK); |
| |
| dma_regs->channels[dma->ch_num].cs |= BCM_DMA_CS_INT; |
| if (stat != ZX_OK) { |
| xprintf("dma interrupt wait failed = %d\n", stat); |
| break; |
| } |
| if (dma->irq_thrd_stop) |
| break; |
| if (dma->callback) |
| (dma->callback)(dma); |
| } |
| |
| dma->irq_thrd_stop = false; |
| xprintf("dma interrupt thread quitting\n"); |
| return 0; |
| } |
| |
| zx_status_t bcm_dma_init(bcm_dma_t* dma, uint32_t ch) { |
| |
| xprintf("Initializing dma channel %u\n", ch); |
| zx_status_t status; |
| zx_handle_t irq_handle = ZX_HANDLE_INVALID; |
| |
| mtx_lock(&dma->dma_lock); |
| |
| if (dma->state > BCM_DMA_STATE_SHUTDOWN) { |
| mtx_unlock(&dma->dma_lock); |
| return ZX_ERR_BAD_STATE; |
| } |
| |
| static_assert(BCM_DMA_MAX_CH < countof(dma_regs->channels), |
| "DMA channel out of range"); |
| if (ch > BCM_DMA_MAX_CH) { // Don't use ch 15 as it has different properties |
| mtx_unlock(&dma->dma_lock); |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| if (dma_regs == NULL) { |
| status = io_buffer_init_physical(&dma->regs_buffer, DMA_BASE, BCM_DMA_PAGE_SIZE, |
| get_root_resource(), ZX_CACHE_POLICY_UNCACHED_DEVICE); |
| if (status != ZX_OK) { |
| goto dma_init_err; |
| } |
| dma_regs = io_buffer_virt(&dma->regs_buffer); |
| } |
| |
| xprintf("Initializing control block buffers\n"); |
| // pre-init the control block buffer |
| status = io_buffer_init(&dma->ctl_blks, |
| BCM_DMA_NUM_CONTROL_BLOCKS * sizeof(bcm_dma_cb_t), |
| IO_BUFFER_RW); |
| if (status != ZX_OK) { |
| xprintf("\nBCM_DMA: Error Allocating control blocks: %d\n", status); |
| goto dma_init_err; |
| } |
| |
| dma->mem_idx = NULL; |
| dma->ch_num = ch; |
| dma->callback = NULL; |
| |
| xprintf("Initializing interrupt handler\n"); |
| status = zx_interrupt_create(get_root_resource(), |
| INTERRUPT_DMA0 + ch, |
| ZX_FLAG_REMAP_IRQ, |
| &irq_handle); |
| if (status != ZX_OK) { |
| xprintf("bcm-dma: failed to create interrupt handle, handle = %d\n", |
| status); |
| goto dma_init_err; |
| } |
| |
| dma->irq_handle = irq_handle; |
| |
| dma_regs->channels[dma->ch_num].cs = BCM_DMA_CS_RESET; //reset the channel |
| |
| // Create a thread to handle IRQs. |
| xprintf("Creating interrupt thread\n"); |
| char thrd_name[20]; |
| snprintf(thrd_name, sizeof(thrd_name), "dma%02u_irq_thrd", ch); |
| int thrd_rc = thrd_create_with_name(&dma->irq_thrd, dma_irq_thread, dma, |
| thrd_name); |
| if (thrd_rc != thrd_success) { |
| xprintf("failed to create irq thread\n"); |
| status = thrd_status_to_zx_status(thrd_rc); |
| goto dma_init_err; |
| } |
| |
| dma->state = BCM_DMA_STATE_INITIALIZED; |
| |
| mtx_unlock(&dma->dma_lock); |
| return ZX_OK; |
| |
| dma_init_err: |
| zx_handle_close(dma->irq_handle); |
| dma->irq_handle = ZX_HANDLE_INVALID; |
| |
| if (io_buffer_is_valid(&dma->ctl_blks)) { |
| io_buffer_release(&dma->ctl_blks); |
| } |
| if (irq_handle > 0) { |
| zx_handle_close(irq_handle); |
| } |
| |
| mtx_unlock(&dma->dma_lock); |
| return status; |
| } |
| |
| zx_paddr_t bcm_dma_get_position(bcm_dma_t* dma) { |
| /* .source_address reports the physical bus address of the memory location, |
| which doesn't neccesarily equal the physical memory address as observed |
| by the ARM cores (depending on L2 configuration). The base address |
| of the bus mapping is BCM_SDRAM_BUS_ADDR_BASE (0xc0000000). We use |
| BCM_PHYS_ADDR_MASK to make to the phys address needed by the cpu. |
| */ |
| uint32_t address = (dma_regs->channels[dma->ch_num].source_addr) & BCM_PHYS_ADDR_MASK; |
| return (zx_paddr_t)address; |
| } |
| |
| zx_status_t bcm_dma_paddr_to_offset(bcm_dma_t* dma, zx_paddr_t paddr, uint32_t* offset) { |
| |
| // This call only works if an index was created for the memory object |
| if (!dma->mem_idx) { |
| return ZX_ERR_BAD_STATE; |
| } |
| |
| for (uint32_t i = 0; i < dma->mem_idx_len; i++) { |
| if ((paddr >= dma->mem_idx[i].paddr) && (paddr < (dma->mem_idx[i].paddr + dma->mem_idx[i].len))) { |
| *offset = dma->mem_idx[i].offset + (paddr - dma->mem_idx[i].paddr); |
| return ZX_OK; |
| } |
| } |
| return ZX_ERR_OUT_OF_RANGE; |
| } |
| |
| /* Builds index of vmo pages. This is used to translate physical addresses |
| reported by the dma status into offsets into the memory object used for the |
| transaction. |
| */ |
| static zx_status_t bcm_dma_build_mem_index(bcm_dma_t* dma, zx_paddr_t* page_list, uint32_t len) { |
| |
| dma->mem_idx = calloc(len, sizeof(bcm_dma_vmo_index_t)); //Allocate worst case sized array |
| ZX_DEBUG_ASSERT(dma->mem_idx); |
| if (!dma->mem_idx) |
| return ZX_ERR_NO_MEMORY; |
| |
| dma->mem_idx_len = 0; |
| |
| for (uint32_t i = 0; i < len; i++) { |
| uint32_t j; |
| |
| for (j = 0; ((page_list[i] > dma->mem_idx[j].paddr) && (j < dma->mem_idx_len)); j++) |
| ; |
| |
| if ((j != 0) && |
| ((i * BCM_DMA_PAGE_SIZE) == (dma->mem_idx[j - 1].offset + dma->mem_idx[j - 1].len)) && |
| ((page_list[i] == (dma->mem_idx[j - 1].paddr + dma->mem_idx[j - 1].len)))) { |
| |
| dma->mem_idx[j - 1].len += BCM_DMA_PAGE_SIZE; |
| |
| } else { |
| |
| if (j < dma->mem_idx_len) { |
| memmove(&dma->mem_idx[j + 1], &dma->mem_idx[j], |
| (dma->mem_idx_len - j)*sizeof(bcm_dma_vmo_index_t)); |
| } |
| dma->mem_idx[j].paddr = page_list[i]; |
| dma->mem_idx[j].offset = i * BCM_DMA_PAGE_SIZE; |
| dma->mem_idx[j].len = BCM_DMA_PAGE_SIZE; |
| dma->mem_idx_len++; |
| } |
| } |
| return ZX_OK; |
| } |
| |
| zx_status_t bcm_dma_init_vmo_to_fifo_trans(bcm_dma_t* dma, zx_handle_t vmo, uint32_t t_info, |
| zx_paddr_t dest, uint32_t flags) { |
| zx_paddr_t* buf_pages = NULL; |
| xprintf("Linking vmo to fifo...\n"); |
| mtx_lock(&dma->dma_lock); |
| |
| if (dma->state < BCM_DMA_STATE_INITIALIZED) { |
| mtx_unlock(&dma->dma_lock); |
| return ZX_ERR_BAD_STATE; |
| } |
| |
| size_t buffsize; |
| zx_status_t status = zx_vmo_get_size(vmo, &buffsize); |
| if (status != ZX_OK) { |
| goto dma_link_err; |
| } |
| |
| uint32_t num_pages = (buffsize + BCM_DMA_PAGE_SIZE - 1) / BCM_DMA_PAGE_SIZE; |
| |
| ZX_DEBUG_ASSERT(num_pages <= BCM_DMA_NUM_CONTROL_BLOCKS); |
| if (num_pages > BCM_DMA_NUM_CONTROL_BLOCKS) { |
| status = ZX_ERR_NO_MEMORY; |
| goto dma_link_err; |
| } |
| |
| buf_pages = calloc(num_pages, sizeof(zx_paddr_t)); |
| if (!buf_pages) { |
| status = ZX_ERR_NO_MEMORY; |
| goto dma_link_err; |
| } |
| |
| status = zx_vmo_op_range(vmo, ZX_VMO_OP_LOOKUP, 0, buffsize, |
| buf_pages, sizeof(zx_paddr_t) * num_pages); |
| if (status != ZX_OK) |
| goto dma_link_err; |
| |
| if (flags & BCM_DMA_FLAGS_USE_MEM_INDEX) { |
| status = bcm_dma_build_mem_index(dma, buf_pages, num_pages); |
| if (status != ZX_OK) |
| goto dma_link_err; |
| } |
| |
| ssize_t total_bytes = buffsize; |
| |
| // Create the control blocks for this vmo. control block iobuffer was inited when |
| // dma object was inited. Currently creates one control block for each page of |
| // memory in the memory object. |
| |
| bcm_dma_cb_t* cb = (bcm_dma_cb_t*)io_buffer_virt(&dma->ctl_blks); |
| // bus address of the control block buffer |
| uint32_t cb_bus_addr = (uint32_t)io_buffer_phys(&dma->ctl_blks) | BCM_SDRAM_BUS_ADDR_BASE; |
| |
| for (uint32_t i = 0; i < num_pages; i++) { |
| |
| cb[i].transfer_info = t_info; |
| |
| cb[i].source_addr = (uint32_t)(buf_pages[i] | BCM_SDRAM_BUS_ADDR_BASE); |
| cb[i].dest_addr = (uint32_t)dest; |
| |
| uint32_t tfer_len = (total_bytes > BCM_DMA_PAGE_SIZE) ? BCM_DMA_PAGE_SIZE : total_bytes; |
| cb[i].transfer_len = tfer_len; |
| total_bytes -= tfer_len; |
| |
| if ((total_bytes > 0) && (i < (num_pages - 1))) { |
| cb[i].next_ctl_blk_addr = cb_bus_addr + (sizeof(bcm_dma_cb_t) * (i + 1)); |
| } else { |
| cb[i].next_ctl_blk_addr = (flags & BCM_DMA_FLAGS_CIRCULAR) ? cb_bus_addr : 0; |
| if (dma->callback) |
| cb[i].transfer_info |= BCM_DMA_TI_INTEN; |
| } |
| } |
| |
| io_buffer_cache_op(&dma->ctl_blks, ZX_VMO_OP_CACHE_CLEAN, 0, num_pages * sizeof(bcm_dma_cb_t)); |
| |
| dma->state = BCM_DMA_STATE_READY; |
| |
| goto dma_link_ret; |
| |
| dma_link_err: |
| if (dma->mem_idx) { |
| free(dma->mem_idx); |
| dma->mem_idx_len = 0; |
| } |
| dma->mem_idx = NULL; |
| dma_link_ret: |
| if (buf_pages) |
| free(buf_pages); |
| mtx_unlock(&dma->dma_lock); |
| return status; |
| } |
| |
| zx_status_t bcm_dma_start(bcm_dma_t* dma) { |
| |
| xprintf("starting dma channel %u\n", dma->ch_num); |
| mtx_lock(&dma->dma_lock); |
| if ((dma_regs == NULL) || (dma->state != BCM_DMA_STATE_READY)) { |
| mtx_unlock(&dma->dma_lock); |
| return ZX_ERR_BAD_STATE; |
| } |
| |
| dma_regs->channels[dma->ch_num].ctl_blk_addr = |
| (uint32_t)(io_buffer_phys(&dma->ctl_blks) | BCM_SDRAM_BUS_ADDR_BASE); |
| |
| dma_regs->channels[dma->ch_num].cs |= (BCM_DMA_CS_ACTIVE | BCM_DMA_CS_WAIT); |
| |
| dma->state = BCM_DMA_STATE_RUNNING; |
| mtx_unlock(&dma->dma_lock); |
| return ZX_OK; |
| } |
| |
| zx_status_t bcm_dma_stop(bcm_dma_t* dma) { |
| xprintf("Stopping dma channel %u\n", dma->ch_num); |
| mtx_lock(&dma->dma_lock); |
| |
| if ((dma_regs == NULL) || (dma->state < BCM_DMA_STATE_READY)) { |
| mtx_unlock(&dma->dma_lock); |
| return ZX_ERR_BAD_STATE; |
| } |
| |
| dma_regs->channels[dma->ch_num].cs &= ~BCM_DMA_CS_ACTIVE; |
| dma->state = BCM_DMA_STATE_READY; |
| |
| mtx_unlock(&dma->dma_lock); |
| return ZX_OK; |
| } |
| |
| void bcm_dma_deinit(bcm_dma_t* dma) { |
| |
| ZX_DEBUG_ASSERT(dma); |
| xprintf("Deiniting dma channel %u\n", dma->ch_num); |
| |
| mtx_lock(&dma->dma_lock); |
| |
| if (dma->irq_handle != ZX_HANDLE_INVALID) { |
| //shut down the irq thread |
| xprintf("Shutting down irq thread\n"); |
| dma->irq_thrd_stop = true; |
| //Signal the interrupt since the thread is waiting on it. |
| zx_interrupt_signal(dma->irq_handle); |
| thrd_join(dma->irq_thrd, NULL); |
| xprintf("irq thread shut down\n"); |
| |
| //Release the irq handle |
| zx_handle_close(dma->irq_handle); |
| dma->irq_handle = ZX_HANDLE_INVALID; |
| } |
| |
| dma_regs->channels[dma->ch_num].cs &= ~BCM_DMA_CS_ACTIVE; |
| |
| //Reset the hardware |
| dma_regs->channels[dma->ch_num].cs = BCM_DMA_CS_RESET; |
| dma_regs->channels[dma->ch_num].ctl_blk_addr = (uint32_t)(0); |
| |
| //Release whatever memory we are sitting on |
| if (dma->mem_idx) { |
| free(dma->mem_idx); |
| dma->mem_idx = NULL; |
| } |
| dma->mem_idx_len = 0; |
| |
| if (io_buffer_is_valid(&dma->ctl_blks)) { |
| io_buffer_release(&dma->ctl_blks); |
| } |
| |
| dma->state = BCM_DMA_STATE_SHUTDOWN; |
| |
| mtx_unlock(&dma->dma_lock); |
| |
| } |