X86: Make the local APIC accessible through the memory system directly, and make the timer work.

This commit is contained in:
Gabe Black 2008-10-12 11:08:00 -07:00
parent d9f9c967fb
commit 42ebebf99a
9 changed files with 244 additions and 209 deletions

View file

@ -55,10 +55,12 @@
* Authors: Gabe Black * Authors: Gabe Black
*/ */
#include "arch/x86/apicregs.hh"
#include "arch/x86/interrupts.hh" #include "arch/x86/interrupts.hh"
#include "cpu/base.hh" #include "cpu/base.hh"
int divideFromConf(uint32_t conf) int
divideFromConf(uint32_t conf)
{ {
// This figures out what division we want from the division configuration // This figures out what division we want from the division configuration
// register in the local APIC. The encoding is a little odd but it can // register in the local APIC. The encoding is a little odd but it can
@ -68,14 +70,171 @@ int divideFromConf(uint32_t conf)
return 1 << shift; return 1 << shift;
} }
uint32_t namespace X86ISA
X86ISA::Interrupts::readRegNoEffect(ApicRegIndex reg)
{ {
return regs[reg];
ApicRegIndex
decodeAddr(Addr paddr)
{
ApicRegIndex regNum;
paddr &= ~mask(3);
switch (paddr)
{
case 0x20:
regNum = APIC_ID;
break;
case 0x30:
regNum = APIC_VERSION;
break;
case 0x80:
regNum = APIC_TASK_PRIORITY;
break;
case 0x90:
regNum = APIC_ARBITRATION_PRIORITY;
break;
case 0xA0:
regNum = APIC_PROCESSOR_PRIORITY;
break;
case 0xB0:
regNum = APIC_EOI;
break;
case 0xD0:
regNum = APIC_LOGICAL_DESTINATION;
break;
case 0xE0:
regNum = APIC_DESTINATION_FORMAT;
break;
case 0xF0:
regNum = APIC_SPURIOUS_INTERRUPT_VECTOR;
break;
case 0x100:
case 0x108:
case 0x110:
case 0x118:
case 0x120:
case 0x128:
case 0x130:
case 0x138:
case 0x140:
case 0x148:
case 0x150:
case 0x158:
case 0x160:
case 0x168:
case 0x170:
case 0x178:
regNum = APIC_IN_SERVICE((paddr - 0x100) / 0x8);
break;
case 0x180:
case 0x188:
case 0x190:
case 0x198:
case 0x1A0:
case 0x1A8:
case 0x1B0:
case 0x1B8:
case 0x1C0:
case 0x1C8:
case 0x1D0:
case 0x1D8:
case 0x1E0:
case 0x1E8:
case 0x1F0:
case 0x1F8:
regNum = APIC_TRIGGER_MODE((paddr - 0x180) / 0x8);
break;
case 0x200:
case 0x208:
case 0x210:
case 0x218:
case 0x220:
case 0x228:
case 0x230:
case 0x238:
case 0x240:
case 0x248:
case 0x250:
case 0x258:
case 0x260:
case 0x268:
case 0x270:
case 0x278:
regNum = APIC_INTERRUPT_REQUEST((paddr - 0x200) / 0x8);
break;
case 0x280:
regNum = APIC_ERROR_STATUS;
break;
case 0x300:
regNum = APIC_INTERRUPT_COMMAND_LOW;
break;
case 0x310:
regNum = APIC_INTERRUPT_COMMAND_HIGH;
break;
case 0x320:
regNum = APIC_LVT_TIMER;
break;
case 0x330:
regNum = APIC_LVT_THERMAL_SENSOR;
break;
case 0x340:
regNum = APIC_LVT_PERFORMANCE_MONITORING_COUNTERS;
break;
case 0x350:
regNum = APIC_LVT_LINT0;
break;
case 0x360:
regNum = APIC_LVT_LINT1;
break;
case 0x370:
regNum = APIC_LVT_ERROR;
break;
case 0x380:
regNum = APIC_INITIAL_COUNT;
break;
case 0x390:
regNum = APIC_CURRENT_COUNT;
break;
case 0x3E0:
regNum = APIC_DIVIDE_CONFIGURATION;
break;
default:
// A reserved register field.
panic("Accessed reserved register field %#x.\n", paddr);
break;
}
return regNum;
}
}
Tick
X86ISA::Interrupts::read(PacketPtr pkt)
{
Addr offset = pkt->getAddr() - pioAddr;
//Make sure we're at least only accessing one register.
if ((offset & ~mask(3)) != ((offset + pkt->getSize()) & ~mask(3)))
panic("Accessed more than one register at a time in the APIC!\n");
ApicRegIndex reg = decodeAddr(offset);
uint32_t val = htog(readReg(reg));
pkt->setData(((uint8_t *)&val) + (offset & mask(3)));
return latency;
}
Tick
X86ISA::Interrupts::write(PacketPtr pkt)
{
Addr offset = pkt->getAddr() - pioAddr;
//Make sure we're at least only accessing one register.
if ((offset & ~mask(3)) != ((offset + pkt->getSize()) & ~mask(3)))
panic("Accessed more than one register at a time in the APIC!\n");
ApicRegIndex reg = decodeAddr(offset);
uint32_t val = regs[reg];
pkt->writeData(((uint8_t *)&val) + (offset & mask(3)));
setReg(reg, gtoh(val));
return latency;
} }
uint32_t uint32_t
X86ISA::Interrupts::readReg(ApicRegIndex reg, ThreadContext * tc) X86ISA::Interrupts::readReg(ApicRegIndex reg)
{ {
if (reg >= APIC_TRIGGER_MODE(0) && if (reg >= APIC_TRIGGER_MODE(0) &&
reg <= APIC_TRIGGER_MODE(15)) { reg <= APIC_TRIGGER_MODE(15)) {
@ -104,24 +263,19 @@ X86ISA::Interrupts::readReg(ApicRegIndex reg, ThreadContext * tc)
break; break;
case APIC_CURRENT_COUNT: case APIC_CURRENT_COUNT:
{ {
uint32_t val = regs[reg] - tc->getCpuPtr()->curCycle(); assert(clock);
uint32_t val = regs[reg] - curTick / clock;
val /= (16 * divideFromConf(regs[APIC_DIVIDE_CONFIGURATION])); val /= (16 * divideFromConf(regs[APIC_DIVIDE_CONFIGURATION]));
return val; return val;
} }
default: default:
break; break;
} }
return readRegNoEffect(reg); return regs[reg];
} }
void void
X86ISA::Interrupts::setRegNoEffect(ApicRegIndex reg, uint32_t val) X86ISA::Interrupts::setReg(ApicRegIndex reg, uint32_t val)
{
regs[reg] = val;
}
void
X86ISA::Interrupts::setReg(ApicRegIndex reg, uint32_t val, ThreadContext *tc)
{ {
uint32_t newVal = val; uint32_t newVal = val;
if (reg >= APIC_IN_SERVICE(0) && if (reg >= APIC_IN_SERVICE(0) &&
@ -201,11 +355,24 @@ X86ISA::Interrupts::setReg(ApicRegIndex reg, uint32_t val, ThreadContext *tc)
} }
break; break;
case APIC_INITIAL_COUNT: case APIC_INITIAL_COUNT:
newVal = bits(val, 31, 0); {
regs[APIC_CURRENT_COUNT] = assert(clock);
tc->getCpuPtr()->curCycle() + newVal = bits(val, 31, 0);
(16 * divideFromConf(regs[APIC_DIVIDE_CONFIGURATION])) * newVal; uint32_t newCount = newVal *
//FIXME This should schedule the timer event. (divideFromConf(regs[APIC_DIVIDE_CONFIGURATION]) * 16);
regs[APIC_CURRENT_COUNT] = newCount + curTick / clock;
// Find out how long a "tick" of the timer should take.
Tick timerTick = 16 * clock;
// Schedule on the edge of the next tick plus the new count.
Tick offset = curTick % timerTick;
if (offset) {
reschedule(apicTimerEvent,
curTick + (newCount + 1) * timerTick - offset, true);
} else {
reschedule(apicTimerEvent,
curTick + newCount * timerTick, true);
}
}
break; break;
case APIC_CURRENT_COUNT: case APIC_CURRENT_COUNT:
//Local APIC Current Count register is read only. //Local APIC Current Count register is read only.
@ -216,7 +383,7 @@ X86ISA::Interrupts::setReg(ApicRegIndex reg, uint32_t val, ThreadContext *tc)
default: default:
break; break;
} }
setRegNoEffect(reg, newVal); regs[reg] = newVal;
return; return;
} }

View file

@ -61,6 +61,7 @@
#include "arch/x86/apicregs.hh" #include "arch/x86/apicregs.hh"
#include "arch/x86/faults.hh" #include "arch/x86/faults.hh"
#include "cpu/thread_context.hh" #include "cpu/thread_context.hh"
#include "dev/io_device.hh"
#include "params/X86LocalApic.hh" #include "params/X86LocalApic.hh"
#include "sim/eventq.hh" #include "sim/eventq.hh"
#include "sim/sim_object.hh" #include "sim/sim_object.hh"
@ -70,10 +71,12 @@ class ThreadContext;
namespace X86ISA namespace X86ISA
{ {
class Interrupts : public SimObject class Interrupts : public BasicPioDevice
{ {
protected: protected:
uint32_t regs[NUM_APIC_REGS]; uint32_t regs[NUM_APIC_REGS];
Tick latency;
Tick clock;
class ApicTimerEvent : public Event class ApicTimerEvent : public Event
{ {
@ -92,20 +95,38 @@ class Interrupts : public SimObject
public: public:
typedef X86LocalApicParams Params; typedef X86LocalApicParams Params;
void setClock(Tick newClock)
{
clock = newClock;
}
const Params * const Params *
params() const params() const
{ {
return dynamic_cast<const Params *>(_params); return dynamic_cast<const Params *>(_params);
} }
uint32_t readRegNoEffect(ApicRegIndex reg); Tick read(PacketPtr pkt);
uint32_t readReg(ApicRegIndex miscReg, ThreadContext *tc); Tick write(PacketPtr pkt);
void setRegNoEffect(ApicRegIndex reg, uint32_t val); void addressRanges(AddrRangeList &range_list)
void setReg(ApicRegIndex reg, uint32_t val, ThreadContext *tc);
Interrupts(Params * p) : SimObject(p)
{ {
range_list.clear();
range_list.push_back(RangeEx(x86LocalAPICAddress(0, 0),
x86LocalAPICAddress(0, 0) + PageBytes));
}
uint32_t readReg(ApicRegIndex miscReg);
void setReg(ApicRegIndex reg, uint32_t val);
void setRegNoEffect(ApicRegIndex reg, uint32_t val)
{
regs[reg] = val;
}
Interrupts(Params * p) : BasicPioDevice(p),
latency(p->pio_latency), clock(0)
{
pioSize = PageBytes;
//Set the local apic DFR to the flat model. //Set the local apic DFR to the flat model.
regs[APIC_DESTINATION_FORMAT] = (uint32_t)(-1); regs[APIC_DESTINATION_FORMAT] = (uint32_t)(-1);
memset(regs, 0, sizeof(regs)); memset(regs, 0, sizeof(regs));

View file

@ -119,23 +119,11 @@ MiscReg MiscRegFile::readRegNoEffect(MiscRegIndex miscReg)
!(miscReg > MISCREG_CR8 && !(miscReg > MISCREG_CR8 &&
miscReg <= MISCREG_CR15)); miscReg <= MISCREG_CR15));
if (isApicReg(miscReg)) {
panic("Can't readRegNoEffect from the local APIC.\n");
}
return regVal[miscReg]; return regVal[miscReg];
} }
MiscReg MiscRegFile::readReg(MiscRegIndex miscReg, ThreadContext * tc) MiscReg MiscRegFile::readReg(MiscRegIndex miscReg, ThreadContext * tc)
{ {
#if FULL_SYSTEM
if (isApicReg(miscReg)) {
Interrupts * interrupts = dynamic_cast<Interrupts *>(
tc->getCpuPtr()->getInterruptController());
assert(interrupts);
return interrupts->readReg(
(ApicRegIndex)(miscReg - MISCREG_APIC_START), tc);
}
#endif
if (miscReg == MISCREG_TSC) { if (miscReg == MISCREG_TSC) {
return regVal[MISCREG_TSC] + tc->getCpuPtr()->curCycle(); return regVal[MISCREG_TSC] + tc->getCpuPtr()->curCycle();
} }
@ -152,9 +140,6 @@ void MiscRegFile::setRegNoEffect(MiscRegIndex miscReg, const MiscReg &val)
miscReg < MISCREG_CR8) && miscReg < MISCREG_CR8) &&
!(miscReg > MISCREG_CR8 && !(miscReg > MISCREG_CR8 &&
miscReg <= MISCREG_CR15)); miscReg <= MISCREG_CR15));
if (isApicReg(miscReg)) {
panic("Can't setRegNoEffect from the local APIC.\n");
}
regVal[miscReg] = val; regVal[miscReg] = val;
} }
@ -162,16 +147,6 @@ void MiscRegFile::setReg(MiscRegIndex miscReg,
const MiscReg &val, ThreadContext * tc) const MiscReg &val, ThreadContext * tc)
{ {
MiscReg newVal = val; MiscReg newVal = val;
#if FULL_SYSTEM
if (isApicReg(miscReg)) {
Interrupts * interrupts = dynamic_cast<Interrupts *>(
tc->getCpuPtr()->getInterruptController());
assert(interrupts);
interrupts->setReg(
ApicRegIndex(miscReg - MISCREG_APIC_START), val, tc);
return;
}
#endif
switch(miscReg) switch(miscReg)
{ {
case MISCREG_CR0: case MISCREG_CR0:

View file

@ -58,7 +58,6 @@
#ifndef __ARCH_X86_MISCREGS_HH__ #ifndef __ARCH_X86_MISCREGS_HH__
#define __ARCH_X86_MISCREGS_HH__ #define __ARCH_X86_MISCREGS_HH__
#include "arch/x86/apicregs.hh"
#include "arch/x86/segmentregs.hh" #include "arch/x86/segmentregs.hh"
#include "arch/x86/x86_traits.hh" #include "arch/x86/x86_traits.hh"
#include "base/bitunion.hh" #include "base/bitunion.hh"
@ -369,22 +368,12 @@ namespace X86ISA
MISCREG_APIC_BASE, MISCREG_APIC_BASE,
// Space for the APIC registers
MISCREG_APIC_START,
MISCREG_APIC_END = MISCREG_APIC_START + NUM_APIC_REGS - 1,
// "Fake" MSRs for internally implemented devices // "Fake" MSRs for internally implemented devices
MISCREG_PCI_CONFIG_ADDRESS, MISCREG_PCI_CONFIG_ADDRESS,
NUM_MISCREGS NUM_MISCREGS
}; };
static inline bool
isApicReg(MiscRegIndex index)
{
return index >= MISCREG_APIC_START && index <= MISCREG_APIC_END;
}
static inline MiscRegIndex static inline MiscRegIndex
MISCREG_CR(int index) MISCREG_CR(int index)
{ {

View file

@ -97,11 +97,7 @@ namespace X86ISA
Addr offset = pkt->getAddr() & mask(3); Addr offset = pkt->getAddr() & mask(3);
MiscRegIndex index = (MiscRegIndex)(pkt->getAddr() / sizeof(MiscReg)); MiscRegIndex index = (MiscRegIndex)(pkt->getAddr() / sizeof(MiscReg));
MiscReg data; MiscReg data;
if (isApicReg(index)) { data = htog(xc->readMiscRegNoEffect(index));
data = htog(xc->readMiscReg(index));
} else {
data = htog(xc->readMiscRegNoEffect(index));
}
// Make sure we don't trot off the end of data. // Make sure we don't trot off the end of data.
assert(offset + pkt->getSize() <= sizeof(MiscReg)); assert(offset + pkt->getSize() <= sizeof(MiscReg));
pkt->writeData(((uint8_t *)&data) + offset); pkt->writeData(((uint8_t *)&data) + offset);

View file

@ -638,10 +638,9 @@ TLB::translate(RequestPtr &req, ThreadContext *tc, bool write, bool execute)
// Check for an access to the local APIC // Check for an access to the local APIC
#if FULL_SYSTEM #if FULL_SYSTEM
LocalApicBase localApicBase = tc->readMiscRegNoEffect(MISCREG_APIC_BASE); LocalApicBase localApicBase = tc->readMiscRegNoEffect(MISCREG_APIC_BASE);
Addr baseAddr = localApicBase.base << 12; Addr baseAddr = localApicBase.base * PageBytes;
Addr paddr = req->getPaddr(); Addr paddr = req->getPaddr();
if (baseAddr <= paddr && baseAddr + (1 << 12) > paddr) { if (baseAddr <= paddr && baseAddr + PageBytes > paddr) {
req->setMmapedIpr(true);
// The Intel developer's manuals say the below restrictions apply, // The Intel developer's manuals say the below restrictions apply,
// but the linux kernel, because of a compiler optimization, breaks // but the linux kernel, because of a compiler optimization, breaks
// them. // them.
@ -653,139 +652,9 @@ TLB::translate(RequestPtr &req, ThreadContext *tc, bool write, bool execute)
if (req->getSize() != (32/8)) if (req->getSize() != (32/8))
return new GeneralProtection(0); return new GeneralProtection(0);
*/ */
// Force the access to be uncacheable.
//Make sure we're at least only accessing one register. req->setFlags(req->getFlags() | UNCACHEABLE);
if ((paddr & ~mask(3)) != ((paddr + req->getSize()) & ~mask(3))) req->setPaddr(x86LocalAPICAddress(tc->readCpuId(), paddr - baseAddr));
panic("Accessed more than one register at a time in the APIC!\n");
MiscReg regNum;
Addr offset = paddr & mask(3);
paddr &= ~mask(3);
switch (paddr - baseAddr)
{
case 0x20:
regNum = APIC_ID;
break;
case 0x30:
regNum = APIC_VERSION;
break;
case 0x80:
regNum = APIC_TASK_PRIORITY;
break;
case 0x90:
regNum = APIC_ARBITRATION_PRIORITY;
break;
case 0xA0:
regNum = APIC_PROCESSOR_PRIORITY;
break;
case 0xB0:
regNum = APIC_EOI;
break;
case 0xD0:
regNum = APIC_LOGICAL_DESTINATION;
break;
case 0xE0:
regNum = APIC_DESTINATION_FORMAT;
break;
case 0xF0:
regNum = APIC_SPURIOUS_INTERRUPT_VECTOR;
break;
case 0x100:
case 0x108:
case 0x110:
case 0x118:
case 0x120:
case 0x128:
case 0x130:
case 0x138:
case 0x140:
case 0x148:
case 0x150:
case 0x158:
case 0x160:
case 0x168:
case 0x170:
case 0x178:
regNum = APIC_IN_SERVICE((paddr - baseAddr - 0x100) / 0x8);
break;
case 0x180:
case 0x188:
case 0x190:
case 0x198:
case 0x1A0:
case 0x1A8:
case 0x1B0:
case 0x1B8:
case 0x1C0:
case 0x1C8:
case 0x1D0:
case 0x1D8:
case 0x1E0:
case 0x1E8:
case 0x1F0:
case 0x1F8:
regNum = APIC_TRIGGER_MODE((paddr - baseAddr - 0x180) / 0x8);
break;
case 0x200:
case 0x208:
case 0x210:
case 0x218:
case 0x220:
case 0x228:
case 0x230:
case 0x238:
case 0x240:
case 0x248:
case 0x250:
case 0x258:
case 0x260:
case 0x268:
case 0x270:
case 0x278:
regNum = APIC_INTERRUPT_REQUEST((paddr - baseAddr - 0x200) / 0x8);
break;
case 0x280:
regNum = APIC_ERROR_STATUS;
break;
case 0x300:
regNum = APIC_INTERRUPT_COMMAND_LOW;
break;
case 0x310:
regNum = APIC_INTERRUPT_COMMAND_HIGH;
break;
case 0x320:
regNum = APIC_LVT_TIMER;
break;
case 0x330:
regNum = APIC_LVT_THERMAL_SENSOR;
break;
case 0x340:
regNum = APIC_LVT_PERFORMANCE_MONITORING_COUNTERS;
break;
case 0x350:
regNum = APIC_LVT_LINT0;
break;
case 0x360:
regNum = APIC_LVT_LINT1;
break;
case 0x370:
regNum = APIC_LVT_ERROR;
break;
case 0x380:
regNum = APIC_INITIAL_COUNT;
break;
case 0x390:
regNum = APIC_CURRENT_COUNT;
break;
case 0x3E0:
regNum = APIC_DIVIDE_CONFIGURATION;
break;
default:
// A reserved register field.
return new GeneralProtection(0);
break;
}
regNum += MISCREG_APIC_START;
req->setPaddr(regNum * sizeof(MiscReg) + offset);
} }
#endif #endif
return NoFault; return NoFault;

View file

@ -268,6 +268,8 @@ void initCPU(ThreadContext *tc, int cpuId)
interrupts->setRegNoEffect(APIC_VERSION, (5 << 16) | 0x14); interrupts->setRegNoEffect(APIC_VERSION, (5 << 16) | 0x14);
interrupts->setClock(tc->getCpuPtr()->ticks(16));
// TODO Set the SMRAM base address (SMBASE) to 0x00030000 // TODO Set the SMRAM base address (SMBASE) to 0x00030000
tc->setMiscReg(MISCREG_VM_CR, 0); tc->setMiscReg(MISCREG_VM_CR, 0);

View file

@ -55,11 +55,13 @@
* Authors: Gabe Black * Authors: Gabe Black
*/ */
#include "sim/host.hh"
#ifndef __ARCH_X86_X86TRAITS_HH__ #ifndef __ARCH_X86_X86TRAITS_HH__
#define __ARCH_X86_X86TRAITS_HH__ #define __ARCH_X86_X86TRAITS_HH__
#include <assert.h>
#include "sim/host.hh"
namespace X86ISA namespace X86ISA
{ {
const int NumMicroIntRegs = 16; const int NumMicroIntRegs = 16;
@ -90,6 +92,10 @@ namespace X86ISA
const Addr PhysAddrPrefixIO = ULL(0x8000000000000000); const Addr PhysAddrPrefixIO = ULL(0x8000000000000000);
const Addr PhysAddrPrefixPciConfig = ULL(0xC000000000000000); const Addr PhysAddrPrefixPciConfig = ULL(0xC000000000000000);
const Addr PhysAddrPrefixLocalAPIC = ULL(0xA000000000000000);
// Each APIC gets two pages. One page is used for local apics to field
// accesses from the CPU, and the other is for all APICs to communicate.
const Addr PhysAddrAPICRangeSize = 1 << 12;
static inline Addr static inline Addr
x86IOAddress(const uint32_t port) x86IOAddress(const uint32_t port)
@ -102,6 +108,13 @@ namespace X86ISA
{ {
return PhysAddrPrefixPciConfig | addr; return PhysAddrPrefixPciConfig | addr;
} }
static inline Addr
x86LocalAPICAddress(const uint8_t id, const uint16_t addr)
{
assert(addr < (1 << 12));
return PhysAddrPrefixLocalAPIC | (id * (1 << 12)) | addr;
}
} }
#endif //__ARCH_X86_X86TRAITS_HH__ #endif //__ARCH_X86_X86TRAITS_HH__

View file

@ -97,8 +97,9 @@ class BaseCPU(MemObject):
dtb = Param.X86DTB(X86DTB(), "Data TLB") dtb = Param.X86DTB(X86DTB(), "Data TLB")
itb = Param.X86ITB(X86ITB(), "Instruction TLB") itb = Param.X86ITB(X86ITB(), "Instruction TLB")
if build_env['FULL_SYSTEM']: if build_env['FULL_SYSTEM']:
interrupts = Param.X86LocalApic( _localApic = X86LocalApic(pio_addr=0xa000000000000000)
X86LocalApic(), "Interrupt Controller") interrupts = \
Param.X86LocalApic(_localApic, "Interrupt Controller")
elif build_env['TARGET_ISA'] == 'mips': elif build_env['TARGET_ISA'] == 'mips':
UnifiedTLB = Param.Bool(True, "Is this a Unified TLB?") UnifiedTLB = Param.Bool(True, "Is this a Unified TLB?")
dtb = Param.MipsDTB(MipsDTB(), "Data TLB") dtb = Param.MipsDTB(MipsDTB(), "Data TLB")
@ -141,7 +142,9 @@ class BaseCPU(MemObject):
_mem_ports = [] _mem_ports = []
if build_env['TARGET_ISA'] == 'x86' and build_env['FULL_SYSTEM']: if build_env['TARGET_ISA'] == 'x86' and build_env['FULL_SYSTEM']:
_mem_ports = ["itb.walker.port", "dtb.walker.port"] _mem_ports = ["itb.walker.port",
"dtb.walker.port",
"interrupts.pio"]
def connectMemPorts(self, bus): def connectMemPorts(self, bus):
for p in self._mem_ports: for p in self._mem_ports: