RE: [PATCH v8 5/5] remoteproc: Add initial zynqmp R5 remoteproc driver

From: Ben Levinsky
Date: Thu Aug 20 2020 - 11:13:32 EST



> -----Original Message-----
> From: Stefano Stabellini <stefano.stabellini@xxxxxxxxxx>
> Sent: Wednesday, August 19, 2020 2:21 PM
> To: Ben Levinsky <BLEVINSK@xxxxxxxxxx>
> Cc: Stefano Stabellini <stefanos@xxxxxxxxxx>; Michal Simek
> <michals@xxxxxxxxxx>; devicetree@xxxxxxxxxxxxxxx;
> mathieu.poirier@xxxxxxxxxx; Ed T. Mooring <emooring@xxxxxxxxxx>; linux-
> remoteproc@xxxxxxxxxxxxxxx; linux-kernel@xxxxxxxxxxxxxxx;
> robh+dt@xxxxxxxxxx; Jiaying Liang <jliang@xxxxxxxxxx>; linux-arm-
> kernel@xxxxxxxxxxxxxxxxxxx
> Subject: RE: [PATCH v8 5/5] remoteproc: Add initial zynqmp R5 remoteproc
> driver
>
> On Tue, 18 Aug 2020, Ben Levinsky wrote:
> > > > +/**
> > > > + * struct zynqmp_r5_mem - zynqmp rpu memory data
> > > > + * @pnode_id: TCM power domain ids
> > > > + * @res: memory resource
> > > > + * @node: list node
> > > > + */
> > > > +struct zynqmp_r5_mem {
> > > > + u32 pnode_id[MAX_MEM_PNODES];
> > > > + struct resource res;
> > > > + struct list_head node;
> > > > +};
> > > > +
> > > > +/**
> > > > + * struct zynqmp_r5_pdata - zynqmp rpu remote processor private data
> > > > + * @dev: device of RPU instance
> > > > + * @rproc: rproc handle
> > > > + * @pnode_id: RPU CPU power domain id
> > > > + * @mems: memory resources
> > > > + * @is_r5_mode_set: indicate if r5 operation mode is set
> > > > + * @tx_mc: tx mailbox client
> > > > + * @rx_mc: rx mailbox client
> > > > + * @tx_chan: tx mailbox channel
> > > > + * @rx_chan: rx mailbox channel
> > > > + * @mbox_work: mbox_work for the RPU remoteproc
> > > > + * @tx_mc_skbs: socket buffers for tx mailbox client
> > > > + * @rx_mc_buf: rx mailbox client buffer to save the rx message
> > > > + */
> > > > +struct zynqmp_r5_pdata {
> > > > + struct device dev;
> > > > + struct rproc *rproc;
> > > > + u32 pnode_id;
> > > > + struct list_head mems;
> > > > + bool is_r5_mode_set;
> > > > + struct mbox_client tx_mc;
> > > > + struct mbox_client rx_mc;
> > > > + struct mbox_chan *tx_chan;
> > > > + struct mbox_chan *rx_chan;
> > > > + struct work_struct mbox_work;
> > > > + struct sk_buff_head tx_mc_skbs;
> > > > + unsigned char rx_mc_buf[RX_MBOX_CLIENT_BUF_MAX];
> > >
> > > A simple small reordering of the struct fields would lead to small
> > > memory savings due to alignment.
> > >
> > >
> > [Ben Levinsky] will do. Do you mean ordering in either ascending or
> descending order?
>
> Each field has a different alignment in the struct, so for example after
> pnode_id there are probably 4 unused bytes because mems is 64bit
> aligned.
>
>
[Ben Levinsky] ok will update this so the alignments are done with less unused memory per struct allocation.
> > > > +/*
> > > > + * TCM needs mapping to R5 relative address and xilinx platform mgmt
> call
> > > > + */
> > > > +struct rproc_mem_entry *handle_tcm_parsing(struct device *dev,
> > > > + struct reserved_mem *rmem,
> > > > + struct device_node *node,
> > > > + int lookup_idx)
> > > > +{
> > > > + void *va;
> > > > + dma_addr_t dma;
> > > > + resource_size_t size;
> > > > + int ret;
> > > > + u32 pnode_id;
> > > > + struct resource rsc;
> > > > + struct rproc_mem_entry *mem;
> > > > +
> > > > + pnode_id = tcm_addr_to_pnode[lookup_idx][1];
> > > > + ret = zynqmp_pm_request_node(pnode_id,
> > > > + ZYNQMP_PM_CAPABILITY_ACCESS, 0,
> > > > + ZYNQMP_PM_REQUEST_ACK_BLOCKING);
> > > > + if (ret < 0) {
> > > > + dev_err(dev, "failed to request power node: %u\n",
> > > pnode_id);
> > > > + return -EINVAL;
> > > > + }
> > > > +
> > > > + ret = of_address_to_resource(node, 0, &rsc);
> > > > + if (ret < 0) {
> > > > + dev_err(dev, "failed to get resource of memory %s",
> > > > + of_node_full_name(node));
> > > > + return -EINVAL;
> > > > + }
> > > > + size = resource_size(&rsc);
> > > > + va = devm_ioremap_wc(dev, rsc.start, size);
> > > > + if (!va)
> > > > + return -ENOMEM;
> > > > +
> > > > + /* zero out tcm base address */
> > > > + if (rsc.start & 0xffe00000) {
> > > > + /* R5 can't see anything past 0xfffff so wipe it */
> > > > + rsc.start &= 0x000fffff;
> > >
> > > If that is the case why not do:
> > >
> > > rsc.start &= 0x000fffff;
> > >
> > > unconditionally? if (rsc.start & 0xffe00000) is superfluous.
> > >
> > > But I thought that actually the R5s could see TCM at both the low
> > > address (< 0x000fffff) and also at the high address (i.e. 0xffe00000).
> > >
> > >
> > [Ben Levinsky] Here yes can make rsc.start &= 0x000fffff undconditional. Will
> update in v9 as such
> > Also, this logic is because this is only for the Xilinx R5
> mappings of TCM banks that are at (from the TCM point of view) 0x0 and
> 0x2000
>
> What I meant is that as far as I understand from the TRM the RPU should
> also be able to access the same banks at the same address of the APU,
> which I imagine is in the 0xffe00000 range. But I could be wrong on
> this.
>
> If we could use the same addresses for RPU and APU, it would simplify
> this driver.
>
>
> > > > + /*
> > > > + * handle tcm banks 1 a and b (0xffe9000 and
> > > > + * 0xffeb0000)
> > > > + */
> > > > + if (rsc.start & 0x80000)
> > > > + rsc.start -= 0x90000;
> > >
> > > It is very unclear to me why we have to do this
> > >
> > >
> > [Ben Levinsky] This is for TCM bank 0B and bank 1B to map them to
> 0x00020000 so that the TCM relative addressing lines up. For example
> (0xffe90000 & 0x000fffff) - 0x90000 = 0x20000
>
> Could you please explain the mapping in an in-code comment?
> The comment currently mentions 0xffe9000 and 0xffeb0000 but:
>
> - 0xffe9000 & 0x000fffff = 0xe9000
> 0xe9000 - 0x90000 = 0x59000
>
> - 0xffeb0000 & 0x000fffff = 0xeb000
> 0xeb000 - 0x90000 = 0xeb000
>
> Either way we don't get 0x20000. What am I missing?
>
[Ben Levinsky] I apologize there is typo in the comment... it should be 0xffe90000 and 0xffeb0000
The output is:
0xffe90000 & 0x000fffff = 0x90000
0x90000 - 0x90000 = 0x0

And
0xffeb0000 & 0x000fffff = 0xB0000
0xB0000 - 0x90000 = 0x20000

So these line up for the relative addressing for RPU's view of TCMs
>
>
> > > > + }
> > > > +
> > > > + dma = (dma_addr_t)rsc.start;
> > >
> > > Given the way the dma parameter is used by
> > > rproc_alloc_registered_carveouts, I think it might be best to pass the
> > > original start address (i.e. 0xffe00000) as dma.
> > >
> > >
> > > > + mem = rproc_mem_entry_init(dev, va, dma, (int)size, rsc.start,
> > > > + NULL, zynqmp_r5_mem_release,
> > >
> > > I don't know too much about the remoteproc APIs, but shouldn't you be
> > > passing zynqmp_r5_rproc_mem_alloc to it instead of NULL?
> > >
> > >
> > [Ben Levinsky] the difference is that for TCM we have to do make the
> relative address work for TCM, so the dma input to rproc_mem_entry_init is
> different in TCM case.
>
> The dma address is the address as seen by the RPU, is that right?
> So you are trying to set the dma address to something in the range 0 -
> 0x20000?
>
>
[Ben Levinsky] yes
> > > > + rsc.name);
> > > > + if (!mem)
> > > > + return -ENOMEM;
> > > > +
> > > > + return mem;
> > > > +}
> > > > +
> > > > +static int parse_mem_regions(struct rproc *rproc)
> > > > +{
> > > > + int num_mems, i;
> > > > + struct zynqmp_r5_pdata *pdata = rproc->priv;
> > > > + struct device *dev = &pdata->dev;
> > > > + struct device_node *np = dev->of_node;
> > > > + struct rproc_mem_entry *mem;
> > > > +
> > > > + num_mems = of_count_phandle_with_args(np, "memory-region",
> > > NULL);
> > > > + if (num_mems <= 0)
> > > > + return 0;
> > > > + for (i = 0; i < num_mems; i++) {
> > > > + struct device_node *node;
> > > > + struct reserved_mem *rmem;
> > > > +
> > > > + node = of_parse_phandle(np, "memory-region", i);
> > >
> > > Check node != NULL ?
> > >
> > [Ben Levinsky] will add this in v9
> > >
> > > > + rmem = of_reserved_mem_lookup(node);
> > > > + if (!rmem) {
> > > > + dev_err(dev, "unable to acquire memory-region\n");
> > > > + return -EINVAL;
> > > > + }
> > > > +
> > > > + if (strstr(node->name, "vdev0buffer")) {
> > >
> > > vdev0buffer is not described in the device tree bindings, is that
> > > normal/expected?
> > >
> > >
> > [Ben Levinsky] vdev0buffer is not required, as there might be simple
> firmware loading with no IPC. Vdev0buffer only needed for IPC.
>
> OK, good. It should probably still be described in the device tree
> binding as optional property.
>
>
> > > > + /* Register DMA region */
> > > > + mem = rproc_mem_entry_init(dev, NULL,
> > > > + (dma_addr_t)rmem->base,
> > > > + rmem->size, rmem->base,
> > > > + NULL, NULL,
> > > > + "vdev0buffer");
> > > > + if (!mem) {
> > > > + dev_err(dev, "unable to initialize memory-
> > > region %s\n",
> > > > + node->name);
> > > > + return -ENOMEM;
> > > > + }
> > > > + dev_dbg(dev, "parsed %s at %llx\r\n", mem->name,
> > > > + mem->dma);
> > > > + } else if (strstr(node->name, "vdev0vring")) {
> > >
> > > Same here
> > >
> > >
> > > > + int vring_id;
> > > > + char name[16];
> > > > +
> > > > + /*
> > > > + * can be 1 of multiple vring IDs per IPC channel
> > > > + * e.g. 'vdev0vring0' and 'vdev0vring1'
> > > > + */
> > > > + vring_id = node->name[14] - '0';
> > >
> > > Where does the "14" comes from? Are we sure it is not possible to have a
> > > node->name smaller than 14 chars?
> > >
> > [Ben Levinsky] Presently there are only 2 vrings used per Xilinx OpenAMP
> channel to RPU. In Xilinx kernel we have hard-coded names as these are the
> only nodes that use it. For example RPU0vdev0vring0 and RPU1vdev0vring0.
> Hence we only check for vdev0vring and not a sscanf("%*s%i") to parse out
> the vring ID or other, cleaner solution.
>
> OK, but I think it is best if we use node->name[14] only if we
> explicitly check for a string of at least 14 characters. Using strstr,
> it could return the string "vdev0vring" which is less than 14 chars,
> leading to a bug.
>
>
> > >
> > > > + snprintf(name, sizeof(name), "vdev0vring%d",
> > > vring_id);
> > > > + /* Register vring */
> > > > + mem = rproc_mem_entry_init(dev, NULL,
> > > > + (dma_addr_t)rmem->base,
> > > > + rmem->size, rmem->base,
> > > > +
> > > zynqmp_r5_rproc_mem_alloc,
> > > > +
> > > zynqmp_r5_rproc_mem_release,
> > > > + name);
> > > > + dev_dbg(dev, "parsed %s at %llx\r\n", mem->name,
> > > > + mem->dma);
> > > > + } else {
> > > > + int idx;
> > > > +
> > > > + /*
> > > > + * if TCM update address space for R5 and
> > > > + * make xilinx platform mgmt call
> > > > + */
> > > > + for (idx = 0; idx < ZYNQMP_R5_NUM_TCM_BANKS;
> > > idx++) {
> > > > + if (tcm_addr_to_pnode[idx][0] == rmem-
> > > >base)
> > > > + break;
> > >
> > > There is something I don't quite understand. If the memory region to use
> > > is TCM, why would it be also described under reserved-memory?
> > > Reserved-memory is for normal memory being reserved, while TCM is not
> > > normal memory. Am I missing something?
> > >
> > [Ben Levinsky] I can change this in v9 as discussed. That is, have no TCM
> under reserved mem. Instead have the banks as nodes in device tree with
> status="[enabled|disabled]" and if enabled, then try to add memories in
> parse_fw call.
> > >
> > > > + }
> > > > +
> > > > + if (idx != ZYNQMP_R5_NUM_TCM_BANKS) {
> > > > + mem = handle_tcm_parsing(dev, rmem, node,
> > > idx);
> > > > + } else {
> > > > + mem = rproc_mem_entry_init(dev, NULL,
> > > > + (dma_addr_t)rmem->base,
> > > > + rmem->size, rmem->base,
> > > > +
> > > zynqmp_r5_rproc_mem_alloc,
> > > > +
> > > zynqmp_r5_rproc_mem_release,
> > > > + node->name);
> > >
> > > This case looks identical to the vdev0vring above. Is the difference
> > > really just in the "name"? If so, can we merge the two cases into one?
> > > no, because the devm_ioremap_wc call has to be done before changing
> the dma address to relative for TCM banks.
> > >
> > > > + }
> > > > +
> > > > + if (!mem) {
> > > > + dev_err(dev,
> > > > + "unable to init memory-region %s\n",
> > > > + node->name);
> > > > + return -ENOMEM;
> > > > + }
> > > > + }
> > > > + rproc_add_carveout(rproc, mem);
> > > > + }
> > > > +
> > > > + return 0;
> > > > +}
> > > > +
> > > > +static int zynqmp_r5_parse_fw(struct rproc *rproc, const struct
> firmware
> > > *fw)
> > > > +{
> > > > + int ret;
> > > > + struct zynqmp_r5_pdata *pdata = rproc->priv;
> > > > + struct device *dev = &pdata->dev;
> > > > +
> > > > + ret = parse_mem_regions(rproc);
> > > > + if (ret) {
> > > > + dev_err(dev, "parse_mem_regions failed %x\n", ret);
> > > > + return ret;
> > > > + }
> > > > +
> > > > + ret = rproc_elf_load_rsc_table(rproc, fw);
> > > > + if (ret == -EINVAL) {
> > > > + dev_info(dev, "no resource table found.\n");
> > > > + ret = 0;
> > >
> > > Why do we want to continue ignoring the error in this case?
> > >
> > [Ben Levinsky] as there can be simple firmware loaded onto R5 that do not
> have resource table. Resource table only needed for specific IPC case.
>
> OK, an in-code comment would be good
>
>
> > > > + struct device *dev;
> > > > + struct zynqmp_r5_mem *mem;
> > > > + int ret;
> > > > + struct property *prop;
> > > > + const __be32 *cur;
> > > > + u32 val;
> > > > + int i;
> > > > +
> > > > + dev = &pdata->dev;
> > > > + mem = devm_kzalloc(dev, sizeof(*mem), GFP_KERNEL);
> > > > + if (!mem)
> > > > + return -ENOMEM;
> > > > + ret = of_address_to_resource(node, 0, &mem->res);
> > > > + if (ret < 0) {
> > > > + dev_err(dev, "failed to get resource of memory %s",
> > > > + of_node_full_name(node));
> > > > + return -EINVAL;
> > > > + }
> > > > +
> > > > + /* Get the power domain id */
> > > > + i = 0;
> > > > + if (of_find_property(node, "pnode-id", NULL)) {
> > > > + of_property_for_each_u32(node, "pnode-id", prop, cur, val)
> > > > + mem->pnode_id[i++] = val;
> > > > + }
> > > > + list_add_tail(&mem->node, &pdata->mems);
> > > > + return 0;
> > > > +}
> > > > +
> > > > +/**
> > > > + * zynqmp_r5_release() - ZynqMP R5 device release function
> > > > + * @dev: pointer to the device struct of ZynqMP R5
> > > > + *
> > > > + * Function to release ZynqMP R5 device.
> > > > + */
> > > > +static void zynqmp_r5_release(struct device *dev)
> > > > +{
> > > > + struct zynqmp_r5_pdata *pdata;
> > > > + struct rproc *rproc;
> > > > + struct sk_buff *skb;
> > > > +
> > > > + pdata = dev_get_drvdata(dev);
> > > > + rproc = pdata->rproc;
> > > > + if (rproc) {
> > > > + rproc_del(rproc);
> > > > + rproc_free(rproc);
> > > > + }
> > > > + if (pdata->tx_chan)
> > > > + mbox_free_channel(pdata->tx_chan);
> > > > + if (pdata->rx_chan)
> > > > + mbox_free_channel(pdata->rx_chan);
> > > > + /* Discard all SKBs */
> > > > + while (!skb_queue_empty(&pdata->tx_mc_skbs)) {
> > > > + skb = skb_dequeue(&pdata->tx_mc_skbs);
> > > > + kfree_skb(skb);
> > > > + }
> > > > +
> > > > + put_device(dev->parent);
> > > > +}
> > > > +
> > > > +/**
> > > > + * event_notified_idr_cb() - event notified idr callback
> > > > + * @id: idr id
> > > > + * @ptr: pointer to idr private data
> > > > + * @data: data passed to idr_for_each callback
> > > > + *
> > > > + * Pass notification to remoteproc virtio
> > > > + *
> > > > + * Return: 0. having return is to satisfy the idr_for_each() function
> > > > + * pointer input argument requirement.
> > > > + **/
> > > > +static int event_notified_idr_cb(int id, void *ptr, void *data)
> > > > +{
> > > > + struct rproc *rproc = data;
> > > > +
> > > > + (void)rproc_vq_interrupt(rproc, id);
> > > > + return 0;
> > > > +}
> > > > +
> > > > +/**
> > > > + * handle_event_notified() - remoteproc notification work funciton
> > > > + * @work: pointer to the work structure
> > > > + *
> > > > + * It checks each registered remoteproc notify IDs.
> > > > + */
> > > > +static void handle_event_notified(struct work_struct *work)
> > > > +{
> > > > + struct rproc *rproc;
> > > > + struct zynqmp_r5_pdata *pdata;
> > > > +
> > > > + pdata = container_of(work, struct zynqmp_r5_pdata, mbox_work);
> > > > +
> > > > + (void)mbox_send_message(pdata->rx_chan, NULL);
> > > > + rproc = pdata->rproc;
> > > > + /*
> > > > + * We only use IPI for interrupt. The firmware side may or may
> > > > + * not write the notifyid when it trigger IPI.
> > > > + * And thus, we scan through all the registered notifyids.
> > > > + */
> > > > + idr_for_each(&rproc->notifyids, event_notified_idr_cb, rproc);
> > >
> > > This looks expensive. Should we at least check whether the notifyid was
> > > written as first thing before doing the scan?
> > >
> > [Ben Levinsky] this will be at most 2 vrings presently per firmware-load and
> only done when the firmware is loaded so the latency so should not impact
> performace or user
>
> OK
>
>
> > > > + /* Get R5 power domain node */
> > > > + ret = of_property_read_u32(node, "pnode-id", &pdata->pnode_id);
> > > > + if (ret) {
> > > > + dev_err(dev, "failed to get power node id.\n");
> > > > + goto error;
> > > > + }
> > > > +
> > > > + /* TODO Check if R5 is running */
> > > > +
> > > > + /* Set up R5 if not already setup */
> > > > + ret = pdata->is_r5_mode_set ? 0 : r5_set_mode(pdata);
> > > > + if (ret) {
> > > > + dev_err(dev, "failed to set R5 operation mode.\n");
> > > > + return ret;
> > > > + }
> > >
> > > is_r5_mode_set is set by r5_set_mode(), which is only called here.
> > > So it looks like this check is important in cases where
> > > zynqmp_r5_probe() is called twice for the same R5 node. But I don't
> > > think that is supposed to happen?
> > >
> > [Ben Levinsky] this is needed as there are cases where user can repeatedly
> load different firmware so the check is needed in cases like this where rpu is
> already configured. It is also possible that a user might repeatedly
> load/unload the module
>
> OK