229 lines
5.4 KiB
C
229 lines
5.4 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
/* Marvell CN10K RVU Hardware Random Number Generator.
|
|
*
|
|
* Copyright (C) 2021 Marvell.
|
|
*
|
|
*/
|
|
|
|
#include <linux/hw_random.h>
|
|
#include <linux/io.h>
|
|
#include <linux/module.h>
|
|
#include <linux/pci.h>
|
|
#include <linux/pci_ids.h>
|
|
#include <linux/delay.h>
|
|
|
|
#include <linux/arm-smccc.h>
|
|
|
|
/* CSRs */
|
|
#define RNM_CTL_STATUS 0x000
|
|
#define RNM_ENTROPY_STATUS 0x008
|
|
#define RNM_CONST 0x030
|
|
#define RNM_EBG_ENT 0x048
|
|
#define RNM_PF_EBG_HEALTH 0x050
|
|
#define RNM_PF_RANDOM 0x400
|
|
#define RNM_TRNG_RESULT 0x408
|
|
|
|
/* Extended TRNG Read and Status Registers */
|
|
#define RNM_PF_TRNG_DAT 0x1000
|
|
#define RNM_PF_TRNG_RES 0x1008
|
|
|
|
struct cn10k_rng {
|
|
void __iomem *reg_base;
|
|
struct hwrng ops;
|
|
struct pci_dev *pdev;
|
|
/* Octeon CN10K-A A0/A1, CNF10K-A A0/A1 and CNF10K-B A0/B0
|
|
* does not support extended TRNG registers
|
|
*/
|
|
bool extended_trng_regs;
|
|
};
|
|
|
|
#define PLAT_OCTEONTX_RESET_RNG_EBG_HEALTH_STATE 0xc2000b0f
|
|
|
|
#define PCI_SUBSYS_DEVID_CN10K_A_RNG 0xB900
|
|
#define PCI_SUBSYS_DEVID_CNF10K_A_RNG 0xBA00
|
|
#define PCI_SUBSYS_DEVID_CNF10K_B_RNG 0xBC00
|
|
|
|
static bool cn10k_is_extended_trng_regs_supported(struct pci_dev *pdev)
|
|
{
|
|
/* CN10K-A A0/A1 */
|
|
if ((pdev->subsystem_device == PCI_SUBSYS_DEVID_CN10K_A_RNG) &&
|
|
(!pdev->revision || (pdev->revision & 0xff) == 0x50 ||
|
|
(pdev->revision & 0xff) == 0x51))
|
|
return false;
|
|
|
|
/* CNF10K-A A0 */
|
|
if ((pdev->subsystem_device == PCI_SUBSYS_DEVID_CNF10K_A_RNG) &&
|
|
(!pdev->revision || (pdev->revision & 0xff) == 0x60 ||
|
|
(pdev->revision & 0xff) == 0x61))
|
|
return false;
|
|
|
|
/* CNF10K-B A0/B0 */
|
|
if ((pdev->subsystem_device == PCI_SUBSYS_DEVID_CNF10K_B_RNG) &&
|
|
(!pdev->revision || (pdev->revision & 0xff) == 0x70 ||
|
|
(pdev->revision & 0xff) == 0x74))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
static unsigned long reset_rng_health_state(struct cn10k_rng *rng)
|
|
{
|
|
struct arm_smccc_res res;
|
|
|
|
/* Send SMC service call to reset EBG health state */
|
|
arm_smccc_smc(PLAT_OCTEONTX_RESET_RNG_EBG_HEALTH_STATE, 0, 0, 0, 0, 0, 0, 0, &res);
|
|
return res.a0;
|
|
}
|
|
|
|
static int check_rng_health(struct cn10k_rng *rng)
|
|
{
|
|
u64 status;
|
|
unsigned long err;
|
|
|
|
/* Skip checking health */
|
|
if (!rng->reg_base)
|
|
return -ENODEV;
|
|
|
|
status = readq(rng->reg_base + RNM_PF_EBG_HEALTH);
|
|
if (status & BIT_ULL(20)) {
|
|
err = reset_rng_health_state(rng);
|
|
if (err) {
|
|
dev_err(&rng->pdev->dev, "HWRNG: Health test failed (status=%llx)\n",
|
|
status);
|
|
dev_err(&rng->pdev->dev, "HWRNG: error during reset (error=%lx)\n",
|
|
err);
|
|
return -EIO;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Returns true when valid data available otherwise return false */
|
|
static bool cn10k_read_trng(struct cn10k_rng *rng, u64 *value)
|
|
{
|
|
u16 retry_count = 0;
|
|
u64 upper, lower;
|
|
u64 status;
|
|
|
|
if (rng->extended_trng_regs) {
|
|
do {
|
|
*value = readq(rng->reg_base + RNM_PF_TRNG_DAT);
|
|
if (*value)
|
|
return true;
|
|
status = readq(rng->reg_base + RNM_PF_TRNG_RES);
|
|
if (!status && (retry_count++ > 0x1000))
|
|
return false;
|
|
} while (!status);
|
|
}
|
|
|
|
*value = readq(rng->reg_base + RNM_PF_RANDOM);
|
|
|
|
/* HW can run out of entropy if large amount random data is read in
|
|
* quick succession. Zeros may not be real random data from HW.
|
|
*/
|
|
if (!*value) {
|
|
upper = readq(rng->reg_base + RNM_PF_RANDOM);
|
|
lower = readq(rng->reg_base + RNM_PF_RANDOM);
|
|
while (!(upper & 0x00000000FFFFFFFFULL))
|
|
upper = readq(rng->reg_base + RNM_PF_RANDOM);
|
|
while (!(lower & 0xFFFFFFFF00000000ULL))
|
|
lower = readq(rng->reg_base + RNM_PF_RANDOM);
|
|
|
|
*value = (upper & 0xFFFFFFFF00000000) | (lower & 0xFFFFFFFF);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static int cn10k_rng_read(struct hwrng *hwrng, void *data,
|
|
size_t max, bool wait)
|
|
{
|
|
struct cn10k_rng *rng = (struct cn10k_rng *)hwrng->priv;
|
|
unsigned int size;
|
|
u8 *pos = data;
|
|
int err = 0;
|
|
u64 value;
|
|
|
|
err = check_rng_health(rng);
|
|
if (err)
|
|
return err;
|
|
|
|
size = max;
|
|
|
|
while (size >= 8) {
|
|
if (!cn10k_read_trng(rng, &value))
|
|
goto out;
|
|
|
|
*((u64 *)pos) = value;
|
|
size -= 8;
|
|
pos += 8;
|
|
}
|
|
|
|
if (size > 0) {
|
|
if (!cn10k_read_trng(rng, &value))
|
|
goto out;
|
|
|
|
while (size > 0) {
|
|
*pos = (u8)value;
|
|
value >>= 8;
|
|
size--;
|
|
pos++;
|
|
}
|
|
}
|
|
|
|
out:
|
|
return max - size;
|
|
}
|
|
|
|
static int cn10k_rng_probe(struct pci_dev *pdev, const struct pci_device_id *id)
|
|
{
|
|
struct cn10k_rng *rng;
|
|
int err;
|
|
|
|
rng = devm_kzalloc(&pdev->dev, sizeof(*rng), GFP_KERNEL);
|
|
if (!rng)
|
|
return -ENOMEM;
|
|
|
|
rng->pdev = pdev;
|
|
pci_set_drvdata(pdev, rng);
|
|
|
|
rng->reg_base = pcim_iomap(pdev, 0, 0);
|
|
if (!rng->reg_base)
|
|
return dev_err_probe(&pdev->dev, -ENOMEM, "Error while mapping CSRs, exiting\n");
|
|
|
|
rng->ops.name = devm_kasprintf(&pdev->dev, GFP_KERNEL,
|
|
"cn10k-rng-%s", dev_name(&pdev->dev));
|
|
if (!rng->ops.name)
|
|
return -ENOMEM;
|
|
|
|
rng->ops.read = cn10k_rng_read;
|
|
rng->ops.priv = (unsigned long)rng;
|
|
|
|
rng->extended_trng_regs = cn10k_is_extended_trng_regs_supported(pdev);
|
|
|
|
reset_rng_health_state(rng);
|
|
|
|
err = devm_hwrng_register(&pdev->dev, &rng->ops);
|
|
if (err)
|
|
return dev_err_probe(&pdev->dev, err, "Could not register hwrng device.\n");
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct pci_device_id cn10k_rng_id_table[] = {
|
|
{ PCI_DEVICE(PCI_VENDOR_ID_CAVIUM, 0xA098) }, /* RNG PF */
|
|
{0,},
|
|
};
|
|
|
|
MODULE_DEVICE_TABLE(pci, cn10k_rng_id_table);
|
|
|
|
static struct pci_driver cn10k_rng_driver = {
|
|
.name = "cn10k_rng",
|
|
.id_table = cn10k_rng_id_table,
|
|
.probe = cn10k_rng_probe,
|
|
};
|
|
|
|
module_pci_driver(cn10k_rng_driver);
|
|
MODULE_AUTHOR("Sunil Goutham <sgoutham@marvell.com>");
|
|
MODULE_DESCRIPTION("Marvell CN10K HW RNG Driver");
|
|
MODULE_LICENSE("GPL v2");
|