// SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note /* * * (C) COPYRIGHT 2021-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 "backend/gpu/mali_kbase_pm_internal.h" #include #include #include #include #include /* * ARMv7 instruction: Branch with Link calls a subroutine at a PC-relative address. */ #define ARMV7_T1_BL_IMM_INSTR 0xd800f000 /* * ARMv7 instruction: Branch with Link calls a subroutine at a PC-relative address, maximum * negative jump offset. */ #define ARMV7_T1_BL_IMM_RANGE_MIN -16777216 /* * ARMv7 instruction: Branch with Link calls a subroutine at a PC-relative address, maximum * positive jump offset. */ #define ARMV7_T1_BL_IMM_RANGE_MAX 16777214 /* * ARMv7 instruction: Double NOP instructions. */ #define ARMV7_DOUBLE_NOP_INSTR 0xbf00bf00 #if defined(CONFIG_DEBUG_FS) static int kbase_csf_firmware_log_enable_mask_read(void *data, u64 *val) { struct kbase_device *kbdev = (struct kbase_device *)data; struct firmware_trace_buffer *tb = kbase_csf_firmware_get_trace_buffer(kbdev, KBASE_CSFFW_LOG_BUF_NAME); if (tb == NULL) { dev_err(kbdev->dev, "Couldn't get the firmware trace buffer"); return -EIO; } /* The enabled traces limited to u64 here, regarded practical */ *val = kbase_csf_firmware_trace_buffer_get_active_mask64(tb); return 0; } static int kbase_csf_firmware_log_enable_mask_write(void *data, u64 val) { struct kbase_device *kbdev = (struct kbase_device *)data; struct firmware_trace_buffer *tb = kbase_csf_firmware_get_trace_buffer(kbdev, KBASE_CSFFW_LOG_BUF_NAME); u64 new_mask; unsigned int enable_bits_count; if (tb == NULL) { dev_err(kbdev->dev, "Couldn't get the firmware trace buffer"); return -EIO; } /* Ignore unsupported types */ enable_bits_count = kbase_csf_firmware_trace_buffer_get_trace_enable_bits_count(tb); if (enable_bits_count > 64) { dev_dbg(kbdev->dev, "Limit enabled bits count from %u to 64", enable_bits_count); enable_bits_count = 64; } new_mask = val & (UINT64_MAX >> (64 - enable_bits_count)); if (new_mask != kbase_csf_firmware_trace_buffer_get_active_mask64(tb)) return kbase_csf_firmware_trace_buffer_set_active_mask64(tb, new_mask); else return 0; } static int kbasep_csf_firmware_log_debugfs_open(struct inode *in, struct file *file) { struct kbase_device *kbdev = in->i_private; file->private_data = kbdev; dev_dbg(kbdev->dev, "Opened firmware trace buffer dump debugfs file"); return 0; } static ssize_t kbasep_csf_firmware_log_debugfs_read(struct file *file, char __user *buf, size_t size, loff_t *ppos) { struct kbase_device *kbdev = file->private_data; struct kbase_csf_firmware_log *fw_log = &kbdev->csf.fw_log; unsigned int n_read; unsigned long not_copied; /* Limit reads to the kernel dump buffer size */ size_t mem = MIN(size, FIRMWARE_LOG_DUMP_BUF_SIZE); int ret; struct firmware_trace_buffer *tb = kbase_csf_firmware_get_trace_buffer(kbdev, KBASE_CSFFW_LOG_BUF_NAME); if (tb == NULL) { dev_err(kbdev->dev, "Couldn't get the firmware trace buffer"); return -EIO; } if (atomic_cmpxchg(&fw_log->busy, 0, 1) != 0) return -EBUSY; /* Reading from userspace is only allowed in manual mode or auto-discard mode */ if (fw_log->mode != KBASE_CSF_FIRMWARE_LOG_MODE_MANUAL && fw_log->mode != KBASE_CSF_FIRMWARE_LOG_MODE_AUTO_DISCARD) { ret = -EINVAL; goto out; } n_read = kbase_csf_firmware_trace_buffer_read_data(tb, fw_log->dump_buf, mem); /* Do the copy, if we have obtained some trace data */ not_copied = (n_read) ? copy_to_user(buf, fw_log->dump_buf, n_read) : 0; if (not_copied) { dev_err(kbdev->dev, "Couldn't copy trace buffer data to user space buffer"); ret = -EFAULT; goto out; } *ppos += n_read; ret = (int)n_read; out: atomic_set(&fw_log->busy, 0); return ret; } static int kbase_csf_firmware_log_mode_read(void *data, u64 *val) { struct kbase_device *kbdev = (struct kbase_device *)data; struct kbase_csf_firmware_log *fw_log = &kbdev->csf.fw_log; *val = fw_log->mode; return 0; } static int kbase_csf_firmware_log_mode_write(void *data, u64 val) { struct kbase_device *kbdev = (struct kbase_device *)data; struct kbase_csf_firmware_log *fw_log = &kbdev->csf.fw_log; int ret = 0; if (atomic_cmpxchg(&fw_log->busy, 0, 1) != 0) return -EBUSY; if (val == fw_log->mode) goto out; switch (val) { case KBASE_CSF_FIRMWARE_LOG_MODE_MANUAL: cancel_delayed_work_sync(&fw_log->poll_work); break; case KBASE_CSF_FIRMWARE_LOG_MODE_AUTO_PRINT: case KBASE_CSF_FIRMWARE_LOG_MODE_AUTO_DISCARD: schedule_delayed_work( &fw_log->poll_work, msecs_to_jiffies((unsigned int)atomic_read(&fw_log->poll_period_ms))); break; default: ret = -EINVAL; goto out; } fw_log->mode = val; out: atomic_set(&fw_log->busy, 0); return ret; } static int kbase_csf_firmware_log_poll_period_read(void *data, u64 *val) { struct kbase_device *kbdev = (struct kbase_device *)data; struct kbase_csf_firmware_log *fw_log = &kbdev->csf.fw_log; *val = (u64)atomic_read(&fw_log->poll_period_ms); return 0; } static int kbase_csf_firmware_log_poll_period_write(void *data, u64 val) { struct kbase_device *kbdev = (struct kbase_device *)data; struct kbase_csf_firmware_log *fw_log = &kbdev->csf.fw_log; atomic_set(&fw_log->poll_period_ms, val); return 0; } DEFINE_DEBUGFS_ATTRIBUTE(kbase_csf_firmware_log_enable_mask_fops, kbase_csf_firmware_log_enable_mask_read, kbase_csf_firmware_log_enable_mask_write, "%llx\n"); static const struct file_operations kbasep_csf_firmware_log_debugfs_fops = { .owner = THIS_MODULE, .open = kbasep_csf_firmware_log_debugfs_open, .read = kbasep_csf_firmware_log_debugfs_read, .llseek = no_llseek, }; DEFINE_DEBUGFS_ATTRIBUTE(kbase_csf_firmware_log_mode_fops, kbase_csf_firmware_log_mode_read, kbase_csf_firmware_log_mode_write, "%llu\n"); DEFINE_DEBUGFS_ATTRIBUTE(kbase_csf_firmware_log_poll_period_fops, kbase_csf_firmware_log_poll_period_read, kbase_csf_firmware_log_poll_period_write, "%llu\n"); #endif /* CONFIG_DEBUG_FS */ static void kbase_csf_firmware_log_discard_buffer(struct kbase_device *kbdev) { struct kbase_csf_firmware_log *fw_log = &kbdev->csf.fw_log; struct firmware_trace_buffer *tb = kbase_csf_firmware_get_trace_buffer(kbdev, KBASE_CSFFW_LOG_BUF_NAME); if (tb == NULL) { dev_dbg(kbdev->dev, "Can't get the trace buffer, firmware log discard skipped"); return; } if (atomic_cmpxchg(&fw_log->busy, 0, 1) != 0) return; kbase_csf_firmware_trace_buffer_discard(tb); atomic_set(&fw_log->busy, 0); } static void kbase_csf_firmware_log_poll(struct work_struct *work) { struct kbase_device *kbdev = container_of(work, struct kbase_device, csf.fw_log.poll_work.work); struct kbase_csf_firmware_log *fw_log = &kbdev->csf.fw_log; if (fw_log->mode == KBASE_CSF_FIRMWARE_LOG_MODE_AUTO_PRINT) kbase_csf_firmware_log_dump_buffer(kbdev); else if (fw_log->mode == KBASE_CSF_FIRMWARE_LOG_MODE_AUTO_DISCARD) kbase_csf_firmware_log_discard_buffer(kbdev); else return; schedule_delayed_work(&fw_log->poll_work, msecs_to_jiffies((unsigned int)atomic_read(&fw_log->poll_period_ms))); } int kbase_csf_firmware_log_init(struct kbase_device *kbdev) { struct kbase_csf_firmware_log *fw_log = &kbdev->csf.fw_log; int err = 0; #if defined(CONFIG_DEBUG_FS) struct dentry *dentry; #endif /* CONFIG_DEBUG_FS */ /* Add one byte for null-termination */ fw_log->dump_buf = kmalloc(FIRMWARE_LOG_DUMP_BUF_SIZE + 1, GFP_KERNEL); if (fw_log->dump_buf == NULL) { err = -ENOMEM; goto out; } /* Ensure null-termination for all strings */ fw_log->dump_buf[FIRMWARE_LOG_DUMP_BUF_SIZE] = 0; /* Set default log polling period */ atomic_set(&fw_log->poll_period_ms, KBASE_CSF_FIRMWARE_LOG_POLL_PERIOD_MS_DEFAULT); INIT_DEFERRABLE_WORK(&fw_log->poll_work, kbase_csf_firmware_log_poll); #ifdef CONFIG_MALI_FW_TRACE_MODE_AUTO_DISCARD fw_log->mode = KBASE_CSF_FIRMWARE_LOG_MODE_AUTO_DISCARD; schedule_delayed_work(&fw_log->poll_work, msecs_to_jiffies(KBASE_CSF_FIRMWARE_LOG_POLL_PERIOD_MS_DEFAULT)); #elif defined(CONFIG_MALI_FW_TRACE_MODE_AUTO_PRINT) fw_log->mode = KBASE_CSF_FIRMWARE_LOG_MODE_AUTO_PRINT; schedule_delayed_work(&fw_log->poll_work, msecs_to_jiffies(KBASE_CSF_FIRMWARE_LOG_POLL_PERIOD_MS_DEFAULT)); #else /* CONFIG_MALI_FW_TRACE_MODE_MANUAL */ fw_log->mode = KBASE_CSF_FIRMWARE_LOG_MODE_MANUAL; #endif atomic_set(&fw_log->busy, 0); #if !defined(CONFIG_DEBUG_FS) return 0; #else /* !CONFIG_DEBUG_FS */ dentry = debugfs_create_file("fw_trace_enable_mask", 0644, kbdev->mali_debugfs_directory, kbdev, &kbase_csf_firmware_log_enable_mask_fops); if (IS_ERR_OR_NULL(dentry)) { dev_err(kbdev->dev, "Unable to create fw_trace_enable_mask\n"); err = -ENOENT; goto free_out; } dentry = debugfs_create_file("fw_traces", 0444, kbdev->mali_debugfs_directory, kbdev, &kbasep_csf_firmware_log_debugfs_fops); if (IS_ERR_OR_NULL(dentry)) { dev_err(kbdev->dev, "Unable to create fw_traces\n"); err = -ENOENT; goto free_out; } dentry = debugfs_create_file("fw_trace_mode", 0644, kbdev->mali_debugfs_directory, kbdev, &kbase_csf_firmware_log_mode_fops); if (IS_ERR_OR_NULL(dentry)) { dev_err(kbdev->dev, "Unable to create fw_trace_mode\n"); err = -ENOENT; goto free_out; } dentry = debugfs_create_file("fw_trace_poll_period_ms", 0644, kbdev->mali_debugfs_directory, kbdev, &kbase_csf_firmware_log_poll_period_fops); if (IS_ERR_OR_NULL(dentry)) { dev_err(kbdev->dev, "Unable to create fw_trace_poll_period_ms"); err = -ENOENT; goto free_out; } return 0; free_out: kfree(fw_log->dump_buf); fw_log->dump_buf = NULL; #endif /* CONFIG_DEBUG_FS */ out: return err; } void kbase_csf_firmware_log_term(struct kbase_device *kbdev) { struct kbase_csf_firmware_log *fw_log = &kbdev->csf.fw_log; if (fw_log->dump_buf) { cancel_delayed_work_sync(&fw_log->poll_work); kfree(fw_log->dump_buf); fw_log->dump_buf = NULL; } } void kbase_csf_firmware_log_dump_buffer(struct kbase_device *kbdev) { struct kbase_csf_firmware_log *fw_log = &kbdev->csf.fw_log; u8 *buf = fw_log->dump_buf, *p, *pnewline, *pend, *pendbuf; unsigned int read_size, remaining_size; struct firmware_trace_buffer *tb = kbase_csf_firmware_get_trace_buffer(kbdev, KBASE_CSFFW_LOG_BUF_NAME); if (tb == NULL) { dev_dbg(kbdev->dev, "Can't get the trace buffer, firmware trace dump skipped"); return; } if (atomic_cmpxchg(&fw_log->busy, 0, 1) != 0) return; /* FW should only print complete messages, so there's no need to handle * partial messages over multiple invocations of this function */ p = buf; pendbuf = &buf[FIRMWARE_LOG_DUMP_BUF_SIZE]; while ((read_size = kbase_csf_firmware_trace_buffer_read_data(tb, p, pendbuf - p))) { pend = p + read_size; p = buf; while (p < pend && (pnewline = memchr(p, '\n', (size_t)(pend - p)))) { /* Null-terminate the string */ *pnewline = 0; dev_err(kbdev->dev, "FW> %s", p); p = pnewline + 1; } remaining_size = pend - p; if (!remaining_size) { p = buf; } else if (remaining_size < FIRMWARE_LOG_DUMP_BUF_SIZE) { /* Copy unfinished string to the start of the buffer */ memmove(buf, p, remaining_size); p = &buf[remaining_size]; } else { /* Print abnormally long string without newlines */ dev_err(kbdev->dev, "FW> %s", buf); p = buf; } } if (p != buf) { /* Null-terminate and print last unfinished string */ *p = 0; dev_err(kbdev->dev, "FW> %s", buf); } atomic_set(&fw_log->busy, 0); } void kbase_csf_firmware_log_parse_logging_call_list_entry(struct kbase_device *kbdev, const uint32_t *entry) { kbdev->csf.fw_log.func_call_list_va_start = entry[0]; kbdev->csf.fw_log.func_call_list_va_end = entry[1]; } /** * toggle_logging_calls_in_loaded_image - Toggles FW log func calls in loaded FW image. * * @kbdev: Instance of a GPU platform device that implements a CSF interface. * @enable: Whether to enable or disable the function calls. */ static void toggle_logging_calls_in_loaded_image(struct kbase_device *kbdev, bool enable) { uint32_t bl_instruction, diff; uint32_t imm11, imm10, i1, i2, j1, j2, sign; uint32_t calling_address = 0, callee_address = 0; uint32_t list_entry = kbdev->csf.fw_log.func_call_list_va_start; const uint32_t list_va_end = kbdev->csf.fw_log.func_call_list_va_end; if (list_entry == 0 || list_va_end == 0) return; if (enable) { for (; list_entry < list_va_end; list_entry += 2 * sizeof(uint32_t)) { /* Read calling address */ kbase_csf_read_firmware_memory(kbdev, list_entry, &calling_address); /* Read callee address */ kbase_csf_read_firmware_memory(kbdev, list_entry + sizeof(uint32_t), &callee_address); diff = callee_address - calling_address - 4; sign = !!(diff & 0x80000000); if (ARMV7_T1_BL_IMM_RANGE_MIN > (int32_t)diff || ARMV7_T1_BL_IMM_RANGE_MAX < (int32_t)diff) { dev_warn(kbdev->dev, "FW log patch 0x%x out of range, skipping", calling_address); continue; } i1 = (diff & 0x00800000) >> 23; j1 = !i1 ^ sign; i2 = (diff & 0x00400000) >> 22; j2 = !i2 ^ sign; imm11 = (diff & 0xffe) >> 1; imm10 = (diff & 0x3ff000) >> 12; /* Compose BL instruction */ bl_instruction = ARMV7_T1_BL_IMM_INSTR; bl_instruction |= j1 << 29; bl_instruction |= j2 << 27; bl_instruction |= imm11 << 16; bl_instruction |= sign << 10; bl_instruction |= imm10; /* Patch logging func calls in their load location */ dev_dbg(kbdev->dev, "FW log patch 0x%x: 0x%x\n", calling_address, bl_instruction); kbase_csf_update_firmware_memory_exe(kbdev, calling_address, bl_instruction); } } else { for (; list_entry < list_va_end; list_entry += 2 * sizeof(uint32_t)) { /* Read calling address */ kbase_csf_read_firmware_memory(kbdev, list_entry, &calling_address); /* Overwrite logging func calls with 2 NOP instructions */ kbase_csf_update_firmware_memory_exe(kbdev, calling_address, ARMV7_DOUBLE_NOP_INSTR); } } } int kbase_csf_firmware_log_toggle_logging_calls(struct kbase_device *kbdev, u32 val) { unsigned long flags; struct kbase_csf_firmware_log *fw_log = &kbdev->csf.fw_log; bool mcu_inactive; bool resume_needed = false; int ret = 0; struct kbase_csf_scheduler *scheduler = &kbdev->csf.scheduler; if (atomic_cmpxchg(&fw_log->busy, 0, 1) != 0) return -EBUSY; /* Suspend all the active CS groups */ dev_dbg(kbdev->dev, "Suspend all the active CS groups"); kbase_csf_scheduler_lock(kbdev); while (scheduler->state != SCHED_SUSPENDED) { kbase_csf_scheduler_unlock(kbdev); kbase_csf_scheduler_pm_suspend(kbdev); kbase_csf_scheduler_lock(kbdev); resume_needed = true; } /* Wait for the MCU to get disabled */ dev_info(kbdev->dev, "Wait for the MCU to get disabled"); ret = kbase_pm_killable_wait_for_desired_state(kbdev); if (ret) { dev_err(kbdev->dev, "wait for PM state failed when toggling FW logging calls"); ret = -EAGAIN; goto out; } spin_lock_irqsave(&kbdev->hwaccess_lock, flags); mcu_inactive = kbase_pm_is_mcu_inactive(kbdev, kbdev->pm.backend.mcu_state); spin_unlock_irqrestore(&kbdev->hwaccess_lock, flags); if (!mcu_inactive) { dev_err(kbdev->dev, "MCU not inactive after PM state wait when toggling FW logging calls"); ret = -EAGAIN; goto out; } /* Toggle FW logging call in the loaded FW image */ toggle_logging_calls_in_loaded_image(kbdev, val); dev_dbg(kbdev->dev, "FW logging: %s", val ? "enabled" : "disabled"); out: kbase_csf_scheduler_unlock(kbdev); if (resume_needed) /* Resume queue groups and start mcu */ kbase_csf_scheduler_pm_resume(kbdev); atomic_set(&fw_log->busy, 0); return ret; }