// SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note /* * * (C) COPYRIGHT 2020-2023 ARM Limited. All rights reserved. * * This program is free software and is provided to you under the terms of the * GNU General Public License version 2 as published by the Free Software * Foundation, and any use by you of this program is subject to the terms * of such GNU license. * * 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, you can access it online at * http://www.gnu.org/licenses/gpl-2.0.html. * */ #include #include #include #include "mali_kbase_csf_firmware_cfg.h" #include "mali_kbase_csf_firmware_log.h" #if CONFIG_SYSFS #define CSF_FIRMWARE_CFG_SYSFS_DIR_NAME "firmware_config" #define CSF_FIRMWARE_CFG_LOG_VERBOSITY_ENTRY_NAME "Log verbosity" #ifdef CONFIG_MALI_HOST_CONTROLS_SC_RAILS #define HOST_CONTROLS_SC_RAILS_CFG_ENTRY_NAME "Host controls SC rails" #endif #define CSF_FIRMWARE_CFG_WA_CFG0_ENTRY_NAME "WA_CFG0" /** * struct firmware_config - Configuration item within the MCU firmware * * @node: List head linking all options to * kbase_device:csf.firmware_config * @kbdev: Pointer to the Kbase device * @kobj: Kobject corresponding to the sysfs sub-directory, * inside CSF_FIRMWARE_CFG_SYSFS_DIR_NAME directory, * representing the configuration option @name. * @kobj_inited: kobject initialization state * @updatable: Indicates whether config items can be updated with * FIRMWARE_CONFIG_UPDATE * @name: NUL-terminated string naming the option * @address: The address in the firmware image of the configuration option * @min: The lowest legal value of the configuration option * @max: The maximum legal value of the configuration option * @cur_val: The current value of the configuration option * * The firmware may expose configuration options. Each option has a name, the * address where the option is controlled and the minimum and maximum values * that the option can take. */ struct firmware_config { struct list_head node; struct kbase_device *kbdev; struct kobject kobj; bool kobj_inited; bool updatable; char *name; u32 address; u32 min; u32 max; u32 cur_val; }; #define FW_CFG_ATTR(_name, _mode) \ struct attribute fw_cfg_attr_##_name = { \ .name = __stringify(_name), \ .mode = VERIFY_OCTAL_PERMISSIONS(_mode), \ } static FW_CFG_ATTR(min, 0444); static FW_CFG_ATTR(max, 0444); static FW_CFG_ATTR(cur, 0644); static void fw_cfg_kobj_release(struct kobject *kobj) { struct firmware_config *config = container_of(kobj, struct firmware_config, kobj); kfree(config); } static ssize_t show_fw_cfg(struct kobject *kobj, struct attribute *attr, char *buf) { struct firmware_config *config = container_of(kobj, struct firmware_config, kobj); struct kbase_device *kbdev = config->kbdev; u32 val = 0; if (!kbdev) return -ENODEV; if (attr == &fw_cfg_attr_max) val = config->max; else if (attr == &fw_cfg_attr_min) val = config->min; else if (attr == &fw_cfg_attr_cur) { unsigned long flags; spin_lock_irqsave(&kbdev->hwaccess_lock, flags); val = config->cur_val; spin_unlock_irqrestore(&kbdev->hwaccess_lock, flags); } else { dev_warn(kbdev->dev, "Unexpected read from entry %s/%s", config->name, attr->name); return -EINVAL; } return scnprintf(buf, PAGE_SIZE, "%u\n", val); } static ssize_t store_fw_cfg(struct kobject *kobj, struct attribute *attr, const char *buf, size_t count) { struct firmware_config *config = container_of(kobj, struct firmware_config, kobj); struct kbase_device *kbdev = config->kbdev; if (!kbdev) return -ENODEV; if (attr == &fw_cfg_attr_cur) { unsigned long flags; u32 val, cur_val; int ret = kstrtouint(buf, 0, &val); if (ret) { dev_err(kbdev->dev, "Couldn't process %s/%s write operation.\n" "Use format \n", config->name, attr->name); return -EINVAL; } #ifdef CONFIG_MALI_HOST_CONTROLS_SC_RAILS if (!strcmp(config->name, HOST_CONTROLS_SC_RAILS_CFG_ENTRY_NAME)) return -EPERM; #endif if (!strcmp(config->name, CSF_FIRMWARE_CFG_WA_CFG0_ENTRY_NAME)) return -EPERM; if ((val < config->min) || (val > config->max)) return -EINVAL; spin_lock_irqsave(&kbdev->hwaccess_lock, flags); cur_val = config->cur_val; if (cur_val == val) { spin_unlock_irqrestore(&kbdev->hwaccess_lock, flags); return (ssize_t)count; } /* If configuration update cannot be performed with * FIRMWARE_CONFIG_UPDATE then we need to do a * silent reset before we update the memory. */ if (!config->updatable) { /* * If there is already a GPU reset pending then inform * the User to retry the write. */ if (kbase_reset_gpu_silent(kbdev)) { spin_unlock_irqrestore(&kbdev->hwaccess_lock, flags); return -EAGAIN; } } /* * GPU reset request has been placed, now update the * firmware image. GPU reset will take place only after * hwaccess_lock is released. * Update made to firmware image in memory would not * be lost on GPU reset as configuration entries reside * in the RONLY section of firmware image, which is not * reloaded on firmware reboot due to GPU reset. */ kbase_csf_update_firmware_memory(kbdev, config->address, val); config->cur_val = val; spin_unlock_irqrestore(&kbdev->hwaccess_lock, flags); /* Enable FW logging only if Log verbosity is non-zero */ if (!strcmp(config->name, CSF_FIRMWARE_CFG_LOG_VERBOSITY_ENTRY_NAME) && (!cur_val || !val)) { ret = kbase_csf_firmware_log_toggle_logging_calls(kbdev, val); if (ret) { /* Undo FW configuration changes */ spin_lock_irqsave(&kbdev->hwaccess_lock, flags); config->cur_val = cur_val; kbase_csf_update_firmware_memory(kbdev, config->address, cur_val); spin_unlock_irqrestore(&kbdev->hwaccess_lock, flags); return ret; } } /* If we can update the config without firmware reset then * we need to just trigger FIRMWARE_CONFIG_UPDATE. */ if (config->updatable) { ret = kbase_csf_trigger_firmware_config_update(kbdev); if (ret) return ret; } /* Wait for the config update to take effect */ if (!config->updatable) kbase_reset_gpu_wait(kbdev); } else { dev_warn(kbdev->dev, "Unexpected write to entry %s/%s", config->name, attr->name); return -EINVAL; } return (ssize_t)count; } static const struct sysfs_ops fw_cfg_ops = { .show = &show_fw_cfg, .store = &store_fw_cfg, }; static struct attribute *fw_cfg_attrs[] = { &fw_cfg_attr_min, &fw_cfg_attr_max, &fw_cfg_attr_cur, NULL, }; #if (KERNEL_VERSION(5, 2, 0) <= LINUX_VERSION_CODE) ATTRIBUTE_GROUPS(fw_cfg); #endif static struct kobj_type fw_cfg_kobj_type = { .release = &fw_cfg_kobj_release, .sysfs_ops = &fw_cfg_ops, #if (KERNEL_VERSION(5, 2, 0) <= LINUX_VERSION_CODE) .default_groups = fw_cfg_groups, #else .default_attrs = fw_cfg_attrs, #endif }; int kbase_csf_firmware_cfg_init(struct kbase_device *kbdev) { struct firmware_config *config; kbdev->csf.fw_cfg_kobj = kobject_create_and_add(CSF_FIRMWARE_CFG_SYSFS_DIR_NAME, &kbdev->dev->kobj); if (!kbdev->csf.fw_cfg_kobj) { kobject_put(kbdev->csf.fw_cfg_kobj); dev_err(kbdev->dev, "Creation of %s sysfs sub-directory failed\n", CSF_FIRMWARE_CFG_SYSFS_DIR_NAME); return -ENOMEM; } list_for_each_entry(config, &kbdev->csf.firmware_config, node) { int err; kbase_csf_read_firmware_memory(kbdev, config->address, &config->cur_val); if (!strcmp(config->name, CSF_FIRMWARE_CFG_LOG_VERBOSITY_ENTRY_NAME) && (config->cur_val)) { err = kbase_csf_firmware_log_toggle_logging_calls(config->kbdev, config->cur_val); if (err) { kobject_put(&config->kobj); dev_err(kbdev->dev, "Failed to enable logging (result: %d)", err); return err; } } err = kobject_init_and_add(&config->kobj, &fw_cfg_kobj_type, kbdev->csf.fw_cfg_kobj, "%s", config->name); if (err) { kobject_put(&config->kobj); dev_err(kbdev->dev, "Creation of %s sysfs sub-directory failed\n", config->name); return err; } config->kobj_inited = true; } return 0; } void kbase_csf_firmware_cfg_term(struct kbase_device *kbdev) { while (!list_empty(&kbdev->csf.firmware_config)) { struct firmware_config *config; config = list_first_entry(&kbdev->csf.firmware_config, struct firmware_config, node); list_del(&config->node); if (config->kobj_inited) { kobject_del(&config->kobj); kobject_put(&config->kobj); } else kfree(config); } kobject_del(kbdev->csf.fw_cfg_kobj); kobject_put(kbdev->csf.fw_cfg_kobj); } int kbase_csf_firmware_cfg_option_entry_parse(struct kbase_device *kbdev, const struct kbase_csf_mcu_fw *const fw, const u32 *entry, unsigned int size, bool updatable) { const char *name = (char *)&entry[3]; struct firmware_config *config; const unsigned int name_len = size - CONFIGURATION_ENTRY_NAME_OFFSET; CSTD_UNUSED(fw); /* Allocate enough space for struct firmware_config and the * configuration option name (with NULL termination) */ config = kzalloc(sizeof(*config) + name_len + 1, GFP_KERNEL); if (!config) return -ENOMEM; config->kbdev = kbdev; config->updatable = updatable; config->name = (char *)(config + 1); config->address = entry[0]; config->min = entry[1]; config->max = entry[2]; memcpy(config->name, name, name_len); config->name[name_len] = 0; list_add(&config->node, &kbdev->csf.firmware_config); dev_dbg(kbdev->dev, "Configuration option '%s' at 0x%x range %u-%u", config->name, config->address, config->min, config->max); return 0; } int kbase_csf_firmware_cfg_find_config_address(struct kbase_device *kbdev, const char *name, u32* addr) { struct firmware_config *config; list_for_each_entry(config, &kbdev->csf.firmware_config, node) { if (strcmp(config->name, name) || !config->address) continue; *addr = config->address; return 0; } return -ENOENT; } int kbase_csf_firmware_cfg_fw_wa_enable(struct kbase_device *kbdev) { struct firmware_config *config; /* "quirks_ext" property is optional */ if (!kbdev->csf.quirks_ext) return 0; list_for_each_entry(config, &kbdev->csf.firmware_config, node) { if (strcmp(config->name, CSF_FIRMWARE_CFG_WA_CFG0_ENTRY_NAME)) continue; dev_info(kbdev->dev, "External quirks 0: 0x%08x", kbdev->csf.quirks_ext[0]); kbase_csf_update_firmware_memory(kbdev, config->address, kbdev->csf.quirks_ext[0]); return 0; } return -ENOENT; } #ifdef CONFIG_MALI_HOST_CONTROLS_SC_RAILS int kbase_csf_firmware_cfg_enable_host_ctrl_sc_rails(struct kbase_device *kbdev) { struct firmware_config *config; list_for_each_entry(config, &kbdev->csf.firmware_config, node) { if (strcmp(config->name, HOST_CONTROLS_SC_RAILS_CFG_ENTRY_NAME)) continue; kbase_csf_update_firmware_memory(kbdev, config->address, 1); return 0; } return -ENOENT; } #endif int kbase_csf_firmware_cfg_fw_wa_init(struct kbase_device *kbdev) { int ret; int entry_count; size_t entry_bytes; /* "quirks-ext" property is optional and may have no value. * Also try fallback "quirks_ext" property if it doesn't exist. */ entry_count = of_property_count_u32_elems(kbdev->dev->of_node, "quirks-ext"); if (entry_count == -EINVAL) entry_count = of_property_count_u32_elems(kbdev->dev->of_node, "quirks_ext"); if (entry_count == -EINVAL || entry_count == -ENODATA) return 0; entry_bytes = (size_t)entry_count * sizeof(u32); kbdev->csf.quirks_ext = kzalloc(entry_bytes, GFP_KERNEL); if (!kbdev->csf.quirks_ext) return -ENOMEM; ret = of_property_read_u32_array(kbdev->dev->of_node, "quirks-ext", kbdev->csf.quirks_ext, (size_t)entry_count); if (ret == -EINVAL) ret = of_property_read_u32_array(kbdev->dev->of_node, "quirks_ext", kbdev->csf.quirks_ext, (size_t)entry_count); if (ret == -EINVAL || ret == -ENODATA) { /* This is unexpected since the property is already accessed for counting the number * of its elements. */ dev_err(kbdev->dev, "\"quirks_ext\" DTB property data read failed"); return ret; } if (ret == -EOVERFLOW) { dev_err(kbdev->dev, "\"quirks_ext\" DTB property data size exceeds 32 bits"); return ret; } return kbase_csf_firmware_cfg_fw_wa_enable(kbdev); } void kbase_csf_firmware_cfg_fw_wa_term(struct kbase_device *kbdev) { kfree(kbdev->csf.quirks_ext); } #else int kbase_csf_firmware_cfg_init(struct kbase_device *kbdev) { return 0; } void kbase_csf_firmware_cfg_term(struct kbase_device *kbdev) { /* !CONFIG_SYSFS: Nothing to do here */ } int kbase_csf_firmware_cfg_option_entry_parse(struct kbase_device *kbdev, const struct kbase_csf_mcu_fw *const fw, const u32 *entry, unsigned int size) { return 0; } #ifdef CONFIG_MALI_HOST_CONTROLS_SC_RAILS int kbase_csf_firmware_cfg_enable_host_ctrl_sc_rails(struct kbase_device *kbdev) { return 0; } #endif int kbase_csf_firmware_cfg_fw_wa_enable(struct kbase_device *kbdev) { return 0; } int kbase_csf_firmware_cfg_fw_wa_init(struct kbase_device *kbdev) { return 0; } #endif /* CONFIG_SYSFS */