b0cc7491c9
The DT of_device.h and of_platform.h date back to the separate of_platform_bus_type before it as merged into the regular platform bus. As part of that merge prepping Arm DT support 13 years ago, they "temporarily" include each other. They also include platform_device.h and of.h. As a result, there's a pretty much random mix of those include files used throughout the tree. In order to detangle these headers and replace the implicit includes with struct declarations, users need to explicitly include the correct includes. Signed-off-by: Rob Herring <robh@kernel.org> Signed-off-by: Herbert Xu <herbert@gondor.apana.org.au>
546 lines
13 KiB
C
546 lines
13 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
/*
|
|
* sl3516-ce-core.c - hardware cryptographic offloader for Storlink SL3516 SoC
|
|
*
|
|
* Copyright (C) 2021 Corentin Labbe <clabbe@baylibre.com>
|
|
*
|
|
* Core file which registers crypto algorithms supported by the CryptoEngine
|
|
*/
|
|
|
|
#include <crypto/engine.h>
|
|
#include <crypto/internal/rng.h>
|
|
#include <crypto/internal/skcipher.h>
|
|
#include <linux/clk.h>
|
|
#include <linux/debugfs.h>
|
|
#include <linux/dev_printk.h>
|
|
#include <linux/dma-mapping.h>
|
|
#include <linux/err.h>
|
|
#include <linux/interrupt.h>
|
|
#include <linux/io.h>
|
|
#include <linux/irq.h>
|
|
#include <linux/kernel.h>
|
|
#include <linux/module.h>
|
|
#include <linux/of.h>
|
|
#include <linux/platform_device.h>
|
|
#include <linux/pm_runtime.h>
|
|
#include <linux/reset.h>
|
|
|
|
#include "sl3516-ce.h"
|
|
|
|
static int sl3516_ce_desc_init(struct sl3516_ce_dev *ce)
|
|
{
|
|
const size_t sz = sizeof(struct descriptor) * MAXDESC;
|
|
int i;
|
|
|
|
ce->tx = dma_alloc_coherent(ce->dev, sz, &ce->dtx, GFP_KERNEL);
|
|
if (!ce->tx)
|
|
return -ENOMEM;
|
|
ce->rx = dma_alloc_coherent(ce->dev, sz, &ce->drx, GFP_KERNEL);
|
|
if (!ce->rx)
|
|
goto err_rx;
|
|
|
|
for (i = 0; i < MAXDESC; i++) {
|
|
ce->tx[i].frame_ctrl.bits.own = CE_CPU;
|
|
ce->tx[i].next_desc.next_descriptor = ce->dtx + (i + 1) * sizeof(struct descriptor);
|
|
}
|
|
ce->tx[MAXDESC - 1].next_desc.next_descriptor = ce->dtx;
|
|
|
|
for (i = 0; i < MAXDESC; i++) {
|
|
ce->rx[i].frame_ctrl.bits.own = CE_CPU;
|
|
ce->rx[i].next_desc.next_descriptor = ce->drx + (i + 1) * sizeof(struct descriptor);
|
|
}
|
|
ce->rx[MAXDESC - 1].next_desc.next_descriptor = ce->drx;
|
|
|
|
ce->pctrl = dma_alloc_coherent(ce->dev, sizeof(struct pkt_control_ecb),
|
|
&ce->dctrl, GFP_KERNEL);
|
|
if (!ce->pctrl)
|
|
goto err_pctrl;
|
|
|
|
return 0;
|
|
err_pctrl:
|
|
dma_free_coherent(ce->dev, sz, ce->rx, ce->drx);
|
|
err_rx:
|
|
dma_free_coherent(ce->dev, sz, ce->tx, ce->dtx);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
static void sl3516_ce_free_descs(struct sl3516_ce_dev *ce)
|
|
{
|
|
const size_t sz = sizeof(struct descriptor) * MAXDESC;
|
|
|
|
dma_free_coherent(ce->dev, sz, ce->tx, ce->dtx);
|
|
dma_free_coherent(ce->dev, sz, ce->rx, ce->drx);
|
|
dma_free_coherent(ce->dev, sizeof(struct pkt_control_ecb), ce->pctrl,
|
|
ce->dctrl);
|
|
}
|
|
|
|
static void start_dma_tx(struct sl3516_ce_dev *ce)
|
|
{
|
|
u32 v;
|
|
|
|
v = TXDMA_CTRL_START | TXDMA_CTRL_CHAIN_MODE | TXDMA_CTRL_CONTINUE | \
|
|
TXDMA_CTRL_INT_FAIL | TXDMA_CTRL_INT_PERR | TXDMA_CTRL_BURST_UNK;
|
|
|
|
writel(v, ce->base + IPSEC_TXDMA_CTRL);
|
|
}
|
|
|
|
static void start_dma_rx(struct sl3516_ce_dev *ce)
|
|
{
|
|
u32 v;
|
|
|
|
v = RXDMA_CTRL_START | RXDMA_CTRL_CHAIN_MODE | RXDMA_CTRL_CONTINUE | \
|
|
RXDMA_CTRL_BURST_UNK | RXDMA_CTRL_INT_FINISH | \
|
|
RXDMA_CTRL_INT_FAIL | RXDMA_CTRL_INT_PERR | \
|
|
RXDMA_CTRL_INT_EOD | RXDMA_CTRL_INT_EOF;
|
|
|
|
writel(v, ce->base + IPSEC_RXDMA_CTRL);
|
|
}
|
|
|
|
static struct descriptor *get_desc_tx(struct sl3516_ce_dev *ce)
|
|
{
|
|
struct descriptor *dd;
|
|
|
|
dd = &ce->tx[ce->ctx];
|
|
ce->ctx++;
|
|
if (ce->ctx >= MAXDESC)
|
|
ce->ctx = 0;
|
|
return dd;
|
|
}
|
|
|
|
static struct descriptor *get_desc_rx(struct sl3516_ce_dev *ce)
|
|
{
|
|
struct descriptor *rdd;
|
|
|
|
rdd = &ce->rx[ce->crx];
|
|
ce->crx++;
|
|
if (ce->crx >= MAXDESC)
|
|
ce->crx = 0;
|
|
return rdd;
|
|
}
|
|
|
|
int sl3516_ce_run_task(struct sl3516_ce_dev *ce, struct sl3516_ce_cipher_req_ctx *rctx,
|
|
const char *name)
|
|
{
|
|
struct descriptor *dd, *rdd = NULL;
|
|
u32 v;
|
|
int i, err = 0;
|
|
|
|
ce->stat_req++;
|
|
|
|
reinit_completion(&ce->complete);
|
|
ce->status = 0;
|
|
|
|
for (i = 0; i < rctx->nr_sgd; i++) {
|
|
dev_dbg(ce->dev, "%s handle DST SG %d/%d len=%d\n", __func__,
|
|
i, rctx->nr_sgd, rctx->t_dst[i].len);
|
|
rdd = get_desc_rx(ce);
|
|
rdd->buf_adr = rctx->t_dst[i].addr;
|
|
rdd->frame_ctrl.bits.buffer_size = rctx->t_dst[i].len;
|
|
rdd->frame_ctrl.bits.own = CE_DMA;
|
|
}
|
|
rdd->next_desc.bits.eofie = 1;
|
|
|
|
for (i = 0; i < rctx->nr_sgs; i++) {
|
|
dev_dbg(ce->dev, "%s handle SRC SG %d/%d len=%d\n", __func__,
|
|
i, rctx->nr_sgs, rctx->t_src[i].len);
|
|
rctx->h->algorithm_len = rctx->t_src[i].len;
|
|
|
|
dd = get_desc_tx(ce);
|
|
dd->frame_ctrl.raw = 0;
|
|
dd->flag_status.raw = 0;
|
|
dd->frame_ctrl.bits.buffer_size = rctx->pctrllen;
|
|
dd->buf_adr = ce->dctrl;
|
|
dd->flag_status.tx_flag.tqflag = rctx->tqflag;
|
|
dd->next_desc.bits.eofie = 0;
|
|
dd->next_desc.bits.dec = 0;
|
|
dd->next_desc.bits.sof_eof = DESC_FIRST | DESC_LAST;
|
|
dd->frame_ctrl.bits.own = CE_DMA;
|
|
|
|
dd = get_desc_tx(ce);
|
|
dd->frame_ctrl.raw = 0;
|
|
dd->flag_status.raw = 0;
|
|
dd->frame_ctrl.bits.buffer_size = rctx->t_src[i].len;
|
|
dd->buf_adr = rctx->t_src[i].addr;
|
|
dd->flag_status.tx_flag.tqflag = 0;
|
|
dd->next_desc.bits.eofie = 0;
|
|
dd->next_desc.bits.dec = 0;
|
|
dd->next_desc.bits.sof_eof = DESC_FIRST | DESC_LAST;
|
|
dd->frame_ctrl.bits.own = CE_DMA;
|
|
start_dma_tx(ce);
|
|
start_dma_rx(ce);
|
|
}
|
|
wait_for_completion_interruptible_timeout(&ce->complete,
|
|
msecs_to_jiffies(5000));
|
|
if (ce->status == 0) {
|
|
dev_err(ce->dev, "DMA timeout for %s\n", name);
|
|
err = -EFAULT;
|
|
}
|
|
v = readl(ce->base + IPSEC_STATUS_REG);
|
|
if (v & 0xFFF) {
|
|
dev_err(ce->dev, "IPSEC_STATUS_REG %x\n", v);
|
|
err = -EFAULT;
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
static irqreturn_t ce_irq_handler(int irq, void *data)
|
|
{
|
|
struct sl3516_ce_dev *ce = (struct sl3516_ce_dev *)data;
|
|
u32 v;
|
|
|
|
ce->stat_irq++;
|
|
|
|
v = readl(ce->base + IPSEC_DMA_STATUS);
|
|
writel(v, ce->base + IPSEC_DMA_STATUS);
|
|
|
|
if (v & DMA_STATUS_TS_DERR)
|
|
dev_err(ce->dev, "AHB bus Error While Tx !!!\n");
|
|
if (v & DMA_STATUS_TS_PERR)
|
|
dev_err(ce->dev, "Tx Descriptor Protocol Error !!!\n");
|
|
if (v & DMA_STATUS_RS_DERR)
|
|
dev_err(ce->dev, "AHB bus Error While Rx !!!\n");
|
|
if (v & DMA_STATUS_RS_PERR)
|
|
dev_err(ce->dev, "Rx Descriptor Protocol Error !!!\n");
|
|
|
|
if (v & DMA_STATUS_TS_EOFI)
|
|
ce->stat_irq_tx++;
|
|
if (v & DMA_STATUS_RS_EOFI) {
|
|
ce->status = 1;
|
|
complete(&ce->complete);
|
|
ce->stat_irq_rx++;
|
|
return IRQ_HANDLED;
|
|
}
|
|
|
|
return IRQ_HANDLED;
|
|
}
|
|
|
|
static struct sl3516_ce_alg_template ce_algs[] = {
|
|
{
|
|
.type = CRYPTO_ALG_TYPE_SKCIPHER,
|
|
.mode = ECB_AES,
|
|
.alg.skcipher.base = {
|
|
.base = {
|
|
.cra_name = "ecb(aes)",
|
|
.cra_driver_name = "ecb-aes-sl3516",
|
|
.cra_priority = 400,
|
|
.cra_blocksize = AES_BLOCK_SIZE,
|
|
.cra_flags = CRYPTO_ALG_TYPE_SKCIPHER |
|
|
CRYPTO_ALG_ASYNC | CRYPTO_ALG_NEED_FALLBACK,
|
|
.cra_ctxsize = sizeof(struct sl3516_ce_cipher_tfm_ctx),
|
|
.cra_module = THIS_MODULE,
|
|
.cra_alignmask = 0xf,
|
|
.cra_init = sl3516_ce_cipher_init,
|
|
.cra_exit = sl3516_ce_cipher_exit,
|
|
},
|
|
.min_keysize = AES_MIN_KEY_SIZE,
|
|
.max_keysize = AES_MAX_KEY_SIZE,
|
|
.setkey = sl3516_ce_aes_setkey,
|
|
.encrypt = sl3516_ce_skencrypt,
|
|
.decrypt = sl3516_ce_skdecrypt,
|
|
},
|
|
.alg.skcipher.op = {
|
|
.do_one_request = sl3516_ce_handle_cipher_request,
|
|
},
|
|
},
|
|
};
|
|
|
|
static int sl3516_ce_debugfs_show(struct seq_file *seq, void *v)
|
|
{
|
|
struct sl3516_ce_dev *ce = seq->private;
|
|
unsigned int i;
|
|
|
|
seq_printf(seq, "HWRNG %lu %lu\n",
|
|
ce->hwrng_stat_req, ce->hwrng_stat_bytes);
|
|
seq_printf(seq, "IRQ %lu\n", ce->stat_irq);
|
|
seq_printf(seq, "IRQ TX %lu\n", ce->stat_irq_tx);
|
|
seq_printf(seq, "IRQ RX %lu\n", ce->stat_irq_rx);
|
|
seq_printf(seq, "nreq %lu\n", ce->stat_req);
|
|
seq_printf(seq, "fallback SG count TX %lu\n", ce->fallback_sg_count_tx);
|
|
seq_printf(seq, "fallback SG count RX %lu\n", ce->fallback_sg_count_rx);
|
|
seq_printf(seq, "fallback modulo16 %lu\n", ce->fallback_mod16);
|
|
seq_printf(seq, "fallback align16 %lu\n", ce->fallback_align16);
|
|
seq_printf(seq, "fallback not same len %lu\n", ce->fallback_not_same_len);
|
|
|
|
for (i = 0; i < ARRAY_SIZE(ce_algs); i++) {
|
|
if (!ce_algs[i].ce)
|
|
continue;
|
|
switch (ce_algs[i].type) {
|
|
case CRYPTO_ALG_TYPE_SKCIPHER:
|
|
seq_printf(seq, "%s %s reqs=%lu fallback=%lu\n",
|
|
ce_algs[i].alg.skcipher.base.base.cra_driver_name,
|
|
ce_algs[i].alg.skcipher.base.base.cra_name,
|
|
ce_algs[i].stat_req, ce_algs[i].stat_fb);
|
|
break;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
DEFINE_SHOW_ATTRIBUTE(sl3516_ce_debugfs);
|
|
|
|
static int sl3516_ce_register_algs(struct sl3516_ce_dev *ce)
|
|
{
|
|
int err;
|
|
unsigned int i;
|
|
|
|
for (i = 0; i < ARRAY_SIZE(ce_algs); i++) {
|
|
ce_algs[i].ce = ce;
|
|
switch (ce_algs[i].type) {
|
|
case CRYPTO_ALG_TYPE_SKCIPHER:
|
|
dev_info(ce->dev, "DEBUG: Register %s\n",
|
|
ce_algs[i].alg.skcipher.base.base.cra_name);
|
|
err = crypto_engine_register_skcipher(&ce_algs[i].alg.skcipher);
|
|
if (err) {
|
|
dev_err(ce->dev, "Fail to register %s\n",
|
|
ce_algs[i].alg.skcipher.base.base.cra_name);
|
|
ce_algs[i].ce = NULL;
|
|
return err;
|
|
}
|
|
break;
|
|
default:
|
|
ce_algs[i].ce = NULL;
|
|
dev_err(ce->dev, "ERROR: tried to register an unknown algo\n");
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static void sl3516_ce_unregister_algs(struct sl3516_ce_dev *ce)
|
|
{
|
|
unsigned int i;
|
|
|
|
for (i = 0; i < ARRAY_SIZE(ce_algs); i++) {
|
|
if (!ce_algs[i].ce)
|
|
continue;
|
|
switch (ce_algs[i].type) {
|
|
case CRYPTO_ALG_TYPE_SKCIPHER:
|
|
dev_info(ce->dev, "Unregister %d %s\n", i,
|
|
ce_algs[i].alg.skcipher.base.base.cra_name);
|
|
crypto_engine_unregister_skcipher(&ce_algs[i].alg.skcipher);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void sl3516_ce_start(struct sl3516_ce_dev *ce)
|
|
{
|
|
ce->ctx = 0;
|
|
ce->crx = 0;
|
|
writel(ce->dtx, ce->base + IPSEC_TXDMA_CURR_DESC);
|
|
writel(ce->drx, ce->base + IPSEC_RXDMA_CURR_DESC);
|
|
writel(0, ce->base + IPSEC_DMA_STATUS);
|
|
}
|
|
|
|
/*
|
|
* Power management strategy: The device is suspended unless a TFM exists for
|
|
* one of the algorithms proposed by this driver.
|
|
*/
|
|
static int sl3516_ce_pm_suspend(struct device *dev)
|
|
{
|
|
struct sl3516_ce_dev *ce = dev_get_drvdata(dev);
|
|
|
|
reset_control_assert(ce->reset);
|
|
clk_disable_unprepare(ce->clks);
|
|
return 0;
|
|
}
|
|
|
|
static int sl3516_ce_pm_resume(struct device *dev)
|
|
{
|
|
struct sl3516_ce_dev *ce = dev_get_drvdata(dev);
|
|
int err;
|
|
|
|
err = clk_prepare_enable(ce->clks);
|
|
if (err) {
|
|
dev_err(ce->dev, "Cannot prepare_enable\n");
|
|
goto error;
|
|
}
|
|
err = reset_control_deassert(ce->reset);
|
|
if (err) {
|
|
dev_err(ce->dev, "Cannot deassert reset control\n");
|
|
goto error;
|
|
}
|
|
|
|
sl3516_ce_start(ce);
|
|
|
|
return 0;
|
|
error:
|
|
sl3516_ce_pm_suspend(dev);
|
|
return err;
|
|
}
|
|
|
|
static const struct dev_pm_ops sl3516_ce_pm_ops = {
|
|
SET_RUNTIME_PM_OPS(sl3516_ce_pm_suspend, sl3516_ce_pm_resume, NULL)
|
|
};
|
|
|
|
static int sl3516_ce_pm_init(struct sl3516_ce_dev *ce)
|
|
{
|
|
int err;
|
|
|
|
pm_runtime_use_autosuspend(ce->dev);
|
|
pm_runtime_set_autosuspend_delay(ce->dev, 2000);
|
|
|
|
err = pm_runtime_set_suspended(ce->dev);
|
|
if (err)
|
|
return err;
|
|
pm_runtime_enable(ce->dev);
|
|
return err;
|
|
}
|
|
|
|
static void sl3516_ce_pm_exit(struct sl3516_ce_dev *ce)
|
|
{
|
|
pm_runtime_disable(ce->dev);
|
|
}
|
|
|
|
static int sl3516_ce_probe(struct platform_device *pdev)
|
|
{
|
|
struct sl3516_ce_dev *ce;
|
|
int err, irq;
|
|
u32 v;
|
|
|
|
ce = devm_kzalloc(&pdev->dev, sizeof(*ce), GFP_KERNEL);
|
|
if (!ce)
|
|
return -ENOMEM;
|
|
|
|
ce->dev = &pdev->dev;
|
|
platform_set_drvdata(pdev, ce);
|
|
|
|
ce->base = devm_platform_ioremap_resource(pdev, 0);
|
|
if (IS_ERR(ce->base))
|
|
return PTR_ERR(ce->base);
|
|
|
|
irq = platform_get_irq(pdev, 0);
|
|
if (irq < 0)
|
|
return irq;
|
|
|
|
err = devm_request_irq(&pdev->dev, irq, ce_irq_handler, 0, "crypto", ce);
|
|
if (err) {
|
|
dev_err(ce->dev, "Cannot request Crypto Engine IRQ (err=%d)\n", err);
|
|
return err;
|
|
}
|
|
|
|
ce->reset = devm_reset_control_get(&pdev->dev, NULL);
|
|
if (IS_ERR(ce->reset))
|
|
return dev_err_probe(&pdev->dev, PTR_ERR(ce->reset),
|
|
"No reset control found\n");
|
|
ce->clks = devm_clk_get(ce->dev, NULL);
|
|
if (IS_ERR(ce->clks)) {
|
|
err = PTR_ERR(ce->clks);
|
|
dev_err(ce->dev, "Cannot get clock err=%d\n", err);
|
|
return err;
|
|
}
|
|
|
|
err = sl3516_ce_desc_init(ce);
|
|
if (err)
|
|
return err;
|
|
|
|
err = sl3516_ce_pm_init(ce);
|
|
if (err)
|
|
goto error_pm;
|
|
|
|
init_completion(&ce->complete);
|
|
|
|
ce->engine = crypto_engine_alloc_init(ce->dev, true);
|
|
if (!ce->engine) {
|
|
dev_err(ce->dev, "Cannot allocate engine\n");
|
|
err = -ENOMEM;
|
|
goto error_engine;
|
|
}
|
|
|
|
err = crypto_engine_start(ce->engine);
|
|
if (err) {
|
|
dev_err(ce->dev, "Cannot start engine\n");
|
|
goto error_engine;
|
|
}
|
|
|
|
err = sl3516_ce_register_algs(ce);
|
|
if (err)
|
|
goto error_alg;
|
|
|
|
err = sl3516_ce_rng_register(ce);
|
|
if (err)
|
|
goto error_rng;
|
|
|
|
err = pm_runtime_resume_and_get(ce->dev);
|
|
if (err < 0)
|
|
goto error_pmuse;
|
|
|
|
v = readl(ce->base + IPSEC_ID);
|
|
dev_info(ce->dev, "SL3516 dev %lx rev %lx\n",
|
|
v & GENMASK(31, 4),
|
|
v & GENMASK(3, 0));
|
|
v = readl(ce->base + IPSEC_DMA_DEVICE_ID);
|
|
dev_info(ce->dev, "SL3516 DMA dev %lx rev %lx\n",
|
|
v & GENMASK(15, 4),
|
|
v & GENMASK(3, 0));
|
|
|
|
pm_runtime_put_sync(ce->dev);
|
|
|
|
if (IS_ENABLED(CONFIG_CRYPTO_DEV_SL3516_DEBUG)) {
|
|
struct dentry *dbgfs_dir __maybe_unused;
|
|
struct dentry *dbgfs_stats __maybe_unused;
|
|
|
|
/* Ignore error of debugfs */
|
|
dbgfs_dir = debugfs_create_dir("sl3516", NULL);
|
|
dbgfs_stats = debugfs_create_file("stats", 0444,
|
|
dbgfs_dir, ce,
|
|
&sl3516_ce_debugfs_fops);
|
|
#ifdef CONFIG_CRYPTO_DEV_SL3516_DEBUG
|
|
ce->dbgfs_dir = dbgfs_dir;
|
|
ce->dbgfs_stats = dbgfs_stats;
|
|
#endif
|
|
}
|
|
|
|
return 0;
|
|
error_pmuse:
|
|
sl3516_ce_rng_unregister(ce);
|
|
error_rng:
|
|
sl3516_ce_unregister_algs(ce);
|
|
error_alg:
|
|
crypto_engine_exit(ce->engine);
|
|
error_engine:
|
|
sl3516_ce_pm_exit(ce);
|
|
error_pm:
|
|
sl3516_ce_free_descs(ce);
|
|
return err;
|
|
}
|
|
|
|
static int sl3516_ce_remove(struct platform_device *pdev)
|
|
{
|
|
struct sl3516_ce_dev *ce = platform_get_drvdata(pdev);
|
|
|
|
sl3516_ce_rng_unregister(ce);
|
|
sl3516_ce_unregister_algs(ce);
|
|
crypto_engine_exit(ce->engine);
|
|
sl3516_ce_pm_exit(ce);
|
|
sl3516_ce_free_descs(ce);
|
|
|
|
#ifdef CONFIG_CRYPTO_DEV_SL3516_DEBUG
|
|
debugfs_remove_recursive(ce->dbgfs_dir);
|
|
#endif
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct of_device_id sl3516_ce_crypto_of_match_table[] = {
|
|
{ .compatible = "cortina,sl3516-crypto"},
|
|
{}
|
|
};
|
|
MODULE_DEVICE_TABLE(of, sl3516_ce_crypto_of_match_table);
|
|
|
|
static struct platform_driver sl3516_ce_driver = {
|
|
.probe = sl3516_ce_probe,
|
|
.remove = sl3516_ce_remove,
|
|
.driver = {
|
|
.name = "sl3516-crypto",
|
|
.pm = &sl3516_ce_pm_ops,
|
|
.of_match_table = sl3516_ce_crypto_of_match_table,
|
|
},
|
|
};
|
|
|
|
module_platform_driver(sl3516_ce_driver);
|
|
|
|
MODULE_DESCRIPTION("SL3516 cryptographic offloader");
|
|
MODULE_LICENSE("GPL");
|
|
MODULE_AUTHOR("Corentin Labbe <clabbe@baylibre.com>");
|