On Thu, Mar 19, 2020 at 5:08 AM Blaž Hrastnik <blaz@xxxxxxx> wrote: > > I'm resubmitting this patch with review feedback addressed: Thank you for an update! Unfortunately it does not apply to our for-next branch. Care to rebase? > > https://patchwork.kernel.org/patch/10584079/ > > The patch was previously not resubmitted because it required a change > that was reverted in the ACPICA. That has since been corrected: > > https://github.com/acpica/acpica/commit/9159c09a2a5897a43f78c95cdffc160d399722c3 > > We've been using this patch for a while and user reports confirm that it > works: > > https://github.com/linux-surface/linux-surface > > Previous description follows. > > --- > > The MSHW0011 device is a chip that replaces the battery firmware > by using ACPI operation regions on the Surface 3. > It is unclear whether or not the chip will be reused somewhere else > (under Windows, the chip is called "Surface Platform Power Driver" > and the driver is provided by Microsoft). > > The values have been obtained by reverse engineering, and are subject to > errors. Looks like it works on overall pretty well. > > I couldn't manage to get the IRQ correctly triggered, so I am using a > good old polling thread to check for changes. This is something > to be fixed in a later version. > > Link: https://bugzilla.kernel.org/show_bug.cgi?id=106231 > > Signed-off-by: Blaž Hrastnik <blaz@xxxxxxx> > Signed-off-by: Benjamin Tissoires <benjamin.tissoires@xxxxxxxxxx> > Signed-off-by: Stephen Just <stephenjust@xxxxxxxxx> > --- > > changes in v2: > - moved to drivers/acpi/ instead of power > - use uuid_le > - fix uper/lower case > - print_hex_dump() used in mshw0011_dump_registers() instead of custom dump > - removed "MSHW0011:00" in mshw0011_id > > changes in v3: > - remove le16_to_cpu() after i2c_smbus_read_word_data() as it already > handles it properly > - use i2c_acpi_new_device() to remove a bunch of duplicated code from I2C > core. > - move the driver in platform/x86 > - add depends ACPI && I2C in Kconfig > - remove the dump registers facility, as it's debugging only > - use of BIT() in MSHW0011 defs > - use of guid_t > - remove useless ret = 0 > - use probe_new() > - remove empty .id_table > - removed mshw0011_i2c_read_block() and use directly > i2c_smbus_read_i2c_block_data() > - use snprintf() instead of custom memcpy() > - renamed the 'version' as 'mask' > - use SPDX license identifier > > changes in v4: > - address review feedback > --- > drivers/platform/x86/Kconfig | 7 + > drivers/platform/x86/Makefile | 1 + > drivers/platform/x86/surface3_power.c | 599 ++++++++++++++++++++++++++ > 3 files changed, 607 insertions(+) > create mode 100644 drivers/platform/x86/surface3_power.c > > diff --git a/drivers/platform/x86/Kconfig b/drivers/platform/x86/Kconfig > index 27d5b40fb717..96c516d1f0cd 100644 > --- a/drivers/platform/x86/Kconfig > +++ b/drivers/platform/x86/Kconfig > @@ -1212,6 +1212,13 @@ config SURFACE_3_BUTTON > ---help--- > This driver handles the power/home/volume buttons on the Microsoft Surface 3 tablet. > > +config SURFACE_3_POWER_OPREGION > + tristate "Surface 3 battery platform operation region support" > + depends on ACPI && I2C > + help > + This driver provides support for ACPI operation > + region of the Surface 3 battery platform driver. > + > config INTEL_PUNIT_IPC > tristate "Intel P-Unit IPC Driver" > ---help--- > diff --git a/drivers/platform/x86/Makefile b/drivers/platform/x86/Makefile > index 42d85a00be4e..d707a8edd738 100644 > --- a/drivers/platform/x86/Makefile > +++ b/drivers/platform/x86/Makefile > @@ -89,6 +89,7 @@ obj-$(CONFIG_INTEL_PMC_IPC) += intel_pmc_ipc.o > obj-$(CONFIG_TOUCHSCREEN_DMI) += touchscreen_dmi.o > obj-$(CONFIG_SURFACE_PRO3_BUTTON) += surfacepro3_button.o > obj-$(CONFIG_SURFACE_3_BUTTON) += surface3_button.o > +obj-$(CONFIG_SURFACE_3_POWER_OPREGION) += surface3_power.o > obj-$(CONFIG_INTEL_PUNIT_IPC) += intel_punit_ipc.o > obj-$(CONFIG_INTEL_BXTWC_PMIC_TMU) += intel_bxtwc_tmu.o > obj-$(CONFIG_INTEL_TELEMETRY) += intel_telemetry_core.o \ > diff --git a/drivers/platform/x86/surface3_power.c b/drivers/platform/x86/surface3_power.c > new file mode 100644 > index 000000000000..2e0e16b984a0 > --- /dev/null > +++ b/drivers/platform/x86/surface3_power.c > @@ -0,0 +1,599 @@ > +// SPDX-License-Identifier: GPL-2.0+ > +/* > + * Supports for the power IC on the Surface 3 tablet. > + * > + * (C) Copyright 2016-2018 Red Hat, Inc > + * (C) Copyright 2016-2018 Benjamin Tissoires <benjamin.tissoires@xxxxxxxxx> > + * (C) Copyright 2016 Stephen Just <stephenjust@xxxxxxxxx> > + * > + * This driver has been reverse-engineered by parsing the DSDT of the Surface 3 > + * and looking at the registers of the chips. > + * > + * The DSDT allowed to find out that: > + * - the driver is required for the ACPI BAT0 device to communicate to the chip > + * through an operation region. > + * - the various defines for the operation region functions to communicate with > + * this driver > + * - the DSM 3f99e367-6220-4955-8b0f-06ef2ae79412 allows to trigger ACPI > + * events to BAT0 (the code is all available in the DSDT). > + * > + * Further findings regarding the 2 chips declared in the MSHW0011 are: > + * - there are 2 chips declared: > + * . 0x22 seems to control the ADP1 line status (and probably the charger) > + * . 0x55 controls the battery directly > + * - the battery chip uses a SMBus protocol (using plain SMBus allows non > + * destructive commands): > + * . the commands/registers used are in the range 0x00..0x7F > + * . if bit 8 (0x80) is set in the SMBus command, the returned value is the > + * same as when it is not set. There is a high chance this bit is the > + * read/write > + * . the various registers semantic as been deduced by observing the register > + * dumps. > + */ > + > +#include <linux/acpi.h> > +#include <linux/freezer.h> > +#include <linux/i2c.h> > +#include <linux/kernel.h> > +#include <linux/kthread.h> > +#include <linux/slab.h> > +#include <linux/uuid.h> > +#include <asm/unaligned.h> > + > +#define POLL_INTERVAL (2 * HZ) > +#define SURFACE_3_STRLEN 10 > + > +struct mshw0011_data { > + struct i2c_client *adp1; > + struct i2c_client *bat0; > + unsigned short notify_mask; > + struct task_struct *poll_task; > + bool kthread_running; > + > + bool charging; > + bool bat_charging; > + u8 trip_point; > + s32 full_capacity; > +}; > + > +struct mshw0011_lookup { > + struct mshw0011_data *cdata; > + unsigned int n; > + unsigned int index; > + int addr; > +}; > + > +struct mshw0011_handler_data { > + struct acpi_connection_info info; > + struct i2c_client *client; > +}; > + > +struct bix { > + u32 revision; > + u32 power_unit; > + u32 design_capacity; > + u32 last_full_charg_capacity; > + u32 battery_technology; > + u32 design_voltage; > + u32 design_capacity_of_warning; > + u32 design_capacity_of_low; > + u32 cycle_count; > + u32 measurement_accuracy; > + u32 max_sampling_time; > + u32 min_sampling_time; > + u32 max_average_interval; > + u32 min_average_interval; > + u32 battery_capacity_granularity_1; > + u32 battery_capacity_granularity_2; > + char model[SURFACE_3_STRLEN]; > + char serial[SURFACE_3_STRLEN]; > + char type[SURFACE_3_STRLEN]; > + char OEM[SURFACE_3_STRLEN]; > +} __packed; > + > +struct bst { > + u32 battery_state; > + s32 battery_present_rate; > + u32 battery_remaining_capacity; > + u32 battery_present_voltage; > +} __packed; > + > +struct gsb_command { > + u8 arg0; > + u8 arg1; > + u8 arg2; > +} __packed; > + > +struct gsb_buffer { > + u8 status; > + u8 len; > + u8 ret; > + union { > + struct gsb_command cmd; > + struct bst bst; > + struct bix bix; > + } __packed; > +} __packed; > + > +#define ACPI_BATTERY_STATE_DISCHARGING BIT(0) > +#define ACPI_BATTERY_STATE_CHARGING BIT(1) > +#define ACPI_BATTERY_STATE_CRITICAL BIT(2) > + > +#define MSHW0011_CMD_DEST_BAT0 0x01 > +#define MSHW0011_CMD_DEST_ADP1 0x03 > + > +#define MSHW0011_CMD_BAT0_STA 0x01 > +#define MSHW0011_CMD_BAT0_BIX 0x02 > +#define MSHW0011_CMD_BAT0_BCT 0x03 > +#define MSHW0011_CMD_BAT0_BTM 0x04 > +#define MSHW0011_CMD_BAT0_BST 0x05 > +#define MSHW0011_CMD_BAT0_BTP 0x06 > +#define MSHW0011_CMD_ADP1_PSR 0x07 > +#define MSHW0011_CMD_BAT0_PSOC 0x09 > +#define MSHW0011_CMD_BAT0_PMAX 0x0a > +#define MSHW0011_CMD_BAT0_PSRC 0x0b > +#define MSHW0011_CMD_BAT0_CHGI 0x0c > +#define MSHW0011_CMD_BAT0_ARTG 0x0d > + > +#define MSHW0011_NOTIFY_GET_VERSION 0x00 > +#define MSHW0011_NOTIFY_ADP1 0x01 > +#define MSHW0011_NOTIFY_BAT0_BST 0x02 > +#define MSHW0011_NOTIFY_BAT0_BIX 0x05 > + > +#define MSHW0011_ADP1_REG_PSR 0x04 > + > +#define MSHW0011_BAT0_REG_CAPACITY 0x0c > +#define MSHW0011_BAT0_REG_FULL_CHG_CAPACITY 0x0e > +#define MSHW0011_BAT0_REG_DESIGN_CAPACITY 0x40 > +#define MSHW0011_BAT0_REG_VOLTAGE 0x08 > +#define MSHW0011_BAT0_REG_RATE 0x14 > +#define MSHW0011_BAT0_REG_OEM 0x45 > +#define MSHW0011_BAT0_REG_TYPE 0x4e > +#define MSHW0011_BAT0_REG_SERIAL_NO 0x56 > +#define MSHW0011_BAT0_REG_CYCLE_CNT 0x6e > + > +#define MSHW0011_EV_2_5_MASK GENMASK(8, 0) > + > +static const guid_t mshw0011_guid = > + GUID_INIT(0x3F99E367, 0x6220, 0x4955, > + 0x8B, 0x0F, 0x06, 0xEF, 0x2A, 0xE7, 0x94, 0x12); > + > +static int > +mshw0011_notify(struct mshw0011_data *cdata, u8 arg1, u8 arg2, > + unsigned int *ret_value) > +{ > + union acpi_object *obj; > + struct acpi_device *adev; > + acpi_handle handle; > + unsigned int i; > + > + handle = ACPI_HANDLE(&cdata->adp1->dev); > + if (!handle || acpi_bus_get_device(handle, &adev)) > + return -ENODEV; > + > + obj = acpi_evaluate_dsm_typed(handle, &mshw0011_guid, arg1, arg2, NULL, > + ACPI_TYPE_BUFFER); > + if (!obj) { > + dev_err(&cdata->adp1->dev, "device _DSM execution failed\n"); > + return -ENODEV; > + } > + > + *ret_value = 0; > + for (i = 0; i < obj->buffer.length; i++) > + *ret_value |= obj->buffer.pointer[i] << (i * 8); > + > + ACPI_FREE(obj); > + return 0; > +} > + > +static const struct bix default_bix = { > + .revision = 0x00, > + .power_unit = 0x01, > + .design_capacity = 0x1dca, > + .last_full_charg_capacity = 0x1dca, > + .battery_technology = 0x01, > + .design_voltage = 0x10df, > + .design_capacity_of_warning = 0x8f, > + .design_capacity_of_low = 0x47, > + .cycle_count = 0xffffffff, > + .measurement_accuracy = 0x00015f90, > + .max_sampling_time = 0x03e8, > + .min_sampling_time = 0x03e8, > + .max_average_interval = 0x03e8, > + .min_average_interval = 0x03e8, > + .battery_capacity_granularity_1 = 0x45, > + .battery_capacity_granularity_2 = 0x11, > + .model = "P11G8M", > + .serial = "", > + .type = "LION", > + .OEM = "", > +}; > + > +static int mshw0011_bix(struct mshw0011_data *cdata, struct bix *bix) > +{ > + struct i2c_client *client = cdata->bat0; > + char buf[SURFACE_3_STRLEN]; > + int ret; > + > + *bix = default_bix; > + > + /* get design capacity */ > + ret = i2c_smbus_read_word_data(client, > + MSHW0011_BAT0_REG_DESIGN_CAPACITY); > + if (ret < 0) { > + dev_err(&client->dev, "Error reading design capacity: %d\n", > + ret); > + return ret; > + } > + bix->design_capacity = ret; > + > + /* get last full charge capacity */ > + ret = i2c_smbus_read_word_data(client, > + MSHW0011_BAT0_REG_FULL_CHG_CAPACITY); > + if (ret < 0) { > + dev_err(&client->dev, > + "Error reading last full charge capacity: %d\n", ret); > + return ret; > + } > + bix->last_full_charg_capacity = ret; > + > + /* get serial number */ > + ret = i2c_smbus_read_i2c_block_data(client, MSHW0011_BAT0_REG_SERIAL_NO, > + sizeof(buf), buf); > + if (ret != sizeof(buf)) { > + dev_err(&client->dev, "Error reading serial no: %d\n", ret); > + return ret; > + } > + snprintf(bix->serial, ARRAY_SIZE(bix->serial), > + "%3pE%6pE", buf + 7, buf); > + > + /* get cycle count */ > + ret = i2c_smbus_read_word_data(client, MSHW0011_BAT0_REG_CYCLE_CNT); > + if (ret < 0) { > + dev_err(&client->dev, "Error reading cycle count: %d\n", ret); > + return ret; > + } > + bix->cycle_count = ret; > + > + /* get OEM name */ > + ret = i2c_smbus_read_i2c_block_data(client, MSHW0011_BAT0_REG_OEM, > + 4, buf); > + if (ret != 4) { > + dev_err(&client->dev, "Error reading cycle count: %d\n", ret); > + return ret; > + } > + snprintf(bix->OEM, ARRAY_SIZE(bix->OEM), "%3pE", buf); > + > + return 0; > +} > + > +static int mshw0011_bst(struct mshw0011_data *cdata, struct bst *bst) > +{ > + struct i2c_client *client = cdata->bat0; > + int rate, capacity, voltage, state; > + s16 tmp; > + > + rate = i2c_smbus_read_word_data(client, MSHW0011_BAT0_REG_RATE); > + if (rate < 0) > + return rate; > + > + capacity = i2c_smbus_read_word_data(client, MSHW0011_BAT0_REG_CAPACITY); > + if (capacity < 0) > + return capacity; > + > + voltage = i2c_smbus_read_word_data(client, MSHW0011_BAT0_REG_VOLTAGE); > + if (voltage < 0) > + return voltage; > + > + tmp = rate; > + bst->battery_present_rate = abs((s32)tmp); > + > + state = 0; > + if ((s32) tmp > 0) > + state |= ACPI_BATTERY_STATE_CHARGING; > + else if ((s32) tmp < 0) > + state |= ACPI_BATTERY_STATE_DISCHARGING; > + bst->battery_state = state; > + > + bst->battery_remaining_capacity = capacity; > + bst->battery_present_voltage = voltage; > + > + return 0; > +} > + > +static int mshw0011_adp_psr(struct mshw0011_data *cdata) > +{ > + struct i2c_client *client = cdata->adp1; > + int ret; > + > + ret = i2c_smbus_read_byte_data(client, MSHW0011_ADP1_REG_PSR); > + if (ret < 0) > + return ret; > + > + return ret; > +} > + > +static int mshw0011_isr(struct mshw0011_data *cdata) > +{ > + struct bst bst; > + struct bix bix; > + int ret; > + bool status, bat_status; > + > + ret = mshw0011_adp_psr(cdata); > + if (ret < 0) > + return ret; > + > + status = ret; > + if (status != cdata->charging) > + mshw0011_notify(cdata, cdata->notify_mask, > + MSHW0011_NOTIFY_ADP1, &ret); > + > + cdata->charging = status; > + > + ret = mshw0011_bst(cdata, &bst); > + if (ret < 0) > + return ret; > + > + bat_status = bst.battery_state; > + if (bat_status != cdata->bat_charging) > + mshw0011_notify(cdata, cdata->notify_mask, > + MSHW0011_NOTIFY_BAT0_BST, &ret); > + > + cdata->bat_charging = bat_status; > + > + ret = mshw0011_bix(cdata, &bix); > + if (ret < 0) > + return ret; > + > + if (bix.last_full_charg_capacity != cdata->full_capacity) > + mshw0011_notify(cdata, cdata->notify_mask, > + MSHW0011_NOTIFY_BAT0_BIX, &ret); > + > + cdata->full_capacity = bix.last_full_charg_capacity; > + > + return 0; > +} > + > +static int mshw0011_poll_task(void *data) > +{ > + struct mshw0011_data *cdata = data; > + int ret = 0; > + > + cdata->kthread_running = true; > + > + set_freezable(); > + > + while (!kthread_should_stop()) { > + schedule_timeout_interruptible(POLL_INTERVAL); > + try_to_freeze(); > + ret = mshw0011_isr(data); > + if (ret) > + break; > + } > + > + cdata->kthread_running = false; > + return ret; > +} > + > +static acpi_status > +mshw0011_space_handler(u32 function, acpi_physical_address command, > + u32 bits, u64 *value64, > + void *handler_context, void *region_context) > +{ > + struct gsb_buffer *gsb = (struct gsb_buffer *)value64; > + struct mshw0011_handler_data *data = handler_context; > + struct acpi_connection_info *info = &data->info; > + struct acpi_resource_i2c_serialbus *sb; > + struct i2c_client *client = data->client; > + struct mshw0011_data *cdata = i2c_get_clientdata(client); > + struct acpi_resource *ares; > + u32 accessor_type = function >> 16; > + acpi_status ret; > + int status = 1; > + > + ret = acpi_buffer_to_resource(info->connection, info->length, &ares); > + if (ACPI_FAILURE(ret)) > + return ret; > + > + if (!value64 || ares->type != ACPI_RESOURCE_TYPE_SERIAL_BUS) { > + ret = AE_BAD_PARAMETER; > + goto err; > + } > + > + sb = &ares->data.i2c_serial_bus; > + if (sb->type != ACPI_RESOURCE_SERIAL_TYPE_I2C) { > + ret = AE_BAD_PARAMETER; > + goto err; > + } > + > + if (accessor_type != ACPI_GSB_ACCESS_ATTRIB_RAW_PROCESS) { > + ret = AE_BAD_PARAMETER; > + goto err; > + } > + > + if (gsb->cmd.arg0 == MSHW0011_CMD_DEST_ADP1 && > + gsb->cmd.arg1 == MSHW0011_CMD_ADP1_PSR) { > + ret = mshw0011_adp_psr(cdata); > + if (ret >= 0) { > + status = ret; > + ret = 0; > + } > + goto out; > + } > + > + if (gsb->cmd.arg0 != MSHW0011_CMD_DEST_BAT0) { > + ret = AE_BAD_PARAMETER; > + goto err; > + } > + > + switch (gsb->cmd.arg1) { > + case MSHW0011_CMD_BAT0_STA: > + break; > + case MSHW0011_CMD_BAT0_BIX: > + ret = mshw0011_bix(cdata, &gsb->bix); > + break; > + case MSHW0011_CMD_BAT0_BTP: > + cdata->trip_point = gsb->cmd.arg2; > + break; > + case MSHW0011_CMD_BAT0_BST: > + ret = mshw0011_bst(cdata, &gsb->bst); > + break; > + default: > + pr_info("command(0x%02x) is not supported.\n", gsb->cmd.arg1); > + ret = AE_BAD_PARAMETER; > + goto err; > + } > + > + out: > + gsb->ret = status; > + gsb->status = 0; > + > + err: > + ACPI_FREE(ares); > + return ret; > +} > + > +static int mshw0011_install_space_handler(struct i2c_client *client) > +{ > + acpi_handle handle; > + struct mshw0011_handler_data *data; > + acpi_status status; > + > + handle = ACPI_HANDLE(&client->dev); > + if (!handle) > + return -ENODEV; > + > + data = kzalloc(sizeof(struct mshw0011_handler_data), > + GFP_KERNEL); > + if (!data) > + return -ENOMEM; > + > + data->client = client; > + status = acpi_bus_attach_private_data(handle, (void *)data); > + if (ACPI_FAILURE(status)) { > + kfree(data); > + return -ENOMEM; > + } > + > + status = acpi_install_address_space_handler(handle, > + ACPI_ADR_SPACE_GSBUS, > + &mshw0011_space_handler, > + NULL, > + data); > + if (ACPI_FAILURE(status)) { > + dev_err(&client->dev, "Error installing i2c space handler\n"); > + acpi_bus_detach_private_data(handle); > + kfree(data); > + return -ENOMEM; > + } > + > + acpi_walk_dep_device_list(handle); > + return 0; > +} > + > +static void mshw0011_remove_space_handler(struct i2c_client *client) > +{ > + struct mshw0011_handler_data *data; > + acpi_handle handle; > + acpi_status status; > + > + handle = ACPI_HANDLE(&client->dev); > + if (!handle) > + return; > + > + acpi_remove_address_space_handler(handle, > + ACPI_ADR_SPACE_GSBUS, > + &mshw0011_space_handler); > + > + status = acpi_bus_get_private_data(handle, (void **)&data); > + if (ACPI_SUCCESS(status)) > + kfree(data); > + > + acpi_bus_detach_private_data(handle); > +} > + > +static int mshw0011_probe(struct i2c_client *client) > +{ > + struct i2c_board_info board_info; > + struct device *dev = &client->dev; > + struct i2c_client *bat0; > + struct mshw0011_data *data; > + int error, mask; > + > + data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); > + if (!data) > + return -ENOMEM; > + > + data->adp1 = client; > + i2c_set_clientdata(client, data); > + > + memset(&board_info, 0, sizeof(board_info)); > + strlcpy(board_info.type, "MSHW0011-bat0", I2C_NAME_SIZE); > + > + bat0 = i2c_acpi_new_device(dev, 1, &board_info); > + if (!bat0) > + return -ENOMEM; > + > + data->bat0 = bat0; > + i2c_set_clientdata(bat0, data); > + > + error = mshw0011_notify(data, 1, MSHW0011_NOTIFY_GET_VERSION, &mask); > + if (error) > + goto out_err; > + > + data->notify_mask = mask == MSHW0011_EV_2_5_MASK; > + > + data->poll_task = kthread_run(mshw0011_poll_task, data, "mshw0011_adp"); > + if (IS_ERR(data->poll_task)) { > + error = PTR_ERR(data->poll_task); > + dev_err(&client->dev, "Unable to run kthread err %d\n", error); > + goto out_err; > + } > + > + error = mshw0011_install_space_handler(client); > + if (error) > + goto out_err; > + > + return 0; > + > +out_err: > + if (data->kthread_running) > + kthread_stop(data->poll_task); > + i2c_unregister_device(data->bat0); > + return error; > +} > + > +static int mshw0011_remove(struct i2c_client *client) > +{ > + struct mshw0011_data *cdata = i2c_get_clientdata(client); > + > + mshw0011_remove_space_handler(client); > + > + if (cdata->kthread_running) > + kthread_stop(cdata->poll_task); > + > + i2c_unregister_device(cdata->bat0); > + > + return 0; > +} > + > +static const struct acpi_device_id mshw0011_acpi_match[] = { > + { "MSHW0011", 0 }, > + { } > +}; > +MODULE_DEVICE_TABLE(acpi, mshw0011_acpi_match); > + > +static struct i2c_driver mshw0011_driver = { > + .probe_new = mshw0011_probe, > + .remove = mshw0011_remove, > + .driver = { > + .name = "mshw0011", > + .acpi_match_table = ACPI_PTR(mshw0011_acpi_match), > + }, > +}; > +module_i2c_driver(mshw0011_driver); > + > +MODULE_AUTHOR("Benjamin Tissoires <benjamin.tissoires@xxxxxxxxx>"); > +MODULE_DESCRIPTION("mshw0011 driver"); > +MODULE_LICENSE("GPL v2"); > -- > 2.24.1 -- With Best Regards, Andy Shevchenko