Repository: amaranth-lang/amaranth
Pull request #575 (2021-01-13 00:26:02)

Lattice: Add support for MachXO2/XO3L internal oscillator
This PR adds support for the internal high-speed oscillator on MachXO2/XO3L devices, called OSCH. Migen had support for the oscillator, but nMigen doesn't have it yet.

It essentially replicates the changes that https://github.com/nmigen/nmigen/pull/530 made on Intel Cyclone V devices and https://github.com/nmigen/nmigen/pull/338 on the iCE40, with the difference that MachXO2/XO3 devices have a configurable frequency and a list of allowed freqs, as opposed to the Cyclone V which has a fixed frequency with a maximum of 100MHz, and the iCE40 which uses a fixed 48MHz oscillator and a divider.

This PR is accompanied by (and would allow) PR https://github.com/nmigen/nmigen-boards/pull/136 in nmigen-boards, which changes the TinyFPGAAX2Platform to use the internal clock (at 2.08 MHz) by default, since the TinyFPGA AX2 has no external clock and thus no better default alternative for the clock signal.

codecov-commenter: # [Codecov](https://codecov.io/gh/amaranth-lang/amaranth/pull/575?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) Report
> Merging [#575](https://codecov.io/gh/amaranth-lang/amaranth/pull/575?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) (0a66153) into [main](https://codecov.io/gh/amaranth-lang/amaranth/commit/7c161957bfe35e27c98ac82777b3fc53564a28fc?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) (7c16195) will **decrease** coverage by `0.03%`.
> The diff coverage is `n/a`.

> :exclamation: Current head 0a66153 differs from pull request most recent head 213a2d6. Consider uploading reports for the commit 213a2d6 to get more accurate results
[![Impacted file tree graph](https://codecov.io/gh/amaranth-lang/amaranth/pull/575/graphs/tree.svg?width=650&height=150&src=pr&token=CBuKpnBjhj&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang)](https://codecov.io/gh/amaranth-lang/amaranth/pull/575?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang)

```diff
@@            Coverage Diff             @@
##             main     #575      +/-   ##
==========================================
- Coverage   81.53%   81.50%   -0.04%     
==========================================
  Files          49       49              
  Lines        6468     6461       -7     
  Branches     1531     1287     -244     
==========================================
- Hits         5274     5266       -8     
- Misses       1003     1007       +4     
+ Partials      191      188       -3     
```


| [Impacted Files](https://codecov.io/gh/amaranth-lang/amaranth/pull/575?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) | Coverage Δ | |
|---|---|---|
| [amaranth/hdl/dsl.py](https://codecov.io/gh/amaranth-lang/amaranth/pull/575/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang#diff-YW1hcmFudGgvaGRsL2RzbC5weQ==) | | |
| [amaranth/compat/genlib/fifo.py](https://codecov.io/gh/amaranth-lang/amaranth/pull/575/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang#diff-YW1hcmFudGgvY29tcGF0L2dlbmxpYi9maWZvLnB5) | | |
| [amaranth/hdl/ast.py](https://codecov.io/gh/amaranth-lang/amaranth/pull/575/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang#diff-YW1hcmFudGgvaGRsL2FzdC5weQ==) | | |
| [amaranth/lib/cdc.py](https://codecov.io/gh/amaranth-lang/amaranth/pull/575/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang#diff-YW1hcmFudGgvbGliL2NkYy5weQ==) | | |
| [amaranth/compat/genlib/coding.py](https://codecov.io/gh/amaranth-lang/amaranth/pull/575/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang#diff-YW1hcmFudGgvY29tcGF0L2dlbmxpYi9jb2RpbmcucHk=) | | |
| [amaranth/compat/genlib/record.py](https://codecov.io/gh/amaranth-lang/amaranth/pull/575/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang#diff-YW1hcmFudGgvY29tcGF0L2dlbmxpYi9yZWNvcmQucHk=) | | |
| [amaranth/hdl/rec.py](https://codecov.io/gh/amaranth-lang/amaranth/pull/575/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang#diff-YW1hcmFudGgvaGRsL3JlYy5weQ==) | | |
| [amaranth/build/run.py](https://codecov.io/gh/amaranth-lang/amaranth/pull/575/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang#diff-YW1hcmFudGgvYnVpbGQvcnVuLnB5) | | |
| [amaranth/sim/\_pyrtl.py](https://codecov.io/gh/amaranth-lang/amaranth/pull/575/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang#diff-YW1hcmFudGgvc2ltL19weXJ0bC5weQ==) | | |
| [amaranth/hdl/mem.py](https://codecov.io/gh/amaranth-lang/amaranth/pull/575/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang#diff-YW1hcmFudGgvaGRsL21lbS5weQ==) | | |
| ... and [88 more](https://codecov.io/gh/amaranth-lang/amaranth/pull/575/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/amaranth-lang/amaranth/pull/575?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/amaranth-lang/amaranth/pull/575?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang). Last update [7c16195...213a2d6](https://codecov.io/gh/amaranth-lang/amaranth/pull/575?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang).

jreyesr: > LGTM, just needs to be rebased I think.

Done. I just made a brand new commit, since my code was outdated by almost a year.

Patch:
diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml
index ce44dee..8840e12 100644
--- a/.github/workflows/main.yaml
+++ b/.github/workflows/main.yaml
@@ -8,7 +8,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: ['3.6', '3.7', '3.8', '3.9', '3.10', pypy3]
+        python-version: ['3.7', '3.8', '3.9', '3.10', 'pypy-3.7-v7.3.3']
     steps:
     - name: Check out source code
       uses: actions/checkout@v2
diff --git a/amaranth/_toolchain/cxx.py b/amaranth/_toolchain/cxx.py
index 3dc403d..ffe5bfc 100644
--- a/amaranth/_toolchain/cxx.py
+++ b/amaranth/_toolchain/cxx.py
@@ -19,7 +19,7 @@ def build_cxx(*, cxx_sources, output_name, include_dirs, macros):
 
         with warnings.catch_warnings():
             warnings.filterwarnings(action="ignore", category=DeprecationWarning)
-            # This emits a DeprecationWarning on Python 3.6 and 3.10.
+            # This emits a DeprecationWarning on Python 3.10.
             from setuptools import distutils
             cc_driver = distutils.ccompiler.new_compiler()
 
diff --git a/amaranth/_toolchain/yosys.py b/amaranth/_toolchain/yosys.py
index c6acb79..9934354 100644
--- a/amaranth/_toolchain/yosys.py
+++ b/amaranth/_toolchain/yosys.py
@@ -12,14 +12,11 @@ except ImportError:
     except ImportError:
         importlib_metadata = None # not installed
 try:
+    from importlib import resources as importlib_resources
     try:
-        from importlib import resources as importlib_resources
-        try:
-            importlib_resources.files # py3.9+ stdlib
-        except AttributeError:
-            import importlib_resources # py3.8- shim
-    except ImportError:
-        import importlib_resources # py3.6- shim
+        importlib_resources.files # py3.9+ stdlib
+    except AttributeError:
+        import importlib_resources # py3.8- shim
 except ImportError:
     importlib_resources = None
 
diff --git a/amaranth/back/rtlil.py b/amaranth/back/rtlil.py
index 6838f37..e42d6f4 100644
--- a/amaranth/back/rtlil.py
+++ b/amaranth/back/rtlil.py
@@ -328,16 +328,23 @@ class _ValueCompilerState:
         else:
             wire_name = signal.name
 
+        is_sync_driven = signal in self.driven and self.driven[signal]
+        
         attrs = dict(signal.attrs)
         if signal._enum_class is not None:
             attrs["enum_base_type"] = signal._enum_class.__name__
             for value in signal._enum_class:
                 attrs["enum_value_{:0{}b}".format(value.value, signal.width)] = value.name
 
+        # For every signal in the sync domain, assign \sig's initial value (using the \init reg
+        # attribute) to the reset value.
+        if is_sync_driven:
+            attrs["init"] = ast.Const(signal.reset, signal.width)
+
         wire_curr = self.rtlil.wire(width=signal.width, name=wire_name,
                                     port_id=port_id, port_kind=port_kind,
                                     attrs=attrs, src=_src(signal.src_loc))
-        if signal in self.driven and self.driven[signal]:
+        if is_sync_driven:
             wire_next = self.rtlil.wire(width=signal.width, name=wire_curr + "$next",
                                         src=_src(signal.src_loc))
         else:
@@ -944,40 +951,42 @@ def _convert_fragment(builder, fragment, name_map, hierarchy):
                     stmt_compiler._has_rhs = False
                     stmt_compiler._wrap_assign = False
                     stmt_compiler(group_stmts)
+        
+        # For every driven signal in the sync domain, create a flop of appropriate type. Which type
+        # is appropriate depends on the domain: for domains with sync reset, it is a $dff, for
+        # domains with async reset it is an $adff. The latter is directly provided with the reset
+        # value as a parameter to the cell, which is directly assigned during reset.
+        for domain, signal in fragment.iter_sync():
+            cd = fragment.domains[domain]
 
-                # For every signal in the sync domain, assign \sig's initial value (which will
-                # end up as the \init reg attribute) to the reset value.
-                with process.sync("init") as sync:
-                    for domain, signal in fragment.iter_sync():
-                        if signal not in group_signals:
-                            continue
-                        wire_curr, wire_next = compiler_state.resolve(signal)
-                        sync.update(wire_curr, rhs_compiler(ast.Const(signal.reset, signal.width)))
-
-                # For every signal in every sync domain, assign \sig to \sig$next. The sensitivity
-                # list, however, differs between domains: for domains with sync reset, it is
-                # `[pos|neg]edge clk`, for sync domains with async reset it is `[pos|neg]edge clk
-                # or posedge rst`.
-                for domain, signals in fragment.drivers.items():
-                    if domain is None:
-                        continue
-
-                    signals = signals & group_signals
-                    if not signals:
-                        continue
-
-                    cd = fragment.domains[domain]
-
-                    triggers = []
-                    triggers.append((cd.clk_edge + "edge", compiler_state.resolve_curr(cd.clk)))
-                    if cd.async_reset:
-                        triggers.append(("posedge", compiler_state.resolve_curr(cd.rst)))
-
-                    for trigger in triggers:
-                        with process.sync(*trigger) as sync:
-                            for signal in signals:
-                                wire_curr, wire_next = compiler_state.resolve(signal)
-                                sync.update(wire_curr, wire_next)
+            wire_clk = compiler_state.resolve_curr(cd.clk)
+            wire_rst = compiler_state.resolve_curr(cd.rst) if cd.rst is not None else None
+            wire_curr, wire_next = compiler_state.resolve(signal)
+
+            if not cd.async_reset:
+                # For sync reset flops, the reset value comes from logic inserted by 
+                # `hdl.xfrm.DomainLowerer`.
+                module.cell("$dff", ports={
+                    "\\CLK": wire_clk,
+                    "\\D": wire_next,
+                    "\\Q": wire_curr
+                }, params={
+                    "CLK_POLARITY": int(cd.clk_edge == "pos"),
+                    "WIDTH": signal.width
+                })
+            else:
+                # For async reset flops, the reset value is provided directly to the cell.
+                module.cell("$adff", ports={
+                    "\\ARST": wire_rst,
+                    "\\CLK": wire_clk,
+                    "\\D": wire_next,
+                    "\\Q": wire_curr
+                }, params={
+                    "ARST_POLARITY": ast.Const(1),
+                    "ARST_VALUE": ast.Const(signal.reset, signal.width),
+                    "CLK_POLARITY": int(cd.clk_edge == "pos"),
+                    "WIDTH": signal.width
+                })
 
         # Any signals that are used but neither driven nor connected to an input port always
         # assume their reset values. We need to assign the reset value explicitly, since only
@@ -1010,7 +1019,7 @@ def _convert_fragment(builder, fragment, name_map, hierarchy):
     for signal in fragment.ports:
         port_map[compiler_state.resolve_curr(signal)] = signal
 
-    # Finally, collect tha names we've given to each wire in RTLIL, and provide these to
+    # Finally, collect the names we've given to each wire in RTLIL, and provide these to
     # the caller, to allow manipulating them in the toolchain.
     for signal in compiler_state.wires:
         wire_name = compiler_state.resolve_curr(signal)
diff --git a/amaranth/back/verilog.py b/amaranth/back/verilog.py
index 62c7192..7a0a8e5 100644
--- a/amaranth/back/verilog.py
+++ b/amaranth/back/verilog.py
@@ -48,5 +48,5 @@ def convert(elaboratable, name="top", platform=None, ports=None, *, emit_src=Tru
         warnings.warn("Implicit port determination is deprecated, specify ports explictly",
                       DeprecationWarning, stacklevel=2)
     fragment = ir.Fragment.get(elaboratable, platform).prepare(ports=ports, **kwargs)
-    verilog_text, name_map = convert_fragment(fragment, name, emit_src=emit_src)
+    verilog_text, name_map = convert_fragment(fragment, name, emit_src=emit_src, strip_internal_attrs=strip_internal_attrs)
     return verilog_text
diff --git a/amaranth/build/plat.py b/amaranth/build/plat.py
index 13cdc4d..3b706ab 100644
--- a/amaranth/build/plat.py
+++ b/amaranth/build/plat.py
@@ -70,11 +70,11 @@ class Platform(ResourceManager, metaclass=ABCMeta):
 
     @property
     def _deprecated_toolchain_env_var(self):
-        return f"NMIGEN_ENV_{self.toolchain}"
+        return f"NMIGEN_ENV_{tool_env_var(self.toolchain)}"
 
     @property
     def _toolchain_env_var(self):
-        return f"AMARANTH_ENV_{self.toolchain}"
+        return f"AMARANTH_ENV_{tool_env_var(self.toolchain)}"
 
     def build(self, elaboratable, name="top",
               build_dir="build", do_build=True,
@@ -315,6 +315,50 @@ class TemplatedPlatform(Platform):
 
         rtlil_text, self._name_map = rtlil.convert_fragment(fragment, name=name)
 
+        # Retrieve an override specified in either the environment or as a kwarg.
+        # expected_type parameter is used to assert the type of kwargs, passing `None` will disable
+        # type checking.
+        def _extract_override(var, *, expected_type):
+            deprecated_var_env = "NMIGEN_{}".format(var)
+            var_env = "AMARANTH_{}".format(var)
+            if deprecated_var_env in os.environ or var_env in os.environ:
+                # On Windows, there is no way to define an "empty but set" variable; it is tempting
+                # to use a quoted empty string, but it doesn't do what one would expect. Recognize
+                # this as a useful pattern anyway, and treat `set VAR=""` on Windows the same way
+                # `export VAR=` is treated on Linux.
+                if var_env in os.environ:
+                    var_env_value = os.environ[var_env]
+                elif deprecated_var_env in os.environ:
+                    var_env_value = os.environ[deprecated_var_env]
+                return re.sub(r'^\"\"$', "", var_env_value)
+            elif var in kwargs:
+                if not isinstance(kwargs[var], expected_type) and not expected_type is None:
+                    raise TypeError("Override '{}' must be a {}, not {!r}".format(var, expected_type.__name__, kwargs[var]))
+                else:
+                    return kwargs[var]
+            else:
+                return jinja2.Undefined(name=var)
+
+        def get_override(var):
+            value = _extract_override(var, expected_type=str)
+            return value
+
+        def get_override_flag(var):
+            value = _extract_override(var, expected_type=bool)
+            if isinstance(value, str):
+                value = value.lower()
+                if value in ("0", "no", "n", "false", ""):
+                    return False
+                if value in ("1", "yes", "y", "true"):
+                    return True
+                else:
+                    raise ValueError("Override '{}' must be one of "
+                                     "(\"0\", \"n\", \"no\", \"false\", \"\") "
+                                     "or "
+                                     "(\"1\", \"y\", \"yes\", \"true\"), not {!r}"
+                                     .format(var, value))
+            return value
+
         def emit_rtlil():
             return rtlil_text
 
@@ -323,8 +367,11 @@ class TemplatedPlatform(Platform):
                 strip_internal_attrs=True, write_verilog_opts=opts)
 
         def emit_debug_verilog(opts=()):
-            return verilog._convert_rtlil_text(rtlil_text,
-                strip_internal_attrs=False, write_verilog_opts=opts)
+            if not get_override_flag("debug_verilog"):
+                return "/* Debug Verilog generation was disabled. */"
+            else:
+                return verilog._convert_rtlil_text(rtlil_text,
+                    strip_internal_attrs=False, write_verilog_opts=opts)
 
         def emit_commands(syntax):
             commands = []
@@ -354,28 +401,7 @@ class TemplatedPlatform(Platform):
 
             return "\n".join(commands)
 
-        def get_override(var):
-            deprecated_var_env = "NMIGEN_{}".format(var)
-            var_env = "AMARANTH_{}".format(var)
-            if deprecated_var_env in os.environ or var_env in os.environ:
-                # On Windows, there is no way to define an "empty but set" variable; it is tempting
-                # to use a quoted empty string, but it doesn't do what one would expect. Recognize
-                # this as a useful pattern anyway, and treat `set VAR=""` on Windows the same way
-                # `export VAR=` is treated on Linux.
-                if var_env in os.environ:
-                    var_env_value = os.environ[var_env]
-                elif deprecated_var_env in os.environ:
-                    var_env_value = os.environ[deprecated_var_env]
-                return re.sub(r'^\"\"$', "", var_env_value)
-            elif var in kwargs:
-                if isinstance(kwargs[var], str):
-                    return textwrap.dedent(kwargs[var]).strip()
-                else:
-                    return kwargs[var]
-            else:
-                return jinja2.Undefined(name=var)
-
-        @jinja2.contextfunction
+        @jinja2.pass_context
         def invoke_tool(context, name):
             env_var = tool_env_var(name)
             if context.parent["syntax"] == "sh":
@@ -409,13 +435,13 @@ class TemplatedPlatform(Platform):
             return '"' + re.sub(r"([$[\\])", r"\\\1", string) + '"'
 
         def verbose(arg):
-            if get_override("verbose"):
+            if get_override_flag("verbose"):
                 return arg
             else:
                 return jinja2.Undefined(name="quiet")
 
         def quiet(arg):
-            if get_override("verbose"):
+            if get_override_flag("verbose"):
                 return jinja2.Undefined(name="quiet")
             else:
                 return arg
@@ -443,6 +469,7 @@ class TemplatedPlatform(Platform):
                 "syntax": syntax,
                 "invoke_tool": invoke_tool,
                 "get_override": get_override,
+                "get_override_flag": get_override_flag,
                 "verbose": verbose,
                 "quiet": quiet,
                 "autogenerated": autogenerated,
diff --git a/amaranth/build/run.py b/amaranth/build/run.py
index f87653d..cb40af6 100644
--- a/amaranth/build/run.py
+++ b/amaranth/build/run.py
@@ -154,10 +154,11 @@ class BuildPlan:
 
                     mkdirs(filename)
 
-                    mode = "wt" if isinstance(content, str) else "wb"
-                    with sftp.file(str(filename), mode) as f:
+                    mode = "t" if isinstance(content, str) else "b"
+                    with sftp.file(str(filename), "w" + mode) as f:
+                        f.set_pipelined()
                         # "b/t" modifier ignored in SFTP.
-                        if mode == "wt":
+                        if mode == "t":
                             f.write(content.encode("utf-8"))
                         else:
                             f.write(content)
@@ -261,6 +262,7 @@ class RemoteSSHBuildProducts(BuildProducts):
                 sftp.chdir(self.__root)
 
                 with sftp.file(filename, "r" + mode) as f:
+                    f.prefetch()
                     # "b/t" modifier ignored in SFTP.
                     if mode == "t":
                         return f.read().decode("utf-8")
diff --git a/amaranth/sim/core.py b/amaranth/sim/core.py
index 5ebe8f5..f8a86bb 100644
--- a/amaranth/sim/core.py
+++ b/amaranth/sim/core.py
@@ -139,6 +139,8 @@ class Simulator:
             # to happen at a non-zero time, distinguishing it from the reset values in the waveform
             # viewer.
             phase = period // 2
+        else:
+            phase = int(phase * 1e12) + period // 2
         self._engine.add_clock_process(domain.clk, phase=phase, period=period)
         self._clocked.add(domain)
 
diff --git a/amaranth/vendor/lattice_ecp5.py b/amaranth/vendor/lattice_ecp5.py
index 288296e..4d5cb7c 100644
--- a/amaranth/vendor/lattice_ecp5.py
+++ b/amaranth/vendor/lattice_ecp5.py
@@ -231,10 +231,12 @@ class LatticeECP5Platform(TemplatedPlatform):
             # {{autogenerated}}
             BLOCK ASYNCPATHS;
             BLOCK RESETPATHS;
-            {% for port_name, pin_name, extras in platform.iter_port_constraints_bits() -%}
+            {% for port_name, pin_name, attrs in platform.iter_port_constraints_bits() -%}
                 LOCATE COMP "{{port_name}}" SITE "{{pin_name}}";
+                {% if attrs -%}
                 IOBUF PORT "{{port_name}}"
-                    {%- for key, value in extras.items() %} {{key}}={{value}}{% endfor %};
+                    {%- for key, value in attrs.items() %} {{key}}={{value}}{% endfor %};
+                {% endif %}
             {% endfor %}
             {{get_override("add_preferences")|default("# (add_preferences placeholder)")}}
         """,
diff --git a/amaranth/vendor/lattice_machxo_2_3l.py b/amaranth/vendor/lattice_machxo_2_3l.py
index f24f4f9..42d9ffc 100644
--- a/amaranth/vendor/lattice_machxo_2_3l.py
+++ b/amaranth/vendor/lattice_machxo_2_3l.py
@@ -154,6 +154,27 @@ class LatticeMachXO2Or3LPlatform(TemplatedPlatform):
             -if {{name}}_impl/{{name}}_impl.bit -of {{name}}_sram.svf
         """,
     ]
+    # These numbers were extracted from
+    # "MachXO2 sysCLOCK PLL Design and Usage Guide"
+    _supported_osch_freqs = [
+        2.08, 2.15, 2.22, 2.29, 2.38, 2.46, 2.56, 2.66, 2.77, 2.89,
+        3.02, 3.17, 3.33, 3.50, 3.69, 3.91, 4.16, 4.29, 4.43, 4.59,
+        4.75, 4.93, 5.12, 5.32, 5.54, 5.78, 6.05, 6.33, 6.65, 7.00,
+        7.39, 7.82, 8.31, 8.58, 8.87, 9.17, 9.50, 9.85, 10.23, 10.64,
+        11.08, 11.57, 12.09, 12.67, 13.30, 14.00, 14.78, 15.65, 15.65, 16.63,
+        17.73, 19.00, 20.46, 22.17, 24.18, 26.60, 29.56, 33.25, 38.00, 44.33,
+        53.20, 66.50, 88.67, 133.00
+    ]
+
+    @property
+    def default_clk_constraint(self):
+        # Internal high-speed oscillator on MachXO2/MachXO3L devices.
+        # It can have a range of frequencies.
+        if self.default_clk == "OSCH":
+            assert self.osch_frequency in self._supported_osch_freqs
+            return Clock(int(self.osch_frequency * 1e6))
+        # Otherwise, use the defined Clock resource.
+        return super().default_clk_constraint
 
     def create_missing_domain(self, name):
         # Lattice MachXO2/MachXO3L devices have two global set/reset signals: PUR, which is driven at
@@ -164,7 +185,12 @@ class LatticeMachXO2Or3LPlatform(TemplatedPlatform):
         # relation to a user clock. To avoid this, a GSR/SGSR instance should be driven
         # synchronized to user clock.
         if name == "sync" and self.default_clk is not None:
-            clk_i = self.request(self.default_clk).i
+            using_osch = False
+            if self.default_clk == "OSCH":
+                using_osch = True
+                clk_i = Signal()
+            else:
+                clk_i = self.request(self.default_clk).i
             if self.default_rst is not None:
                 rst_i = self.request(self.default_rst).i
             else:
@@ -183,6 +209,13 @@ class LatticeMachXO2Or3LPlatform(TemplatedPlatform):
                 # more reliable. (None of this is documented.)
                 Instance("SGSR", i_CLK=clk_i, i_GSR=gsr1),
             ]
+            if using_osch:
+                osch_freq = self.osch_frequency
+                if osch_freq not in self._supported_osch_freqs:
+                    raise ValueError("Frequency {!r} is not valid for OSCH clock. Valid frequencies are {!r}"
+                             .format(osch_freq, self._supported_osch_freqs))
+                osch_freq_param = "{:.2f}".format(float(osch_freq))
+                m.submodules += [ Instance("OSCH", p_NOM_FREQ=osch_freq_param, i_STDBY=Const(0), o_OSC=clk_i, o_SEDSTDBY=Signal()) ]
             # GSR implicitly connects to every appropriate storage element. As such, the sync
             # domain is reset-less; domains driven by other clocks would need to have dedicated
             # reset circuitry or otherwise meet setup/hold constraints on their own.
diff --git a/docs/changes.rst b/docs/changes.rst
index 84ba53b..9a8db6d 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -4,6 +4,19 @@ Changelog
 This document describes changes to the public interfaces in the Amaranth language and standard library. It does not include most bug fixes or implementation changes.
 
 
+Next version
+============
+
+Support for Python 3.6 has been dropped.
+
+Toolchain changes
+-----------------
+
+.. currentmodule:: amaranth
+
+* Added: ``debug_verilog`` override in :class:`build.TemplatedPlatform`.
+
+
 Version 0.3
 ============
 
diff --git a/docs/install.rst b/docs/install.rst
index eeb4a1f..e8d07e3 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -6,7 +6,7 @@ System requirements
 
 .. |yosys-version| replace:: 0.10 (or newer)
 
-Amaranth HDL requires Python 3.6; it works on CPython_ 3.6 (or newer), and works faster on PyPy3.6_ 7.2 (or newer).
+Amaranth HDL requires Python 3.7; it works on CPython_ 3.7 (or newer), and works faster on PyPy3.7_ 7.3.3 (or newer).
 
 For most workflows, Amaranth requires Yosys_ |yosys-version|. A compatible version of Yosys is distributed via PyPI_ for most popular platforms.
 
@@ -17,7 +17,7 @@ Synthesizing, placing and routing an Amaranth design for an FPGA requires the FP
 .. TODO: Link to FPGA family docs here
 
 .. _CPython: https://www.python.org/
-.. _PyPy3.6: https://www.pypy.org/
+.. _PyPy3.7: https://www.pypy.org/
 .. _Yosys: https://yosyshq.net/yosys/
 .. _PyPI: https://pypi.org/
 .. _GTKWave: http://gtkwave.sourceforge.net/
diff --git a/docs/requirements.txt b/docs/requirements.txt
index 305e14a..3232632 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -1,3 +1,3 @@
 sphinx~=4.2
-sphinxcontrib-platformpicker~=1.1
-sphinx-rtd-theme~=0.5.0
+sphinxcontrib-platformpicker~=1.3
+sphinx-rtd-theme~=1.0
diff --git a/docs/tutorial.rst b/docs/tutorial.rst
index a937f61..c1797fd 100644
--- a/docs/tutorial.rst
+++ b/docs/tutorial.rst
@@ -6,6 +6,5 @@ Tutorial
    The official tutorial is still being written. Until it's ready, consider following one of the tutorials written by the Amaranth community:
 
       * `Learning FPGA Design with nMigen <https://vivonomicon.com/2020/04/14/learning-fpga-design-with-nmigen/>`_ by Vivonomicon;
-      * `nMigen Step by Step Tutorial <http://blog.lambdaconcept.com/doku.php?id=nmigen:tutorial>`_ by LambdaConcept;
       * `"I want to learn nMigen" <https://github.com/kbob/nmigen-examples>`_ by kbob;
       * `A tutorial for using nMigen <https://github.com/robertbaruch/nmigen-tutorial>`_ by Robert Baruch.
diff --git a/setup.py b/setup.py
index 5619073..197fdd3 100644
--- a/setup.py
+++ b/setup.py
@@ -24,7 +24,7 @@ def doc_version():
     if not git:
         return ""
     elif git.exact:
-        return git.format_with("{tag}")
+        return git.format_with("v{tag}")
     else:
         return "latest"
 
@@ -37,13 +37,13 @@ setup(
     description="Amaranth hardware definition language",
     #long_description="""TODO""",
     license="BSD",
-    python_requires="~=3.6",
+    python_requires="~=3.7",
     setup_requires=["wheel", "setuptools", "setuptools_scm"],
     install_requires=[
         "importlib_metadata; python_version<'3.8'",  # for __version__ and amaranth._toolchain.yosys
         "importlib_resources; python_version<'3.9'", # for amaranth._toolchain.yosys
-        "pyvcd~=0.2.2", # for amaranth.pysim
-        "Jinja2~=2.11", # for amaranth.build
+        "pyvcd>=0.2.2,<0.4", # for amaranth.pysim
+        "Jinja2~=3.0",  # for amaranth.build
     ],
     extras_require={
         # this version requirement needs to be synchronized with the one in amaranth.back.verilog!


Test patch:
diff --git a/tests/test_sim.py b/tests/test_sim.py
index e8ffa5d..72c1764 100644
--- a/tests/test_sim.py
+++ b/tests/test_sim.py
@@ -516,6 +516,45 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
                 self.assertEqual((yield self.o), 0)
             sim.add_sync_process(process)
 
+    def setUp_clock_phase(self):
+        self.m = Module()
+        self.phase0 = self.m.domains.phase0 = ClockDomain()
+        self.phase90 = self.m.domains.phase90 = ClockDomain()
+        self.phase180 = self.m.domains.phase180 = ClockDomain()
+        self.phase270 = self.m.domains.phase270 = ClockDomain()
+        self.check = self.m.domains.check = ClockDomain()
+
+        self.expected = [
+            [0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0],
+            [0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1],
+            [0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1],
+            [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0],
+        ]
+
+    def test_clock_phase(self):
+        self.setUp_clock_phase()
+        with self.assertSimulation(self.m) as sim:
+            period=1
+            sim.add_clock(period/8, phase=0,          domain="check")
+            sim.add_clock(period,   phase=0*period/4, domain="phase0")
+            sim.add_clock(period,   phase=1*period/4, domain="phase90")
+            sim.add_clock(period,   phase=2*period/4, domain="phase180")
+            sim.add_clock(period,   phase=3*period/4, domain="phase270")
+
+            def proc():
+                clocks = [
+                    self.phase0.clk,
+                    self.phase90.clk,
+                    self.phase180.clk,
+                    self.phase270.clk
+                ]
+                for i in range(16):
+                    yield
+                    for j, c in enumerate(clocks):
+                        self.assertEqual((yield c), self.expected[j][i])
+
+            sim.add_sync_process(proc, domain="check")
+
     def setUp_multiclock(self):
         self.sys = ClockDomain()
         self.pix = ClockDomain()