// SPDX-License-Identifier: GPL-2.0-only /* * Copyright (c) 2017-2026 Morse Micro */ #include "yaps_hw.h" #include "bus.h" #include "hif.h" #include "yaps.h" #define YAPS_HW_WINDOW_SIZE_BYTES 32768 #define YAPS_MAX_PKT_SIZE_BYTES 16128 #define YAPS_METADATA_PAGE_COUNT 1 #define YAPS_PHANDLE_CORRUPTION_WAR_EXTRA_PAGE 1 #define YAPS_PAGE_SIZE 256 /* Calculate padding required for yaps transaction */ #define YAPS_CALC_PADDING(_bytes) ((_bytes) & 0x3 ? (4 - ((_bytes) & 0x3)) : 0) #define YAPS_RESERVED_PAGE_SIZE 256 /* * Yaps data stream delimiter is a 32 bit word with the following fields: * * pkt_size (14 bits) - Packet size not including delimiter or padding * pool_id (3 bits) - Pool that pages should be allocated from. * padding (2 bits) - Padding required to bring packet to word (4 byte) * irq (1 bit ) - Raise a PKT_IRQ on the YDS this is sent to * reserved (5 bits) - Reserved, must write as 0 * crc (7 bits) - YAPS CRC */ /* Packet size not including delimiter or padding */ #define YAPS_DELIM_GET_PKT_SIZE(_delim) \ (((_delim) & 0x3FFF) - YAPS_RESERVED_PAGE_SIZE) #define YAPS_DELIM_SET_PKT_SIZE(_pkt_size) \ (((_pkt_size) & 0x3FFF) + YAPS_RESERVED_PAGE_SIZE) #define YAPS_DELIM_GET_PHANDLE_SIZE(_delim) (((_delim) & 0x3FFF)) /* Pool that pages should be allocated from. */ #define YAPS_DELIM_SET_POOL_ID(_pool_id) (((_pool_id) & 0x7) << 14) /* Padding required to bring packet to word (4 byte) boundary */ #define YAPS_DELIM_GET_PADDING(_delim) (((_delim) >> 17) & 0x3) #define YAPS_DELIM_SET_PADDING(_padding) (((_padding) & 0x3) << 17) /* Raise a PKT_IRQ on the YDS this is sent to */ #define YAPS_DELIM_SET_IRQ(_irq) (((_irq) & 0x1) << 19) /* YAPS CRC */ #define YAPS_DELIM_GET_CRC(_delim) (((_delim) >> 25) & 0x7F) #define YAPS_DELIM_SET_CRC(_crc) (((_crc) & 0x7F) << 25) struct mm81x_yaps_status_regs { /* Allocation pools */ u32 tc_tx_pool_num_pages; u32 tc_cmd_pool_num_pages; u32 tc_beacon_pool_num_pages; u32 tc_mgmt_pool_num_pages; u32 fc_rx_pool_num_pages; u32 fc_resp_pool_num_pages; u32 fc_tx_sts_pool_num_pages; u32 fc_aux_pool_num_pages; u32 tc_tx_num_pkts; u32 tc_cmd_num_pkts; u32 tc_beacon_num_pkts; u32 tc_mgmt_num_pkts; u32 fc_num_pkts; u32 fc_done_num_pkts; u32 fc_rx_bytes_in_queue; u32 tc_delim_crc_fail_detected; u32 fc_host_ysl_status; u32 lock; } __packed __aligned(8); struct mm81x_yaps_hw_status_regs { __le32 tc_tx_pool_num_pages; __le32 tc_cmd_pool_num_pages; __le32 tc_beacon_pool_num_pages; __le32 tc_mgmt_pool_num_pages; __le32 fc_rx_pool_num_pages; __le32 fc_resp_pool_num_pages; __le32 fc_tx_sts_pool_num_pages; __le32 fc_aux_pool_num_pages; __le32 tc_tx_num_pkts; __le32 tc_cmd_num_pkts; __le32 tc_beacon_num_pkts; __le32 tc_mgmt_num_pkts; __le32 fc_num_pkts; __le32 fc_done_num_pkts; __le32 fc_rx_bytes_in_queue; __le32 tc_delim_crc_fail_detected; __le32 fc_host_ysl_status; __le32 lock; } __packed __aligned(8); struct mm81x_yaps_hw_aux_data { unsigned long access_lock; u32 yds_addr; u32 ysl_addr; u32 status_regs_addr; /* Alloc pool sizes */ u16 tc_tx_pool_size; u16 tc_cmd_pool_size; u8 tc_beacon_pool_size; u8 tc_mgmt_pool_size; u8 fc_rx_pool_size; u8 fc_resp_pool_size; u8 fc_tx_sts_pool_size; u8 fc_aux_pool_size; /* To chip/from chip queue sizes */ u8 tc_tx_q_size; u8 tc_cmd_q_size; u8 tc_beacon_q_size; u8 tc_mgmt_q_size; u8 fc_q_size; u8 fc_done_q_size; u16 reserved_yaps_page_size; /* Buffers to/from chip to support large contiguous reads/writes */ char *to_chip_buffer; char *from_chip_buffer; /* status registers in host endian */ struct mm81x_yaps_status_regs status_regs; /* DMA target buffer in firmware endian */ struct mm81x_yaps_hw_status_regs hw_status_regs; }; static int mm81x_yaps_hw_lock(struct mm81x_yaps *yaps) { if (test_and_set_bit_lock(0, &yaps->aux_data->access_lock)) return -1; return 0; } static void mm81x_yaps_hw_unlock(struct mm81x_yaps *yaps) { clear_bit_unlock(0, &yaps->aux_data->access_lock); } static void mm81x_yaps_hw_fill_aux_data_from_hw_tbl(struct mm81x_yaps_hw_aux_data *a, struct mm81x_yaps_hw_table *t) { a->ysl_addr = __le32_to_cpu(t->ysl_addr); a->yds_addr = __le32_to_cpu(t->yds_addr); a->status_regs_addr = __le32_to_cpu(t->status_regs_addr); a->tc_tx_pool_size = __le16_to_cpu(t->tc_tx_pool_size); a->fc_rx_pool_size = __le16_to_cpu(t->fc_rx_pool_size); a->tc_cmd_pool_size = t->tc_cmd_pool_size; a->tc_beacon_pool_size = t->tc_beacon_pool_size; a->tc_mgmt_pool_size = t->tc_mgmt_pool_size; a->fc_resp_pool_size = t->fc_resp_pool_size; a->fc_tx_sts_pool_size = t->fc_tx_sts_pool_size; a->fc_aux_pool_size = t->fc_aux_pool_size; a->tc_tx_q_size = t->tc_tx_q_size; a->tc_cmd_q_size = t->tc_cmd_q_size; a->tc_beacon_q_size = t->tc_beacon_q_size; a->tc_mgmt_q_size = t->tc_mgmt_q_size; a->fc_q_size = t->fc_q_size; a->fc_done_q_size = t->fc_done_q_size; a->reserved_yaps_page_size = le16_to_cpu(t->yaps_reserved_page_size); } static u8 mm81x_yaps_hw_crc(u32 word) { u8 crc = 0; u8 byte; int i; /* Mask to look at only non-CRC bits */ word &= 0x1ffffff; for (i = 0; i < 4; i++) { byte = (word >> 24) & 0xff; crc = crc7_be(crc, &byte, 1); word <<= 8; } return crc >> 1; } static u32 mm81x_write_pkts_h_build_delim(struct mm81x_yaps *yaps, unsigned int size, u8 pool_id, bool irq) { u32 delim = 0; delim |= YAPS_DELIM_SET_PKT_SIZE(size); delim |= YAPS_DELIM_SET_PADDING(YAPS_CALC_PADDING(size)); delim |= YAPS_DELIM_SET_POOL_ID(pool_id); delim |= YAPS_DELIM_SET_IRQ(irq); delim |= YAPS_DELIM_SET_CRC(mm81x_yaps_hw_crc(delim)); return delim; } void mm81x_yaps_hw_enable_irqs(struct mm81x *mors, bool enable) { mm81x_hw_irq_enable(mors, MM81X_INT_YAPS_FC_PKT_WAITING_IRQN, enable); mm81x_hw_irq_enable(mors, MM81X_INT_YAPS_FC_PACKET_FREED_UP_IRQN, enable); } void mm81x_yaps_hw_read_table(struct mm81x *mors, struct mm81x_yaps_hw_table *tbl_ptr) { mm81x_yaps_hw_fill_aux_data_from_hw_tbl(mors->hif.u.yaps.aux_data, tbl_ptr); mm81x_yaps_hw_enable_irqs(mors, true); } static unsigned int mm81x_write_pkts_h_pages_required(struct mm81x_yaps *yaps, unsigned int size_bytes) { /* Always account for the first metadata page */ return DIV_ROUND_UP(size_bytes + yaps->aux_data->reserved_yaps_page_size, YAPS_PAGE_SIZE) + YAPS_METADATA_PAGE_COUNT + YAPS_PHANDLE_CORRUPTION_WAR_EXTRA_PAGE; } /* * Checks if a single pkt will fit in the chip using the pool/alloc holding * information from the last status register read. */ static bool mm81x_write_pkts_h_will_fit(struct mm81x_yaps *yaps, struct mm81x_yaps_pkt *pkt, bool update) { bool will_fit = true; const int pages_required = mm81x_write_pkts_h_pages_required(yaps, pkt->skb->len); int *pool_pages_avail = NULL; int *pkts_in_queue = NULL; int queue_pkts_avail = 0; switch (pkt->tc_queue) { case MM81X_YAPS_TX_Q: pool_pages_avail = &yaps->aux_data->status_regs.tc_tx_pool_num_pages; pkts_in_queue = &yaps->aux_data->status_regs.tc_tx_num_pkts; queue_pkts_avail = yaps->aux_data->tc_tx_q_size - *pkts_in_queue; break; case MM81X_YAPS_CMD_Q: pool_pages_avail = &yaps->aux_data->status_regs.tc_cmd_pool_num_pages; pkts_in_queue = &yaps->aux_data->status_regs.tc_cmd_num_pkts; queue_pkts_avail = yaps->aux_data->tc_cmd_q_size - *pkts_in_queue; break; case MM81X_YAPS_BEACON_Q: pool_pages_avail = &yaps->aux_data->status_regs.tc_beacon_pool_num_pages; pkts_in_queue = &yaps->aux_data->status_regs.tc_beacon_num_pkts; queue_pkts_avail = yaps->aux_data->tc_beacon_q_size - *pkts_in_queue; break; case MM81X_YAPS_MGMT_Q: pool_pages_avail = &yaps->aux_data->status_regs.tc_mgmt_pool_num_pages; pkts_in_queue = &yaps->aux_data->status_regs.tc_mgmt_num_pkts; queue_pkts_avail = yaps->aux_data->tc_mgmt_q_size - *pkts_in_queue; break; default: dev_err(yaps->mors->dev, "yaps invalid tc queue"); return false; } WARN_ON(queue_pkts_avail < 0); if (pages_required > *pool_pages_avail) will_fit = false; if (queue_pkts_avail == 0) will_fit = false; if (will_fit && update) { *pool_pages_avail -= pages_required; *pkts_in_queue += 1; } return will_fit; } static int mm81x_write_pkts_h_err_check(struct mm81x_yaps *yaps, struct mm81x_yaps_pkt *pkt) { if (pkt->skb->len + yaps->aux_data->reserved_yaps_page_size > YAPS_MAX_PKT_SIZE_BYTES) return -EMSGSIZE; if (pkt->tc_queue >= MM81X_YAPS_NUM_TC_Q) return -EINVAL; if (!mm81x_write_pkts_h_will_fit(yaps, pkt, true)) return -EAGAIN; return 0; } static int mm81x_yaps_hw_write_pkts(struct mm81x_yaps *yaps, struct mm81x_yaps_pkt *pkts, int num_pkts, int *num_pkts_sent) { int ret = 0; int i; u32 delim = 0; int tx_len; int batch_txn_len = 0; int pkts_pending = 0; bool delim_irq = false; char *to_chip_buffer_aligned = PTR_ALIGN(yaps->aux_data->to_chip_buffer, mm81x_bus_get_alignment(yaps->mors)); char *write_buf = to_chip_buffer_aligned; ret = mm81x_yaps_hw_lock(yaps); if (ret) { dev_dbg(yaps->mors->dev, "yaps lock failed %d", ret); return ret; } *num_pkts_sent = 0; /* Check packet conditions */ ret = mm81x_write_pkts_h_err_check(yaps, &pkts[0]); if (ret) goto exit; /* Batch packets into larger transactions */ for (i = 0; i < num_pkts; ++i) { u32 pkt_size = pkts[i].skb->len + YAPS_CALC_PADDING(pkts[i].skb->len); tx_len = pkt_size + sizeof(delim); /* * Send when we have reached window size, don't split pkt over * boundary */ if ((batch_txn_len + tx_len) > YAPS_HW_WINDOW_SIZE_BYTES) { ret = mm81x_dm_write(yaps->mors, yaps->aux_data->yds_addr, to_chip_buffer_aligned, batch_txn_len); batch_txn_len = 0; if (ret) goto exit; write_buf = to_chip_buffer_aligned; *num_pkts_sent += pkts_pending; pkts_pending = 0; } if ((i + 1) == num_pkts) { /* The last packet in the queue has IRQ set */ delim_irq = true; } else { /* * Since this is not the last packet, we can check for * the next one. In case of errors in the next packet * set the IRQ */ ret = mm81x_write_pkts_h_err_check(yaps, &pkts[i + 1]); if (ret) delim_irq = true; } /* Build stream header*/ delim = mm81x_write_pkts_h_build_delim( yaps, pkt_size, pkts[i].tc_queue, delim_irq); *((__le32 *)write_buf) = cpu_to_le32(delim); memcpy(write_buf + sizeof(delim), pkts[i].skb->data, pkts[i].skb->len); write_buf += tx_len; batch_txn_len += tx_len; pkts_pending++; if (ret) goto exit; } exit: if (batch_txn_len > 0) { ret = mm81x_dm_write(yaps->mors, yaps->aux_data->yds_addr, to_chip_buffer_aligned, batch_txn_len); *num_pkts_sent += pkts_pending; } mm81x_yaps_hw_unlock(yaps); return ret; } static bool mm81x_read_pkts_h_is_valid_delim(u32 delim) { u8 calc_crc = mm81x_yaps_hw_crc(delim); int pkt_size = YAPS_DELIM_GET_PHANDLE_SIZE(delim); int padding = YAPS_DELIM_GET_PADDING(delim); if (calc_crc != YAPS_DELIM_GET_CRC(delim)) return false; if (pkt_size == 0) return false; if ((pkt_size + padding) > YAPS_MAX_PKT_SIZE_BYTES) return false; /* Pkt length + padding should not require more padding */ if (YAPS_CALC_PADDING(pkt_size) != padding) return false; return true; } static int mm81x_read_pkts_h_bytes_remaining(struct mm81x_yaps *yaps) { u32 bytes_in_queue = yaps->aux_data->status_regs.fc_rx_bytes_in_queue; u32 delim_overhead = yaps->aux_data->status_regs.fc_num_pkts * sizeof(u32); u32 reserved_bytes = yaps->aux_data->status_regs.fc_num_pkts * yaps->aux_data->reserved_yaps_page_size; if (WARN_ON(bytes_in_queue > INT_MAX) || WARN_ON(delim_overhead > INT_MAX) || WARN_ON(reserved_bytes > INT_MAX)) return -EIO; return (int)bytes_in_queue; } static int mm81x_yaps_hw_read_pkts(struct mm81x_yaps *yaps, struct mm81x_yaps_pkt *pkts, int num_pkts_max, int *num_pkts_received) { int ret; int i = 0; char *from_chip_buffer_aligned = PTR_ALIGN(yaps->aux_data->from_chip_buffer, mm81x_bus_get_alignment(yaps->mors)); char *read_ptr = from_chip_buffer_aligned; int bytes_remaining = mm81x_read_pkts_h_bytes_remaining(yaps); bool again = false; *num_pkts_received = 0; if (num_pkts_max == 0 || bytes_remaining == 0) return 0; if (bytes_remaining < 0) return bytes_remaining; if (bytes_remaining > YAPS_HW_WINDOW_SIZE_BYTES) { bytes_remaining = YAPS_HW_WINDOW_SIZE_BYTES; again = true; } /* * This is more coarse-grained than it needs to be - once the data * is read into a local buffer the lock can be released, however * access to from_chip_buffer will need to be protected with its * own lock */ ret = mm81x_yaps_hw_lock(yaps); if (ret) { dev_dbg(yaps->mors->dev, "yaps lock failed %d", ret); return ret; } /* Read all available packets to the buffer */ ret = mm81x_dm_read(yaps->mors, yaps->aux_data->ysl_addr, from_chip_buffer_aligned, bytes_remaining); if (ret) goto exit; /* Split serialised packets from buffer */ while (i < num_pkts_max && bytes_remaining > 0) { u32 delim; int total_len; int pkt_size; delim = le32_to_cpu(*((__le32 *)read_ptr)); read_ptr += sizeof(delim); bytes_remaining -= sizeof(delim); /* End of stream */ if (!delim) break; if (!mm81x_read_pkts_h_is_valid_delim(delim)) { /* * This will start a hunt for a valid delimiter. Given * the CRC is only 7 bit it's possible to find an * invalid block with a valid delimiter, leading to * desynchronisation. */ dev_warn(yaps->mors->dev, "yaps invalid delim"); break; } /* Total length in chip */ pkt_size = YAPS_DELIM_GET_PKT_SIZE(delim); total_len = pkt_size + YAPS_DELIM_GET_PADDING(delim); if (pkts[i].skb) dev_err(yaps->mors->dev, "yaps packet leak"); /* SKB doesn't want padding */ pkts[i].skb = dev_alloc_skb(pkt_size); if (!pkts[i].skb) { ret = -ENOMEM; dev_err(yaps->mors->dev, "yaps no mem for skb"); goto exit; } skb_put(pkts[i].skb, pkt_size); if (total_len <= bytes_remaining) { memcpy(pkts[i].skb->data, read_ptr, pkt_size); read_ptr += total_len; bytes_remaining -= total_len; } else { const int read_overhang_len = total_len - bytes_remaining; const int pkt_overhang_len = pkt_size - bytes_remaining; memcpy(pkts[i].skb->data, read_ptr, bytes_remaining); read_ptr = from_chip_buffer_aligned; ret = mm81x_dm_read( yaps->mors, /* Offset by 4 to avoid retry logic */ yaps->aux_data->ysl_addr + 4, read_ptr, read_overhang_len); if (ret) goto exit; memcpy(pkts[i].skb->data + bytes_remaining, read_ptr, pkt_overhang_len); read_ptr += read_overhang_len; bytes_remaining = 0; } *num_pkts_received += 1; i++; } if (again) ret = -EAGAIN; exit: mm81x_yaps_hw_unlock(yaps); return ret; } static int mm81x_yaps_hw_update_status(struct mm81x_yaps *yaps) { int ret; int tc_total_pkt_count; unsigned long reg_read_timeout; struct mm81x_yaps_status_regs *r = &yaps->aux_data->status_regs; struct mm81x_yaps_hw_status_regs *hw_r = &yaps->aux_data->hw_status_regs; ret = mm81x_yaps_hw_lock(yaps); if (ret) { dev_dbg(yaps->mors->dev, "yaps lock failed %d", ret); return ret; } reg_read_timeout = jiffies + msecs_to_jiffies(100); do { if (time_after(jiffies, reg_read_timeout)) { dev_err(yaps->mors->dev, "timed out reading status registers: %d", ret); ret = -ETIMEDOUT; break; } ret = mm81x_dm_read(yaps->mors, yaps->aux_data->status_regs_addr, (u8 *)hw_r, sizeof(*hw_r)); } while (!ret && le32_to_cpu(hw_r->lock)); if (ret) { if (ret != -ENODEV) { dev_err(yaps->mors->dev, "error reading yaps status registers: %d", ret); } goto exit_unlock; } r->tc_tx_pool_num_pages = le32_to_cpu(hw_r->tc_tx_pool_num_pages); r->tc_cmd_pool_num_pages = le32_to_cpu(hw_r->tc_cmd_pool_num_pages); r->tc_beacon_pool_num_pages = le32_to_cpu(hw_r->tc_beacon_pool_num_pages); r->tc_mgmt_pool_num_pages = le32_to_cpu(hw_r->tc_mgmt_pool_num_pages); r->fc_rx_pool_num_pages = le32_to_cpu(hw_r->fc_rx_pool_num_pages); r->fc_resp_pool_num_pages = le32_to_cpu(hw_r->fc_resp_pool_num_pages); r->fc_tx_sts_pool_num_pages = le32_to_cpu(hw_r->fc_tx_sts_pool_num_pages); r->fc_aux_pool_num_pages = le32_to_cpu(hw_r->fc_aux_pool_num_pages); r->tc_tx_num_pkts = le32_to_cpu(hw_r->tc_tx_num_pkts); r->tc_cmd_num_pkts = le32_to_cpu(hw_r->tc_cmd_num_pkts); r->tc_beacon_num_pkts = le32_to_cpu(hw_r->tc_beacon_num_pkts); r->tc_mgmt_num_pkts = le32_to_cpu(hw_r->tc_mgmt_num_pkts); r->fc_num_pkts = le32_to_cpu(hw_r->fc_num_pkts); r->fc_done_num_pkts = le32_to_cpu(hw_r->fc_done_num_pkts); r->fc_rx_bytes_in_queue = le32_to_cpu(hw_r->fc_rx_bytes_in_queue); r->tc_delim_crc_fail_detected = le32_to_cpu(hw_r->tc_delim_crc_fail_detected); r->lock = le32_to_cpu(hw_r->lock); r->fc_host_ysl_status = le32_to_cpu(hw_r->fc_host_ysl_status); tc_total_pkt_count = r->tc_tx_num_pkts + r->tc_cmd_num_pkts + r->tc_beacon_num_pkts + r->tc_mgmt_num_pkts; if (r->tc_delim_crc_fail_detected) { /* * Host and chip have become desynchronised. This can happen if * the chip crashes during a YAPS transaction. We cannot * recover from this. */ dev_err(yaps->mors->dev, "to-chip yaps delimiter CRC fail, pkt_count=%d", tc_total_pkt_count); ret = -EIO; } if (mm81x_read_pkts_h_bytes_remaining(yaps)) set_bit(MM81X_HIF_EVT_RX_PEND, &yaps->mors->hif.event_flags); exit_unlock: mm81x_yaps_hw_unlock(yaps); return ret; } static const struct mm81x_yaps_ops mm81x_yaps_hw_ops = { .write_pkts = mm81x_yaps_hw_write_pkts, .read_pkts = mm81x_yaps_hw_read_pkts, .update_status = mm81x_yaps_hw_update_status, }; int mm81x_yaps_hw_init(struct mm81x *mors) { int ret = 0; struct mm81x_yaps *yaps = NULL; int aux_data_len = sizeof(struct mm81x_yaps_hw_aux_data); int alignment = mm81x_bus_get_alignment(mors); yaps = &mors->hif.u.yaps; yaps->aux_data = kzalloc(aux_data_len, GFP_KERNEL); if (!yaps->aux_data) { ret = -ENOMEM; goto err_exit; } yaps->aux_data->to_chip_buffer = kzalloc(YAPS_HW_WINDOW_SIZE_BYTES + alignment - 1, GFP_KERNEL); if (!yaps->aux_data->to_chip_buffer) { ret = -ENOMEM; goto err_exit; } yaps->aux_data->from_chip_buffer = kzalloc(YAPS_HW_WINDOW_SIZE_BYTES + alignment - 1, GFP_KERNEL); if (!yaps->aux_data->from_chip_buffer) { ret = -ENOMEM; goto err_exit; } if (!IS_ALIGNED((uintptr_t)&yaps->aux_data->status_regs, alignment)) { dev_warn(mors->dev, "Status registers are not aligned to %d bytes", alignment); } yaps->ops = &mm81x_yaps_hw_ops; return ret; err_exit: mm81x_yaps_hw_finish(mors); return ret; } void mm81x_yaps_hw_finish(struct mm81x *mors) { struct mm81x_yaps *yaps; yaps = &mors->hif.u.yaps; if (yaps->aux_data) { kfree(yaps->aux_data->from_chip_buffer); yaps->aux_data->from_chip_buffer = NULL; kfree(yaps->aux_data->to_chip_buffer); yaps->aux_data->to_chip_buffer = NULL; kfree(yaps->aux_data); yaps->aux_data = NULL; } }