/* * GRUB -- GRand Unified Bootloader * Copyright (C) 2016 Free Software Foundation, Inc. * * GRUB 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 3 of the License, or * (at your option) any later version. * * GRUB 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 GRUB. If not, see . */ #include #include #include static const void *dtb; static grub_size_t root_address_cells, root_size_cells; /* Pointer to this symbol signals invalid mapping. */ char grub_fdtbus_invalid_mapping[1]; struct grub_fdtbus_dev { struct grub_fdtbus_dev *next; struct grub_fdtbus_dev *parent; int node; struct grub_fdtbus_driver *driver; }; struct grub_fdtbus_dev *devs; struct grub_fdtbus_driver *drivers; static int is_compatible (struct grub_fdtbus_driver *driver, int node) { grub_size_t compatible_size; const char *compatible = grub_fdt_get_prop (dtb, node, "compatible", &compatible_size); if (!compatible) return 0; const char *compatible_end = compatible + compatible_size; while (compatible < compatible_end) { if (grub_strcmp (driver->compatible, compatible) == 0) return 1; compatible += grub_strlen (compatible) + 1; } return 0; } static void fdtbus_scan (struct grub_fdtbus_dev *parent) { int node; for (node = grub_fdt_first_node (dtb, parent ? parent->node : 0); node >= 0; node = grub_fdt_next_node (dtb, node)) { struct grub_fdtbus_dev *dev; struct grub_fdtbus_driver *driver; dev = grub_zalloc (sizeof (*dev)); if (!dev) { grub_print_error (); return; } dev->node = node; dev->next = devs; dev->parent = parent; devs = dev; FOR_LIST_ELEMENTS(driver, drivers) if (!dev->driver && is_compatible (driver, node)) { if (driver->attach(dev) == GRUB_ERR_NONE) { dev->driver = driver; break; } grub_print_error (); } fdtbus_scan (dev); } } void grub_fdtbus_register (struct grub_fdtbus_driver *driver) { struct grub_fdtbus_dev *dev; grub_list_push (GRUB_AS_LIST_P (&drivers), GRUB_AS_LIST (driver)); for (dev = devs; dev; dev = dev->next) if (!dev->driver && is_compatible (driver, dev->node)) { if (driver->attach(dev) == GRUB_ERR_NONE) dev->driver = driver; grub_print_error (); } } void grub_fdtbus_unregister (struct grub_fdtbus_driver *driver) { grub_list_remove (GRUB_AS_LIST (driver)); struct grub_fdtbus_dev *dev; for (dev = devs; dev; dev = dev->next) if (dev->driver == driver) { if (driver->detach) driver->detach(dev); dev->driver = 0; } } void grub_fdtbus_init (const void *dtb_in, grub_size_t size) { if (!dtb_in || grub_fdt_check_header (dtb_in, size) < 0) grub_fatal ("invalid FDT"); dtb = dtb_in; const grub_uint32_t *prop = grub_fdt_get_prop (dtb, 0, "#address-cells", 0); if (prop) root_address_cells = grub_be_to_cpu32 (*prop); else root_address_cells = 1; prop = grub_fdt_get_prop (dtb, 0, "#size-cells", 0); if (prop) root_size_cells = grub_be_to_cpu32 (*prop); else root_size_cells = 1; fdtbus_scan (0); } static int get_address_cells (const struct grub_fdtbus_dev *dev) { const grub_uint32_t *prop; if (!dev) return root_address_cells; prop = grub_fdt_get_prop (dtb, dev->node, "#address-cells", 0); if (prop) return grub_be_to_cpu32 (*prop); return 1; } static int get_size_cells (const struct grub_fdtbus_dev *dev) { const grub_uint32_t *prop; if (!dev) return root_size_cells; prop = grub_fdt_get_prop (dtb, dev->node, "#size-cells", 0); if (prop) return grub_be_to_cpu32 (*prop); return 1; } static grub_uint64_t get64 (const grub_uint32_t *reg, grub_size_t cells) { grub_uint64_t val = 0; if (cells >= 1) val = grub_be_to_cpu32 (reg[cells - 1]); if (cells >= 2) val |= ((grub_uint64_t) grub_be_to_cpu32 (reg[cells - 2])) << 32; return val; } static volatile void * translate (const struct grub_fdtbus_dev *dev, const grub_uint32_t *reg) { volatile void *ret; const grub_uint32_t *ranges; grub_size_t ranges_size, cells_per_mapping; grub_size_t parent_address_cells, child_address_cells, child_size_cells; grub_size_t nmappings, i; if (dev == 0) { grub_uint64_t val; val = get64 (reg, root_address_cells); if (sizeof (void *) == 4 && (val >> 32)) return grub_fdtbus_invalid_mapping; return (void *) (grub_addr_t) val; } ranges = grub_fdt_get_prop (dtb, dev->node, "ranges", &ranges_size); if (!ranges) return grub_fdtbus_invalid_mapping; if (ranges_size == 0) return translate (dev->parent, reg); parent_address_cells = get_address_cells (dev->parent); child_address_cells = get_address_cells (dev); child_size_cells = get_size_cells (dev); cells_per_mapping = parent_address_cells + child_address_cells + child_size_cells; nmappings = ranges_size / 4 / cells_per_mapping; for (i = 0; i < nmappings; i++) { const grub_uint32_t *child_addr = &ranges[i * cells_per_mapping]; const grub_uint32_t *parent_addr = child_addr + child_address_cells; grub_uint64_t child_size = get64 (parent_addr + parent_address_cells, child_size_cells); if (child_address_cells > 2 && grub_memcmp (reg, child_addr, (child_address_cells - 2) * 4) != 0) continue; if (get64 (reg, child_address_cells) < get64 (child_addr, child_address_cells)) continue; grub_uint64_t offset = get64 (reg, child_address_cells) - get64 (child_addr, child_address_cells); if (offset >= child_size) continue; ret = translate (dev->parent, parent_addr); if (grub_fdtbus_is_mapping_valid (ret)) ret = (volatile char *) ret + offset; return ret; } return grub_fdtbus_invalid_mapping; } volatile void * grub_fdtbus_map_reg (const struct grub_fdtbus_dev *dev, int regno, grub_size_t *size) { grub_size_t address_cells, size_cells; address_cells = get_address_cells (dev->parent); size_cells = get_size_cells (dev->parent); const grub_uint32_t *reg = grub_fdt_get_prop (dtb, dev->node, "reg", 0); if (size && size_cells) *size = reg[(address_cells + size_cells) * regno + address_cells]; if (size && !size_cells) *size = 0; return translate (dev->parent, reg + (address_cells + size_cells) * regno); } const char * grub_fdtbus_get_name (const struct grub_fdtbus_dev *dev) { return grub_fdt_get_nodename (dtb, dev->node); } const void * grub_fdtbus_get_prop (const struct grub_fdtbus_dev *dev, const char *name, grub_uint32_t *len) { return grub_fdt_get_prop (dtb, dev->node, name, len); } const void * grub_fdtbus_get_fdt (void) { return dtb; }