imx6: ocotp: Add On-Chip OTP registers write support
FUSEs (OTP registers) can be written via /dev/imx-ocotp character device. For example, writing MAC 12:34:56:78:9A:BC can be performed as > mw -l -d /dev/imx-ocotp 0x8c 0x00001234 > mw -l -d /dev/imx-ocotp 0x88 0x56789ABC and reading as > md -l -s /dev/imx-ocotp 0x88+8 00000088: 56789ABC 00001234 , where 0x88 (0x22*4) and 0x8C (0x23*4) are offsets of MAC OTP registers. Notice: FUSEs are PROM, so "0" (unprogrammed) bits can be replaced with "1" (but not vice versa) only once. Also, for MAC there are convinient parameters: > ocotp0.permanent_write_enable=1 > ocotp0.mac_addr=12:34:56:78:9A:BC imx_ocotp 21bc000.ocotp: reloading shadow registers... imx_ocotp 21bc000.ocotp: reloading shadow registers... > echo $ocotp0.mac_addr 12:34:56:78:9A:BC Signed-off-by: Uladzimir Bely <u.bely@sam-solutions.net> Signed-off-by: Sascha Hauer <s.hauer@pengutronix.de>
This commit is contained in:
parent
e0afbe3f84
commit
825fca396f
|
@ -630,6 +630,16 @@ config IMX_OCOTP
|
|||
only supported functionality is reading the MAC address and assigning
|
||||
it to an ethernet device.
|
||||
|
||||
config IMX_OCOTP_WRITE
|
||||
bool
|
||||
prompt "Enable write support of i.MX6 CPUs OTP fuses"
|
||||
depends on IMX_OCOTP
|
||||
help
|
||||
This adds write support to IMX6 On-Chip OTP registers.
|
||||
Example of set MAC to 12:34:56:78:9A:BC (2 words with offset 0x22 * 4):
|
||||
mw -l -d /dev/imx-ocotp 0x8C 0x00001234
|
||||
mw -l -d /dev/imx-ocotp 0x88 0x56789ABC
|
||||
|
||||
endmenu
|
||||
|
||||
endif
|
||||
|
|
|
@ -433,6 +433,7 @@ static int imx6_ccm_probe(struct device_d *dev)
|
|||
clkdev_add_physbase(clks[ecspi_root], MX6_ECSPI5_BASE_ADDR, NULL);
|
||||
clkdev_add_physbase(clks[ipg_per], MX6_GPT_BASE_ADDR, NULL);
|
||||
clkdev_add_physbase(clks[ipg], MX6_ENET_BASE_ADDR, NULL);
|
||||
clkdev_add_physbase(clks[ipg], MX6_OCOTP_BASE_ADDR, NULL);
|
||||
clkdev_add_physbase(clks[usdhc1_podf], MX6_USDHC1_BASE_ADDR, NULL);
|
||||
clkdev_add_physbase(clks[usdhc2_podf], MX6_USDHC2_BASE_ADDR, NULL);
|
||||
clkdev_add_physbase(clks[usdhc3_podf], MX6_USDHC3_BASE_ADDR, NULL);
|
||||
|
|
|
@ -25,6 +25,8 @@
|
|||
#include <net.h>
|
||||
#include <io.h>
|
||||
#include <of.h>
|
||||
#include <clock.h>
|
||||
#include <linux/clk.h>
|
||||
|
||||
/*
|
||||
* a single MAC address reference has the form
|
||||
|
@ -32,6 +34,292 @@
|
|||
*/
|
||||
#define MAC_ADDRESS_PROPLEN (2 * sizeof(__be32))
|
||||
|
||||
/* OCOTP Registers offsets */
|
||||
#define OCOTP_CTRL_SET 0x04
|
||||
#define OCOTP_CTRL_CLR 0x08
|
||||
#define OCOTP_TIMING 0x10
|
||||
#define OCOTP_DATA 0x20
|
||||
#define OCOTP_READ_CTRL 0x30
|
||||
#define OCOTP_READ_FUSE_DATA 0x40
|
||||
|
||||
/* OCOTP Registers bits and masks */
|
||||
#define OCOTP_CTRL_WR_UNLOCK 16
|
||||
#define OCOTP_CTRL_WR_UNLOCK_KEY 0x3E77
|
||||
#define OCOTP_CTRL_WR_UNLOCK_MASK 0xFFFF0000
|
||||
#define OCOTP_CTRL_ADDR 0
|
||||
#define OCOTP_CTRL_ADDR_MASK 0x0000007F
|
||||
#define OCOTP_CTRL_BUSY (1 << 8)
|
||||
#define OCOTP_CTRL_ERROR (1 << 9)
|
||||
#define OCOTP_CTRL_RELOAD_SHADOWS (1 << 10)
|
||||
|
||||
#define OCOTP_TIMING_STROBE_READ 16
|
||||
#define OCOTP_TIMING_STROBE_READ_MASK 0x003F0000
|
||||
#define OCOTP_TIMING_RELAX 12
|
||||
#define OCOTP_TIMING_RELAX_MASK 0x0000F000
|
||||
#define OCOTP_TIMING_STROBE_PROG 0
|
||||
#define OCOTP_TIMING_STROBE_PROG_MASK 0x00000FFF
|
||||
|
||||
#define OCOTP_READ_CTRL_READ_FUSE 0x00000001
|
||||
|
||||
#define BF(value, field) (((value) << field) & field##_MASK)
|
||||
|
||||
/* Other definitions */
|
||||
#define FUSE_REGS_COUNT (16 * 8)
|
||||
#define IMX6_OTP_DATA_ERROR_VAL 0xBADABADA
|
||||
#define DEF_RELAX 20
|
||||
#define MAC_OFFSET (0x22 * 4)
|
||||
#define MAC_BYTES 8
|
||||
|
||||
struct ocotp_priv {
|
||||
struct cdev cdev;
|
||||
void __iomem *base;
|
||||
struct clk *clk;
|
||||
struct device_d dev;
|
||||
int permanent_write_enable;
|
||||
int sense_enable;
|
||||
char ethaddr[6];
|
||||
};
|
||||
|
||||
static int imx6_ocotp_set_timing(struct ocotp_priv *priv)
|
||||
{
|
||||
u32 clk_rate;
|
||||
u32 relax, strobe_read, strobe_prog;
|
||||
u32 timing;
|
||||
|
||||
clk_rate = clk_get_rate(priv->clk);
|
||||
|
||||
relax = clk_rate / (1000000000 / DEF_RELAX) - 1;
|
||||
strobe_prog = clk_rate / (1000000000 / 10000) + 2 * (DEF_RELAX + 1) - 1;
|
||||
strobe_read = clk_rate / (1000000000 / 40) + 2 * (DEF_RELAX + 1) - 1;
|
||||
|
||||
timing = BF(relax, OCOTP_TIMING_RELAX);
|
||||
timing |= BF(strobe_read, OCOTP_TIMING_STROBE_READ);
|
||||
timing |= BF(strobe_prog, OCOTP_TIMING_STROBE_PROG);
|
||||
|
||||
writel(timing, priv->base + OCOTP_TIMING);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int imx6_ocotp_wait_busy(u32 flags, struct ocotp_priv *priv)
|
||||
{
|
||||
uint64_t start = get_time_ns();
|
||||
|
||||
while ((OCOTP_CTRL_BUSY | OCOTP_CTRL_ERROR | flags) &
|
||||
readl(priv->base)) {
|
||||
if (is_timeout(start, MSECOND)) {
|
||||
/* Clear ERROR bit */
|
||||
writel(OCOTP_CTRL_ERROR, priv->base + OCOTP_CTRL_CLR);
|
||||
return -ETIMEDOUT;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int imx6_ocotp_prepare(struct ocotp_priv *priv)
|
||||
{
|
||||
int ret;
|
||||
|
||||
ret = imx6_ocotp_set_timing(priv);
|
||||
if (ret)
|
||||
return ret;
|
||||
|
||||
ret = imx6_ocotp_wait_busy(0, priv);
|
||||
if (ret)
|
||||
return ret;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int fuse_read_addr(u32 addr, u32 *pdata, struct ocotp_priv *priv)
|
||||
{
|
||||
u32 ctrl_reg;
|
||||
int ret;
|
||||
|
||||
ctrl_reg = readl(priv->base);
|
||||
ctrl_reg &= ~OCOTP_CTRL_ADDR_MASK;
|
||||
ctrl_reg &= ~OCOTP_CTRL_WR_UNLOCK_MASK;
|
||||
ctrl_reg |= BF(addr, OCOTP_CTRL_ADDR);
|
||||
writel(ctrl_reg, priv->base);
|
||||
|
||||
writel(OCOTP_READ_CTRL_READ_FUSE, priv->base + OCOTP_READ_CTRL);
|
||||
ret = imx6_ocotp_wait_busy(0, priv);
|
||||
if (ret)
|
||||
return ret;
|
||||
|
||||
*pdata = readl(priv->base + OCOTP_READ_FUSE_DATA);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int imx6_ocotp_read_one_u32(u32 index, u32 *pdata, struct ocotp_priv *priv)
|
||||
{
|
||||
int ret;
|
||||
|
||||
ret = imx6_ocotp_prepare(priv);
|
||||
if (ret) {
|
||||
dev_err(priv->cdev.dev, "failed to prepare read fuse 0x%08x\n",
|
||||
index);
|
||||
return ret;
|
||||
}
|
||||
|
||||
ret = fuse_read_addr(index, pdata, priv);
|
||||
if (ret) {
|
||||
dev_err(priv->cdev.dev, "failed to read fuse 0x%08x\n", index);
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (readl(priv->base) & OCOTP_CTRL_ERROR) {
|
||||
dev_err(priv->cdev.dev, "bad read status at fuse 0x%08x\n", index);
|
||||
return -EFAULT;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static ssize_t imx6_ocotp_cdev_read(struct cdev *cdev, void *buf,
|
||||
size_t count, loff_t offset, unsigned long flags)
|
||||
{
|
||||
u32 index;
|
||||
ssize_t read_count = 0;
|
||||
int ret, i;
|
||||
struct ocotp_priv *priv = container_of(cdev, struct ocotp_priv, cdev);
|
||||
|
||||
index = offset >> 2;
|
||||
count >>= 2;
|
||||
|
||||
if (count > (FUSE_REGS_COUNT - index))
|
||||
count = FUSE_REGS_COUNT - index - 1;
|
||||
|
||||
for (i = index; i < (index + count); i++) {
|
||||
if (priv->sense_enable) {
|
||||
ret = imx6_ocotp_read_one_u32(i, buf, priv);
|
||||
if (ret)
|
||||
return ret;
|
||||
} else {
|
||||
*(u32 *)buf = readl(priv->base + 0x400 + i * 0x10);
|
||||
}
|
||||
|
||||
buf += 4;
|
||||
read_count++;
|
||||
}
|
||||
|
||||
return read_count << 2;
|
||||
}
|
||||
|
||||
static int fuse_blow_addr(u32 addr, u32 value, struct ocotp_priv *priv)
|
||||
{
|
||||
u32 ctrl_reg;
|
||||
int ret;
|
||||
|
||||
/* Control register */
|
||||
ctrl_reg = readl(priv->base);
|
||||
ctrl_reg &= ~OCOTP_CTRL_ADDR_MASK;
|
||||
ctrl_reg |= BF(addr, OCOTP_CTRL_ADDR);
|
||||
ctrl_reg |= BF(OCOTP_CTRL_WR_UNLOCK_KEY, OCOTP_CTRL_WR_UNLOCK);
|
||||
writel(ctrl_reg, priv->base);
|
||||
|
||||
writel(value, priv->base + OCOTP_DATA);
|
||||
ret = imx6_ocotp_wait_busy(0, priv);
|
||||
if (ret)
|
||||
return ret;
|
||||
|
||||
/* Write postamble */
|
||||
udelay(2000);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int imx6_ocotp_reload_shadow(struct ocotp_priv *priv)
|
||||
{
|
||||
dev_info(priv->cdev.dev, "reloading shadow registers...\n");
|
||||
writel(OCOTP_CTRL_RELOAD_SHADOWS, priv->base + OCOTP_CTRL_SET);
|
||||
udelay(1);
|
||||
|
||||
return imx6_ocotp_wait_busy(OCOTP_CTRL_RELOAD_SHADOWS, priv);
|
||||
}
|
||||
|
||||
int imx6_ocotp_blow_one_u32(u32 index, u32 data, u32 *pfused_value,
|
||||
struct ocotp_priv *priv)
|
||||
{
|
||||
int ret;
|
||||
|
||||
ret = imx6_ocotp_prepare(priv);
|
||||
if (ret) {
|
||||
dev_err(priv->cdev.dev, "prepare to write failed\n");
|
||||
return ret;
|
||||
}
|
||||
|
||||
ret = fuse_blow_addr(index, data, priv);
|
||||
if (ret) {
|
||||
dev_err(priv->cdev.dev, "fuse blow failed\n");
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (readl(priv->base) & OCOTP_CTRL_ERROR) {
|
||||
dev_err(priv->cdev.dev, "bad write status\n");
|
||||
return -EFAULT;
|
||||
}
|
||||
|
||||
ret = imx6_ocotp_read_one_u32(index, pfused_value, priv);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static ssize_t imx6_ocotp_cdev_write(struct cdev *cdev, const void *buf,
|
||||
size_t count, loff_t offset, unsigned long flags)
|
||||
{
|
||||
struct ocotp_priv *priv = cdev->priv;
|
||||
int index, i;
|
||||
ssize_t write_count = 0;
|
||||
const u32 *data;
|
||||
u32 pfuse;
|
||||
int ret;
|
||||
|
||||
/* We could do better, but currently this is what's implemented */
|
||||
if (offset & 0x3 || count & 0x3) {
|
||||
dev_err(cdev->dev, "only u32 aligned writes allowed\n");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
index = offset >> 2;
|
||||
count >>= 2;
|
||||
|
||||
if (count > (FUSE_REGS_COUNT - index))
|
||||
count = FUSE_REGS_COUNT - index - 1;
|
||||
|
||||
data = buf;
|
||||
|
||||
for (i = index; i < (index + count); i++) {
|
||||
if (priv->permanent_write_enable) {
|
||||
ret = imx6_ocotp_blow_one_u32(i, *data,
|
||||
&pfuse, priv);
|
||||
if (ret < 0) {
|
||||
goto out;
|
||||
}
|
||||
} else {
|
||||
writel(*data, priv->base + 0x400 + i * 0x10);
|
||||
}
|
||||
|
||||
data++;
|
||||
write_count++;
|
||||
}
|
||||
|
||||
ret = 0;
|
||||
|
||||
out:
|
||||
if (priv->permanent_write_enable)
|
||||
imx6_ocotp_reload_shadow(priv);
|
||||
|
||||
return ret < 0 ? ret : (write_count << 2);
|
||||
}
|
||||
|
||||
static struct file_operations imx6_ocotp_ops = {
|
||||
.read = imx6_ocotp_cdev_read,
|
||||
.write = imx6_ocotp_cdev_write,
|
||||
.lseek = dev_lseek_default,
|
||||
};
|
||||
|
||||
static void imx_ocotp_init_dt(struct device_d *dev, void __iomem *base)
|
||||
{
|
||||
char mac[6];
|
||||
|
@ -70,9 +358,43 @@ static void imx_ocotp_init_dt(struct device_d *dev, void __iomem *base)
|
|||
}
|
||||
}
|
||||
|
||||
static int imx_ocotp_get_mac(struct param_d *param, void *priv)
|
||||
{
|
||||
struct ocotp_priv *ocotp_priv = priv;
|
||||
char buf[8];
|
||||
int i;
|
||||
|
||||
imx6_ocotp_cdev_read(&ocotp_priv->cdev, buf, MAC_BYTES, MAC_OFFSET, 0);
|
||||
|
||||
for (i = 0; i < 6; i++)
|
||||
ocotp_priv->ethaddr[i] = buf[5 - i];
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int imx_ocotp_set_mac(struct param_d *param, void *priv)
|
||||
{
|
||||
struct ocotp_priv *ocotp_priv = priv;
|
||||
char buf[8];
|
||||
int i, ret;
|
||||
|
||||
for (i = 0; i < 6; i++)
|
||||
buf[5 - i] = ocotp_priv->ethaddr[i];
|
||||
buf[6] = 0; buf[7] = 0;
|
||||
|
||||
ret = imx6_ocotp_cdev_write(&ocotp_priv->cdev, buf, MAC_BYTES, MAC_OFFSET, 0);
|
||||
if (ret < 0)
|
||||
return ret;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int imx_ocotp_probe(struct device_d *dev)
|
||||
{
|
||||
void __iomem *base;
|
||||
struct ocotp_priv *priv;
|
||||
struct cdev *cdev;
|
||||
int ret = 0;
|
||||
|
||||
base = dev_request_mem_region(dev, 0);
|
||||
if (!base)
|
||||
|
@ -80,6 +402,40 @@ static int imx_ocotp_probe(struct device_d *dev)
|
|||
|
||||
imx_ocotp_init_dt(dev, base);
|
||||
|
||||
priv = xzalloc(sizeof(*priv));
|
||||
|
||||
priv->base = base;
|
||||
priv->clk = clk_get(dev, NULL);
|
||||
if (IS_ERR(priv->clk))
|
||||
return PTR_ERR(priv->clk);
|
||||
|
||||
cdev = &priv->cdev;
|
||||
cdev->dev = dev;
|
||||
cdev->ops = &imx6_ocotp_ops;
|
||||
cdev->priv = priv;
|
||||
cdev->size = 192;
|
||||
cdev->name = "imx-ocotp";
|
||||
if (cdev->name == NULL)
|
||||
return -ENOMEM;
|
||||
|
||||
ret = devfs_create(cdev);
|
||||
|
||||
if (ret < 0)
|
||||
return ret;
|
||||
|
||||
strcpy(priv->dev.name, "ocotp");
|
||||
priv->dev.parent = dev;
|
||||
register_device(&priv->dev);
|
||||
|
||||
if (IS_ENABLED(CONFIG_IMX_OCOTP_WRITE)) {
|
||||
dev_add_param_bool(&(priv->dev), "permanent_write_enable",
|
||||
NULL, NULL, &priv->permanent_write_enable, NULL);
|
||||
}
|
||||
|
||||
dev_add_param_mac(&(priv->dev), "mac_addr", imx_ocotp_set_mac,
|
||||
imx_ocotp_get_mac, priv->ethaddr, priv);
|
||||
dev_add_param_bool(&(priv->dev), "sense_enable", NULL, NULL, &priv->sense_enable, priv);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue