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.
| File | What it is |
|---|---|
power_soc.cpp | Top level: power domains, the SoC, stimulus, power probes, policy switches |
soc_core.h | tiny16, a 16-register core with a 15-instruction ISA, fetching from ROM over the bus |
soc_periph.h | ROM, RAM, bus, PMU (power management unit and interrupt controller), timer, GPIO, UART |
soc_firmware.h | The firmware, written with a 40-line assembler, and its power policies |
tb_power_soc.sv | trsim testbench: drives the SoC through DPI-C, checks function and the power budget |
verify_power.icpp | ICPP script: runs every power policy through MCP and compares them |
power_soc.golden | Expected output of the standalone run |
run_all.ps1 | Runs 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 |
+-------------------------------+
| Address | Block | Domain | Registers |
|---|---|---|---|
0x0000_0000 | ROM 4 KB | core | firmware |
0x1000_0000 | RAM 4 KB | core | data |
0x4000_0000 | PMU | aon | IRQ_STATUS (W1C), IRQ_ENABLE, SLEEP_MODE, CPU_OP, IO_POWER |
0x4000_1000 | Timer | aon | PERIOD_US, ENABLE |
0x4000_2000 | GPIO | aon | SENSOR (in), LED (out); button edge interrupt |
0x4000_3000 | UART | io | TX, 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:
- Always on (
aon). The PMU, timer and GPIO must stay powered: they are what wakes the system up. - Voltage and frequency scaling (
core). The core and its memories are the biggest consumers when running. - Power gating (
io). The UART is needed for 10 µs every 200 µs. Gating it removes its leakage for the other 95 %, at a switching cost of 6 nJ per report.
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:
| Switch | Choices | What it changes |
|---|---|---|
wfi | yes / no | WFI between events, or busy-poll the PMU |
sleep | deep / idle | WFI enters retention (2 µs wake-up) or only gates the clock |
cpu | slow / fast | core domain at 0.7 V / 25 MHz or 1.0 V / 100 MHz |
io | gated / on | UART 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:
- the integer probes
avg_power_uw,cpu_asleep_pctandenergy_nj, refreshed by the model every 10 µs; - an average power of 500 µW or less;
- at least 80 % of the time asleep.
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:
- Power management does not change the application. Every policy sends the same bytes and leaves the LED the same.
- The recommended policy uses the least energy.
- 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
- WFI is the first and largest saving. Polling keeps the core and the bus busy all the time: 10.9 µJ against 2.1 µJ for the same work.
- Deep sleep beats clock gating here, but not for free. Each wake-up costs 2 µs and 2.5 nJ; with a 50 µs period that is worth it (900 against 1101 nJ). Shorten the timer period and the balance shifts.
- "Slow" beats "fast" — because of the busy-wait. At 100 MHz the UART status loop spins four times as often (3168 instructions against 1692). The time is spent in a poll, not in useful work, so a faster clock only adds energy.
- Power gating pays when the duty cycle is low. The UART is busy 4.5 % of the time. Gating it saves 321 nJ over the run, after paying 55 nJ for the switching.
- Leakage of always-on logic sets the floor. With the core asleep,
aonplus the memories still draw about 200 µW. Look at thestaticcolumn.
7. Exercises
- 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. Doescpu=faststill lose? - 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? - Forget to power on. Remove the
IO_POWER = 1write from the firmware. Run it and read thetiny16bus-error warnings. This is the bug class thecharge()error response exists to catch. - 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? - 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? - A governor. Make the firmware choose
CPU_OPitself: 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;
}
}
};
#endifsoc_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);
}
};
#endifsoc_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();
}
#endiftb_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
endmoduleverify_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 $failedpower_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