System on Chip design auxiliaries

For mass generation of bus decoders, a register bit map must be associated with corresponding control, status or data signals. This SoC concept follows the MaSoCist register map design rules:

  • Registers are mapped into memory space and are accessed by an address, hence.
  • They can be flagged read-only, write-only or volatile:
    • READONLY: Writing to the register has no effect
    • WRITEONLY: Reading from this register returns an undefined value
    • VOLATILE: Write or read access triggers a pulse on the corresponding select lines. This allows to implement W1C (write one to clear) behaviour, or optimized data in/out transfers.
  • Registers contain bit fields that can be READONLY or WRITEONLY
  • Two register definitions (one READONLY, one WRITEONLY) can be mapped to one address. This is used for data I/O.

To skip the entire elaboration on myIRL auxiliaries, jump to the actual register map decoder implementation

Class enhancements

The NamedBitfield and Register class is extended with flags:

In [1]:
import sys
sys.path.insert(0, "../../")
In [2]:
from myirl.library.registers import Register, NamedBitfield
from myirl.kernel.components import ContainerExtension, ContainerBase, Signal, ChildAlias
from myirl.library.bulksignals import BulkWrapperSig
from myhdl import intbv
from myirl.kernel.sensitivity import hdlmacro
from myirl import targets

class BF(NamedBitfield):
    READONLY = 0x01
    WRITEONLY = 0x02
    
    def __init__(self, name, msb, lsb, flags = 0, default = None):
        super().__init__(name, msb, lsb)
        self.init = default # Initialization value when reset
        self.flags = flags  # Access flags

class Reg(Register):
    VOLATILE = 0x04
    READONLY = 0x01
    WRITEONLY = 0x02

    def __init__(self, size, bitfields, flags = 0):
        super().__init__(size, bitfields)
        self.flags = flags
    
    def has_select(self, flag):
        if (self.flags & Reg.VOLATILE) and not (self.flags & flag):
            return True
    
        return False

We then create a RegisterSignal derived from the ContainerExtension that takes a register as argument. To allow aliasing, we also need to be able to pass a signal dictionary, optionally.

This class also adds a few special assignment methods for its signal members, according to their WRITEONLY/READONLY configuration.

In [3]:
class RegisterSignal(ContainerExtension):
    def __init__(self, name, reg, signals = None, virtual = None):
        if name is None:
            name = kernel.utils.get_uuid('reg_')
        
        # Hack to allow cloning/aliasing
        if signals is not None:
            super().__init__(name, signals, template = reg, virtual = True, twoway = True)
            return
            
        if not isinstance(reg, Register):
            raise TypeError("expects register as argument")
            
        bitfields = reg.members().items()
            
        signals = {
            n : Signal(intbv(ival := i.init if i.init is not None else 0)[i.msb + 1-i.lsb:] if i.msb != i.lsb else bool(ival),
                       name = i.name)
            for n, i in bitfields
        }
        # Selection signals ('W1C' etc.)
        
        select_sigs = {}

        if reg.flags & reg.VOLATILE:
            if not reg.flags & reg.READONLY:
                select_sigs["sel_w"] = Signal(bool(0))
            if not reg.flags & reg.WRITEONLY:
                select_sigs["sel_r"] = Signal(bool(0))

        inputs = filter(lambda t: (t[1].flags & BF.WRITEONLY) == False, bitfields)
        outputs = filter(lambda t: (t[1].flags & BF.READONLY) == False, bitfields)
        
        input_container  = self.create(name, 'read', { n : signals[n] for n, _ in inputs }, ())
        output_container = self.create(name, 'write', { n : signals[n] for n, _ in outputs }, ())

        select_container = self.create(name, 'sel', select_sigs, ())
                
        sigs = {}
        
        if input_container is not None:
            sigs['read'] = input_container
        if output_container is not None:
            output_container.driven = True
            output_container._aux = False
            sigs['write'] = output_container
        if select_container is not None:
            select_container.driven = True
            sigs['select'] =  select_container

        for n, s in sigs.items():
            s.rename(name + '_' + n)
            
        super().__init__(name, sigs, template = reg, virtual = True, twoway = True)
        
    def create(self, name, mname, children, bases = tuple()):
        "Dynamically create a subclass from self._type"
        if len(children) == 0:
            return None
        d = { '_templ' : children, '_virtual' : True,
              '_rank' : ContainerBase._rank }

        sign = name + '_' + mname
        
        container = type(sign, (BulkWrapperSig, *bases), d)
        targets.vhdl.register_type(container, sign)
        inst = container()
        inst._populate(children)
        return inst
    
    def alias(self, name):
        "We need a different .alias() method, as we pass a Register for a new object"
        signals = {}
        for n, s in self._members.items():
            nn = n
            sig = ChildAlias(self, s, n, is_member = False)
            signals[nn] = sig
            
        new = type(self)(name, self._template, signals)
        new.rename(name) # Need explicit rename here XXX
        return new

    # Here we don't need a @hdlmacro, because Register.assign() already returns
    # a generator
    def assign(self, data):
        d = {}
        try:
            readport_members = self.get_children()['read'].members().items()
            for n, i in readport_members:
                bf = self._template.bfmap[n]
                d[n] = i
 
            gen = self._template.assign(data, **d)
            return gen
        except KeyError:
            return None

    @hdlmacro
    def select_reset(self):
        try:
            members = self.get_children()['select'].members().items()
            gen = []
            for n, i in members:
                gen.append(i.set(False))
            yield gen
        except KeyError:
            yield []
        
    @hdlmacro
    def set(self, other):
        gen = []
        try:
            w = self.get_children()['write']
            m = w.members()
            for n, i in m.items():
                bf = self._template.bfmap[n]
                if bf.msb == bf.lsb:
                    gen.append(i.set(other[bf.msb]))
                else:
                    gen.append(i.set(other[bf.msb + 1: bf.lsb]))

            yield gen
        except KeyError:
            yield []

Register definitions

Add a few register with bit fields and flags:

In [4]:
reg01 = Reg(16,
    [
        BF("im", 3, 1, flags = BF.READONLY),
        BF("ex", 7, 6),
        BF("inv", 4, 4, flags = BF.WRITEONLY),
        BF("mode", 14, 10, default = 2)
    ]
)

reg02 = Reg(16,
    [
        BF("gna", 6, 1, default = 8),
        BF("reset", 7, 7, default = True)
    ],
    flags = Reg.VOLATILE | Reg.WRITEONLY
)

# This is a description for an address map
regdesc = {
    0x01: ['stat', reg01],
    0x02: ['ctrl', reg02],
    0x04: ['TXD',  Reg(16, [ BF("DATA", 15, 0)], flags = Reg.WRITEONLY | Reg.VOLATILE) ],
    0x05: ['RXD',  Reg(16, [ BF("DATA", 15, 0)], flags = Reg.READONLY | Reg.VOLATILE)]
}

Register decoder table generator

To turn this into a register decoder circuit, we create a factory function, returning a worker process:

In [5]:
from myirl.kernel import sensitivity
from myirl.kernel.sig import ConstSig
from myirl import simulation

def gen_regdec(regmap, port, clk, reset, wr, addr, idata, odata,
               REPORT_ILLEGAL_ACCESS = False,
               RESET_DEFAULTS = False):
    
    kwargs = { 'EDGE' : clk.POS }
    if RESET_DEFAULTS:
        kwargs['RESET'] = reset
    
    @sensitivity.process(clk, **kwargs)
    def worker(logic):
        """Creates a case/when flow the procedural way"""
        cw, cr = [ logic.Case(addr) for _ in range(2) ]
        N = addr.size()
                
        _reset = []
        for k, rdesc in regmap.items():
            name, rd = rdesc[0], rdesc[1]
            if rd.has_select(Reg.READONLY):
                maybe_write_sel = port[name].select.sel_w.set(True)
            else:
                maybe_write_sel = None
            if rd.has_select(Reg.WRITEONLY):
                maybe_read_sel = port[name].select.sel_r.set(True)
            else:
                maybe_read_sel = None
                                
            reg = port[name]   
            _reset += [ reg.select_reset(), ]
            s = ConstSig(k, N)
            if not rd.flags & Reg.READONLY:
                cw = cw.When(s)(reg.set(idata), maybe_write_sel)
            if not rd.flags & Reg.WRITEONLY:
                cr = cr.When(s)(reg.assign(odata), maybe_read_sel)
                
        if REPORT_ILLEGAL_ACCESS:
            cw = cw.Other(sim.raise_(ValueError("Illegal WRITE address")))
            cr = cr.Other(sim.raise_(ValueError("Illegal READ address")))
        else:
            cw = cw.Other(None)
            cr = cr.Other(None)
        
        _if = logic.If(wr == True).Then(cw).Else(cr)
 
        logic += _reset
        logic += [_if ]
        
    return worker

The register decoder for this specific memory mapped register has a dynamic registerbank dictionary passed to the interface, containing the register in/out wires. This variable argument construct is inferred to a HDL description.

Register map decoder

The actual register map decoder consists of the code below.

In [6]:
from myirl.emulation.myhdl import *
from myirl.library.portion import *

SigType = Signal.Type

Bool = SigType(bool)
Addr = SigType(intbv, 12)
Data = SigType(intbv, 16)

@block
def mmr_decode(
    clk : ClkSignal,
    reset : ResetSignal,
    addr : Addr,
    wr   : Bool,
    data_in : Data,
    data_out : Data.Output,
    REGDESC,
    **registerbank
):
    # We use a partially assigneable signal:
    
    idata = PASignal(intbv()[len(data_out):])
    
    # Then generate the decoder from the register map description passed:
    wk = gen_regdec(REGDESC, registerbank, clk, reset, wr, addr, data_in, idata,
                   RESET_DEFAULTS = True)

    @always(clk.posedge)
    def drive():
        data_out.next = idata
            
    return instances()

Test bench

We define an interface generation function that creates a signal dictionary out of the register description:

In [7]:
# Interface generation:

def gen_interface(rd):
    d = {}
    for k, rdesc in rd.items():
        n, reg = rdesc[0], rdesc[1]
        sig = RegisterSignal(n, reg)
        sig.rename(n)
        d[n] = sig
          
    return d

We might pack all MMR signals into a port structure including auxiliary methods. We need to decorate them with @hdlmacro in order to return a generator element usable within the myHDL @instance.

In [8]:
@container()
class MMRPort:
    _inputs = ['din', 'wr', 'addr']
    _outputs = ['dout']
    _other = ['clk', 'rst']
        
    def __init__(self):
        self.clk = ClkSignal()
        self.wr = Signal(bool())
        self.addr = Addr()
        self.rst = ResetSignal(0, 1)
        self.din, self.dout = [ Data() for _ in range(2) ]
        
    @hdlmacro
    def reset_sequence(self):
        p = self
        yield [
            p.rst.set(True),
            simulation.wait(2 * (p.clk.posedge, )),
            p.rst.set(False)  
        ]

    @hdlmacro
    def write_sequence(self, a, d):
        p = self
        yield [
            p.addr.set(a),
            p.din.set(d),
            simulation.wait(p.clk.posedge),
            p.wr.set(True),
            simulation.wait(p.clk.posedge),
            p.wr.set(False),
        ]
        
    @hdlmacro
    def assert_read(self, addr, data):
        yield [
            self.addr.set(addr),
            self.wr.set(False),
            simulation.wait(2 * (self.clk.posedge,)),
            simulation.assert_(self.dout == data, "Read mismatch")
        ]

The test bench

Finally, we run a reset/write on the decoder:

In [9]:
@block
def testbench(regdesc):
    p = MMRPort()
    clk = ClkSignal('clk')
    
    mon_gna = Signal(intbv()[6:])
    mon_select = Signal(bool())
    debug = Signal(bool())

    interface = gen_interface(regdesc)
    
    wires = [
        mon_gna.wireup(interface['ctrl'].read.gna),
        mon_select.wireup(interface['ctrl'].select.sel_w),
        p.clk.wireup(clk)
    ]
    
    inst = mmr_decode(clk, p.rst, p.addr, p.wr, p.din, p.dout, regdesc, **interface )
    
    @always(delay(2))
    def clkgen():
        clk.next = ~clk

    ctrl = interface['ctrl']
    stat = interface['stat']
    
    @instance
    def stimulus():
        print("START")
        debug.next = False
        p.wr.next = False
        p.addr.next = 0x001
 
        p.reset_sequence()
                
        stat.read.ex.next = 0
        stat.read.mode.next = 4
        stat.read.im.next = 2

        p.assert_read(0x001, 0x1004)
        
        p.write_sequence(0x002, 0xfa)
        debug.next = True
        
        yield clk.posedge
        assert ctrl.select.sel_w == True
        assert ctrl.write.gna == 0x3d
        yield clk.negedge
        assert ctrl.select.sel_w == False

        p.write_sequence(0x001, 0x10)
        assert stat.write.inv == True

        yield 2 * (clk.posedge, )
    
        print("DONE")

        raise StopSimulation
    
    return instances()

def test():
    from myirl.test.common_test import run_ghdl
    tb = testbench(regdesc)
    f = tb.elab(targets.VHDL, elab_all = True)
    # Turn 'debug' on for simulation output
    run_ghdl(f, tb, debug = True, vcdfile = 'testbench.vcd')
    return f

f = test()
../../myirl/kernel/components.py:139: UserWarning: Base `dict` (Port 'regdesc') does not add to the interface.
Local signal instances will be created. Use BulkSignal classes instead.
  base.warnings.warn("""Base `dict` (Port '%s') does not add to the interface.
../../myirl/kernel/components.py:139: UserWarning: Base `dict` (Port 'REGDESC') does not add to the interface.
Local signal instances will be created. Use BulkSignal classes instead.
  base.warnings.warn("""Base `dict` (Port '%s') does not add to the interface.
Creating process 'mmr_decode/drive' with sensitivity (clk'rising,)
Creating process 'testbench/clkgen' with sensitivity ([ DeltaT 2 ns ],)
Creating sequential 'testbench/stimulus' 
 Elaborating component mmr_decode_s1_s1_s12_s1_s16_s16_d_1_2_4_5 
 DEBUG: Skip virtual/interface: 'ctrl_select.sel_w' 
 DEBUG: Skip virtual/interface: 'TXD_select.sel_w' 
 DEBUG: Skip virtual/interface: 'RXD_select.sel_r' 
 DEBUG: Skip virtual/interface: 'TXD_write.DATA' 
 DEBUG: Skip virtual/interface: 'ctrl_write.gna' 
 DEBUG: Skip virtual/interface: 'ctrl_write.reset' 
 DEBUG: Skip virtual/interface: 'stat_write.ex' 
 DEBUG: Skip virtual/interface: 'stat_write.inv' 
 DEBUG: Skip virtual/interface: 'stat_write.mode' 
 Writing 'mmr_decode' to file /tmp/myirl_top_testbench_o0qeq0rq/mmr_decode.vhdl 
 Elaborating component testbench_d_1_2_4_5 
 DEBUG: Skip virtual/interface: 'stat_read.ex' 
 DEBUG: Skip virtual/interface: 'stat_read.mode' 
 DEBUG: Skip virtual/interface: 'stat_read.im' 
 Writing 'testbench' to file /tmp/myirl_top_testbench_o0qeq0rq/testbench.vhdl 
 Creating library file /tmp/myirl_module_defs_8q69ouxt/module_defs.vhdl 
==== COSIM stdout ====

==== COSIM stderr ====

==== COSIM stdout ====
analyze /home/testing/src/myhdl2/myirl/targets/../test/vhdl/txt_util.vhdl
analyze /home/testing/src/myhdl2/myirl/targets/libmyirl.vhdl
analyze /tmp/myirl_module_defs_8q69ouxt/module_defs.vhdl
analyze /tmp/myirl_top_testbench_o0qeq0rq/mmr_decode.vhdl
analyze /tmp/myirl_top_testbench_o0qeq0rq/testbench.vhdl
elaborate testbench

==== COSIM stderr ====

==== COSIM stdout ====
START
DONE
/tmp/myirl_top_testbench_o0qeq0rq/testbench.vhdl:114:9:@42ns:(assertion failure): Stop Simulation
/tmp/testbench:error: assertion failed
in process .testbench(myirl).stimulus
/tmp/testbench:error: simulation failed

==== COSIM stderr ====

In [10]:
! cat -n {f[0]}
     1	-- File generated from source:
     2	--     /tmp/ipykernel_1274/810171885.py
     3	-- (c) 2016-2021 section5.ch
     4	-- Modifications may be lost, edit the source file instead.
     5	
     6	library IEEE;
     7	use IEEE.std_logic_1164.all;
     8	use IEEE.numeric_std.all;
     9	
    10	library work;
    11	
    12	use work.module_defs.all;
    13	use work.txt_util.all;
    14	use work.myirl_conversion.all;
    15	
    16	entity mmr_decode is
    17	    port (
    18	        clk : in std_ulogic;
    19	        reset : in std_ulogic;
    20	        addr : in unsigned(11 downto 0);
    21	        wr : in std_ulogic;
    22	        data_in : in unsigned(15 downto 0);
    23	        data_out : out unsigned(15 downto 0);
    24	        stat_read : in t_stat_read;
    25	        stat_write : out t_stat_write;
    26	        ctrl_read : in t_ctrl_read;
    27	        ctrl_write : out t_ctrl_write;
    28	        ctrl_select : out t_ctrl_sel;
    29	        TXD_read : in t_TXD_read;
    30	        TXD_write : out t_TXD_write;
    31	        TXD_select : out t_TXD_sel;
    32	        RXD_read : in t_RXD_read;
    33	        RXD_write : out t_RXD_write;
    34	        RXD_select : out t_RXD_sel
    35	    );
    36	end entity mmr_decode;
    37	
    38	architecture MyIRL of mmr_decode is
    39	    -- Local type declarations
    40	    -- Signal declarations
    41	    signal idata : unsigned(15 downto 0);
    42	begin
    43	    
    44	worker:
    45	    process(clk, reset)
    46	    begin
    47	        if rising_edge(clk) then
    48	            if reset = '1' then
    49	                ctrl_select.sel_w <= '0';
    50	                TXD_select.sel_w <= '0';
    51	                RXD_select.sel_r <= '0';
    52	                idata <= x"0000";
    53	                TXD_write.DATA <= x"0000";
    54	                ctrl_write.gna <= "001000";
    55	                ctrl_write.reset <= '1';
    56	                stat_write.ex <= "00";
    57	                stat_write.inv <= '0';
    58	                stat_write.mode <= "00010";
    59	            else
    60	                ctrl_select.sel_w <= '0';
    61	                TXD_select.sel_w <= '0';
    62	                RXD_select.sel_r <= '0';
    63	                if (wr = '1') then
    64	                    case addr is
    65	                    when x"001" =>
    66	                        stat_write.ex <= data_in(8-1 downto 6);
    67	                        stat_write.inv <= data_in(4);
    68	                        stat_write.mode <= data_in(15-1 downto 10);
    69	                    when x"002" =>
    70	                        ctrl_write.gna <= data_in(7-1 downto 1);
    71	                        ctrl_write.reset <= data_in(7);
    72	                        ctrl_select.sel_w <= '1';
    73	                    when x"004" =>
    74	                        TXD_write.DATA <= data_in(16-1 downto 0);
    75	                        TXD_select.sel_w <= '1';
    76	                    when others =>
    77	                    end case;
    78	                else
    79	                    case addr is
    80	                    when x"001" =>
    81	                        idata(3 downto 1) <= stat_read.im;
    82	                        idata(7 downto 6) <= stat_read.ex;
    83	                        idata(14 downto 10) <= stat_read.mode;
    84	                    when x"005" =>
    85	                        idata(15 downto 0) <= RXD_read.DATA;
    86	                        RXD_select.sel_r <= '1';
    87	                    when others =>
    88	                    end case;
    89	                end if;
    90	            end if;
    91	        end if;
    92	    end process;
    93	    
    94	drive:
    95	    process(clk)
    96	    begin
    97	        if rising_edge(clk) then
    98	            data_out <= idata;
    99	        end if;
   100	    end process;
   101	end architecture MyIRL;
   102	
In [11]:
! cat {f[1]}
-- File generated from source:
--     /tmp/ipykernel_1274/3694240014.py
-- (c) 2016-2021 section5.ch
-- Modifications may be lost, edit the source file instead.

library IEEE;
use IEEE.std_logic_1164.all;
use IEEE.numeric_std.all;

library work;

use work.module_defs.all;
use work.txt_util.all;
use work.myirl_conversion.all;

entity testbench is
end entity testbench;

architecture MyIRL of testbench is
    -- Local type declarations
    -- Signal declarations
    signal bulkc209_out : t_MMRPort_out;
    signal stat_write : t_stat_write;
    signal ctrl_write : t_ctrl_write;
    signal ctrl_select : t_ctrl_sel;
    signal TXD_write : t_TXD_write;
    signal TXD_select : t_TXD_sel;
    signal RXD_write : t_RXD_write;
    signal RXD_select : t_RXD_sel;
    signal clk : std_ulogic := '0';
    signal stat_read : t_stat_read;
    signal ctrl_read : t_ctrl_read;
    signal TXD_read : t_TXD_read;
    signal RXD_read : t_RXD_read;
    signal debug : std_ulogic;
    signal bulkc209_in : t_MMRPort_in;
    signal bulkc209_aux : t_MMRPort_aux;
    signal mon_gna : unsigned(5 downto 0);
    signal mon_select : std_ulogic;
begin
    
    -- Instance mmr_decode
    inst_mmr_decode_0: entity work.mmr_decode
    port map (
        clk => clk,
        reset => bulkc209_aux.rst,
        addr => bulkc209_in.addr,
        wr => bulkc209_in.wr,
        data_in => bulkc209_in.din,
        data_out => bulkc209_out.dout,
        stat_read => stat_read,
        stat_write => stat_write,
        ctrl_read => ctrl_read,
        ctrl_write => ctrl_write,
        ctrl_select => ctrl_select,
        TXD_read => TXD_read,
        TXD_write => TXD_write,
        TXD_select => TXD_select,
        RXD_read => RXD_read,
        RXD_write => RXD_write,
        RXD_select => RXD_select
    );
    
clkgen:
    clk <= not clk after 2 ns;
    
    
stimulus:
    process
    begin
        print("START");
        debug <= '0';
        bulkc209_in.wr <= '0';
        bulkc209_in.addr <= x"001";
        bulkc209_aux.rst <= '1';
        wait until rising_edge(bulkc209_aux.clk);
        wait until rising_edge(bulkc209_aux.clk);
        bulkc209_aux.rst <= '0';
        stat_read.ex <= "00";
        stat_read.mode <= "00100";
        stat_read.im <= "010";
        bulkc209_in.addr <= x"001";
        bulkc209_in.wr <= '0';
        wait until rising_edge(bulkc209_aux.clk);
        wait until rising_edge(bulkc209_aux.clk);
        assert (bulkc209_out.dout = x"1004")
            report "Read mismatch" severity failure;
        bulkc209_in.addr <= x"002";
        bulkc209_in.din <= x"00fa";
        wait until rising_edge(bulkc209_aux.clk);
        bulkc209_in.wr <= '1';
        wait until rising_edge(bulkc209_aux.clk);
        bulkc209_in.wr <= '0';
        debug <= '1';
        wait until rising_edge(clk);
        assert (ctrl_select.sel_w = '1')
            report "Failed in /tmp/ipykernel_1274/3694240014.py:testbench():46" severity failure;
        assert (ctrl_write.gna = "111101")
            report "Failed in /tmp/ipykernel_1274/3694240014.py:testbench():47" severity failure;
        wait until falling_edge(clk);
        assert (ctrl_select.sel_w = '0')
            report "Failed in /tmp/ipykernel_1274/3694240014.py:testbench():49" severity failure;
        bulkc209_in.addr <= x"001";
        bulkc209_in.din <= x"0010";
        wait until rising_edge(bulkc209_aux.clk);
        bulkc209_in.wr <= '1';
        wait until rising_edge(bulkc209_aux.clk);
        bulkc209_in.wr <= '0';
        assert (stat_write.inv = '1')
            report "Failed in /tmp/ipykernel_1274/3694240014.py:testbench():52" severity failure;
        wait until rising_edge(clk);
        wait until rising_edge(clk);
        print("DONE");
        assert false
            report "Stop Simulation" severity failure;
        wait;
    end process;
    mon_gna <= ctrl_read.gna;
    mon_select <= ctrl_select.sel_w;
    bulkc209_aux.clk <= clk;
end architecture MyIRL;

In [12]:
# ! cat {mmr_decode.ctx.path_prefix}module_defs.vhdl

Waveform display

The *.vcd format hides the MMRPort record members from the trace. Therefore we need a few monitoring auxiliary signals.

In [13]:
import wavedraw
import nbwavedrom
In [14]:
TB = "testbench"

waveform = wavedraw.vcd2wave(TB+ ".vcd", TB + '.clk', None)
    
nbwavedrom.draw(waveform)
In [ ]:
 
In [ ]: