| // 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"); |