Data mover R5 to A53#

Idea#

The goal is to move sampled data from the R5 processor via the A53 to the host PC running a GUI, which displays and logs the data. Important requirements are

  • no data is lost, all samples are transfered and logged

  • on the R5 side, it has to run synchronous to the control ISR, since all the data is updated in each call of the ISR, i.e., in each time step.

  • the sampling frequency can be up 100kHz

  • minimal effort for R5 to avoid stealing computation time from the control ISR

At the same time, a data path in the opposite direction GUI -> A53 -> R5 is needed to enable and disable the system and set references. This path is not time critical and is described in detail at the end of this section.

Please see also the matching description of the APU code.

graph LR subgraph ISR_Control A(JavaScope_update) end subgraph js_fetchData A-->B(Copy Data to Shared Memory) B-->C(Flush Cache) C-->D(IPI to A53) end

Fig. 59 Signal flow of data mover#

Shared header file#

To this end, the shared header file APU_RPU_shared.h located at vitis/software/shared is included in both software projects, i.e., R5 and A53. The shared memory can be in OCM or DDR, here we use the OCM (on-chip memory) of the A53. The start address of the OCM is hard-coded into the software since it is specific to the UltraScale+ memory map.

//APU_RPU_shared.h
#pragma once
// OCM Bank Adresses
// See UG1085 v2.4 table 18-1 OCM Mapping Summary (https://docs.amd.com/r/en-US/ug1085-zynq-ultrascale-trm)
#define MEM_SHARED_START_OCM_BANK_1_RPU_TO_APU 	0xFFFD0000 // bank 1 is for r5->a53 user data
#define MEM_SHARED_START_OCM_BANK_2_APU_TO_RPU 	0xFFFE0000 // bank 2 is for a53->r5 user data
#define MEM_SHARED_START_OCM_BANK_3_JAVASCOPE 	0xFFFF0000 // bank 3 is for r5->a53 javascope
#define JS_CHANNELS 		20
// update by hand when changing JS_CHANNELS
// Bank 3 of OCM has 64 KB, thus a maximum of 16K float values can be stored
#define JAVASCOPE_DATA_SIZE_2POW  	128

// Experimental feature - read docs before use
#define USE_A53_AS_ACCELERATOR_FOR_R5_ISR		FALSE

struct javascope_data_t
{
	uint32_t    status;
	float	    slowDataContent;
	uint32_t    slowDataID;
	float       scope_ch[JS_CHANNELS];
};

struct APU_to_RPU_t
{
	uint32_t id;
	float value;
};

struct APU_to_RPU_user_data_t
{
	// create variables that you want to share from A53 to R5
	uint32_t slowDataCounter;
};

struct RPU_to_APU_user_data_t
{
	// create variables that you want to share from R5 to A53
	uint32_t slowDataCounter;
};

It defines the following:

  • struct javascope_data_t which will be passed from R5 to A53,

  • the number float channels JS_CHANNELS inside this struct, and

  • the start address of the shared memory MEM_SHARED_START used to pass data from R5 to A53.

Write to shared memory and trigger APU interrupt#

Inside javascope.c a pointer to type struct javascope_data_t named javascope_data is defined and initialized to point to the shared memory starting address MEM_SHARED_START.

In function js_fetchData(),

  • the selected data in js_ch_selected is written to javascope_data

  • Also slowDataID, slowDataContent, js_status_BareToRTOS are copied to to javascope_data

  • The shared memory is cached and the above mentioned changes are not visible to the APU yet. Therefore, we have to flush the cache for the size of javascope_data. This triggers the memory controller to write the updated javascope_data into the actual OCM.

  • Afterwards, with XIpiPsu_TriggerIpi, the IPI (inter processor interrupt), is used to trigger the execution of Transfer_ipc_Intr_Handler on the APU, as described in the section on the APU data mover

#include "xil_cache.h"
#include "APU_RPU_shared.h"
// create pointer of type struct javascope_data_t named javascope_data located at MEM_SHARED_START
static struct javascope_data_t volatile * const javascope_data = (struct javascope_data_t*)MEM_SHARED_START;

void js_fetchData(){
   // write data to shared memory
   for(int j=0; j<JS_CHANNELS; j++){
      javascope_data->scope_ch[j] = *js_ch_selected[j];
   }
   javascope_data->slowDataID      = js_cnt_slowData;
   javascope_data->slowDataContent = js_slowDataArray[js_cnt_slowData].u;
   javascope_data->status          = js_status_BareToRTOS;

   // flush data cache of shared memory region to make sure shared memory is updated
   Xil_DCacheFlushRange(MEM_SHARED_START, JAVASCOPE_DATA_SIZE_2POW);

  //Trigger IPI interrupt to APU
  status = XIpiPsu_TriggerIpi(&INTCInst_IPI,XPAR_XIPIPS_TARGET_PSU_CORTEXA53_0_CH0_MASK);
}

Selection of transmitted channels#

In javascope.h an enumeration variable enum JS_OberservableData is defined that is used to identify the observable data with a unique name.

enum JS_OberservableData {
   JSO_ZEROVALUE=0,
   JSO_ia,
   JSO_ib,
   JSO_Speed_rpm,
   //...//
   JSO_ENDMARKER
};

In javascope.c in function JavaScope_initalize(DS_Data* data), the array float * js_ch_observable is initialized and holds the pointers to all observable data.

float *js_ch_observable[JSO_ENDMARKER];

int JavaScope_initalize(DS_Data* data)
{
   js_ch_observable[JSO_Speed_rpm]  = &data->av.mechanicalRotorSpeed;
   js_ch_observable[JSO_ia]         = &data->av.I_U;
   js_ch_observable[JSO_ib]         = &data->av.I_V;
   // ... //
}

In ipc_ARM.c, the selected channels are written to js_ch_selected. The selection is decided in the JavaScope application.

extern float *js_ch_observable[JSO_ENDMARKER];
extern float *js_ch_selected[JS_CHANNELS];

void ipc_Control_func(uint16_t msgId, uint16_t value, DS_Data* data)
{
   if (msgId == 1) {}
   else if (msgId == 204) // SELECT_DATA_CH1_bits{
      if ( value >= 0 && value < JSO_ENDMARKER ) {
         js_ch_selected[0] = js_ch_observable[value];
      }
   }
   else if (msgId == 205) // SELECT_DATA_CH2_bits{
      if ( value >= 0 && value < JSO_ENDMARKER ){
         js_ch_selected[1] = js_ch_observable[value];
      }
   }
   // ... same for all other channels  //
}

Where value relates to an entry in enum JS_OberservableData which is also known to the JavaScope application.

Known issues#

See also#