• GeneralnMigen
  • weird simulation behaviour involving sync and complex module hierarchies

we've encountered several situations where simulation goes awry, involving sync.

the following works fine:

class SubModule1:

   def elaborate(self, platform):
          m.d.sync += self.sync1.eq(self.sync)
          m.d.sync += self.sync2.eq(self.sync1)

class MainModule:

   def elaborate(self, platform):
       m.submodules.sm1 = sm1 = SubModule1()
       m.d.comb += self.comb1.eq(sm1.sync2)

However the following does NOT work fine:

class SubModule2:

   def elaborate(self, platform):
          m.d.comb += self.input.eq(self.sync2)
          m.d.comb += self.output.eq(self.input)

class MainModule:

   def elaborate(self, platform):
       m.submodules.sm1 = sm1 = SubModule1()
       m.submodules.sm2 = sm2 = SubModule2()
       m.d.comb += sm1.sync2.eq(sm1.sync2)
       m.d.comb += self.output.eq(sm2.output)

the 2nd example is detected as a combinatorial loop, and fails simulation. clearly it is NOT a combinatorial loop.

the "fix" is as follows:

class SubModule1:

   def elaborate(self, platform):
          m.d.sync += self.sync1.eq(self.sync)
          m.d.sync += temp_sync2.eq(self.sync1)
          m.d.comb += self.sync2.eq(temp_sync2)

when that "trick" is deployed, where the "public API" is in a combinatorial variable instead of accessing the sync'd object, the "problem" goes away.

it is critical to note that it is only when "chained" sync objects are used - a sync on a sync on a sync - followed by then connecting that sync-sync-sync'd output as the input to a second module - that we get problems.

Note: the chances of this issue being fixed promptly significantly increase if you post an MCVE reproducing it.