Arm Community
Arm Community
  • Site
  • User
  • Site
  • Search
  • User
Arm Community blogs
Arm Community blogs
Tools, Software and IDEs blog Cortex-M33 AMBA-PV TLM-2.0 Subsystem using Fast Models
  • Blogs
  • Mentions
  • Sub-Groups
  • Tags
  • Jump...
  • Cancel
More blogs in Arm Community blogs
  • AI blog

  • Announcements

  • Architectures and Processors blog

  • Automotive blog

  • Embedded and Microcontrollers blog

  • Internet of Things (IoT) blog

  • Laptops and Desktops blog

  • Mobile, Graphics, and Gaming blog

  • Operating Systems blog

  • Servers and Cloud Computing blog

  • SoC Design and Simulation blog

  • Tools, Software and IDEs blog

Tags
  • RTX
  • Simulation
  • Fast Models
  • Cortex-M33
  • Hardware Modelling/Simulation
Actions
  • RSS
  • More
  • Cancel
Related blog posts
Related forum threads

Cortex-M33 AMBA-PV TLM-2.0 Subsystem using Fast Models

Naveen Khajuria
Naveen Khajuria
May 19, 2020
18 minute read time.

In the previous post, Getting started with Fast Models and RTX, we created a custom Cortex-M33-based platform using the IP Components already available in the Fast Models IP Library. Most times you would want to add your own IP to the design and create a platform to test your software. In this post, we explore how to do that. If you want to add some of your own components in the platform, then you can export the assembled Cortex-M33 platform as a SystemC component (i.e subsystem) using System Canvas and then connect the generated subsystem with your own SystemC IP using AMBA-PV TLM-2.0 Compliant ports.

We will learn how-to do all the following:

  • Generate a Cortex-M33-based SystemC subsystem using System Canvas.
  • Explore the Fast Model generated SystemC subsystem by looking at the code.
  • Specify a new hardware IP (A basic external counter), model it in SystemC, connect it to the generated subsystem in sc_main and then build the system using a Makefile.
  • Update our RTX software from the previous blog post and recompile it to run with on our new custom platform.
  • Run the compiled RTX app on the generated Cortex-M33 (with external counter) platform.

You can download the code for Cortex-M33 subsystem and RTX app from our GitHub repo.

Generate a Cortex-M33 based SystemC subsystem using System Canvas

This part builds on the Cortex-M33 custom platform that we built in the previous blog post. If you have not looked at that I suggest you read that first. Once you are done reading that then return to proceed from here.

In this section, we make following changes to our System Canvas project:

  • Add AMBA-PV master port to the design so that we can connect an AMBA-PV TLM-2.0 compatible slave IP to this design.
    • Use a PVBus2AMBAPV bridge to convert transactions coming from PVBusMapper's master port to AMBAPV master protocol.
    • Also, configure PVBusMapper's connection to define an address range over which the external IP will be accessible.
  • Add AMBAPVSignal slave port to receive interrupt signal from an external IP.
    • Use AMBAPVSignal2SGSignal bridge to convert incoming AMBAPV Signal transactions to SGSignal protocol.
    • Connect the incoming interrupt signal to the IRQ ports of Cortex-M33.
  • Build the platform.

Let's look at the detailed steps to achieve the above targets:

  • Open the project from our previous post using System Canvas. Add a PVBus2AMBAPV bridge to the Block Diagram.

     Fast Models
  • Then select "Connect" from the Ribbon menu

    fast models

  • Make a connection from PVBusDecoder's "pvbus_m_range" port to PVBus2AMBAPV's "pvbus_s" port.

     Fast Models

  • Once the connection has been made, proceed to add transaction routing information to PVBusDecoder. Select "Edit" from the Ribbon menu and then select the "pvbus_m_range" port in PVBusDecoder.

     Arm Fast Models
     Arm Fast Models

    P.S: Check out the error that is in the 2nd screen shot, I forgot to update the routing information for "pvbus_m_range" and I got that error when I tried to build the platform.

  • Press the "Properties" button from the Ribbon menu that opens up the properties of the port "pvbus_m_range". Select the port that has no address range associated with it and press "Edit Connection".


     Fast Models

     Fast Models

  • This opens up the "Edit Connection" window. Select the "Enable Address Mapping" tick box and update Start to 0x21000000, End to 0x21000FFF. That calculates the size to 0x1000.


     Fast Models

  • Press "Ok" to close the "Edit Connection" window, then press "Ok" to close the "Port properties" window.
  • Now, let us add some ports to communicate with the SystemC IP outside this subsystem. To add a Port, right click on the grid in the "Block Diagram" window, this opens up the context menu:


     Add port dialog box

  • Press "Add Port" that opens up the following window where you need to add the name and type of the port. Add the instance name as "m_port_to_external_counter".

     Arm Fast Models

  • Press the "Select" button next to Protocol field. That opens up the list of available protocols supported by Fast Models. Select "AMBAPV" and press "Ok".


     Fast Models

     Fast Models

  • Then press "Ok" to close the window. This causes a new port to appear with your mouse pointer. Place this new port next to PVBus2AMBAPV bridge on the block diagram.


     Fast Models

  • Now, to connect the new port to the bridge, select "Connect" from the Ribbon menu and then connect PVBus2AMBAPV's "amba_pv_m" port to "m_port_to_external_counter" port.


     Fast Models

     Fast Models

  • Now, add a new slave port by right clicking on the grid. Add a "slave_counter_irq_in" as the slave port name and "AMBAPVSignal" as the Protocol.


     Fast Models

  • Drag and drop "AMBAPVSignal2SGSignal" bridge from the components list and place it on the grid next to the "slave_counter_irq_in" port.


     Fast Models

  • Switch to "Connect" mode by clicking "Connect" button on the ribbon menu. Then connect "slave_counter_irq_in" port to "amba_pv_signal_s" port on AMBAPVSignal2SGSignal bridge.

     Fast Models

  • In the connect mode, click on "sg_signal_m" port of AMBAPVSignal2SGSignal bridge and connect it to "irq" port of Cortex-M33 component. Add array index "30" in the "Add Connection" pop-up window.


     Fast Models

  • Now, we are ready to export this subsystem as a SystemC module. Fast Models product supports various configurations that you can choose based on your operating system and compiler.  In the GitHub repository, you'll find support for building this project using all the configurations that Fast Models product supports. For detailed instructions on choosing different configurations, refer to README file in the fast-models-examples/cortex-m33-subsystem directory. In this post, we choose Linux as the OS and GCC-6.4 as our compiler. Go to Project→Project Settings menu and select "Linux64-Release-GCC-6.4" configuration from the "Configuration" drop down.
  • Then Select "SystemC component" as the Target from Targets window. De-select all other options. Then press "Apply" and "Ok" to continue.


     Fast Models

  • Final, step is to press the "Build" button from the Ribbon menu. System Canvas prompts to save the modified "Project Configuration" and the "MyTopComponent.lisa" files. Press "Ok" to continue. This should make the build process to start and complete successfully.




Explore the Fast Model generated SystemC subsystem by looking at the code

Now, let us have a look at the Fast Model generated SystemC Module. Fast Models generated code has filenames, classes, and components generated with "scx_" prefix. Fast Models SystemC framework uses the namespace "scx". You can recognize these patterns in the following code snippets.

In your working directory, you can see the following files and directories:

$ ls -l
total 42992
drwxr-x--- 3 navkha01 navkha01     4096 Mar  2 16:50 Linux64-Release-GCC-6.4
-rw-r----- 1 navkha01 navkha01     1213 Mar  2 16:50 MyTopComponent.lisa
-rw-r----- 1 navkha01 navkha01   298136 Mar  2 16:50 MyTopComponent.sgcanvas
-rw-r----- 1 navkha01 navkha01     7166 Jan  8 10:16 MyTopComponent.sgproj
-rw-rw-r-- 1 navkha01 navkha01  1588016 Apr 18  2019 libMAXCOREInitSimulationEngine.3.so
-rw-r--r-- 1 navkha01 navkha01  1221177 Jan 26  2017 libSDL2-2.0.so.0.4.0
-rw-rw-r-- 1 navkha01 navkha01 39562768 Dec 10 18:23 libarmctmodel.so
-rw-rw-r-- 1 navkha01 navkha01    68176 Nov 18 20:37 libarmfastmodelsanalytics.1.1.0.so
-rw-r--r-- 1 navkha01 navkha01  1260624 May 15  2018 librui_5.2.0.x64.so
 
$ ls -l Linux64-Release-GCC-6.4/
total 11796
-rw-r----- 1 navkha01 navkha01   14759 Jan  8 10:19 MyTopComponent_Linux64-Release-GCC-6.4_Makefile.sg
drwxr-x--- 2 navkha01 navkha01   36864 Mar  2 16:50 gen
-rw-rw-r-- 1 navkha01 navkha01 1588016 Apr 18  2019 libMAXCOREInitSimulationEngine.3.so
-rwxr-x--- 1 navkha01 navkha01 4958288 Feb 28 14:01 libMyTopComponent-Linux64-Release-GCC-6.4.so
-rw-r--r-- 1 navkha01 navkha01 1221177 Jan 26  2017 libSDL2-2.0.so.0.4.0
-rw-rw-r-- 1 navkha01 navkha01   68176 Nov 18 20:37 libarmfastmodelsanalytics.1.1.0.so
-rw-r--r-- 1 navkha01 navkha01 1260624 May 15  2018 librui_5.2.0.x64.so
-rw-r----- 1 navkha01 navkha01 2603702 Mar  2 16:50 libscx-MyTopComponent-Linux64-Release-GCC-6.4.a
-rw-r----- 1 navkha01 navkha01  311056 Jan  8 10:19 libscx.a

Open the file "Linux64-Release-GCC-6.4/gen/scx_evs_MyTopComponent.h", it looks like this:

/*
 *
 *
 * This is an automatically generated file. Do not edit.
 *
 * Copyright 2011 - 2019 ARM Limited.
 *
 * All rights reserved.
 */
 
#ifndef scx_evs_MyTopComponent__H
#define scx_evs_MyTopComponent__H
 
/* Includes */
#include <sg/IncludeMeFirst.h>
 
#if defined (SG_TARGET_SCX_CADI) || ! defined(SG_BUILDING_DSO_MyTopComponent)
#if ! defined(SC_INCLUDE_DYNAMIC_PROCESSES)
#define SC_INCLUDE_DYNAMIC_PROCESSES
#endif
#include <sg/SystemCInclude.h>
#endif
 
#include <eslapi/eslapi_stdint.h>
 
#include <scx/scx.h>
 
/* Includes from the 'includes' section of the protocols */
#line 9 "/work/tests/fmProduct/11.9/install/FastModelsPortfolio_11.9/LISA/AMBAPVProtocol.lisa"
 
        #include <amba_pv.h>
     
#line 34 "./Linux64-Release-GCC-6.4/gen/scx_evs_MyTopComponent.h"
 
 
namespace MyTopComponent_NMS {
 
/* Forward declarations */
class AMBAPV_EB;
class AMBAPVSignal_EB;
 
 
/* Slave port classes TLM2 draft1*/
class sc_slave_port_class_slave_counter_irq_in: public amba_pv::signal_slave_base<bool>
{
public:
    sc_slave_port_class_slave_counter_irq_in(const std::string &portname);
    ~sc_slave_port_class_slave_counter_irq_in();
    AMBAPVSignal_EB *sg_port;
    void set_state(int export_id, const bool& state);
};
 
 
/* Slave port classes TLM2 final */
 
 
/* Master port clases TLM2 final */
class sc_master_port_class_m_port_to_external_counter: public amba_pv::amba_pv_master_base
{
public:
    sc_master_port_class_m_port_to_external_counter(const std::string &portname);
    ~sc_master_port_class_m_port_to_external_counter();
    AMBAPV_EB *sg_port;
    void invalidate_direct_mem_ptr(int socket_id, sc_dt::uint64 start_range, sc_dt::uint64 end_range);
};
 
 
/* System C wrapper class */
class scx_evs_MyTopComponent:
    public sc_core::sc_module,
    public scx::scx_evs_base {
 
        /* Ports */
    public:
 
        /* Slave ports TLM2 final */
 
 
        /* Slave ports TLM2 draft 1 and TLM 1 (Signal port interfaces) */
    amba_pv::signal_slave_export<bool> slave_counter_irq_in;
    sc_slave_port_class_slave_counter_irq_in sc_slave_port_slave_counter_irq_in;
 
 
        /* Master ports TLM2 final */
    amba_pv::amba_pv_master_socket<64> m_port_to_external_counter;
    sc_master_port_class_m_port_to_external_counter sc_master_port_m_port_to_external_counter;
 
 
        /* Master ports TLM2 draft 1 and TLM 1 (Signal port interfaces ) */
 
 
    /* Construction */
        explicit scx_evs_MyTopComponent(sc_core::sc_module_name);
        ~scx_evs_MyTopComponent() override;
 
    /* sc_object override-ables */
        const char * kind() const override;
 
    /* Implementation */
    protected:
 
        /* sc_module override-ables */
        void before_end_of_elaboration() override;
        void end_of_elaboration() override;
        void start_of_simulation() override;
        void end_of_simulation() override;
};
 
#if defined(SG_TARGET_SCX_CADI)
void scx_evs_MyTopComponent_create_sim_thread();
#endif
 
}   /* namespace MyTopComponent_NMS */
 
using MyTopComponent_NMS::scx_evs_MyTopComponent;
 
#endif  /* defined(scx_evs_MyTopComponent__H) */

In the generated SystemC component, on line numbers 80 and 85, there is one master and one slave port respectively that we added into our subsystem.

  • AMBAPV Master port - amba_pv::apmba_pv_master_socket<64> m_port_to_external_counter
    • This port can be connected to a AMBA-PV Slave port exposed by a SystemC Module.
  • AMBAPV Signal Slave export - amba_pv::signal_slave_export<bool> slave_counter_irq_in
    • This export is connected to a amba_pv::signal_master_port<bool> present in an external SystemC Module.

Now we have a SystemC subsystem with AMBA-PV TLM ports exposed that can be connected to another SystemC IP with corresponding AMBA-PV TLM ports. We are ready to write a standalone SystemC Module that acts as an external IP with corresponding AMBA-PV TLM ports exposed. We can then connect our generated Cortex-M33 SystemC subsystem with the external IP.

Specify a basic external counter, model it in SystemC, connect it to the Cortex-M33 subsystem and create the platform.

Now let us write a simple SystemC Counter that we can connect with our exported SystemC subsystem and also write sc_main to make appropriate connections and simulate the system.

Before we write the Counter IP, we need to specify the it.

Specification of example SystemC Counter IP

  • Registers
    • Counter
      • Desc: Number of ticks that the counter will run
      • Offset: 0x8
      • Size: 32 bits
      • Fields:
        • [31:0] -> Unsigned integer value
    • Control
      • Desc: Register to control the execution of the counter
      • Offset: 0x0
      • Size: 32 bits
      • Fields:
        • [0] -> Enable bit (1 means counter is enabled and counting, 0 means disabled so not counting.)
        • [31:1] -> Ignored
    • Status
      • Desc: Register displaying the status of the counter
      • Offset: 0x4
      • Size: 32 bits
      • Fields:
        • [0] -> Counter Status (1 means counter expired, 0 means counter running or stopped. Writing 1 clears the Status[0] bit as well as the Control[0] bit, thus disabling the counter.)
        • [31:1] -> Ignored
  • Ports
    • amba_pv_s
      • Type: amba_pv_slave_socket<64>
      • Desc: Slave port for programming the registers of the IP
    • irq_out
      • Type: signal_master_port<bool>
      • Desc: Master IRQ port that is asserted when the counter expires
  • Functionality
    • When the counter is enabled by writing '1' into 'control' register, IP starts a timer that expires after "counter" register ticks. On timer expiry, IRQ signal is asserted along with "status" register is updated to value '1'. On writing '1' to status register user can clear the interrupt signal and IRQ is de-asserted.
    • Programming sequence involves setting up an appropriate value in the "Counter register", then enabling the timer by writing '1' into "Control register". On timer expiry, IRQ is asserted and "Status register" has value '1'. On writing '1' to status register, IRQ signal is de-asserted and "Control/Status registers" cleared as well.

Implementation of example SystemC Counter IP


In our implementation, we declare a sc_module named "external_counter".

  • Register read/write functionality
    • Inherit from amba_pv_slave_base<BUSWIDTH> to support register read/write functionality.
      • Our counter implementation inherits from amba_pv_slave_base<BUSWIDTH> to support the register read/write functionality. amba_pv_slave_base<BUSWIDTH> is a templated class the implements the amba_pv_if<BUSWIDTH>. All IPs that expect to provide an amba_pv_slave_socket<BUSWIDTH> for connecting with an amba_pv_master_socket<BUSWIDTH> need to implement amba_pv_if<BUSWIDTH> to support binding to the slave socket. All read/write transactions arriving on the slave socket are routed to the read()/write() functions implemented by the IP. 
    • Declare amba_pv_slave_socket<BUSWIDTH> port to be available for connection to an external master. "amba_pv_s" is added as amba_pv_slave_socket<BUSWIDTH> to our IP. "amba_pv_slave" socket is further bound to (*this) in the constructor (refer line no. 88 in code below) to bind the implementation of amba_pv_if<BUSWIDTH> to the slave socket.
    • Implement the read/write functions
      • Use a switch case on the address received in the read/write functions to access appropriate registers using their address offsets.
      • In the write function, copy the incoming value in to the register variable in the IP
      • In the read function, copy the value from the register into the received "data" pointer to return the value to the calling master.
  • Support IRQ generation
    • Add a signal_master_port<bool> named "irq_out" to generate an interrupt signal when the counter completes.
    • Add a SC_METHOD named "triggerIrq" that is sensitive to an internal sc_event named "checkCounterEvent". "checkCounterEvent" is triggered when the counter timer completes and thus causes the SC_METHOD "triggerIrq" to be called. "triggerIrq" asserts the "irq_out" signal and updates the "status" register to show that the counter has expired.
  • Starting the counter and generating interrupt
    • When "1" is written into "control" register, our model schedules the sc_event "checkCounterEvent" to be fired after "counter register" ticks interval. When the event gets fired, it calls the SC_METHOD "triggerIrq" that sets the "irq_out" signal to true, thus causing an interrupt to be delivered to Cortex-M33.

Here is the full implementation of the SystemC external Counter IP:

/* name: external_counter.h
 * desc: Example external counter
 * Copyright Arm Ltd 2020
 */
#ifndef EXTERNAL_COUNTER_H
#define EXTERNAL_COUNTER_H

#include <sg/IncludeMeFirst.h>
#include <sg/SystemCInclude.h>
#include <iostream>
#include "amba_pv.h"
#include "sockets/amba_pv_slave_socket.h"

using namespace amba_pv;

template<unsigned int BUSWIDTH = 64>
class external_counter:
    public sc_core::sc_module,
    public amba_pv_slave_base<BUSWIDTH>
{
    public:
        /* Constructor/Destructor */
        external_counter(sc_core::sc_module_name);
        ~external_counter();

        /* Ports */
        amba_pv_slave_socket<BUSWIDTH> amba_pv_s;
        signal_master_port<bool>       irq_out;

        /* Registers */
        static constexpr unsigned control_addr {0x0};
        static constexpr unsigned status_addr  {0x4};
        static constexpr unsigned counter_addr {0x8};

        uint32_t control{0};
        unsigned control_ctrl_bit_mask{1};
        uint32_t status{0};
        unsigned status_clr_bit_mask{1};
        uint32_t counter{0};

        /* sc_object overridables */
        virtual const char * kind() const;

        /* User-layer interface */
        virtual amba_pv_resp_t read(int,
                                    const sc_dt::uint64 &,
                                    unsigned char *,
                                    unsigned int,
                                    const amba_pv_control *,
                                    sc_core::sc_time &);
        virtual amba_pv_resp_t write(int,
                                     const sc_dt::uint64 &,
                                     unsigned char *,
                                     unsigned int,
                                     const amba_pv_control *,
                                     unsigned char *,
                                     sc_core::sc_time &);
        virtual bool get_direct_mem_ptr(int,
                                        tlm::tlm_command,
                                        const sc_dt::uint64 &,
                                        const amba_pv_control *,
                                        tlm::tlm_dmi &);

         /* Debug interface */
         virtual unsigned int debug_read(int,
                                         const sc_dt::uint64 &,
                                         unsigned char *,
                                         unsigned int,
                                         const amba_pv_control *);
         virtual unsigned int debug_write(int,
                                          const sc_dt::uint64 &,
                                          unsigned char *,
                                          unsigned int,
                                          const amba_pv_control *);
    private:
         sc_core::sc_event checkCounterEvent{};
         void              triggerIrq();
};

template<unsigned int BUSWIDTH>
inline
external_counter<BUSWIDTH>::external_counter(sc_core::sc_module_name name):
    sc_core::sc_module(name),
    amba_pv_slave_base<BUSWIDTH>((const char *) name),
    amba_pv_s("amba_pv_s"),
    irq_out("irq_out")
{

    /* Bindings... */
    amba_pv_s(* this);
    SC_HAS_PROCESS(external_counter);
    SC_METHOD(triggerIrq);
    sensitive << checkCounterEvent;
    dont_initialize();
}
/*
 * Destructor.
 */
template<unsigned int BUSWIDTH>
inline
external_counter<BUSWIDTH>::~external_counter()
{
}

/*
 * Returns the kind string of this memory.
 */
template<unsigned int BUSWIDTH>
inline const char *
external_counter<BUSWIDTH>::kind() const
{
    return ("external_counter");
}

/*
 * Completes a read transaction
 */
template<unsigned int BUSWIDTH>
inline amba_pv_resp_t
external_counter<BUSWIDTH>::read(int socket_id,
                               const sc_dt::uint64 & addr,
                               unsigned char * data,
                               unsigned int size,
                               const amba_pv_control * ctrl,
                               sc_core::sc_time & t)
{
    std::cout << this->name() << ": call to read() for addr=" << addr << std::endl;
    switch(addr)
    {
        case control_addr:
            memcpy(data, &control, sizeof(uint32_t));
            break;
        case status_addr:
            memcpy(data, &status,  sizeof(uint32_t));
            break;
        case counter_addr:
            memcpy(data, &counter, sizeof(uint32_t));
            break;
        default:
            std::cout << this->name() << ": address not found" << std::endl;
    }
    return (AMBA_PV_OKAY);
}
/*
 * Completes a write transaction.
 */
template<unsigned int BUSWIDTH>
inline amba_pv_resp_t
external_counter<BUSWIDTH>::write(int socket_id,
                                const sc_dt::uint64 & addr,
                                unsigned char * data,
                                unsigned int size,
                                const amba_pv_control * ctrl,
                                unsigned char * strb,
                                sc_core::sc_time & t)
{
    std::cout << this->name() << ": call to write() for addr=" << addr << " value=" << *((uint32_t*)data) << std::endl;
    switch(addr)
    {
        case control_addr:
            memcpy(&control, data, sizeof(uint32_t));
            if(control & control_ctrl_bit_mask)
            {
                checkCounterEvent.notify(sc_core::sc_time(counter, sc_core::SC_NS));
            }
            break;
        case status_addr:
            memcpy(&status,  data,sizeof(uint32_t));
            if(status & status_clr_bit_mask)
            {
                control = 0;
                status  = 0;
                irq_out.set_state(false);
            }
            break;
        case counter_addr:
            memcpy(&counter,  data,sizeof(uint32_t));
            break;
        default:
            std::cout << this->name() << ": address not found" << std::endl;
    }
    return (AMBA_PV_OKAY);
}
/*
 * Non-intrusive debug read transaction.
 */
template<unsigned int BUSWIDTH>
inline unsigned int
external_counter<BUSWIDTH>::debug_read(int socket_id,
                                     const sc_dt::uint64& addr,
                                     unsigned char * data,
                                     unsigned int length,
                                     const amba_pv_control * ctrl)
{
    std::cout << this->name() << ": call to debug_read(), addr=0x"
              << std::hex << addr << std::endl;
    return 0;
}

/*
 * Non-intrusive debug write transaction.
 */
template<unsigned int BUSWIDTH>
inline unsigned int
external_counter<BUSWIDTH>::debug_write(int socket_id,
                                      const sc_dt::uint64 & addr,
                                      unsigned char * data,
                                      unsigned int length,
                                      const amba_pv_control * ctrl)
{
    std::cout << this->name() << ": call to debug_write(), addr=0x"
              << std::hex << addr << std::endl;
    return 0;
}

/*
 * Requests DMI access to the specified address and returns a reference to a
 * DMI descriptor.
 */
template<unsigned int BUSWIDTH>
inline bool
external_counter<BUSWIDTH>::get_direct_mem_ptr(int socket_id,
                                         tlm::tlm_command command,
                                         const sc_dt::uint64 & addr,
                                         const amba_pv_control * ctrl,
                                         tlm::tlm_dmi & dmi_data)
{
    return false;
}
template<unsigned int BUSWIDTH>
inline void
external_counter<BUSWIDTH>::triggerIrq()
{
    std::cout << "IrqOut " << std::endl;
    status = 1;
    irq_out.set_state(true);
}
#endif /* EXTERNAL_COUNTER_H */

Writing sc_main and making the connections

To assemble our platform, we write sc_main.

  • In sc_main, we first call scx::scx_initialize() to initialise Fast Models SystemC internals.
  • We declare instances of the generated subsystem i.e scx_evs_MyTopComponent and our hand written external counter i.e external_counter<64>.
  • After the declarations,
    • we make appropriate master-slave connections
    • setup the global quantum and min-max latency
    • parse extra arguments to support inbuilt Fast Model options like "-l" to list parameters or "--plugin" to load Fast Models plugins like TarmacTrace or GenericTrace
    • Finally, we call sc_start to start the simulation.

Here is the complete main.cpp:

/*
 * Copyright (c) 2020 Arm Limited. All rights reserved.
 */
#include "scx_evs_MyTopComponent.h"
#include <amba_pv.h>
#include "external_counter.h"


int sc_main(int argc, char *argv[])
{
    double quantum = 10000.0;
    double latency = 100.0;

    scx::scx_initialize("MyTopComponent", scx::scx_get_default_simcontrol());

    MyTopComponent_NMS::scx_evs_MyTopComponent cortex_m33_subsystem("cortex-m33-subsystem");
    external_counter<64>                   ext_counter("ext_counter");

    cortex_m33_subsystem.m_port_to_external_counter(ext_counter.amba_pv_s);
    ext_counter.irq_out(cortex_m33_subsystem.slave_counter_irq_in);

    /* Simulation quantum, i.e. seconds to run per quantum */
    tlm::tlm_global_quantum::instance().set(sc_core::sc_time(quantum
                                                             / 100000000.0,
                                                             sc_core::SC_SEC));

    /* Simulation minimum synchronization latency */
    scx::scx_set_min_sync_latency(latency / 100000000.0);

    /* From command-line options... */
    scx::scx_parse_and_configure(argc, argv, "");

    sc_core::sc_start();

    return 0;
}

Compile the new platform

  • Change directory to "system/systemc-plt" folder.
  • On a Linux host, run "make rel_gccNN_64" to build the platform, where NN is one of the supported gcc variants. The options are currently "49", "64" or "73".
  • On a Windows host, run "nmake /nologo /f nMakefile rel_vs141_64" to build the platform with Visual Studio 2017 or "nmake /nologo /f nMakefile rel_vs14_64" to build with Visual Studio 2017.

This completes our custom platform creation. Now, lets proceed to update our RTX app to program the external IP and then receive an interrupt when the external counter expires.

Update our RTX app and compile it to run on our custom platform

In our RTX app, we need to configure the external timer by writing a counter value into "counter" register and then writing "1" into "control" register to start the counter. We also register an interrupt handler for interrupt number 30 using NVIC_SetVector() and NVIC_EnableIRQ() calls.

When program the external counter hits the OneShotTimer callback, we setup the external counter in that callback and then wait for the interrupt to be generated. As soon as the interrupt is generated, out interrupt handler externalInterruptHandler() is called that clears the interrupt by writing "1" into the "status register".

Here is the updated app/timer.c file:

#include <stdio.h>
#include "cmsis_os2.h"
#include "ARMCM33.h"
#include "core_cm33.h"

static const uint32_t external_counter_base_addr = 0x21000000;
static uint32_t * const external_counter_base    = (uint32_t*)external_counter_base_addr;
static const uint32_t counter_value              = 5000000;
static const uint32_t counter_enable             = 1;
static const uint32_t counter_register_addr_offset = 2;
static const uint32_t control_register_addr_offset = 0;
static const uint32_t status_register_addr_offset = 1;

void periodicTimerCallback(void *argument);
void oneShotTimerCallback(void *argument);
void app_main (void *argument);

osTimerId_t tidPeriodic;
osTimerId_t tidOneShot;

void periodicTimerCallback(void *argument) {
    printf("Periodic Timer hit\n");
}

void oneShotTimerCallback(void *argument) {
    printf("One shot Timer hit\n");
    uint32_t * const counter_addr = (external_counter_base + counter_register_addr_offset);
    *counter_addr = counter_value;
    uint32_t * const control_addr = (external_counter_base + control_register_addr_offset);
    *control_addr = counter_enable;
}

void app_main (void *argument) {
    tidPeriodic = osTimerNew(periodicTimerCallback, osTimerPeriodic, NULL, NULL);
    tidOneShot  = osTimerNew(oneShotTimerCallback,  osTimerOnce,     NULL, NULL);

    osTimerStart(tidPeriodic, 100);
    osTimerStart(tidOneShot, 2000);
    while(1);
}

void externalInterruptHandler(void)
{
    printf("External interrupt handler called \n");
    uint32_t * const status_addr = (external_counter_base + status_register_addr_offset);
    *status_addr = 1; /* write 1 to clean interrupt and status register */
}

void setupExternalInterrupt(unsigned irq_no)
{
    NVIC_SetVector(irq_no, (uint32_t)(&externalInterruptHandler));
    NVIC_EnableIRQ(irq_no);
    __enable_irq();
    printf("External Interrupt Enabled \n");
}

int main (void) {
    osKernelInitialize();
    osThreadNew(app_main, NULL, NULL);
    setupExternalInterrupt(30);
    osKernelStart();
}

Run the compiled RTX app on the generated Cortex-M33 (with external counter) platform

Final step is to run the compiled app on the custom platform. Change directory to "system/systemc-plt" and execute the platform binary as follows:

on Linux

$ ./Cortex-M33-example.x -a ../../software/test.axf

on Windows

Cortex-M33-example.exe -a ..\..\software\test.axf

Here is the command and the output:

$ ./Cortex-M33-example.x -a ../../software/test.axf

Fast Models [11.9.47 (Dec 10 2019)]
Copyright 2000-2019 ARM Limited.
All Rights Reserved.

External Interrupt Enabled
Periodic Timer hit
Periodic Timer hit
Periodic Timer hit
Periodic Timer hit
Periodic Timer hit
Periodic Timer hit
Periodic Timer hit
Periodic Timer hit
Periodic Timer hit
Periodic Timer hit
Periodic Timer hit
Periodic Timer hit
Periodic Timer hit
Periodic Timer hit
Periodic Timer hit
Periodic Timer hit
Periodic Timer hit
Periodic Timer hit
Periodic Timer hit
One shot Timer hit
ext_counter: call to write() for addr=8 value=5000000
ext_counter: call to write() for addr=0 value=1
IrqOut
External interrupt handler called
ext_counter: call to write() for addr=4 value=1
Periodic Timer hit
Periodic Timer hit
Periodic Timer hit
Periodic Timer hit
Periodic Timer hit
Periodic Timer hit
Periodic Timer hit
Periodic Timer hit
^C
Stopping simulation...


Info: /OSCI/SystemC: Simulation stopped by user.

This completes our demo on how-to generate a custom Cortex-M33 based subsystem, attach our own AMBA-PV TLM-2.0 compliant model to the subsystem and update our RTX App to run on the new platform.

To discuss Fast Models and other simulation models that Arm provides, please head over to our discussion forum:

Models Discussion Forum

Anonymous
Tools, Software and IDEs blog
  • Python on Arm: 2025 Update

    Diego Russo
    Diego Russo
    Python powers applications across Machine Learning (ML), automation, data science, DevOps, web development, and developer tooling.
    • August 21, 2025
  • Product update: Arm Development Studio 2025.0 now available

    Stephen Theobald
    Stephen Theobald
    Arm Development Studio 2025.0 now available with Arm Toolchain for Embedded Professional.
    • July 18, 2025
  • GCC 15: Continuously Improving

    Tamar Christina
    Tamar Christina
    GCC 15 brings major Arm optimizations: enhanced vectorization, FP8 support, Neoverse tuning, and 3–5% performance gains on SPEC CPU 2017.
    • June 26, 2025