diff --git a/.github/workflows/rogue_ci.yml b/.github/workflows/rogue_ci.yml index 81487af53..1f8e2c909 100644 --- a/.github/workflows/rogue_ci.yml +++ b/.github/workflows/rogue_ci.yml @@ -240,7 +240,7 @@ jobs: uses: docker/build-push-action@v2 with: context: . - file: ./Dockerfile + file: ./docker/rogue push: true tags: tidair/rogue:${{ steps.get_image_info.outputs.tag }}, tidair/rogue:latest build-args: branch=${{ steps.get_image_info.outputs.branch }} diff --git a/LICENSE.txt b/LICENSE.txt index 833255764..ae08fda9d 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2023, The Board of Trustees of the Leland Stanford Junior +Copyright (c) 2024, The Board of Trustees of the Leland Stanford Junior University, through SLAC National Accelerator Laboratory (subject to receipt of any required approvals from the U.S. Dept. of Energy). All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/docker/rogue-anaconda/Dockerfile b/docker/rogue-anaconda/Dockerfile new file mode 100644 index 000000000..5ee89f3ae --- /dev/null +++ b/docker/rogue-anaconda/Dockerfile @@ -0,0 +1,19 @@ +FROM continuumio/anaconda3 + +RUN apt-get update && \ + apt-get install -y \ + g++ \ + gcc \ + git \ + cmake \ + make + +RUN conda config --set channel_priority strict &&\ + conda install -n base conda-libmamba-solver &&\ + conda config --set solver libmamba + +RUN conda create -n rogue_tag -c tidair-tag -c conda-forge rogue + +RUN echo "source activate rogue_tag" > ~/.bashrc + +ENV PATH /opt/conda/envs/rogue_tag/bin:$PATH diff --git a/Dockerfile b/docker/rogue/Dockerfile similarity index 96% rename from Dockerfile rename to docker/rogue/Dockerfile index 4afd12ed3..b0e02b969 100644 --- a/Dockerfile +++ b/docker/rogue/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:22.10 +FROM ubuntu:22.04 # Install system tools RUN apt-get update && apt-get install -y \ @@ -21,7 +21,7 @@ RUN pip3 install PyYAML parse click ipython pyzmq packaging matplotlib numpy p4p RUN pip3 install pydm>=1.18.0 # Install Rogue -ARG branch +ARG branch=main WORKDIR /usr/local/src RUN git clone https://github.com/slaclab/rogue.git -b $branch WORKDIR rogue diff --git a/docs/src/migration/rogue_v6.rst b/docs/src/migration/rogue_v6.rst index c5a348b1b..68a77b19b 100644 --- a/docs/src/migration/rogue_v6.rst +++ b/docs/src/migration/rogue_v6.rst @@ -18,17 +18,17 @@ Similiarly the previous feature which allowed the user to pass the root class to class ExampleRoot(pyrogue.Root): def __init__(self): - pyrogue.Root.__init__(self, - description="Example Root", - timeout=2.0, - pollEn=True) + pyrogue.Root.__init__(self, + description="Example Root", + timeout=2.0, + pollEn=True) - # Add zmq server, keep it as an attribute so we can access it later - self.zmqServer = pyrogue.interfaces.ZmqServer(root=self, addr='*', port=0) - self.addInterface(self.zmqServer) + # Add zmq server, keep it as an attribute so we can access it later + self.zmqServer = pyrogue.interfaces.ZmqServer(root=self, addr='127.0.0.1', port=0) + self.addInterface(self.zmqServer) with ExampleRoot() as root: - pyrogue.pydm.runPyDM(serverList=root.zmqServer.address,title='Test UI',sizeX=1000,sizeY=500) + pyrogue.pydm.runPyDM(serverList=root.zmqServer.address,title='Test UI',sizeX=1000,sizeY=500) More information can be found int he ZmqServer class documenation (TBD). @@ -44,13 +44,13 @@ Similiar to the zmqServer, the sql logger is now removed to be an external inter class ExampleRoot(pyrogue.Root): def __init__(self): - pyrogue.Root.__init__(self, - description="Example Root", - timeout=2.0, - pollEn=True) + pyrogue.Root.__init__(self, + description="Example Root", + timeout=2.0, + pollEn=True) - # Add sql logger - self.addInterface(pyrogue.interfaces.SqlLogger(root=self, url='sqlite:///test.db')) + # Add sql logger + self.addInterface(pyrogue.interfaces.SqlLogger(root=self, url='sqlite:///test.db')) More information can be found int he SqlLogger class documenation (TBD). @@ -66,17 +66,17 @@ In previous versions of rogue the Root class automatically supported the ability class ExampleRoot(pyrogue.Root): def __init__(self): - pyrogue.Root.__init__(self, - description="Example Root", - timeout=2.0, - pollEn=True) + pyrogue.Root.__init__(self, + description="Example Root", + timeout=2.0, + pollEn=True) - # Create configuration stream - stream = pyrogue.interfaces.stream.Variable(root=self) + # Create configuration stream + stream = pyrogue.interfaces.stream.Variable(root=self) - # Create StreamWriter with the configuration stream included as channel 1 - sw = pyrogue.utilities.fileio.StreamWriter(configStream={1: stream}) - self.add(sw) + # Create StreamWriter with the configuration stream included as channel 1 + sw = pyrogue.utilities.fileio.StreamWriter(configStream={1: stream}) + self.add(sw) EPICS Version 3 Channel Access Server @@ -89,13 +89,13 @@ Epics version 3 channel access server is removed from Rogue V6. Please use the e class ExampleRoot(pyrogue.Root): def __init__(self): - pyrogue.Root.__init__(self, - description="Example Root", - timeout=2.0, - pollEn=True) + pyrogue.Root.__init__(self, + description="Example Root", + timeout=2.0, + pollEn=True) - pvserv = pyrogue.protocols.epicsV4.EpicsPvServer(base="test", root=self,incGroups=None,excGroups=None) - self.addProtocol(pvserv) + pvserv = pyrogue.protocols.epicsV4.EpicsPvServer(base="test", root=self,incGroups=None,excGroups=None) + self.addProtocol(pvserv) RawWrite and RawRead @@ -105,4 +105,24 @@ The deprecated rawWrite and rawRead calls are removed from Rogue V6. The new arr +Setting pollInterval +==================== + +There API for setting a Variable's pollInterval has +changed. Previously, it could be set directly: + +.. code:: + + someVar.pollInterval = 5 # Poll someVar every 5 seconds + +This has been deprecated in favor of: + +.. code:: + + someVar.setPollInterval(5) # Poll someVar every 5 seconds + + +The reasoning is that a lot happens behind the scences when changing a +poll interval, and masking this with a setter decorator gives the user +the impression that it is much simpler than it is. diff --git a/python/pyrogue/_Device.py b/python/pyrogue/_Device.py index 2450613e1..c5e2a3ffe 100644 --- a/python/pyrogue/_Device.py +++ b/python/pyrogue/_Device.py @@ -442,7 +442,7 @@ def setPollInterval(self, interval, variables=None): variables = [k for k,v in self.variables.items() if v.pollInterval != 0] for x in variables: - self.node(x).pollInterval = interval + self.node(x).setPollInterval(interval) def hideVariables(self, hidden, variables=None): """ diff --git a/python/pyrogue/_Node.py b/python/pyrogue/_Node.py index 6a4576258..3395656a4 100644 --- a/python/pyrogue/_Node.py +++ b/python/pyrogue/_Node.py @@ -154,6 +154,7 @@ class Node(object): ------- """ + _nodeCount = 0 def __init__(self, *, name, description="", expand=True, hidden=False, groups=None, guiGroup=None): """ @@ -183,6 +184,7 @@ def __init__(self, *, name, description="", expand=True, hidden=False, groups=No ------- """ + pr.Node._nodeCount += 1 # Public attributes self._name = name @@ -213,6 +215,10 @@ def __init__(self, *, name, description="", expand=True, hidden=False, groups=No def __repr__(self): return f'{self.__class__} - {self.path}' + @property + def nodeCount(self): + return pr.Node._nodeCount + @property def name(self): """ @@ -559,7 +565,7 @@ def addNodes(self, nodeClass, number, stride, **kwargs): def nodeList(self): """Get a recursive list of nodes.""" lst = [] - for key,value in self.nodes.items(): + for key,value in self._nodes.items(): lst.append(value) lst.extend(value.nodeList) return lst diff --git a/python/pyrogue/examples/_ExampleRoot.py b/python/pyrogue/examples/_ExampleRoot.py index 36357b3e3..89a6cfbc3 100755 --- a/python/pyrogue/examples/_ExampleRoot.py +++ b/python/pyrogue/examples/_ExampleRoot.py @@ -73,7 +73,7 @@ def __init__(self, epics4En=False): self.add(pyrogue.RunControl()) # Add zmq server - self.zmqServer = pyrogue.interfaces.ZmqServer(root=self, addr='*', port=0) + self.zmqServer = pyrogue.interfaces.ZmqServer(root=self, addr='127.0.0.1', port=0) self.addInterface(self.zmqServer) # Add sql logger diff --git a/python/pyrogue/interfaces/_ZmqServer.py b/python/pyrogue/interfaces/_ZmqServer.py index 6f8b0246d..5fcf76422 100644 --- a/python/pyrogue/interfaces/_ZmqServer.py +++ b/python/pyrogue/interfaces/_ZmqServer.py @@ -20,12 +20,16 @@ class ZmqServer(rogue.interfaces.ZmqServer): def __init__(self,*,root,addr,port,incGroups=None, excGroups=['NoServe']): rogue.interfaces.ZmqServer.__init__(self,addr,port) self._root = root + self._addr = addr self._root.addVarListener(func=self._varUpdate, done=self._varDone, incGroups=incGroups, excGroups=excGroups) self._updateList = {} @property def address(self): - return f"localhost:{self.port()}" + if self._addr == "*": + return f"127.0.0.1:{self.port()}" + else: + return f"{self._addr}:{self.port()}" def _doOperation(self,d): path = d['path'] if 'path' in d else None diff --git a/python/pyrogue/protocols/epicsV4.py b/python/pyrogue/protocols/epicsV4.py index 9ac333673..c9776eddf 100644 --- a/python/pyrogue/protocols/epicsV4.py +++ b/python/pyrogue/protocols/epicsV4.py @@ -1,11 +1,11 @@ -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- # Title : PyRogue epics support -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- # Description: # Module containing epics support classes and routines # TODO: # Not clear on to force a read on get -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- # This file is part of the rogue software platform. It is subject to # the license terms in the LICENSE.txt file found in the top-level directory # of this distribution and at: @@ -13,7 +13,7 @@ # No part of the rogue software platform, including this file, may be # copied, modified, propagated, or distributed except according to the terms # contained in the LICENSE.txt file. -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- import pyrogue import time @@ -30,22 +30,22 @@ def EpicsConvStatus(varValue): if varValue.status == "AlarmLoLo": - return 5 # epicsAlarmLoLo + return 5 # epicsAlarmLoLo elif varValue.status == "AlarmLow": - return 6 # epicsAlarmLow + return 6 # epicsAlarmLow elif varValue.status == "AlarmHiHi": - return 3 # epicsAlarmHiHi + return 3 # epicsAlarmHiHi elif varValue.status == "AlarmHigh": - return 4 # epicsAlarmHigh + return 4 # epicsAlarmHigh else: return 0 def EpicsConvSeverity(varValue): if varValue.severity == "AlarmMinor": - return 1 # epicsSevMinor + return 1 # epicsSevMinor elif varValue.severity == "AlarmMajor": - return 2 # epicsSevMajor + return 2 # epicsSevMajor else: return 0 @@ -53,11 +53,12 @@ def EpicsConvSeverity(varValue): class EpicsPvHandler(p4p.server.thread.Handler): def __init__(self, valType, var, log): self._valType = valType - self._var = var - self._log = log + self._var = var + self._log = log def put(self, pv, op): - if self._var.isVariable and (self._var.mode == 'RW' or self._var.mode == 'WO'): + if self._var.isVariable and ( + self._var.mode == 'RW' or self._var.mode == 'WO'): try: if self._valType == 'enum': self._var.setDisp(str(op.value())) @@ -92,7 +93,8 @@ def rpc(self, pv, op): if ret is None: ret = 'None' - v = p4p.Value(p4p.Type([('value',self._valType)]), {'value':ret}) + v = p4p.Value( + p4p.Type([('value', self._valType)]), {'value': ret}) op.done(value=(v)) except Exception as msg: @@ -101,21 +103,21 @@ def rpc(self, pv, op): else: op.done(error='Rpc Not Supported On Variables') - def onFirstConnect(self, pv): # may be omitted - #print(f"PV First connect called pv={pv}") + def onFirstConnect(self, pv): # may be omitted + # print(f"PV First connect called pv={pv}") pass - def onLastDisconnect(self, pv): # may be omitted - #print(f"PV Last Disconnect called pv={pv}") + def onLastDisconnect(self, pv): # may be omitted + # print(f"PV Last Disconnect called pv={pv}") pass class EpicsPvHolder(object): - def __init__(self,provider,name,var,log): - self._var = var + def __init__(self, provider, name, var, log): + self._var = var self._name = name - self._log = log - self._pv = None + self._log = log + self._pv = None if self._var.isCommand: typeStr = self._var.retTypeStr @@ -125,7 +127,7 @@ def __init__(self,provider,name,var,log): # Convert valType if var.nativeType is np.ndarray: self._valType = 'ndarray' - #self._valType = 's' + # self._valType = 's' elif self._var.disp == 'enum': self._valType = 'enum' elif typeStr is None or var.nativeType is list or var.nativeType is dict: @@ -134,7 +136,7 @@ def __init__(self,provider,name,var,log): # Unsigned if 'UInt' in typeStr: - m = re.search('^UInt(\\d+)\\.*',typeStr) + m = re.search('^UInt(\\d+)\\.*', typeStr) if m is not None and m.lastindex == 1: bits = int(m[1]) @@ -154,7 +156,7 @@ def __init__(self,provider,name,var,log): # Signed elif 'int' in typeStr or 'Int' in typeStr: - m = re.search('^Int(\\d+)\\.*',typeStr) + m = re.search('^Int(\\d+)\\.*', typeStr) if m is not None and m.lastindex == 1: bits = int(m[1]) @@ -191,37 +193,66 @@ def __init__(self,provider,name,var,log): if varVal.valueDisp is None: varVal.valueDisp = '' - self._log.info("Adding {} with type {} init={}".format(self._name,self._valType,varVal.valueDisp)) + self._log.info( + "Adding {} with type {} init={}".format( + self._name, + self._valType, + varVal.valueDisp)) try: if self._valType == 'ndarray': - nt = p4p.nt.NTNDArray() - #iv = nt.wrap(varVal.value) + # If a 1D array is encountered, use a NTScalar. Note, if an + # NTScalar is used, the values will be automatically converted + # to doubles. + if varVal.value.ndim == 1: + nt = p4p.nt.NTScalar('ad') + else: + nt = p4p.nt.NTNDArray() iv = varVal.value elif self._valType == 'enum': - nt = p4p.nt.NTEnum(display=False, control=False, valueAlarm=False) + nt = p4p.nt.NTEnum( + display=False, + control=False, + valueAlarm=False) enum = list(self._var.enum.values()) - iv = {'choices':enum, 'index':enum.index(varVal.valueDisp)} + iv = {'choices': enum, 'index': enum.index(varVal.valueDisp)} elif self._valType == 's': - nt = p4p.nt.NTScalar(self._valType, display=False, control=False, valueAlarm=False) + nt = p4p.nt.NTScalar( + self._valType, + display=False, + control=False, + valueAlarm=False) iv = nt.wrap(varVal.valueDisp) else: - nt = p4p.nt.NTScalar(self._valType, display=True, control=True, valueAlarm=True) - #print(f"Setting value {varVal.value} to {self._name}") + nt = p4p.nt.NTScalar( + self._valType, + display=True, + control=True, + valueAlarm=True) + # print(f"Setting value {varVal.value} to {self._name}") iv = nt.wrap(varVal.value) except Exception as e: - raise Exception("Failed to add {} with type {} ndtype={} init={}. Error={}".format(self._name,self._valType,self._var.ndType,varVal.valueDisp,e)) + raise Exception( + "Failed to add {} with type {} ndtype={} init={}. Error={}".format( + self._name, + self._valType, + self._var.ndType, + varVal.valueDisp, + e)) # Setup variable try: - self._pv = p4p.server.thread.SharedPV(queue=None, - handler=EpicsPvHandler(self._valType,self._var,self._log), - initial=iv, - nt=nt, - options={}) + self._pv = p4p.server.thread.SharedPV(queue=None, handler=EpicsPvHandler( + self._valType, self._var, self._log), initial=iv, nt=nt, options={}) except Exception as e: - raise Exception("Failed to start {} with type {} ndtype={} init={}. Error={}".format(self._name,self._valType,self._var.ndType,varVal.valueDisp,e)) - - provider.add(self._name,self._pv) + raise Exception( + "Failed to start {} with type {} ndtype={} init={}. Error={}".format( + self._name, + self._valType, + self._var.ndType, + varVal.valueDisp, + e)) + + provider.add(self._name, self._pv) self._var.addListener(self._varUpdated) # Update fields in numeric types @@ -232,36 +263,37 @@ def __init__(self,provider,name,var,log): curr.raw.alarm.severity = EpicsConvSeverity(varVal) curr.raw.display.description = self._var.description - if self._var.units is not None: - curr.raw.display.units = self._var.units - if self._var.maximum is not None: + if self._var.units is not None: + curr.raw.display.units = self._var.units + if self._var.maximum is not None: curr.raw.display.limitHigh = self._var.maximum - if self._var.minimum is not None: - curr.raw.display.limitLow = self._var.minimum + if self._var.minimum is not None: + curr.raw.display.limitLow = self._var.minimum - if self._var.lowWarning is not None: - curr.raw.valueAlarm.lowWarningLimit = self._var.lowWarning - if self._var.lowAlarm is not None: - curr.raw.valueAlarm.lowAlarmLimit = self._var.lowAlarm + if self._var.lowWarning is not None: + curr.raw.valueAlarm.lowWarningLimit = self._var.lowWarning + if self._var.lowAlarm is not None: + curr.raw.valueAlarm.lowAlarmLimit = self._var.lowAlarm if self._var.highWarning is not None: - curr.raw.valueAlarm.highWarningLimit = self._var.highWarning - if self._var.highAlarm is not None: - curr.raw.valueAlarm.highAlarmLimit = self._var.highAlarm + curr.raw.valueAlarm.highWarningLimit = self._var.highWarning + if self._var.highAlarm is not None: + curr.raw.valueAlarm.highAlarmLimit = self._var.highAlarm # Precision ? self._pv.post(curr) - def _varUpdated(self,path,value): + def _varUpdated(self, path, value): if self._valType == 'enum' or self._valType == 's': self._pv.post(value.valueDisp) elif self._valType == 'ndarray': self._pv.post(value.value) else: curr = self._pv.current() - curr.raw.value = value.value - curr.raw.alarm.status = EpicsConvStatus(value) + curr.raw.value = value.value + curr.raw.alarm.status = EpicsConvStatus(value) curr.raw.alarm.severity = EpicsConvSeverity(value) - curr.raw['timeStamp.secondsPastEpoch'], curr.raw['timeStamp.nanoseconds'] = divmod(float(time.time_ns()), 1.0e9) + curr.raw['timeStamp.secondsPastEpoch'], curr.raw['timeStamp.nanoseconds'] = divmod( + float(time.time_ns()), 1.0e9) self._pv.post(curr) @@ -269,15 +301,16 @@ class EpicsPvServer(object): """ Class to contain an epics PV server """ - def __init__(self,*,base,root,incGroups,excGroups,pvMap=None): - self._root = root - self._base = base - self._log = pyrogue.logInit(cls=self) - self._server = None + + def __init__(self, *, base, root, incGroups, excGroups, pvMap=None): + self._root = root + self._base = base + self._log = pyrogue.logInit(cls=self) + self._server = None self._incGroups = incGroups self._excGroups = excGroups - self._pvMap = pvMap - self._started = False + self._pvMap = pvMap + self._started = False self._provider = p4p.server.StaticProvider(__name__) @@ -296,7 +329,8 @@ def _start(self): self._list = {} if not self._root.running: - raise Exception("Epics can not be setup on a tree which is not started") + raise Exception( + "Epics can not be setup on a tree which is not started") # Figure out mapping mode if self._pvMap is None: @@ -310,35 +344,36 @@ def _start(self): eName = None if doAll: - if v.filterByGroup(self._incGroups,self._excGroups): - eName = self._base + ':' + v.path.replace('.',':') + if v.filterByGroup(self._incGroups, self._excGroups): + eName = self._base + ':' + v.path.replace('.', ':') self._pvMap[v.path] = eName elif v.path in self._pvMap: eName = self._pvMap[v.path] if eName is not None: - pvh = EpicsPvHolder(self._provider,eName,v,self._log) + pvh = EpicsPvHolder(self._provider, eName, v, self._log) self._list[v] = pvh # Check for missing map entries if len(self._pvMap) != len(self._list): - for k,v in self._pvMap.items(): + for k, v in self._pvMap.items(): if k not in self._list: - self._log.error(f"Failed to find {k} from P4P mapping in Rogue tree!") + self._log.error( + f"Failed to find {k} from P4P mapping in Rogue tree!") self._server = p4p.server.Server(providers=[self._provider]) def list(self): return self._pvMap - def dump(self,fname=None): + def dump(self, fname=None): if fname is not None: try: - with open(fname,'w') as f: - for k,v in self._pvMap.items(): - print("{} -> {}".format(v,k),file=f) + with open(fname, 'w') as f: + for k, v in self._pvMap.items(): + print("{} -> {}".format(v, k), file=f) except Exception: raise Exception("Failed to dump epics map to {}".format(fname)) else: - for k,v in self._pvMap.items(): - print("{} -> {}".format(v,k)) + for k, v in self._pvMap.items(): + print("{} -> {}".format(v, k)) diff --git a/src/rogue/interfaces/memory/Block.cpp b/src/rogue/interfaces/memory/Block.cpp index 5e3a106e5..f717bf465 100644 --- a/src/rogue/interfaces/memory/Block.cpp +++ b/src/rogue/interfaces/memory/Block.cpp @@ -148,7 +148,7 @@ uint64_t rim::Block::offset() { // Get full address of this Block uint64_t rim::Block::address() { - return (reqAddress() | offset_); + return (reqAddress() + offset_); } // Get size of this block in bytes. diff --git a/src/rogue/interfaces/memory/Hub.cpp b/src/rogue/interfaces/memory/Hub.cpp index 1f572c318..708d05ea8 100644 --- a/src/rogue/interfaces/memory/Hub.cpp +++ b/src/rogue/interfaces/memory/Hub.cpp @@ -64,7 +64,7 @@ uint64_t rim::Hub::getOffset() { //! Get address uint64_t rim::Hub::getAddress() { - return (reqAddress() | offset_); + return (reqAddress() + offset_); } //! Return id to requesting master @@ -105,7 +105,7 @@ uint64_t rim::Hub::doAddress() { if (root_) return (0); else - return (reqAddress() | offset_); + return (reqAddress() + offset_); } //! Post a transaction. Master will call this method with the access attributes. @@ -113,7 +113,7 @@ void rim::Hub::doTransaction(rim::TransactionPtr tran) { uint32_t maxAccess = getSlave()->doMaxAccess(); // Adjust address - tran->address_ |= offset_; + tran->address_ += offset_; // Split into smaller transactions if necessary if (tran->size() > maxAccess) { diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 000000000..1cda54be9 --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1 @@ +*.yml diff --git a/tests/test_block_overlap.py b/tests/test_block_overlap.py index 4025ea20c..148c3d414 100644 --- a/tests/test_block_overlap.py +++ b/tests/test_block_overlap.py @@ -13,6 +13,24 @@ #rogue.Logging.setLevel(rogue.Logging.Debug) +class SimpleVarDevice(pr.Device): + def __init__(self, **kwargs): + super().__init__(**kwargs) + + self.add(pr.RemoteVariable( + name = 'Var1', + offset = 0x0, + bitSize = 32, + bitOffset = 0, + )) + + self.add(pr.RemoteVariable( + name = 'Var2', + offset = 0x4, + bitSize = 32, + bitOffset = 0, + )) + class BlockDevice(pr.Device): def __init__(self, **kwargs): super().__init__(**kwargs) @@ -85,7 +103,12 @@ def __init__(self): self.addInterface(sim) self.add(BlockDevice( - offset = 0, + offset = 0x0000, + memBase = sim, + )) + + self.add(SimpleVarDevice( + offset = 0xF034, # non-4kB aligned base offset memBase = sim, ))