| /* |
| * Copyright (c) 2017 Jason Lowe-Power |
| * All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are |
| * met: redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer; |
| * redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution; |
| * neither the name of the copyright holders nor the names of its |
| * contributors may be used to endorse or promote products derived from |
| * this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| /** |
| * This file contains a simple example MSI protocol. |
| * |
| * The protocol in this file is based off of the MSI protocol found in |
| * A Primer on Memory Consistency and Cache Coherence |
| * Daniel J. Sorin, Mark D. Hill, and David A. Wood |
| * Synthesis Lectures on Computer Architecture 2011 6:3, 141-149 |
| * |
| * Table 8.1 contains the transitions and actions found in this file and |
| * section 8.2.4 explains the protocol in detail. |
| * |
| * See Learning gem5 Part 3: Ruby for more details. |
| */ |
| |
| /// Declare a machine with type L1Cache. |
| machine(MachineType:L1Cache, "MSI cache") |
| : Sequencer *sequencer; // Incoming request from CPU come from this |
| CacheMemory *cacheMemory; // This stores the data and cache states |
| bool send_evictions; // Needed to support O3 CPU and mwait |
| |
| // Other declarations |
| // Message buffers are required to send and receive data from the Ruby |
| // network. The from/to and request/response can be confusing! |
| // Virtual networks are needed to prevent deadlock (e.g., it is bad if a |
| // response gets stuck behind a stalled request). In this protocol, we are |
| // using three virtual networks. The highest priority is responses, |
| // followed by forwarded requests, then requests have the lowest priority. |
| |
| // Requests *to* the directory |
| MessageBuffer * requestToDir, network="To", virtual_network="0", |
| vnet_type="request"; |
| // Responses *to* the directory or other caches |
| MessageBuffer * responseToDirOrSibling, network="To", virtual_network="2", |
| vnet_type="response"; |
| |
| // Requests *from* the directory for fwds, invs, and put acks. |
| MessageBuffer * forwardFromDir, network="From", virtual_network="1", |
| vnet_type="forward"; |
| // Responses *from* directory and other caches for this cache's reqs. |
| MessageBuffer * responseFromDirOrSibling, network="From", |
| virtual_network="2", vnet_type="response"; |
| |
| // This is all of the incoming requests from the core via the sequencer |
| MessageBuffer * mandatoryQueue; |
| { |
| // Declare the states that this cache will use. These are both stable |
| // states (no underscore) and transient states (with underscore). Letters |
| // after the underscores are superscript in Sorin et al. |
| // Underscores and "desc" are used when generating HTML tables. |
| // Access permissions are used for functional accesses. For reads, the |
| // functional access reads *all* of the blocks with a matching address that |
| // have read-only or read-write permission. For functional writes, all |
| // blocks are updated with new data if they have busy, read-only, or |
| // read-write permission. |
| state_declaration(State, desc="Cache states") { |
| I, AccessPermission:Invalid, |
| desc="Not present/Invalid"; |
| |
| // States moving out of I |
| IS_D, AccessPermission:Invalid, |
| desc="Invalid, moving to S, waiting for data"; |
| IM_AD, AccessPermission:Invalid, |
| desc="Invalid, moving to M, waiting for acks and data"; |
| IM_A, AccessPermission:Busy, |
| desc="Invalid, moving to M, waiting for acks"; |
| |
| S, AccessPermission:Read_Only, |
| desc="Shared. Read-only, other caches may have the block"; |
| |
| // States moving out of S |
| SM_AD, AccessPermission:Read_Only, |
| desc="Shared, moving to M, waiting for acks and 'data'"; |
| SM_A, AccessPermission:Read_Only, |
| desc="Shared, moving to M, waiting for acks"; |
| |
| M, AccessPermission:Read_Write, |
| desc="Modified. Read & write permissions. Owner of block"; |
| |
| // States moving to Invalid |
| MI_A, AccessPermission:Busy, |
| desc="Was modified, moving to I, waiting for put ack"; |
| SI_A, AccessPermission:Busy, |
| desc="Was shared, moving to I, waiting for put ack"; |
| II_A, AccessPermission:Invalid, |
| desc="Sent valid data before receiving put ack. "; |
| //"Waiting for put ack."; |
| } |
| |
| // Events that can be triggered on incoming messages. These are the events |
| // that will trigger transitions |
| enumeration(Event, desc="Cache events") { |
| // From the processor/sequencer/mandatory queue |
| Load, desc="Load from processor"; |
| Store, desc="Store from processor"; |
| |
| // Internal event (only triggered from processor requests) |
| Replacement, desc="Triggered when block is chosen as victim"; |
| |
| // Forwarded reqeust from other cache via dir on the forward network |
| FwdGetS, desc="Directory sent us a request to satisfy GetS. "; |
| //"We must have the block in M to respond to this."; |
| FwdGetM, desc="Directory sent us a request to satisfy GetM. "; |
| //"We must have the block in M to respond to this."; |
| Inv, desc="Invalidate from the directory."; |
| PutAck, desc="Response from directory after we issue a put. "; |
| //"This must be on the fwd network to avoid"; |
| //"deadlock."; |
| |
| // Responses from directory |
| DataDirNoAcks, desc="Data from directory (acks = 0)"; |
| DataDirAcks, desc="Data from directory (acks > 0)"; |
| |
| // Responses from other caches |
| DataOwner, desc="Data from owner"; |
| InvAck, desc="Invalidation ack from other cache after Inv"; |
| |
| // Special internally triggered event to simplify implementation |
| LastInvAck, desc="Triggered after the last ack is received"; |
| } |
| |
| // A structure for the cache entry. This stores the cache data and state |
| // as defined above. You can put any other information here you like. |
| // The AbstractCacheEntry is defined in |
| // src/mem/ruby/slic_interface/AbstractCacheEntry.hh |
| // If you want to use any of the functions in the abstract entry declare |
| // them here. |
| structure(Entry, desc="Cache entry", interface="AbstractCacheEntry") { |
| State CacheState, desc="cache state"; |
| DataBlock DataBlk, desc="Data in the block"; |
| } |
| |
| // TBE is the "transaction buffer entry". This stores information needed |
| // during transient states. This is *like* an MSHR. It functions as an MSHR |
| // in this protocol, but the entry is also allocated for other uses. |
| structure(TBE, desc="Entry for transient requests") { |
| State TBEState, desc="State of block"; |
| DataBlock DataBlk, desc="Data for the block. Needed for MI_A"; |
| int AcksOutstanding, default=0, desc="Number of acks left to receive."; |
| } |
| |
| // Table of TBE entries. This is defined externally in |
| // src/mem/ruby/structures/TBETable.hh. It is templatized on the TBE |
| // structure defined above. |
| structure(TBETable, external="yes") { |
| TBE lookup(Addr); |
| void allocate(Addr); |
| void deallocate(Addr); |
| bool isPresent(Addr); |
| } |
| |
| /*************************************************************************/ |
| // Some declarations of member functions and member variables. |
| |
| // The TBE table for this machine. It is templatized under the covers. |
| // NOTE: SLICC mangles names with the machine type. Thus, the TBE declared |
| // above will be L1Cache_TBE in C++. |
| // We also have to pass through a parameter to the machine to the TBETable. |
| TBETable TBEs, template="<L1Cache_TBE>", constructor="m_number_of_TBEs"; |
| |
| // Declare all of the functions of the AbstractController that we may use |
| // in this file. |
| // Functions from clocked object |
| Tick clockEdge(); |
| |
| // Functions we must use to set things up for the transitions to execute |
| // correctly. |
| // These next set/unset functions are used to populate the implicit |
| // variables used in actions. This is required when a transition has |
| // multiple actions. |
| void set_cache_entry(AbstractCacheEntry a); |
| void unset_cache_entry(); |
| void set_tbe(TBE b); |
| void unset_tbe(); |
| |
| // Given an address and machine type this queries the network to check |
| // where it should be sent. In a real implementation, this might be fixed |
| // at design time, but this function gives us flexibility at runtime. |
| // For example, if you have multiple memory channels, this function will |
| // tell you which addresses to send to which memory controller. |
| MachineID mapAddressToMachine(Addr addr, MachineType mtype); |
| |
| // Convience function to look up the cache entry. |
| // Needs a pointer so it will be a reference and can be updated in actions |
| Entry getCacheEntry(Addr address), return_by_pointer="yes" { |
| return static_cast(Entry, "pointer", cacheMemory.lookup(address)); |
| } |
| |
| /*************************************************************************/ |
| // Functions that we need to define/override to use our specific structures |
| // in this implementation. |
| |
| // Required function for getting the current state of the block. |
| // This is called from the transition to know which transition to execute |
| State getState(TBE tbe, Entry cache_entry, Addr addr) { |
| // The TBE state will override the state in cache memory, if valid |
| if (is_valid(tbe)) { return tbe.TBEState; } |
| // Next, if the cache entry is valid, it holds the state |
| else if (is_valid(cache_entry)) { return cache_entry.CacheState; } |
| // If the block isn't present, then it's state must be I. |
| else { return State:I; } |
| } |
| |
| |
| // Required function for setting the current state of the block. |
| // This is called from the transition to set the ending state. |
| // Needs to set both the TBE and the cache entry state. |
| // This is also called when transitioning to I so it's possible the TBE and/ |
| // or the cache_entry is invalid. |
| void setState(TBE tbe, Entry cache_entry, Addr addr, State state) { |
| if (is_valid(tbe)) { tbe.TBEState := state; } |
| if (is_valid(cache_entry)) { cache_entry.CacheState := state; } |
| } |
| |
| // Required function to override. Used for functional access to know where |
| // the valid data is. NOTE: L1Cache_State_to_permission is automatically |
| // created based on the access permissions in the state_declaration. |
| // This is mangled by both the MachineType and the name of the state |
| // declaration ("State" in this case) |
| AccessPermission getAccessPermission(Addr addr) { |
| TBE tbe := TBEs[addr]; |
| if(is_valid(tbe)) { |
| return L1Cache_State_to_permission(tbe.TBEState); |
| } |
| |
| Entry cache_entry := getCacheEntry(addr); |
| if(is_valid(cache_entry)) { |
| return L1Cache_State_to_permission(cache_entry.CacheState); |
| } |
| |
| return AccessPermission:NotPresent; |
| } |
| |
| // Required function to override. Like above function, but sets thte state. |
| void setAccessPermission(Entry cache_entry, Addr addr, State state) { |
| if (is_valid(cache_entry)) { |
| cache_entry.changePermission(L1Cache_State_to_permission(state)); |
| } |
| } |
| |
| // Required function to override for functionally reading/writing data. |
| // NOTE: testAndRead/Write defined in src/mem/ruby/slicc_interface/Util.hh |
| void functionalRead(Addr addr, Packet *pkt) { |
| TBE tbe := TBEs[addr]; |
| if(is_valid(tbe)) { |
| testAndRead(addr, tbe.DataBlk, pkt); |
| } else { |
| testAndRead(addr, getCacheEntry(addr).DataBlk, pkt); |
| } |
| } |
| |
| int functionalWrite(Addr addr, Packet *pkt) { |
| TBE tbe := TBEs[addr]; |
| if(is_valid(tbe)) { |
| if (testAndWrite(addr, tbe.DataBlk, pkt)) { |
| return 1; |
| } else { |
| return 0; |
| } |
| } else { |
| if (testAndWrite(addr, getCacheEntry(addr).DataBlk, pkt)) { |
| return 1; |
| } else { |
| return 0; |
| } |
| } |
| } |
| |
| /*************************************************************************/ |
| // Input/output network definitions |
| |
| // Output ports. This defines the message types that will flow ocross the |
| // output buffers as defined above. These must be "to" networks. |
| // "request_out" is the name we'll use later to send requests. |
| // "RequestMsg" is the message type we will send (see MSI-msg.sm) |
| // "requestToDir" is the name of the MessageBuffer declared above that |
| // we are sending these requests out of. |
| out_port(request_out, RequestMsg, requestToDir); |
| out_port(response_out, ResponseMsg, responseToDirOrSibling); |
| |
| // Input ports. The order here is/(can be) important. The code in each |
| // in_port is executed in the order specified in this file (or by the rank |
| // parameter). Thus, we must sort these based on the network priority. |
| // In this cache, the order is responses from other caches, forwards, then |
| // requests from the CPU. |
| |
| // Like the out_port above |
| // "response_in" is the name we'll use later when we refer to this port |
| // "ResponseMsg" is the type of message we expect on this port |
| // "responseFromDirOrSibling" is the name of the buffer this in_port is |
| // connected to for responses from other caches and the directory. |
| in_port(response_in, ResponseMsg, responseFromDirOrSibling) { |
| // NOTE: You have to check to make sure the message buffer has a valid |
| // message at the head. The code in in_port is executed either way. |
| if (response_in.isReady(clockEdge())) { |
| // Peek is a special function. Any code inside a peek statement has |
| // a special variable declared and populated: in_msg. This contains |
| // the message (of type RequestMsg in this case) at the head. |
| // "forward_in" is the port we want to peek into |
| // "RequestMsg" is the type of message we expect. |
| peek(response_in, ResponseMsg) { |
| // Grab the entry and tbe if they exist. |
| Entry cache_entry := getCacheEntry(in_msg.addr); |
| TBE tbe := TBEs[in_msg.addr]; |
| // The TBE better exist since this is a response and we need to |
| // be able to check the remaining acks. |
| assert(is_valid(tbe)); |
| |
| // If it's from the directory... |
| if (machineIDToMachineType(in_msg.Sender) == |
| MachineType:Directory) { |
| if (in_msg.Type != CoherenceResponseType:Data) { |
| error("Directory should only reply with data"); |
| } |
| // Take the in_msg acks and add (sub) the Acks we've seen. |
| // The InvAck will decrement the acks we're waiting for in |
| // tbe.AcksOutstanding to below 0 if we haven't gotten the |
| // dir resp yet. So, if this is 0 we don't need to wait |
| assert(in_msg.Acks + tbe.AcksOutstanding >= 0); |
| if (in_msg.Acks + tbe.AcksOutstanding == 0) { |
| trigger(Event:DataDirNoAcks, in_msg.addr, cache_entry, |
| tbe); |
| } else { |
| // If it's not 0, then we need to wait for more acks |
| // and we'll trigger LastInvAck later. |
| trigger(Event:DataDirAcks, in_msg.addr, cache_entry, |
| tbe); |
| } |
| } else { |
| // This is from another cache. |
| if (in_msg.Type == CoherenceResponseType:Data) { |
| trigger(Event:DataOwner, in_msg.addr, cache_entry, |
| tbe); |
| } else if (in_msg.Type == CoherenceResponseType:InvAck) { |
| DPRINTF(RubySlicc, "Got inv ack. %d left\n", |
| tbe.AcksOutstanding); |
| if (tbe.AcksOutstanding == 1) { |
| // If there is exactly one ack remaining then we |
| // know it is the last ack. |
| trigger(Event:LastInvAck, in_msg.addr, cache_entry, |
| tbe); |
| } else { |
| trigger(Event:InvAck, in_msg.addr, cache_entry, |
| tbe); |
| } |
| } else { |
| error("Unexpected response from other cache"); |
| } |
| } |
| } |
| } |
| } |
| |
| // Forward requests for other caches. |
| in_port(forward_in, RequestMsg, forwardFromDir) { |
| if (forward_in.isReady(clockEdge())) { |
| peek(forward_in, RequestMsg) { |
| // Grab the entry and tbe if they exist. |
| Entry cache_entry := getCacheEntry(in_msg.addr); |
| TBE tbe := TBEs[in_msg.addr]; |
| |
| if (in_msg.Type == CoherenceRequestType:GetS) { |
| // This is a special function that will trigger a |
| // transition (as defined below). It *must* have these |
| // parameters. |
| trigger(Event:FwdGetS, in_msg.addr, cache_entry, tbe); |
| } else if (in_msg.Type == CoherenceRequestType:GetM) { |
| trigger(Event:FwdGetM, in_msg.addr, cache_entry, tbe); |
| } else if (in_msg.Type == CoherenceRequestType:Inv) { |
| trigger(Event:Inv, in_msg.addr, cache_entry, tbe); |
| } else if (in_msg.Type == CoherenceRequestType:PutAck) { |
| trigger(Event:PutAck, in_msg.addr, cache_entry, tbe); |
| } else { |
| error("Unexpected forward message!"); |
| } |
| } |
| } |
| } |
| |
| // The "mandatory queue" is the port/queue from the CPU or other processor. |
| // This is *always* a RubyRequest |
| in_port(mandatory_in, RubyRequest, mandatoryQueue) { |
| if (mandatory_in.isReady(clockEdge())) { |
| // Block all requests if there is already an outstanding request |
| // that has the same line address. This is unblocked when we |
| // finally respond to the request. |
| peek(mandatory_in, RubyRequest, block_on="LineAddress") { |
| // NOTE: Using LineAddress here to promote smaller requests to |
| // full cache block requests. |
| Entry cache_entry := getCacheEntry(in_msg.LineAddress); |
| TBE tbe := TBEs[in_msg.LineAddress]; |
| // If there isn't a matching entry and no room in the cache, |
| // then we need to find a victim. |
| if (is_invalid(cache_entry) && |
| cacheMemory.cacheAvail(in_msg.LineAddress) == false ) { |
| // make room for the block |
| // The "cacheProbe" function looks at the cache set for |
| // the address and queries the replacement protocol for |
| // the address to replace. It returns the address to repl. |
| Addr addr := cacheMemory.cacheProbe(in_msg.LineAddress); |
| Entry victim_entry := getCacheEntry(addr); |
| TBE victim_tbe := TBEs[addr]; |
| trigger(Event:Replacement, addr, victim_entry, victim_tbe); |
| } else { |
| if (in_msg.Type == RubyRequestType:LD || |
| in_msg.Type == RubyRequestType:IFETCH) { |
| trigger(Event:Load, in_msg.LineAddress, cache_entry, |
| tbe); |
| } else if (in_msg.Type == RubyRequestType:ST) { |
| trigger(Event:Store, in_msg.LineAddress, cache_entry, |
| tbe); |
| } else { |
| error("Unexpected type from processor"); |
| } |
| } |
| } |
| } |
| } |
| |
| |
| /*************************************************************************/ |
| // Below are all of the actions that might be taken on a transition. |
| |
| // Each actions has a name, a shorthand, and a description. |
| // The shorthand is used when generating the HTML tables for the protocol. |
| // "\" in the shorthand cause that letter to be bold. Underscores insert a |
| // space, ^ makes the rest of the letters superscript. |
| // The description is also shown in the HTML table when clicked |
| |
| // The first set of actions are things we will do to interact with the |
| // rest of the system. Things like sending requests/responses. |
| |
| // Action blocks define a number of implicit variables that are useful. |
| // These variables come straight from the trigger() call in the in_port |
| // blocks. |
| // address: The address passed in the trigger (usually the in_msg.addr, |
| // though it can be different. E.g., on a replacement it is the |
| // victim address). |
| // cache_entry: The cache entry passed in the trigger call |
| // tbe: The TBE passed in the trigger call |
| action(sendGetS, 'gS', desc="Send GetS to the directory") { |
| // The syntax for enqueue is a lot like peek. Instead of populating |
| // in_msg, enqueue has an out_msg reference. Whatever you set on out_msg |
| // is sent through the out port specified. "request_out" is the port |
| // we're sending the message out of "RequestMsg" is the type of message |
| // we're sending "1" is the latency (in cycles) the port waits before |
| // sending the message. |
| enqueue(request_out, RequestMsg, 1) { |
| out_msg.addr := address; |
| // This type is defined in MSI-msg.sm for this protocol. |
| out_msg.Type := CoherenceRequestType:GetS; |
| // The destination may change depending on the address striping |
| // across different directories, so query the network. |
| out_msg.Destination.add(mapAddressToMachine(address, |
| MachineType:Directory)); |
| // See mem/ruby/protocol/RubySlicc_Exports.sm for possible sizes. |
| out_msg.MessageSize := MessageSizeType:Control; |
| // Set that the reqeustor is this machine so we get the response. |
| out_msg.Requestor := machineID; |
| } |
| } |
| |
| action(sendGetM, "gM", desc="Send GetM to the directory") { |
| enqueue(request_out, RequestMsg, 1) { |
| out_msg.addr := address; |
| out_msg.Type := CoherenceRequestType:GetM; |
| out_msg.Destination.add(mapAddressToMachine(address, |
| MachineType:Directory)); |
| out_msg.MessageSize := MessageSizeType:Control; |
| out_msg.Requestor := machineID; |
| } |
| } |
| |
| // NOTE: Clean evict. Required to keep the directory state up-to-date |
| action(sendPutS, "pS", desc="Send PutS to the directory") { |
| enqueue(request_out, RequestMsg, 1) { |
| out_msg.addr := address; |
| out_msg.Type := CoherenceRequestType:PutS; |
| out_msg.Destination.add(mapAddressToMachine(address, |
| MachineType:Directory)); |
| out_msg.MessageSize := MessageSizeType:Control; |
| out_msg.Requestor := machineID; |
| } |
| } |
| |
| action(sendPutM, "pM", desc="Send putM+data to the directory") { |
| enqueue(request_out, RequestMsg, 1) { |
| out_msg.addr := address; |
| out_msg.Type := CoherenceRequestType:PutM; |
| out_msg.Destination.add(mapAddressToMachine(address, |
| MachineType:Directory)); |
| out_msg.DataBlk := cache_entry.DataBlk; |
| out_msg.MessageSize := MessageSizeType:Data; |
| out_msg.Requestor := machineID; |
| } |
| } |
| |
| action(sendCacheDataToReq, "cdR", desc="Send cache data to requestor") { |
| // We have to peek into the request to see who to send to. |
| // If we are in both the peek and the enqueue block then we have access |
| // to both in_msg and out_msg. |
| assert(is_valid(cache_entry)); |
| peek(forward_in, RequestMsg) { |
| enqueue(response_out, ResponseMsg, 1) { |
| out_msg.addr := address; |
| out_msg.Type := CoherenceResponseType:Data; |
| out_msg.Destination.add(in_msg.Requestor); |
| out_msg.DataBlk := cache_entry.DataBlk; |
| out_msg.MessageSize := MessageSizeType:Data; |
| out_msg.Sender := machineID; |
| } |
| } |
| } |
| |
| action(sendCacheDataToDir, "cdD", desc="Send the cache data to the dir") { |
| enqueue(response_out, ResponseMsg, 1) { |
| out_msg.addr := address; |
| out_msg.Type := CoherenceResponseType:Data; |
| out_msg.Destination.add(mapAddressToMachine(address, |
| MachineType:Directory)); |
| out_msg.DataBlk := cache_entry.DataBlk; |
| out_msg.MessageSize := MessageSizeType:Data; |
| out_msg.Sender := machineID; |
| } |
| } |
| |
| action(sendInvAcktoReq, "iaR", desc="Send inv-ack to requestor") { |
| peek(forward_in, RequestMsg) { |
| enqueue(response_out, ResponseMsg, 1) { |
| out_msg.addr := address; |
| out_msg.Type := CoherenceResponseType:InvAck; |
| out_msg.Destination.add(in_msg.Requestor); |
| out_msg.DataBlk := cache_entry.DataBlk; |
| out_msg.MessageSize := MessageSizeType:Control; |
| out_msg.Sender := machineID; |
| } |
| } |
| } |
| |
| action(decrAcks, "da", desc="Decrement the number of acks") { |
| assert(is_valid(tbe)); |
| tbe.AcksOutstanding := tbe.AcksOutstanding - 1; |
| // This annotates the protocol trace |
| APPEND_TRANSITION_COMMENT("Acks: "); |
| APPEND_TRANSITION_COMMENT(tbe.AcksOutstanding); |
| } |
| |
| action(storeAcks, "sa", desc="Store the needed acks to the TBE") { |
| assert(is_valid(tbe)); |
| peek(response_in, ResponseMsg) { |
| tbe.AcksOutstanding := in_msg.Acks + tbe.AcksOutstanding; |
| } |
| assert(tbe.AcksOutstanding > 0); |
| } |
| |
| // Responses to CPU requests (e.g., hits and store acks) |
| |
| action(loadHit, "Lh", desc="Load hit") { |
| assert(is_valid(cache_entry)); |
| // Set this entry as the most recently used for the replacement policy |
| cacheMemory.setMRU(cache_entry); |
| // Send the data back to the sequencer/CPU. NOTE: False means it was |
| // not an "external hit", but hit in this local cache. |
| sequencer.readCallback(address, cache_entry.DataBlk, false); |
| } |
| |
| action(externalLoadHit, "xLh", desc="External load hit (was a miss)") { |
| assert(is_valid(cache_entry)); |
| peek(response_in, ResponseMsg) { |
| cacheMemory.setMRU(cache_entry); |
| // Forward the type of machine that responded to this request |
| // E.g., another cache or the directory. This is used for tracking |
| // statistics. |
| sequencer.readCallback(address, cache_entry.DataBlk, true, |
| machineIDToMachineType(in_msg.Sender)); |
| } |
| } |
| |
| action(storeHit, "Sh", desc="Store hit") { |
| assert(is_valid(cache_entry)); |
| cacheMemory.setMRU(cache_entry); |
| // The same as the read callback above. |
| sequencer.writeCallback(address, cache_entry.DataBlk, false); |
| } |
| |
| action(externalStoreHit, "xSh", desc="External store hit (was a miss)") { |
| assert(is_valid(cache_entry)); |
| peek(response_in, ResponseMsg) { |
| cacheMemory.setMRU(cache_entry); |
| sequencer.writeCallback(address, cache_entry.DataBlk, true, |
| // Note: this could be the last ack. |
| machineIDToMachineType(in_msg.Sender)); |
| } |
| } |
| |
| action(forwardEviction, "e", desc="sends eviction notification to CPU") { |
| if (send_evictions) { |
| sequencer.evictionCallback(address); |
| } |
| } |
| |
| // Cache management actions |
| |
| action(allocateCacheBlock, "a", desc="Allocate a cache block") { |
| assert(is_invalid(cache_entry)); |
| assert(cacheMemory.cacheAvail(address)); |
| // Create a new entry and update cache_entry to the new entry |
| set_cache_entry(cacheMemory.allocate(address, new Entry)); |
| } |
| |
| action(deallocateCacheBlock, "d", desc="Deallocate a cache block") { |
| assert(is_valid(cache_entry)); |
| cacheMemory.deallocate(address); |
| // clear the cache_entry variable (now it's invalid) |
| unset_cache_entry(); |
| } |
| |
| action(writeDataToCache, "wd", desc="Write data to the cache") { |
| peek(response_in, ResponseMsg) { |
| assert(is_valid(cache_entry)); |
| cache_entry.DataBlk := in_msg.DataBlk; |
| } |
| } |
| |
| action(allocateTBE, "aT", desc="Allocate TBE") { |
| assert(is_invalid(tbe)); |
| TBEs.allocate(address); |
| // this updates the tbe variable for other actions |
| set_tbe(TBEs[address]); |
| } |
| |
| action(deallocateTBE, "dT", desc="Deallocate TBE") { |
| assert(is_valid(tbe)); |
| TBEs.deallocate(address); |
| // this makes the tbe varible invalid |
| unset_tbe(); |
| } |
| |
| // Queue management actions |
| |
| action(popMandatoryQueue, "pQ", desc="Pop the mandatory queue") { |
| mandatory_in.dequeue(clockEdge()); |
| } |
| |
| action(popResponseQueue, "pR", desc="Pop the response queue") { |
| response_in.dequeue(clockEdge()); |
| } |
| |
| action(popForwardQueue, "pF", desc="Pop the forward queue") { |
| forward_in.dequeue(clockEdge()); |
| } |
| |
| // Stalling actions |
| |
| action(stall, "z", desc="Stall the incoming request") { |
| // Do nothing. However, the transition must have some action to be |
| // valid which is why this is needed. |
| // NOTE: There are other more complicated but higher performing stalls |
| // in Ruby like recycle() or stall_and_wait. |
| // z_stall stalls everything in the queue behind this request. |
| } |
| |
| |
| /*************************************************************************/ |
| // These are the transition definition. These are simply each cell in the |
| // table from Sorin et al. These are mostly in upper-left to bottom-right |
| // order |
| |
| // Each transtiion has (up to) 3 parameters, the current state, the |
| // triggering event and the final state. Thus, the below transition reads |
| // "Move from state I on a Load event to state IS_D". Below are other |
| // examples of transition statements. |
| // Within the transition statement is a set of action to take during the |
| // transition. These actions are executed atomically (i.e., all or nothing) |
| transition(I, Load, IS_D) { |
| // Make sure there is room in the cache to put the block whenever the |
| // miss returns. Otherwise we could deadlock. |
| allocateCacheBlock; |
| // We may need to track acks for this block and only the TBE holds an |
| // ack count. Thus, we need to allocate both a TBE and cache block. |
| allocateTBE; |
| // Actually send the request to the directory |
| sendGetS; |
| // Since we have handled this request on the mandatory queue, we can pop |
| popMandatoryQueue; |
| } |
| |
| transition(I, Store, IM_AD) { |
| allocateCacheBlock; |
| allocateTBE; |
| sendGetM; |
| popMandatoryQueue; |
| } |
| |
| // You can use {} to specify multiple states or events for which the |
| // transition applies. For instance, below. If we are in IS_D, then on any |
| // of the following Events (Load, Store, Replacement, Inv) we should stall |
| // When there is no third parameter to transition, it means that we want |
| // to stay in the initial state. |
| transition(IS_D, {Load, Store, Replacement, Inv}) { |
| stall; |
| } |
| |
| // Similarly, on either DataDirNoAcks or DataOwner we should go to S |
| transition(IS_D, {DataDirNoAcks, DataOwner}, S) { |
| writeDataToCache; |
| deallocateTBE; |
| externalLoadHit; |
| popResponseQueue; |
| } |
| |
| transition({IM_AD, IM_A}, {Load, Store, Replacement, FwdGetS, FwdGetM}) { |
| stall; |
| } |
| |
| transition({IM_AD, SM_AD}, {DataDirNoAcks, DataOwner}, M) { |
| writeDataToCache; |
| deallocateTBE; |
| externalStoreHit; |
| popResponseQueue; |
| } |
| |
| transition(IM_AD, DataDirAcks, IM_A) { |
| writeDataToCache; |
| storeAcks; |
| popResponseQueue; |
| } |
| |
| transition({IM_AD, IM_A, SM_AD, SM_A}, InvAck) { |
| decrAcks; |
| popResponseQueue; |
| } |
| |
| transition({IM_A, SM_A}, LastInvAck, M) { |
| deallocateTBE; |
| externalStoreHit; |
| popResponseQueue; |
| } |
| |
| transition({S, SM_AD, SM_A, M}, Load) { |
| loadHit; |
| popMandatoryQueue; |
| } |
| |
| transition(S, Store, SM_AD) { |
| allocateTBE; |
| sendGetM; |
| popMandatoryQueue; |
| } |
| |
| transition(S, Replacement, SI_A) { |
| sendPutS; |
| } |
| |
| transition(S, Inv, I) { |
| sendInvAcktoReq; |
| forwardEviction; |
| deallocateCacheBlock; |
| popForwardQueue; |
| } |
| |
| transition({SM_AD, SM_A}, {Store, Replacement, FwdGetS, FwdGetM}) { |
| stall; |
| } |
| |
| transition(SM_AD, Inv, IM_AD) { |
| sendInvAcktoReq; |
| popForwardQueue; |
| } |
| |
| transition(SM_AD, DataDirAcks, SM_A) { |
| writeDataToCache; |
| storeAcks; |
| popResponseQueue; |
| } |
| |
| transition(M, Store) { |
| storeHit; |
| forwardEviction; |
| popMandatoryQueue; |
| } |
| |
| transition(M, Replacement, MI_A) { |
| sendPutM; |
| } |
| |
| transition(M, FwdGetS, S) { |
| sendCacheDataToReq; |
| sendCacheDataToDir; |
| popForwardQueue; |
| } |
| |
| transition(M, FwdGetM, I) { |
| sendCacheDataToReq; |
| deallocateCacheBlock; |
| popForwardQueue; |
| } |
| |
| transition({MI_A, SI_A, II_A}, {Load, Store, Replacement}) { |
| stall; |
| } |
| |
| transition(MI_A, FwdGetS, SI_A) { |
| sendCacheDataToReq; |
| sendCacheDataToDir; |
| popForwardQueue; |
| } |
| |
| transition(MI_A, FwdGetM, II_A) { |
| sendCacheDataToReq; |
| popForwardQueue; |
| } |
| |
| transition({MI_A, SI_A, II_A}, PutAck, I) { |
| deallocateCacheBlock; |
| popForwardQueue; |
| } |
| |
| transition(SI_A, Inv, II_A) { |
| sendInvAcktoReq; |
| popForwardQueue; |
| } |
| |
| } |