| // 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"); |