linux/drivers/nfc/nfcmrvl/fw_dnld.c
Vincent Cuissard 3194c68701 NFC: nfcmrvl: add firmware download support
Implement firmware download protocol for Marvell NFC controllers.
This protocol is based on NCI frames that's why parts of its
implementation use some NCI generic functions.

Signed-off-by: Vincent Cuissard <cuissard@marvell.com>
Signed-off-by: Samuel Ortiz <sameo@linux.intel.com>
2015-10-27 04:18:44 +01:00

554 lines
14 KiB
C

/*
* Marvell NFC driver: Firmware downloader
*
* Copyright (C) 2015, Marvell International Ltd.
*
* This software file (the "File") is distributed by Marvell International
* Ltd. under the terms of the GNU General Public License Version 2, June 1991
* (the "License"). You may use, redistribute and/or modify this File in
* accordance with the terms and conditions of the License, a copy of which
* is available on the worldwide web at
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
*
* THE FILE IS DISTRIBUTED AS-IS, WITHOUT WARRANTY OF ANY KIND, AND THE
* IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE
* ARE EXPRESSLY DISCLAIMED. The License provides additional details about
* this warranty disclaimer.
*/
#include <linux/module.h>
#include <linux/unaligned/access_ok.h>
#include <linux/firmware.h>
#include <linux/nfc.h>
#include <net/nfc/nci.h>
#include <net/nfc/nci_core.h>
#include "nfcmrvl.h"
#define FW_DNLD_TIMEOUT 15000
#define NCI_OP_PROPRIETARY_BOOT_CMD nci_opcode_pack(NCI_GID_PROPRIETARY, \
NCI_OP_PROP_BOOT_CMD)
/* FW download states */
enum {
STATE_RESET = 0,
STATE_INIT,
STATE_SET_REF_CLOCK,
STATE_SET_HI_CONFIG,
STATE_OPEN_LC,
STATE_FW_DNLD,
STATE_CLOSE_LC,
STATE_BOOT
};
enum {
SUBSTATE_WAIT_COMMAND = 0,
SUBSTATE_WAIT_ACK_CREDIT,
SUBSTATE_WAIT_NACK_CREDIT,
SUBSTATE_WAIT_DATA_CREDIT,
};
/*
** Patterns for responses
*/
static const uint8_t nci_pattern_core_reset_ntf[] = {
0x60, 0x00, 0x02, 0xA0, 0x01
};
static const uint8_t nci_pattern_core_init_rsp[] = {
0x40, 0x01, 0x11
};
static const uint8_t nci_pattern_core_set_config_rsp[] = {
0x40, 0x02, 0x02, 0x00, 0x00
};
static const uint8_t nci_pattern_core_conn_create_rsp[] = {
0x40, 0x04, 0x04, 0x00
};
static const uint8_t nci_pattern_core_conn_close_rsp[] = {
0x40, 0x05, 0x01, 0x00
};
static const uint8_t nci_pattern_core_conn_credits_ntf[] = {
0x60, 0x06, 0x03, 0x01, NCI_CORE_LC_CONNID_PROP_FW_DL, 0x01
};
static const uint8_t nci_pattern_proprietary_boot_rsp[] = {
0x4F, 0x3A, 0x01, 0x00
};
static struct sk_buff *alloc_lc_skb(struct nfcmrvl_private *priv, uint8_t plen)
{
struct sk_buff *skb;
struct nci_data_hdr *hdr;
skb = nci_skb_alloc(priv->ndev, (NCI_DATA_HDR_SIZE + plen), GFP_KERNEL);
if (!skb) {
pr_err("no memory for data\n");
return NULL;
}
hdr = (struct nci_data_hdr *) skb_put(skb, NCI_DATA_HDR_SIZE);
hdr->conn_id = NCI_CORE_LC_CONNID_PROP_FW_DL;
hdr->rfu = 0;
hdr->plen = plen;
nci_mt_set((__u8 *)hdr, NCI_MT_DATA_PKT);
nci_pbf_set((__u8 *)hdr, NCI_PBF_LAST);
return skb;
}
static void fw_dnld_over(struct nfcmrvl_private *priv, u32 error)
{
if (priv->fw_dnld.fw) {
release_firmware(priv->fw_dnld.fw);
priv->fw_dnld.fw = NULL;
priv->fw_dnld.header = NULL;
priv->fw_dnld.binary_config = NULL;
}
atomic_set(&priv->ndev->cmd_cnt, 0);
del_timer_sync(&priv->ndev->cmd_timer);
del_timer_sync(&priv->fw_dnld.timer);
nfc_info(priv->dev, "FW loading over (%d)]\n", error);
if (error != 0) {
/* failed, halt the chip to avoid power consumption */
nfcmrvl_chip_halt(priv);
}
nfc_fw_download_done(priv->ndev->nfc_dev, priv->fw_dnld.name, error);
}
static void fw_dnld_timeout(unsigned long arg)
{
struct nfcmrvl_private *priv = (struct nfcmrvl_private *) arg;
nfc_err(priv->dev, "FW loading timeout");
priv->fw_dnld.state = STATE_RESET;
fw_dnld_over(priv, -ETIMEDOUT);
}
static int process_state_reset(struct nfcmrvl_private *priv,
struct sk_buff *skb)
{
if (sizeof(nci_pattern_core_reset_ntf) != skb->len ||
memcmp(skb->data, nci_pattern_core_reset_ntf,
sizeof(nci_pattern_core_reset_ntf)))
return -EINVAL;
nfc_info(priv->dev, "BootROM reset, start fw download\n");
/* Start FW download state machine */
priv->fw_dnld.state = STATE_INIT;
nci_send_cmd(priv->ndev, NCI_OP_CORE_INIT_CMD, 0, NULL);
return 0;
}
static int process_state_init(struct nfcmrvl_private *priv, struct sk_buff *skb)
{
struct nci_core_set_config_cmd cmd;
if (sizeof(nci_pattern_core_init_rsp) >= skb->len ||
memcmp(skb->data, nci_pattern_core_init_rsp,
sizeof(nci_pattern_core_init_rsp)))
return -EINVAL;
cmd.num_params = 1;
cmd.param.id = NFCMRVL_PROP_REF_CLOCK;
cmd.param.len = 4;
memcpy(cmd.param.val, &priv->fw_dnld.header->ref_clock, 4);
nci_send_cmd(priv->ndev, NCI_OP_CORE_SET_CONFIG_CMD, 3 + cmd.param.len,
&cmd);
priv->fw_dnld.state = STATE_SET_REF_CLOCK;
return 0;
}
static void create_lc(struct nfcmrvl_private *priv)
{
uint8_t param[2] = { NCI_CORE_LC_PROP_FW_DL, 0x0 };
priv->fw_dnld.state = STATE_OPEN_LC;
nci_send_cmd(priv->ndev, NCI_OP_CORE_CONN_CREATE_CMD, 2, param);
}
static int process_state_set_ref_clock(struct nfcmrvl_private *priv,
struct sk_buff *skb)
{
struct nci_core_set_config_cmd cmd;
if (sizeof(nci_pattern_core_set_config_rsp) != skb->len ||
memcmp(skb->data, nci_pattern_core_set_config_rsp, skb->len))
return -EINVAL;
cmd.num_params = 1;
cmd.param.id = NFCMRVL_PROP_SET_HI_CONFIG;
switch (priv->phy) {
case NFCMRVL_PHY_UART:
cmd.param.len = 5;
memcpy(cmd.param.val,
&priv->fw_dnld.binary_config->uart.baudrate,
4);
cmd.param.val[4] =
priv->fw_dnld.binary_config->uart.flow_control;
break;
case NFCMRVL_PHY_I2C:
cmd.param.len = 5;
memcpy(cmd.param.val,
&priv->fw_dnld.binary_config->i2c.clk,
4);
cmd.param.val[4] = 0;
break;
case NFCMRVL_PHY_SPI:
cmd.param.len = 5;
memcpy(cmd.param.val,
&priv->fw_dnld.binary_config->spi.clk,
4);
cmd.param.val[4] = 0;
break;
default:
create_lc(priv);
return 0;
}
priv->fw_dnld.state = STATE_SET_HI_CONFIG;
nci_send_cmd(priv->ndev, NCI_OP_CORE_SET_CONFIG_CMD, 3 + cmd.param.len,
&cmd);
return 0;
}
static int process_state_set_hi_config(struct nfcmrvl_private *priv,
struct sk_buff *skb)
{
if (sizeof(nci_pattern_core_set_config_rsp) != skb->len ||
memcmp(skb->data, nci_pattern_core_set_config_rsp, skb->len))
return -EINVAL;
create_lc(priv);
return 0;
}
static int process_state_open_lc(struct nfcmrvl_private *priv,
struct sk_buff *skb)
{
if (sizeof(nci_pattern_core_conn_create_rsp) >= skb->len ||
memcmp(skb->data, nci_pattern_core_conn_create_rsp,
sizeof(nci_pattern_core_conn_create_rsp)))
return -EINVAL;
priv->fw_dnld.state = STATE_FW_DNLD;
priv->fw_dnld.substate = SUBSTATE_WAIT_COMMAND;
priv->fw_dnld.offset = priv->fw_dnld.binary_config->offset;
return 0;
}
static int process_state_fw_dnld(struct nfcmrvl_private *priv,
struct sk_buff *skb)
{
uint16_t len;
uint16_t comp_len;
struct sk_buff *out_skb;
switch (priv->fw_dnld.substate) {
case SUBSTATE_WAIT_COMMAND:
/*
* Command format:
* B0..2: NCI header
* B3 : Helper command (0xA5)
* B4..5: le16 data size
* B6..7: le16 data size complement (~)
* B8..N: payload
*/
/* Remove NCI HDR */
skb_pull(skb, 3);
if (skb->data[0] != HELPER_CMD_PACKET_FORMAT || skb->len != 5) {
nfc_err(priv->dev, "bad command");
return -EINVAL;
}
skb_pull(skb, 1);
memcpy(&len, skb->data, 2);
skb_pull(skb, 2);
memcpy(&comp_len, skb->data, 2);
skb_pull(skb, 2);
len = get_unaligned_le16(&len);
comp_len = get_unaligned_le16(&comp_len);
if (((~len) & 0xFFFF) != comp_len) {
nfc_err(priv->dev, "bad len complement: %x %x %x",
len, comp_len, (~len & 0xFFFF));
out_skb = alloc_lc_skb(priv, 1);
if (!out_skb)
return -ENOMEM;
*skb_put(out_skb, 1) = 0xBF;
nci_send_frame(priv->ndev, out_skb);
priv->fw_dnld.substate = SUBSTATE_WAIT_NACK_CREDIT;
return 0;
}
priv->fw_dnld.chunk_len = len;
out_skb = alloc_lc_skb(priv, 1);
if (!out_skb)
return -ENOMEM;
*skb_put(out_skb, 1) = HELPER_ACK_PACKET_FORMAT;
nci_send_frame(priv->ndev, out_skb);
priv->fw_dnld.substate = SUBSTATE_WAIT_ACK_CREDIT;
break;
case SUBSTATE_WAIT_ACK_CREDIT:
if (sizeof(nci_pattern_core_conn_credits_ntf) != skb->len ||
memcmp(nci_pattern_core_conn_credits_ntf, skb->data,
skb->len)) {
nfc_err(priv->dev, "bad packet: waiting for credit");
return -EINVAL;
}
if (priv->fw_dnld.chunk_len == 0) {
/* FW Loading is done */
uint8_t conn_id = NCI_CORE_LC_CONNID_PROP_FW_DL;
priv->fw_dnld.state = STATE_CLOSE_LC;
nci_send_cmd(priv->ndev, NCI_OP_CORE_CONN_CLOSE_CMD,
1, &conn_id);
} else {
out_skb = alloc_lc_skb(priv, priv->fw_dnld.chunk_len);
if (!out_skb)
return -ENOMEM;
memcpy(skb_put(out_skb, priv->fw_dnld.chunk_len),
((uint8_t *)priv->fw_dnld.fw->data) +
priv->fw_dnld.offset,
priv->fw_dnld.chunk_len);
nci_send_frame(priv->ndev, out_skb);
priv->fw_dnld.substate = SUBSTATE_WAIT_DATA_CREDIT;
}
break;
case SUBSTATE_WAIT_DATA_CREDIT:
if (sizeof(nci_pattern_core_conn_credits_ntf) != skb->len ||
memcmp(nci_pattern_core_conn_credits_ntf, skb->data,
skb->len)) {
nfc_err(priv->dev, "bad packet: waiting for credit");
return -EINVAL;
}
priv->fw_dnld.offset += priv->fw_dnld.chunk_len;
priv->fw_dnld.chunk_len = 0;
priv->fw_dnld.substate = SUBSTATE_WAIT_COMMAND;
break;
case SUBSTATE_WAIT_NACK_CREDIT:
if (sizeof(nci_pattern_core_conn_credits_ntf) != skb->len ||
memcmp(nci_pattern_core_conn_credits_ntf, skb->data,
skb->len)) {
nfc_err(priv->dev, "bad packet: waiting for credit");
return -EINVAL;
}
priv->fw_dnld.substate = SUBSTATE_WAIT_COMMAND;
break;
}
return 0;
}
static int process_state_close_lc(struct nfcmrvl_private *priv,
struct sk_buff *skb)
{
if (sizeof(nci_pattern_core_conn_close_rsp) != skb->len ||
memcmp(skb->data, nci_pattern_core_conn_close_rsp, skb->len))
return -EINVAL;
priv->fw_dnld.state = STATE_BOOT;
nci_send_cmd(priv->ndev, NCI_OP_PROPRIETARY_BOOT_CMD, 0, NULL);
return 0;
}
static int process_state_boot(struct nfcmrvl_private *priv, struct sk_buff *skb)
{
if (sizeof(nci_pattern_proprietary_boot_rsp) != skb->len ||
memcmp(skb->data, nci_pattern_proprietary_boot_rsp, skb->len))
return -EINVAL;
/*
* Update HI config to use the right configuration for the next
* data exchanges.
*/
priv->if_ops->nci_update_config(priv,
&priv->fw_dnld.binary_config->config);
if (priv->fw_dnld.binary_config == &priv->fw_dnld.header->helper) {
/*
* This is the case where an helper was needed and we have
* uploaded it. Now we have to wait the next RESET NTF to start
* FW download.
*/
priv->fw_dnld.state = STATE_RESET;
priv->fw_dnld.binary_config = &priv->fw_dnld.header->firmware;
nfc_info(priv->dev, "FW loading: helper loaded");
} else {
nfc_info(priv->dev, "FW loading: firmware loaded");
fw_dnld_over(priv, 0);
}
return 0;
}
static void fw_dnld_rx_work(struct work_struct *work)
{
int ret;
struct sk_buff *skb;
struct nfcmrvl_fw_dnld *fw_dnld = container_of(work,
struct nfcmrvl_fw_dnld,
rx_work);
struct nfcmrvl_private *priv = container_of(fw_dnld,
struct nfcmrvl_private,
fw_dnld);
while ((skb = skb_dequeue(&fw_dnld->rx_q))) {
nfc_send_to_raw_sock(priv->ndev->nfc_dev, skb,
RAW_PAYLOAD_NCI, NFC_DIRECTION_RX);
switch (fw_dnld->state) {
case STATE_RESET:
ret = process_state_reset(priv, skb);
break;
case STATE_INIT:
ret = process_state_init(priv, skb);
break;
case STATE_SET_REF_CLOCK:
ret = process_state_set_ref_clock(priv, skb);
break;
case STATE_SET_HI_CONFIG:
ret = process_state_set_hi_config(priv, skb);
break;
case STATE_OPEN_LC:
ret = process_state_open_lc(priv, skb);
break;
case STATE_FW_DNLD:
ret = process_state_fw_dnld(priv, skb);
break;
case STATE_CLOSE_LC:
ret = process_state_close_lc(priv, skb);
break;
case STATE_BOOT:
ret = process_state_boot(priv, skb);
break;
default:
ret = -EFAULT;
}
kfree_skb(skb);
if (ret != 0) {
nfc_err(priv->dev, "FW loading error");
fw_dnld_over(priv, ret);
break;
}
}
}
int nfcmrvl_fw_dnld_init(struct nfcmrvl_private *priv)
{
char name[32];
INIT_WORK(&priv->fw_dnld.rx_work, fw_dnld_rx_work);
snprintf(name, sizeof(name), "%s_nfcmrvl_fw_dnld_rx_wq",
dev_name(priv->dev));
priv->fw_dnld.rx_wq = create_singlethread_workqueue(name);
if (!priv->fw_dnld.rx_wq)
return -ENOMEM;
skb_queue_head_init(&priv->fw_dnld.rx_q);
return 0;
}
void nfcmrvl_fw_dnld_deinit(struct nfcmrvl_private *priv)
{
destroy_workqueue(priv->fw_dnld.rx_wq);
}
void nfcmrvl_fw_dnld_recv_frame(struct nfcmrvl_private *priv,
struct sk_buff *skb)
{
/* Allow next command */
atomic_set(&priv->ndev->cmd_cnt, 1);
del_timer_sync(&priv->ndev->cmd_timer);
/* Queue and trigger rx work */
skb_queue_tail(&priv->fw_dnld.rx_q, skb);
queue_work(priv->fw_dnld.rx_wq, &priv->fw_dnld.rx_work);
}
void nfcmrvl_fw_dnld_abort(struct nfcmrvl_private *priv)
{
fw_dnld_over(priv, -EHOSTDOWN);
}
int nfcmrvl_fw_dnld_start(struct nci_dev *ndev, const char *firmware_name)
{
struct nfcmrvl_private *priv = nci_get_drvdata(ndev);
struct nfcmrvl_fw_dnld *fw_dnld = &priv->fw_dnld;
if (!priv->support_fw_dnld)
return -ENOTSUPP;
if (!firmware_name || !firmware_name[0])
return -EINVAL;
strcpy(fw_dnld->name, firmware_name);
/*
* Retrieve FW binary file and parse it to initialize FW download
* state machine.
*/
/* Retrieve FW binary */
if (request_firmware(&fw_dnld->fw, firmware_name, priv->dev) < 0) {
nfc_err(priv->dev, "failed to retrieve FW %s", firmware_name);
return -ENOENT;
}
fw_dnld->header = (const struct nfcmrvl_fw *) priv->fw_dnld.fw->data;
if (fw_dnld->header->magic != NFCMRVL_FW_MAGIC ||
fw_dnld->header->phy != priv->phy) {
nfc_err(priv->dev, "bad firmware binary %s magic=0x%x phy=%d",
firmware_name, fw_dnld->header->magic,
fw_dnld->header->phy);
release_firmware(fw_dnld->fw);
fw_dnld->header = NULL;
return -EINVAL;
}
if (fw_dnld->header->helper.offset != 0) {
nfc_info(priv->dev, "loading helper");
fw_dnld->binary_config = &fw_dnld->header->helper;
} else {
nfc_info(priv->dev, "loading firmware");
fw_dnld->binary_config = &fw_dnld->header->firmware;
}
/* Configure a timer for timeout */
setup_timer(&priv->fw_dnld.timer, fw_dnld_timeout,
(unsigned long) priv);
mod_timer(&priv->fw_dnld.timer,
jiffies + msecs_to_jiffies(FW_DNLD_TIMEOUT));
/* Ronfigure HI to be sure that it is the bootrom values */
priv->if_ops->nci_update_config(priv,
&fw_dnld->header->bootrom.config);
/* Allow first command */
atomic_set(&priv->ndev->cmd_cnt, 1);
/* First, reset the chip */
priv->fw_dnld.state = STATE_RESET;
nfcmrvl_chip_reset(priv);
/* Now wait for CORE_RESET_NTF or timeout */
return 0;
}