Hiring, or looking for work? Employers: post a job on Far Reach, compare proposals, and fund a milestone that is released when you accept the work. Freelancers: join Far Reach and get hired for what you do best. Post a job on Far Reach →Join as a freelancer →
Advertisement
Buy and Sell your new and used items in Nouhata.com
Visit Nouhata.com →
Advertisement
Project and team management with CSMS.work
Visit CSMS.work →

TSystemC › Examples › Power-aware SoC

SystemC example · TSystemC 1.0.1

Teaching project: a small power-aware SoC

This project builds a complete, if tiny, system-on-chip in SystemC/TLM-2.0 with TSystemC's power modeling, and verifies it in three ways. It is meant for teaching: every part is small enough to read in one sitting, and every number can be traced back to one line of code.

FileWhat it is
power_soc.cppTop level: power domains, the SoC, stimulus, power probes, policy switches
soc_core.htiny16, a 16-register core with a 15-instruction ISA, fetching from ROM over the bus
soc_periph.hROM, RAM, bus, PMU (power management unit and interrupt controller), timer, GPIO, UART
soc_firmware.hThe firmware, written with a 40-line assembler, and its power policies
tb_power_soc.svtrsim testbench: drives the SoC through DPI-C, checks function and the power budget
verify_power.icppICPP script: runs every power policy through MCP and compares them
power_soc.goldenExpected output of the standalone run
run_all.ps1Runs all three flows

1. The system

                 +------------------------------ domain "core" (DVFS) ---+
                 |  tiny16 core        ROM 4 KB         RAM 4 KB         |
                 +-------|------------------|----------------|-----------+
                         |        TLM-2.0, loosely timed     |
+-- domain "aon" (always on) ---------------------------------v-----------+
|  Bus ---> PMU (power control + interrupts)   Timer   GPIO (sensor, LED) |
+--------|----------------------------------------------------------------+
         v
+-- domain "io" (power-gated) --+
|  UART                         |
+-------------------------------+
AddressBlockDomainRegisters
0x0000_0000ROM 4 KBcorefirmware
0x1000_0000RAM 4 KBcoredata
0x4000_0000PMUaonIRQ_STATUS (W1C), IRQ_ENABLE, SLEEP_MODE, CPU_OP, IO_POWER
0x4000_1000TimeraonPERIOD_US, ENABLE
0x4000_2000GPIOaonSENSOR (in), LED (out); button edge interrupt
0x4000_3000UARTioTX, STATUS (busy); 1 Mbaud, 10 µs per byte

The application. Every 50 µs the timer wakes the core, which reads the sensor and stores the sample in RAM. Every fourth sample, it sends the sum of the last four over the UART. A button press toggles the LED.

2. The power intent

This is the part to study first. Power design starts with a power intent — which blocks share a supply, which can be switched off, which can be slowed down — and the model states it in a few lines of power_soc.cpp:

power_domain aon("aon", 1.0);                                     // always on
power_domain core_dom("core_dom", 1.0, sc_time(10, SC_NS));       // 1.0 V, 100 MHz nominal
core_dom.add_operating_point("fast", 1.0, sc_time(10, SC_NS))
        .add_operating_point("slow", 0.7, sc_time(40, SC_NS));    // 0.7 V, 25 MHz
power_domain io_dom("io_dom", 1.0);                               // power-gated
io_dom.set_switch_energy(5e-9, 1e-9);                             // 5 nJ on, 1 nJ off

Why these choices:

Each block then describes its own power behaviour. The core, in soc_core.h:

power.add_state("run",   0.3e-3, 4.0e-3)    // leakage, dynamic at 1.0 V / 100 MHz
     .add_state("idle",  0.3e-3)            // clock gated
     .add_state("sleep", 0.02e-3)           // retention
     .add_transition("run",   "sleep", sc_time(0.5, SC_US), 0.5e-9)
     .add_transition("sleep", "run",   sc_time(2, SC_US),   2e-9)
     .add_operation("alu", 10e-12).add_operation("mem", 15e-12).add_operation("branch", 12e-12);

The peripherals each call sc_power::charge() at the top of b_transport. That books the energy of the access, and it turns an access to a powered-off block into TLM_GENERIC_ERROR_RESPONSE — exactly what firmware sees if it forgets to switch the UART on.

3. Software controls power

The hardware provides mechanisms. The firmware chooses a policy by writing PMU registers. soc_firmware.h builds the same application under different policies:

SwitchChoicesWhat it changes
wfiyes / noWFI between events, or busy-poll the PMU
sleepdeep / idleWFI enters retention (2 µs wake-up) or only gates the clock
cpuslow / fastcore domain at 0.7 V / 25 MHz or 1.0 V / 100 MHz
iogated / onUART powered only while sending, or always

Note what happens when gating the UART (io=gated): the firmware switches it on, waits for the 1 µs settle time (the PMU write takes that long), sends, waits for STATUS to clear, and switches it off again.

4. Run it

tsystemc run power_soc.cpp                          the recommended policy
tsystemc run power_soc.cpp -- cpu=fast io=on        any other policy

The output lists the UART bytes, a summary and the power report:

uart bytes 9, last 20, led 0, instructions 1692, bus errors 0
energy 900 nJ, average power 453 uW, core asleep 88 %

model           state  power       avg power   energy      static      dynamic     transitions
soc.cpu.power   sleep  14 uW       153.1 uW    306.3 nJ    74.65 nJ    128.6 nJ    103 nJ (83)
...
soc.uart.power  idle*  0 W         69.23 uW    138.5 nJ    20.41 nJ    118 nJ      0 J (18)
total                  209 uW      451.3 uW    902.6 nJ  (domain switching 55 nJ)

5. Verify it

With trsim: function and budget, from a Verilog testbench

This needs trsim 1.1.1 or later, from https://chapai.ai/trsim.html.

set TUWASIM_DPI_LIBS=C:\tools\tsystemc-1.0.1\bin\tsystemc.dll
trsim -top tb_power_soc tb_power_soc.sv

The testbench opens the SoC through DPI-C in hosted mode. It owns the pins (sensor, button) and steps SystemC time with sysc_dpi_run_ps. It checks the UART reports and the LED, then the power budget:

4. power budget
  ok    avg_power_uw = 459 (limit 500)
  ok    cpu_asleep_pct = 88 (at least 80)
PASS  tb_power_soc: 9 checks

A budget check in the RTL-style testbench means a firmware or hardware change that breaks the power target fails the regression, just as a functional bug would.

With ICPP: compare the policies

This needs the ICPP interpreter (kcpp) from KCPP_ICPP_x64_2026-09-30.zip or later, on https://chapai.ai/downloads.html. Older builds of kcpp cannot hold strings in $ variables and stop with "cannot start mcp".

set TSYSTEMC_EXE=C:/tools/tsystemc-1.0.1/bin/tsystemc.exe
kcpp -icpp verify_power.icpp

The script opens the SoC through tsystemc mcp once per policy and applies the same stimulus each time. It checks:

  1. Power management does not change the application. Every policy sends the same bytes and leaves the LED the same.
  2. The recommended policy uses the least energy.
  3. The recommended policy meets the budget.
policy                                    bytes   last  led  energy nJ     avg uW  asleep
wfi=yes sleep=deep cpu=slow io=gated          9     20    0        900        453     88%
wfi=yes sleep=deep cpu=slow io=on             9     20    0       1221        614     89%
wfi=yes sleep=deep cpu=fast io=gated          9     20    0       1769        889     90%
wfi=yes sleep=idle cpu=slow io=gated          9     20    0       1101        553     92%
wfi=yes sleep=idle cpu=fast io=on             9     20    0       2131       1071     94%
wfi=no cpu=fast io=on                         9     20    0      10895       5475      0%

recommended policy: 900 nJ, 91% less energy than the worst (10895 nJ)
PASS  verify_power: 6 policies, function identical, recommended policy cheapest and within 500 uW

run_all.ps1 runs the standalone model, the trsim testbench and the ICPP script in turn.

6. What the numbers teach

7. Exercises

  1. Interrupt instead of polling. Add a TX-done interrupt to the UART (bit 2 in the PMU). Change the firmware to WFI while the byte goes out instead of polling STATUS. Measure the saving. Does cpu=fast still lose?
  2. Break the budget. Change the timer period to 10 µs (build_firmware(policy, 10)). Which trsim checks fail? Which policy is cheapest now, and why?
  3. Forget to power on. Remove the IO_POWER = 1 write from the firmware. Run it and read the tiny16 bus-error warnings. This is the bug class the charge() error response exists to catch.
  4. Retention RAM. Put the RAM in its own domain with a retention state (add_state("retention", ...)) and let the PMU drop it to retention during deep sleep. What fraction of the floor does that remove?
  5. Race to idle. Add an operating point "turbo" (1.1 V, 125 MHz). With interrupt-driven TX (exercise 1), is it better to finish fast and sleep, or to run slow?
  6. A governor. Make the firmware choose CPU_OP itself: fast while a UART report is pending, slow otherwise.

The source

Every file of the project, exactly as it ships in examples\power_soc of the TSystemC download. Open a file to read it.

power_soc.cpp 192 lines
// TSystemC teaching project: a small power-aware SoC.
//
//                      +---------------------------- domain "core" (DVFS) ---+
//                      |  tiny16 core      ROM 4 KB       RAM 4 KB          |
//                      +------|---------------|--------------|---------------+
//                             |  TLM-2.0 (loosely timed)     |
//   +-- domain "aon" ---------v------------------------------v---------------+
//   |  Bus ---> PMU (power control, interrupts)   Timer   GPIO (sensor, LED) |
//   +--------|---------------------------------------------------------------+
//            v
//   +-- domain "io" (power-gated by the PMU) --+
//   |  UART                                    |
//   +------------------------------------------+
//
// Run it:
//   tsystemc run power_soc.cpp                         default policy, built-in stimulus
//   tsystemc run power_soc.cpp -- cpu=fast io=on       another power policy
//
// Arguments (any order):
//   hosted                 no built-in stimulus, no time limit: a host (trsim through
//                          DPI-C, ICPP through MCP, a tsystemc script) drives the pins
//   wfi=yes|no  sleep=deep|idle  cpu=slow|fast  io=gated|on    the power policy
//   quiet                  do not print UART bytes
//
// Pins and probes, by hierarchical name, for hosts:
//   sensor (int, in)  button (bool, in)  led (bool, out)  uart_count, uart_last (int, out)
//   energy_nj  avg_power_uw  cpu_asleep_pct  instructions  (int, refreshed every 10 us)
//   soc.cpu.power, soc.uart.power, ... (power models)  core_dom, io_dom, aon (power domains)
#include "soc_core.h"
#include "soc_firmware.h"
#include "soc_periph.h"

#include <cstring>
#include <iostream>
#include <string>

using namespace sc_core;
using namespace sc_power;

struct Soc : sc_module {
    sc_in<int> sensor{"sensor"};
    sc_in<bool> button{"button"};
    sc_out<bool> led{"led"};
    sc_out<int> uart_count{"uart_count"}, uart_last{"uart_last"};

    TinyCore cpu;
    Memory rom, ram;
    Bus bus;
    Pmu pmu;
    Timer timer;
    Gpio gpio;
    Uart uart;
    sc_signal<bool> irq{"irq"}, deep_sleep{"deep_sleep"}, timer_irq{"timer_irq"}, button_irq{"button_irq"};

    Soc(sc_module_name n, power_domain &aon, power_domain &core, power_domain &io, const Policy &policy)
        : sc_module(n), cpu("cpu", core), rom("rom", core, 4096, true, 0.05e-3, 5e-12, 0),
          ram("ram", core, 4096, false, 0.1e-3, 8e-12, 10e-12), bus("bus", aon), pmu("pmu", aon, core, io),
          timer("timer", aon), gpio("gpio", aon), uart("uart", io) {
        rom.load(build_firmware(policy));
        cpu.bus.bind(bus.cpu);
        bus.rom.bind(rom.socket);
        bus.ram.bind(ram.socket);
        bus.pmu.bind(pmu.socket);
        bus.timer.bind(timer.socket);
        bus.gpio.bind(gpio.socket);
        bus.uart.bind(uart.socket);
        cpu.irq(irq);
        cpu.deep_sleep(deep_sleep);
        pmu.irq(irq);
        pmu.deep_sleep(deep_sleep);
        pmu.timer_irq(timer_irq);
        pmu.button_irq(button_irq);
        timer.irq(timer_irq);
        gpio.irq(button_irq);
        gpio.sensor(sensor);
        gpio.button(button);
        gpio.led(led);
        uart.tx_count(uart_count);
        uart.tx_last(uart_last);
    }
};

// Integer probes of the power state, for hosts and testbenches that read signals.
struct PowerProbe : sc_module {
    sc_out<int> energy_nj{"energy_nj"}, avg_power_uw{"avg_power_uw"}, cpu_asleep_pct{"cpu_asleep_pct"},
        instructions{"instructions"};
    power_model *cpu;
    const unsigned long long *retired;

    PowerProbe(sc_module_name n, power_model &cpu_, const unsigned long long &retired_)
        : sc_module(n), cpu(&cpu_), retired(&retired_) {
        SC_THREAD(run);
    }
    void run() {
        for (;;) {
            wait(10, SC_US);
            power_monitor &m = power_monitor::instance();
            double t = sc_time_stamp().to_seconds();
            energy_nj.write(int(m.total_energy() * 1e9 + 0.5));
            avg_power_uw.write(int(m.total_energy() / t * 1e6 + 0.5));
            double asleep = (cpu->time_in_state("idle") + cpu->time_in_state("sleep")).to_seconds();
            cpu_asleep_pct.write(int(asleep / t * 100.0 + 0.5));
            instructions.write(int(*retired));
        }
    }
};

// Built-in stimulus for standalone runs.
struct Stimulus : sc_module {
    sc_out<int> sensor{"sensor"};
    sc_out<bool> button{"button"};
    SC_CTOR(Stimulus) { SC_THREAD(run); }
    void run() {
        sensor.write(10);
        wait(330, SC_US);
        press();
        wait(680, SC_US);   // 1010 us
        sensor.write(5);
        wait(320, SC_US);   // 1330 us
        press();
    }
    void press() {
        button.write(true);
        wait(2, SC_US);
        button.write(false);
    }
};

int sc_main(int argc, char *argv[]) {
    Policy policy;
    bool hosted = false, quiet = false;
    for (int i = 1; i < argc; ++i) {
        std::string a = argv[i];
        if (a == "hosted") hosted = true;
        else if (a == "quiet") quiet = true;
        else if (a == "wfi=no") policy.wfi = false;
        else if (a == "wfi=yes") policy.wfi = true;
        else if (a == "sleep=idle") policy.deep_sleep = false;
        else if (a == "sleep=deep") policy.deep_sleep = true;
        else if (a == "cpu=fast") policy.slow_cpu = false;
        else if (a == "cpu=slow") policy.slow_cpu = true;
        else if (a == "io=on") policy.gate_io = false;
        else if (a == "io=gated") policy.gate_io = true;
        else {
            std::cerr << "unknown argument: " << a << "\n";
            return 2;
        }
    }

    // Power intent: three domains. The core domain has two operating points.
    power_domain aon("aon", 1.0);
    power_domain core_dom("core_dom", 1.0, sc_time(10, SC_NS));   // "fast": 1.0 V, 100 MHz
    core_dom.add_operating_point("fast", 1.0, sc_time(10, SC_NS)).add_operating_point("slow", 0.7, sc_time(40, SC_NS));
    power_domain io_dom("io_dom", 1.0);
    io_dom.set_switch_energy(5e-9, 1e-9);
    io_dom.power_off();   // the UART starts unpowered; the firmware's policy decides

    sc_signal<int> sensor("sensor"), uart_count("uart_count"), uart_last("uart_last");
    sc_signal<bool> button("button"), led("led");
    sc_signal<int> energy_nj("energy_nj"), avg_power_uw("avg_power_uw"), cpu_asleep_pct("cpu_asleep_pct"),
        instructions("instructions");

    Soc soc("soc", aon, core_dom, io_dom, policy);
    soc.sensor(sensor);
    soc.button(button);
    soc.led(led);
    soc.uart_count(uart_count);
    soc.uart_last(uart_last);
    soc.uart.quiet = quiet;
    PowerProbe probe("probe", soc.cpu.power, soc.cpu.retired);
    probe.energy_nj(energy_nj);
    probe.avg_power_uw(avg_power_uw);
    probe.cpu_asleep_pct(cpu_asleep_pct);
    probe.instructions(instructions);

    if (hosted) {
        sc_start();   // parks here; the host steps time
        return 0;
    }

    Stimulus stim("stim");
    stim.sensor(sensor);
    stim.button(button);
    std::printf("policy %s\n", policy.name().c_str());
    sc_start(2, SC_MS);
    std::printf("\nuart bytes %d, last %d, led %d, instructions %d, bus errors %lu\n", uart_count.read(),
                uart_last.read(), int(led.read()), instructions.read(), soc.cpu.bus_errors);
    std::printf("energy %d nJ, average power %d uW, core asleep %d %%\n\n", energy_nj.read(), avg_power_uw.read(),
                cpu_asleep_pct.read());
    power_monitor::instance().report(std::cout);
    return 0;
}
soc_core.h 157 lines
// Power-aware SoC teaching example: the core.
//
// "tiny16" is a 32-bit core with 16 registers (r0 reads as 0) and a 15-instruction
// ISA. It fetches every instruction from ROM over the bus, so instruction fetch costs
// bus and memory energy like on a real microcontroller.
//
//   encoding:  op[31:24] rd[23:20] rs[19:16] imm[15:0]
//
//   LI   rd, imm        rd = sign-extended imm          ALU    1 cycle
//   LUI  rd, imm        rd = imm << 16                  ALU    1
//   ADD  rd, rs         rd = rd + rs                    ALU    1
//   ADDI rd, rs, imm    rd = rs + imm                   ALU    1
//   ANDI rd, rs, imm    rd = rs & imm (zero-extended)   ALU    1
//   XORI rd, rs, imm    rd = rs ^ imm                   ALU    1
//   SHRI rd, rs, imm    rd = rs >> imm                  ALU    1
//   LW   rd, imm(rs)    rd = mem[rs + imm]              MEM    2 + bus
//   SW   rd, imm(rs)    mem[rs + imm] = rd              MEM    2 + bus
//   BEQ  rd, rs, label  if rd == rs goto label          BRANCH 2
//   BNE  rd, rs, label  if rd != rs goto label          BRANCH 2
//   J    label                                          BRANCH 2
//   WFI                 wait for an enabled interrupt (idle or deep sleep, see the PMU)
//   HALT                stop
//   NOP
//
// Power: states "run" (clock running), "idle" (clock gated, WFI with light sleep) and
// "sleep" (retention, WFI with deep sleep). Leaving deep sleep takes 2 us. Each
// instruction class has its own energy, scaled by the core domain's voltage.
#ifndef POWER_SOC_CORE_H
#define POWER_SOC_CORE_H

#include <sc_power_tlm.h>
#include <tlm_utils/simple_initiator_socket.h>

#include <cstdint>
#include <cstdio>

namespace tiny16 {
enum op : uint8_t { NOP, LI, LUI, ADD, ADDI, ANDI, XORI, SHRI, LW, SW, BEQ, BNE, J, WFI, HALT };
inline uint32_t enc(op o, unsigned rd, unsigned rs, int imm) {
    return (uint32_t(o) << 24) | ((rd & 15u) << 20) | ((rs & 15u) << 16) | (uint32_t(imm) & 0xffffu);
}
} // namespace tiny16

struct TinyCore : sc_core::sc_module {
    tlm_utils::simple_initiator_socket<TinyCore> bus{"bus"};
    sc_core::sc_in<bool> irq{"irq"};          // from the PMU: an enabled interrupt is pending
    sc_core::sc_in<bool> deep_sleep{"deep_sleep"};   // from the PMU: WFI enters deep sleep
    sc_power::power_domain &dom;
    sc_power::power_model power;
    unsigned long long retired = 0;
    unsigned long bus_errors = 0;

    TinyCore(sc_core::sc_module_name n, sc_power::power_domain &d) : sc_module(n), dom(d), power("power", d) {
        using sc_core::sc_time;
        using sc_core::SC_US;
        power.add_state("run", 0.3e-3, 4.0e-3)   // at 1.0 V, 100 MHz: 0.3 mW leakage + 4 mW clock/logic
            .add_state("idle", 0.3e-3)            // clock gated
            .add_state("sleep", 0.02e-3)          // retention
            .add_transition("run", "sleep", sc_time(0.5, SC_US), 0.5e-9)
            .add_transition("sleep", "run", sc_time(2, SC_US), 2e-9)
            .add_operation("alu", 10e-12)
            .add_operation("mem", 15e-12)
            .add_operation("branch", 12e-12);
        SC_THREAD(run);
    }

private:
    uint32_t r[16] = {};
    uint32_t pc = 0;

    // One bus transfer; waits for the annotated delay. Returns false on a bus error.
    bool access(tlm::tlm_command cmd, uint32_t addr, uint32_t &data) {
        tlm::tlm_generic_payload t;
        sc_core::sc_time delay = sc_core::SC_ZERO_TIME;
        t.set_command(cmd);
        t.set_address(addr);
        t.set_data_ptr(reinterpret_cast<unsigned char *>(&data));
        t.set_data_length(4);
        t.set_streaming_width(4);
        t.set_response_status(tlm::TLM_INCOMPLETE_RESPONSE);
        bus->b_transport(t, delay);
        wait(delay);
        if (t.is_response_error()) {
            ++bus_errors;
            char m[96];
            std::snprintf(m, sizeof m, "bus error: %s 0x%08x at pc 0x%04x (%s)", cmd == tlm::TLM_READ_COMMAND ? "read" : "write",
                          addr, pc, t.get_response_string().c_str());
            SC_REPORT_WARNING("tiny16", m);
            return false;
        }
        return true;
    }

    void cycles(const char *cls, unsigned n) {
        power.consume(cls);
        wait(dom.cycles(n));
    }

    void wfi() {
        if (irq.read()) return;   // already pending: no sleep at all
        bool deep = deep_sleep.read();
        sc_core::sc_time lat = power.set_state(deep ? "sleep" : "idle");
        wait(lat);
        while (!irq.read()) wait(irq.posedge_event());
        wait(power.set_state("run"));   // wake-up latency (2 us from deep sleep)
    }

    void run() {
        using namespace tiny16;
        for (;;) {
            uint32_t w = 0;
            if (!access(tlm::TLM_READ_COMMAND, pc, w)) return;   // fetch
            uint32_t next = pc + 4;
            op o = op(w >> 24);
            unsigned rd = (w >> 20) & 15, rs = (w >> 16) & 15;
            int32_t simm = int16_t(w & 0xffff);
            uint32_t uimm = w & 0xffff;
            uint32_t a = r[rs] + uint32_t(simm);
            switch (o) {
            case NOP: cycles("alu", 1); break;
            case LI: r[rd] = uint32_t(simm); cycles("alu", 1); break;
            case LUI: r[rd] = uimm << 16; cycles("alu", 1); break;
            case ADD: r[rd] += r[rs]; cycles("alu", 1); break;
            case ADDI: r[rd] = r[rs] + uint32_t(simm); cycles("alu", 1); break;
            case ANDI: r[rd] = r[rs] & uimm; cycles("alu", 1); break;
            case XORI: r[rd] = r[rs] ^ uimm; cycles("alu", 1); break;
            case SHRI: r[rd] = r[rs] >> (uimm & 31); cycles("alu", 1); break;
            case LW: {
                uint32_t v = 0;
                access(tlm::TLM_READ_COMMAND, a, v);
                r[rd] = v;
                cycles("mem", 2);
                break;
            }
            case SW: {
                uint32_t v = r[rd];
                access(tlm::TLM_WRITE_COMMAND, a, v);
                cycles("mem", 2);
                break;
            }
            case BEQ: if (r[rd] == r[rs]) next = uimm * 4; cycles("branch", 2); break;
            case BNE: if (r[rd] != r[rs]) next = uimm * 4; cycles("branch", 2); break;
            case J: next = uimm * 4; cycles("branch", 2); break;
            case WFI: cycles("alu", 1); wfi(); break;
            case HALT: power.set_state("idle"); return;
            default:
                SC_REPORT_ERROR("tiny16", "illegal instruction");
                return;
            }
            r[0] = 0;
            pc = next;
            ++retired;
        }
    }
};

#endif
soc_periph.h 325 lines
// Power-aware SoC teaching example: memories, bus and peripherals.
//
// Address map                                   power domain
//   0x0000_0000  ROM   4 KB  (firmware)          core   (DVFS: fast 1.0 V 100 MHz, slow 0.7 V 25 MHz)
//   0x1000_0000  RAM   4 KB                      core
//   0x4000_0000  PMU   power management + interrupt controller   aon (always on)
//   0x4000_1000  TIMER periodic wake-up          aon
//   0x4000_2000  GPIO  sensor input, button, LED aon
//   0x4000_3000  UART  transmit only, 1 Mbaud    io     (power-gated by the PMU)
//
// Every target calls sc_power::charge() first: it books the access energy, adds any
// wake-up latency, and answers with TLM_GENERIC_ERROR_RESPONSE while the target's
// domain is off - which is what a forgotten "power on" looks like to firmware.
#ifndef POWER_SOC_PERIPH_H
#define POWER_SOC_PERIPH_H

#include <sc_power_tlm.h>
#include <tlm_utils/simple_initiator_socket.h>
#include <tlm_utils/simple_target_socket.h>

#include <cstdint>
#include <cstdio>
#include <cstring>
#include <vector>

using sc_core::sc_time;
using sc_core::SC_NS;
using sc_core::SC_US;

namespace soc {
const uint32_t ROM = 0x00000000, RAM = 0x10000000, PMU = 0x40000000, TIMER = 0x40001000, GPIO = 0x40002000,
               UART = 0x40003000;
}

// A 32-bit register access helper for the peripherals below.
inline bool reg_access(tlm::tlm_generic_payload &t, uint32_t &value) {
    if (t.get_data_length() != 4) {
        t.set_response_status(tlm::TLM_BURST_ERROR_RESPONSE);
        return false;
    }
    if (t.is_write()) std::memcpy(&value, t.get_data_ptr(), 4);
    else std::memcpy(t.get_data_ptr(), &value, 4);
    t.set_response_status(tlm::TLM_OK_RESPONSE);
    return true;
}

// ------------------------------------------------------------------ memory
struct Memory : sc_core::sc_module {
    tlm_utils::simple_target_socket<Memory> socket{"socket"};
    sc_power::power_model power;
    std::vector<uint8_t> mem;
    bool read_only;

    Memory(sc_core::sc_module_name n, sc_power::power_domain &d, unsigned size, bool rom, double static_w,
           double read_j, double write_j)
        : sc_module(n), power("power", d), mem(size), read_only(rom) {
        socket.register_b_transport(this, &Memory::b_transport);
        power.add_state("on", static_w).add_operation("read", read_j, 0.5e-12);
        if (!rom) power.add_operation("write", write_j, 0.6e-12);
    }
    void load(const std::vector<uint32_t> &words) { std::memcpy(mem.data(), words.data(), words.size() * 4); }

    void b_transport(tlm::tlm_generic_payload &t, sc_time &delay) {
        if (!sc_power::charge(power, t, delay)) return;
        uint64_t a = t.get_address();
        unsigned n = t.get_data_length();
        if (a + n > mem.size()) return t.set_response_status(tlm::TLM_ADDRESS_ERROR_RESPONSE);
        if (t.is_write() && read_only) return t.set_response_status(tlm::TLM_COMMAND_ERROR_RESPONSE);
        if (t.is_read()) std::memcpy(t.get_data_ptr(), &mem[a], n);
        else std::memcpy(&mem[a], t.get_data_ptr(), n);
        delay += sc_time(10, SC_NS);
        t.set_response_status(tlm::TLM_OK_RESPONSE);
    }
};

// ------------------------------------------------------------------ bus
struct Bus : sc_core::sc_module {
    tlm_utils::simple_target_socket<Bus> cpu{"cpu"};
    tlm_utils::simple_initiator_socket<Bus> rom{"rom"}, ram{"ram"}, pmu{"pmu"}, timer{"timer"}, gpio{"gpio"},
        uart{"uart"};
    sc_power::power_model power;

    Bus(sc_core::sc_module_name n, sc_power::power_domain &aon) : sc_module(n), power("power", aon) {
        cpu.register_b_transport(this, &Bus::b_transport);
        power.add_state("on", 0.05e-3).add_operation("access", 2e-12, 0.2e-12);
    }
    void b_transport(tlm::tlm_generic_payload &t, sc_time &delay) {
        sc_power::charge(power, t, delay);
        delay += sc_time(5, SC_NS);
        uint32_t a = uint32_t(t.get_address());
        uint32_t base = a & 0xfffff000u;
        tlm_utils::simple_initiator_socket<Bus> *to = nullptr;
        if (a < soc::ROM + 0x1000) to = &rom, base = soc::ROM;
        else if (a >= soc::RAM && a < soc::RAM + 0x1000) to = &ram, base = soc::RAM;
        else if (base == soc::PMU) to = &pmu;
        else if (base == soc::TIMER) to = &timer;
        else if (base == soc::GPIO) to = &gpio;
        else if (base == soc::UART) to = &uart;
        if (!to) return t.set_response_status(tlm::TLM_ADDRESS_ERROR_RESPONSE);
        t.set_address(a - base);
        (*to)->b_transport(t, delay);
        t.set_address(a);
    }
};

// ------------------------------------------------------------------ interrupt pulse helper
// A peripheral raises an interrupt by pulsing its line high for 10 ns. One method per
// peripheral drives the line (a signal has a single writer): drive() on fire or clear.
struct IrqLine {
    sc_core::sc_out<bool> &out;
    sc_core::sc_event fire{"irq_fire"}, clear{"irq_clear"};
    explicit IrqLine(sc_core::sc_out<bool> &o) : out(o) {}
    void pulse() { fire.notify(sc_core::SC_ZERO_TIME); }
    void drive() {
        if (fire.triggered()) {
            out.write(true);
            clear.notify(sc_time(10, SC_NS));
        } else {
            out.write(false);
        }
    }
};

// ------------------------------------------------------------------ PMU
// Registers (32-bit):
//   0x00 IRQ_STATUS   bit0 timer, bit1 button; write 1 to clear
//   0x04 IRQ_ENABLE
//   0x08 SLEEP_MODE   0 = WFI idles (clock gated), 1 = WFI deep-sleeps (retention)
//   0x0C CPU_OP       0 = fast (1.0 V, 100 MHz), 1 = slow (0.7 V, 25 MHz)
//   0x10 IO_POWER     1 = io domain (UART) on; switching on takes 1 us to settle
struct Pmu : sc_core::sc_module {
    tlm_utils::simple_target_socket<Pmu> socket{"socket"};
    sc_core::sc_in<bool> timer_irq{"timer_irq"}, button_irq{"button_irq"};
    sc_core::sc_out<bool> irq{"irq"}, deep_sleep{"deep_sleep"};
    sc_power::power_model power;
    sc_power::power_domain &core, &io;

    Pmu(sc_core::sc_module_name n, sc_power::power_domain &aon, sc_power::power_domain &core_, sc_power::power_domain &io_)
        : sc_module(n), power("power", aon), core(core_), io(io_) {
        socket.register_b_transport(this, &Pmu::b_transport);
        power.add_state("on", 0.01e-3).add_operation("access", 1e-12);
        SC_METHOD(on_timer);
        sensitive << timer_irq.pos();
        dont_initialize();
        SC_METHOD(on_button);
        sensitive << button_irq.pos();
        dont_initialize();
        SC_METHOD(update_irq);
        sensitive << m_changed;
    }

private:
    uint32_t m_status = 0, m_enable = 0, m_sleep = 0, m_op = 0, m_io = 0;
    sc_core::sc_event m_changed{"changed"};

    void on_timer() { m_status |= 1; m_changed.notify(); }
    void on_button() { m_status |= 2; m_changed.notify(); }
    void update_irq() {
        irq.write((m_status & m_enable) != 0);
        deep_sleep.write(m_sleep != 0);
    }

    void b_transport(tlm::tlm_generic_payload &t, sc_time &delay) {
        if (!sc_power::charge(power, t, delay)) return;
        uint32_t v = 0;
        switch (t.get_address()) {
        case 0x00: v = m_status; break;
        case 0x04: v = m_enable; break;
        case 0x08: v = m_sleep; break;
        case 0x0c: v = m_op; break;
        case 0x10: v = m_io; break;
        default: return t.set_response_status(tlm::TLM_ADDRESS_ERROR_RESPONSE);
        }
        if (!reg_access(t, v)) return;
        if (t.is_read()) return;
        switch (t.get_address()) {
        case 0x00: m_status &= ~v; break;
        case 0x04: m_enable = v; break;
        case 0x08: m_sleep = v & 1; break;
        case 0x0c:
            m_op = v & 1;
            core.set_operating_point(m_op ? "slow" : "fast");
            break;
        case 0x10:
            m_io = v & 1;
            if (m_io && !io.is_on()) {
                io.power_on();
                delay += sc_time(1, SC_US);   // supply settles before the write completes
            } else if (!m_io && io.is_on()) {
                io.power_off();
            }
            break;
        }
        m_changed.notify(delay);
    }
};

// ------------------------------------------------------------------ timer
// 0x00 PERIOD_US, 0x04 ENABLE. Pulses its interrupt every PERIOD_US microseconds.
struct Timer : sc_core::sc_module {
    tlm_utils::simple_target_socket<Timer> socket{"socket"};
    sc_core::sc_out<bool> irq{"irq"};
    sc_power::power_model power;

    Timer(sc_core::sc_module_name n, sc_power::power_domain &aon) : sc_module(n), power("power", aon) {
        socket.register_b_transport(this, &Timer::b_transport);
        power.add_state("on", 0.02e-3).add_operation("access", 1e-12);
        SC_THREAD(tick);
        SC_METHOD(irq_driver);
        sensitive << m_line.fire << m_line.clear;
        dont_initialize();
    }

private:
    uint32_t m_period = 0, m_enable = 0;
    sc_core::sc_event m_cfg{"cfg"};
    IrqLine m_line{irq};

    void irq_driver() { m_line.drive(); }
    void tick() {
        for (;;) {
            if (!m_enable || !m_period) {
                wait(m_cfg);
                continue;
            }
            wait(sc_time(m_period, SC_US), m_cfg);
            if (!m_cfg.triggered() && m_enable) m_line.pulse();
        }
    }
    void b_transport(tlm::tlm_generic_payload &t, sc_time &delay) {
        if (!sc_power::charge(power, t, delay)) return;
        uint32_t &r = t.get_address() == 0 ? m_period : m_enable;
        if (t.get_address() > 4) return t.set_response_status(tlm::TLM_ADDRESS_ERROR_RESPONSE);
        if (reg_access(t, r) && t.is_write()) m_cfg.notify(delay);
    }
};

// ------------------------------------------------------------------ GPIO
// 0x00 SENSOR (read: the 8-bit sensor input), 0x04 LED (read/write bit 0).
// A rising edge on the button input raises the button interrupt.
struct Gpio : sc_core::sc_module {
    tlm_utils::simple_target_socket<Gpio> socket{"socket"};
    sc_core::sc_in<int> sensor{"sensor"};
    sc_core::sc_in<bool> button{"button"};
    sc_core::sc_out<bool> led{"led"};
    sc_core::sc_out<bool> irq{"irq"};
    sc_power::power_model power;

    Gpio(sc_core::sc_module_name n, sc_power::power_domain &aon) : sc_module(n), power("power", aon) {
        socket.register_b_transport(this, &Gpio::b_transport);
        power.add_state("on", 0.01e-3).add_operation("access", 1e-12);
        SC_METHOD(on_button);
        sensitive << button.pos();
        dont_initialize();
        SC_METHOD(irq_driver);
        sensitive << m_line.fire << m_line.clear;
        dont_initialize();
    }

private:
    uint32_t m_led = 0;
    IrqLine m_line{irq};
    void on_button() { m_line.pulse(); }
    void irq_driver() { m_line.drive(); }
    void b_transport(tlm::tlm_generic_payload &t, sc_time &delay) {
        if (!sc_power::charge(power, t, delay)) return;
        if (t.get_address() == 0) {
            uint32_t v = uint32_t(sensor.read()) & 0xff;
            if (t.is_write()) return t.set_response_status(tlm::TLM_COMMAND_ERROR_RESPONSE);
            reg_access(t, v);
        } else if (t.get_address() == 4) {
            if (reg_access(t, m_led) && t.is_write()) led.write((m_led & 1) != 0);
        } else {
            t.set_response_status(tlm::TLM_ADDRESS_ERROR_RESPONSE);
        }
    }
};

// ------------------------------------------------------------------ UART
// 0x00 TX (write a byte; 10 bit times at 1 Mbaud = 10 us), 0x04 STATUS (bit 0 busy).
// States: "idle" (powered, waiting) and "tx" (shifting a byte out).
struct Uart : sc_core::sc_module {
    tlm_utils::simple_target_socket<Uart> socket{"socket"};
    sc_core::sc_out<int> tx_count{"tx_count"}, tx_last{"tx_last"};
    sc_power::power_model power;
    bool quiet = false;

    Uart(sc_core::sc_module_name n, sc_power::power_domain &io) : sc_module(n), power("power", io) {
        socket.register_b_transport(this, &Uart::b_transport);
        power.add_state("idle", 0.2e-3).add_state("tx", 0.2e-3, 1.3e-3).add_operation("write", 5e-12).add_operation("read", 2e-12);
        SC_METHOD(on_done);
        sensitive << m_done;
        dont_initialize();
    }

private:
    bool m_busy = false;
    int m_count = 0;
    sc_core::sc_event m_done{"done"};
    void on_done() {
        m_busy = false;
        power.set_state("idle");
    }
    void b_transport(tlm::tlm_generic_payload &t, sc_time &delay) {
        if (!sc_power::charge(power, t, delay)) return;
        uint32_t v = 0;
        if (t.get_address() == 4) {
            v = m_busy ? 1 : 0;
            if (t.is_write()) return t.set_response_status(tlm::TLM_COMMAND_ERROR_RESPONSE);
            reg_access(t, v);
            return;
        }
        if (t.get_address() != 0 || !t.is_write()) return t.set_response_status(tlm::TLM_COMMAND_ERROR_RESPONSE);
        if (!reg_access(t, v)) return;
        m_busy = true;
        power.set_state("tx");
        m_done.notify(delay + sc_time(10, SC_US));
        tx_count.write(++m_count);
        tx_last.write(int(v & 0xff));
        if (!quiet)
            std::printf("%9s  uart  0x%02x (%u)\n", (sc_core::sc_time_stamp() + delay).to_string().c_str(), v & 0xff, v & 0xff);
    }
};

#endif
soc_firmware.h 129 lines
// Power-aware SoC teaching example: the firmware, and a two-pass assembler for tiny16.
//
// What the firmware does (the "application"):
//   * every 50 us the timer wakes the core, which reads the sensor and keeps it in RAM;
//   * every 4th sample it sends the sum of the last four over the UART;
//   * a button press toggles the LED.
//
// How it manages power (the "power policy", chosen at build time):
//   wfi    yes: WFI between events;      no: busy-poll the PMU status register
//   sleep  deep: WFI = retention sleep;  idle: WFI = clock gating only
//   cpu    slow: 0.7 V / 25 MHz;         fast: 1.0 V / 100 MHz
//   io     gated: UART powered only while sending;  on: UART always powered
//
// The application must behave identically under every policy - only time and energy
// may change. verify_power.icpp checks exactly that.
#ifndef POWER_SOC_FIRMWARE_H
#define POWER_SOC_FIRMWARE_H

#include "soc_core.h"

#include <map>
#include <stdexcept>
#include <string>
#include <vector>

struct Policy {
    bool wfi = true;
    bool deep_sleep = true;
    bool slow_cpu = true;
    bool gate_io = true;
    std::string name() const {
        return std::string(wfi ? "wfi" : "poll") + (wfi ? (deep_sleep ? "+deep" : "+idle") : "") +
               (slow_cpu ? "+slow" : "+fast") + (gate_io ? "+gated-io" : "+io-on");
    }
};

class Asm {
public:
    void label(const std::string &l) { m_labels[l] = int(m_code.size()); }
    void op(tiny16::op o, unsigned rd = 0, unsigned rs = 0, int imm = 0) { m_code.push_back(tiny16::enc(o, rd, rs, imm)); }
    void branch(tiny16::op o, unsigned rd, unsigned rs, const std::string &target) {
        m_fix.push_back({int(m_code.size()), target});
        op(o, rd, rs, 0);
    }
    std::vector<uint32_t> link() {
        for (auto &f : m_fix) {
            auto it = m_labels.find(f.second);
            if (it == m_labels.end()) throw std::runtime_error("undefined label " + f.second);
            m_code[f.first] = (m_code[f.first] & 0xffff0000u) | uint32_t(it->second);
        }
        return m_code;
    }

private:
    std::vector<uint32_t> m_code;
    std::map<std::string, int> m_labels;
    std::vector<std::pair<int, std::string>> m_fix;
};

inline std::vector<uint32_t> build_firmware(const Policy &p, unsigned period_us = 50) {
    using namespace tiny16;
    Asm a;
    // r1 PMU  r2 TIMER  r3 GPIO  r4 UART  r11 RAM  r6 sample count  r7 running sum
    a.op(LUI, 1, 0, 0x4000);
    a.op(ADDI, 2, 1, 0x1000);
    a.op(ADDI, 3, 1, 0x2000);
    a.op(ADDI, 4, 1, 0x3000);
    a.op(LUI, 11, 0, 0x1000);
    a.op(LI, 5, 0, int(period_us));
    a.op(SW, 5, 2, 0x0);                 // TIMER.PERIOD_US
    a.op(LI, 5, 0, 1);
    a.op(SW, 5, 2, 0x4);                 // TIMER.ENABLE
    a.op(LI, 5, 0, 3);
    a.op(SW, 5, 1, 0x4);                 // PMU.IRQ_ENABLE = timer | button
    a.op(LI, 5, 0, p.deep_sleep ? 1 : 0);
    a.op(SW, 5, 1, 0x8);                 // PMU.SLEEP_MODE
    a.op(LI, 5, 0, p.slow_cpu ? 1 : 0);
    a.op(SW, 5, 1, 0xc);                 // PMU.CPU_OP
    a.op(LI, 5, 0, p.gate_io ? 0 : 1);
    a.op(SW, 5, 1, 0x10);                // PMU.IO_POWER (always on unless gated)
    a.op(LI, 6, 0, 0);
    a.op(LI, 7, 0, 0);

    a.label("loop");
    if (p.wfi) {
        a.op(WFI);
        a.op(LW, 8, 1, 0x0);             // PMU.IRQ_STATUS
    } else {
        a.label("poll_irq");             // no WFI: spin on the status register
        a.op(LW, 8, 1, 0x0);
        a.branch(BEQ, 8, 0, "poll_irq");
    }
    a.op(SW, 8, 1, 0x0);                 // acknowledge what we saw

    a.op(ANDI, 9, 8, 2);                 // button?
    a.branch(BEQ, 9, 0, "timer_check");
    a.op(LW, 10, 3, 0x4);
    a.op(XORI, 10, 10, 1);
    a.op(SW, 10, 3, 0x4);                // toggle the LED

    a.label("timer_check");
    a.op(ANDI, 9, 8, 1);                 // timer?
    a.branch(BEQ, 9, 0, "loop");
    a.op(LW, 10, 3, 0x0);                // GPIO.SENSOR
    a.op(ANDI, 12, 6, 3);                // RAM[(count & 3) * 4] = sample
    a.op(ADD, 12, 12);
    a.op(ADD, 12, 12);
    a.op(ADD, 12, 11);
    a.op(SW, 10, 12, 0);
    a.op(ADD, 7, 10);                    // sum += sample
    a.op(ADDI, 6, 6, 1);
    a.op(ANDI, 9, 6, 3);
    a.branch(BNE, 9, 0, "loop");         // report every 4th sample

    if (p.gate_io) {
        a.op(LI, 10, 0, 1);
        a.op(SW, 10, 1, 0x10);           // PMU.IO_POWER = 1 (waits for the supply to settle)
    }
    a.op(SW, 7, 4, 0x0);                 // UART.TX = sum
    a.label("tx_wait");
    a.op(LW, 10, 4, 0x4);                // UART.STATUS
    a.branch(BNE, 10, 0, "tx_wait");
    if (p.gate_io) a.op(SW, 0, 1, 0x10); // PMU.IO_POWER = 0
    a.op(LI, 7, 0, 0);
    a.branch(J, 0, 0, "loop");
    return a.link();
}

#endif
tb_power_soc.sv 103 lines
// TSystemC teaching project: trsim testbench for the power-aware SoC.
//
// The SoC is the SystemC/TLM model in power_soc.cpp, loaded through tsystemc.dll
// (DPI-C) in hosted mode: this testbench owns the pins and steps SystemC time.
//
//   set TUWASIM_DPI_LIBS=C:\tools\tsystemc-1.0.1\bin\tsystemc.dll
//   trsim -top tb_power_soc tb_power_soc.sv          (run from this directory)
//
// It checks the application (UART reports, LED) and the power budget (average power,
// time the core spends asleep) with the integer probes the model exposes.
module tb_power_soc;
  import "DPI-C" function int     sysc_dpi_open(input string model, input string args);
  import "DPI-C" function int     sysc_dpi_run_ps(input int h, input longint ps);
  import "DPI-C" function longint sysc_dpi_get_int(input int h, input string name);
  import "DPI-C" function int     sysc_dpi_set_int(input int h, input string name, input longint v);
  import "DPI-C" function longint sysc_dpi_time_ps(input int h);
  import "DPI-C" function int     sysc_dpi_close(input int h);

  // Power budget for this firmware and policy (wfi + deep sleep + slow core + gated UART).
  localparam int BUDGET_UW       = 500;   // average power, microwatts
  localparam int MIN_ASLEEP_PCT  = 80;    // the core must sleep at least this much

  int h, r, errors, checks;
  longint v;

  task run_us(input int us);
    r = sysc_dpi_run_ps(h, us * 64'd1000000);
  endtask

  task expect_eq(input string name, input longint want);
    v = sysc_dpi_get_int(h, name);
    checks = checks + 1;
    if (v == want) $display("  ok    %0s = %0d", name, v);
    else begin
      $display("  FAIL  %0s = %0d, expected %0d (at %0d ns)", name, v, want, sysc_dpi_time_ps(h) / 1000);
      errors = errors + 1;
    end
  endtask

  task expect_le(input string name, input longint limit);
    v = sysc_dpi_get_int(h, name);
    checks = checks + 1;
    if (v <= limit) $display("  ok    %0s = %0d (limit %0d)", name, v, limit);
    else begin
      $display("  FAIL  %0s = %0d, above the limit %0d", name, v, limit);
      errors = errors + 1;
    end
  endtask

  task expect_ge(input string name, input longint limit);
    v = sysc_dpi_get_int(h, name);
    checks = checks + 1;
    if (v >= limit) $display("  ok    %0s = %0d (at least %0d)", name, v, limit);
    else begin
      $display("  FAIL  %0s = %0d, below %0d", name, v, limit);
      errors = errors + 1;
    end
  endtask

  task press_button;
    r = sysc_dpi_set_int(h, "button", 1);
    run_us(2);
    r = sysc_dpi_set_int(h, "button", 0);
  endtask

  initial begin
    errors = 0;
    checks = 0;
    h = sysc_dpi_open("power_soc.cpp", "hosted quiet");
    if (h <= 0) begin
      $display("FAIL: cannot open power_soc.cpp");
      $finish;
    end

    $display("1. sensor = 10: every 4th sample (200 us) the UART reports the sum");
    r = sysc_dpi_set_int(h, "sensor", 10);
    run_us(410);
    expect_eq("uart_count", 2);
    expect_eq("uart_last", 40);
    expect_eq("led", 0);

    $display("2. a button press toggles the LED");
    press_button();
    run_us(20);
    expect_eq("led", 1);

    $display("3. sensor = 7: the next reports follow");
    r = sysc_dpi_set_int(h, "sensor", 7);
    run_us(400);
    expect_eq("uart_count", 4);
    expect_eq("uart_last", 28);

    $display("4. power budget");
    expect_le("avg_power_uw", BUDGET_UW);
    expect_ge("cpu_asleep_pct", MIN_ASLEEP_PCT);
    expect_ge("energy_nj", 1);

    r = sysc_dpi_close(h);
    if (errors == 0) $display("PASS  tb_power_soc: %0d checks", checks);
    else $display("FAIL  tb_power_soc: %0d of %0d checks failed", errors, checks);
    $finish;
  end
endmodule
verify_power.icpp 116 lines
// TSystemC teaching project: power-policy verification in ICPP.
//
// Drives the power-aware SoC (power_soc.cpp) through `tsystemc mcp` under every power
// policy, with the same stimulus each time, and checks three things:
//
//   1. Power management must not change the application: every policy sends the same
//      UART bytes and leaves the LED in the same state.
//   2. The recommended policy (wfi + deep sleep + slow core + gated UART) uses the
//      least energy of all of them.
//   3. It meets the power budget: average power at most BUDGET_UW microwatts.
//
//   set TSYSTEMC_EXE=C:/tools/tsystemc-1.0.1/bin/tsystemc.exe    (optional)
//   kcpp -icpp verify_power.icpp                                  (run from this directory)
//
// Needs kcpp from KCPP_ICPP_x64_2026-09-30 or later (string $variables).
//
// Exit code: the number of failed checks.

// The value of a model object, from "name = value".
char *get_text(int h, char *name)
{
    $r = icpp_mcp_call(h, "get", icpp_concat("{'name':'", name, "'}"));
    $p = icpp_strstr($r, " = ");
    if ($p < 0) return "";
    return icpp_trim(icpp_substr($r, $p + 3, 200));
}

int get_int(int h, char *name)
{
    return icpp_atoi(get_text(h, name));
}

int main()
{
    int BUDGET_UW;
    int h;
    int n;
    int i;
    int errors;
    int ref_count;
    int ref_last;
    int ref_led;
    int best;
    int worst;
    BUDGET_UW = 500;
    errors = 0;

    $exe = icpp_getenv("TSYSTEMC_EXE");
    if (icpp_strlen($exe) == 0) {
        $exe = "C:/tools/tsystemc-1.0.1/bin/tsystemc.exe";
    }
    h = icpp_mcp_connect(icpp_concat($exe, " mcp"));
    if (h == 0) {
        printf("cannot start %s mcp\n", $exe);
        return 1;
    }

    // The recommended policy first: it is the reference for the functional check.
    $policies = icpp_split("wfi=yes sleep=deep cpu=slow io=gated|wfi=yes sleep=deep cpu=slow io=on|wfi=yes sleep=deep cpu=fast io=gated|wfi=yes sleep=idle cpu=slow io=gated|wfi=yes sleep=idle cpu=fast io=on|wfi=no cpu=fast io=on", "|");
    n = icpp_list_count($policies);
    // Same stimulus for every run: 2 ms, sensor 10 then 5, two button presses.
    $stimulus = "set sensor 10; run 330 us; set button 1; run 2 us; set button 0; run 678 us; set sensor 5; run 320 us; set button 1; run 2 us; set button 0; run 668 us";

    printf("%-40s %6s %6s %4s %10s %10s %7s\n", "policy", "bytes", "last", "led", "energy nJ", "avg uW", "asleep");
    best = 0;
    worst = 0;
    $energies = icpp_list_new();
    for (i = 0; i < n; i = i + 1) {
        $pol = icpp_list_get($policies, i);
        $r = icpp_mcp_call(h, "open", icpp_concat("{'model':'power_soc.cpp','args':'hosted quiet ", $pol, "'}"));
        if (icpp_strstr($r, "opened") < 0) {
            printf("cannot open the model: %s\n", $r);
            return 1;
        }
        $r = icpp_mcp_call(h, "script", icpp_concat("{'commands':'", $stimulus, "'}"));
        $count = get_int(h, "uart_count");
        $last = get_int(h, "uart_last");
        $led = get_int(h, "led");
        $energy = get_int(h, "energy_nj");
        $avg = get_int(h, "avg_power_uw");
        $asleep = get_int(h, "cpu_asleep_pct");
        printf("%-40s %6d %6d %4d %10d %10d %6d%%\n", $pol, $count, $last, $led, $energy, $avg, $asleep);
        icpp_list_add($energies, icpp_concat("", $energy));

        if (i == 0) {
            ref_count = $count;
            ref_last = $last;
            ref_led = $led;
            best = $energy;
            worst = $energy;
            if ($avg > BUDGET_UW) {
                printf("  FAIL  recommended policy: %d uW is over the %d uW budget\n", $avg, BUDGET_UW);
                errors = errors + 1;
            }
            $report = icpp_mcp_call(h, "power", "{}");
        } else {
            if ($count != ref_count || $last != ref_last || $led != ref_led) {
                printf("  FAIL  %s changes the application: bytes %d last %d led %d\n", $pol, $count, $last, $led);
                errors = errors + 1;
            }
            if ($energy < best) {
                printf("  FAIL  %s uses less energy than the recommended policy\n", $pol);
                errors = errors + 1;
            }
            if ($energy > worst) worst = $energy;
        }
        $r = icpp_mcp_call(h, "close", "{}");
    }

    printf("\nrecommended policy: %d nJ, %d%% less energy than the worst (%d nJ)\n", best, (worst - best) * 100 / worst, worst);
    printf("\nits power report:\n%s", $report);
    icpp_mcp_disconnect(h);
    if (errors == 0) printf("PASS  verify_power: %d policies, function identical, recommended policy cheapest and within %d uW\n", n, BUDGET_UW);
    else printf("FAIL  verify_power: %d check(s) failed\n", errors);
    return errors;
}
run_all.ps1 45 lines
# TSystemC teaching project: run the power-aware SoC three ways.
#
#   powershell -ExecutionPolicy Bypass -File run_all.ps1 [-TSystemC <install dir>] [-Trsim <trsim.exe>] [-Kcpp <kcpp.exe>]
#
# 1. standalone (tsystemc run), compared with power_soc.golden
# 2. trsim testbench through DPI-C (skipped if trsim is not found)
# 3. ICPP policy verification through MCP (skipped if kcpp is not found)
#
# Needs trsim 1.1.1 or later and kcpp from KCPP_ICPP_x64_2026-09-30 or later.
param(
    [string]$TSystemC = (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent),
    [string]$Trsim = "trsim.exe",
    [string]$Kcpp = "kcpp.exe"
)
$ErrorActionPreference = 'Continue'
Set-Location $PSScriptRoot
$exe = Join-Path $TSystemC "bin\tsystemc.exe"
$dll = Join-Path $TSystemC "bin\tsystemc.dll"
if (-not (Test-Path $exe)) { Write-Host "tsystemc.exe not found under $TSystemC (use -TSystemC)"; exit 1 }
$failed = 0

Write-Host "== 1. standalone run"
$out = & $exe run power_soc.cpp 2>&1 | Out-String
$golden = Get-Content power_soc.golden -Raw
if (($out -replace "`r", "").Trim() -eq ($golden -replace "`r", "").Trim()) { Write-Host "  ok    output matches power_soc.golden" }
else { Write-Host "  FAIL  output differs from power_soc.golden"; $out | Set-Content run_standalone.txt; $failed++ }

Write-Host "== 2. trsim testbench (DPI-C)"
if (Get-Command $Trsim -ErrorAction SilentlyContinue) {
    $env:TUWASIM_DPI_LIBS = $dll
    $t = & $Trsim -top tb_power_soc tb_power_soc.sv 2>&1 | Out-String
    $t -split "`n" | Where-Object { $_ -match '^\s*(ok|FAIL|PASS|\d\.)' } | ForEach-Object { Write-Host "  $($_.TrimEnd())" }
    if ($t -notmatch 'PASS  tb_power_soc') { $failed++ }
} else { Write-Host "  skip  trsim not found (use -Trsim)" }

Write-Host "== 3. ICPP policy verification (MCP)"
if (Get-Command $Kcpp -ErrorAction SilentlyContinue) {
    $env:TSYSTEMC_EXE = $exe -replace '\\', '/'
    $v = & $Kcpp -icpp verify_power.icpp 2>&1 | Out-String
    $v -split "`n" | Where-Object { $_ -match '^(policy|wfi=|recommended|PASS|FAIL|  FAIL)' } | ForEach-Object { Write-Host "  $($_.TrimEnd())" }
    if ($v -notmatch 'PASS  verify_power') { $failed++ }
} else { Write-Host "  skip  kcpp not found (use -Kcpp)" }

if ($failed -eq 0) { Write-Host "ALL PASSED" } else { Write-Host "$failed flow(s) FAILED" }
exit $failed
power_soc.golden 37 lines
policy wfi+deep+slow+gated-io
204600 ns  uart  0x28 (40)
404600 ns  uart  0x28 (40)
604600 ns  uart  0x28 (40)
804600 ns  uart  0x28 (40)
1004600 ns  uart  0x28 (40)
1204600 ns  uart  0x14 (20)
1404600 ns  uart  0x14 (20)
1604600 ns  uart  0x14 (20)
1804600 ns  uart  0x14 (20)

uart bytes 9, last 20, led 0, instructions 1692, bus errors 0
energy 900 nJ, average power 453 uW, core asleep 88 %

Power report at 2 ms
model           state  power       avg power   energy      static      dynamic     transitions 
soc.cpu.power   sleep  14 uW       153.1 uW    306.3 nJ    74.65 nJ    128.6 nJ    103 nJ (83)
soc.rom.power   on     35 uW       37.93 uW    75.87 nJ    70.01 nJ    5.861 nJ    0 J (0)
soc.ram.power   on     70 uW       70.12 uW    140.2 nJ    140 nJ      237 pJ      0 J (0)
soc.bus.power   on     50 uW       53.3 uW     106.6 nJ    100 nJ      6.602 nJ    0 J (0)
soc.pmu.power   on     10 uW       10.05 uW    20.1 nJ     20 nJ       104 pJ      0 J (0)
soc.timer.power on     20 uW       20 uW       40 nJ       40 nJ       2 pJ        0 J (0)
soc.gpio.power  on     10 uW       10.02 uW    20.04 nJ    20 nJ       43 pJ       0 J (0)
soc.uart.power  idle*  0 W         69.23 uW    138.5 nJ    20.41 nJ    118 nJ      0 J (18)
total                  209 uW      451.3 uW    902.6 nJ  (domain switching 55 nJ)
* the model's power domain is off
  soc.cpu.power: run 11.9% idle 0.0% sleep 88.1%  alu x430  mem x665  branch x598
  soc.rom.power: on 100.0%  read x1693
  soc.ram.power: on 100.0%  read x0  write x39
  soc.bus.power: on 100.0%  access x2358
  soc.pmu.power: on 100.0%  access x104
  soc.timer.power: on 100.0%  access x2
  soc.gpio.power: on 100.0%  access x43
  soc.uart.power: idle 95.5% tx 4.5%  write x9  read x468
  domain aon: on 1 V op=nominal power=90 uW energy=186.8 nJ
  domain core_dom: on 700 mV 25 MHz op=slow power=119 uW energy=522.4 nJ
  domain io_dom: off 1 V op=nominal power=0 W energy=193.5 nJ switches=19

← TSystemC