Commit 205fdba5 authored by James Ogletree's avatar James Ogletree Committed by Lee Jones
Browse files

firmware: cs_dsp: Add write sequence interface



A write sequence is a sequence of register addresses
and values executed by some Cirrus DSPs following
certain power state transitions.

Add support for Cirrus drivers to update or add to a
write sequence present in firmware.

Reviewed-by: default avatarCharles Keepax <ckeepax@opensource.cirrus.com>
Signed-off-by: default avatarJames Ogletree <jogletre@opensource.cirrus.com>
Reviewed-by: default avatarJeff LaBundy <jeff@labundy.com>
Link: https://lore.kernel.org/r/20240620161745.2312359-2-jogletre@opensource.cirrus.com


Signed-off-by: default avatarLee Jones <lee@kernel.org>
parent 1613e604
Loading
Loading
Loading
Loading
+278 −0
Original line number Diff line number Diff line
@@ -275,6 +275,12 @@
#define HALO_MPU_VIO_ERR_SRC_MASK           0x00007fff
#define HALO_MPU_VIO_ERR_SRC_SHIFT                   0

/*
 * Write Sequence
 */
#define WSEQ_OP_MAX_WORDS	3
#define WSEQ_END_OF_SCRIPT	0xFFFFFF

struct cs_dsp_ops {
	bool (*validate_version)(struct cs_dsp *dsp, unsigned int version);
	unsigned int (*parse_sizes)(struct cs_dsp *dsp,
@@ -3398,6 +3404,278 @@ int cs_dsp_chunk_read(struct cs_dsp_chunk *ch, int nbits)
}
EXPORT_SYMBOL_NS_GPL(cs_dsp_chunk_read, FW_CS_DSP);


struct cs_dsp_wseq_op {
	struct list_head list;
	u32 address;
	u32 data;
	u16 offset;
	u8 operation;
};

static void cs_dsp_wseq_clear(struct cs_dsp *dsp, struct cs_dsp_wseq *wseq)
{
	struct cs_dsp_wseq_op *op, *op_tmp;

	list_for_each_entry_safe(op, op_tmp, &wseq->ops, list) {
		list_del(&op->list);
		devm_kfree(dsp->dev, op);
	}
}

static int cs_dsp_populate_wseq(struct cs_dsp *dsp, struct cs_dsp_wseq *wseq)
{
	struct cs_dsp_wseq_op *op = NULL;
	struct cs_dsp_chunk chunk;
	u8 *words;
	int ret;

	if (!wseq->ctl) {
		cs_dsp_err(dsp, "No control for write sequence\n");
		return -EINVAL;
	}

	words = kzalloc(wseq->ctl->len, GFP_KERNEL);
	if (!words)
		return -ENOMEM;

	ret = cs_dsp_coeff_read_ctrl(wseq->ctl, 0, words, wseq->ctl->len);
	if (ret) {
		cs_dsp_err(dsp, "Failed to read %s: %d\n", wseq->ctl->subname, ret);
		goto err_free;
	}

	INIT_LIST_HEAD(&wseq->ops);

	chunk = cs_dsp_chunk(words, wseq->ctl->len);

	while (!cs_dsp_chunk_end(&chunk)) {
		op = devm_kzalloc(dsp->dev, sizeof(*op), GFP_KERNEL);
		if (!op) {
			ret = -ENOMEM;
			goto err_free;
		}

		op->offset = cs_dsp_chunk_bytes(&chunk);
		op->operation = cs_dsp_chunk_read(&chunk, 8);

		switch (op->operation) {
		case CS_DSP_WSEQ_END:
			op->data = WSEQ_END_OF_SCRIPT;
			break;
		case CS_DSP_WSEQ_UNLOCK:
			op->data = cs_dsp_chunk_read(&chunk, 16);
			break;
		case CS_DSP_WSEQ_ADDR8:
			op->address = cs_dsp_chunk_read(&chunk, 8);
			op->data = cs_dsp_chunk_read(&chunk, 32);
			break;
		case CS_DSP_WSEQ_H16:
		case CS_DSP_WSEQ_L16:
			op->address = cs_dsp_chunk_read(&chunk, 24);
			op->data = cs_dsp_chunk_read(&chunk, 16);
			break;
		case CS_DSP_WSEQ_FULL:
			op->address = cs_dsp_chunk_read(&chunk, 32);
			op->data = cs_dsp_chunk_read(&chunk, 32);
			break;
		default:
			ret = -EINVAL;
			cs_dsp_err(dsp, "Unsupported op: %X\n", op->operation);
			devm_kfree(dsp->dev, op);
			goto err_free;
		}

		list_add_tail(&op->list, &wseq->ops);

		if (op->operation == CS_DSP_WSEQ_END)
			break;
	}

	if (op && op->operation != CS_DSP_WSEQ_END) {
		cs_dsp_err(dsp, "%s missing end terminator\n", wseq->ctl->subname);
		ret = -ENOENT;
	}

err_free:
	kfree(words);

	return ret;
}

/**
 * cs_dsp_wseq_init() - Initialize write sequences contained within the loaded DSP firmware
 * @dsp: Pointer to DSP structure
 * @wseqs: List of write sequences to initialize
 * @num_wseqs: Number of write sequences to initialize
 *
 * Return: Zero for success, a negative number on error.
 */
int cs_dsp_wseq_init(struct cs_dsp *dsp, struct cs_dsp_wseq *wseqs, unsigned int num_wseqs)
{
	int i, ret;

	lockdep_assert_held(&dsp->pwr_lock);

	for (i = 0; i < num_wseqs; i++) {
		ret = cs_dsp_populate_wseq(dsp, &wseqs[i]);
		if (ret) {
			cs_dsp_wseq_clear(dsp, &wseqs[i]);
			return ret;
		}
	}

	return 0;
}
EXPORT_SYMBOL_NS_GPL(cs_dsp_wseq_init, FW_CS_DSP);

static struct cs_dsp_wseq_op *cs_dsp_wseq_find_op(u32 addr, u8 op_code,
						  struct list_head *wseq_ops)
{
	struct cs_dsp_wseq_op *op;

	list_for_each_entry(op, wseq_ops, list) {
		if (op->operation == op_code && op->address == addr)
			return op;
	}

	return NULL;
}

/**
 * cs_dsp_wseq_write() - Add or update an entry in a write sequence
 * @dsp: Pointer to a DSP structure
 * @wseq: Write sequence to write to
 * @addr: Address of the register to be written to
 * @data: Data to be written
 * @op_code: The type of operation of the new entry
 * @update: If true, searches for the first entry in the write sequence with
 * the same address and op_code, and replaces it. If false, creates a new entry
 * at the tail
 *
 * This function formats register address and value pairs into the format
 * required for write sequence entries, and either updates or adds the
 * new entry into the write sequence.
 *
 * If update is set to true and no matching entry is found, it will add a new entry.
 *
 * Return: Zero for success, a negative number on error.
 */
int cs_dsp_wseq_write(struct cs_dsp *dsp, struct cs_dsp_wseq *wseq,
		      u32 addr, u32 data, u8 op_code, bool update)
{
	struct cs_dsp_wseq_op *op_end, *op_new = NULL;
	u32 words[WSEQ_OP_MAX_WORDS];
	struct cs_dsp_chunk chunk;
	int new_op_size, ret;

	if (update)
		op_new = cs_dsp_wseq_find_op(addr, op_code, &wseq->ops);

	/* If entry to update is not found, treat it as a new operation */
	if (!op_new) {
		op_end = cs_dsp_wseq_find_op(0, CS_DSP_WSEQ_END, &wseq->ops);
		if (!op_end) {
			cs_dsp_err(dsp, "Missing terminator for %s\n", wseq->ctl->subname);
			return -EINVAL;
		}

		op_new = devm_kzalloc(dsp->dev, sizeof(*op_new), GFP_KERNEL);
		if (!op_new)
			return -ENOMEM;

		op_new->operation = op_code;
		op_new->address = addr;
		op_new->offset = op_end->offset;
		update = false;
	}

	op_new->data = data;

	chunk = cs_dsp_chunk(words, sizeof(words));
	cs_dsp_chunk_write(&chunk, 8, op_new->operation);

	switch (op_code) {
	case CS_DSP_WSEQ_FULL:
		cs_dsp_chunk_write(&chunk, 32, op_new->address);
		cs_dsp_chunk_write(&chunk, 32, op_new->data);
		break;
	case CS_DSP_WSEQ_L16:
	case CS_DSP_WSEQ_H16:
		cs_dsp_chunk_write(&chunk, 24, op_new->address);
		cs_dsp_chunk_write(&chunk, 16, op_new->data);
		break;
	default:
		ret = -EINVAL;
		cs_dsp_err(dsp, "Operation %X not supported\n", op_code);
		goto op_new_free;
	}

	new_op_size = cs_dsp_chunk_bytes(&chunk);

	if (!update) {
		if (wseq->ctl->len - op_end->offset < new_op_size) {
			cs_dsp_err(dsp, "Not enough memory in %s for entry\n", wseq->ctl->subname);
			ret = -E2BIG;
			goto op_new_free;
		}

		op_end->offset += new_op_size;

		ret = cs_dsp_coeff_write_ctrl(wseq->ctl, op_end->offset / sizeof(u32),
					      &op_end->data, sizeof(u32));
		if (ret)
			goto op_new_free;

		list_add_tail(&op_new->list, &op_end->list);
	}

	ret = cs_dsp_coeff_write_ctrl(wseq->ctl, op_new->offset / sizeof(u32),
				      words, new_op_size);
	if (ret)
		goto op_new_free;

	return 0;

op_new_free:
	devm_kfree(dsp->dev, op_new);

	return ret;
}
EXPORT_SYMBOL_NS_GPL(cs_dsp_wseq_write, FW_CS_DSP);

/**
 * cs_dsp_wseq_multi_write() - Add or update multiple entries in a write sequence
 * @dsp: Pointer to a DSP structure
 * @wseq: Write sequence to write to
 * @reg_seq: List of address-data pairs
 * @num_regs: Number of address-data pairs
 * @op_code: The types of operations of the new entries
 * @update: If true, searches for the first entry in the write sequence with
 * the same address and op_code, and replaces it. If false, creates a new entry
 * at the tail
 *
 * This function calls cs_dsp_wseq_write() for multiple address-data pairs.
 *
 * Return: Zero for success, a negative number on error.
 */
int cs_dsp_wseq_multi_write(struct cs_dsp *dsp, struct cs_dsp_wseq *wseq,
			    const struct reg_sequence *reg_seq, int num_regs,
			    u8 op_code, bool update)
{
	int i, ret;

	for (i = 0; i < num_regs; i++) {
		ret = cs_dsp_wseq_write(dsp, wseq, reg_seq[i].reg,
					reg_seq[i].def, op_code, update);
		if (ret)
			return ret;
	}

	return 0;
}
EXPORT_SYMBOL_NS_GPL(cs_dsp_wseq_multi_write, FW_CS_DSP);

MODULE_DESCRIPTION("Cirrus Logic DSP Support");
MODULE_AUTHOR("Simon Trimmer <simont@opensource.cirrus.com>");
MODULE_LICENSE("GPL v2");
+27 −0
Original line number Diff line number Diff line
@@ -42,6 +42,16 @@
#define CS_DSP_ACKED_CTL_MIN_VALUE           0
#define CS_DSP_ACKED_CTL_MAX_VALUE           0xFFFFFF

/*
 * Write sequence operation codes
 */
#define CS_DSP_WSEQ_FULL	0x00
#define CS_DSP_WSEQ_ADDR8	0x02
#define CS_DSP_WSEQ_L16		0x04
#define CS_DSP_WSEQ_H16		0x05
#define CS_DSP_WSEQ_UNLOCK	0xFD
#define CS_DSP_WSEQ_END		0xFF

/**
 * struct cs_dsp_region - Describes a logical memory region in DSP address space
 * @type:	Memory region type
@@ -258,6 +268,23 @@ struct cs_dsp_alg_region *cs_dsp_find_alg_region(struct cs_dsp *dsp,

const char *cs_dsp_mem_region_name(unsigned int type);

/**
 * struct cs_dsp_wseq - Describes a write sequence
 * @ctl:	Write sequence cs_dsp control
 * @ops:	Operations contained within
 */
struct cs_dsp_wseq {
	struct cs_dsp_coeff_ctl *ctl;
	struct list_head ops;
};

int cs_dsp_wseq_init(struct cs_dsp *dsp, struct cs_dsp_wseq *wseqs, unsigned int num_wseqs);
int cs_dsp_wseq_write(struct cs_dsp *dsp, struct cs_dsp_wseq *wseq, u32 addr, u32 data,
		      u8 op_code, bool update);
int cs_dsp_wseq_multi_write(struct cs_dsp *dsp, struct cs_dsp_wseq *wseq,
			    const struct reg_sequence *reg_seq, int num_regs,
			    u8 op_code, bool update);

/**
 * struct cs_dsp_chunk - Describes a buffer holding data formatted for the DSP
 * @data:	Pointer to underlying buffer memory