gateware: merge kasli_generic into kasli. Closes #2279

This commit is contained in:
Sebastien Bourdeauducq 2023-11-14 14:01:17 +08:00
parent de10e584f6
commit e81e8f28cf
4 changed files with 160 additions and 328 deletions

View File

@ -77,6 +77,9 @@ Accesses to the data argument should be replaced as below:
Refer to the updated ``no_hardware/arguments_demo.py`` example for current usage.
* Almazny v1.2 is incompatible with the legacy versions and is the default.
To use legacy versions, specify ``almazny_hw_rev`` in the JSON description.
* kasli_generic.py has been merged into kasli.py, and the demonstration designs without JSON descriptions
have been removed. The base classes remain present in kasli.py to support third-party flows without
JSON descriptions.
* Legacy PYON databases should be converted to LMDB with the script below:
::

View File

@ -1,6 +1,8 @@
#!/usr/bin/env python3
import argparse
import logging
from packaging.version import Version
from migen import *
from migen.genlib.resetsync import AsyncResetSynchronizer
@ -14,16 +16,20 @@ from misoc.targets.kasli import (
BaseSoC, MiniSoC, soc_kasli_args, soc_kasli_argdict)
from misoc.integration.builder import builder_args, builder_argdict
from artiq import __version__ as artiq_version
from artiq.gateware.amp import AMPSoC
from artiq.gateware import rtio
from artiq.gateware.rtio.phy import ttl_simple, ttl_serdes_7series, edge_counter
from artiq.gateware.rtio.xilinx_clocking import fix_serdes_timing_path
from artiq.gateware import eem
from artiq.gateware import rtio, eem, eem_7series
from artiq.gateware.drtio.transceiver import gtp_7series, eem_serdes
from artiq.gateware.drtio.siphaser import SiPhaser7Series
from artiq.gateware.drtio.rx_synchronizer import XilinxRXSynchronizer
from artiq.gateware.drtio import *
from artiq.build_soc import *
from artiq.coredevice import jsondesc
logger = logging.getLogger(__name__)
class SMAClkinForward(Module):
@ -130,89 +136,6 @@ class StandaloneBase(MiniSoC, AMPSoC):
self.csr_devices.append("rtio_analyzer")
class Tester(StandaloneBase):
"""
Configuration for CI tests. Contains the maximum number of different EEMs.
"""
def __init__(self, hw_rev=None, dds=None, **kwargs):
if hw_rev is None:
hw_rev = "v2.0"
if dds is None:
dds = "ad9910"
StandaloneBase.__init__(self, hw_rev=hw_rev, **kwargs)
# self.config["SI5324_EXT_REF"] = None
self.config["RTIO_FREQUENCY"] = "125.0"
if hw_rev == "v1.0":
# EEM clock fan-out from Si5324, not MMCX
self.comb += self.platform.request("clk_sel").eq(1)
self.rtio_channels = []
eem.DIO.add_std(self, 5,
ttl_serdes_7series.InOut_8X, ttl_serdes_7series.Output_8X,
edge_counter_cls=edge_counter.SimpleEdgeCounter)
eem.Urukul.add_std(self, 0, 1, ttl_serdes_7series.Output_8X, dds,
ttl_simple.ClockGen)
eem.Sampler.add_std(self, 3, 2, ttl_serdes_7series.Output_8X)
eem.Zotino.add_std(self, 4, ttl_serdes_7series.Output_8X)
if hw_rev in ("v1.0", "v1.1"):
for i in (1, 2):
sfp_ctl = self.platform.request("sfp_ctl", i)
phy = ttl_simple.Output(sfp_ctl.led)
self.submodules += phy
self.rtio_channels.append(rtio.Channel.from_phy(phy))
self.config["HAS_RTIO_LOG"] = None
self.config["RTIO_LOG_CHANNEL"] = len(self.rtio_channels)
self.rtio_channels.append(rtio.LogChannel())
self.add_rtio(self.rtio_channels)
class SUServo(StandaloneBase):
"""
SUServo (Sampler-Urukul-Servo) extension variant configuration
"""
def __init__(self, hw_rev=None, **kwargs):
if hw_rev is None:
hw_rev = "v2.0"
StandaloneBase.__init__(self, hw_rev=hw_rev, **kwargs)
# self.config["SI5324_EXT_REF"] = None
self.config["RTIO_FREQUENCY"] = "125.0"
if hw_rev == "v1.0":
# EEM clock fan-out from Si5324, not MMCX
self.comb += self.platform.request("clk_sel").eq(1)
self.rtio_channels = []
# EEM0, EEM1: DIO
eem.DIO.add_std(self, 0,
ttl_serdes_7series.InOut_8X, ttl_serdes_7series.Output_8X)
eem.DIO.add_std(self, 1,
ttl_serdes_7series.Output_8X, ttl_serdes_7series.Output_8X)
# EEM3/2: Sampler, EEM5/4: Urukul, EEM7/6: Urukul
eem.SUServo.add_std(self,
eems_sampler=(3, 2),
eems_urukul=[[5, 4], [7, 6]])
for i in (1, 2):
sfp_ctl = self.platform.request("sfp_ctl", i)
phy = ttl_simple.Output(sfp_ctl.led)
self.submodules += phy
self.rtio_channels.append(rtio.Channel.from_phy(phy))
self.config["HAS_RTIO_LOG"] = None
self.config["RTIO_LOG_CHANNEL"] = len(self.rtio_channels)
self.rtio_channels.append(rtio.LogChannel())
self.add_rtio(self.rtio_channels)
pads = self.platform.lookup_request("sampler3_adc_data_p")
self.platform.add_false_path_constraints(
pads.clkout, self.crg.cd_sys.clk)
class MasterBase(MiniSoC, AMPSoC):
mem_map = {
"cri_con": 0x10000000,
@ -640,77 +563,184 @@ class SatelliteBase(BaseSoC, AMPSoC):
self.get_native_sdram_if(), cpu_dw=self.cpu_dw)
self.csr_devices.append("rtio_analyzer")
class Master(MasterBase):
def __init__(self, hw_rev=None, **kwargs):
class GenericStandalone(StandaloneBase):
def __init__(self, description, hw_rev=None,**kwargs):
if hw_rev is None:
hw_rev = "v2.0"
MasterBase.__init__(self, hw_rev=hw_rev, **kwargs)
hw_rev = description["hw_rev"]
self.class_name_override = description["variant"]
StandaloneBase.__init__(self, hw_rev=hw_rev, **kwargs)
self.config["RTIO_FREQUENCY"] = "{:.1f}".format(description["rtio_frequency"]/1e6)
if "ext_ref_frequency" in description:
self.config["SI5324_EXT_REF"] = None
self.config["EXT_REF_FREQUENCY"] = "{:.1f}".format(
description["ext_ref_frequency"]/1e6)
if hw_rev == "v1.0":
# EEM clock fan-out from Si5324, not MMCX
self.comb += self.platform.request("clk_sel").eq(1)
has_grabber = any(peripheral["type"] == "grabber" for peripheral in description["peripherals"])
if has_grabber:
self.grabber_csr_group = []
self.rtio_channels = []
phy = ttl_simple.Output(self.platform.request("user_led", 0))
eem_7series.add_peripherals(self, description["peripherals"])
if hw_rev in ("v1.0", "v1.1"):
for i in (1, 2):
print("SFP LED at RTIO channel 0x{:06x}".format(len(self.rtio_channels)))
sfp_ctl = self.platform.request("sfp_ctl", i)
phy = ttl_simple.Output(sfp_ctl.led)
self.submodules += phy
self.rtio_channels.append(rtio.Channel.from_phy(phy))
if hw_rev in ("v1.1", "v2.0"):
for i in range(3):
print("USER LED at RTIO channel 0x{:06x}".format(len(self.rtio_channels)))
phy = ttl_simple.Output(self.platform.request("user_led", i))
self.submodules += phy
self.rtio_channels.append(rtio.Channel.from_phy(phy))
# matches Tester EEM numbers
eem.DIO.add_std(self, 5,
ttl_serdes_7series.InOut_8X, ttl_serdes_7series.Output_8X)
eem.Urukul.add_std(self, 0, 1, ttl_serdes_7series.Output_8X)
self.config["HAS_RTIO_LOG"] = None
self.config["RTIO_LOG_CHANNEL"] = len(self.rtio_channels)
self.rtio_channels.append(rtio.LogChannel())
self.add_rtio(self.rtio_channels)
self.add_rtio(self.rtio_channels, sed_lanes=description["sed_lanes"])
if has_grabber:
self.config["HAS_GRABBER"] = None
self.add_csr_group("grabber", self.grabber_csr_group)
for grabber in self.grabber_csr_group:
self.platform.add_false_path_constraints(
self.crg.cd_sys.clk, getattr(self, grabber).deserializer.cd_cl.clk)
class Satellite(SatelliteBase):
def __init__(self, hw_rev=None, **kwargs):
class GenericMaster(MasterBase):
def __init__(self, description, hw_rev=None, **kwargs):
if hw_rev is None:
hw_rev = "v2.0"
SatelliteBase.__init__(self, hw_rev=hw_rev, **kwargs)
hw_rev = description["hw_rev"]
self.class_name_override = description["variant"]
has_drtio_over_eem = any(peripheral["type"] == "shuttler" for peripheral in description["peripherals"])
MasterBase.__init__(self,
hw_rev=hw_rev,
rtio_clk_freq=description["rtio_frequency"],
enable_sata=description["enable_sata_drtio"],
enable_sys5x=has_drtio_over_eem,
**kwargs)
if "ext_ref_frequency" in description:
self.config["SI5324_EXT_REF"] = None
self.config["EXT_REF_FREQUENCY"] = "{:.1f}".format(
description["ext_ref_frequency"]/1e6)
if hw_rev == "v1.0":
# EEM clock fan-out from Si5324, not MMCX
self.comb += self.platform.request("clk_sel").eq(1)
if has_drtio_over_eem:
self.eem_drtio_channels = []
has_grabber = any(peripheral["type"] == "grabber" for peripheral in description["peripherals"])
if has_grabber:
self.grabber_csr_group = []
self.rtio_channels = []
phy = ttl_simple.Output(self.platform.request("user_led", 0))
eem_7series.add_peripherals(self, description["peripherals"])
if hw_rev in ("v1.1", "v2.0"):
for i in range(3):
print("USER LED at RTIO channel 0x{:06x}".format(len(self.rtio_channels)))
phy = ttl_simple.Output(self.platform.request("user_led", i))
self.submodules += phy
self.rtio_channels.append(rtio.Channel.from_phy(phy))
# matches Tester EEM numbers
eem.DIO.add_std(self, 5,
ttl_serdes_7series.InOut_8X, ttl_serdes_7series.Output_8X)
self.add_rtio(self.rtio_channels)
self.config["HAS_RTIO_LOG"] = None
self.config["RTIO_LOG_CHANNEL"] = len(self.rtio_channels)
self.rtio_channels.append(rtio.LogChannel())
if has_drtio_over_eem:
self.add_eem_drtio(self.eem_drtio_channels)
self.add_drtio_cpuif_groups()
self.add_rtio(self.rtio_channels, sed_lanes=description["sed_lanes"])
if has_grabber:
self.config["HAS_GRABBER"] = None
self.add_csr_group("grabber", self.grabber_csr_group)
for grabber in self.grabber_csr_group:
self.platform.add_false_path_constraints(
self.gt_drtio.gtps[0].txoutclk, getattr(self, grabber).deserializer.cd_cl.clk)
VARIANTS = {cls.__name__.lower(): cls for cls in [Tester, SUServo, Master, Satellite]}
class GenericSatellite(SatelliteBase):
def __init__(self, description, hw_rev=None, **kwargs):
if hw_rev is None:
hw_rev = description["hw_rev"]
self.class_name_override = description["variant"]
SatelliteBase.__init__(self,
hw_rev=hw_rev,
rtio_clk_freq=description["rtio_frequency"],
enable_sata=description["enable_sata_drtio"],
**kwargs)
if hw_rev == "v1.0":
# EEM clock fan-out from Si5324, not MMCX
self.comb += self.platform.request("clk_sel").eq(1)
has_grabber = any(peripheral["type"] == "grabber" for peripheral in description["peripherals"])
if has_grabber:
self.grabber_csr_group = []
self.rtio_channels = []
eem_7series.add_peripherals(self, description["peripherals"])
if hw_rev in ("v1.1", "v2.0"):
for i in range(3):
print("USER LED at RTIO channel 0x{:06x}".format(len(self.rtio_channels)))
phy = ttl_simple.Output(self.platform.request("user_led", i))
self.submodules += phy
self.rtio_channels.append(rtio.Channel.from_phy(phy))
self.config["HAS_RTIO_LOG"] = None
self.config["RTIO_LOG_CHANNEL"] = len(self.rtio_channels)
self.rtio_channels.append(rtio.LogChannel())
self.add_rtio(self.rtio_channels, sed_lanes=description["sed_lanes"])
if has_grabber:
self.config["HAS_GRABBER"] = None
self.add_csr_group("grabber", self.grabber_csr_group)
for grabber in self.grabber_csr_group:
self.platform.add_false_path_constraints(
self.gt_drtio.gtps[0].txoutclk, getattr(self, grabber).deserializer.cd_cl.clk)
def main():
parser = argparse.ArgumentParser(
description="ARTIQ device binary builder for Kasli systems")
description="ARTIQ device binary builder for generic Kasli systems")
builder_args(parser)
soc_kasli_args(parser)
parser.set_defaults(output_dir="artiq_kasli")
parser.add_argument("-V", "--variant", default="tester",
help="variant: {} (default: %(default)s)".format(
"/".join(sorted(VARIANTS.keys()))))
parser.add_argument("--tester-dds", default=None,
help="Tester variant DDS type: ad9910/ad9912 "
"(default: ad9910)")
parser.add_argument("description", metavar="DESCRIPTION",
help="JSON system description file")
parser.add_argument("--gateware-identifier-str", default=None,
help="Override ROM identifier")
args = parser.parse_args()
description = jsondesc.load(args.description)
argdict = dict()
argdict["gateware_identifier_str"] = args.gateware_identifier_str
argdict["dds"] = args.tester_dds
min_artiq_version = description["min_artiq_version"]
if Version(artiq_version) < Version(min_artiq_version):
logger.warning("ARTIQ version mismatch: current %s < %s minimum",
artiq_version, min_artiq_version)
variant = args.variant.lower()
try:
cls = VARIANTS[variant]
except KeyError:
raise SystemExit("Invalid variant (-V/--variant)")
if description["target"] != "kasli":
raise ValueError("Description is for a different target")
soc = cls(**soc_kasli_argdict(args), **argdict)
if description["drtio_role"] == "standalone":
cls = GenericStandalone
elif description["drtio_role"] == "master":
cls = GenericMaster
elif description["drtio_role"] == "satellite":
cls = GenericSatellite
else:
raise ValueError("Invalid DRTIO role")
has_shuttler = any(peripheral["type"] == "shuttler" for peripheral in description["peripherals"])
if has_shuttler and (description["drtio_role"] == "standalone"):
raise ValueError("Shuttler requires DRTIO, please switch role to master")
soc = cls(description, gateware_identifier_str=args.gateware_identifier_str, **soc_kasli_argdict(args))
args.variant = description["variant"]
build_artiq_soc(soc, builder_argdict(args))

View File

@ -1,201 +0,0 @@
#!/usr/bin/env python3
import argparse
import logging
from packaging.version import Version
from misoc.integration.builder import builder_args, builder_argdict
from misoc.targets.kasli import soc_kasli_args, soc_kasli_argdict
from artiq import __version__ as artiq_version
from artiq.coredevice import jsondesc
from artiq.gateware import rtio, eem_7series
from artiq.gateware.rtio.phy import ttl_simple
from artiq.gateware.targets.kasli import StandaloneBase, MasterBase, SatelliteBase
from artiq.build_soc import *
logger = logging.getLogger(__name__)
class GenericStandalone(StandaloneBase):
def __init__(self, description, hw_rev=None,**kwargs):
if hw_rev is None:
hw_rev = description["hw_rev"]
self.class_name_override = description["variant"]
StandaloneBase.__init__(self, hw_rev=hw_rev, **kwargs)
self.config["RTIO_FREQUENCY"] = "{:.1f}".format(description["rtio_frequency"]/1e6)
if "ext_ref_frequency" in description:
self.config["SI5324_EXT_REF"] = None
self.config["EXT_REF_FREQUENCY"] = "{:.1f}".format(
description["ext_ref_frequency"]/1e6)
if hw_rev == "v1.0":
# EEM clock fan-out from Si5324, not MMCX
self.comb += self.platform.request("clk_sel").eq(1)
has_grabber = any(peripheral["type"] == "grabber" for peripheral in description["peripherals"])
if has_grabber:
self.grabber_csr_group = []
self.rtio_channels = []
eem_7series.add_peripherals(self, description["peripherals"])
if hw_rev in ("v1.0", "v1.1"):
for i in (1, 2):
print("SFP LED at RTIO channel 0x{:06x}".format(len(self.rtio_channels)))
sfp_ctl = self.platform.request("sfp_ctl", i)
phy = ttl_simple.Output(sfp_ctl.led)
self.submodules += phy
self.rtio_channels.append(rtio.Channel.from_phy(phy))
if hw_rev in ("v1.1", "v2.0"):
for i in range(3):
print("USER LED at RTIO channel 0x{:06x}".format(len(self.rtio_channels)))
phy = ttl_simple.Output(self.platform.request("user_led", i))
self.submodules += phy
self.rtio_channels.append(rtio.Channel.from_phy(phy))
self.config["HAS_RTIO_LOG"] = None
self.config["RTIO_LOG_CHANNEL"] = len(self.rtio_channels)
self.rtio_channels.append(rtio.LogChannel())
self.add_rtio(self.rtio_channels, sed_lanes=description["sed_lanes"])
if has_grabber:
self.config["HAS_GRABBER"] = None
self.add_csr_group("grabber", self.grabber_csr_group)
for grabber in self.grabber_csr_group:
self.platform.add_false_path_constraints(
self.crg.cd_sys.clk, getattr(self, grabber).deserializer.cd_cl.clk)
class GenericMaster(MasterBase):
def __init__(self, description, hw_rev=None, **kwargs):
if hw_rev is None:
hw_rev = description["hw_rev"]
self.class_name_override = description["variant"]
has_drtio_over_eem = any(peripheral["type"] == "shuttler" for peripheral in description["peripherals"])
MasterBase.__init__(self,
hw_rev=hw_rev,
rtio_clk_freq=description["rtio_frequency"],
enable_sata=description["enable_sata_drtio"],
enable_sys5x=has_drtio_over_eem,
**kwargs)
if "ext_ref_frequency" in description:
self.config["SI5324_EXT_REF"] = None
self.config["EXT_REF_FREQUENCY"] = "{:.1f}".format(
description["ext_ref_frequency"]/1e6)
if hw_rev == "v1.0":
# EEM clock fan-out from Si5324, not MMCX
self.comb += self.platform.request("clk_sel").eq(1)
if has_drtio_over_eem:
self.eem_drtio_channels = []
has_grabber = any(peripheral["type"] == "grabber" for peripheral in description["peripherals"])
if has_grabber:
self.grabber_csr_group = []
self.rtio_channels = []
eem_7series.add_peripherals(self, description["peripherals"])
if hw_rev in ("v1.1", "v2.0"):
for i in range(3):
print("USER LED at RTIO channel 0x{:06x}".format(len(self.rtio_channels)))
phy = ttl_simple.Output(self.platform.request("user_led", i))
self.submodules += phy
self.rtio_channels.append(rtio.Channel.from_phy(phy))
self.config["HAS_RTIO_LOG"] = None
self.config["RTIO_LOG_CHANNEL"] = len(self.rtio_channels)
self.rtio_channels.append(rtio.LogChannel())
if has_drtio_over_eem:
self.add_eem_drtio(self.eem_drtio_channels)
self.add_drtio_cpuif_groups()
self.add_rtio(self.rtio_channels, sed_lanes=description["sed_lanes"])
if has_grabber:
self.config["HAS_GRABBER"] = None
self.add_csr_group("grabber", self.grabber_csr_group)
for grabber in self.grabber_csr_group:
self.platform.add_false_path_constraints(
self.gt_drtio.gtps[0].txoutclk, getattr(self, grabber).deserializer.cd_cl.clk)
class GenericSatellite(SatelliteBase):
def __init__(self, description, hw_rev=None, **kwargs):
if hw_rev is None:
hw_rev = description["hw_rev"]
self.class_name_override = description["variant"]
SatelliteBase.__init__(self,
hw_rev=hw_rev,
rtio_clk_freq=description["rtio_frequency"],
enable_sata=description["enable_sata_drtio"],
**kwargs)
if hw_rev == "v1.0":
# EEM clock fan-out from Si5324, not MMCX
self.comb += self.platform.request("clk_sel").eq(1)
has_grabber = any(peripheral["type"] == "grabber" for peripheral in description["peripherals"])
if has_grabber:
self.grabber_csr_group = []
self.rtio_channels = []
eem_7series.add_peripherals(self, description["peripherals"])
if hw_rev in ("v1.1", "v2.0"):
for i in range(3):
print("USER LED at RTIO channel 0x{:06x}".format(len(self.rtio_channels)))
phy = ttl_simple.Output(self.platform.request("user_led", i))
self.submodules += phy
self.rtio_channels.append(rtio.Channel.from_phy(phy))
self.config["HAS_RTIO_LOG"] = None
self.config["RTIO_LOG_CHANNEL"] = len(self.rtio_channels)
self.rtio_channels.append(rtio.LogChannel())
self.add_rtio(self.rtio_channels, sed_lanes=description["sed_lanes"])
if has_grabber:
self.config["HAS_GRABBER"] = None
self.add_csr_group("grabber", self.grabber_csr_group)
for grabber in self.grabber_csr_group:
self.platform.add_false_path_constraints(
self.gt_drtio.gtps[0].txoutclk, getattr(self, grabber).deserializer.cd_cl.clk)
def main():
parser = argparse.ArgumentParser(
description="ARTIQ device binary builder for generic Kasli systems")
builder_args(parser)
soc_kasli_args(parser)
parser.set_defaults(output_dir="artiq_kasli")
parser.add_argument("description", metavar="DESCRIPTION",
help="JSON system description file")
parser.add_argument("--gateware-identifier-str", default=None,
help="Override ROM identifier")
args = parser.parse_args()
description = jsondesc.load(args.description)
min_artiq_version = description["min_artiq_version"]
if Version(artiq_version) < Version(min_artiq_version):
logger.warning("ARTIQ version mismatch: current %s < %s minimum",
artiq_version, min_artiq_version)
if description["target"] != "kasli":
raise ValueError("Description is for a different target")
if description["drtio_role"] == "standalone":
cls = GenericStandalone
elif description["drtio_role"] == "master":
cls = GenericMaster
elif description["drtio_role"] == "satellite":
cls = GenericSatellite
else:
raise ValueError("Invalid DRTIO role")
has_shuttler = any(peripheral["type"] == "shuttler" for peripheral in description["peripherals"])
if has_shuttler and (description["drtio_role"] == "standalone"):
raise ValueError("Shuttler requires DRTIO, please switch role to master")
soc = cls(description, gateware_identifier_str=args.gateware_identifier_str, **soc_kasli_argdict(args))
args.variant = description["variant"]
build_artiq_soc(soc, builder_argdict(args))
if __name__ == "__main__":
main()

View File

@ -16,7 +16,7 @@ ARTIQ itself does not depend on Nix, and it is also possible to compile everythi
* Enable flakes in Nix by e.g. adding ``experimental-features = nix-command flakes`` to ``nix.conf`` (for example ``~/.config/nix/nix.conf``).
* Clone the ARTIQ Git repository and run ``nix develop`` at the root (where ``flake.nix`` is).
* Make the current source code of ARTIQ available to the Python interpreter by running ``export PYTHONPATH=`pwd`:$PYTHONPATH``.
* You can then build the firmware and gateware with a command such as ``$ python -m artiq.gateware.targets.kasli``. If you are using a JSON system description file, use ``$ python -m artiq.gateware.targets.kasli_generic file.json``.
* You can then build the firmware and gateware with a command such as ``$ python -m artiq.gateware.targets.kasli file.json``.
* Flash the binaries into the FPGA board with a command such as ``$ artiq_flash --srcbuild -d artiq_kasli/<your_variant>``. You need to configure OpenOCD as explained :ref:`in the user section <configuring-openocd>`. OpenOCD is already part of the flake's development environment.
* Check that the board boots and examine the UART messages by running a serial terminal program, e.g. ``$ flterm /dev/ttyUSB1`` (``flterm`` is part of MiSoC and installed in the flake's development environment). Leave the terminal running while you are flashing the board, so that you see the startup messages when the board boots immediately after flashing. You can also restart the board (without reflashing it) with ``$ artiq_flash start``.
* The communication parameters are 115200 8-N-1. Ensure that your user has access to the serial device (e.g. by adding the user account to the ``dialout`` group).