| // 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), |
| }; |