blob: 1ae8b40c96c53ffc414070f8a0933cb6489b6374 [file] [log] [blame]
// SPDX-License-Identifier: GPL-2.0-only
/* Copyright 2020 Google LLC. All Rights Reserved.
*
* Interface to the AoC control service
*/
#define pr_fmt(fmt) "aoc_control: " fmt
#include <linux/delay.h>
#include <linux/device.h>
#include <linux/jiffies.h>
#include <linux/kernel.h>
#include <linux/kthread.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/types.h>
#include "aoc.h"
#include "aoc-interface.h"
#define AOC_CONTROL_NAME "aoc_stats"
#define AOC_SERVICE_NAME "control"
struct aoc_stat {
char name[8];
u8 type;
};
struct stats_prvdata {
/* Exclusive access to the channel for attribute transactions */
struct mutex lock;
struct work_struct discovery_work;
struct aoc_service_dev *service;
struct aoc_stat *discovered_stats;
int total_stats;
long service_timeout;
};
/* Driver methods */
/*
* Convenience method to send a write/read combination to the AoC.
* Returns # of bytes returned, or negative codes for errors.
*/
static ssize_t read_attribute(struct stats_prvdata *prvdata, void *in_cmd,
size_t in_size, void *out_cmd, size_t out_size)
{
struct aoc_service_dev *service = prvdata->service;
ssize_t ret;
ret = mutex_lock_interruptible(&prvdata->lock);
if (ret != 0)
return ret;
if (aoc_service_flush_read_data(service))
pr_err("Previous response left in channel\n");
ret = aoc_service_write_timeout(service, in_cmd, in_size, prvdata->service_timeout);
if (ret < 0)
goto out;
ret = aoc_service_read_timeout(service, out_cmd, out_size, prvdata->service_timeout);
out:
mutex_unlock(&prvdata->lock);
return ret;
}
static const char * const service_names[] = {
AOC_SERVICE_NAME,
NULL,
};
static ssize_t read_core_build_info(int core, struct device *dev, char *buf)
{
struct stats_prvdata *prvdata = dev_get_drvdata(dev);
struct CMD_SYS_VERSION_GET version_get = { 0 };
int ret;
AocCmdHdrSet(&version_get.parent.parent, CMD_SYS_VERSION_GET_ID,
sizeof(version_get));
version_get.parent.core = core;
ret = read_attribute(prvdata, &version_get, sizeof(version_get),
&version_get, sizeof(version_get));
if (ret < 0)
return ret;
if (strnlen(version_get.version, sizeof(version_get.version)) ==
sizeof(version_get.version))
dev_err(dev, "invalid version string returned\n");
if (strnlen(version_get.link_time, sizeof(version_get.link_time)) ==
sizeof(version_get.link_time))
dev_err(dev, "invalid link time string returned\n");
ret = scnprintf(buf, PAGE_SIZE, "Build Hash: %.64s\nLink Time: %.64s\n",
version_get.version, version_get.link_time);
return ret;
}
static ssize_t a32_build_info_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
return read_core_build_info(1, dev, buf);
}
static DEVICE_ATTR_RO(a32_build_info);
static ssize_t f1_build_info_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
return read_core_build_info(2, dev, buf);
}
static DEVICE_ATTR_RO(f1_build_info);
static ssize_t hf_build_info_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
return read_core_build_info(3, dev, buf);
}
static DEVICE_ATTR_RO(hf_build_info);
static ssize_t memory_exception_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct stats_prvdata *prvdata = dev_get_drvdata(dev);
struct CMD_GET_MEMORY_EXCEPTION_DATA_COUNT get_count_cmd;
struct CMD_GET_MEMORY_EXCEPTION_DATA get_data_cmd;
u32 total_count;
u32 i;
u32 bytes_written;
int ret;
AocCmdHdrSet(&get_count_cmd.parent, CMD_GET_MEMORY_EXCEPTION_DATA_COUNT_ID,
sizeof(get_count_cmd));
ret = read_attribute(prvdata, &get_count_cmd, sizeof(get_count_cmd),
&get_count_cmd, sizeof(get_count_cmd));
if (ret < 0)
return ret;
total_count = get_count_cmd.num_memory_exception;
if (total_count > 16) {
total_count = 16;
}
bytes_written = 0;
for (i = 0; i < total_count; i++) {
AocCmdHdrSet(&get_data_cmd.parent, CMD_GET_MEMORY_EXCEPTION_DATA_ID,
sizeof(get_data_cmd));
get_data_cmd.log_index = i;
get_data_cmd.valid = false;
ret = read_attribute(prvdata, &get_data_cmd, sizeof(get_data_cmd),
&get_data_cmd, sizeof(get_data_cmd));
if (ret < 0)
return ret;
if (get_data_cmd.valid) {
bytes_written += scnprintf(buf + bytes_written, PAGE_SIZE - bytes_written,
"index = %u, id = %u, return_address = 0x%x, fault_address = 0x%x, task_name = %s\n",
i, get_data_cmd.id, get_data_cmd.return_address,
get_data_cmd.fault_address, get_data_cmd.task_name);
}
}
return bytes_written;
}
static DEVICE_ATTR_RO(memory_exception);
static ssize_t memory_votes_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct stats_prvdata *prvdata = dev_get_drvdata(dev);
struct CMD_GET_MEMORY_VOTES_DATA_COUNT get_count_cmd;
struct CMD_GET_MEMORY_VOTES_DATA get_data_cmd;
u32 total_count;
u32 i;
u32 bytes_written;
int ret;
AocCmdHdrSet(&get_count_cmd.parent, CMD_GET_MEMORY_VOTES_DATA_COUNT_ID,
sizeof(get_count_cmd));
ret = read_attribute(prvdata, &get_count_cmd, sizeof(get_count_cmd),
&get_count_cmd, sizeof(get_count_cmd));
if (ret < 0)
return ret;
total_count = get_count_cmd.num_of_clients;
bytes_written = 0;
for (i = 0; i < total_count; i++) {
AocCmdHdrSet(&get_data_cmd.parent, CMD_GET_MEMORY_VOTES_DATA_ID, sizeof(get_data_cmd));
get_data_cmd.app_id = i;
get_data_cmd.valid = false;
ret = read_attribute(prvdata, &get_data_cmd, sizeof(get_data_cmd),
&get_data_cmd, sizeof(get_data_cmd));
if (ret < 0)
return ret;
if (get_data_cmd.valid) {
bytes_written += scnprintf(buf + bytes_written, PAGE_SIZE - bytes_written,
"App %hhu, votes Curr/Tot/ON %5u / %5u / %5u Last %10llu us, Dur %10llu us\n",
get_data_cmd.app_id,
get_data_cmd.votes,
get_data_cmd.total_votes,
get_data_cmd.on_votes,
get_data_cmd.last_on_time_ns / 1000ULL,
get_data_cmd.total_time_us);
}
}
return bytes_written;
}
static DEVICE_ATTR_RO(memory_votes);
/* Driver methods */
/*
* Convenience method to send a write to the AoC.
* Returns negative codes for errors.
*/
static ssize_t write_attribute(struct stats_prvdata *prvdata, void *in_cmd,
size_t in_size)
{
struct aoc_service_dev *service = prvdata->service;
ssize_t ret;
ret = mutex_lock_interruptible(&prvdata->lock);
if (ret != 0)
return ret;
if (aoc_service_flush_read_data(service))
pr_err("Previous response left in channel\n");
ret = aoc_service_write_timeout(service, in_cmd, in_size, prvdata->service_timeout);
mutex_unlock(&prvdata->lock);
return ret;
}
static ssize_t udfps_set_clock_source_store(struct device *dev,
struct device_attribute *attr, const char *buf, size_t count)
{
struct stats_prvdata *prvdata = dev_get_drvdata(dev);
struct CMD_UDFPS_SET_CLOCK_SOURCE clock_src = { 0 };
uint8_t type;
int ret;
AocCmdHdrSet(&clock_src.parent, CMD_UDFPS_SET_CLOCK_SOURCE_ID,
sizeof(clock_src));
ret = kstrtou8(buf, 10, &type);
if (ret < 0)
return ret;
if (type < SOURCE_TOT)
clock_src.clock_source = type;
else
dev_err(dev, "Invalid input parameter = %d\n", type);
ret = write_attribute(prvdata, &clock_src, sizeof(clock_src));
if (ret < 0) {
dev_err(dev, "udfps freq start ret = %d\n", ret);
}
return ret;
}
static DEVICE_ATTR_WO(udfps_set_clock_source);
static ssize_t udfps_get_clock_frequency(uint8_t clk_src, struct device *dev, char *buf)
{
struct stats_prvdata *prvdata = dev_get_drvdata(dev);
struct CMD_UDFPS_GET_CLOCK_FREQUENCY get_freq_cmd = { 0 };
int ret;
AocCmdHdrSet(&get_freq_cmd.parent, CMD_UDFPS_GET_CLOCK_FREQUENCY_ID,
sizeof(get_freq_cmd));
get_freq_cmd.clock_source = clk_src;
ret = read_attribute(prvdata, &get_freq_cmd, sizeof(get_freq_cmd),
&get_freq_cmd, sizeof(get_freq_cmd));
if (ret < 0)
return ret;
return scnprintf(buf, PAGE_SIZE, "%u\n", get_freq_cmd.clock_freq_in_u32);
}
static ssize_t udfps_get_osc_freq_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
return udfps_get_clock_frequency(0, dev, buf);
}
static DEVICE_ATTR_RO(udfps_get_osc_freq);
static ssize_t udfps_get_disp_freq_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
return udfps_get_clock_frequency(1, dev, buf);
}
static DEVICE_ATTR_RO(udfps_get_disp_freq);
static ssize_t read_timed_stat(struct device *dev, char *buf, int index)
{
struct stats_prvdata *prvdata = dev_get_drvdata(dev);
struct CMD_SYS_STATS_TIMED_GET get_stat = { 0 };
int ret;
AocCmdHdrSet(&get_stat.parent.parent, CMD_SYS_STATS_TIMED_GET_ID,
sizeof(get_stat));
get_stat.index = index;
ret = read_attribute(prvdata, &get_stat, sizeof(get_stat), &get_stat,
sizeof(get_stat));
/* Partial response */
if (ret >= 0 && ret < sizeof(get_stat))
ret = -EIO;
if (ret < 0)
goto out;
ret = scnprintf(buf, PAGE_SIZE,
"Counter: %llu\nCumulative time: %llu\nTime last entered: %llu\nTime last exited: %llu\n",
le64_to_cpu(get_stat.timed.counter),
le64_to_cpu(get_stat.timed.cumulative_time),
le64_to_cpu(get_stat.timed.timestamp_enter_last),
le64_to_cpu(get_stat.timed.timestamp_exit_last));
out:
return ret;
}
static ssize_t read_data_stat(struct device *dev, char *buf, int index)
{
struct stats_prvdata *prvdata = dev_get_drvdata(dev);
struct CMD_SYS_STATS_DATA_GET get_stat = { 0 };
int ret;
AocCmdHdrSet(&get_stat.parent.parent, CMD_SYS_STATS_DATA_GET_ID,
sizeof(get_stat));
get_stat.index = index;
ret = read_attribute(prvdata, &get_stat, sizeof(get_stat), &get_stat,
sizeof(get_stat));
/* Partial response */
if (ret >= 0 && ret < sizeof(get_stat))
ret = -EIO;
if (ret < 0)
goto out;
ret = scnprintf(buf, PAGE_SIZE,
"Counter: %llu\nFailed: %llu\nTx: %llu\nRx: %llu\n",
le64_to_cpu(get_stat.transfer.counter),
le64_to_cpu(get_stat.transfer.counter_failed),
le64_to_cpu(get_stat.transfer.transfer_tx),
le64_to_cpu(get_stat.transfer.transfer_rx));
out:
return ret;
}
static ssize_t read_stat_by_name(struct device *dev, char *buf,
const char *name)
{
struct stats_prvdata *prvdata = dev_get_drvdata(dev);
int i;
if (!prvdata)
return 0;
for (i = 0; i < prvdata->total_stats; i++) {
if (strcmp(name, prvdata->discovered_stats[i].name))
continue;
switch (prvdata->discovered_stats[i].type) {
case 0:
dev_dbg(dev, "Reading timed stat %d (%s)\n", i, name);
return read_timed_stat(dev, buf, i);
case 1:
dev_dbg(dev, "Reading transfer stat %d (%s)\n", i,
name);
return read_data_stat(dev, buf, i);
default:
dev_err(dev, "Unknown stats type %d\n",
prvdata->discovered_stats[i].type);
return 0;
}
}
dev_err(dev, "failed to find stat with name %s\n", name);
return 0;
}
#define DECLARE_STAT(stat_name, sysfs_name) \
static ssize_t sysfs_name##_show( \
struct device *dev, struct device_attribute *attr, char *buf) \
{ \
return read_stat_by_name(dev, buf, stat_name); \
} \
static DEVICE_ATTR_RO(sysfs_name)
DECLARE_STAT("A3-WFI", a32_wfi);
DECLARE_STAT("A3-RET", a32_retention);
DECLARE_STAT("A3-DWN", a32_off);
DECLARE_STAT("F1-WFI", ff1_wfi);
DECLARE_STAT("F1-RET", ff1_retention);
DECLARE_STAT("F1-DWN", ff1_off);
DECLARE_STAT("H0-WFI", hf0_wfi);
DECLARE_STAT("H0-RET", hf0_retention);
DECLARE_STAT("H0-DWN", hf0_off);
DECLARE_STAT("H1-WFI", hf1_wfi);
DECLARE_STAT("H1-RET", hf1_retention);
DECLARE_STAT("H1-DWN", hf1_off);
DECLARE_STAT("AC-MON", monitor_mode);
DECLARE_STAT("I2C0", i2c0);
DECLARE_STAT("I2C1", i2c1);
DECLARE_STAT("I2C2", i2c2);
DECLARE_STAT("I2C3", i2c3);
DECLARE_STAT("I2C4", i2c4);
DECLARE_STAT("SPI0", spi0);
DECLARE_STAT("SPI1", spi1);
DECLARE_STAT("I3C0", i3c0);
DECLARE_STAT("PDM", pdm);
DECLARE_STAT("PDM-SW", pdm_16khz);
DECLARE_STAT("TDM", tdm);
DECLARE_STAT("MIF_UP", mif_up);
DECLARE_STAT("SLC_UP", slc_up);
DECLARE_STAT("SLEEP", sleep);
DECLARE_STAT("SICD", sicd);
DECLARE_STAT("V_NOM", voltage_nominal);
DECLARE_STAT("V_UD", voltage_underdrive);
DECLARE_STAT("V_SUD", voltage_super_underdrive);
DECLARE_STAT("V_UUD", voltage_ultra_underdrive);
DECLARE_STAT("RING_W", ring_buffer_wakeup);
DECLARE_STAT("HOST_W", host_ipc_wakeup);
DECLARE_STAT("USF_W", usf_wakeup);
DECLARE_STAT("AUD_W", audio_wakeup);
DECLARE_STAT("LOG_W", logging_wakeup);
DECLARE_STAT("WORD_W", hotword_wakeup);
static struct attribute *aoc_stats_attrs[] = {
&dev_attr_a32_build_info.attr,
&dev_attr_f1_build_info.attr,
&dev_attr_hf_build_info.attr,
&dev_attr_a32_wfi.attr,
&dev_attr_a32_retention.attr,
&dev_attr_a32_off.attr,
&dev_attr_ff1_wfi.attr,
&dev_attr_ff1_retention.attr,
&dev_attr_ff1_off.attr,
&dev_attr_hf0_wfi.attr,
&dev_attr_hf0_retention.attr,
&dev_attr_hf0_off.attr,
&dev_attr_hf1_wfi.attr,
&dev_attr_hf1_retention.attr,
&dev_attr_hf1_off.attr,
&dev_attr_monitor_mode.attr,
&dev_attr_i2c0.attr,
&dev_attr_i2c1.attr,
&dev_attr_i2c2.attr,
&dev_attr_i2c3.attr,
&dev_attr_i2c4.attr,
&dev_attr_spi0.attr,
&dev_attr_spi1.attr,
&dev_attr_i3c0.attr,
&dev_attr_pdm.attr,
&dev_attr_pdm_16khz.attr,
&dev_attr_tdm.attr,
&dev_attr_mif_up.attr,
&dev_attr_slc_up.attr,
&dev_attr_sleep.attr,
&dev_attr_sicd.attr,
&dev_attr_voltage_nominal.attr,
&dev_attr_voltage_underdrive.attr,
&dev_attr_voltage_super_underdrive.attr,
&dev_attr_voltage_ultra_underdrive.attr,
&dev_attr_ring_buffer_wakeup.attr,
&dev_attr_host_ipc_wakeup.attr,
&dev_attr_usf_wakeup.attr,
&dev_attr_audio_wakeup.attr,
&dev_attr_logging_wakeup.attr,
&dev_attr_hotword_wakeup.attr,
&dev_attr_memory_exception.attr,
&dev_attr_memory_votes.attr,
&dev_attr_udfps_set_clock_source.attr,
&dev_attr_udfps_get_osc_freq.attr,
&dev_attr_udfps_get_disp_freq.attr,
NULL
};
ATTRIBUTE_GROUPS(aoc_stats);
static int aoc_control_map_handler(u32 handle, phys_addr_t p, size_t size,
bool mapped, void *ctx)
{
struct device *dev = ctx;
struct stats_prvdata *prvdata = dev_get_drvdata(dev);
struct CMD_MEM_MAP_REGION mem_map_region = { 0 };
int ret;
AocCmdHdrSet(&mem_map_region.parent.parent, CMD_MEM_MAP_REGION_ID,
sizeof(mem_map_region));
mem_map_region.handle = handle;
mem_map_region.base = (u32)p;
mem_map_region.size = size;
mem_map_region.mapped = mapped;
ret = read_attribute(prvdata, &mem_map_region, sizeof(mem_map_region),
&mem_map_region, sizeof(mem_map_region));
if (ret < 0)
pr_notice("CMD_MEM_MAP_REGION_ID returned %d\n", ret);
return ret;
}
static void discovery_workitem(struct work_struct *work)
{
struct stats_prvdata *prvdata =
container_of(work, struct stats_prvdata, discovery_work);
struct CMD_SYS_STATS_TOT cmd_total;
struct device *dev = &prvdata->service->dev;
struct aoc_stat *stats;
int i, ret;
dev_dbg(dev, "started the discovery thread\n");
AocCmdHdrSet(&cmd_total.parent.parent, CMD_SYS_STATS_TOT_ID,
sizeof(cmd_total));
ret = read_attribute(prvdata, &cmd_total, sizeof(cmd_total), &cmd_total,
sizeof(cmd_total));
if (ret < sizeof(cmd_total)) {
dev_err(dev, "failed to read the number of statistics\n");
goto out;
}
if (cmd_total.tot > 256 || cmd_total.tot < 0) {
dev_err(dev, "invalid number of statistics %d\n",
cmd_total.tot);
goto out;
}
stats = devm_kcalloc(dev, cmd_total.tot, sizeof(struct aoc_stat),
GFP_KERNEL | __GFP_ZERO);
if (!stats)
goto out;
prvdata->total_stats = cmd_total.tot;
dev_dbg(dev, "Returned %d, %d stats", ret, prvdata->total_stats);
for (i = 0; i < prvdata->total_stats; i++) {
struct CMD_SYS_STATS_INFO_GET cmd_info;
memset(&cmd_info, 0, sizeof(cmd_info));
AocCmdHdrSet(&cmd_info.parent.parent, CMD_SYS_STATS_INFO_GET_ID,
sizeof(cmd_info));
cmd_info.index = i;
ret = read_attribute(prvdata, &cmd_info, sizeof(cmd_info),
&cmd_info, sizeof(cmd_info));
if (ret == sizeof(cmd_info)) {
stats[i].type = cmd_info.info.type;
memcpy(stats[i].name, cmd_info.info.name,
STATS_ENTRY_LEN);
dev_dbg(dev, "stat %d name %s type %d\n", i,
stats[i].name, stats[i].type);
} else {
dev_err(dev, "failed to read stat %d info: %d\n", i,
ret);
}
}
prvdata->discovered_stats = stats;
ret = device_add_groups(&prvdata->service->dev, aoc_stats_groups);
aoc_set_map_handler(prvdata->service, aoc_control_map_handler, dev);
out:
return;
}
static int aoc_control_probe(struct aoc_service_dev *sd)
{
struct device *dev = &sd->dev;
struct stats_prvdata *prvdata;
pr_debug("probe service with name %s\n", dev_name(dev));
prvdata = devm_kzalloc(dev, sizeof(*prvdata), GFP_KERNEL);
prvdata->service = sd;
prvdata->service_timeout = msecs_to_jiffies(100);
mutex_init(&prvdata->lock);
INIT_WORK(&prvdata->discovery_work, discovery_workitem);
dev_set_drvdata(dev, prvdata);
schedule_work(&prvdata->discovery_work);
return 0;
}
static int aoc_control_remove(struct aoc_service_dev *sd)
{
struct device *dev = &sd->dev;
struct stats_prvdata *prvdata = dev_get_drvdata(dev);
pr_debug("remove service with name %s\n", dev_name(dev));
cancel_work_sync(&prvdata->discovery_work);
device_remove_groups(dev, aoc_stats_groups);
aoc_remove_map_handler(prvdata->service);
return 0;
}
static struct aoc_driver aoc_control_driver = {
.drv = {
.name = AOC_CONTROL_NAME,
},
.service_names = service_names,
.probe = aoc_control_probe,
.remove = aoc_control_remove,
};
module_aoc_driver(aoc_control_driver);
MODULE_LICENSE("GPL v2");