Signed-off-by: Christopher Heiny <cheiny@xxxxxxxxxxxxx> Cc: Dmitry Torokhov <dmitry.torokhov@xxxxxxxxx> Cc: Linus Walleij <linus.walleij@xxxxxxxxxxxxxx> Cc: Naveen Kumar Gaddipati <naveen.gaddipati@xxxxxxxxxxxxxx> Cc: Joeri de Gram <j.de.gram@xxxxxxxxx> Acked-by: Jean Delvare <khali@xxxxxxxxxxxx> --- drivers/input/rmi4/rmi_fw_update.c | 724 ++++++++++++++++++++++++++++++++++++ 1 files changed, 724 insertions(+), 0 deletions(-) diff --git a/drivers/input/rmi4/rmi_fw_update.c b/drivers/input/rmi4/rmi_fw_update.c new file mode 100644 index 0000000..7f6c315 --- /dev/null +++ b/drivers/input/rmi4/rmi_fw_update.c @@ -0,0 +1,724 @@ +/* + * Copyright (c) 2012 Synaptics Incorporated + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * 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, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#define DEBUG + +#define RIM_HACK 1 + +#include <linux/delay.h> +#include <linux/firmware.h> +#include <linux/ihex.h> +#include <linux/kernel.h> +#include<linux/moduleparam.h> +#include <linux/rmi.h> +#include <linux/time.h> +#include "rmi_driver.h" +#include "rmi_f01.h" +#include "rmi_f34.h" + +#define HAS_BSR_MASK 0x20 + +#define CHECKSUM_OFFSET 0 +#define BOOTLOADER_VERSION_OFFSET 0x07 +#define IMAGE_SIZE_OFFSET 0x08 +#define CONFIG_SIZE_OFFSET 0x0C +#define PRODUCT_ID_OFFSET 0x10 +#define PRODUCT_ID_SIZE 10 +#define PRODUCT_INFO_OFFSET 0x1E +#define PRODUCT_INFO_SIZE 2 + +#define F01_RESET_MASK 0x01 + +#define ENABLE_WAIT_US (300 * 1000) + +/** Image file V5, Option 0 + */ +struct image_header { + u32 checksum; + unsigned int image_size; + unsigned int config_size; + unsigned char options; + unsigned char bootloader_version; + u8 product_id[RMI_PRODUCT_ID_LENGTH + 1]; + unsigned char product_info[PRODUCT_INFO_SIZE]; +}; + +static u32 extract_u32(const u8 *ptr) +{ + return (u32)ptr[0] + + (u32)ptr[1] * 0x100 + + (u32)ptr[2] * 0x10000 + + (u32)ptr[3] * 0x1000000; +} + +struct reflash_data { + struct rmi_device *rmi_dev; + struct pdt_entry *f01_pdt; + union f01_basic_queries f01_queries; + union f01_device_control_0 f01_controls; + char product_id[RMI_PRODUCT_ID_LENGTH+1]; + struct pdt_entry *f34_pdt; + u8 bootloader_id[2]; + union f34_query_regs f34_queries; + union f34_control_status f34_controls; + const u8 *firmware_data; + const u8 *config_data; +}; + +/* If this parameter is true, we will update the firmware regardless of + * the versioning info. + */ +static bool force = 1; +module_param(force, bool, S_IRUGO | S_IWUSR); +MODULE_PARM_DESC(param, "Force reflash of RMI4 devices"); + +/* If this parameter is not NULL, we'll use that name for the firmware image, + * instead of getting it from the F01 queries. + */ +static char *img_name; +module_param(img_name, charp, S_IRUGO | S_IWUSR); +MODULE_PARM_DESC(param, "Name of the RMI4 firmware image"); + +#define RMI4_IMAGE_FILE_REV1_OFFSET 30 +#define RMI4_IMAGE_FILE_REV2_OFFSET 31 +#define IMAGE_FILE_CHECKSUM_SIZE 4 +#define FIRMWARE_IMAGE_AREA_OFFSET 0x100 + +static void extract_header(const u8 *data, int pos, struct image_header *header) +{ + header->checksum = extract_u32(&data[pos + CHECKSUM_OFFSET]); + header->bootloader_version = data[pos + BOOTLOADER_VERSION_OFFSET]; + header->image_size = extract_u32(&data[pos + IMAGE_SIZE_OFFSET]); + header->config_size = extract_u32(&data[pos + CONFIG_SIZE_OFFSET]); + memcpy(header->product_id, &data[pos + PRODUCT_ID_OFFSET], + RMI_PRODUCT_ID_LENGTH); + header->product_id[PRODUCT_ID_SIZE] = 0; + memcpy(header->product_info, &data[pos + PRODUCT_INFO_OFFSET], + RMI_PRODUCT_ID_LENGTH); +} + +static int rescan_pdt(struct reflash_data *data) +{ + int retval; + bool f01_found; + bool f34_found; + struct pdt_entry pdt_entry; + int i; + struct rmi_device *rmi_dev = data->rmi_dev; + struct pdt_entry *f34_pdt = data->f34_pdt; + struct pdt_entry *f01_pdt = data->f01_pdt; + + /* Per spec, once we're in reflash we only need to look at the first + * PDT page for potentially changed F01 and F34 information. + */ + for (i = PDT_START_SCAN_LOCATION; i >= PDT_END_SCAN_LOCATION; + i -= sizeof(pdt_entry)) { + retval = rmi_read_block(rmi_dev, i, (u8 *)&pdt_entry, + sizeof(pdt_entry)); + if (retval != sizeof(pdt_entry)) { + dev_err(&rmi_dev->dev, + "Read PDT entry at %#06x failed: %d.\n", + i, retval); + return retval; + } + + if (RMI4_END_OF_PDT(pdt_entry.function_number)) + break; + + if (pdt_entry.function_number == 0x01) { + memcpy(f01_pdt, &pdt_entry, sizeof(pdt_entry)); + f01_found = true; + } else if (pdt_entry.function_number == 0x34) { + memcpy(f34_pdt, &pdt_entry, sizeof(pdt_entry)); + f34_found = true; + } + } + + if (!f01_found) { + dev_err(&rmi_dev->dev, "Failed to find F01 PDT entry.\n"); + retval = -ENODEV; + } else if (!f34_found) { + dev_err(&rmi_dev->dev, "Failed to find F34 PDT entry.\n"); + retval = -ENODEV; + } else + retval = 0; + + return retval; +} + +static int read_f34_controls(struct reflash_data *data) +{ + int retval; + + retval = rmi_read(data->rmi_dev, data->f34_controls.address, + data->f34_controls.regs); + if (retval < 0) + return retval; + + return 0; +} + +static int read_f01_status(struct reflash_data *data, + union f01_device_status *device_status) +{ + int retval; + + retval = rmi_read(data->rmi_dev, data->f01_pdt->data_base_addr, + device_status->regs); + if (retval < 0) + return retval; + + return 0; +} + +static int read_f01_controls(struct reflash_data *data) +{ + int retval; + + retval = rmi_read(data->rmi_dev, data->f01_pdt->control_base_addr, + data->f01_controls.regs); + if (retval < 0) + return retval; + return 0; +} + +static int write_f01_controls(struct reflash_data *data) +{ + int retval; + + retval = rmi_write(data->rmi_dev, data->f01_pdt->control_base_addr, + data->f01_controls.regs[0]); + if (retval < 0) + return retval; + return 0; +} + +#define MIN_SLEEP_TIME_US 50 +#define MAX_SLEEP_TIME_US 100 + +/* Wait until the status is idle and we're ready to continue */ +static int wait_for_idle(struct reflash_data *data, int timeout_ms) +{ + int timeout_count = ((timeout_ms * 1000) / MAX_SLEEP_TIME_US) + 1; + int count = 0; + union f34_control_status *controls = &data->f34_controls; + int retval; + + do { + if (count || timeout_count == 1) + usleep_range(MIN_SLEEP_TIME_US, MAX_SLEEP_TIME_US); + retval = read_f34_controls(data); + count++; + if (retval < 0) + continue; + else if (IS_IDLE(controls)) { + if (!data->f34_controls.program_enabled) { + /** This works around a bug in certain device + * firmwares, where the idle state is reached, + * but the program_enabled bit is not yet set. + */ + dev_warn(&data->rmi_dev->dev, "Yikes! We're not enabled!\n"); + msleep(1000); + read_f34_controls(data); + } + return 0; + } + } while (count < timeout_count); + + dev_err(&data->rmi_dev->dev, + "ERROR: Timeout waiting for idle status, last status: %#04x.\n", + controls->regs[0]); + dev_err(&data->rmi_dev->dev, "Command: %#04x\n", controls->command); + dev_err(&data->rmi_dev->dev, "Status: %#04x\n", controls->status); + dev_err(&data->rmi_dev->dev, "Enabled: %d\n", + controls->program_enabled); + dev_err(&data->rmi_dev->dev, "Idle: %d\n", IS_IDLE(controls)); + return -ETIMEDOUT; +} + + +static int read_f01_queries(struct reflash_data *data) +{ + int retval; + u16 addr = data->f01_pdt->query_base_addr; + + retval = rmi_read_block(data->rmi_dev, addr, data->f01_queries.regs, + ARRAY_SIZE(data->f01_queries.regs)); + if (retval < 0) { + dev_err(&data->rmi_dev->dev, + "Failed to read F34 queries (code %d).\n", retval); + return retval; + } + addr += ARRAY_SIZE(data->f01_queries.regs); + + retval = rmi_read_block(data->rmi_dev, addr, data->product_id, + RMI_PRODUCT_ID_LENGTH); + if (retval < 0) { + dev_err(&data->rmi_dev->dev, + "Failed to read product ID (code %d).\n", retval); + return retval; + } + data->product_id[RMI_PRODUCT_ID_LENGTH] = 0; + dev_info(&data->rmi_dev->dev, "F01 Product id: %s\n", + data->product_id); + dev_info(&data->rmi_dev->dev, "F01 product info: %#04x %#04x\n", + data->f01_queries.productinfo_1, + data->f01_queries.productinfo_2); + + return 0; +} + +static int read_f34_queries(struct reflash_data *data) +{ + int retval; + u8 id_str[3]; + + retval = rmi_read_block(data->rmi_dev, data->f34_pdt->query_base_addr, + data->bootloader_id, 2); + if (retval < 0) { + dev_err(&data->rmi_dev->dev, + "Failed to read F34 bootloader_id (code %d).\n", + retval); + return retval; + } + retval = rmi_read_block(data->rmi_dev, data->f34_pdt->query_base_addr+2, + data->f34_queries.regs, + ARRAY_SIZE(data->f34_queries.regs)); + if (retval < 0) { + dev_err(&data->rmi_dev->dev, + "Failed to read F34 queries (code %d).\n", retval); + return retval; + } + data->f34_queries.block_size = + le16_to_cpu(data->f34_queries.block_size); + data->f34_queries.fw_block_count = + le16_to_cpu(data->f34_queries.fw_block_count); + data->f34_queries.config_block_count = + le16_to_cpu(data->f34_queries.config_block_count); + id_str[0] = data->bootloader_id[0]; + id_str[1] = data->bootloader_id[1]; + id_str[2] = 0; +#ifdef DEBUG + dev_info(&data->rmi_dev->dev, "Got F34 data->f34_queries.\n"); + dev_info(&data->rmi_dev->dev, "F34 bootloader id: %s (%#04x %#04x)\n", + id_str, data->bootloader_id[0], data->bootloader_id[1]); + dev_info(&data->rmi_dev->dev, "F34 has config id: %d\n", + data->f34_queries.has_config_id); + dev_info(&data->rmi_dev->dev, "F34 unlocked: %d\n", + data->f34_queries.unlocked); + dev_info(&data->rmi_dev->dev, "F34 regMap: %d\n", + data->f34_queries.reg_map); + dev_info(&data->rmi_dev->dev, "F34 block size: %d\n", + data->f34_queries.block_size); + dev_info(&data->rmi_dev->dev, "F34 fw blocks: %d\n", + data->f34_queries.fw_block_count); + dev_info(&data->rmi_dev->dev, "F34 config blocks: %d\n", + data->f34_queries.config_block_count); +#endif + + data->f34_controls.address = data->f34_pdt->data_base_addr + + F34_BLOCK_DATA_OFFSET + data->f34_queries.block_size; + + return 0; +} + +static int write_bootloader_id(struct reflash_data *data) +{ + int retval; + struct rmi_device *rmi_dev = data->rmi_dev; + struct pdt_entry *f34_pdt = data->f34_pdt; + + retval = rmi_write_block(rmi_dev, + f34_pdt->data_base_addr + F34_BLOCK_DATA_OFFSET, + data->bootloader_id, ARRAY_SIZE(data->bootloader_id)); + if (retval < 0) { + dev_err(&rmi_dev->dev, + "Failed to write bootloader ID. Code: %d.\n", retval); + return retval; + } + + return 0; +} + +static int write_f34_command(struct reflash_data *data, u8 command) +{ + int retval; + struct rmi_device *rmi_dev = data->rmi_dev; + + retval = rmi_write(rmi_dev, data->f34_controls.address, command); + if (retval < 0) { + dev_err(&rmi_dev->dev, + "Failed to write F34 command %#04x. Code: %d.\n", + command, retval); + return retval; + } + + return 0; +} + +static int enter_flash_programming(struct reflash_data *data) +{ + int retval; + union f01_device_status device_status; + struct rmi_device *rmi_dev = data->rmi_dev; + + retval = write_bootloader_id(data); + if (retval < 0) + return retval; + + dev_info(&rmi_dev->dev, "Enabling flash programming.\n"); + retval = write_f34_command(data, F34_ENABLE_FLASH_PROG); + if (retval < 0) + return retval; + +#if RIM_HACK + data->f01_controls.nosleep = true; + retval = write_f01_controls(data); + if (retval < 0) + return retval; +#endif + + retval = wait_for_idle(data, F34_ENABLE_WAIT_MS); + if (retval) { + dev_err(&rmi_dev->dev, "Did not reach idle state after %d ms. Code: %d.\n", + F34_ENABLE_WAIT_MS, retval); + return retval; + } + if (!data->f34_controls.program_enabled) { + dev_err(&rmi_dev->dev, "Reached idle, but programming not enabled (current status register: %#04x).\n", + data->f34_controls.regs[0]); + return -EINVAL; + } + dev_info(&rmi_dev->dev, "HOORAY! Programming is enabled!\n"); + + retval = rescan_pdt(data); + if (retval) { + dev_err(&rmi_dev->dev, "Failed to rescan pdt. Code: %d.\n", + retval); + return retval; + } + + retval = read_f01_status(data, &device_status); + if (retval) { + dev_err(&rmi_dev->dev, "Failed to read F01 status after enabling reflash. Code: %d.\n", + retval); + return retval; + } + if (!(device_status.flash_prog)) { + dev_err(&rmi_dev->dev, "Device reports as not in flash programming mode.\n"); + return -EINVAL; + } + + retval = read_f34_queries(data); + if (retval) { + dev_err(&rmi_dev->dev, "F34 queries failed, code = %d.\n", + retval); + return retval; + } + + retval = read_f01_controls(data); + if (retval) { + dev_err(&rmi_dev->dev, "F01 controls read failed, code = %d.\n", + retval); + return retval; + } + data->f01_controls.nosleep = true; + data->f01_controls.sleep_mode = RMI_SLEEP_MODE_NORMAL; + + retval = write_f01_controls(data); + if (retval) { + dev_err(&rmi_dev->dev, "F01 controls write failed, code = %d.\n", + retval); + return retval; + } + + return retval; +} + +static void reset_device(struct reflash_data *data) +{ + int retval; + struct rmi_device_platform_data *pdata = + to_rmi_platform_data(data->rmi_dev); + + dev_info(&data->rmi_dev->dev, "Resetting...\n"); + retval = rmi_write(data->rmi_dev, data->f01_pdt->command_base_addr, + F01_RESET_MASK); + if (retval < 0) + dev_warn(&data->rmi_dev->dev, + "WARNING - post-flash reset failed, code: %d.\n", + retval); + msleep(pdata->reset_delay_ms); + dev_info(&data->rmi_dev->dev, "Reset completed.\n"); +} + +/* + * Send data to the device one block at a time. + */ +static int write_blocks(struct reflash_data *data, u8 *block_ptr, + u16 block_count, u8 cmd) +{ + int block_num; + u8 zeros[] = {0, 0}; + int retval; + u16 addr = data->f34_pdt->data_base_addr + F34_BLOCK_DATA_OFFSET; + + retval = rmi_write_block(data->rmi_dev, data->f34_pdt->data_base_addr, + zeros, ARRAY_SIZE(zeros)); + if (retval < 0) { + dev_err(&data->rmi_dev->dev, "Failed to write initial zeros. Code=%d.\n", + retval); + return retval; + } + + for (block_num = 0; block_num < block_count; ++block_num) { + retval = rmi_write_block(data->rmi_dev, addr, block_ptr, + data->f34_queries.block_size); + if (retval < 0) { + dev_err(&data->rmi_dev->dev, "Failed to write block %d. Code=%d.\n", + block_num, retval); + return retval; + } + + retval = write_f34_command(data, cmd); + if (retval) { + dev_err(&data->rmi_dev->dev, "Failed to write command for block %d. Code=%d.\n", + block_num, retval); + return retval; + } + + + retval = wait_for_idle(data, F34_IDLE_WAIT_MS); + if (retval) { + dev_err(&data->rmi_dev->dev, "Failed to go idle after writing block %d. Code=%d.\n", + block_num, retval); + return retval; + } + + block_ptr += data->f34_queries.block_size; + } + + return 0; +} + +static int write_firmware(struct reflash_data *data) +{ + return write_blocks(data, (u8 *) data->firmware_data, + data->f34_queries.fw_block_count, F34_WRITE_FW_BLOCK); +} + +static int write_configuration(struct reflash_data *data) +{ + return write_blocks(data, (u8 *) data->config_data, + data->f34_queries.config_block_count, F34_WRITE_CONFIG_BLOCK); +} + +static void reflash_firmware(struct reflash_data *data) +{ +#ifdef DEBUG + struct timespec start; + struct timespec end; + s64 duration_ns; +#endif + int retval = 0; + + retval = enter_flash_programming(data); + if (retval) + return; + + retval = write_bootloader_id(data); + if (retval) + return; + +#ifdef DEBUG + dev_info(&data->rmi_dev->dev, "Erasing FW...\n"); + getnstimeofday(&start); +#endif + retval = write_f34_command(data, F34_ERASE_ALL); + if (retval) + return; + + retval = wait_for_idle(data, F34_ERASE_WAIT_MS); + if (retval) { + dev_err(&data->rmi_dev->dev, + "Failed to reach idle state. Code: %d.\n", retval); + return; + } +#ifdef DEBUG + getnstimeofday(&end); + duration_ns = timespec_to_ns(&end) - timespec_to_ns(&start); + dev_info(&data->rmi_dev->dev, + "Erase complete, time: %lld ns.\n", duration_ns); +#endif + + if (data->firmware_data) { +#ifdef DEBUG + dev_info(&data->rmi_dev->dev, "Writing firmware...\n"); + getnstimeofday(&start); +#endif + retval = write_firmware(data); + if (retval) + return; +#ifdef DEBUG + getnstimeofday(&end); + duration_ns = timespec_to_ns(&end) - timespec_to_ns(&start); + dev_info(&data->rmi_dev->dev, + "Done writing FW, time: %lld ns.\n", duration_ns); +#endif + } + + if (data->config_data) { +#ifdef DEBUG + dev_info(&data->rmi_dev->dev, "Writing configuration...\n"); + getnstimeofday(&start); +#endif + retval = write_configuration(data); + if (retval) + return; +#ifdef DEBUG + getnstimeofday(&end); + duration_ns = timespec_to_ns(&end) - timespec_to_ns(&start); + dev_info(&data->rmi_dev->dev, + "Done writing config, time: %lld ns.\n", duration_ns); +#endif + } +} + +/* Returns false if the firmware should not be reflashed. + */ +static bool go_nogo(struct reflash_data *data, struct image_header *header) +{ + union f01_device_status device_status; + int retval; + + if (data->f01_queries.productinfo_1 < header->product_info[0] || + data->f01_queries.productinfo_2 < header->product_info[1]) { + dev_info(&data->rmi_dev->dev, + "FW product ID is older than image product ID.\n"); + return true; + } + + retval = read_f01_status(data, &device_status); + if (retval) + dev_err(&data->rmi_dev->dev, + "Failed to read F01 status. Code: %d.\n", retval); + + return device_status.flash_prog || force; +} + +void rmi4_fw_update(struct rmi_device *rmi_dev, + struct pdt_entry *f01_pdt, struct pdt_entry *f34_pdt) +{ +#ifdef DEBUG + struct timespec start; + struct timespec end; + s64 duration_ns; +#endif + char firmware_name[RMI_PRODUCT_ID_LENGTH + 12]; + const struct firmware *fw_entry = NULL; + int retval; + struct image_header header; + union pdt_properties pdt_props; + struct reflash_data data = { + .rmi_dev = rmi_dev, + .f01_pdt = f01_pdt, + .f34_pdt = f34_pdt, + }; + struct rmi_device_platform_data *pdata = to_rmi_platform_data(rmi_dev); + + dev_info(&rmi_dev->dev, "%s called.\n", __func__); +#ifdef DEBUG + getnstimeofday(&start); +#endif + + retval = rmi_read(rmi_dev, PDT_PROPERTIES_LOCATION, pdt_props.regs); + if (retval < 0) { + dev_warn(&rmi_dev->dev, + "Failed to read PDT props at %#06x (code %d). Assuming 0x00.\n", + PDT_PROPERTIES_LOCATION, retval); + } + if (pdt_props.has_bsr) { + dev_warn(&rmi_dev->dev, + "Firmware update for LTS not currently supported.\n"); + return; + } + + retval = read_f01_queries(&data); + if (retval) { + dev_err(&rmi_dev->dev, "F01 queries failed, code = %d.\n", + retval); + return; + } + retval = read_f34_queries(&data); + if (retval) { + dev_err(&rmi_dev->dev, "F34 queries failed, code = %d.\n", + retval); + return; + } + if (pdata->firmware_name && strlen(pdata->firmware_name)) + snprintf(firmware_name, sizeof(firmware_name), "rmi4/%s.img", + pdata->firmware_name); + else + snprintf(firmware_name, sizeof(firmware_name), "rmi4/%s.img", + (img_name && strlen(img_name)) + ? img_name : data.product_id); + dev_info(&rmi_dev->dev, "Requesting %s.\n", firmware_name); + retval = request_firmware(&fw_entry, firmware_name, &rmi_dev->dev); + if (retval != 0) { + dev_err(&rmi_dev->dev, "Firmware %s not available, code = %d\n", + firmware_name, retval); + return; + } + +#ifdef DEBUG + dev_info(&rmi_dev->dev, "Got firmware, size: %d.\n", fw_entry->size); + extract_header(fw_entry->data, 0, &header); + dev_info(&rmi_dev->dev, "Img checksum: %#08X\n", + header.checksum); + dev_info(&rmi_dev->dev, "Img image size: %d\n", + header.image_size); + dev_info(&rmi_dev->dev, "Img config size: %d\n", + header.config_size); + dev_info(&rmi_dev->dev, "Img bootloader version: %d\n", + header.bootloader_version); + dev_info(&rmi_dev->dev, "Img product id: %s\n", + header.product_id); + dev_info(&rmi_dev->dev, "Img product info: %#04x %#04x\n", + header.product_info[0], header.product_info[1]); +#endif + + if (header.image_size) + data.firmware_data = fw_entry->data + F34_FW_IMAGE_OFFSET; + if (header.config_size) + data.config_data = fw_entry->data + F34_FW_IMAGE_OFFSET + + header.image_size; + + if (go_nogo(&data, &header)) { + reflash_firmware(&data); + reset_device(&data); + } else + dev_info(&rmi_dev->dev, "Go/NoGo said don't reflash.\n"); + + if (fw_entry) + release_firmware(fw_entry); +#ifdef DEBUG + getnstimeofday(&end); + duration_ns = timespec_to_ns(&end) - timespec_to_ns(&start); + dev_info(&rmi_dev->dev, "Time to reflash: %lld ns.\n", duration_ns); +#endif +} -- To unsubscribe from this list: send the line "unsubscribe linux-input" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html