blob: bfcc6c87d363379d83b86a0a70d839127d548d26 [file] [log] [blame]
// SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note
/*
*
* (C) COPYRIGHT 2022 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 <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>
#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, FIRMWARE_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, FIRMWARE_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 & ((1 << enable_bits_count) - 1);
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, FIRMWARE_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 */
if (fw_log->mode != KBASE_CSF_FIRMWARE_LOG_MODE_MANUAL) {
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:
schedule_delayed_work(&fw_log->poll_work,
msecs_to_jiffies(KBASE_CSF_FIRMWARE_LOG_POLL_PERIOD_MS));
break;
default:
ret = -EINVAL;
goto out;
}
fw_log->mode = val;
out:
atomic_set(&fw_log->busy, 0);
return ret;
}
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");
#endif /* CONFIG_DEBUG_FS */
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;
schedule_delayed_work(&fw_log->poll_work,
msecs_to_jiffies(KBASE_CSF_FIRMWARE_LOG_POLL_PERIOD_MS));
kbase_csf_firmware_log_dump_buffer(kbdev);
}
int kbase_csf_firmware_log_init(struct kbase_device *kbdev)
{
struct kbase_csf_firmware_log *fw_log = &kbdev->csf.fw_log;
/* 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)
return -ENOMEM;
/* Ensure null-termination for all strings */
fw_log->dump_buf[FIRMWARE_LOG_DUMP_BUF_SIZE] = 0;
fw_log->mode = KBASE_CSF_FIRMWARE_LOG_MODE_MANUAL;
atomic_set(&fw_log->busy, 0);
INIT_DEFERRABLE_WORK(&fw_log->poll_work, kbase_csf_firmware_log_poll);
#if defined(CONFIG_DEBUG_FS)
debugfs_create_file("fw_trace_enable_mask", 0644, kbdev->mali_debugfs_directory, kbdev,
&kbase_csf_firmware_log_enable_mask_fops);
debugfs_create_file("fw_traces", 0444, kbdev->mali_debugfs_directory, kbdev,
&kbasep_csf_firmware_log_debugfs_fops);
debugfs_create_file("fw_trace_mode", 0644, kbdev->mali_debugfs_directory, kbdev,
&kbase_csf_firmware_log_mode_fops);
#endif /* CONFIG_DEBUG_FS */
return 0;
}
void kbase_csf_firmware_log_term(struct kbase_device *kbdev)
{
struct kbase_csf_firmware_log *fw_log = &kbdev->csf.fw_log;
cancel_delayed_work_sync(&fw_log->poll_work);
kfree(fw_log->dump_buf);
}
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, FIRMWARE_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);
}