Skip to content

Commit 58d9958

Browse files
Improve comments around check_mem_access and mmio_load/store
1 parent 7b4b780 commit 58d9958

File tree

2 files changed

+139
-39
lines changed

2 files changed

+139
-39
lines changed

dv/cosim/cosim.h

+5-5
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
// Information about a dside transaction observed on the DUT memory interface
1212
struct DSideAccessInfo {
13-
// set when the access is a store, in which case `data` is the store data from
13+
// Set when the access is a store, in which case `data` is the store data from
1414
// the DUT. Otherwise `data` is the load data provided to the DUT.
1515
bool store;
1616
// `addr` is the address and must be 32-bit aligned. `data` and `be` are
@@ -20,7 +20,7 @@ struct DSideAccessInfo {
2020
uint32_t addr;
2121
uint32_t be;
2222

23-
// set if an error response to the transaction is seen.
23+
// Set to indicate an error response to the attempted DUT memory access.
2424
bool error;
2525

2626
// `misaligned_first` and `misaligned_second` are set when the transaction is
@@ -30,9 +30,9 @@ struct DSideAccessInfo {
3030
//
3131
// For example an unaligned 32-bit load to 0x3 produces a transaction with
3232
// `addr` as 0x0 and `misaligned_first` set to true, then a transaction with
33-
// `addr` as 0x4 and `misaligned_second` set to true. An unaligned 16-bit load
34-
// to 0x01 only produces a transaction with `addr` as 0x0 and
35-
// `misaligned_first` set to true, there is no second half.
33+
// `addr` as 0x4 and `misaligned_second` set to true.
34+
// An unaligned 16-bit load to 0x01 only produces a transaction with `addr`
35+
// as 0x0 and `misaligned_first` set to true, there is no second half.
3636
bool misaligned_first;
3737
bool misaligned_second;
3838
};

dv/cosim/spike_cosim.cc

+134-34
Original file line numberDiff line numberDiff line change
@@ -71,40 +71,56 @@ SpikeCosim::SpikeCosim(const std::string &isa_string, uint32_t start_pc,
7171
char *SpikeCosim::addr_to_mem(reg_t addr) { return nullptr; }
7272

7373
bool SpikeCosim::mmio_load(reg_t addr, size_t len, uint8_t *bytes) {
74+
// Load from the ISS memory model, while checking if the access is
75+
// consistent with the observed DUT memory acceses.
76+
// Return false if any errors occur, or the checking fails.
77+
// Within Spike (mmu_t::load_slow_path), this causes a load_access_fault.
78+
79+
// Spike does not differentiate between iside and dside memory accesses
80+
// (as ibex does) so we make an educated guess which is which, and
81+
// then after fetching the data, check against the recorded DUT access
82+
// that should be queued as pending.
83+
//
84+
// Spike may attempt to access up to 8-bytes from the PC when fetching, so
85+
// assume as a dside access when it falls outside that range.
86+
// N.B. Heuristic
87+
uint32_t pc = processor->get_state()->pc;
88+
bool is_probably_dmem_access = (addr < pc || addr >= (pc + 8));
89+
// Check we don't have a heuristic mismatch (this may be a false-positive but
90+
// worth failing explicity if it is seen)
91+
assert(!(is_probably_dmem_access && pending_iside_error));
92+
93+
// Attempt to make an identical load from the bus memory device.
94+
// This can fail e.g. if accessing an unmapped address (see SpikeCosim::add_memory)
7495
bool bus_error = !bus.load(addr, len, bytes);
7596

76-
bool dut_error = false;
97+
// If the DUT encountered a bus error for its access, or the checking failed,
98+
// this is a dut_error.
99+
bool dut_error = is_probably_dmem_access ? (check_mem_access(false, addr, len, bytes) != kCheckMemOk) : false;
77100

78-
// Incoming access may be an iside or dside access. Use PC to help determine
79-
// which.
80-
uint32_t pc = processor->get_state()->pc;
81101
uint32_t aligned_addr = addr & 0xfffffffc;
82-
83-
if (pending_iside_error && (aligned_addr == pending_iside_err_addr)) {
84-
// Check if the incoming access is subject to an iside error, in which case
85-
// assume it's an iside access and produce an error.
102+
if ( pending_iside_error &&
103+
(pending_iside_err_addr == aligned_addr)) {
104+
// Check if the aligned PC matches with the aligned address or an
105+
// incremented aligned PC (to capture the unaligned 4-byte instruction
106+
// case).
107+
// If the pending_iside_error has been set, produce a dut_error.
86108
pending_iside_error = false;
87109
dut_error = true;
88-
} else if (addr < pc || addr >= (pc + 8)) {
89-
// Spike may attempt to access up to 8-bytes from the PC when fetching, so
90-
// only check as a dside access when it falls outside that range.
91-
92-
// Otherwise check if the aligned PC matches with the aligned address or an
93-
// incremented aligned PC (to capture the unaligned 4-byte instruction
94-
// case). Assume a successful iside access if either of these checks are
95-
// true, otherwise assume a dside access and check against DUT dside
96-
// accesses. If the RTL produced a bus error for the access, or the
97-
// checking failed produce a memory fault in spike.
98-
dut_error = (check_mem_access(false, addr, len, bytes) != kCheckMemOk);
99-
}
110+
};
100111

101112
return !(bus_error || dut_error);
102113
}
103114

104115
bool SpikeCosim::mmio_store(reg_t addr, size_t len, const uint8_t *bytes) {
116+
// Store to the ISS memory model, while checking if the access is
117+
// consistent with the observed DUT memory acceses.
118+
// Return false if any errors occur, or the checking fails.
119+
// Within Spike (mmu_t::store_slow_path), this causes a store_access_fault.
120+
105121
bool bus_error = !bus.store(addr, len, bytes);
106-
// If the RTL produced a bus error for the access, or the checking failed
107-
// produce a memory fault in spike.
122+
// If the DUT encountered a bus error for its access, or the checking failed,
123+
// this is a dut_error.
108124
bool dut_error = (check_mem_access(true, addr, len, bytes) != kCheckMemOk);
109125

110126
return !(bus_error || dut_error);
@@ -519,12 +535,76 @@ void SpikeCosim::fixup_csr(int csr_num, uint32_t csr_val) {
519535

520536
SpikeCosim::check_mem_result_e SpikeCosim::check_mem_access(
521537
bool store, uint32_t addr, size_t len, const uint8_t *bytes) {
538+
// Because Spike is only fed instructions that have been made available
539+
// on the RVFI by IBEX (retired and synchronously trapping), spike will
540+
// always run slightly-behind the ibex, and hence we need to store memory
541+
// accesses and errors when they are seen by the testbench, and then pop
542+
// them from the queues when spike actually executes them.
543+
// This delay may be a few cycles in general, but may never happen if
544+
// the access was iside-speculative.
545+
//
546+
// Whenever a load/store is attempted by spike, we check the following
547+
// conditions....
548+
//
549+
// 0) First, if an iside error has been detected and
550+
// pending_iside_error=True, a dut_error is generated within mmio_load(), and
551+
// this function is not called. This function is called if the heuristics
552+
// believe the access is a dmem access. (dmem may be accessed with stores or
553+
// loads, while imem is only loads.)
554+
//
555+
// As well as checking for correspondence between the spike access and the
556+
// queued DUT dmem access, we also want to pass along the error if the dmem
557+
// access response came back with an error from the DUT memory system bus. So,
558+
// if all the below checks pass, the final step is to check for a marked error
559+
// in the queued dmem access, and pass it along to Spike if so.
560+
//
561+
// The correspondence checks are as follows:
562+
//
563+
// 1) Check there is actually a dmem access in the queue at all.
564+
// Pop the top dmem access from the queue, and then...
565+
// 2) Check the addr of spike access matches that of the queued dmem access.
566+
// 3) Check the type of spike access (load/store) matches that of the queued
567+
// dmem access.
568+
//
569+
// If the dut dmem access was misaligned :
570+
// (Because ibex dmem can make misaligned and byte-enabled accesses, and spike
571+
// will not make accesses in this way, we need to check the final result of
572+
// the two systems is the same. Spike will make multiple single-byte accesses
573+
// for the same end-result). Therefore, this function (and mmio_load/store)
574+
// may be called multiple times for a single dut dmem access.
575+
// By checking the byte-enables of the memory accesses, we can check that..
576+
// 4) The spike-accesses do not repeat accessing any bytes the dut accesses.
577+
// 5) The spike-access does not touch any bytes not accessed by the dut.
578+
// ..and while doing this, record the bytes accessed so next time around we
579+
// can make the checks 4) and 5).
580+
// When all of the bytes have been accessed matching the queued access,
581+
// discard this recorded data in preparation for the next access.
582+
//
583+
// If the dut dmem access is aligned :
584+
// 6) Check that the spike-accessed bytes match the dut dmem byte-enables in
585+
// one go.
586+
//
587+
// Until this point, we have not even checked the data of the accesses.
588+
// For accesses not tagged with a dut error:
589+
// 7) Check the data of the spike access matches that of the ibex dmem access,
590+
// for all stores and for loads without errors.
591+
//
592+
// For dut dmem accesses with errors, we can't check the data, but one more
593+
// possible sanity test is that for misaligned accesses...
594+
// 8) Check the second access exists in the pending queue
595+
// 9) Check the second (next) access has the correct address
596+
// After 9), pop both dmem accesses from pending. Only return a single error if needed.
597+
//
598+
// Finally, pass along the error to spike if the dut access at the top of the
599+
// queue was marked with one (DSideAccessInfo.error)
600+
522601
assert(len >= 1 && len <= 4);
523602
// Expect that no spike memory accesses cross a 32-bit boundary
524603
assert(((addr + (len - 1)) & 0xfffffffc) == (addr & 0xfffffffc));
525604

526605
std::string iss_action = store ? "store" : "load";
527606

607+
// 1)
528608
// Check if there are any pending DUT accesses to check against
529609
if (pending_dside_accesses.size() == 0) {
530610
std::stringstream err_str;
@@ -540,6 +620,7 @@ SpikeCosim::check_mem_result_e SpikeCosim::check_mem_access(
540620

541621
std::string dut_action = top_pending_access_info.store ? "store" : "load";
542622

623+
// 2)
543624
// Check for an address match
544625
uint32_t aligned_addr = addr & 0xfffffffc;
545626
if (aligned_addr != top_pending_access_info.addr) {
@@ -552,6 +633,7 @@ SpikeCosim::check_mem_result_e SpikeCosim::check_mem_access(
552633
return kCheckMemCheckFailed;
553634
}
554635

636+
// 3)
555637
// Check access type match
556638
if (store != top_pending_access_info.store) {
557639
std::stringstream err_str;
@@ -575,6 +657,7 @@ SpikeCosim::check_mem_result_e SpikeCosim::check_mem_access(
575657
// accesses where the DUT will generate an access covering all bytes within
576658
// an aligned 32-bit word.
577659

660+
// 4)
578661
// Check bytes accessed this time haven't already been been seen for the DUT
579662
// access we are trying to match against
580663
if ((expected_be & top_pending_access.be_spike) != 0) {
@@ -590,6 +673,7 @@ SpikeCosim::check_mem_result_e SpikeCosim::check_mem_access(
590673
return kCheckMemCheckFailed;
591674
}
592675

676+
// 5)
593677
// Check expected access isn't trying to access bytes that the DUT access
594678
// didn't access.
595679
if ((expected_be & ~top_pending_access_info.be) != 0) {
@@ -610,6 +694,7 @@ SpikeCosim::check_mem_result_e SpikeCosim::check_mem_access(
610694
pending_access_done = true;
611695
}
612696
} else {
697+
// 6)
613698
// For aligned accesses bytes from spike access must precisely match bytes
614699
// from DUT access in one go
615700
if (expected_be != top_pending_access_info.be) {
@@ -626,9 +711,10 @@ SpikeCosim::check_mem_result_e SpikeCosim::check_mem_access(
626711
pending_access_done = true;
627712
}
628713

714+
// 7)
629715
// Check data from expected access matches pending DUT access.
630-
// Data is ignored on error responses to loads so don't check it. Always check
631-
// store data.
716+
// Always check store data.
717+
// (Data is ignored on error responses to loads so don't check it.)
632718
if (store || !top_pending_access_info.error) {
633719
// Combine bytes into a single word
634720
uint32_t expected_data = 0;
@@ -647,10 +733,10 @@ SpikeCosim::check_mem_result_e SpikeCosim::check_mem_access(
647733

648734
if (expected_data != masked_dut_data) {
649735
std::stringstream err_str;
650-
err_str << "DUT generated " << iss_action << " at address " << std::hex
651-
<< top_pending_access_info.addr << " with data "
652-
<< masked_dut_data << " but data " << expected_data
653-
<< " was expected with byte mask " << expected_be;
736+
err_str << "DUT generated " << iss_action << " dmem access at address "
737+
<< std::hex << top_pending_access_info.addr << " with data "
738+
<< masked_dut_data << " but ISS access(es) got data " << std::hex << expected_data
739+
<< " with equivalent byte mask of " << std::hex << expected_be;
654740

655741
errors.emplace_back(err_str.str());
656742

@@ -659,15 +745,24 @@ SpikeCosim::check_mem_result_e SpikeCosim::check_mem_access(
659745
}
660746

661747
bool pending_access_error = top_pending_access_info.error;
748+
// If the pending dside access has an error, then we don't need to check to data
749+
// of the accesses. However, we can still check if the correct number of accesses
750+
// took place, and then immediately pop the erroneous dmem access(es) off the top
751+
// of the queue.
662752

663753
if (pending_access_error && misaligned) {
664-
// When misaligned accesses see an error, if they have crossed a 32-bit
665-
// boundary DUT will generate two accesses. If the top pending access from
666-
// the DUT was the first half of a misaligned access which accesses the top
667-
// byte, it must have crossed the 32-bit boundary and generated a second
668-
// access
669-
if (top_pending_access_info.misaligned_first &&
754+
// If requested to make a misaligned access that crosses a 32-bit
755+
// boundary (this could be a word or halfword access), the DUT
756+
// LSU generates two word-aligned accesses with appropriate
757+
// byte-enables set.
758+
//
759+
// If the top pending access from the DUT was the first half of a misaligned
760+
// access which accesses the top byte, it must have crossed the 32-bit
761+
// boundary and generated a second access
762+
if ( top_pending_access_info.misaligned_first &&
670763
((top_pending_access_info.be & 0x8) != 0)) {
764+
765+
// 8)
671766
// Check the second access DUT exists
672767
if ((pending_dside_accesses.size() < 2) ||
673768
!pending_dside_accesses[1].dut_access_info.misaligned_second) {
@@ -681,6 +776,7 @@ SpikeCosim::check_mem_result_e SpikeCosim::check_mem_access(
681776
return kCheckMemCheckFailed;
682777
}
683778

779+
// 9)
684780
// Check the second access had the expected address
685781
if (pending_dside_accesses[1].dut_access_info.addr !=
686782
(top_pending_access_info.addr + 4)) {
@@ -712,6 +808,10 @@ SpikeCosim::check_mem_result_e SpikeCosim::check_mem_access(
712808
pending_dside_accesses.erase(pending_dside_accesses.begin());
713809
}
714810

811+
// If we got this far, just check if the DUT access was marked as
812+
// an error, and pass this on to Spike if so as a dut_error.
813+
// (If we just erased the top dmem access from the pending queue, this
814+
// returned error refers to that.)
715815
return pending_access_error ? kCheckMemBusError : kCheckMemOk;
716816
}
717817

0 commit comments

Comments
 (0)