dev, arm: Refactor and clean up the generic timer model
This changeset cleans up the generic timer a bit and moves most of the register juggling from the ISA code into a separate class in the same source file as the rest of the generic timer. It also removes the assumption that there is always 8 or fewer CPUs in the system. Instead of having a fixed limit, we now instantiate per-core timers as they are requested. This is all in preparation for other patches that add support for virtual timers and a memory mapped interface.
This commit is contained in:
parent
5435f25ec8
commit
65f3f097d3
7 changed files with 539 additions and 324 deletions
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2014 ARM Limited
|
||||
* Copyright (c) 2010-2015 ARM Limited
|
||||
* All rights reserved
|
||||
*
|
||||
* The license below extends only to copyright in the software and shall
|
||||
|
@ -45,6 +45,7 @@
|
|||
#include "cpu/base.hh"
|
||||
#include "debug/Arm.hh"
|
||||
#include "debug/MiscRegs.hh"
|
||||
#include "dev/arm/generic_timer.hh"
|
||||
#include "params/ArmISA.hh"
|
||||
#include "sim/faults.hh"
|
||||
#include "sim/stat_control.hh"
|
||||
|
@ -730,52 +731,14 @@ ISA::readMiscReg(int misc_reg, ThreadContext *tc)
|
|||
return readMiscRegNoEffect(MISCREG_SCR_EL3);
|
||||
}
|
||||
}
|
||||
|
||||
// Generic Timer registers
|
||||
case MISCREG_CNTFRQ:
|
||||
case MISCREG_CNTFRQ_EL0:
|
||||
inform_once("Read CNTFREQ_EL0 frequency\n");
|
||||
return getSystemCounter(tc)->freq();
|
||||
case MISCREG_CNTPCT:
|
||||
case MISCREG_CNTPCT_EL0:
|
||||
return getSystemCounter(tc)->value();
|
||||
case MISCREG_CNTVCT:
|
||||
return getSystemCounter(tc)->value();
|
||||
case MISCREG_CNTVCT_EL0:
|
||||
return getSystemCounter(tc)->value();
|
||||
case MISCREG_CNTP_CVAL:
|
||||
case MISCREG_CNTP_CVAL_EL0:
|
||||
return getArchTimer(tc, tc->cpuId())->compareValue();
|
||||
case MISCREG_CNTP_TVAL:
|
||||
case MISCREG_CNTP_TVAL_EL0:
|
||||
return getArchTimer(tc, tc->cpuId())->timerValue();
|
||||
case MISCREG_CNTP_CTL:
|
||||
case MISCREG_CNTP_CTL_EL0:
|
||||
return getArchTimer(tc, tc->cpuId())->control();
|
||||
// PL1 phys. timer, secure
|
||||
// AArch64
|
||||
// case MISCREG_CNTPS_CVAL_EL1:
|
||||
// case MISCREG_CNTPS_TVAL_EL1:
|
||||
// case MISCREG_CNTPS_CTL_EL1:
|
||||
// PL2 phys. timer, non-secure
|
||||
// AArch32
|
||||
// case MISCREG_CNTHCTL:
|
||||
// case MISCREG_CNTHP_CVAL:
|
||||
// case MISCREG_CNTHP_TVAL:
|
||||
// case MISCREG_CNTHP_CTL:
|
||||
// AArch64
|
||||
// case MISCREG_CNTHCTL_EL2:
|
||||
// case MISCREG_CNTHP_CVAL_EL2:
|
||||
// case MISCREG_CNTHP_TVAL_EL2:
|
||||
// case MISCREG_CNTHP_CTL_EL2:
|
||||
// Virtual timer
|
||||
// AArch32
|
||||
// case MISCREG_CNTV_CVAL:
|
||||
// case MISCREG_CNTV_TVAL:
|
||||
// case MISCREG_CNTV_CTL:
|
||||
// AArch64
|
||||
// case MISCREG_CNTV_CVAL_EL2:
|
||||
// case MISCREG_CNTV_TVAL_EL2:
|
||||
// case MISCREG_CNTV_CTL_EL2:
|
||||
case MISCREG_CNTFRQ ... MISCREG_CNTHP_CTL:
|
||||
case MISCREG_CNTPCT ... MISCREG_CNTHP_CVAL:
|
||||
case MISCREG_CNTKCTL_EL1 ... MISCREG_CNTV_CVAL_EL0:
|
||||
case MISCREG_CNTVOFF_EL2 ... MISCREG_CNTPS_CVAL_EL1:
|
||||
return getGenericTimer(tc).readMiscReg(misc_reg);
|
||||
|
||||
default:
|
||||
break;
|
||||
|
||||
|
@ -1853,47 +1816,11 @@ ISA::setMiscReg(int misc_reg, const MiscReg &val, ThreadContext *tc)
|
|||
break;
|
||||
|
||||
// Generic Timer registers
|
||||
case MISCREG_CNTFRQ:
|
||||
case MISCREG_CNTFRQ_EL0:
|
||||
getSystemCounter(tc)->setFreq(val);
|
||||
break;
|
||||
case MISCREG_CNTP_CVAL:
|
||||
case MISCREG_CNTP_CVAL_EL0:
|
||||
getArchTimer(tc, tc->cpuId())->setCompareValue(val);
|
||||
break;
|
||||
case MISCREG_CNTP_TVAL:
|
||||
case MISCREG_CNTP_TVAL_EL0:
|
||||
getArchTimer(tc, tc->cpuId())->setTimerValue(val);
|
||||
break;
|
||||
case MISCREG_CNTP_CTL:
|
||||
case MISCREG_CNTP_CTL_EL0:
|
||||
getArchTimer(tc, tc->cpuId())->setControl(val);
|
||||
break;
|
||||
// PL1 phys. timer, secure
|
||||
// AArch64
|
||||
case MISCREG_CNTPS_CVAL_EL1:
|
||||
case MISCREG_CNTPS_TVAL_EL1:
|
||||
case MISCREG_CNTPS_CTL_EL1:
|
||||
// PL2 phys. timer, non-secure
|
||||
// AArch32
|
||||
case MISCREG_CNTHCTL:
|
||||
case MISCREG_CNTHP_CVAL:
|
||||
case MISCREG_CNTHP_TVAL:
|
||||
case MISCREG_CNTHP_CTL:
|
||||
// AArch64
|
||||
case MISCREG_CNTHCTL_EL2:
|
||||
case MISCREG_CNTHP_CVAL_EL2:
|
||||
case MISCREG_CNTHP_TVAL_EL2:
|
||||
case MISCREG_CNTHP_CTL_EL2:
|
||||
// Virtual timer
|
||||
// AArch32
|
||||
case MISCREG_CNTV_CVAL:
|
||||
case MISCREG_CNTV_TVAL:
|
||||
case MISCREG_CNTV_CTL:
|
||||
// AArch64
|
||||
// case MISCREG_CNTV_CVAL_EL2:
|
||||
// case MISCREG_CNTV_TVAL_EL2:
|
||||
// case MISCREG_CNTV_CTL_EL2:
|
||||
case MISCREG_CNTFRQ ... MISCREG_CNTHP_CTL:
|
||||
case MISCREG_CNTPCT ... MISCREG_CNTHP_CVAL:
|
||||
case MISCREG_CNTKCTL_EL1 ... MISCREG_CNTV_CVAL_EL0:
|
||||
case MISCREG_CNTVOFF_EL2 ... MISCREG_CNTPS_CVAL_EL1:
|
||||
getGenericTimer(tc).setMiscReg(misc_reg, newVal);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -1988,26 +1915,23 @@ ISA::tlbiMVA(ThreadContext *tc, MiscReg newVal, bool secure_lookup, bool hyp,
|
|||
}
|
||||
}
|
||||
|
||||
::GenericTimer::SystemCounter *
|
||||
ISA::getSystemCounter(ThreadContext *tc)
|
||||
BaseISADevice &
|
||||
ISA::getGenericTimer(ThreadContext *tc)
|
||||
{
|
||||
::GenericTimer::SystemCounter *cnt = ((ArmSystem *) tc->getSystemPtr())->
|
||||
getSystemCounter();
|
||||
if (cnt == NULL) {
|
||||
panic("System counter not available\n");
|
||||
}
|
||||
return cnt;
|
||||
}
|
||||
// We only need to create an ISA interface the first time we try
|
||||
// to access the timer.
|
||||
if (timer)
|
||||
return *timer.get();
|
||||
|
||||
::GenericTimer::ArchTimer *
|
||||
ISA::getArchTimer(ThreadContext *tc, int cpu_id)
|
||||
{
|
||||
::GenericTimer::ArchTimer *timer = ((ArmSystem *) tc->getSystemPtr())->
|
||||
getArchTimer(cpu_id);
|
||||
if (timer == NULL) {
|
||||
panic("Architected timer not available\n");
|
||||
assert(system);
|
||||
GenericTimer *generic_timer(system->getGenericTimer());
|
||||
if (!generic_timer) {
|
||||
panic("Trying to get a generic timer from a system that hasn't "
|
||||
"been configured to use a generic timer.\n");
|
||||
}
|
||||
return timer;
|
||||
|
||||
timer.reset(new GenericTimerISA(*generic_timer, tc->cpuId()));
|
||||
return *timer.get();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2010, 2012-2014 ARM Limited
|
||||
* Copyright (c) 2010, 2012-2015 ARM Limited
|
||||
* All rights reserved
|
||||
*
|
||||
* The license below extends only to copyright in the software and shall
|
||||
|
@ -49,7 +49,6 @@
|
|||
#include "arch/arm/tlb.hh"
|
||||
#include "arch/arm/types.hh"
|
||||
#include "debug/Checkpoint.hh"
|
||||
#include "dev/arm/generic_timer.hh"
|
||||
#include "sim/sim_object.hh"
|
||||
|
||||
struct ArmISAParams;
|
||||
|
@ -139,6 +138,9 @@ namespace ArmISA
|
|||
// PMU belonging to this ISA
|
||||
BaseISADevice *pmu;
|
||||
|
||||
// Generic timer interface belonging to this ISA
|
||||
std::unique_ptr<BaseISADevice> timer;
|
||||
|
||||
// Cached copies of system-level properties
|
||||
bool haveSecurity;
|
||||
bool haveLPAE;
|
||||
|
@ -205,9 +207,7 @@ namespace ArmISA
|
|||
}
|
||||
}
|
||||
|
||||
::GenericTimer::SystemCounter * getSystemCounter(ThreadContext *tc);
|
||||
::GenericTimer::ArchTimer * getArchTimer(ThreadContext *tc,
|
||||
int cpu_id);
|
||||
BaseISADevice &getGenericTimer(ThreadContext *tc);
|
||||
|
||||
|
||||
private:
|
||||
|
|
|
@ -151,24 +151,6 @@ ArmSystem::initState()
|
|||
}
|
||||
}
|
||||
|
||||
GenericTimer::ArchTimer *
|
||||
ArmSystem::getArchTimer(int cpu_id) const
|
||||
{
|
||||
if (_genericTimer) {
|
||||
return _genericTimer->getArchTimer(cpu_id);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
GenericTimer::SystemCounter *
|
||||
ArmSystem::getSystemCounter() const
|
||||
{
|
||||
if (_genericTimer) {
|
||||
return _genericTimer->getSystemCounter();
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool
|
||||
ArmSystem::haveSecurity(ThreadContext *tc)
|
||||
{
|
||||
|
|
|
@ -46,13 +46,13 @@
|
|||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "dev/arm/generic_timer.hh"
|
||||
#include "kern/linux/events.hh"
|
||||
#include "params/ArmSystem.hh"
|
||||
#include "params/GenericArmSystem.hh"
|
||||
#include "sim/sim_object.hh"
|
||||
#include "sim/system.hh"
|
||||
|
||||
class GenericTimer;
|
||||
class ThreadContext;
|
||||
|
||||
class ArmSystem : public System
|
||||
|
@ -166,11 +166,8 @@ class ArmSystem : public System
|
|||
_genericTimer = generic_timer;
|
||||
}
|
||||
|
||||
/** Returns a pointer to the system counter. */
|
||||
GenericTimer::SystemCounter *getSystemCounter() const;
|
||||
|
||||
/** Returns a pointer to the appropriate architected timer. */
|
||||
GenericTimer::ArchTimer *getArchTimer(int cpu_id) const;
|
||||
/** Get a pointer to the system's generic timer model */
|
||||
GenericTimer *getGenericTimer() const { return _genericTimer; }
|
||||
|
||||
/** Returns true if the register width of the highest implemented exception
|
||||
* level is 64 bits (ARMv8) */
|
||||
|
|
|
@ -136,7 +136,7 @@ class GenericTimer(SimObject):
|
|||
cxx_header = "dev/arm/generic_timer.hh"
|
||||
system = Param.System(Parent.any, "system")
|
||||
gic = Param.BaseGic(Parent.any, "GIC to use for interrupting")
|
||||
int_num = Param.UInt32("Interrupt number used per-cpu to GIC")
|
||||
int_phys = Param.UInt32("Interrupt number used per-cpu to GIC")
|
||||
# @todo: for now only one timer per CPU is supported, which is the
|
||||
# normal behaviour when Security and Virt. extensions are disabled.
|
||||
|
||||
|
@ -457,7 +457,7 @@ class VExpress_EMM(RealView):
|
|||
idreg=0x02250000, pio_addr=0x1C010000)
|
||||
gic = Pl390(dist_addr=0x2C001000, cpu_addr=0x2C002000)
|
||||
local_cpu_timer = CpuLocalTimer(int_num_timer=29, int_num_watchdog=30, pio_addr=0x2C080000)
|
||||
generic_timer = GenericTimer(int_num=29)
|
||||
generic_timer = GenericTimer(int_phys=29)
|
||||
timer0 = Sp804(int_num0=34, int_num1=34, pio_addr=0x1C110000, clock0='1MHz', clock1='1MHz')
|
||||
timer1 = Sp804(int_num0=35, int_num1=35, pio_addr=0x1C120000, clock0='1MHz', clock1='1MHz')
|
||||
clcd = Pl111(pio_addr=0x1c1f0000, int_num=46)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2013 ARM Limited
|
||||
* Copyright (c) 2013, 2015 ARM Limited
|
||||
* All rights reserved.
|
||||
*
|
||||
* The license below extends only to copyright in the software and shall
|
||||
|
@ -35,16 +35,24 @@
|
|||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* Authors: Giacomo Gabrielli
|
||||
* Andreas Sandberg
|
||||
*/
|
||||
|
||||
#include "arch/arm/system.hh"
|
||||
#include "debug/Checkpoint.hh"
|
||||
#include "debug/Timer.hh"
|
||||
#include "dev/arm/base_gic.hh"
|
||||
#include "dev/arm/generic_timer.hh"
|
||||
|
||||
#include "arch/arm/system.hh"
|
||||
#include "debug/Timer.hh"
|
||||
#include "dev/arm/base_gic.hh"
|
||||
#include "params/GenericTimer.hh"
|
||||
|
||||
SystemCounter::SystemCounter()
|
||||
: _freq(0), _period(0), _resetTick(0), _regCntkctl(0)
|
||||
{
|
||||
setFreq(0x01800000);
|
||||
}
|
||||
|
||||
void
|
||||
GenericTimer::SystemCounter::setFreq(uint32_t freq)
|
||||
SystemCounter::setFreq(uint32_t freq)
|
||||
{
|
||||
if (_freq != 0) {
|
||||
// Altering the frequency after boot shouldn't be done in practice.
|
||||
|
@ -56,147 +64,389 @@ GenericTimer::SystemCounter::setFreq(uint32_t freq)
|
|||
}
|
||||
|
||||
void
|
||||
GenericTimer::SystemCounter::serialize(std::ostream &os)
|
||||
SystemCounter::serialize(std::ostream &os) const
|
||||
{
|
||||
SERIALIZE_SCALAR(_regCntkctl);
|
||||
SERIALIZE_SCALAR(_freq);
|
||||
SERIALIZE_SCALAR(_period);
|
||||
SERIALIZE_SCALAR(_resetTick);
|
||||
}
|
||||
|
||||
void
|
||||
GenericTimer::SystemCounter::unserialize(Checkpoint *cp,
|
||||
const std::string §ion)
|
||||
SystemCounter::unserialize(Checkpoint *cp,
|
||||
const std::string §ion)
|
||||
{
|
||||
// We didn't handle CNTKCTL in this class before, assume it's zero
|
||||
// if it isn't present.
|
||||
if (!UNSERIALIZE_OPT_SCALAR(_regCntkctl))
|
||||
_regCntkctl = 0;
|
||||
UNSERIALIZE_SCALAR(_freq);
|
||||
UNSERIALIZE_SCALAR(_period);
|
||||
UNSERIALIZE_SCALAR(_resetTick);
|
||||
}
|
||||
|
||||
|
||||
|
||||
ArchTimer::ArchTimer(const std::string &name,
|
||||
SimObject &parent,
|
||||
SystemCounter &sysctr,
|
||||
const Interrupt &interrupt)
|
||||
: _name(name), _parent(parent), _systemCounter(sysctr),
|
||||
_interrupt(interrupt),
|
||||
_control(0), _counterLimit(0),
|
||||
_counterLimitReachedEvent(this)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
GenericTimer::ArchTimer::counterLimitReached()
|
||||
ArchTimer::counterLimitReached()
|
||||
{
|
||||
_control.istatus = 1;
|
||||
|
||||
if (!_control.enable)
|
||||
return;
|
||||
|
||||
// DPRINTF(Timer, "Counter limit reached\n");
|
||||
|
||||
DPRINTF(Timer, "Counter limit reached\n");
|
||||
if (!_control.imask) {
|
||||
// DPRINTF(Timer, "Causing interrupt\n");
|
||||
_parent->_gic->sendPPInt(_intNum, _cpuNum);
|
||||
DPRINTF(Timer, "Causing interrupt\n");
|
||||
_interrupt.send();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
GenericTimer::ArchTimer::setCompareValue(uint64_t val)
|
||||
ArchTimer::updateCounter()
|
||||
{
|
||||
_counterLimit = val;
|
||||
if (_counterLimitReachedEvent.scheduled())
|
||||
_parent->deschedule(_counterLimitReachedEvent);
|
||||
if (counterValue() >= _counterLimit) {
|
||||
_parent.deschedule(_counterLimitReachedEvent);
|
||||
if (value() >= _counterLimit) {
|
||||
counterLimitReached();
|
||||
} else {
|
||||
const auto period(_systemCounter.period());
|
||||
_control.istatus = 0;
|
||||
_parent->schedule(_counterLimitReachedEvent,
|
||||
curTick() + (_counterLimit - counterValue()) * _counter->period());
|
||||
_parent.schedule(_counterLimitReachedEvent,
|
||||
curTick() + (_counterLimit - value()) * period);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
GenericTimer::ArchTimer::setTimerValue(uint32_t val)
|
||||
ArchTimer::setCompareValue(uint64_t val)
|
||||
{
|
||||
setCompareValue(counterValue() + sext<32>(val));
|
||||
_counterLimit = val;
|
||||
updateCounter();
|
||||
}
|
||||
|
||||
void
|
||||
GenericTimer::ArchTimer::setControl(uint32_t val)
|
||||
ArchTimer::setTimerValue(uint32_t val)
|
||||
{
|
||||
setCompareValue(value() + sext<32>(val));
|
||||
}
|
||||
|
||||
void
|
||||
ArchTimer::setControl(uint32_t val)
|
||||
{
|
||||
ArchTimerCtrl new_ctl = val;
|
||||
if ((new_ctl.enable && !new_ctl.imask) &&
|
||||
!(_control.enable && !_control.imask)) {
|
||||
// Re-evalute the timer condition
|
||||
if (_counterLimit >= counterValue()) {
|
||||
if (_counterLimit >= value()) {
|
||||
_control.istatus = 1;
|
||||
|
||||
DPRINTF(Timer, "Causing interrupt in control\n");
|
||||
//_parent->_gic->sendPPInt(_intNum, _cpuNum);
|
||||
//_interrupt.send();
|
||||
}
|
||||
}
|
||||
_control.enable = new_ctl.enable;
|
||||
_control.imask = new_ctl.imask;
|
||||
}
|
||||
|
||||
void
|
||||
GenericTimer::ArchTimer::serialize(std::ostream &os)
|
||||
uint64_t
|
||||
ArchTimer::value() const
|
||||
{
|
||||
SERIALIZE_SCALAR(_cpuNum);
|
||||
SERIALIZE_SCALAR(_intNum);
|
||||
uint32_t control_serial = _control;
|
||||
SERIALIZE_SCALAR(control_serial);
|
||||
return _systemCounter.value();
|
||||
}
|
||||
|
||||
void
|
||||
ArchTimer::serialize(std::ostream &os) const
|
||||
{
|
||||
paramOut(os, "control_serial", _control);
|
||||
SERIALIZE_SCALAR(_counterLimit);
|
||||
bool event_scheduled = _counterLimitReachedEvent.scheduled();
|
||||
|
||||
const bool event_scheduled(_counterLimitReachedEvent.scheduled());
|
||||
SERIALIZE_SCALAR(event_scheduled);
|
||||
Tick event_time;
|
||||
if (event_scheduled) {
|
||||
event_time = _counterLimitReachedEvent.when();
|
||||
const Tick event_time(_counterLimitReachedEvent.when());
|
||||
SERIALIZE_SCALAR(event_time);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
GenericTimer::ArchTimer::unserialize(Checkpoint *cp, const std::string §ion)
|
||||
ArchTimer::unserialize(Checkpoint *cp,
|
||||
const std::string §ion)
|
||||
{
|
||||
UNSERIALIZE_SCALAR(_cpuNum);
|
||||
UNSERIALIZE_SCALAR(_intNum);
|
||||
uint32_t control_serial;
|
||||
UNSERIALIZE_SCALAR(control_serial);
|
||||
_control = control_serial;
|
||||
paramIn(cp, section, "control_serial", _control);
|
||||
bool event_scheduled;
|
||||
UNSERIALIZE_SCALAR(event_scheduled);
|
||||
Tick event_time;
|
||||
if (event_scheduled) {
|
||||
Tick event_time;
|
||||
UNSERIALIZE_SCALAR(event_time);
|
||||
_parent->schedule(_counterLimitReachedEvent, event_time);
|
||||
_parent.schedule(_counterLimitReachedEvent, event_time);
|
||||
}
|
||||
}
|
||||
|
||||
GenericTimer::GenericTimer(Params *p)
|
||||
: SimObject(p), _gic(p->gic)
|
||||
void
|
||||
ArchTimer::Interrupt::send()
|
||||
{
|
||||
for (int i = 0; i < CPU_MAX; ++i) {
|
||||
std::stringstream oss;
|
||||
oss << name() << ".arch_timer" << i;
|
||||
_archTimers[i]._name = oss.str();
|
||||
_archTimers[i]._parent = this;
|
||||
_archTimers[i]._counter = &_systemCounter;
|
||||
_archTimers[i]._cpuNum = i;
|
||||
_archTimers[i]._intNum = p->int_num;
|
||||
}
|
||||
if (_ppi) {
|
||||
_gic.sendPPInt(_irq, _cpu);
|
||||
} else {
|
||||
_gic.sendInt(_irq);
|
||||
}
|
||||
}
|
||||
|
||||
((ArmSystem *) p->system)->setGenericTimer(this);
|
||||
|
||||
void
|
||||
ArchTimer::Interrupt::clear()
|
||||
{
|
||||
if (_ppi) {
|
||||
_gic.clearPPInt(_irq, _cpu);
|
||||
} else {
|
||||
_gic.clearInt(_irq);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
GenericTimer::GenericTimer(GenericTimerParams *p)
|
||||
: SimObject(p),
|
||||
gic(p->gic),
|
||||
irqPhys(p->int_phys)
|
||||
{
|
||||
dynamic_cast<ArmSystem &>(*p->system).setGenericTimer(this);
|
||||
}
|
||||
|
||||
void
|
||||
GenericTimer::serialize(std::ostream &os)
|
||||
{
|
||||
paramOut(os, "cpu_count", timers.size());
|
||||
|
||||
nameOut(os, csprintf("%s.sys_counter", name()));
|
||||
_systemCounter.serialize(os);
|
||||
for (int i = 0; i < CPU_MAX; ++i) {
|
||||
nameOut(os, csprintf("%s.arch_timer%d", name(), i));
|
||||
_archTimers[i].serialize(os);
|
||||
systemCounter.serialize(os);
|
||||
|
||||
for (int i = 0; i < timers.size(); ++i) {
|
||||
CoreTimers &core(getTimers(i));
|
||||
|
||||
nameOut(os, core.phys.name());
|
||||
core.phys.serialize(os);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
GenericTimer::unserialize(Checkpoint *cp, const std::string §ion)
|
||||
{
|
||||
_systemCounter.unserialize(cp, csprintf("%s.sys_counter", section));
|
||||
for (int i = 0; i < CPU_MAX; ++i) {
|
||||
_archTimers[i].unserialize(cp, csprintf("%s.arch_timer%d", section, i));
|
||||
systemCounter.unserialize(cp, csprintf("%s.sys_counter", section));
|
||||
|
||||
// Try to unserialize the CPU count. Old versions of the timer
|
||||
// model assumed a 8 CPUs, so we fall back to that if the field
|
||||
// isn't present.
|
||||
static const unsigned OLD_CPU_MAX = 8;
|
||||
unsigned cpu_count;
|
||||
if (!UNSERIALIZE_OPT_SCALAR(cpu_count)) {
|
||||
warn("Checkpoint does not contain CPU count, assuming %i CPUs\n",
|
||||
OLD_CPU_MAX);
|
||||
cpu_count = OLD_CPU_MAX;
|
||||
}
|
||||
|
||||
for (int i = 0; i < cpu_count; ++i) {
|
||||
CoreTimers &core(getTimers(i));
|
||||
// This should really be phys_timerN, but we are stuck with
|
||||
// arch_timer for backwards compatibility.
|
||||
core.phys.unserialize(cp, csprintf("%s.arch_timer%d", section, i));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
GenericTimer::CoreTimers &
|
||||
GenericTimer::getTimers(int cpu_id)
|
||||
{
|
||||
if (cpu_id >= timers.size())
|
||||
createTimers(cpu_id + 1);
|
||||
|
||||
return *timers[cpu_id];
|
||||
}
|
||||
|
||||
void
|
||||
GenericTimer::createTimers(unsigned cpus)
|
||||
{
|
||||
assert(timers.size() < cpus);
|
||||
|
||||
const unsigned old_cpu_count(timers.size());
|
||||
timers.resize(cpus);
|
||||
for (unsigned i = old_cpu_count; i < cpus; ++i) {
|
||||
timers[i].reset(
|
||||
new CoreTimers(*this, i, irqPhys));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
GenericTimer::setMiscReg(int reg, unsigned cpu, MiscReg val)
|
||||
{
|
||||
CoreTimers &core(getTimers(cpu));
|
||||
|
||||
switch (reg) {
|
||||
case MISCREG_CNTFRQ:
|
||||
case MISCREG_CNTFRQ_EL0:
|
||||
systemCounter.setFreq(val);
|
||||
return;
|
||||
|
||||
case MISCREG_CNTKCTL:
|
||||
case MISCREG_CNTKCTL_EL1:
|
||||
systemCounter.setKernelControl(val);
|
||||
return;
|
||||
|
||||
// Physical timer
|
||||
case MISCREG_CNTP_CVAL:
|
||||
case MISCREG_CNTP_CVAL_NS:
|
||||
case MISCREG_CNTP_CVAL_EL0:
|
||||
core.phys.setCompareValue(val);
|
||||
return;
|
||||
|
||||
case MISCREG_CNTP_TVAL:
|
||||
case MISCREG_CNTP_TVAL_NS:
|
||||
case MISCREG_CNTP_TVAL_EL0:
|
||||
core.phys.setTimerValue(val);
|
||||
return;
|
||||
|
||||
case MISCREG_CNTP_CTL:
|
||||
case MISCREG_CNTP_CTL_NS:
|
||||
case MISCREG_CNTP_CTL_EL0:
|
||||
core.phys.setControl(val);
|
||||
return;
|
||||
|
||||
// Count registers
|
||||
case MISCREG_CNTPCT:
|
||||
case MISCREG_CNTPCT_EL0:
|
||||
case MISCREG_CNTVCT:
|
||||
case MISCREG_CNTVCT_EL0:
|
||||
warn("Ignoring write to read only count register: %s\n",
|
||||
miscRegName[reg]);
|
||||
return;
|
||||
|
||||
// Virtual timer
|
||||
case MISCREG_CNTVOFF:
|
||||
case MISCREG_CNTVOFF_EL2:
|
||||
case MISCREG_CNTV_CVAL:
|
||||
case MISCREG_CNTV_CVAL_EL0:
|
||||
case MISCREG_CNTV_TVAL:
|
||||
case MISCREG_CNTV_TVAL_EL0:
|
||||
case MISCREG_CNTV_CTL:
|
||||
case MISCREG_CNTV_CTL_EL0:
|
||||
/* FALLTHROUGH */
|
||||
|
||||
// PL1 phys. timer, secure
|
||||
case MISCREG_CNTP_CTL_S:
|
||||
case MISCREG_CNTPS_CVAL_EL1:
|
||||
case MISCREG_CNTPS_TVAL_EL1:
|
||||
case MISCREG_CNTPS_CTL_EL1:
|
||||
/* FALLTHROUGH */
|
||||
|
||||
// PL2 phys. timer, non-secure
|
||||
case MISCREG_CNTHCTL:
|
||||
case MISCREG_CNTHCTL_EL2:
|
||||
case MISCREG_CNTHP_CVAL:
|
||||
case MISCREG_CNTHP_CVAL_EL2:
|
||||
case MISCREG_CNTHP_TVAL:
|
||||
case MISCREG_CNTHP_TVAL_EL2:
|
||||
case MISCREG_CNTHP_CTL:
|
||||
case MISCREG_CNTHP_CTL_EL2:
|
||||
warn("Writing to unimplemented register: %s\n",
|
||||
miscRegName[reg]);
|
||||
return;
|
||||
|
||||
default:
|
||||
warn("Writing to unknown register: %s\n", miscRegName[reg]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
MiscReg
|
||||
GenericTimer::readMiscReg(int reg, unsigned cpu)
|
||||
{
|
||||
CoreTimers &core(getTimers(cpu));
|
||||
|
||||
switch (reg) {
|
||||
case MISCREG_CNTFRQ:
|
||||
case MISCREG_CNTFRQ_EL0:
|
||||
return systemCounter.freq();
|
||||
|
||||
case MISCREG_CNTKCTL:
|
||||
case MISCREG_CNTKCTL_EL1:
|
||||
return systemCounter.getKernelControl();
|
||||
|
||||
// Physical timer
|
||||
case MISCREG_CNTP_CVAL:
|
||||
case MISCREG_CNTP_CVAL_EL0:
|
||||
return core.phys.compareValue();
|
||||
|
||||
case MISCREG_CNTP_TVAL:
|
||||
case MISCREG_CNTP_TVAL_EL0:
|
||||
return core.phys.timerValue();
|
||||
|
||||
case MISCREG_CNTP_CTL:
|
||||
case MISCREG_CNTP_CTL_EL0:
|
||||
case MISCREG_CNTP_CTL_NS:
|
||||
return core.phys.control();
|
||||
|
||||
case MISCREG_CNTPCT:
|
||||
case MISCREG_CNTPCT_EL0:
|
||||
return core.phys.value();
|
||||
|
||||
|
||||
// Virtual timer
|
||||
case MISCREG_CNTVCT:
|
||||
case MISCREG_CNTVCT_EL0:
|
||||
warn_once("Virtual timer not implemented, "
|
||||
"returning physical timer value\n");
|
||||
return core.phys.value();
|
||||
|
||||
case MISCREG_CNTVOFF:
|
||||
case MISCREG_CNTVOFF_EL2:
|
||||
case MISCREG_CNTV_CVAL:
|
||||
case MISCREG_CNTV_CVAL_EL0:
|
||||
case MISCREG_CNTV_TVAL:
|
||||
case MISCREG_CNTV_TVAL_EL0:
|
||||
case MISCREG_CNTV_CTL:
|
||||
case MISCREG_CNTV_CTL_EL0:
|
||||
/* FALLTHROUGH */
|
||||
|
||||
// PL1 phys. timer, secure
|
||||
case MISCREG_CNTP_CTL_S:
|
||||
case MISCREG_CNTPS_CVAL_EL1:
|
||||
case MISCREG_CNTPS_TVAL_EL1:
|
||||
case MISCREG_CNTPS_CTL_EL1:
|
||||
/* FALLTHROUGH */
|
||||
|
||||
// PL2 phys. timer, non-secure
|
||||
case MISCREG_CNTHCTL:
|
||||
case MISCREG_CNTHCTL_EL2:
|
||||
case MISCREG_CNTHP_CVAL:
|
||||
case MISCREG_CNTHP_CVAL_EL2:
|
||||
case MISCREG_CNTHP_TVAL:
|
||||
case MISCREG_CNTHP_TVAL_EL2:
|
||||
case MISCREG_CNTHP_CTL:
|
||||
case MISCREG_CNTHP_CTL_EL2:
|
||||
warn("Reading from unimplemented register: %s\n",
|
||||
miscRegName[reg]);
|
||||
return 0;
|
||||
|
||||
|
||||
default:
|
||||
warn("Reading from unknown register: %s\n", miscRegName[reg]);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
GenericTimer *
|
||||
GenericTimerParams::create()
|
||||
{
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2013 ARM Limited
|
||||
* Copyright (c) 2013, 2015 ARM Limited
|
||||
* All rights reserved.
|
||||
*
|
||||
* The license below extends only to copyright in the software and shall
|
||||
|
@ -35,13 +35,15 @@
|
|||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* Authors: Giacomo Gabrielli
|
||||
* Andreas Sandberg
|
||||
*/
|
||||
|
||||
#ifndef __DEV_ARM_GENERIC_TIMER_HH__
|
||||
#define __DEV_ARM_GENERIC_TIMER_HH__
|
||||
|
||||
#include "arch/arm/isa_device.hh"
|
||||
#include "base/bitunion.hh"
|
||||
#include "params/GenericTimer.hh"
|
||||
#include "dev/arm/base_gic.hh"
|
||||
#include "sim/core.hh"
|
||||
#include "sim/sim_object.hh"
|
||||
|
||||
|
@ -51,149 +53,209 @@
|
|||
/// ARM, Issue C, Chapter 17).
|
||||
|
||||
class Checkpoint;
|
||||
class BaseGic;
|
||||
class GenericTimerParams;
|
||||
|
||||
/// Wrapper around the actual counters and timers of the Generic Timer
|
||||
/// extension.
|
||||
class GenericTimer : public SimObject
|
||||
/// Global system counter. It is shared by the architected timers.
|
||||
/// @todo: implement memory-mapped controls
|
||||
class SystemCounter
|
||||
{
|
||||
protected:
|
||||
/// Counter frequency (as specified by CNTFRQ).
|
||||
uint64_t _freq;
|
||||
/// Cached copy of the counter period (inverse of the frequency).
|
||||
Tick _period;
|
||||
/// Tick when the counter was reset.
|
||||
Tick _resetTick;
|
||||
|
||||
uint32_t _regCntkctl;
|
||||
|
||||
public:
|
||||
SystemCounter();
|
||||
|
||||
/// Returns the current value of the physical counter.
|
||||
uint64_t value() const
|
||||
{
|
||||
if (_freq == 0)
|
||||
return 0; // Counter is still off.
|
||||
return (curTick() - _resetTick) / _period;
|
||||
}
|
||||
|
||||
/// Returns the counter frequency.
|
||||
uint64_t freq() const { return _freq; }
|
||||
/// Sets the counter frequency.
|
||||
/// @param freq frequency in Hz.
|
||||
void setFreq(uint32_t freq);
|
||||
|
||||
/// Returns the counter period.
|
||||
Tick period() const { return _period; }
|
||||
|
||||
void setKernelControl(uint32_t val) { _regCntkctl = val; }
|
||||
uint32_t getKernelControl() { return _regCntkctl; }
|
||||
|
||||
void serialize(std::ostream &os) const;
|
||||
void unserialize(Checkpoint *cp, const std::string §ion);
|
||||
|
||||
private:
|
||||
// Disable copying
|
||||
SystemCounter(const SystemCounter &c);
|
||||
};
|
||||
|
||||
/// Per-CPU architected timer.
|
||||
class ArchTimer
|
||||
{
|
||||
public:
|
||||
|
||||
/// Global system counter. It is shared by the architected timers.
|
||||
/// @todo: implement memory-mapped controls
|
||||
class SystemCounter
|
||||
class Interrupt
|
||||
{
|
||||
protected:
|
||||
/// Counter frequency (as specified by CNTFRQ).
|
||||
uint64_t _freq;
|
||||
/// Cached copy of the counter period (inverse of the frequency).
|
||||
Tick _period;
|
||||
/// Tick when the counter was reset.
|
||||
Tick _resetTick;
|
||||
|
||||
public:
|
||||
/// Ctor.
|
||||
SystemCounter()
|
||||
: _freq(0), _period(0), _resetTick(0)
|
||||
{
|
||||
setFreq(0x01800000);
|
||||
}
|
||||
Interrupt(BaseGic &gic, unsigned irq)
|
||||
: _gic(gic), _ppi(false), _irq(irq), _cpu(0) {}
|
||||
|
||||
/// Returns the current value of the physical counter.
|
||||
uint64_t value() const
|
||||
{
|
||||
if (_freq == 0)
|
||||
return 0; // Counter is still off.
|
||||
return (curTick() - _resetTick) / _period;
|
||||
}
|
||||
Interrupt(BaseGic &gic, unsigned irq, unsigned cpu)
|
||||
: _gic(gic), _ppi(true), _irq(irq), _cpu(cpu) {}
|
||||
|
||||
/// Returns the counter frequency.
|
||||
uint64_t freq() const { return _freq; }
|
||||
/// Sets the counter frequency.
|
||||
/// @param freq frequency in Hz.
|
||||
void setFreq(uint32_t freq);
|
||||
void send();
|
||||
void clear();
|
||||
|
||||
/// Returns the counter period.
|
||||
Tick period() const { return _period; }
|
||||
|
||||
void serialize(std::ostream &os);
|
||||
void unserialize(Checkpoint *cp, const std::string §ion);
|
||||
};
|
||||
|
||||
/// Per-CPU architected timer.
|
||||
class ArchTimer
|
||||
{
|
||||
protected:
|
||||
/// Control register.
|
||||
BitUnion32(ArchTimerCtrl)
|
||||
Bitfield<0> enable;
|
||||
Bitfield<1> imask;
|
||||
Bitfield<2> istatus;
|
||||
EndBitUnion(ArchTimerCtrl)
|
||||
|
||||
/// Name of this timer.
|
||||
std::string _name;
|
||||
/// Pointer to parent class.
|
||||
GenericTimer *_parent;
|
||||
/// Pointer to the global system counter.
|
||||
SystemCounter *_counter;
|
||||
/// ID of the CPU this timer is attached to.
|
||||
int _cpuNum;
|
||||
/// ID of the interrupt to be triggered.
|
||||
int _intNum;
|
||||
/// Cached value of the control register ({CNTP/CNTHP/CNTV}_CTL).
|
||||
ArchTimerCtrl _control;
|
||||
/// Programmed limit value for the upcounter ({CNTP/CNTHP/CNTV}_CVAL).
|
||||
uint64_t _counterLimit;
|
||||
|
||||
/// Called when the upcounter reaches the programmed value.
|
||||
void counterLimitReached();
|
||||
EventWrapper<ArchTimer, &ArchTimer::counterLimitReached>
|
||||
_counterLimitReachedEvent;
|
||||
|
||||
/// Returns the value of the counter which this timer relies on.
|
||||
uint64_t counterValue() const { return _counter->value(); }
|
||||
|
||||
public:
|
||||
/// Ctor.
|
||||
ArchTimer()
|
||||
: _control(0), _counterLimit(0), _counterLimitReachedEvent(this)
|
||||
{}
|
||||
|
||||
/// Returns the timer name.
|
||||
std::string name() const { return _name; }
|
||||
|
||||
/// Returns the CompareValue view of the timer.
|
||||
uint64_t compareValue() const { return _counterLimit; }
|
||||
/// Sets the CompareValue view of the timer.
|
||||
void setCompareValue(uint64_t val);
|
||||
|
||||
/// Returns the TimerValue view of the timer.
|
||||
uint32_t timerValue() const { return _counterLimit - counterValue(); }
|
||||
/// Sets the TimerValue view of the timer.
|
||||
void setTimerValue(uint32_t val);
|
||||
|
||||
/// Sets the control register.
|
||||
uint32_t control() const { return _control; }
|
||||
void setControl(uint32_t val);
|
||||
|
||||
virtual void serialize(std::ostream &os);
|
||||
virtual void unserialize(Checkpoint *cp, const std::string §ion);
|
||||
|
||||
friend class GenericTimer;
|
||||
private:
|
||||
BaseGic &_gic;
|
||||
const bool _ppi;
|
||||
const unsigned _irq;
|
||||
const unsigned _cpu;
|
||||
};
|
||||
|
||||
protected:
|
||||
/// Control register.
|
||||
BitUnion32(ArchTimerCtrl)
|
||||
Bitfield<0> enable;
|
||||
Bitfield<1> imask;
|
||||
Bitfield<2> istatus;
|
||||
EndBitUnion(ArchTimerCtrl)
|
||||
|
||||
static const int CPU_MAX = 8;
|
||||
/// Name of this timer.
|
||||
const std::string _name;
|
||||
|
||||
/// Pointer to the GIC, needed to trigger timer interrupts.
|
||||
BaseGic *_gic;
|
||||
/// System counter.
|
||||
SystemCounter _systemCounter;
|
||||
/// Per-CPU architected timers.
|
||||
// @todo: this would become a 2-dim. array with Security and Virt.
|
||||
ArchTimer _archTimers[CPU_MAX];
|
||||
/// Pointer to parent class.
|
||||
SimObject &_parent;
|
||||
|
||||
SystemCounter &_systemCounter;
|
||||
|
||||
Interrupt _interrupt;
|
||||
|
||||
/// Value of the control register ({CNTP/CNTHP/CNTV}_CTL).
|
||||
ArchTimerCtrl _control;
|
||||
/// Programmed limit value for the upcounter ({CNTP/CNTHP/CNTV}_CVAL).
|
||||
uint64_t _counterLimit;
|
||||
|
||||
/**
|
||||
* Timer settings or the offset has changed, re-evaluate
|
||||
* trigger condition and raise interrupt if necessary.
|
||||
*/
|
||||
void updateCounter();
|
||||
|
||||
/// Called when the upcounter reaches the programmed value.
|
||||
void counterLimitReached();
|
||||
EventWrapper<ArchTimer, &ArchTimer::counterLimitReached>
|
||||
_counterLimitReachedEvent;
|
||||
|
||||
public:
|
||||
typedef GenericTimerParams Params;
|
||||
const Params *
|
||||
params() const
|
||||
{
|
||||
return dynamic_cast<const Params *>(_params);
|
||||
ArchTimer(const std::string &name,
|
||||
SimObject &parent,
|
||||
SystemCounter &sysctr,
|
||||
const Interrupt &interrupt);
|
||||
|
||||
/// Returns the timer name.
|
||||
std::string name() const { return _name; }
|
||||
|
||||
/// Returns the CompareValue view of the timer.
|
||||
uint64_t compareValue() const { return _counterLimit; }
|
||||
/// Sets the CompareValue view of the timer.
|
||||
void setCompareValue(uint64_t val);
|
||||
|
||||
/// Returns the TimerValue view of the timer.
|
||||
uint32_t timerValue() const { return _counterLimit - value(); }
|
||||
/// Sets the TimerValue view of the timer.
|
||||
void setTimerValue(uint32_t val);
|
||||
|
||||
/// Sets the control register.
|
||||
uint32_t control() const { return _control; }
|
||||
void setControl(uint32_t val);
|
||||
|
||||
/// Returns the value of the counter which this timer relies on.
|
||||
uint64_t value() const;
|
||||
|
||||
void serialize(std::ostream &os) const;
|
||||
void unserialize(Checkpoint *cp, const std::string §ion);
|
||||
|
||||
private:
|
||||
// Disable copying
|
||||
ArchTimer(const ArchTimer &t);
|
||||
};
|
||||
|
||||
class GenericTimer : public SimObject
|
||||
{
|
||||
public:
|
||||
GenericTimer(GenericTimerParams *p);
|
||||
|
||||
void serialize(std::ostream &os) M5_ATTR_OVERRIDE;
|
||||
void unserialize(Checkpoint *cp, const std::string &sec) M5_ATTR_OVERRIDE;
|
||||
|
||||
public:
|
||||
void setMiscReg(int misc_reg, unsigned cpu, ArmISA::MiscReg val);
|
||||
ArmISA::MiscReg readMiscReg(int misc_reg, unsigned cpu);
|
||||
|
||||
protected:
|
||||
struct CoreTimers {
|
||||
CoreTimers(GenericTimer &parent, unsigned cpu,
|
||||
unsigned _irqPhys)
|
||||
: irqPhys(*parent.gic, _irqPhys, cpu),
|
||||
// This should really be phys_timerN, but we are stuck with
|
||||
// arch_timer for backwards compatibility.
|
||||
phys(csprintf("%s.arch_timer%d", parent.name(), cpu),
|
||||
parent, parent.systemCounter,
|
||||
irqPhys)
|
||||
{}
|
||||
|
||||
ArchTimer::Interrupt irqPhys;
|
||||
ArchTimer phys;
|
||||
|
||||
private:
|
||||
// Disable copying
|
||||
CoreTimers(const CoreTimers &c);
|
||||
};
|
||||
|
||||
CoreTimers &getTimers(int cpu_id);
|
||||
void createTimers(unsigned cpus);
|
||||
|
||||
/// System counter.
|
||||
SystemCounter systemCounter;
|
||||
|
||||
/// Per-CPU physical architected timers.
|
||||
std::vector<std::unique_ptr<CoreTimers>> timers;
|
||||
|
||||
protected: // Configuration
|
||||
/// Pointer to the GIC, needed to trigger timer interrupts.
|
||||
BaseGic *const gic;
|
||||
|
||||
/// Physical timer interrupt
|
||||
const unsigned irqPhys;
|
||||
};
|
||||
|
||||
class GenericTimerISA : public ArmISA::BaseISADevice
|
||||
{
|
||||
public:
|
||||
GenericTimerISA(GenericTimer &_parent, unsigned _cpu)
|
||||
: parent(_parent), cpu(_cpu) {}
|
||||
|
||||
void setMiscReg(int misc_reg, ArmISA::MiscReg val) M5_ATTR_OVERRIDE {
|
||||
parent.setMiscReg(misc_reg, cpu, val);
|
||||
}
|
||||
ArmISA::MiscReg readMiscReg(int misc_reg) M5_ATTR_OVERRIDE {
|
||||
return parent.readMiscReg(misc_reg, cpu);
|
||||
}
|
||||
|
||||
/// Ctor.
|
||||
GenericTimer(Params *p);
|
||||
|
||||
/// Returns a pointer to the system counter.
|
||||
SystemCounter *getSystemCounter() { return &_systemCounter; }
|
||||
|
||||
/// Returns a pointer to the architected timer for cpu_id.
|
||||
ArchTimer *getArchTimer(int cpu_id) { return &_archTimers[cpu_id]; }
|
||||
|
||||
virtual void serialize(std::ostream &os);
|
||||
virtual void unserialize(Checkpoint *cp, const std::string §ion);
|
||||
protected:
|
||||
GenericTimer &parent;
|
||||
unsigned cpu;
|
||||
};
|
||||
|
||||
#endif // __DEV_ARM_GENERIC_TIMER_HH__
|
||||
|
|
Loading…
Reference in a new issue