opps, +Stanimir On Mon, Oct 5, 2015 at 11:44 AM, Rob Clark <robdclark@xxxxxxxxx> wrote: > The OCMEM driver handles allocation and configuration of the On Chip > MEMory that is present on some snapdragon devices. > > Devices which have OCMEM do not have GMEM inside the gpu core, so the > gpu must instead use OCMEM to be functional. Since currently the gpu > is the only OCMEM user with an upstream driver, this is just a minimal > implementation sufficient for statically allocating to the gpu it's > chunk of OCMEM. > > Signed-off-by: Rob Clark <robdclark@xxxxxxxxx> > --- > drivers/soc/qcom/Kconfig | 6 + > drivers/soc/qcom/Makefile | 1 + > drivers/soc/qcom/ocmem.c | 408 +++++++++++++++++++++++++++++++++++++++++++ > drivers/soc/qcom/ocmem.xml.h | 106 +++++++++++ > include/soc/qcom/ocmem.h | 40 +++++ > 5 files changed, 561 insertions(+) > create mode 100644 drivers/soc/qcom/ocmem.c > create mode 100644 drivers/soc/qcom/ocmem.xml.h > create mode 100644 include/soc/qcom/ocmem.h > > diff --git a/drivers/soc/qcom/Kconfig b/drivers/soc/qcom/Kconfig > index 266060b..b485043 100644 > --- a/drivers/soc/qcom/Kconfig > +++ b/drivers/soc/qcom/Kconfig > @@ -74,3 +74,9 @@ config QCOM_WCNSS_CTRL > help > Client driver for the WCNSS_CTRL SMD channel, used to download nv > firmware to a newly booted WCNSS chip. > + > +config QCOM_OCMEM > + tristate "Qualcomm OCMEM driver" > + depends on QCOM_SMD > + help > + Allocator for OCMEM (On Chip MEMory). > diff --git a/drivers/soc/qcom/Makefile b/drivers/soc/qcom/Makefile > index fdd664e..940bb35 100644 > --- a/drivers/soc/qcom/Makefile > +++ b/drivers/soc/qcom/Makefile > @@ -7,3 +7,4 @@ obj-$(CONFIG_QCOM_SMEM_STATE) += smem_state.o > obj-$(CONFIG_QCOM_SMP2P) += smp2p.o > obj-$(CONFIG_QCOM_SMSM) += smsm.o > obj-$(CONFIG_QCOM_WCNSS_CTRL) += wcnss_ctrl.o > +obj-$(CONFIG_QCOM_OCMEM) += ocmem.o > diff --git a/drivers/soc/qcom/ocmem.c b/drivers/soc/qcom/ocmem.c > new file mode 100644 > index 0000000..2628aaf > --- /dev/null > +++ b/drivers/soc/qcom/ocmem.c > @@ -0,0 +1,408 @@ > +/* > + * Copyright (C) 2015 Red Hat > + * Author: Rob Clark <robdclark@xxxxxxxxx> > + * > + * This program is free software; you can redistribute it and/or modify it > + * under the terms of the GNU General Public License version 2 as published by > + * the Free Software Foundation. > + * > + * 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 <linux/clk.h> > +#include <linux/io.h> > +#include <linux/kernel.h> > +#include <linux/module.h> > +#include <linux/of_device.h> > +#include <linux/platform_device.h> > +#include <linux/qcom_scm.h> > +#include <linux/sizes.h> > +#include <linux/slab.h> > +#include <linux/types.h> > + > +#include <soc/qcom/ocmem.h> > +#include "ocmem.xml.h" > + > +enum region_mode { > + WIDE_MODE = 0x0, > + THIN_MODE, > + MODE_DEFAULT = WIDE_MODE, > +}; > + > +struct ocmem_region { > + unsigned psgsc_ctrl; > + bool interleaved; > + enum region_mode mode; > + unsigned int num_macros; > + enum ocmem_macro_state macro_state[4]; > + unsigned long macro_size; > + unsigned long region_size; > +}; > + > +struct ocmem_config { > + uint8_t num_regions; > + uint32_t macro_size; > +}; > + > +struct ocmem { > + struct device *dev; > + const struct ocmem_config *config; > + struct resource *ocmem_mem; > + struct clk *core_clk; > + struct clk *iface_clk; > + void __iomem *mmio; > + > + unsigned num_ports; > + unsigned num_macros; > + bool interleaved; > + > + struct ocmem_region *regions; > +}; > + > +#define FIELD(val, name) (((val) & name ## __MASK) >> name ## __SHIFT) > + > +static struct ocmem *ocmem; > + > +static bool ocmem_exists(void); > + > +static inline void ocmem_write(struct ocmem *ocmem, u32 reg, u32 data) > +{ > + writel(data, ocmem->mmio + reg); > +} > + > +static inline u32 ocmem_read(struct ocmem *ocmem, u32 reg) > +{ > + return readl(ocmem->mmio + reg); > +} > + > +static int ocmem_clk_enable(struct ocmem *ocmem) > +{ > + int ret; > + > + ret = clk_prepare_enable(ocmem->core_clk); > + if (ret) > + return ret; > + > + ret = clk_prepare_enable(ocmem->iface_clk); > + if (ret) > + return ret; > + > + return 0; > +} > + > +static void ocmem_clk_disable(struct ocmem *ocmem) > +{ > + clk_disable_unprepare(ocmem->iface_clk); > + clk_disable_unprepare(ocmem->core_clk); > +} > + > +static int ocmem_dev_remove(struct platform_device *pdev) > +{ > + ocmem_clk_disable(ocmem); > + return 0; > +} > + > +static void update_ocmem(struct ocmem *ocmem) > +{ > + uint32_t region_mode_ctrl = 0x0; > + unsigned pos = 0; > + unsigned i = 0; > + > + if (!qcom_scm_ocmem_lock_available()) { > + for (i = 0; i < ocmem->config->num_regions; i++) { > + struct ocmem_region *region = &ocmem->regions[i]; > + pos = i << 2; > + if (region->mode == THIN_MODE) > + region_mode_ctrl |= BIT(pos); > + } > + dev_dbg(ocmem->dev, "ocmem_region_mode_control %x\n", region_mode_ctrl); > + ocmem_write(ocmem, REG_OCMEM_REGION_MODE_CTL, region_mode_ctrl); > + } > + > + for (i = 0; i < ocmem->config->num_regions; i++) { > + struct ocmem_region *region = &ocmem->regions[i]; > + > + ocmem_write(ocmem, REG_OCMEM_PSGSC_CTL(i), > + OCMEM_PSGSC_CTL_MACRO0_MODE(region->macro_state[0]) | > + OCMEM_PSGSC_CTL_MACRO1_MODE(region->macro_state[1]) | > + OCMEM_PSGSC_CTL_MACRO2_MODE(region->macro_state[2]) | > + OCMEM_PSGSC_CTL_MACRO3_MODE(region->macro_state[3])); > + } > +} > + > +static unsigned long phys_to_offset(unsigned long addr) > +{ > + if ((addr < ocmem->ocmem_mem->start) || > + (addr >= ocmem->ocmem_mem->end)) > + return 0; > + return addr - ocmem->ocmem_mem->start; > +} > + > +static unsigned long device_address(enum ocmem_client client, unsigned long addr) > +{ > + /* TODO, gpu uses phys_to_offset, but others do not.. */ > + return phys_to_offset(addr); > +} > + > +static void update_range(struct ocmem *ocmem, struct ocmem_buf *buf, > + enum ocmem_macro_state mstate, enum region_mode rmode) > +{ > + unsigned long offset = 0; > + int i, j; > + > + /* > + * TODO probably should assert somewhere that range is aligned > + * to macro boundaries.. > + */ > + > + for (i = 0; i < ocmem->config->num_regions; i++) { > + struct ocmem_region *region = &ocmem->regions[i]; > + if ((buf->offset <= offset) && (offset < (buf->offset + buf->len))) > + region->mode = rmode; > + for (j = 0; j < region->num_macros; j++) { > + if ((buf->offset <= offset) && (offset < (buf->offset + buf->len))) > + region->macro_state[j] = mstate; > + offset += region->macro_size; > + } > + } > + > + update_ocmem(ocmem); > +} > + > +struct ocmem_buf *ocmem_allocate(enum ocmem_client client, unsigned long size) > +{ > + struct ocmem_buf *buf; > + > + if (!ocmem) { > + if (ocmem_exists()) > + return ERR_PTR(-EPROBE_DEFER); > + return ERR_PTR(-ENXIO); > + } > + > + buf = kzalloc(sizeof(*buf), GFP_KERNEL); > + if (!buf) > + return ERR_PTR(-ENOMEM); > + > + /* > + * TODO less hard-coded allocation that works for more than > + * one user: > + */ > + > + buf->offset = 0; > + buf->addr = device_address(client, buf->offset); > + buf->len = size; > + > + update_range(ocmem, buf, CORE_ON, WIDE_MODE); > + > + if (qcom_scm_ocmem_lock_available()) { > + int ret; > + ret = qcom_scm_ocmem_lock(QCOM_SCM_OCMEM_GRAPHICS_ID, > + buf->offset, buf->len, WIDE_MODE); > + if (ret) > + dev_err(ocmem->dev, "could not lock: %d\n", ret); > + } else { > + if (client == OCMEM_GRAPHICS) { > + ocmem_write(ocmem, REG_OCMEM_GFX_MPU_START, buf->offset); > + ocmem_write(ocmem, REG_OCMEM_GFX_MPU_END, buf->offset + buf->len); > + } > + } > + > + return buf; > +} > +EXPORT_SYMBOL(ocmem_allocate); > + > +void ocmem_free(enum ocmem_client client, struct ocmem_buf *buf) > +{ > + update_range(ocmem, buf, CLK_OFF, MODE_DEFAULT); > + > + if (qcom_scm_ocmem_lock_available()) { > + int ret; > + ret = qcom_scm_ocmem_unlock(QCOM_SCM_OCMEM_GRAPHICS_ID, > + buf->offset, buf->len); > + if (ret) > + dev_err(ocmem->dev, "could not unlock: %d\n", ret); > + } else { > + if (client == OCMEM_GRAPHICS) { > + ocmem_write(ocmem, REG_OCMEM_GFX_MPU_START, 0x0); > + ocmem_write(ocmem, REG_OCMEM_GFX_MPU_END, 0x0); > + } > + } > + > + kfree(buf); > +} > +EXPORT_SYMBOL(ocmem_free); > + > +static const struct ocmem_config ocmem_8974_config = { > + .num_regions = 3, .macro_size = SZ_128K, > +}; > + > +static const struct of_device_id ocmem_dt_match[] = { > + { .compatible = "qcom,ocmem-msm8974", .data = &ocmem_8974_config }, > + {} > +}; > + > +static int ocmem_dev_probe(struct platform_device *pdev) > +{ > + struct device *dev = &pdev->dev; > + const struct ocmem_config *config = NULL; > + const struct of_device_id *match; > + struct resource *res; > + uint32_t reg, num_banks, region_size; > + int i, j, ret; > + > + /* we need scm to be available: */ > + if (!qcom_scm_is_available()) > + return -EPROBE_DEFER; > + > + match = of_match_device(ocmem_dt_match, dev); > + if (match) > + config = match->data; > + > + if (!config) { > + dev_err(dev, "unknown config: %s\n", dev->of_node->name); > + return -ENXIO; > + } > + > + ocmem = devm_kzalloc(dev, sizeof(*ocmem), GFP_KERNEL); > + if (!ocmem) > + return -ENOMEM; > + > + ocmem->dev = dev; > + ocmem->config = config; > + > + ocmem->core_clk = devm_clk_get(dev, "core_clk"); > + if (IS_ERR(ocmem->core_clk)) { > + dev_err(dev, "Unable to get the core clock\n"); > + return PTR_ERR(ocmem->core_clk); > + } > + > + ocmem->iface_clk = devm_clk_get(dev, "iface_clk"); > + if (IS_ERR_OR_NULL(ocmem->iface_clk)) { > + ret = PTR_ERR(ocmem->iface_clk); > + ocmem->iface_clk = NULL; > + /* in probe-defer case, propagate error up and try again later: */ > + if (ret == -EPROBE_DEFER) > + goto fail; > + } > + > + res = platform_get_resource_byname(pdev, IORESOURCE_MEM, > + "ocmem_ctrl_physical"); > + if (!res) { > + dev_err(&pdev->dev, "failed to get memory resource\n"); > + ret = -EINVAL; > + goto fail; > + } > + > + ocmem->mmio = devm_ioremap_nocache(&pdev->dev, res->start, > + resource_size(res)); > + if (IS_ERR(ocmem->mmio)) { > + dev_err(&pdev->dev, "failed to ioremap memory resource\n"); > + ret = -EINVAL; > + goto fail; > + } > + > + ocmem->ocmem_mem = platform_get_resource_byname(pdev, IORESOURCE_MEM, > + "ocmem_physical"); > + if (!ocmem->ocmem_mem) { > + dev_err(dev, "could not get OCMEM region\n"); > + return -ENXIO; > + } > + > + /* The core clock is synchronous with graphics */ > + WARN_ON(clk_set_rate(ocmem->core_clk, 1000) < 0); > + > + ret = ocmem_clk_enable(ocmem); > + if (ret) > + goto fail; > + > + if (qcom_scm_restore_sec_config_available()) { > + dev_dbg(dev, "configuring scm\n"); > + ret = qcom_scm_restore_sec_config(QCOM_SCM_OCMEM_DEV_ID); > + if (ret) > + goto fail; > + } > + > + reg = ocmem_read(ocmem, REG_OCMEM_HW_PROFILE); > + ocmem->num_ports = FIELD(reg, OCMEM_HW_PROFILE_NUM_PORTS); > + ocmem->num_macros = FIELD(reg, OCMEM_HW_PROFILE_NUM_MACROS); > + ocmem->interleaved = !!(reg & OCMEM_HW_PROFILE_INTERLEAVING); > + > + num_banks = ocmem->num_ports / 2; > + region_size = config->macro_size * num_banks; > + > + dev_info(dev, "%u ports, %u regions, %u macros, %sinterleaved\n", > + ocmem->num_ports, config->num_regions, ocmem->num_macros, > + ocmem->interleaved ? "" : "not "); > + > + ocmem->regions = devm_kcalloc(dev, config->num_regions, > + sizeof(struct ocmem_region), GFP_KERNEL); > + if (!ocmem->regions) { > + ret = -ENOMEM; > + goto fail; > + } > + > + for (i = 0; i < config->num_regions; i++) { > + struct ocmem_region *region = &ocmem->regions[i]; > + > + if (WARN_ON(num_banks > ARRAY_SIZE(region->macro_state))) { > + ret = -EINVAL; > + goto fail; > + } > + > + region->mode = MODE_DEFAULT; > + region->num_macros = num_banks; > + > + if ((i == (config->num_regions - 1)) && > + (reg & OCMEM_HW_PROFILE_LAST_REGN_HALFSIZE)) { > + region->macro_size = config->macro_size / 2; > + region->region_size = region_size / 2; > + } else { > + region->macro_size = config->macro_size; > + region->region_size = region_size; > + } > + > + for (j = 0; j < ARRAY_SIZE(region->macro_state); j++) > + region->macro_state[j] = CLK_OFF; > + } > + > + return 0; > + > +fail: > + dev_err(dev, "probe failed\n"); > + ocmem_dev_remove(pdev); > + return ret; > +} > + > +MODULE_DEVICE_TABLE(of, ocmem_dt_match); > + > +static struct platform_driver ocmem_driver = { > + .probe = ocmem_dev_probe, > + .remove = ocmem_dev_remove, > + .driver = { > + .name = "ocmem", > + .of_match_table = ocmem_dt_match, > + }, > +}; > + > +static bool ocmem_exists(void) > +{ > + struct device_driver *drv = &ocmem_driver.driver; > + struct device *d; > + > + d = bus_find_device(&platform_bus_type, NULL, drv, > + (void *)platform_bus_type.match); > + if (d) { > + put_device(d); > + return true; > + } > + > + return false; > +} > + > +module_platform_driver(ocmem_driver); > diff --git a/drivers/soc/qcom/ocmem.xml.h b/drivers/soc/qcom/ocmem.xml.h > new file mode 100644 > index 0000000..51c12d5 > --- /dev/null > +++ b/drivers/soc/qcom/ocmem.xml.h > @@ -0,0 +1,106 @@ > +#ifndef OCMEM_XML > +#define OCMEM_XML > + > +/* Autogenerated file, DO NOT EDIT manually! > + > +This file was generated by the rules-ng-ng headergen tool in this git repository: > +http://github.com/freedreno/envytools/ > +git clone https://github.com/freedreno/envytools.git > + > +The rules-ng-ng source files this header was generated from are: > +- /home/robclark/src/freedreno/envytools/rnndb/adreno/ocmem.xml ( 1773 bytes, from 2015-09-24 17:30:00) > + > +Copyright (C) 2013-2015 by the following authors: > +- Rob Clark <robdclark@xxxxxxxxx> (robclark) > + > +Permission is hereby granted, free of charge, to any person obtaining > +a copy of this software and associated documentation files (the > +"Software"), to deal in the Software without restriction, including > +without limitation the rights to use, copy, modify, merge, publish, > +distribute, sublicense, and/or sell copies of the Software, and to > +permit persons to whom the Software is furnished to do so, subject to > +the following conditions: > + > +The above copyright notice and this permission notice (including the > +next paragraph) shall be included in all copies or substantial > +portions of the Software. > + > +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, > +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF > +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. > +IN NO EVENT SHALL THE COPYRIGHT OWNER(S) AND/OR ITS SUPPLIERS BE > +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION > +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION > +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. > +*/ > + > + > +enum ocmem_macro_state { > + PASSTHROUGH = 0, > + PERI_ON = 1, > + CORE_ON = 2, > + CLK_OFF = 4, > +}; > + > +#define REG_OCMEM_HW_VERSION 0x00000000 > + > +#define REG_OCMEM_HW_PROFILE 0x00000004 > +#define OCMEM_HW_PROFILE_NUM_PORTS__MASK 0x0000000f > +#define OCMEM_HW_PROFILE_NUM_PORTS__SHIFT 0 > +static inline uint32_t OCMEM_HW_PROFILE_NUM_PORTS(uint32_t val) > +{ > + return ((val) << OCMEM_HW_PROFILE_NUM_PORTS__SHIFT) & OCMEM_HW_PROFILE_NUM_PORTS__MASK; > +} > +#define OCMEM_HW_PROFILE_NUM_MACROS__MASK 0x00003f00 > +#define OCMEM_HW_PROFILE_NUM_MACROS__SHIFT 8 > +static inline uint32_t OCMEM_HW_PROFILE_NUM_MACROS(uint32_t val) > +{ > + return ((val) << OCMEM_HW_PROFILE_NUM_MACROS__SHIFT) & OCMEM_HW_PROFILE_NUM_MACROS__MASK; > +} > +#define OCMEM_HW_PROFILE_LAST_REGN_HALFSIZE 0x00010000 > +#define OCMEM_HW_PROFILE_INTERLEAVING 0x00020000 > + > +#define REG_OCMEM_GEN_STATUS 0x0000000c > + > +#define REG_OCMEM_PSGSC_STATUS 0x00000038 > + > +static inline uint32_t REG_OCMEM_PSGSC(uint32_t i0) { return 0x0000003c + 0x1*i0; } > + > +static inline uint32_t REG_OCMEM_PSGSC_CTL(uint32_t i0) { return 0x0000003c + 0x1*i0; } > +#define OCMEM_PSGSC_CTL_MACRO0_MODE__MASK 0x00000007 > +#define OCMEM_PSGSC_CTL_MACRO0_MODE__SHIFT 0 > +static inline uint32_t OCMEM_PSGSC_CTL_MACRO0_MODE(enum ocmem_macro_state val) > +{ > + return ((val) << OCMEM_PSGSC_CTL_MACRO0_MODE__SHIFT) & OCMEM_PSGSC_CTL_MACRO0_MODE__MASK; > +} > +#define OCMEM_PSGSC_CTL_MACRO1_MODE__MASK 0x00000070 > +#define OCMEM_PSGSC_CTL_MACRO1_MODE__SHIFT 4 > +static inline uint32_t OCMEM_PSGSC_CTL_MACRO1_MODE(enum ocmem_macro_state val) > +{ > + return ((val) << OCMEM_PSGSC_CTL_MACRO1_MODE__SHIFT) & OCMEM_PSGSC_CTL_MACRO1_MODE__MASK; > +} > +#define OCMEM_PSGSC_CTL_MACRO2_MODE__MASK 0x00000700 > +#define OCMEM_PSGSC_CTL_MACRO2_MODE__SHIFT 8 > +static inline uint32_t OCMEM_PSGSC_CTL_MACRO2_MODE(enum ocmem_macro_state val) > +{ > + return ((val) << OCMEM_PSGSC_CTL_MACRO2_MODE__SHIFT) & OCMEM_PSGSC_CTL_MACRO2_MODE__MASK; > +} > +#define OCMEM_PSGSC_CTL_MACRO3_MODE__MASK 0x00007000 > +#define OCMEM_PSGSC_CTL_MACRO3_MODE__SHIFT 12 > +static inline uint32_t OCMEM_PSGSC_CTL_MACRO3_MODE(enum ocmem_macro_state val) > +{ > + return ((val) << OCMEM_PSGSC_CTL_MACRO3_MODE__SHIFT) & OCMEM_PSGSC_CTL_MACRO3_MODE__MASK; > +} > + > +#define REG_OCMEM_REGION_MODE_CTL 0x00001000 > +#define OCMEM_REGION_MODE_CTL_REG0_THIN 0x00000001 > +#define OCMEM_REGION_MODE_CTL_REG1_THIN 0x00000002 > +#define OCMEM_REGION_MODE_CTL_REG2_THIN 0x00000004 > +#define OCMEM_REGION_MODE_CTL_REG3_THIN 0x00000008 > + > +#define REG_OCMEM_GFX_MPU_START 0x00001004 > + > +#define REG_OCMEM_GFX_MPU_END 0x00001008 > + > + > +#endif /* OCMEM_XML */ > diff --git a/include/soc/qcom/ocmem.h b/include/soc/qcom/ocmem.h > new file mode 100644 > index 0000000..47f6548 > --- /dev/null > +++ b/include/soc/qcom/ocmem.h > @@ -0,0 +1,40 @@ > +/* > + * Copyright (C) 2015 Red Hat > + * Author: Rob Clark <robdclark@xxxxxxxxx> > + * > + * This program is free software; you can redistribute it and/or modify it > + * under the terms of the GNU General Public License version 2 as published by > + * the Free Software Foundation. > + * > + * 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/>. > + */ > + > +#ifndef __OCMEM_H__ > +#define __OCMEM_H__ > + > +enum ocmem_client { > + /* GMEM clients */ > + OCMEM_GRAPHICS = 0x0, > + /* > + * TODO add more once ocmem_allocate() is clever enough to > + * deal with multiple clients. > + */ > + OCMEM_CLIENT_MAX, > +}; > + > +struct ocmem_buf { > + unsigned long offset; > + unsigned long addr; > + unsigned long len; > +}; > + > +struct ocmem_buf *ocmem_allocate(enum ocmem_client client, unsigned long size); > +void ocmem_free(enum ocmem_client client, struct ocmem_buf *buf); > + > +#endif /* __OCMEM_H__ */ > -- > 2.4.3 > -- To unsubscribe from this list: send the line "unsubscribe linux-arm-msm" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html