|
@@ -0,0 +1,422 @@
|
|
|
+From 8eff8eb83fc0ae8b5f76220e2bb8644d836e99ff Mon Sep 17 00:00:00 2001
|
|
|
+From: Nicolas Frattaroli <[email protected]>
|
|
|
+Date: Tue, 4 Feb 2025 16:35:50 +0100
|
|
|
+Subject: [PATCH] hwrng: rockchip - add support for rk3588's standalone TRNG
|
|
|
+
|
|
|
+The RK3588 SoC includes several TRNGs, one part of the Crypto IP block,
|
|
|
+and the other one (referred to as "trngv1") as a standalone new IP.
|
|
|
+
|
|
|
+Add support for this new standalone TRNG to the driver by both
|
|
|
+generalising it to support multiple different rockchip RNGs and then
|
|
|
+implementing the required functionality for the new hardware.
|
|
|
+
|
|
|
+This work was partly based on the downstream vendor driver by Rockchip's
|
|
|
+Lin Jinhan, which is why they are listed as a Co-author.
|
|
|
+
|
|
|
+While the hardware does support notifying the CPU with an IRQ when the
|
|
|
+random data is ready, I've discovered while implementing the code to use
|
|
|
+this interrupt that this results in significantly slower throughput of
|
|
|
+the TRNG even when under heavy CPU load. I assume this is because with
|
|
|
+only 32 bytes of data per invocation, the overhead of reinitialising a
|
|
|
+completion, enabling the interrupt, sleeping and then triggering the
|
|
|
+completion in the IRQ handler is way more expensive than busylooping.
|
|
|
+
|
|
|
+Speaking of busylooping, the poll interval for reading the ISTAT is an
|
|
|
+atomic read with a delay of 0. In my testing, I've found that this gives
|
|
|
+us the largest throughput, and it appears the random data is ready
|
|
|
+pretty much the moment we begin polling, as increasing the poll delay
|
|
|
+leads to a drop in throughput significant enough to not just be due to
|
|
|
+the poll interval missing the ideal timing by a microsecond or two.
|
|
|
+
|
|
|
+According to downstream, the IP should take 1024 clock cycles to
|
|
|
+generate 56 bits of random data, which at 150MHz should work out to
|
|
|
+6.8us. I did not test whether the data really does take 256/56*6.8us
|
|
|
+to arrive, though changing the readl to a __raw_readl makes no
|
|
|
+difference in throughput, and this data does pass the rngtest FIPS
|
|
|
+checks, so I'm not entirely sure what's going on but I presume it's got
|
|
|
+something to do with the AHB bus speed and the memory barriers that
|
|
|
+mainline's readl/writel functions insert.
|
|
|
+
|
|
|
+The only other current SoC that uses this new IP is the Rockchip RV1106,
|
|
|
+but that SoC does not have mainline support as of the time of writing,
|
|
|
+so we make no effort to declare it as supported for now.
|
|
|
+
|
|
|
+Co-developed-by: Lin Jinhan <[email protected]>
|
|
|
+Signed-off-by: Lin Jinhan <[email protected]>
|
|
|
+Signed-off-by: Nicolas Frattaroli <[email protected]>
|
|
|
+Signed-off-by: Herbert Xu <[email protected]>
|
|
|
+---
|
|
|
+ drivers/char/hw_random/Kconfig | 3 +-
|
|
|
+ drivers/char/hw_random/rockchip-rng.c | 234 +++++++++++++++++++++++---
|
|
|
+ 2 files changed, 216 insertions(+), 21 deletions(-)
|
|
|
+
|
|
|
+--- a/drivers/char/hw_random/Kconfig
|
|
|
++++ b/drivers/char/hw_random/Kconfig
|
|
|
+@@ -580,7 +580,8 @@ config HW_RANDOM_ROCKCHIP
|
|
|
+ default HW_RANDOM
|
|
|
+ help
|
|
|
+ This driver provides kernel-side support for the True Random Number
|
|
|
+- Generator hardware found on some Rockchip SoC like RK3566 or RK3568.
|
|
|
++ Generator hardware found on some Rockchip SoCs like RK3566, RK3568
|
|
|
++ or RK3588.
|
|
|
+
|
|
|
+ To compile this driver as a module, choose M here: the
|
|
|
+ module will be called rockchip-rng.
|
|
|
+--- a/drivers/char/hw_random/rockchip-rng.c
|
|
|
++++ b/drivers/char/hw_random/rockchip-rng.c
|
|
|
+@@ -1,12 +1,14 @@
|
|
|
+ // SPDX-License-Identifier: GPL-2.0
|
|
|
+ /*
|
|
|
+- * rockchip-rng.c True Random Number Generator driver for Rockchip RK3568 SoC
|
|
|
++ * rockchip-rng.c True Random Number Generator driver for Rockchip SoCs
|
|
|
+ *
|
|
|
+ * Copyright (c) 2018, Fuzhou Rockchip Electronics Co., Ltd.
|
|
|
+ * Copyright (c) 2022, Aurelien Jarno
|
|
|
++ * Copyright (c) 2025, Collabora Ltd.
|
|
|
+ * Authors:
|
|
|
+ * Lin Jinhan <[email protected]>
|
|
|
+ * Aurelien Jarno <[email protected]>
|
|
|
++ * Nicolas Frattaroli <[email protected]>
|
|
|
+ */
|
|
|
+ #include <linux/clk.h>
|
|
|
+ #include <linux/hw_random.h>
|
|
|
+@@ -32,6 +34,9 @@
|
|
|
+ */
|
|
|
+ #define RK_RNG_SAMPLE_CNT 1000
|
|
|
+
|
|
|
++/* after how many bytes of output TRNGv1 implementations should be reseeded */
|
|
|
++#define RK_TRNG_V1_AUTO_RESEED_CNT 16000
|
|
|
++
|
|
|
+ /* TRNG registers from RK3568 TRM-Part2, section 5.4.1 */
|
|
|
+ #define TRNG_RST_CTL 0x0004
|
|
|
+ #define TRNG_RNG_CTL 0x0400
|
|
|
+@@ -49,25 +54,85 @@
|
|
|
+ #define TRNG_RNG_SAMPLE_CNT 0x0404
|
|
|
+ #define TRNG_RNG_DOUT 0x0410
|
|
|
+
|
|
|
++/*
|
|
|
++ * TRNG V1 register definitions
|
|
|
++ * The TRNG V1 IP is a stand-alone TRNG implementation (not part of a crypto IP)
|
|
|
++ * and can be found in the Rockchip RK3588 SoC
|
|
|
++ */
|
|
|
++#define TRNG_V1_CTRL 0x0000
|
|
|
++#define TRNG_V1_CTRL_NOP 0x00
|
|
|
++#define TRNG_V1_CTRL_RAND 0x01
|
|
|
++#define TRNG_V1_CTRL_SEED 0x02
|
|
|
++
|
|
|
++#define TRNG_V1_STAT 0x0004
|
|
|
++#define TRNG_V1_STAT_SEEDED BIT(9)
|
|
|
++#define TRNG_V1_STAT_GENERATING BIT(30)
|
|
|
++#define TRNG_V1_STAT_RESEEDING BIT(31)
|
|
|
++
|
|
|
++#define TRNG_V1_MODE 0x0008
|
|
|
++#define TRNG_V1_MODE_128_BIT (0x00 << 3)
|
|
|
++#define TRNG_V1_MODE_256_BIT (0x01 << 3)
|
|
|
++
|
|
|
++/* Interrupt Enable register; unused because polling is faster */
|
|
|
++#define TRNG_V1_IE 0x0010
|
|
|
++#define TRNG_V1_IE_GLBL_EN BIT(31)
|
|
|
++#define TRNG_V1_IE_SEED_DONE_EN BIT(1)
|
|
|
++#define TRNG_V1_IE_RAND_RDY_EN BIT(0)
|
|
|
++
|
|
|
++#define TRNG_V1_ISTAT 0x0014
|
|
|
++#define TRNG_V1_ISTAT_RAND_RDY BIT(0)
|
|
|
++
|
|
|
++/* RAND0 ~ RAND7 */
|
|
|
++#define TRNG_V1_RAND0 0x0020
|
|
|
++#define TRNG_V1_RAND7 0x003C
|
|
|
++
|
|
|
++/* Auto Reseed Register */
|
|
|
++#define TRNG_V1_AUTO_RQSTS 0x0060
|
|
|
++
|
|
|
++#define TRNG_V1_VERSION 0x00F0
|
|
|
++#define TRNG_v1_VERSION_CODE 0x46bc
|
|
|
++/* end of TRNG_V1 register definitions */
|
|
|
++
|
|
|
++/* Before removing this assert, give rk3588_rng_read an upper bound of 32 */
|
|
|
++static_assert(RK_RNG_MAX_BYTE <= (TRNG_V1_RAND7 + 4 - TRNG_V1_RAND0),
|
|
|
++ "You raised RK_RNG_MAX_BYTE and broke rk3588-rng, congrats.");
|
|
|
++
|
|
|
+ struct rk_rng {
|
|
|
+ struct hwrng rng;
|
|
|
+ void __iomem *base;
|
|
|
+ int clk_num;
|
|
|
+ struct clk_bulk_data *clk_bulks;
|
|
|
++ const struct rk_rng_soc_data *soc_data;
|
|
|
+ struct device *dev;
|
|
|
+ };
|
|
|
+
|
|
|
++struct rk_rng_soc_data {
|
|
|
++ int (*rk_rng_init)(struct hwrng *rng);
|
|
|
++ int (*rk_rng_read)(struct hwrng *rng, void *buf, size_t max, bool wait);
|
|
|
++ void (*rk_rng_cleanup)(struct hwrng *rng);
|
|
|
++ unsigned short quality;
|
|
|
++ bool reset_optional;
|
|
|
++};
|
|
|
++
|
|
|
+ /* The mask in the upper 16 bits determines the bits that are updated */
|
|
|
+ static void rk_rng_write_ctl(struct rk_rng *rng, u32 val, u32 mask)
|
|
|
+ {
|
|
|
+ writel((mask << 16) | val, rng->base + TRNG_RNG_CTL);
|
|
|
+ }
|
|
|
+
|
|
|
+-static int rk_rng_init(struct hwrng *rng)
|
|
|
++static inline void rk_rng_writel(struct rk_rng *rng, u32 val, u32 offset)
|
|
|
+ {
|
|
|
+- struct rk_rng *rk_rng = container_of(rng, struct rk_rng, rng);
|
|
|
+- int ret;
|
|
|
++ writel(val, rng->base + offset);
|
|
|
++}
|
|
|
+
|
|
|
++static inline u32 rk_rng_readl(struct rk_rng *rng, u32 offset)
|
|
|
++{
|
|
|
++ return readl(rng->base + offset);
|
|
|
++}
|
|
|
++
|
|
|
++static int rk_rng_enable_clks(struct rk_rng *rk_rng)
|
|
|
++{
|
|
|
++ int ret;
|
|
|
+ /* start clocks */
|
|
|
+ ret = clk_bulk_prepare_enable(rk_rng->clk_num, rk_rng->clk_bulks);
|
|
|
+ if (ret < 0) {
|
|
|
+@@ -75,6 +140,18 @@ static int rk_rng_init(struct hwrng *rng
|
|
|
+ return ret;
|
|
|
+ }
|
|
|
+
|
|
|
++ return 0;
|
|
|
++}
|
|
|
++
|
|
|
++static int rk3568_rng_init(struct hwrng *rng)
|
|
|
++{
|
|
|
++ struct rk_rng *rk_rng = container_of(rng, struct rk_rng, rng);
|
|
|
++ int ret;
|
|
|
++
|
|
|
++ ret = rk_rng_enable_clks(rk_rng);
|
|
|
++ if (ret < 0)
|
|
|
++ return ret;
|
|
|
++
|
|
|
+ /* set the sample period */
|
|
|
+ writel(RK_RNG_SAMPLE_CNT, rk_rng->base + TRNG_RNG_SAMPLE_CNT);
|
|
|
+
|
|
|
+@@ -87,7 +164,7 @@ static int rk_rng_init(struct hwrng *rng
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+-static void rk_rng_cleanup(struct hwrng *rng)
|
|
|
++static void rk3568_rng_cleanup(struct hwrng *rng)
|
|
|
+ {
|
|
|
+ struct rk_rng *rk_rng = container_of(rng, struct rk_rng, rng);
|
|
|
+
|
|
|
+@@ -98,7 +175,7 @@ static void rk_rng_cleanup(struct hwrng
|
|
|
+ clk_bulk_disable_unprepare(rk_rng->clk_num, rk_rng->clk_bulks);
|
|
|
+ }
|
|
|
+
|
|
|
+-static int rk_rng_read(struct hwrng *rng, void *buf, size_t max, bool wait)
|
|
|
++static int rk3568_rng_read(struct hwrng *rng, void *buf, size_t max, bool wait)
|
|
|
+ {
|
|
|
+ struct rk_rng *rk_rng = container_of(rng, struct rk_rng, rng);
|
|
|
+ size_t to_read = min_t(size_t, max, RK_RNG_MAX_BYTE);
|
|
|
+@@ -128,6 +205,114 @@ out:
|
|
|
+ return (ret < 0) ? ret : to_read;
|
|
|
+ }
|
|
|
+
|
|
|
++static int rk3588_rng_init(struct hwrng *rng)
|
|
|
++{
|
|
|
++ struct rk_rng *rk_rng = container_of(rng, struct rk_rng, rng);
|
|
|
++ u32 version, status, mask, istat;
|
|
|
++ int ret;
|
|
|
++
|
|
|
++ ret = rk_rng_enable_clks(rk_rng);
|
|
|
++ if (ret < 0)
|
|
|
++ return ret;
|
|
|
++
|
|
|
++ version = rk_rng_readl(rk_rng, TRNG_V1_VERSION);
|
|
|
++ if (version != TRNG_v1_VERSION_CODE) {
|
|
|
++ dev_err(rk_rng->dev,
|
|
|
++ "wrong trng version, expected = %08x, actual = %08x\n",
|
|
|
++ TRNG_V1_VERSION, version);
|
|
|
++ ret = -EFAULT;
|
|
|
++ goto err_disable_clk;
|
|
|
++ }
|
|
|
++
|
|
|
++ mask = TRNG_V1_STAT_SEEDED | TRNG_V1_STAT_GENERATING |
|
|
|
++ TRNG_V1_STAT_RESEEDING;
|
|
|
++ if (readl_poll_timeout(rk_rng->base + TRNG_V1_STAT, status,
|
|
|
++ (status & mask) == TRNG_V1_STAT_SEEDED,
|
|
|
++ RK_RNG_POLL_PERIOD_US, RK_RNG_POLL_TIMEOUT_US) < 0) {
|
|
|
++ dev_err(rk_rng->dev, "timed out waiting for hwrng to reseed\n");
|
|
|
++ ret = -ETIMEDOUT;
|
|
|
++ goto err_disable_clk;
|
|
|
++ }
|
|
|
++
|
|
|
++ /*
|
|
|
++ * clear ISTAT flag, downstream advises to do this to avoid
|
|
|
++ * auto-reseeding "on power on"
|
|
|
++ */
|
|
|
++ istat = rk_rng_readl(rk_rng, TRNG_V1_ISTAT);
|
|
|
++ rk_rng_writel(rk_rng, istat, TRNG_V1_ISTAT);
|
|
|
++
|
|
|
++ /* auto reseed after RK_TRNG_V1_AUTO_RESEED_CNT bytes */
|
|
|
++ rk_rng_writel(rk_rng, RK_TRNG_V1_AUTO_RESEED_CNT / 16, TRNG_V1_AUTO_RQSTS);
|
|
|
++
|
|
|
++ return 0;
|
|
|
++err_disable_clk:
|
|
|
++ clk_bulk_disable_unprepare(rk_rng->clk_num, rk_rng->clk_bulks);
|
|
|
++ return ret;
|
|
|
++}
|
|
|
++
|
|
|
++static void rk3588_rng_cleanup(struct hwrng *rng)
|
|
|
++{
|
|
|
++ struct rk_rng *rk_rng = container_of(rng, struct rk_rng, rng);
|
|
|
++
|
|
|
++ clk_bulk_disable_unprepare(rk_rng->clk_num, rk_rng->clk_bulks);
|
|
|
++}
|
|
|
++
|
|
|
++static int rk3588_rng_read(struct hwrng *rng, void *buf, size_t max, bool wait)
|
|
|
++{
|
|
|
++ struct rk_rng *rk_rng = container_of(rng, struct rk_rng, rng);
|
|
|
++ size_t to_read = min_t(size_t, max, RK_RNG_MAX_BYTE);
|
|
|
++ int ret = 0;
|
|
|
++ u32 reg;
|
|
|
++
|
|
|
++ ret = pm_runtime_resume_and_get(rk_rng->dev);
|
|
|
++ if (ret < 0)
|
|
|
++ return ret;
|
|
|
++
|
|
|
++ /* Clear ISTAT, even without interrupts enabled, this will be updated */
|
|
|
++ reg = rk_rng_readl(rk_rng, TRNG_V1_ISTAT);
|
|
|
++ rk_rng_writel(rk_rng, reg, TRNG_V1_ISTAT);
|
|
|
++
|
|
|
++ /* generate 256 bits of random data */
|
|
|
++ rk_rng_writel(rk_rng, TRNG_V1_MODE_256_BIT, TRNG_V1_MODE);
|
|
|
++ rk_rng_writel(rk_rng, TRNG_V1_CTRL_RAND, TRNG_V1_CTRL);
|
|
|
++
|
|
|
++ ret = readl_poll_timeout_atomic(rk_rng->base + TRNG_V1_ISTAT, reg,
|
|
|
++ (reg & TRNG_V1_ISTAT_RAND_RDY), 0,
|
|
|
++ RK_RNG_POLL_TIMEOUT_US);
|
|
|
++ if (ret < 0)
|
|
|
++ goto out;
|
|
|
++
|
|
|
++ /* Read random data that's in registers TRNG_V1_RAND0 through RAND7 */
|
|
|
++ memcpy_fromio(buf, rk_rng->base + TRNG_V1_RAND0, to_read);
|
|
|
++
|
|
|
++out:
|
|
|
++ /* Clear ISTAT */
|
|
|
++ rk_rng_writel(rk_rng, reg, TRNG_V1_ISTAT);
|
|
|
++ /* close the TRNG */
|
|
|
++ rk_rng_writel(rk_rng, TRNG_V1_CTRL_NOP, TRNG_V1_CTRL);
|
|
|
++
|
|
|
++ pm_runtime_mark_last_busy(rk_rng->dev);
|
|
|
++ pm_runtime_put_sync_autosuspend(rk_rng->dev);
|
|
|
++
|
|
|
++ return (ret < 0) ? ret : to_read;
|
|
|
++}
|
|
|
++
|
|
|
++static const struct rk_rng_soc_data rk3568_soc_data = {
|
|
|
++ .rk_rng_init = rk3568_rng_init,
|
|
|
++ .rk_rng_read = rk3568_rng_read,
|
|
|
++ .rk_rng_cleanup = rk3568_rng_cleanup,
|
|
|
++ .quality = 900,
|
|
|
++ .reset_optional = false,
|
|
|
++};
|
|
|
++
|
|
|
++static const struct rk_rng_soc_data rk3588_soc_data = {
|
|
|
++ .rk_rng_init = rk3588_rng_init,
|
|
|
++ .rk_rng_read = rk3588_rng_read,
|
|
|
++ .rk_rng_cleanup = rk3588_rng_cleanup,
|
|
|
++ .quality = 999, /* as determined by actual testing */
|
|
|
++ .reset_optional = true,
|
|
|
++};
|
|
|
++
|
|
|
+ static int rk_rng_probe(struct platform_device *pdev)
|
|
|
+ {
|
|
|
+ struct device *dev = &pdev->dev;
|
|
|
+@@ -139,6 +324,7 @@ static int rk_rng_probe(struct platform_
|
|
|
+ if (!rk_rng)
|
|
|
+ return -ENOMEM;
|
|
|
+
|
|
|
++ rk_rng->soc_data = of_device_get_match_data(dev);
|
|
|
+ rk_rng->base = devm_platform_ioremap_resource(pdev, 0);
|
|
|
+ if (IS_ERR(rk_rng->base))
|
|
|
+ return PTR_ERR(rk_rng->base);
|
|
|
+@@ -148,24 +334,30 @@ static int rk_rng_probe(struct platform_
|
|
|
+ return dev_err_probe(dev, rk_rng->clk_num,
|
|
|
+ "Failed to get clks property\n");
|
|
|
+
|
|
|
+- rst = devm_reset_control_array_get_exclusive(dev);
|
|
|
+- if (IS_ERR(rst))
|
|
|
+- return dev_err_probe(dev, PTR_ERR(rst), "Failed to get reset property\n");
|
|
|
+-
|
|
|
+- reset_control_assert(rst);
|
|
|
+- udelay(2);
|
|
|
+- reset_control_deassert(rst);
|
|
|
++ if (rk_rng->soc_data->reset_optional)
|
|
|
++ rst = devm_reset_control_array_get_optional_exclusive(dev);
|
|
|
++ else
|
|
|
++ rst = devm_reset_control_array_get_exclusive(dev);
|
|
|
++
|
|
|
++ if (rst) {
|
|
|
++ if (IS_ERR(rst))
|
|
|
++ return dev_err_probe(dev, PTR_ERR(rst), "Failed to get reset property\n");
|
|
|
++
|
|
|
++ reset_control_assert(rst);
|
|
|
++ udelay(2);
|
|
|
++ reset_control_deassert(rst);
|
|
|
++ }
|
|
|
+
|
|
|
+ platform_set_drvdata(pdev, rk_rng);
|
|
|
+
|
|
|
+ rk_rng->rng.name = dev_driver_string(dev);
|
|
|
+ if (!IS_ENABLED(CONFIG_PM)) {
|
|
|
+- rk_rng->rng.init = rk_rng_init;
|
|
|
+- rk_rng->rng.cleanup = rk_rng_cleanup;
|
|
|
++ rk_rng->rng.init = rk_rng->soc_data->rk_rng_init;
|
|
|
++ rk_rng->rng.cleanup = rk_rng->soc_data->rk_rng_cleanup;
|
|
|
+ }
|
|
|
+- rk_rng->rng.read = rk_rng_read;
|
|
|
++ rk_rng->rng.read = rk_rng->soc_data->rk_rng_read;
|
|
|
+ rk_rng->dev = dev;
|
|
|
+- rk_rng->rng.quality = 900;
|
|
|
++ rk_rng->rng.quality = rk_rng->soc_data->quality;
|
|
|
+
|
|
|
+ pm_runtime_set_autosuspend_delay(dev, RK_RNG_AUTOSUSPEND_DELAY);
|
|
|
+ pm_runtime_use_autosuspend(dev);
|
|
|
+@@ -184,7 +376,7 @@ static int __maybe_unused rk_rng_runtime
|
|
|
+ {
|
|
|
+ struct rk_rng *rk_rng = dev_get_drvdata(dev);
|
|
|
+
|
|
|
+- rk_rng_cleanup(&rk_rng->rng);
|
|
|
++ rk_rng->soc_data->rk_rng_cleanup(&rk_rng->rng);
|
|
|
+
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+@@ -193,7 +385,7 @@ static int __maybe_unused rk_rng_runtime
|
|
|
+ {
|
|
|
+ struct rk_rng *rk_rng = dev_get_drvdata(dev);
|
|
|
+
|
|
|
+- return rk_rng_init(&rk_rng->rng);
|
|
|
++ return rk_rng->soc_data->rk_rng_init(&rk_rng->rng);
|
|
|
+ }
|
|
|
+
|
|
|
+ static const struct dev_pm_ops rk_rng_pm_ops = {
|
|
|
+@@ -204,7 +396,8 @@ static const struct dev_pm_ops rk_rng_pm
|
|
|
+ };
|
|
|
+
|
|
|
+ static const struct of_device_id rk_rng_dt_match[] = {
|
|
|
+- { .compatible = "rockchip,rk3568-rng", },
|
|
|
++ { .compatible = "rockchip,rk3568-rng", .data = (void *)&rk3568_soc_data },
|
|
|
++ { .compatible = "rockchip,rk3588-rng", .data = (void *)&rk3588_soc_data },
|
|
|
+ { /* sentinel */ },
|
|
|
+ };
|
|
|
+
|
|
|
+@@ -221,8 +414,9 @@ static struct platform_driver rk_rng_dri
|
|
|
+
|
|
|
+ module_platform_driver(rk_rng_driver);
|
|
|
+
|
|
|
+-MODULE_DESCRIPTION("Rockchip RK3568 True Random Number Generator driver");
|
|
|
++MODULE_DESCRIPTION("Rockchip True Random Number Generator driver");
|
|
|
+ MODULE_AUTHOR("Lin Jinhan <[email protected]>");
|
|
|
+ MODULE_AUTHOR("Aurelien Jarno <[email protected]>");
|
|
|
+ MODULE_AUTHOR("Daniel Golle <[email protected]>");
|
|
|
++MODULE_AUTHOR("Nicolas Frattaroli <[email protected]>");
|
|
|
+ MODULE_LICENSE("GPL");
|