| /* |
| * Copyright 2019, Data61, CSIRO (ABN 41 687 119 230) |
| * |
| * SPDX-License-Identifier: BSD-2-Clause |
| */ |
| |
| #include <assert.h> |
| #include <stdbool.h> |
| |
| #include <platsupport/fdt.h> |
| |
| #include "irqchip.h" |
| |
| /* Force the _allocated_irqs section to be created even if no modules are defined. */ |
| static USED SECTION("_ps_irqchips") struct {} dummy_ps_irqchips; |
| /* Definitions so that we can find the exposed IRQ information */ |
| extern ps_irqchip_t *__start__ps_irqchips[]; |
| extern ps_irqchip_t *__stop__ps_irqchips[]; |
| |
| /* Private internal struct */ |
| struct ps_fdt_cookie { |
| int node_offset; |
| }; |
| |
| /* Used for the ps_fdt_index_* helper functions */ |
| |
| typedef struct index_helper_token { |
| ps_io_ops_t *io_ops; |
| unsigned desired_offset; |
| /* These are used for the 'register index' helper */ |
| void *mapped_addr; |
| pmem_region_t region; |
| /* These are used for the 'IRQ index' helper */ |
| irq_id_t irq_id; |
| irq_callback_fn_t irq_callback; |
| void *irq_callback_data; |
| } index_helper_token_t; |
| |
| int ps_fdt_read_path(ps_io_fdt_t *io_fdt, ps_malloc_ops_t *malloc_ops, const char *path, ps_fdt_cookie_t **ret_cookie) |
| { |
| if (!path || !ret_cookie) { |
| return -EINVAL; |
| } |
| |
| int error = ps_calloc(malloc_ops, 1, sizeof(**ret_cookie), (void **) ret_cookie); |
| if (error) { |
| return -ENOMEM; |
| } |
| |
| char *dtb_blob = ps_io_fdt_get(io_fdt); |
| if (!dtb_blob) { |
| ZF_LOGF_IF(ps_fdt_cleanup_cookie(malloc_ops, *ret_cookie), "Failed to cleanup FDT cookie"); |
| return -EINVAL; |
| } |
| |
| int node_offset = fdt_path_offset(dtb_blob, path); |
| if (node_offset < 0) { |
| ZF_LOGF_IF(ps_fdt_cleanup_cookie(malloc_ops, *ret_cookie), "Failed to cleanup FDT cookie"); |
| return node_offset; |
| } |
| |
| (*ret_cookie)->node_offset = node_offset; |
| |
| return 0; |
| } |
| |
| int ps_fdt_cleanup_cookie(ps_malloc_ops_t *malloc_ops, ps_fdt_cookie_t *cookie) |
| { |
| if (!malloc_ops || !cookie) { |
| return -EINVAL; |
| } |
| |
| return ps_free(malloc_ops, sizeof(*cookie), cookie); |
| } |
| |
| int ps_fdt_walk_registers(ps_io_fdt_t *io_fdt, ps_fdt_cookie_t *cookie, reg_walk_cb_fn_t callback, void *token) |
| { |
| if (!io_fdt || !callback || !cookie) { |
| return -EINVAL; |
| } |
| |
| char *dtb_blob = ps_io_fdt_get(io_fdt); |
| if (!dtb_blob) { |
| return -EINVAL; |
| } |
| |
| int node_offset = cookie->node_offset; |
| |
| /* NOTE: apparently fdt_parent_offset is expensive to use, |
| * maybe manipulate the path to get the parent's node path instead? */ |
| int parent_offset = fdt_parent_offset(dtb_blob, node_offset); |
| if (parent_offset < 0) { |
| return parent_offset; |
| } |
| |
| /* get the number of address and size cells */ |
| int num_address_cells = fdt_address_cells(dtb_blob, parent_offset); |
| if (num_address_cells < 0) { |
| return num_address_cells; |
| } |
| int num_size_cells = fdt_size_cells(dtb_blob, parent_offset); |
| if (num_size_cells < 0) { |
| return num_size_cells; |
| } |
| |
| if (num_address_cells == 0 || num_size_cells == 0) { |
| /* this isn't really a standard device, so we just return */ |
| return -FDT_ERR_NOTFOUND; |
| } |
| |
| int prop_len = 0; |
| const void *reg_prop = fdt_getprop(dtb_blob, node_offset, "reg", &prop_len); |
| if (!reg_prop) { |
| /* The error is written to the variable passed in */ |
| return prop_len; |
| } |
| int total_cells = prop_len / sizeof(uint32_t); |
| |
| /* sanity check */ |
| assert(total_cells % (num_address_cells + num_size_cells) == 0); |
| |
| int stride = num_address_cells + num_size_cells; |
| int num_regs = total_cells / stride; |
| |
| for (int i = 0; i < num_regs; i++) { |
| pmem_region_t curr_pmem = {0}; |
| const void *curr = reg_prop + (i * stride * sizeof(uint32_t)); |
| curr_pmem.type = PMEM_TYPE_DEVICE; |
| curr_pmem.base_addr = READ_CELL(num_address_cells, curr, 0); |
| curr_pmem.length = READ_CELL(num_size_cells, curr, num_address_cells); |
| int error = callback(curr_pmem, i, num_regs, token); |
| if (error) { |
| return error; |
| } |
| } |
| |
| return 0; |
| } |
| |
| static inline ps_irqchip_t **find_compatible_irq_controller(char *dtb_blob, int intr_controller_offset) |
| { |
| for (ps_irqchip_t **irqchip = __start__ps_irqchips; irqchip < __stop__ps_irqchips; irqchip++) { |
| for (char **compatible_str = (*irqchip)->compatible_list; *compatible_str != NULL; compatible_str++) { |
| if (fdt_node_check_compatible(dtb_blob, intr_controller_offset, *compatible_str) == 0) { |
| return irqchip; |
| } |
| } |
| } |
| |
| return NULL; |
| } |
| |
| int ps_fdt_walk_irqs(ps_io_fdt_t *io_fdt, ps_fdt_cookie_t *cookie, irq_walk_cb_fn_t callback, void *token) |
| { |
| if (!io_fdt || !callback || !cookie) { |
| return -EINVAL; |
| } |
| |
| char *dtb_blob = ps_io_fdt_get(io_fdt); |
| if (!dtb_blob) { |
| return -EINVAL; |
| } |
| |
| int node_offset = cookie->node_offset; |
| |
| /* check that this node actually has interrupts */ |
| const void *intr_addr = fdt_getprop(dtb_blob, node_offset, "interrupts", NULL); |
| if (!intr_addr) { |
| intr_addr = fdt_getprop(dtb_blob, node_offset, "interrupts-extended", NULL); |
| if (!intr_addr) { |
| return -FDT_ERR_NOTFOUND; |
| } |
| } |
| |
| /* get the interrupt controller of the node */ |
| int curr_offset = node_offset; |
| bool found_controller = false; |
| const void *intr_parent_prop; |
| while (curr_offset >= 0 && !found_controller) { |
| intr_parent_prop = fdt_getprop(dtb_blob, curr_offset, "interrupt-parent", NULL); |
| if (!intr_parent_prop) { |
| /* move up a level |
| * NOTE: fdt_parent_offset is apparently expensive to use, |
| * maybe avoid using fdt_parent_offset and manipulate the path instead? */ |
| curr_offset = fdt_parent_offset(dtb_blob, curr_offset); |
| } else { |
| found_controller = true; |
| } |
| } |
| |
| if (!found_controller) { |
| /* something is really wrong with the FDT */ |
| return -FDT_ERR_BADSTRUCTURE; |
| } |
| |
| /* check if this controller is just a interrupt forwarder/not the root interrupt controller, |
| * and if it is, find the root controller |
| */ |
| bool is_root_controller = false; |
| int root_intr_controller_offset = 0; |
| uint32_t root_intr_controller_phandle = 0; |
| while (!is_root_controller) { |
| uint32_t intr_controller_phandle = READ_CELL(1, intr_parent_prop, 0); |
| ZF_LOGF_IF(intr_controller_phandle == 0, |
| "Failed to get the phandle of the interrupt controller of this node"); |
| int intr_controller_offset = fdt_node_offset_by_phandle(dtb_blob, intr_controller_phandle); |
| ZF_LOGF_IF(intr_controller_offset < 0, "Failed to get the offset of the interrupt controller"); |
| intr_parent_prop = fdt_getprop(dtb_blob, intr_controller_offset, "interrupt-parent", NULL); |
| |
| /* The root interrupt controller node has one of two characteristics: |
| * 1. It has a 'interrupt-parent' property that points back to itself |
| * 2. It has no 'interrupt-parent' property |
| */ |
| if (intr_parent_prop && intr_controller_phandle == READ_CELL(1, intr_parent_prop, 0)) { |
| is_root_controller = true; |
| } else if (!intr_parent_prop) { |
| is_root_controller = true; |
| } |
| |
| if (is_root_controller) { |
| root_intr_controller_offset = intr_controller_offset; |
| root_intr_controller_phandle = intr_controller_phandle; |
| } |
| } |
| |
| /* check the compatible string against our list of support interrupt controllers */ |
| ps_irqchip_t **irqchip = find_compatible_irq_controller(dtb_blob, root_intr_controller_offset); |
| if (irqchip == NULL) { |
| ZF_LOGE("Could not find a parser for this particular interrupt controller"); |
| return -ENOENT; |
| } |
| |
| /* delegate to the interrupt controller specific code */ |
| int error = (*irqchip)->parser_fn(dtb_blob, node_offset, root_intr_controller_phandle, callback, token); |
| if (error) { |
| ZF_LOGE("Failed to parse and walk the interrupt field"); |
| return error; |
| } |
| |
| return 0; |
| } |
| |
| static int register_index_helper_walker(pmem_region_t pmem, unsigned curr_num, size_t num_regs, void *token) |
| { |
| index_helper_token_t *helper_token = token; |
| if (helper_token->desired_offset >= num_regs) { |
| /* Bail early if we will never find it */ |
| return -ENOENT; |
| } |
| |
| if (helper_token->desired_offset == curr_num) { |
| void *ret_addr = ps_pmem_map(helper_token->io_ops, pmem, false, PS_MEM_NORMAL); |
| if (ret_addr == NULL) { |
| return -ENOMEM; |
| } else { |
| helper_token->mapped_addr = ret_addr; |
| helper_token->region = pmem; |
| } |
| } |
| |
| return 0; |
| } |
| |
| void *ps_fdt_index_map_register(ps_io_ops_t *io_ops, ps_fdt_cookie_t *cookie, unsigned offset, |
| pmem_region_t *ret_pmem) |
| { |
| if (io_ops == NULL) { |
| ZF_LOGE("io_ops is NULL!"); |
| return NULL; |
| } |
| |
| if (cookie == NULL) { |
| ZF_LOGE("cookie is NULL"); |
| return NULL; |
| } |
| |
| index_helper_token_t token = { .io_ops = io_ops, .desired_offset = offset }; |
| |
| int error = ps_fdt_walk_registers(&io_ops->io_fdt, cookie, register_index_helper_walker, |
| &token); |
| if (error) { |
| return NULL; |
| } |
| |
| if (ret_pmem) { |
| *ret_pmem = token.region; |
| } |
| |
| return token.mapped_addr; |
| } |
| |
| static int irq_index_helper_walker(ps_irq_t irq, unsigned curr_num, size_t num_irqs, void *token) |
| { |
| index_helper_token_t *helper_token = token; |
| if (helper_token->desired_offset >= num_irqs) { |
| /* Bail early if we will never find it */ |
| return -ENOENT; |
| } |
| |
| if (helper_token->desired_offset == curr_num) { |
| irq_id_t registered_id = ps_irq_register(&helper_token->io_ops->irq_ops, |
| irq, helper_token->irq_callback, |
| helper_token->irq_callback_data); |
| if (registered_id >= 0) { |
| helper_token->irq_id = registered_id; |
| } else { |
| /* Bail on error */ |
| return registered_id; |
| } |
| } |
| |
| return 0; |
| } |
| |
| irq_id_t ps_fdt_index_register_irq(ps_io_ops_t *io_ops, ps_fdt_cookie_t *cookie, unsigned offset, |
| irq_callback_fn_t irq_callback, void *irq_callback_data) |
| { |
| if (io_ops == NULL) { |
| ZF_LOGE("io_ops is NULL!"); |
| return -EINVAL; |
| } |
| |
| if (cookie == NULL) { |
| ZF_LOGE("cookie is NULL"); |
| return -EINVAL; |
| } |
| |
| index_helper_token_t token = { .io_ops = io_ops, .desired_offset = offset, |
| .irq_callback = irq_callback, |
| .irq_callback_data = irq_callback_data |
| }; |
| |
| int error = ps_fdt_walk_irqs(&io_ops->io_fdt, cookie, irq_index_helper_walker, &token); |
| if (error) { |
| assert(error <= PS_INVALID_IRQ_ID); |
| return error; |
| } |
| |
| return token.irq_id; |
| } |