blob: b46e2dfff88a81c756bde596407781bdb6b6cddf [file] [log] [blame]
// SPDX-License-Identifier: GPL-2.0
/*
* Driver for NXP A71CH
* Protocol documentation from https://www.nxp.com/docs/en/supporting-information/AN12207.pdf
* Copyright (C) 2020 Google LLC
*/
#include <common.h>
#include <dm.h>
#include <i2c.h>
#include <asm/byteorder.h>
#include "a71ch.h"
// #define LOG_I2C 1
#define PCB_STATUS (0x7)
#define PCB_SOFT_RESET (0x1F)
#define PCB_READ_ANSWER_TO_RESET (0x2F)
#define PCB_PARAMETER_EXCHANGE (0x3F)
#define PCB_DATA_M2S (0x0)
#define PCB_DATA_S2M (0x2)
#define S2M_SHIFT (6)
#define M2S_SHIFT (2)
#define M2S_NEG_SHIFT (4)
#define SEQ_CTR_SHIFT (4)
#define M2S_MORE (0x80)
#define APDU_SUCCESS (0x9000)
#define NRETRY_SRST (3)
#define NRETRY_RATR (3)
#define NRETRY_PE (3)
#define NRETRY_STATUS (3)
#define NRETRY_M2S (3)
#define DELAY_RESET_MS (10)
#define DELAY_READ_BLOCK_MS (1)
#define BLOCK_SIZE (32)
#define STATUS_MASK (0xF)
#define M2S_MASK (0x3)
#define S2M_MASK (0x3)
#define SEQ_CTR_MAX (7)
struct a71ch {
int s2m;
int m2s;
u8 seq_ctr;
u8 uid[A71CH_UID_LEN];
};
enum {
s2m_31Bytes = 0,
s2m_63Bytes,
s2m_127Bytes,
s2m_254Bytes
};
enum {
m2s_32Bytes = 0,
m2s_64Bytes,
m2s_128Bytes,
m2s_255Bytes
};
enum {
STATUS_OKAY = 0x0,
STATUS_BUSY = 0x1
};
static int a71ch_m2s_to_bytes(int m2s) {
switch (m2s) {
case m2s_32Bytes:
return 32;
case m2s_64Bytes:
return 64;
case m2s_128Bytes:
return 128;
case m2s_255Bytes:
return 255;
default:
return 32;
}
}
static int a71ch_read_block(struct udevice *dev, u8* wr_buf, int wr_len, u8* rd_buf, int tries) {
int ret = -1;
struct dm_i2c_chip *chip = dev_get_parent_platdata(dev);
mdelay(DELAY_READ_BLOCK_MS);
#if defined(LOG_I2C)
printf("Tx(%d): ", wr_len);
for (int i = 0; i < wr_len; i++) {
printf("0x%x ", wr_buf[i]);
}
printf("\n");
#endif
do {
struct i2c_msg msgs[2];
msgs[0].addr = chip->chip_addr;
msgs[0].flags = 0;
msgs[0].len = wr_len;
msgs[0].buf = wr_buf;
msgs[1].addr = chip->chip_addr;
msgs[1].flags = I2C_M_RD | I2C_M_RECV_LEN;
msgs[1].buf = rd_buf;
msgs[1].len = BLOCK_SIZE;
ret = dm_i2c_xfer(dev, msgs, 2);
tries--;
} while (tries && ret);
#if defined(LOG_I2C)
printf("Rx(%d): ", rd_buf[0]);
for (int i = 1; i <= rd_buf[0]; i++) {
printf("0x%x ", rd_buf[i]);
}
printf("\n");
#endif
return ret;
}
static int a71ch_write(struct udevice *dev, u8 *wr_buf, int wr_len, int tries) {
int ret = -1;
struct dm_i2c_chip *chip = dev_get_parent_platdata(dev);
#if defined(LOG_I2C)
printf("Tx(%d): ", wr_len);
for (int i = 0; i < wr_len; i++) {
printf("0x%x ", wr_buf[i]);
}
printf("\n");
#endif
do {
struct i2c_msg msgs[1];
msgs[0].addr = chip->chip_addr;
msgs[0].flags = 0;
msgs[0].len = wr_len;
msgs[0].buf = wr_buf;
ret = dm_i2c_xfer(dev, msgs, 1);
tries--;
} while (tries && ret);
return ret;
}
static int a71ch_get_status(struct udevice *dev, u8 *status) {
int ret;
if (status == NULL) {
return -EINVAL;
}
u8 wr_buf[] = { PCB_STATUS };
u8 rd_buf[BLOCK_SIZE] = {};
ret = a71ch_read_block(dev, wr_buf, sizeof(wr_buf), rd_buf, NRETRY_STATUS);
if (ret) {
return ret;
}
*status = ((rd_buf[1] >> 4) & STATUS_MASK);
return 0;
}
static int a71ch_soft_reset(struct udevice *dev) {
int ret;
u8 wr_buf[] = { PCB_SOFT_RESET };
u8 rd_buf[BLOCK_SIZE] = {};
ret = a71ch_read_block(dev, wr_buf, sizeof(wr_buf), rd_buf, NRETRY_SRST);
if (ret)
return ret;
mdelay(DELAY_RESET_MS);
if (rd_buf[1]) {
return -EIO;
}
return 0;
}
static int a71ch_read_answer_to_reset(struct udevice *dev) {
int ret;
u8 wr_buf[] = { PCB_READ_ANSWER_TO_RESET };
u8 rd_buf[BLOCK_SIZE] = {};
ret = a71ch_read_block(dev, wr_buf, sizeof(wr_buf), rd_buf, NRETRY_RATR);
if (ret)
return ret;
return 0;
}
static int a71ch_parameter_exchange(struct udevice *dev) {
struct a71ch *priv = dev_get_priv(dev);
int ret;
u8 wr_buf[] = { PCB_PARAMETER_EXCHANGE | (s2m_31Bytes << S2M_SHIFT) };
u8 rd_buf[BLOCK_SIZE];
int retries = NRETRY_PE;
do {
ret = a71ch_read_block(dev, wr_buf, sizeof(wr_buf), rd_buf, NRETRY_PE);
if (ret)
return ret;
u8 px_val = rd_buf[1];
u8 s2m = (px_val >> S2M_SHIFT) & S2M_MASK;
u8 m2s = (px_val >> M2S_SHIFT) & M2S_MASK;
u8 m2s_neg = (~px_val >> M2S_NEG_SHIFT) & M2S_MASK;
if ((m2s == m2s_neg) && (s2m == s2m_31Bytes)) {
ret = 0;
priv->m2s = m2s;
priv->s2m = s2m;
} else {
ret = -EINVAL;
}
} while (retries && ret);
return ret;
}
static int a71ch_data_exchange(struct udevice *dev, u8 *data, int data_len, u8 *out_buf, int out_len) {
struct a71ch *priv = dev_get_priv(dev);
int data_remaining = data_len;
int ret;
// Leave space for the control byte and length byte.
int max_packet_len = a71ch_m2s_to_bytes(priv->m2s) - 2;
u8 wr_buf[BLOCK_SIZE];
u8 rd_buf[BLOCK_SIZE];
while (data_remaining) {
wr_buf[0] = PCB_DATA_M2S | (priv->seq_ctr++ << SEQ_CTR_SHIFT);
if (priv->seq_ctr > SEQ_CTR_MAX) {
priv->seq_ctr = 0;
}
if (data_remaining > max_packet_len) {
wr_buf[0] |= M2S_MORE;
}
u8 packet_len = (data_remaining > max_packet_len) ? max_packet_len : data_remaining;
wr_buf[1] = packet_len;
memcpy(&wr_buf[2], data + (data_len - data_remaining), packet_len);
// +2 is for control byte, packet len
ret = a71ch_write(dev, wr_buf, packet_len + 2, NRETRY_M2S);
u8 status;
do {
ret = a71ch_get_status(dev, &status);
if (ret == -EREMOTEIO) {
udelay(1000);
continue;
}
if (status == STATUS_BUSY) {
udelay(1000);
continue;
} else if (status == STATUS_OKAY) {
ret = 0;
} else {
ret = -1;
return ret;
}
} while (status != STATUS_OKAY);
data_remaining -= packet_len;
}
// Sending done, read back.
// NOTE: We don't currently support responses longer than one packet,
// as our use case doesn't need them.
wr_buf[0] = PCB_DATA_S2M;
ret = a71ch_read_block(dev, wr_buf, 1, rd_buf, 1);
// recv_len is one less than the actual size received, for the returned control byte.
int recv_len = rd_buf[0] - 1;
if (out_len != recv_len) {
printf("out_len and recv_len don't match.");
return -EINVAL;
}
memcpy(out_buf, &rd_buf[2], out_len);
u16 apdu_ret = ntohs((u16)rd_buf[recv_len]);
ret = (apdu_ret != APDU_SUCCESS);
return ret;
}
static int a71ch_gp_select(struct udevice *dev) {
int ret;
// Breakdown of APDU:
// [0] - CLA_ISO7816
// [1] - INS_SELECT
// [2] - P1 Offset
// [3] - P2 Offset
// [4] - Length
// [5:9] - "A71CH"
// [10] - Expected return length
u8 gp_select_data[] = { 0x00, 0xA4, 0x04, 0x00, 0x05, 0x61, 0x37, 0x31, 0x63, 0x68, 0x00 };
int gp_select_len = sizeof(gp_select_data);
u8 gp_select_out[4];
ret = a71ch_data_exchange(dev, gp_select_data, gp_select_len, gp_select_out, sizeof(gp_select_out));
return ret;
}
static int a71ch_get_uid(struct udevice *dev) {
int ret;
struct a71ch *priv = dev_get_priv(dev);
// Breakdown of APDU:
// [0] - CLA_A71CH
// [1] - A71CH_INS_GET_MODULE
// [2] - P1
// [3] - P2 (Unique ID)
u8 get_uid_data[] = { 0x80, 0x91, 0x0, 0x1 };
int get_uid_data_len = sizeof(get_uid_data);
u8 get_uid_out[20];
ret = a71ch_data_exchange(dev, get_uid_data, get_uid_data_len, get_uid_out, sizeof(get_uid_out));
if (!ret) {
memcpy(priv->uid, get_uid_out, 18);
}
return ret;
}
static int a71ch_probe(struct udevice *dev) {
int ret;
struct a71ch *priv = dev_get_priv(dev);
memset(priv, 0, sizeof(*priv));
ret = a71ch_soft_reset(dev);
if (ret) {
printf("A71CH: Soft reset failed.\n");
return -ENOENT;
}
ret = a71ch_read_answer_to_reset(dev);
if (ret) {
printf("A71CH: Read answer to reset failed.\n");
return -ENOENT;
}
u8 status;
ret = a71ch_get_status(dev, &status);
if (ret) {
printf("A71CH: Get status failed.\n");
return -ENOENT;
}
ret = a71ch_parameter_exchange(dev);
if (ret) {
printf("A71CH: Parameter exchange failed.\n");
return -ENOENT;
}
ret = a71ch_gp_select(dev);
if (ret) {
printf("A71CH: GP Select failed.\n");
return -ENOENT;
}
ret = a71ch_get_uid(dev);
if (ret) {
printf("A71CH: Get UID failed.\n");
return -ENOENT;
}
return 0;
}
u8* a71ch_uid(struct udevice *dev) {
struct a71ch *priv = dev_get_priv(dev);
return priv->uid;
}
static const struct udevice_id a71ch_ids[] = {
{ .compatible = "nxp,a71ch" },
{ }
};
U_BOOT_DRIVER(a71ch) = {
.name = "a71ch",
.id = UCLASS_I2C_GENERIC,
.of_match = a71ch_ids,
.probe = a71ch_probe,
.priv_auto_alloc_size = sizeof(struct a71ch),
};