generic: routerboot sysfs platform driver

This driver exposes the data encoded in the "hard_config" flash segment
of MikroTik RouterBOARDs devices. It presents the data in a sysfs folder
named "hard_config". The WLAN calibration data is available on demand via
the 'wlan_data' sysfs file in that folder.

This driver permanently allocates a chunk of RAM as large as the
"hard_config" MTD partition (typically 4KB), although it is technically
possible to operate entirely from the MTD device without using a local
buffer (except when requesting WLAN calibration data), at the cost of a
performance penalty.

This driver does not reuse any of the existing code previously found in
routerboot.c.

This driver has been successfully tested on BE (ath79) and LE (ipq40xx
and ramips) hardware.

Tested-by: Roger Pueyo Centelles <roger.pueyo@guifi.net>
Tested-by: Baptiste Jonglez <git@bitsofnetworks.org>
Tested-by: Tobias Schramm <t.schramm@manjaro.org>
Tested-by: Christopher Hill <ch6574@gmail.com>
Signed-off-by: Thibaut VARÈNE <hacks@slashdirt.org>
master
Thibaut VARÈNE 4 years ago committed by Koen Vandeputte
parent 21869e8f80
commit 5ecf7d9654

@ -0,0 +1,18 @@
menuconfig MIKROTIK
bool "Platform support for MikroTik RouterBoard virtual devices"
default n
depends on MTD
select LZO_DECOMPRESS
help
Say Y here to get to see options for the MikroTik RouterBoard platform.
This option alone does not add any kernel code.
if MIKROTIK
config MIKROTIK_RB_SYSFS
tristate "RouterBoot sysfs support"
help
This driver exposes RouterBoot configuration in sysfs.
endif # MIKROTIK

@ -0,0 +1,4 @@
#
# Makefile for MikroTik RouterBoard platform specific drivers
#
obj-$(CONFIG_MIKROTIK_RB_SYSFS) += routerboot.o rb_hardconfig.o

@ -0,0 +1,481 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Driver for MikroTik RouterBoot hard config.
*
* Copyright (C) 2020 Thibaut VARÈNE <hacks+kernel@slashdirt.org>
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 as published
* by the Free Software Foundation.
*
* This driver exposes the data encoded in the "hard_config" flash segment of
* MikroTik RouterBOARDs devices. It presents the data in a sysfs folder
* named "hard_config". The WLAN calibration data is available on demand via
* the 'wlan_data' sysfs file in that folder.
*
* This driver permanently allocates a chunk of RAM as large as the hard_config
* MTD partition, although it is technically possible to operate entirely from
* the MTD device without using a local buffer (except when requesting WLAN
* calibration data), at the cost of a performance penalty.
*
* Some constant defines extracted from routerboot.{c,h} by Gabor Juhos
* <juhosg@openwrt.org>
*/
#include <linux/types.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/errno.h>
#include <linux/kobject.h>
#include <linux/bitops.h>
#include <linux/string.h>
#include <linux/mtd/mtd.h>
#include <linux/sysfs.h>
#include <linux/lzo.h>
#include "routerboot.h"
#define RB_HARDCONFIG_VER "0.01"
#define RB_HC_PR_PFX "[rb_hardconfig] "
/* ID values for hardware settings */
#define RB_ID_FLASH_INFO 0x03
#define RB_ID_MAC_ADDRESS_PACK 0x04
#define RB_ID_BOARD_PRODUCT_CODE 0x05
#define RB_ID_BIOS_VERSION 0x06
#define RB_ID_SDRAM_TIMINGS 0x08
#define RB_ID_DEVICE_TIMINGS 0x09
#define RB_ID_SOFTWARE_ID 0x0A
#define RB_ID_SERIAL_NUMBER 0x0B
#define RB_ID_MEMORY_SIZE 0x0D
#define RB_ID_MAC_ADDRESS_COUNT 0x0E
#define RB_ID_HW_OPTIONS 0x15
#define RB_ID_WLAN_DATA 0x16
#define RB_ID_BOARD_IDENTIFIER 0x17
#define RB_ID_PRODUCT_NAME 0x21
#define RB_ID_DEFCONF 0x26
/* Bit definitions for hardware options */
#define RB_HW_OPT_NO_UART BIT(0)
#define RB_HW_OPT_HAS_VOLTAGE BIT(1)
#define RB_HW_OPT_HAS_USB BIT(2)
#define RB_HW_OPT_HAS_ATTINY BIT(3)
#define RB_HW_OPT_NO_NAND BIT(14)
#define RB_HW_OPT_HAS_LCD BIT(15)
#define RB_HW_OPT_HAS_POE_OUT BIT(16)
#define RB_HW_OPT_HAS_uSD BIT(17)
#define RB_HW_OPT_HAS_SIM BIT(18)
#define RB_HW_OPT_HAS_SFP BIT(20)
#define RB_HW_OPT_HAS_WIFI BIT(21)
#define RB_HW_OPT_HAS_TS_FOR_ADC BIT(22)
#define RB_HW_OPT_HAS_PLC BIT(29)
static struct kobject *hc_kobj;
static u8 *hc_buf; // ro buffer after init(): no locking required
static size_t hc_buflen;
/* Array of known hw_options bits with human-friendly parsing */
static struct hc_hwopt {
const u32 bit;
const char *str;
} const hc_hwopts[] = {
{
.bit = RB_HW_OPT_NO_UART,
.str = "no UART\t\t",
}, {
.bit = RB_HW_OPT_HAS_VOLTAGE,
.str = "has Vreg\t",
}, {
.bit = RB_HW_OPT_HAS_USB,
.str = "has usb\t\t",
}, {
.bit = RB_HW_OPT_HAS_ATTINY,
.str = "has ATtiny\t",
}, {
.bit = RB_HW_OPT_NO_NAND,
.str = "no NAND\t\t",
}, {
.bit = RB_HW_OPT_HAS_LCD,
.str = "has LCD\t\t",
}, {
.bit = RB_HW_OPT_HAS_POE_OUT,
.str = "has POE out\t",
}, {
.bit = RB_HW_OPT_HAS_uSD,
.str = "has MicroSD\t",
}, {
.bit = RB_HW_OPT_HAS_SIM,
.str = "has SIM\t\t",
}, {
.bit = RB_HW_OPT_HAS_SFP,
.str = "has SFP\t\t",
}, {
.bit = RB_HW_OPT_HAS_WIFI,
.str = "has WiFi\t",
}, {
.bit = RB_HW_OPT_HAS_TS_FOR_ADC,
.str = "has TS ADC\t",
}, {
.bit = RB_HW_OPT_HAS_PLC,
.str = "has PLC\t\t",
},
};
static ssize_t hc_tag_show_string(const u8 *pld, u16 pld_len, char *buf)
{
return snprintf(buf, pld_len+1, "%s\n", pld);
}
static ssize_t hc_tag_show_u32(const u8 *pld, u16 pld_len, char *buf)
{
char *out = buf;
u32 data; // cpu-endian
/* Caller ensures pld_len > 0 */
if (pld_len % sizeof(data))
return -EINVAL;
data = *(u32 *)pld;
do {
out += sprintf(out, "0x%08x\n", data);
data++;
} while ((pld_len -= sizeof(data)));
return out - buf;
}
/*
* The MAC is stored network-endian on all devices, in 2 32-bit segments:
* <XX:XX:XX:XX> <XX:XX:00:00>. Kernel print has us covered.
*/
static ssize_t hc_tag_show_mac(const u8 *pld, u16 pld_len, char *buf)
{
if (8 != pld_len)
return -EINVAL;
return sprintf(buf, "%pM\n", pld);
}
/*
* Print HW options in a human readable way:
* The raw number and in decoded form
*/
static ssize_t hc_tag_show_hwoptions(const u8 *pld, u16 pld_len, char *buf)
{
char *out = buf;
u32 data; // cpu-endian
int i;
if (sizeof(data) != pld_len)
return -EINVAL;
data = *(u32 *)pld;
out += sprintf(out, "raw\t\t: 0x%08x\n\n", data);
for (i = 0; i < ARRAY_SIZE(hc_hwopts); i++)
out += sprintf(out, "%s: %s\n", hc_hwopts[i].str,
(data & hc_hwopts[i].bit) ? "true" : "false");
return out - buf;
}
static ssize_t hc_wlan_data_bin_read(struct file *filp, struct kobject *kobj,
struct bin_attribute *attr, char *buf,
loff_t off, size_t count);
static struct hc_wlan_attr {
struct bin_attribute battr;
u16 pld_ofs;
u16 pld_len;
} hc_wlandata_battr = {
.battr = __BIN_ATTR(wlan_data, S_IRUSR, hc_wlan_data_bin_read, NULL, 0),
};
static ssize_t hc_attr_show(struct kobject *kobj, struct kobj_attribute *attr,
char *buf);
/* Array of known tags to publish in sysfs */
static struct hc_attr {
const u16 tag_id;
ssize_t (* const tshow)(const u8 *pld, u16 pld_len, char *buf);
struct kobj_attribute kattr;
u16 pld_ofs;
u16 pld_len;
} hc_attrs[] = {
{
.tag_id = RB_ID_FLASH_INFO,
.tshow = hc_tag_show_u32,
.kattr = __ATTR(flash_info, S_IRUSR, hc_attr_show, NULL),
}, {
.tag_id = RB_ID_MAC_ADDRESS_PACK,
.tshow = hc_tag_show_mac,
.kattr = __ATTR(mac_base, S_IRUSR, hc_attr_show, NULL),
}, {
.tag_id = RB_ID_BOARD_PRODUCT_CODE,
.tshow = hc_tag_show_string,
.kattr = __ATTR(board_product_code, S_IRUSR, hc_attr_show, NULL),
}, {
.tag_id = RB_ID_BIOS_VERSION,
.tshow = hc_tag_show_string,
.kattr = __ATTR(booter_version, S_IRUSR, hc_attr_show, NULL),
}, {
.tag_id = RB_ID_SERIAL_NUMBER,
.tshow = hc_tag_show_string,
.kattr = __ATTR(board_serial, S_IRUSR, hc_attr_show, NULL),
}, {
.tag_id = RB_ID_MEMORY_SIZE,
.tshow = hc_tag_show_u32,
.kattr = __ATTR(mem_size, S_IRUSR, hc_attr_show, NULL),
}, {
.tag_id = RB_ID_MAC_ADDRESS_COUNT,
.tshow = hc_tag_show_u32,
.kattr = __ATTR(mac_count, S_IRUSR, hc_attr_show, NULL),
}, {
.tag_id = RB_ID_HW_OPTIONS,
.tshow = hc_tag_show_hwoptions,
.kattr = __ATTR(hw_options, S_IRUSR, hc_attr_show, NULL),
}, {
.tag_id = RB_ID_WLAN_DATA,
.tshow = NULL,
}, {
.tag_id = RB_ID_BOARD_IDENTIFIER,
.tshow = hc_tag_show_string,
.kattr = __ATTR(board_identifier, S_IRUSR, hc_attr_show, NULL),
}, {
.tag_id = RB_ID_PRODUCT_NAME,
.tshow = hc_tag_show_string,
.kattr = __ATTR(product_name, S_IRUSR, hc_attr_show, NULL),
}, {
.tag_id = RB_ID_DEFCONF,
.tshow = hc_tag_show_string,
.kattr = __ATTR(defconf, S_IRUSR, hc_attr_show, NULL),
}
};
/*
* If the RB_ID_WLAN_DATA payload starts with RB_MAGIC_ERD, then past
* that magic number the payload itself contains a routerboot tag node
* locating the LZO-compressed calibration data at id 0x1.
*/
static int hc_wlan_data_unpack_erd(const u8 *inbuf, size_t inlen,
void *outbuf, size_t *outlen)
{
u16 lzo_ofs, lzo_len;
int ret;
/* Find embedded tag */
ret = routerboot_tag_find(inbuf, inlen, 0x1, // always id 1
&lzo_ofs, &lzo_len);
if (ret) {
pr_debug(RB_HC_PR_PFX "ERD data not found\n");
goto fail;
}
if (lzo_len > inlen) {
pr_debug(RB_HC_PR_PFX "Invalid ERD data length\n");
ret = -EINVAL;
goto fail;
}
ret = lzo1x_decompress_safe(inbuf+lzo_ofs, lzo_len, outbuf, outlen);
if (ret)
pr_debug(RB_HC_PR_PFX "LZO decompression error (%d)\n", ret);
fail:
return ret;
}
static int hc_wlan_data_unpack(const size_t tofs, size_t tlen,
void *outbuf, size_t *outlen)
{
const u8 *lbuf;
u32 magic;
int ret;
/* Caller ensure tlen > 0. tofs is aligned */
if ((tofs + tlen) > hc_buflen)
return -EIO;
lbuf = hc_buf + tofs;
magic = *(u32 *)lbuf;
ret = -ENODATA;
switch (magic) {
case RB_MAGIC_ERD:
/* Skip magic */
lbuf += sizeof(magic);
tlen -= sizeof(magic);
ret = hc_wlan_data_unpack_erd(lbuf, tlen, outbuf, outlen);
break;
default:
/*
* If the RB_ID_WLAN_DATA payload doesn't start with a
* magic number, the payload itself is the raw RLE-encoded
* calibration data.
*/
ret = routerboot_rle_decode(lbuf, tlen, outbuf, outlen);
if (ret)
pr_debug(RB_HC_PR_PFX "RLE decoding error (%d)\n", ret);
break;
}
return ret;
}
static ssize_t hc_attr_show(struct kobject *kobj, struct kobj_attribute *attr,
char *buf)
{
struct hc_attr *hc_attr;
const u8 *pld;
u16 pld_len;
hc_attr = container_of(attr, typeof(*hc_attr), kattr);
if (!hc_attr->pld_len)
return -ENOENT;
pld = hc_buf + hc_attr->pld_ofs;
pld_len = hc_attr->pld_len;
return hc_attr->tshow(pld, pld_len, buf);
}
/*
* This function will allocate and free memory every time it is called. This
* is not the fastest way to do this, but since the data is rarely read (mainly
* at boot time to load wlan caldata), this makes it possible to save memory for
* the system.
*/
static ssize_t hc_wlan_data_bin_read(struct file *filp, struct kobject *kobj,
struct bin_attribute *attr, char *buf,
loff_t off, size_t count)
{
struct hc_wlan_attr *hc_wattr;
size_t outlen;
void *outbuf;
int ret;
hc_wattr = container_of(attr, typeof(*hc_wattr), battr);
if (!hc_wattr->pld_len)
return -ENOENT;
outlen = RB_ART_SIZE;
/* Don't bother unpacking if the source is already too large */
if (hc_wattr->pld_len > outlen)
return -EFBIG;
outbuf = kmalloc(outlen, GFP_KERNEL);
if (!outbuf)
return -ENOMEM;
ret = hc_wlan_data_unpack(hc_wattr->pld_ofs, hc_wattr->pld_len, outbuf, &outlen);
if (ret) {
kfree(outbuf);
return ret;
}
if (off >= outlen) {
kfree(outbuf);
return 0;
}
if (off + count > outlen)
count = outlen - off;
memcpy(buf, outbuf + off, count);
kfree(outbuf);
return count;
}
int __init rb_hardconfig_init(struct kobject *rb_kobj)
{
struct mtd_info *mtd;
size_t bytes_read, buflen;
const u8 *buf;
int i, ret;
u32 magic;
// TODO allow override
mtd = get_mtd_device_nm(RB_MTD_HARD_CONFIG);
if (IS_ERR(mtd))
return -ENODEV;
hc_buflen = mtd->size;
hc_buf = kmalloc(hc_buflen, GFP_KERNEL);
if (!hc_buf)
return -ENOMEM;
ret = mtd_read(mtd, 0, hc_buflen, &bytes_read, hc_buf);
if (bytes_read != hc_buflen) {
ret = -EIO;
goto fail;
}
/* Check we have what we expect */
magic = *(const u32 *)hc_buf;
if (RB_MAGIC_HARD != magic) {
ret = -EINVAL;
goto fail;
}
/* Skip magic */
buf = hc_buf + sizeof(magic);
buflen = hc_buflen - sizeof(magic);
/* Populate sysfs */
ret = -ENOMEM;
hc_kobj = kobject_create_and_add(RB_MTD_HARD_CONFIG, rb_kobj);
if (!hc_kobj)
goto fail;
/* Locate and publish all known tags */
for (i = 0; i < ARRAY_SIZE(hc_attrs); i++) {
ret = routerboot_tag_find(buf, buflen, hc_attrs[i].tag_id,
&hc_attrs[i].pld_ofs, &hc_attrs[i].pld_len);
if (ret) {
hc_attrs[i].pld_ofs = hc_attrs[i].pld_len = 0;
continue;
}
/* Account for skipped magic */
hc_attrs[i].pld_ofs += sizeof(magic);
/* Special case RB_ID_WLAN_DATA to prep and create the binary attribute */
if ((RB_ID_WLAN_DATA == hc_attrs[i].tag_id) && hc_attrs[i].pld_len) {
hc_wlandata_battr.pld_ofs = hc_attrs[i].pld_ofs;
hc_wlandata_battr.pld_len = hc_attrs[i].pld_len;
ret = sysfs_create_bin_file(hc_kobj, &hc_wlandata_battr.battr);
if (ret)
pr_err(RB_HC_PR_PFX "Could not create %s sysfs entry (%d)\n",
hc_wlandata_battr.battr.attr.name, ret);
}
/* All other tags are published via standard attributes */
else {
ret = sysfs_create_file(hc_kobj, &hc_attrs[i].kattr.attr);
if (ret)
pr_err(RB_HC_PR_PFX "Could not create %s sysfs entry (%d)\n",
hc_attrs[i].kattr.attr.name, ret);
}
}
pr_info("MikroTik RouterBOARD hardware configuration sysfs driver v" RB_HARDCONFIG_VER "\n");
return 0;
fail:
kfree(hc_buf);
return ret;
}
void __exit rb_hardconfig_exit(void)
{
kobject_put(hc_kobj);
kfree(hc_buf);
}

@ -0,0 +1,183 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Driver for MikroTik RouterBoot flash data. Common routines.
*
* Copyright (C) 2020 Thibaut VARÈNE <hacks+kernel@slashdirt.org>
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 as published
* by the Free Software Foundation.
*/
#include <linux/types.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/sysfs.h>
#include "routerboot.h"
static struct kobject *rb_kobj;
/**
* routerboot_tag_find() - Locate a given tag in routerboot config data.
* @bufhead: the buffer to look into. Must start with a tag node.
* @buflen: size of bufhead
* @tag_id: the tag identifier to look for
* @pld_ofs: will be updated with tag payload offset in bufhead, if tag found
* @pld_len: will be updated with tag payload size, if tag found
*
* This incarnation of tag_find() does only that: it finds a specific routerboot
* tag node in the input buffer. Routerboot tag nodes are u32 values:
* - The low nibble is the tag identification number,
* - The high nibble is the tag payload length (node excluded) in bytes.
* The payload immediately follows the tag node. Tag nodes are 32bit-aligned.
* The returned pld_ofs will always be aligned. pld_len may not end on 32bit
* boundary (the only known case is when parsing ERD data).
* The nodes are cpu-endian on the flash media. The payload is cpu-endian when
* applicable. Tag nodes are not ordered (by ID) on flash.
*
* Return: 0 on success (tag found) or errno
*/
int routerboot_tag_find(const u8 *bufhead, const size_t buflen, const u16 tag_id,
u16 *pld_ofs, u16 *pld_len)
{
const u32 *datum, *bufend;
u32 node;
u16 id, len;
int ret;
if (!bufhead || !tag_id)
return -EINVAL;
ret = -ENOENT;
datum = (const u32 *)bufhead;
bufend = (const u32 *)(bufhead + buflen);
while (datum < bufend) {
node = *datum++;
/* Tag list ends with null node */
if (!node)
break;
id = node & 0xFFFF;
len = node >> 16;
if (tag_id == id) {
if (datum >= bufend)
break;
if (pld_ofs)
*pld_ofs = (u16)((u8 *)datum - bufhead);
if (pld_len)
*pld_len = len;
ret = 0;
break;
}
/*
* The only known situation where len may not end on 32bit
* boundary is within ERD data. Since we're only extracting
* one tag (the first and only one) from that data, we should
* never need to forcefully ALIGN(). Do it anyway, this is not a
* performance path.
*/
len = ALIGN(len, sizeof(*datum));
datum += len / sizeof(*datum);
}
return ret;
}
/**
* routerboot_rle_decode() - Simple RLE (MikroTik variant) decoding routine.
* @in: input buffer to decode
* @inlen: size of in
* @out: output buffer to write decoded data to
* @outlen: pointer to out size when function is called, will be updated with
* size of decoded output on return
*
* MikroTik's variant of RLE operates as follows, considering a signed run byte:
* - positive run => classic RLE
* - negative run => the next -<run> bytes must be copied verbatim
* The API is matched to the lzo1x routines for convenience.
*
* NB: The output buffer cannot overlap with the input buffer.
*
* Return: 0 on success or errno
*/
int routerboot_rle_decode(const u8 *in, size_t inlen, u8 *out, size_t *outlen)
{
int ret, run, nbytes; // use native types for speed
u8 byte;
if (!in || (inlen < 2) || !out)
return -EINVAL;
ret = -ENOSPC;
nbytes = 0;
while (inlen >= 2) {
run = *in++;
inlen--;
/* Verbatim copies */
if (run & 0x80) {
/* Invert run byte sign */
run = ~run & 0xFF;
run++;
if (run > inlen)
goto fail;
inlen -= run;
nbytes += run;
if (nbytes > *outlen)
goto fail;
/* Basic memcpy */
while (run-- > 0)
*out++ = *in++;
}
/* Stream of half-words RLE: <run><byte>. run == 0 is ignored */
else {
byte = *in++;
inlen--;
nbytes += run;
if (nbytes > *outlen)
goto fail;
while (run-- > 0)
*out++ = byte;
}
}
ret = 0;
fail:
*outlen = nbytes;
return ret;
}
static int __init routerboot_init(void)
{
rb_kobj = kobject_create_and_add("mikrotik", firmware_kobj);
if (!rb_kobj)
return -ENOMEM;
return rb_hardconfig_init(rb_kobj);
}
static void __exit routerboot_exit(void)
{
rb_hardconfig_exit();
kobject_put(rb_kobj); // recursive afaict
}
module_init(routerboot_init);
module_exit(routerboot_exit);
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("MikroTik RouterBoot sysfs support");
MODULE_AUTHOR("Thibaut VARENE");

@ -0,0 +1,31 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Common definitions for MikroTik RouterBoot data.
*
* Copyright (C) 2020 Thibaut VARÈNE <hacks+kernel@slashdirt.org>
*/
#ifndef _ROUTERBOOT_H_
#define _ROUTERBOOT_H_
#include <linux/types.h>
// these magic values are stored in cpu-endianness on flash
#define RB_MAGIC_HARD (('H') | ('a' << 8) | ('r' << 16) | ('d' << 24))
#define RB_MAGIC_SOFT (('S') | ('o' << 8) | ('f' << 16) | ('t' << 24))
#define RB_MAGIC_LZOR (('L') | ('Z' << 8) | ('O' << 16) | ('R' << 24))
#define RB_MAGIC_ERD (('E' << 16) | ('R' << 8) | ('D'))
#define RB_ART_SIZE 0x10000
#define RB_MTD_HARD_CONFIG "hard_config"
#define RB_MTD_SOFT_CONFIG "soft_config"
int routerboot_tag_find(const u8 *bufhead, const size_t buflen, const u16 tag_id, u16 *pld_ofs, u16 *pld_len);
int routerboot_rle_decode(const u8 *in, size_t inlen, u8 *out, size_t *outlen);
int __init rb_hardconfig_init(struct kobject *rb_kobj);
void __exit rb_hardconfig_exit(void);
#endif /* _ROUTERBOOT_H_ */
Loading…
Cancel
Save