Skip to content

Commit b20bcb6

Browse files
Improve comments around check_mem_access and mmio_load/store
1 parent 83ac7a9 commit b20bcb6

File tree

2 files changed

+139
-39
lines changed

2 files changed

+139
-39
lines changed

Diff for: 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
};

Diff for: dv/cosim/spike_cosim.cc

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

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

73-
bool dut_error = false;
94+
// If the DUT encountered a bus error for its access, or the checking failed,
95+
// this is a dut_error.
96+
bool dut_error = is_probably_dmem_access ? (check_mem_access(false, addr, len, bytes) != kCheckMemOk) : false;
7497

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

98109
return !(bus_error || dut_error);
99110
}
100111

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

107123
return !(bus_error || dut_error);
@@ -516,12 +532,76 @@ void SpikeCosim::fixup_csr(int csr_num, uint32_t csr_val) {
516532

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

523602
std::string iss_action = store ? "store" : "load";
524603

604+
// 1)
525605
// Check if there are any pending DUT accesses to check against
526606
if (pending_dside_accesses.size() == 0) {
527607
std::stringstream err_str;
@@ -537,6 +617,7 @@ SpikeCosim::check_mem_result_e SpikeCosim::check_mem_access(
537617

538618
std::string dut_action = top_pending_access_info.store ? "store" : "load";
539619

620+
// 2)
540621
// Check for an address match
541622
uint32_t aligned_addr = addr & 0xfffffffc;
542623
if (aligned_addr != top_pending_access_info.addr) {
@@ -549,6 +630,7 @@ SpikeCosim::check_mem_result_e SpikeCosim::check_mem_access(
549630
return kCheckMemCheckFailed;
550631
}
551632

633+
// 3)
552634
// Check access type match
553635
if (store != top_pending_access_info.store) {
554636
std::stringstream err_str;
@@ -572,6 +654,7 @@ SpikeCosim::check_mem_result_e SpikeCosim::check_mem_access(
572654
// accesses where the DUT will generate an access covering all bytes within
573655
// an aligned 32-bit word.
574656

657+
// 4)
575658
// Check bytes accessed this time haven't already been been seen for the DUT
576659
// access we are trying to match against
577660
if ((expected_be & top_pending_access.be_spike) != 0) {
@@ -587,6 +670,7 @@ SpikeCosim::check_mem_result_e SpikeCosim::check_mem_access(
587670
return kCheckMemCheckFailed;
588671
}
589672

673+
// 5)
590674
// Check expected access isn't trying to access bytes that the DUT access
591675
// didn't access.
592676
if ((expected_be & ~top_pending_access_info.be) != 0) {
@@ -607,6 +691,7 @@ SpikeCosim::check_mem_result_e SpikeCosim::check_mem_access(
607691
pending_access_done = true;
608692
}
609693
} else {
694+
// 6)
610695
// For aligned accesses bytes from spike access must precisely match bytes
611696
// from DUT access in one go
612697
if (expected_be != top_pending_access_info.be) {
@@ -623,9 +708,10 @@ SpikeCosim::check_mem_result_e SpikeCosim::check_mem_access(
623708
pending_access_done = true;
624709
}
625710

711+
// 7)
626712
// Check data from expected access matches pending DUT access.
627-
// Data is ignored on error responses to loads so don't check it. Always check
628-
// store data.
713+
// Always check store data.
714+
// (Data is ignored on error responses to loads so don't check it.)
629715
if (store || !top_pending_access_info.error) {
630716
// Combine bytes into a single word
631717
uint32_t expected_data = 0;
@@ -644,10 +730,10 @@ SpikeCosim::check_mem_result_e SpikeCosim::check_mem_access(
644730

645731
if (expected_data != masked_dut_data) {
646732
std::stringstream err_str;
647-
err_str << "DUT generated " << iss_action << " at address " << std::hex
648-
<< top_pending_access_info.addr << " with data "
649-
<< masked_dut_data << " but data " << expected_data
650-
<< " was expected with byte mask " << expected_be;
733+
err_str << "DUT generated " << iss_action << " dmem access at address "
734+
<< std::hex << top_pending_access_info.addr << " with data "
735+
<< masked_dut_data << " but ISS access(es) got data " << std::hex << expected_data
736+
<< " with equivalent byte mask of " << std::hex << expected_be;
651737

652738
errors.emplace_back(err_str.str());
653739

@@ -656,15 +742,24 @@ SpikeCosim::check_mem_result_e SpikeCosim::check_mem_access(
656742
}
657743

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

660750
if (pending_access_error && misaligned) {
661-
// When misaligned accesses see an error, if they have crossed a 32-bit
662-
// boundary DUT will generate two accesses. If the top pending access from
663-
// the DUT was the first half of a misaligned access which accesses the top
664-
// byte, it must have crossed the 32-bit boundary and generated a second
665-
// access
666-
if (top_pending_access_info.misaligned_first &&
751+
// If requested to make a misaligned access that crosses a 32-bit
752+
// boundary (this could be a word or halfword access), the DUT
753+
// LSU generates two word-aligned accesses with appropriate
754+
// byte-enables set.
755+
//
756+
// If the top pending access from the DUT was the first half of a misaligned
757+
// access which accesses the top byte, it must have crossed the 32-bit
758+
// boundary and generated a second access
759+
if ( top_pending_access_info.misaligned_first &&
667760
((top_pending_access_info.be & 0x8) != 0)) {
761+
762+
// 8)
668763
// Check the second access DUT exists
669764
if ((pending_dside_accesses.size() < 2) ||
670765
!pending_dside_accesses[1].dut_access_info.misaligned_second) {
@@ -678,6 +773,7 @@ SpikeCosim::check_mem_result_e SpikeCosim::check_mem_access(
678773
return kCheckMemCheckFailed;
679774
}
680775

776+
// 9)
681777
// Check the second access had the expected address
682778
if (pending_dside_accesses[1].dut_access_info.addr !=
683779
(top_pending_access_info.addr + 4)) {
@@ -709,6 +805,10 @@ SpikeCosim::check_mem_result_e SpikeCosim::check_mem_access(
709805
pending_dside_accesses.erase(pending_dside_accesses.begin());
710806
}
711807

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

0 commit comments

Comments
 (0)