Skip to content

Latest commit

 

History

History
283 lines (190 loc) · 7.95 KB

File metadata and controls

283 lines (190 loc) · 7.95 KB

RUNTIME MODE EXAMPLE

In this example, the graph can be changed at runtime without recompiling.

Runtime mode has lot of security consequences. The data representing the graph to create and run should have to be validated before being used. This validation can be complex and it may not even be possible to fully validate everything.

Validation is not implemented in this example.

For those reasons, the runtime feature is provided as an example and not integrated into CMSIS-Stream. This example should not be used as it is in a product

How to build

python create.py
make -f Makefile.windows

(makefile for other platforms are provided)

runtime_mode.exe

Current test is running one iteration of the scheduling.

The content of the scheduling is defined in sched_flat.dat loaded at runtime. This .dat file is generated by the Python script.

A copy of flatbuffer library is included in this example to make it easy to build it on a host computer. To build for FVP using CMSIS build tools, you should add a dependency to the flatbuffer pack instead of using the copy from this example.

Graph implemented in this example

runtime_mode

Expected result

Sink
10
26
42
58
74
90
106
122
Sink
10
26
42
58
74
90
106
122
After schedule hook
Nb iterations = 1
Normal end of the scheduler

A source node is generating the sequence 0,1,2,3,4,5,6,7,0,1,2 ...

We have 16 sources that are added, so the input of the processing node should be:

0, 16, 32, 48, 64, 80, 96, 112 ...

The processing node is adding the offset 10 (as defined in the Python script). The output of processing (and thus the display of the sink) should be:

10, 26, 42, 58, 74, 90, 106, 122 ...

Limitations:

  • No way to select different FIFO implementations for different branches from the Python script
  • No direct support of pure C function. They have to be packaged into a C++ wrapper (original build mode can call directly a pure function with no state)

Differences

Build mode node identification is using a #define generated by the Python.

The runtime mode is using a std::string : the name of the node defined in the Python.

(Only when identification mode is on and a specific node must be identified. Otherwise names are not used)

How to use

C++ part

The AppNodes.h file must include the generic runtime nodes instead of the standard generic nodes. The flat buffer generated API must also be included.

#include "GenericRuntimeNodes.h"
#include "stream_generated.h"

Each node class must contain a static constant uuid for identification and a methodmkNode used to create an instance of this class.

The class must also implements the NodeBase interface.

Here is an example with a source node.

Source node example

class Source: public GenericRuntimeSource<float>
{
public:
    Source(const Node &n,
           RuntimeEdge &dst)
        :GenericRuntimeSource<float>(n,dst){};

    constexpr static std::array<uint8_t,16> uuid    = {0xc0,0x08,0x9f,0x59,0x2f,0x33,0x4e,0xc4,0x90,0x23,0x30,0xf6,0x9f,0x0f,0x48,0x33};


    static NodeBase* mkNode(const RuntimeContext &ctx, 
                            const Node *ndesc)
    {
        auto outputs = ndesc->outputs();
        RuntimeEdge &i = *ctx.fifos[outputs->Get(0)->id()];

        Source *node=new Source(*ndesc,i);
        return(static_cast<NodeBase*>(node));
    }

The class is inheriting from GenericRuntimeSource rather than GenericSource for the build time mode.

The RuntimeContext argument of mkNode is containing the FIFOs that have been created.

The ndesc is the flat buffer description of the node that is containing the FIFO ids for inputs and outputs.

We get the flat buffer description for the outputs of this node:

auto outputs = ndesc->outputs();

We extract the corresponding FIFO reference from the runtime context using the flat buffer ID. Here we get the FIFO for the output port 0 of the node:

RuntimeEdge &i = *ctx.fifos[outputs->Get(0)->id()];

Then, we call the node constructor:

Source *node=new Source(*ndesc,i);
return(static_cast<NodeBase*>(node));

The node must also implement the virtual function from the root class NodeBase:

int prepareForRunning() final
int run() final

It is a similar implementation to what must be done with the build time mode of CMSIS-Stream.

The flatbuffer description of the node is an argument of the constructor because the implementation of the wrapper needs to know how many samples to write to the output port.

mkNode can also access node specific initialization data provided in the flatbuffer as an untyped buffer (see implementation of Processing node as an example).

Registering the nodes

Once the nodes have been defined, they need to be registered so that the graph interpreter knows where to find the code. The code must already be available in the firmware (the code is not updated or downloaded at runtime):

#include "runtime_sched.h"
using namespace arm_cmsis_stream;

static Registry register_nodes()
{
    Registry res;

    Component<Source>::reg(res);
    Component<Sink>::reg(res);
    Component<ProcessingNode>::reg(res);
    Component<AdderNode>::reg(res);
    Component<RuntimeDuplicate>::reg(res);
   
    return(res);
};

The RuntimeDuplicate must always be registered. It is defined in GenericRuntimeNodes.

Reading the graph

In current example, we are reading the graph from a file. The graph API just needs a buffer in memory:

Byte buffer created from file content:

std::ifstream input( "sched_flat.dat", std::ios::binary );
std::vector<unsigned char> buffer(std::istreambuf_iterator<char>(input), {});

Graph created from the buffer:

auto maybe_ctx = create_graph(buffer.data(),buffer.size(), registered_nodes);

It is using the list of registered nodes and an optional runtime context is returned.

maybe_ctx is a std::optional. In case of error, it does not contain anything. Before using the graph. you need to extract the context from this value:

if (maybe_ctx.has_value())
{
        const RuntimeContext &ctx = maybe_ctx.value();
    ...
}

Running the graph

Before running this graph, some scheduler hooks can be defined to customize the execution:

SchedulerHooks hooks;
        
hooks.before_schedule=nullptr;
hooks.before_iteration=nullptr;
        
// Only used in async mode
hooks.async_before_node_check=nullptr;
hooks.async_after_node_check=nullptr;
hooks.async_node_not_executed=nullptr;
        
hooks.before_node_execution=nullptr;
hooks.after_node_execution=nullptr;
        
hooks.after_iteration=nullptr;
hooks.after_schedule=nullptr;

The graph can be run with:

uint32_t nbIterations = run_graph(hooks,ctx,&error);

Python side

A node needs only two new additional properties.

UUID

It is the UUID used also in the C code. It replaces the typeName property that is used with the compilation version of the library.

@property
def uuid(self):
    return "3ff62b0c9ad8445dbbe9208d87423446"

We can also use some node specific initialization data using the node_data property:

@property
def node_data(self):
    return(struct.pack('<i', self._v))

If the node constructor needs to use additional data, this function must generate a byte array. Otherwise it should generate None.

Here we are generating an int. This byte array is written to the flat buffer and can be used at runtime to customize a node. It is done in the mkNode function.

Here is how this data is used by the ProcessingNode:

// Extract values from data
const int8_t *d = ndesc->node_data()->data();
const int32_t *v = reinterpret_cast<const int32_t*>(d);

In mkNode, we get a pointer to the node data and we cast it to be able to read its content.

It implies that the mkNode function should validate this buffer content before trying to use it. There are risks implied by the use of an untyped buffer coming from outside of the application.