| // 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 <mali_kbase.h> |
| #include "backend/gpu/mali_kbase_pm_internal.h" |
| #include <csf/mali_kbase_csf_firmware_log.h> |
| #include <csf/mali_kbase_csf_trace_buffer.h> |
| #include <linux/debugfs.h> |
| #include <linux/string.h> |
| #include <linux/workqueue.h> |
| |
| /* |
| * 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 = 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(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 = 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(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', 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_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; |
| } |