[PATCH 1/3] Introduce new phy_link and phy_instance abstraction

This is merely a historical archive of years 2008-2021, before the migration to mailman3.

A maintained and still updated list archive can be found at https://lists.osmocom.org/hyperkitty/list/OpenBSC@lists.osmocom.org/.

Harald Welte laforge at gnumonks.org
Fri Feb 5 11:41:21 UTC 2016


This way we can model a flexible mapping between any number of PHYs,
each having multiple instances, and then map BTSs with TRXx on top of
those PHYs.
---
 doc/examples/octphy/osmo-bts.cfg    |   8 +-
 doc/phy_link.txt                    |  57 ++++++
 doc/startup.txt                     |  42 ++++
 include/osmo-bts/Makefile.am        |   2 +-
 include/osmo-bts/bts.h              |   2 +-
 include/osmo-bts/bts_model.h        |   8 +
 include/osmo-bts/gsm_data.h         |  15 --
 include/osmo-bts/phy_link.h         | 115 +++++++++++
 include/osmo-bts/scheduler.h        |   4 +-
 include/osmo-bts/vty.h              |   7 +-
 src/common/Makefile.am              |   2 +-
 src/common/bts.c                    |  30 ++-
 src/common/main.c                   |  20 ++
 src/common/phy_link.c               | 159 +++++++++++++++
 src/common/scheduler.c              |   6 +-
 src/common/vty.c                    | 194 +++++++++++++++++-
 src/osmo-bts-octphy/l1_if.c         | 170 +++++++++++-----
 src/osmo-bts-octphy/l1_if.h         |  31 ++-
 src/osmo-bts-octphy/l1_oml.c        | 163 ++++++++-------
 src/osmo-bts-octphy/main.c          |   3 +-
 src/osmo-bts-octphy/octphy_hw_api.c |  14 +-
 src/osmo-bts-octphy/octphy_vty.c    | 198 +++++++++++--------
 src/osmo-bts-trx/l1_if.c            |  84 ++++----
 src/osmo-bts-trx/l1_if.h            |  18 +-
 src/osmo-bts-trx/main.c             |  78 +++-----
 src/osmo-bts-trx/scheduler_trx.c    |  33 ++--
 src/osmo-bts-trx/trx_if.c           | 133 +++++++++----
 src/osmo-bts-trx/trx_if.h           |   1 +
 src/osmo-bts-trx/trx_vty.c          | 381 ++++++++++++++++++++++--------------
 29 files changed, 1424 insertions(+), 554 deletions(-)
 create mode 100644 doc/phy_link.txt
 create mode 100644 doc/startup.txt
 create mode 100644 include/osmo-bts/phy_link.h
 create mode 100644 src/common/phy_link.c

diff --git a/doc/examples/octphy/osmo-bts.cfg b/doc/examples/octphy/osmo-bts.cfg
index 07bcae0..4f27c8d 100644
--- a/doc/examples/octphy/osmo-bts.cfg
+++ b/doc/examples/octphy/osmo-bts.cfg
@@ -20,9 +20,13 @@ log stderr
 line vty
  no login
 !
+phy 0
+ octphy hw-addr 00:0C:90:2e:80:1e
+ octphy net-device eth0.2342
+ instance 0
 bts 0
  band 1800
  ipa unit-id 1234 0
  oml remote-ip 127.0.0.1
- phy-hw-addr 00:0C:90:2e:80:1e
- phy-netdev eth0.2342
+ trx 0
+  phy 0 instance 0
diff --git a/doc/phy_link.txt b/doc/phy_link.txt
new file mode 100644
index 0000000..c49e328
--- /dev/null
+++ b/doc/phy_link.txt
@@ -0,0 +1,57 @@
+
+== OsmoBTS PHY interface abstraction
+
+The OsmoBTS PHY interface serves as an abstraction layer between given
+PHY hardware and the actual logical transceivers (TRXs) of a BTS inside
+the OsmoBTS code base.
+
+
+=== PHY link
+
+A PHY link is a physical connection / link towards a given PHY.  This
+might be, for example,
+
+* a set of file descriptors to device nodes in the /dev/ directory
+  (sysmobts, litecell15)
+* a packet socket for sending raw Ethernet frames to an OCTPHY
+* a set of UDP sockets for interacting with OsmoTRX
+
+Each PHY interface has a set of attribute/parameters and a list of 1 to
+n PHY instances.
+
+PHY links are numbered 0..n globally inside OsmoBTS.
+
+Each PHY link is configured via the VTY using its individual top-level
+vty node.  Given the different bts-model / phy specific properties, the
+VTY configuration options (if any) of the PHY instance differ between
+BTS models.
+
+The PHY links and instances must be configured above the BTS/TRX nodes
+in the configuration file.  If the file is saved via the VTY, the code
+automatically ensures this.
+
+
+=== PHY instance
+
+A PHY instance is an instance of a PHY, accessed via a PHY link.
+
+In the case of osmo-bts-sysmo and osmo-bts-trx, there is only one
+instance in every PHY link.  This is due to the fact that the API inside
+that PHY link does not permit for distinguishing multiple different
+logical TRXs.
+
+Other PHY implementations like the OCTPHY however do support addressing
+multiple PHY instances via a single PHY link.
+
+PHY instances are numbered 0..n inside each PHY link.
+
+Each PHY instance is configured via the VTY as a separate node beneath each
+PHY link.  Given the different bts-model / phy specific properties, the
+VTY configuration options (if any) of the PHY instance differ between
+BTS models.
+
+
+=== Mapping PHY instances to TRXs
+
+Each TRX node in the VTY must use the 'phy N instance M' command in
+order to specify which PHY instance is allocated to this specific TRX.
diff --git a/doc/startup.txt b/doc/startup.txt
new file mode 100644
index 0000000..cc64375
--- /dev/null
+++ b/doc/startup.txt
@@ -0,0 +1,42 @@
+
+== start-up / sequencing during OsmoBTS start
+
+The start-up procedure of OsmoBTS can be described as follows:
+
+|===
+| bts-specific | main() |
+| common | bts_main() | initialization of talloc contexts
+| common | bts_log_init() | initialization of logging
+| common | handle_options() | common option parsing
+| bts-specific | bts_model_handle_options() | model-specific option parsing
+| common | gsm_bts_alloc() | allocation of BTS/TRX/TS data structures
+| common | vty_init() | Initialziation of VTY core, libosmo-abis and osmo-bts VTY
+| common | main() | Setting of scheduler RR priority (if configured)
+| common | main() | Initialization of GSMTAP (if configured)
+| common | bts_init() | configuration of defaults in bts/trx/s object
+| bts-specific | bts_model_init | ?
+| common | abis_init() | Initialization of libosmo-abis
+| common | vty_read_config_file() | Reading of configuration file
+| bts-specific | bts_model_phy_link_set_defaults() | Called for every PHY link created
+| bts-specific | bts_model_phy_instance_set_defaults() | Called for every PHY Instance created
+| common | bts_controlif_setup() | Initialization of Control Interface
+| bts-specific | bts_model_ctrl_cmds_install()
+| common | telnet_init() | Initialization of telnet interface
+| common | pcu_sock_init() | Initializaiton of PCU socket
+| common | main() | Installation of signal handlers
+| common | abis_open() | Start of the A-bis connection to BSC
+| common | phy_links_open() | Iterate over list of configured PHY links
+| bts-specific | bts_model_phy_link_open() | Open each of the configured PHY links
+| common | write_pid_file() | Generate the pid file
+| common | osmo_daemonize() | Fork as daemon in background (if configured)
+| common | bts_main() | Run main loop until global variable quit >= 2
+| bts-specific | bts_model_oml_estab() | Called by core once OML link is established
+| bts-specific | bts_model_check_oml() | called each time OML sets some attributes on a MO, checks if attributes are valid
+| bts-specific | bts_model_apply_oml() | called each time OML sets some attributes on a MO, stores attribute contents in data structures
+| bts-specific | bts_model_opstart() | for NM_OC_BTS, NM_OC_SITE_MANAGER, NM_OC_GPRS_NSE, NM_OC_GPRS_CELL, NMO_OC_GPRS_NSVC
+| bts-specific | bts_model_opstart() | for NM_OC_RADIO_CARRIER for each trx
+| bts-specific | bts_model_opstart() | for NM_OC_BASEB_TRANSC for each trx
+| bts-specific | bts_model_opstart() | for NM_OC_CHANNEL for each timeslot on each trx
+| bts-specific | bts_model_change_power() | change transmit power for each trx (power ramp-up/ramp-down
+
+| bts-specific | bts_model_abis_close() | called when either one of the RSL links or the OML link are down
diff --git a/include/osmo-bts/Makefile.am b/include/osmo-bts/Makefile.am
index af88a1a..d0b55ff 100644
--- a/include/osmo-bts/Makefile.am
+++ b/include/osmo-bts/Makefile.am
@@ -1,4 +1,4 @@
 noinst_HEADERS = abis.h bts.h bts_model.h gsm_data.h logging.h measurement.h \
 		 oml.h paging.h rsl.h signal.h vty.h amr.h pcu_if.h pcuif_proto.h \
 		 handover.h msg_utils.h tx_power.h control_if.h cbch.h l1sap.h \
-		 power_control.h scheduler.h scheduler_backend.h
+		 power_control.h scheduler.h scheduler_backend.h phy_link.h
diff --git a/include/osmo-bts/bts.h b/include/osmo-bts/bts.h
index 9e21186..ec58edd 100644
--- a/include/osmo-bts/bts.h
+++ b/include/osmo-bts/bts.h
@@ -21,12 +21,12 @@ void destroy_bts(struct gsm_bts *bts);
 int work_bts(struct gsm_bts *bts);
 int bts_link_estab(struct gsm_bts *bts);
 int trx_link_estab(struct gsm_bts_trx *trx);
+int trx_set_available(struct gsm_bts_trx *trx, int avail);
 void bts_new_si(void *arg);
 void bts_setup_slot(struct gsm_bts_trx_ts *slot, uint8_t comb);
 
 int bts_agch_enqueue(struct gsm_bts *bts, struct msgb *msg);
 struct msgb *bts_agch_dequeue(struct gsm_bts *bts);
-void bts_update_agch_max_queue_length(struct gsm_bts *bts);
 int bts_agch_max_queue_length(int T, int bcch_conf);
 int bts_ccch_copy_msg(struct gsm_bts *bts, uint8_t *out_buf, struct gsm_time *gt,
 		      int is_ag_res);
diff --git a/include/osmo-bts/bts_model.h b/include/osmo-bts/bts_model.h
index 41b5e93..6252557 100644
--- a/include/osmo-bts/bts_model.h
+++ b/include/osmo-bts/bts_model.h
@@ -8,6 +8,9 @@
 
 #include <osmo-bts/gsm_data.h>
 
+struct phy_link;
+struct phy_instance;
+
 /* BTS model specific functions needed by the common code */
 
 int bts_model_init(struct gsm_bts *bts);
@@ -32,6 +35,8 @@ int bts_model_vty_init(struct gsm_bts *bts);
 
 void bts_model_config_write_bts(struct vty *vty, struct gsm_bts *bts);
 void bts_model_config_write_trx(struct vty *vty, struct gsm_bts_trx *trx);
+void bts_model_config_write_phy(struct vty *vty, struct phy_link *plink);
+void bts_model_config_write_phy_inst(struct vty *vty, struct phy_instance *pinst);
 
 int bts_model_oml_estab(struct gsm_bts *bts);
 
@@ -47,4 +52,7 @@ int bts_model_ctrl_cmds_install(struct gsm_bts *bts);
 int bts_model_handle_options(int argc, char **argv);
 void bts_model_print_help();
 
+void bts_model_phy_link_set_defaults(struct phy_link *plink);
+void bts_model_phy_instance_set_defaults(struct phy_instance *pinst);
+
 #endif
diff --git a/include/osmo-bts/gsm_data.h b/include/osmo-bts/gsm_data.h
index 6a7bca8..f9e6ed1 100644
--- a/include/osmo-bts/gsm_data.h
+++ b/include/osmo-bts/gsm_data.h
@@ -119,21 +119,6 @@ enum lchan_ciph_state {
 
 #include "openbsc/gsm_data_shared.h"
 
-struct femtol1_hdl;
-
-static inline struct femtol1_hdl *trx_femtol1_hdl(struct gsm_bts_trx *trx)
-{
-	return trx->role_bts.l1h;
-}
-
-struct trx_l1h;
-
-static inline struct trx_l1h *trx_l1h_hdl(struct gsm_bts_trx *trx)
-{
-	return trx->role_bts.l1h;
-}
-
-
 void lchan_set_state(struct gsm_lchan *lchan, enum gsm_lchan_state state);
 
 /* cipher code */
diff --git a/include/osmo-bts/phy_link.h b/include/osmo-bts/phy_link.h
new file mode 100644
index 0000000..b9fc412
--- /dev/null
+++ b/include/osmo-bts/phy_link.h
@@ -0,0 +1,115 @@
+#pragma once
+
+#include <stdint.h>
+#include <osmocom/core/linuxlist.h>
+
+#include <osmo-bts/scheduler.h>
+
+#include <linux/if_packet.h>
+
+struct gsm_bts_trx;
+
+enum phy_link_type {
+	PHY_LINK_T_NONE,
+	PHY_LINK_T_SYSMOBTS,
+	PHY_LINK_T_OSMOTRX,
+};
+
+enum phy_link_state {
+	PHY_LINK_SHUTDOWN,
+	PHY_LINK_CONNECTING,
+	PHY_LINK_CONNECTED,
+};
+
+/* A PHY link represents the connection to a given PHYsical layer
+ * implementation.  That PHY link contains 1...N PHY instances, one for
+ * each TRX */
+struct phy_link {
+	struct llist_head list;
+	int num;
+	enum phy_link_type type;
+	enum phy_link_state state;
+	struct llist_head instances;
+	char *description;
+	union {
+		struct {
+		} sysmobts;
+		struct {
+			char *transceiver_ip;
+			uint16_t base_port_local;
+			uint16_t base_port_remote;
+			struct osmo_fd trx_ofd_clk;
+
+			uint32_t clock_advance;
+			uint32_t rts_advance;
+
+			int	rxgain_valid;
+			int	rxgain;
+			int	rxgain_sent;
+
+			int	power_valid;
+			int	power;
+			int	power_oml;
+			int	power_sent;
+		} osmotrx;
+		struct {
+			/* MAC address of the PHY */
+			struct sockaddr_ll phy_addr;
+			/* Network device name */
+			char *netdev_name;
+
+			/* configuration */
+			uint32_t rf_port_index;
+			uint32_t rx_gain_db;
+			uint32_t tx_atten_db;
+
+			struct octphy_hdl *hdl;
+		} octphy;
+	} u;
+};
+
+struct phy_instance {
+	/* liked inside phy_link.linstances */
+	struct llist_head list;
+	int num;
+	char *description;
+
+	/* pointer to the PHY link to which we belong */
+	struct phy_link *phy_link;
+
+	/* back-pointer to the TRX to which we're associated */
+	struct gsm_bts_trx *trx;
+
+	union {
+		struct {
+		} sysmobts;
+		struct {
+			struct trx_l1h *hdl;
+		} osmotrx;
+		struct {
+			/* logical transceiver number within one PHY */
+			uint32_t trx_id;
+		} octphy;
+	} u;
+};
+
+struct phy_link *phy_link_by_num(int num);
+struct phy_link *phy_link_create(void *ctx, int num);
+void phy_link_destroy(struct phy_link *plink);
+void phy_link_state_set(struct phy_link *plink, enum phy_link_state state);
+int phy_links_open(void);
+
+struct phy_instance *phy_instance_by_num(struct phy_link *plink, int num);
+struct phy_instance *phy_instance_create(struct phy_link *plink, int num);
+void phy_instance_link_to_trx(struct phy_instance *pinst, struct gsm_bts_trx *trx);
+void phy_instance_destroy(struct phy_instance *pinst);
+const char *phy_instance_name(struct phy_instance *pinst);
+
+void phy_user_statechg_notif(struct phy_instance *pinst, enum phy_link_state link_state);
+
+static inline struct phy_instance *trx_phy_instance(struct gsm_bts_trx *trx)
+{
+	return trx->role_bts.l1h;
+}
+
+int bts_model_phy_link_open(struct phy_link *plink);
diff --git a/include/osmo-bts/scheduler.h b/include/osmo-bts/scheduler.h
index 13ef051..b11e6f1 100644
--- a/include/osmo-bts/scheduler.h
+++ b/include/osmo-bts/scheduler.h
@@ -1,6 +1,8 @@
 #ifndef TRX_SCHEDULER_H
 #define TRX_SCHEDULER_H
 
+#include <osmo-bts/gsm_data.h>
+
 /* These types define the different channels on a multiframe.
  * Each channel has queues and can be activated individually.
  */
@@ -133,7 +135,7 @@ extern uint32_t transceiver_last_fn;
 
 
 /*! \brief Initialize the scheudler data structures */
-int trx_sched_init(struct l1sched_trx *l1t);
+int trx_sched_init(struct l1sched_trx *l1t, struct gsm_bts_trx *trx);
 
 /*! \brief De-initialize the scheudler data structures */
 void trx_sched_exit(struct l1sched_trx *l1t);
diff --git a/include/osmo-bts/vty.h b/include/osmo-bts/vty.h
index 546729c..5d8d4a7 100644
--- a/include/osmo-bts/vty.h
+++ b/include/osmo-bts/vty.h
@@ -5,7 +5,12 @@
 #include <osmocom/vty/command.h>
 
 enum bts_vty_node {
-	BTS_NODE = _LAST_OSMOVTY_NODE + 1,
+	/* PHY_NODE must come before BTS node to ensure the phy
+	 * instances are created at the time the TRX nodes want to refer
+	 * to them */
+	PHY_NODE = _LAST_OSMOVTY_NODE + 1,
+	PHY_INST_NODE,
+	BTS_NODE,
 	TRX_NODE,
 };
 
diff --git a/src/common/Makefile.am b/src/common/Makefile.am
index fea205c..8df6513 100644
--- a/src/common/Makefile.am
+++ b/src/common/Makefile.am
@@ -8,6 +8,6 @@ libbts_a_SOURCES = gsm_data_shared.c sysinfo.c logging.c abis.c oml.c bts.c \
 		   load_indication.c pcu_sock.c handover.c msg_utils.c \
 		   load_indication.c pcu_sock.c handover.c msg_utils.c \
 		   tx_power.c bts_ctrl_commands.c bts_ctrl_lookup.c \
-		   l1sap.c cbch.c power_control.c main.c
+		   l1sap.c cbch.c power_control.c main.c phy_link.c
 
 libl1sched_a_SOURCES = scheduler.c
diff --git a/src/common/bts.c b/src/common/bts.c
index 43f4c25..2d0ad8e 100644
--- a/src/common/bts.c
+++ b/src/common/bts.c
@@ -44,6 +44,7 @@
 #include <osmo-bts/oml.h>
 #include <osmo-bts/signal.h>
 
+static void bts_update_agch_max_queue_length(struct gsm_bts *bts);
 
 struct gsm_network bts_gsmnet = {
 	.bts_list = { &bts_gsmnet.bts_list, &bts_gsmnet.bts_list },
@@ -72,6 +73,8 @@ static int bts_signal_cbfn(unsigned int subsys, unsigned int signal,
 	return 0;
 }
 
+/* Initialize the BTS (and TRX) data structures, called before config
+ * file reading */
 int bts_init(struct gsm_bts *bts)
 {
 	struct gsm_bts_role_bts *btsb;
@@ -255,6 +258,31 @@ int trx_link_estab(struct gsm_bts_trx *trx)
 	return 0;
 }
 
+/* set the availability of the TRX (used by PHY driver) */
+int trx_set_available(struct gsm_bts_trx *trx, int avail)
+{
+	int tn;
+
+	LOGP(DSUM, LOGL_INFO, "TRX(%d): Setting available = %d\n",
+		trx->nr, avail);
+	if (avail) {
+		oml_mo_state_chg(&trx->mo,  NM_OPSTATE_DISABLED, NM_AVSTATE_OK);
+		oml_mo_tx_sw_act_rep(&trx->mo);
+		oml_mo_state_chg(&trx->bb_transc.mo, -1, NM_AVSTATE_OK);
+		oml_mo_tx_sw_act_rep(&trx->bb_transc.mo);
+
+		for (tn = 0; tn < ARRAY_SIZE(trx->ts); tn++)
+			oml_mo_state_chg(&trx->ts[tn].mo, NM_OPSTATE_DISABLED, NM_AVSTATE_DEPENDENCY);
+	} else {
+		oml_mo_state_chg(&trx->mo,  NM_OPSTATE_DISABLED, NM_AVSTATE_OFF_LINE);
+		oml_mo_state_chg(&trx->bb_transc.mo, -1, NM_AVSTATE_OFF_LINE);
+
+		for (tn = 0; tn < ARRAY_SIZE(trx->ts); tn++)
+			oml_mo_state_chg(&trx->ts[tn].mo, NM_OPSTATE_DISABLED, NM_AVSTATE_OFF_LINE);
+	}
+	return 0;
+}
+
 int lchan_init_lapdm(struct gsm_lchan *lchan)
 {
 	struct lapdm_channel *lc = &lchan->lapdm_ch;
@@ -310,7 +338,7 @@ int bts_agch_max_queue_length(int T, int bcch_conf)
 	return (T + 2 * S) * ccch_rach_ratio256 / 256;
 }
 
-void bts_update_agch_max_queue_length(struct gsm_bts *bts)
+static void bts_update_agch_max_queue_length(struct gsm_bts *bts)
 {
 	struct gsm_bts_role_bts *btsb = bts_role_bts(bts);
 	struct gsm48_system_information_type_3 *si3;
diff --git a/src/common/main.c b/src/common/main.c
index fabe482..e454a28 100644
--- a/src/common/main.c
+++ b/src/common/main.c
@@ -42,6 +42,7 @@
 #include <osmocom/core/gsmtap.h>
 
 #include <osmo-bts/gsm_data.h>
+#include <osmo-bts/phy_link.h>
 #include <osmo-bts/logging.h>
 #include <osmo-bts/abis.h>
 #include <osmo-bts/bts.h>
@@ -284,6 +285,19 @@ int bts_main(int argc, char **argv)
 		exit(1);
 	}
 
+	if (!phy_link_by_num(0)) {
+		fprintf(stderr, "You need to configure at last phy0\n");
+		exit(1);
+	}
+
+	llist_for_each_entry(trx, &bts->trx_list, list) {
+		if (!trx->role_bts.l1h) {
+			fprintf(stderr, "TRX %u has no associated PHY instance\n",
+				trx->nr);
+			exit(1);
+		}
+	}
+
 	write_pid_file("osmo-bts");
 
 	bts_controlif_setup(bts);
@@ -317,6 +331,12 @@ int bts_main(int argc, char **argv)
 		exit(2);
 	}
 
+	rc = phy_links_open();
+	if (rc < 0) {
+		fprintf(stderr, "unable ot open PHY link(s)\n");
+		exit(2);
+	}
+
 	if (daemonize) {
 		rc = osmo_daemonize();
 		if (rc < 0) {
diff --git a/src/common/phy_link.c b/src/common/phy_link.c
new file mode 100644
index 0000000..4103ede
--- /dev/null
+++ b/src/common/phy_link.c
@@ -0,0 +1,159 @@
+#include <stdint.h>
+
+#include <osmocom/core/linuxlist.h>
+#include <osmocom/core/talloc.h>
+
+#include <osmo-bts/bts.h>
+#include <osmo-bts/gsm_data.h>
+#include <osmo-bts/phy_link.h>
+#include <osmo-bts/oml.h>
+#include <osmo-bts/logging.h>
+
+static LLIST_HEAD(g_phy_links);
+
+struct phy_link *phy_link_by_num(int num)
+{
+	struct phy_link *plink;
+
+	llist_for_each_entry(plink, &g_phy_links, list) {
+		if (plink->num == num)
+			return plink;
+	}
+
+	return NULL;
+}
+
+struct phy_link *phy_link_create(void *ctx, int num)
+{
+	struct phy_link *plink = talloc_zero(ctx, struct phy_link);
+
+	if (phy_link_by_num(num))
+		return NULL;
+
+	plink = talloc_zero(ctx, struct phy_link);
+	plink->num = num;
+	plink->state = PHY_LINK_SHUTDOWN;
+	INIT_LLIST_HEAD(&plink->instances);
+	llist_add_tail(&plink->list, &g_phy_links);
+
+	bts_model_phy_link_set_defaults(plink);
+
+	return plink;
+}
+
+const struct value_string phy_link_state_vals[] = {
+	{ PHY_LINK_SHUTDOWN, 	"shutdown" },
+	{ PHY_LINK_CONNECTING,	"connectiong" },
+	{ PHY_LINK_CONNECTED,	"connected" },
+	{ 0, NULL }
+};
+
+void phy_link_state_set(struct phy_link *plink, enum phy_link_state state)
+{
+	struct phy_instance *pinst;
+
+	LOGP(DL1C, LOGL_INFO, "PHY link state change %s -> %s\n",
+	     get_value_string(phy_link_state_vals, plink->state),
+	     get_value_string(phy_link_state_vals, state));
+
+	/* notify all TRX associated with this phy */
+	llist_for_each_entry(pinst, &plink->instances, list) {
+		struct gsm_bts_trx *trx = pinst->trx;
+		if (!trx)
+			continue;
+
+		switch (state) {
+		case PHY_LINK_CONNECTED:
+			LOGP(DL1C, LOGL_INFO, "trx_set_avail(1)\n");
+			trx_set_available(trx, 1);
+			break;
+		default:
+			LOGP(DL1C, LOGL_INFO, "trx_set_avail(0)\n");
+			trx_set_available(trx, 0);
+			break;
+		}
+	}
+
+	plink->state = state;
+}
+
+struct phy_instance *phy_instance_by_num(struct phy_link *plink, int num)
+{
+	struct phy_instance *pinst;
+
+	llist_for_each_entry(pinst, &plink->instances, list) {
+		if (pinst->num == num)
+			return pinst;
+	}
+	return NULL;
+}
+
+struct phy_instance *phy_instance_create(struct phy_link *plink, int num)
+{
+	struct phy_instance *pinst = talloc_zero(plink, struct phy_instance);
+
+	if (phy_instance_by_num(plink, num))
+		return NULL;
+
+	pinst = talloc_zero(plink, struct phy_instance);
+	pinst->num = num;
+	pinst->phy_link = plink;
+	llist_add_tail(&pinst->list, &plink->instances);
+
+	bts_model_phy_instance_set_defaults(pinst);
+
+	return pinst;
+}
+
+void phy_instance_link_to_trx(struct phy_instance *pinst, struct gsm_bts_trx *trx)
+{
+	trx->role_bts.l1h = pinst;
+	pinst->trx = trx;
+}
+
+void phy_instance_destroy(struct phy_instance *pinst)
+{
+	/* remove from list of instances in the link */
+	llist_del(&pinst->list);
+
+	/* remove reverse link from TRX */
+	OSMO_ASSERT(pinst->trx->role_bts.l1h == pinst);
+	pinst->trx->role_bts.l1h = NULL;
+	pinst->trx = NULL;
+
+	talloc_free(pinst);
+}
+
+void phy_link_destroy(struct phy_link *plink)
+{
+	struct phy_instance *pinst, *pinst2;
+
+	llist_for_each_entry_safe(pinst, pinst2, &plink->instances, list)
+		phy_instance_destroy(pinst);
+
+	talloc_free(plink);
+}
+
+int phy_links_open(void)
+{
+	struct phy_link *plink;
+
+	llist_for_each_entry(plink, &g_phy_links, list) {
+		int rc;
+
+		rc = bts_model_phy_link_open(plink);
+		if (rc < 0)
+			return rc;
+	}
+
+	return 0;
+}
+
+const char *phy_instance_name(struct phy_instance *pinst)
+{
+	static char buf[32];
+
+	snprintf(buf, sizeof(buf), "phy%u.%u", pinst->phy_link->num,
+		 pinst->num);
+	return buf;
+}
diff --git a/src/common/scheduler.c b/src/common/scheduler.c
index 64d89ac..de09fbf 100644
--- a/src/common/scheduler.c
+++ b/src/common/scheduler.c
@@ -134,11 +134,13 @@ const struct trx_chan_desc trx_chan_desc[_TRX_CHAN_MAX] = {
  * init / exit
  */
 
-int trx_sched_init(struct l1sched_trx *l1t)
+int trx_sched_init(struct l1sched_trx *l1t, struct gsm_bts_trx *trx)
 {
 	uint8_t tn;
 	int i;
 
+	l1t->trx = trx;
+
 	LOGP(DL1C, LOGL_NOTICE, "Init scheduler for trx=%u\n", l1t->trx->nr);
 
 	for (tn = 0; tn < ARRAY_SIZE(l1t->ts); tn++) {
@@ -191,7 +193,7 @@ void trx_sched_exit(struct l1sched_trx *l1t)
 void trx_sched_reset(struct l1sched_trx *l1t)
 {
 	trx_sched_exit(l1t);
-	trx_sched_init(l1t);
+	trx_sched_init(l1t, l1t->trx);
 }
 
 struct msgb *_sched_dequeue_prim(struct l1sched_trx *l1t, int8_t tn, uint32_t fn,
diff --git a/src/common/vty.c b/src/common/vty.c
index 4fd65d0..94ccdbe 100644
--- a/src/common/vty.c
+++ b/src/common/vty.c
@@ -40,6 +40,7 @@
 
 #include <osmo-bts/logging.h>
 #include <osmo-bts/gsm_data.h>
+#include <osmo-bts/phy_link.h>
 #include <osmo-bts/abis.h>
 #include <osmo-bts/bts.h>
 #include <osmo-bts/rsl.h>
@@ -53,6 +54,13 @@
 int bts_vty_go_parent(struct vty *vty)
 {
 	switch (vty->node) {
+	case PHY_INST_NODE:
+		vty->node = PHY_NODE;
+		{
+			struct phy_instance *pinst = vty->index;
+			vty->index = pinst->phy_link;
+		}
+		break;
 	case TRX_NODE:
 		vty->node = BTS_NODE;
 		{
@@ -60,6 +68,7 @@ int bts_vty_go_parent(struct vty *vty)
 			vty->index = trx->bts;
 		}
 		break;
+	case PHY_NODE:
 	default:
 		vty->node = CONFIG_NODE;
 	}
@@ -71,6 +80,8 @@ int bts_vty_is_config_node(struct vty *vty, int node)
 	switch (node) {
 	case TRX_NODE:
 	case BTS_NODE:
+	case PHY_NODE:
+	case PHY_INST_NODE:
 		return 1;
 	default:
 		return 0;
@@ -81,6 +92,17 @@ gDEFUN(ournode_exit, ournode_exit_cmd, "exit",
 	"Exit current node, go down to provious node")
 {
 	switch (vty->node) {
+	case PHY_INST_NODE:
+		vty->node = PHY_NODE;
+		{
+			struct phy_instance *pinst = vty->index;
+			vty->index = pinst->phy_link;
+		}
+		break;
+	case PHY_NODE:
+		vty->node = CONFIG_NODE;
+		vty->index = NULL;
+		break;
 	case TRX_NODE:
 		vty->node = BTS_NODE;
 		{
@@ -141,6 +163,7 @@ static struct cmd_node trx_node = {
 	1,
 };
 
+
 DEFUN(cfg_bts_trx, cfg_bts_trx_cmd,
 	"trx <0-254>",
 	"Select a TRX to configure\n" "TRX number\n")
@@ -151,7 +174,7 @@ DEFUN(cfg_bts_trx, cfg_bts_trx_cmd,
 
 	trx = gsm_bts_trx_num(bts, trx_nr);
 	if (!trx) {
-		vty_out(vty, "Unknown TRX %u. Aavialable TRX are: 0..%d%s",
+		vty_out(vty, "Unknown TRX %u. Aavialable TRX are: 0..%u%s",
 			trx_nr, bts->num_trx - 1, VTY_NEWLINE);
 		return CMD_WARNING;
 	}
@@ -232,6 +255,7 @@ static void config_write_bts_single(struct vty *vty, struct gsm_bts *bts)
 
 	llist_for_each_entry(trx, &bts->trx_list, list) {
 		struct trx_power_params *tpp = &trx->power_params;
+		struct phy_instance *pinst = trx_phy_instance(trx);
 		vty_out(vty, " trx %u%s", trx->nr, VTY_NEWLINE);
 
 		if (trx->power_params.user_gain_mdB)
@@ -246,6 +270,8 @@ static void config_write_bts_single(struct vty *vty, struct gsm_bts *bts)
 		vty_out(vty, "  ms-power-control %s%s",
 			trx->ms_power_control == 0 ? "dsp" : "osmo",
 			VTY_NEWLINE);
+		vty_out(vty, "  phy %u instance %u%s", pinst->phy_link->num,
+			pinst->num, VTY_NEWLINE);
 
 		bts_model_config_write_trx(vty, trx);
 	}
@@ -262,6 +288,35 @@ static int config_write_bts(struct vty *vty)
 	return CMD_SUCCESS;
 }
 
+static void config_write_phy_single(struct vty *vty, struct phy_link *plink)
+{
+	int i;
+
+	vty_out(vty, "phy %u%s", plink->num, VTY_NEWLINE);
+	bts_model_config_write_phy(vty, plink);
+
+	for (i = 0; i < 255; i++) {
+		struct phy_instance *pinst = phy_instance_by_num(plink, i);
+		if (!pinst)
+			break;
+		vty_out(vty, " instance %u%s", pinst->num, VTY_NEWLINE);
+	}
+}
+
+static int config_write_phy(struct vty *vty)
+{
+	int i;
+
+	for (i = 0; i < 255; i++) {
+		struct phy_link *plink = phy_link_by_num(i);
+		if (!plink)
+			break;
+		config_write_phy_single(vty, plink);
+	}
+
+	return CMD_SUCCESS;
+}
+
 static int config_write_dummy(struct vty *vty)
 {
 	return CMD_SUCCESS;
@@ -532,6 +587,33 @@ DEFUN(cfg_trx_ms_power_control, cfg_trx_ms_power_control_cmd,
 	return CMD_SUCCESS;
 }
 
+DEFUN(cfg_trx_phy, cfg_trx_phy_cmd,
+	"phy <0-255> instance <0-255>",
+	"Configure PHY Link+Instance for this TRX\n"
+	"PHY Link number\n" "PHY instance\n" "PHY Instance number")
+{
+	struct gsm_bts_trx *trx = vty->index;
+	struct phy_link *plink = phy_link_by_num(atoi(argv[0]));
+	struct phy_instance *pinst;
+
+	if (!plink) {
+		vty_out(vty, "phy%s does not exist%s",
+			argv[0], VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	pinst = phy_instance_by_num(plink, atoi(argv[1]));
+	if (!pinst) {
+		vty_out(vty, "phy%s instance %s does not exit%s",
+			argv[0], argv[1], VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	trx->role_bts.l1h = pinst;
+	pinst->trx = trx;
+
+	return CMD_SUCCESS;
+}
 
 /* ======================================================================
  * SHOW
@@ -702,6 +784,106 @@ DEFUN(cfg_trx_no_gsmtap_sapi, cfg_trx_no_gsmtap_sapi_cmd,
 	return CMD_SUCCESS;
 }
 
+static struct cmd_node phy_node = {
+	PHY_NODE,
+	"%s(phy)#",
+	1,
+};
+
+static struct cmd_node phy_inst_node = {
+	PHY_INST_NODE,
+	"%s(phy-inst)#",
+	1,
+};
+
+DEFUN(cfg_phy, cfg_phy_cmd,
+	"phy <0-255>",
+	"Select a PHY to configure\n" "PHY number\n")
+{
+	int phy_nr = atoi(argv[0]);
+	struct phy_link *plink;
+
+	plink = phy_link_by_num(phy_nr);
+	if (!plink)
+		plink = phy_link_create(tall_bts_ctx, phy_nr);
+	if (!plink)
+		return CMD_WARNING;
+
+	vty->index = plink;
+	vty->index_sub = &plink->description;
+	vty->node = PHY_NODE;
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_phy_inst, cfg_phy_inst_cmd,
+	"instance <0-255>",
+	"Select a PHY instance to configure\n" "PHY Instance number\n")
+{
+	int inst_nr = atoi(argv[0]);
+	struct phy_link *plink = vty->index;
+	struct phy_instance *pinst;
+
+	pinst = phy_instance_by_num(plink, inst_nr);
+	if (!pinst) {
+		pinst = phy_instance_create(plink, inst_nr);
+		if (!pinst) {
+			vty_out(vty, "Unable to create phy%u instance %u%s",
+				plink->num, inst_nr, VTY_NEWLINE);
+			return CMD_WARNING;
+		}
+	}
+
+	vty->index = pinst;
+	vty->index_sub = &pinst->description;
+	vty->node = PHY_INST_NODE;
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_phy_no_inst, cfg_phy_no_inst_cmd,
+	"no instance <0-255>"
+	NO_STR "Select a PHY instance to remove\n", "PHY Instance number\n")
+{
+	int inst_nr = atoi(argv[0]);
+	struct phy_link *plink = vty->index;
+	struct phy_instance *pinst;
+
+	pinst = phy_instance_by_num(plink, inst_nr);
+	if (!pinst) {
+		vty_out(vty, "No such instance %u%s", inst_nr, VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	phy_instance_destroy(pinst);
+
+	return CMD_SUCCESS;
+}
+
+#if 0
+DEFUN(cfg_phy_type, cfg_phy_type_cmd,
+	"type (sysmobts|osmo-trx|virtual)",
+	"configure the type of the PHY\n"
+	"sysmocom sysmoBTS PHY\n"
+	"OsmoTRX based PHY\n"
+	"Virtual PHY (GSMTAP based)\n")
+{
+	struct phy_link *plink = vty->index;
+
+	if (plink->state != PHY_LINK_SHUTDOWN) {
+		vty_out(vty, "Cannot change type of active PHY%s", VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	if (!strcmp(argv[0], "sysmobts"))
+		plink->type = PHY_LINK_T_SYSMOBTS;
+	else if (!strcmp(argv[0], "osmo-trx"))
+		plink->type = PHY_LINK_T_OSMOTRX;
+	else if (!strcmp(argv[0], "virtual"))
+		plink->type = PHY_LINK_T_VIRTUAL;
+}
+#endif
+
 DEFUN(bts_t_t_l_jitter_buf,
 	bts_t_t_l_jitter_buf_cmd,
 	"bts <0-0> trx <0-0> ts <0-7> lchan <0-1> rtp jitter-buffer <0-10000>",
@@ -813,10 +995,20 @@ int bts_vty_init(struct gsm_bts *bts, const struct log_info *cat)
 	install_element(TRX_NODE, &cfg_trx_pr_step_size_cmd);
 	install_element(TRX_NODE, &cfg_trx_pr_step_interval_cmd);
 	install_element(TRX_NODE, &cfg_trx_ms_power_control_cmd);
+	install_element(TRX_NODE, &cfg_trx_phy_cmd);
 
 	install_element(ENABLE_NODE, &bts_t_t_l_jitter_buf_cmd);
 	install_element(ENABLE_NODE, &bts_t_t_l_loopback_cmd);
 	install_element(ENABLE_NODE, &no_bts_t_t_l_loopback_cmd);
 
+	install_element(CONFIG_NODE, &cfg_phy_cmd);
+	install_node(&phy_node, config_write_phy);
+	install_default(PHY_NODE);
+	install_element(PHY_NODE, &cfg_phy_inst_cmd);
+	install_element(PHY_NODE, &cfg_phy_no_inst_cmd);
+
+	install_node(&phy_inst_node, config_write_dummy);
+	install_default(PHY_INST_NODE);
+
 	return 0;
 }
diff --git a/src/osmo-bts-octphy/l1_if.c b/src/osmo-bts-octphy/l1_if.c
index db6e032..3215aa1 100644
--- a/src/osmo-bts-octphy/l1_if.c
+++ b/src/osmo-bts-octphy/l1_if.c
@@ -38,6 +38,7 @@
 #include <osmocom/core/socket.h>
 
 #include <osmo-bts/gsm_data.h>
+#include <osmo-bts/bts_model.h>
 #include <osmo-bts/oml.h>
 #include <osmo-bts/logging.h>
 #include <osmo-bts/l1sap.h>
@@ -109,6 +110,17 @@ osmocom_to_octphy_band(enum gsm_band osmo_band, unsigned int arfcn)
 	}
 };
 
+struct gsm_bts_trx *trx_by_l1h(struct octphy_hdl *fl1h, unsigned int trx_id)
+{
+	struct phy_instance *pinst;
+
+	pinst = phy_instance_by_num(fl1h->phy_link, trx_id);
+	if (!pinst)
+		return NULL;
+
+	return pinst->trx;
+}
+
 struct gsm_lchan *get_lchan_by_lchid(struct gsm_bts_trx *trx,
 				tOCTVC1_GSM_LOGICAL_CHANNEL_ID *lch_id)
 {
@@ -282,10 +294,9 @@ int l1if_req_compl(struct octphy_hdl *fl1h, struct msgb *msg,
 }
 
 /* For OctPHY, this only about sending state changes to BSC */
-int l1if_activate_rf(struct octphy_hdl *fl1h, int on)
+int l1if_activate_rf(struct gsm_bts_trx *trx, int on)
 {
 	int i;
-	struct gsm_bts_trx *trx = fl1h->priv;
 	if (on) {
 		/* signal availability */
 		oml_mo_state_chg(&trx->mo, NM_OPSTATE_DISABLED, NM_AVSTATE_OK);
@@ -422,7 +433,8 @@ static void empty_req_from_rts_ind(tOCTVC1_GSM_MSG_TRX_REQUEST_LOGICAL_CHANNEL_E
 static int ph_data_req(struct gsm_bts_trx *trx, struct msgb *msg,
 			struct osmo_phsap_prim *l1sap)
 {
-	struct octphy_hdl *fl1h = trx_octphy_hdl(trx);
+	struct phy_instance *pinst = trx_phy_instance(trx);
+	struct octphy_hdl *fl1h = pinst->phy_link->u.octphy.hdl;
 	struct msgb *l1msg = l1p_msgb_alloc();
 	uint32_t u32Fn;
 	uint8_t u8Tn, subCh, sapi = 0;
@@ -489,7 +501,7 @@ static int ph_data_req(struct gsm_bts_trx *trx, struct msgb *msg,
 		l1if_fill_msg_hdr(&data_req->Header, l1msg, fl1h, cOCTVC1_MSG_TYPE_COMMAND,
 			  	  cOCTVC1_GSM_MSG_TRX_REQUEST_LOGICAL_CHANNEL_DATA_CID);
 
-		data_req->TrxId.byTrxId = trx->nr;
+		data_req->TrxId.byTrxId = pinst->u.octphy.trx_id;
 		data_req->LchId.byTimeslotNb = u8Tn;
 		data_req->LchId.bySAPI = sapi;
 		data_req->LchId.bySubChannelNb = subCh;
@@ -508,7 +520,7 @@ static int ph_data_req(struct gsm_bts_trx *trx, struct msgb *msg,
 		l1if_fill_msg_hdr(&empty_frame_req->Header, l1msg, fl1h, cOCTVC1_MSG_TYPE_COMMAND,
 			  	  cOCTVC1_GSM_MSG_TRX_REQUEST_LOGICAL_CHANNEL_EMPTY_FRAME_CID);
 
-		empty_frame_req->TrxId.byTrxId = trx->nr;
+		empty_frame_req->TrxId.byTrxId = pinst->u.octphy.trx_id;
 		empty_frame_req->LchId.byTimeslotNb = u8Tn;
 		empty_frame_req->LchId.bySAPI = sapi;
 		empty_frame_req->LchId.bySubChannelNb = subCh;
@@ -527,7 +539,8 @@ done:
 static int ph_tch_req(struct gsm_bts_trx *trx, struct msgb *msg,
 		      struct osmo_phsap_prim *l1sap)
 {
-	struct octphy_hdl *fl1h = trx_octphy_hdl(trx);
+	struct phy_instance *pinst = trx_phy_instance(trx);
+	struct octphy_hdl *fl1h = pinst->phy_link->u.octphy.hdl;
 	struct gsm_lchan *lchan;
 	uint32_t u32Fn;
 	uint8_t u8Tn, subCh, sapi, ss;
@@ -565,7 +578,7 @@ static int ph_tch_req(struct gsm_bts_trx *trx, struct msgb *msg,
 		l1if_fill_msg_hdr(&data_req->Header, nmsg, fl1h, cOCTVC1_MSG_TYPE_COMMAND,
 			  	  cOCTVC1_GSM_MSG_TRX_REQUEST_LOGICAL_CHANNEL_DATA_CID);
 
-		data_req->TrxId.byTrxId = trx->nr;
+		data_req->TrxId.byTrxId = pinst->u.octphy.trx_id;
 		data_req->LchId.byTimeslotNb = u8Tn;
 		data_req->LchId.bySAPI = sapi;
 		data_req->LchId.bySubChannelNb = subCh;
@@ -590,7 +603,7 @@ static int ph_tch_req(struct gsm_bts_trx *trx, struct msgb *msg,
 		l1if_fill_msg_hdr(&empty_frame_req->Header, nmsg, fl1h, cOCTVC1_MSG_TYPE_COMMAND,
 			  	  cOCTVC1_GSM_MSG_TRX_REQUEST_LOGICAL_CHANNEL_EMPTY_FRAME_CID);
 
-		empty_frame_req->TrxId.byTrxId = trx->nr;
+		empty_frame_req->TrxId.byTrxId = pinst->u.octphy.trx_id;
 		empty_frame_req->LchId.byTimeslotNb = u8Tn;
 		empty_frame_req->LchId.bySAPI = sapi;
 		empty_frame_req->LchId.bySubChannelNb = subCh;
@@ -686,32 +699,78 @@ int bts_model_l1sap_down(struct gsm_bts_trx *trx, struct osmo_phsap_prim *l1sap)
 	return rc;
 }
 
+static int trx_close_all_cb(struct octphy_hdl *fl1, struct msgb *resp, void *data)
+{
+	tOCTVC1_GSM_MSG_TRX_CLOSE_ALL_RSP *car =
+		(tOCTVC1_GSM_MSG_TRX_CLOSE_ALL_RSP *) resp->l2h;
+
+	/* in a completion call-back, we take msgb ownership and must
+	 * release it before returning */
+
+	mOCTVC1_GSM_MSG_TRX_CLOSE_ALL_RSP_SWAP(car);
+
+	/* we now know that the PHY link is connected */
+	phy_link_state_set(fl1->phy_link, PHY_LINK_CONNECTED);
+
+	msgb_free(resp);
+
+	return 0;
+}
+
+static int phy_link_trx_close_all(struct phy_link *plink)
+{
+	struct octphy_hdl *fl1h = plink->u.octphy.hdl;
+	struct msgb *msg = l1p_msgb_alloc();
+	tOCTVC1_GSM_MSG_TRX_CLOSE_ALL_CMD *cac;
+
+	cac = (tOCTVC1_GSM_MSG_TRX_CLOSE_ALL_CMD *)
+				msgb_put(msg, sizeof(*cac));
+	l1if_fill_msg_hdr(&cac->Header, msg, fl1h, cOCTVC1_MSG_TYPE_COMMAND,
+			  cOCTVC1_GSM_MSG_TRX_CLOSE_ALL_CID);
+
+	mOCTVC1_GSM_MSG_TRX_CLOSE_ALL_CMD_SWAP(cac);
+
+	return l1if_req_compl(fl1h, msg, trx_close_all_cb, NULL);
+}
+
+int bts_model_phy_link_open(struct phy_link *plink)
+{
+	if (plink->u.octphy.hdl)
+		l1if_close(plink->u.octphy.hdl);
+
+	phy_link_state_set(plink, PHY_LINK_CONNECTING);
+
+	plink->u.octphy.hdl = l1if_open(plink);
+	if (!plink->u.octphy.hdl) {
+		phy_link_state_set(plink, PHY_LINK_SHUTDOWN);
+		return -1;
+	}
+
+	/* do we need to iterate over the list of instances and do some
+	 * instance-specific initialization? */
+
+	/* close all TRXs that might still exist in this link from
+	 * previous execitions / sessions */
+	phy_link_trx_close_all(plink);
+
+	/* in the call-back to the above we will set the link state to
+	 * connected */
+
+	return 0;
+}
+
 int bts_model_init(struct gsm_bts *bts)
 {
 	struct gsm_bts_role_bts *btsb;
-	struct octphy_hdl *fl1h;
 
 	LOGP(DL1C, LOGL_NOTICE, "model_init()\n");
 
 	btsb = bts_role_bts(bts);
 	btsb->support.ciphers = CIPHER_A5(1) | CIPHER_A5(2) | CIPHER_A5(3);
 
-	fl1h = talloc_zero(bts, struct octphy_hdl);
-	if (!fl1h)
-		return -ENOMEM;
-
-	INIT_LLIST_HEAD(&fl1h->wlc_list);
-	INIT_LLIST_HEAD(&fl1h->wlc_postponed);
-	fl1h->priv = bts->c0;
-	bts->c0->role_bts.l1h = fl1h;
 	/* FIXME: what is the nominal transmit power of the PHY/board? */
 	bts->c0->nominal_power = 15;
 
-	/* configure some reasonable defaults, to be overridden by VTY */
-	fl1h->config.rf_port_index = 0;
-	fl1h->config.rx_gain_db = 70;
-	fl1h->config.tx_atten_db = 0;
-
 	bts_model_vty_init(bts);
 
 	return 0;
@@ -750,23 +809,15 @@ static void dump_meas_res(int ll, tOCTVC1_GSM_MEASUREMENT_INFO * m)
 
 static int handle_mph_time_ind(struct octphy_hdl *fl1, uint8_t trx_id, uint32_t fn)
 {
-	struct gsm_bts_trx *trx = fl1->priv;
-	struct gsm_bts *bts = trx->bts;
+	struct gsm_bts_trx *trx = trx_by_l1h(fl1, trx_id);
 	struct osmo_phsap_prim l1sap;
 
 	/* increment the primitive count for the alive timer */
 	fl1->alive_prim_cnt++;
 
 	/* ignore every time indication, except for c0 */
-	if (trx != bts->c0)
-		return 0;
-
-	if (trx_id != trx->nr) {
-		LOGP(DL1C, LOGL_FATAL,
-		     "TRX id %d from response does not match the L1 context trx %d\n",
-		     trx_id, trx->nr);
+	if (trx != trx->bts->c0)
 		return 0;
-	}
 
 	memset(&l1sap, 0, sizeof(l1sap));
 	osmo_prim_init(&l1sap.oph, SAP_GSM_PH, PRIM_MPH_INFO,
@@ -783,7 +834,7 @@ static int handle_ph_readytosend_ind(struct octphy_hdl *fl1,
 	tOCTVC1_GSM_MSG_TRX_LOGICAL_CHANNEL_READY_TO_SEND_INDICATION_EVT *evt,
 	struct msgb *l1p_msg)
 {
-	struct gsm_bts_trx *trx = fl1->priv;
+	struct gsm_bts_trx *trx = trx_by_l1h(fl1, evt->TrxId.byTrxId);
 	struct gsm_bts *bts = trx->bts;
 	struct osmo_phsap_prim *l1sap;
 	struct gsm_time g_time;
@@ -908,7 +959,7 @@ static int handle_ph_data_ind(struct octphy_hdl *fl1,
 		tOCTVC1_GSM_MSG_TRX_LOGICAL_CHANNEL_DATA_INDICATION_EVT *data_ind,
 		struct msgb *l1p_msg)
 {
-	struct gsm_bts_trx *trx = fl1->priv;
+	struct gsm_bts_trx *trx = trx_by_l1h(fl1, data_ind->TrxId.byTrxId);
 	uint8_t chan_nr, link_id;
 	struct osmo_phsap_prim *l1sap;
 	uint32_t fn;
@@ -992,7 +1043,7 @@ static int handle_ph_rach_ind(struct octphy_hdl *fl1,
 		tOCTVC1_GSM_MSG_TRX_LOGICAL_CHANNEL_RACH_INDICATION_EVT *ra_ind,
 		struct msgb *l1p_msg)
 {
-	struct gsm_bts_trx *trx = fl1->priv;
+	struct gsm_bts_trx *trx = trx_by_l1h(fl1, ra_ind->TrxId.byTrxId);
 	struct gsm_bts *bts = trx->bts;
 	struct gsm_bts_role_bts *btsb = bts_role_bts(bts);
 	struct gsm_lchan *lchan;
@@ -1131,7 +1182,7 @@ static int rx_octvc1_resp(struct msgb *msg, uint32_t msg_id, uint32_t trans_id)
 			if (wlc->cb) {
 				/* call-back function must take msgb
 				 * ownership. */
-				rc = wlc->cb(fl1h->priv, msg, wlc->cb_data);
+				rc = wlc->cb(fl1h, msg, wlc->cb_data);
 			} else {
 				rc = 0;
 				msgb_free(msg);
@@ -1485,6 +1536,18 @@ static int rx_octphy_msg(struct msgb *msg)
 	return rc;
 }
 
+void bts_model_phy_link_set_defaults(struct phy_link *plink)
+{
+	/* configure some reasonable defaults, to be overridden by VTY */
+	plink->u.octphy.rf_port_index = 0;
+	plink->u.octphy.rx_gain_db = 70;
+	plink->u.octphy.tx_atten_db = 0;
+}
+
+void bts_model_phy_instance_set_defaults(struct phy_instance *pinst)
+{
+}
+
 /***********************************************************************
  * octphy socket / main loop integration
  ***********************************************************************/
@@ -1534,15 +1597,25 @@ static int octphy_write_cb(struct osmo_fd *fd, struct msgb *msg)
 	return rc;
 }
 
-int l1if_open(struct octphy_hdl *fl1h)
+struct octphy_hdl *l1if_open(struct phy_link *plink)
 {
+	struct octphy_hdl *fl1h;
 	struct ifreq ifr;
 	int sfd, rc;
-	char *phy_dev = fl1h->netdev_name;
+	char *phy_dev = plink->u.octphy.netdev_name;
+
+	fl1h = talloc_zero(plink, struct octphy_hdl);
+	if (!fl1h)
+		return NULL;
+
+	INIT_LLIST_HEAD(&fl1h->wlc_list);
+	INIT_LLIST_HEAD(&fl1h->wlc_postponed);
+	fl1h->phy_link = plink;
 
 	if (!phy_dev) {
 		LOGP(DL1C, LOGL_ERROR, "You have to specify a phy-netdev\n");
-		return -EINVAL;
+		talloc_free(fl1h);
+		return NULL;
 	}
 
 	LOGP(DL1C, LOGL_NOTICE, "Opening L1 interface for OctPHY (%s)\n",
@@ -1553,7 +1626,8 @@ int l1if_open(struct octphy_hdl *fl1h)
 	if (sfd < 0) {
 		LOGP(DL1C, LOGL_FATAL, "Error opening PHY socket: %s\n",
 			strerror(errno));
-		return -EIO;
+		talloc_free(fl1h);
+		return NULL;
 	}
 
 	/* resolve the string device name to an ifindex */
@@ -1564,18 +1638,21 @@ int l1if_open(struct octphy_hdl *fl1h)
 		LOGP(DL1C, LOGL_FATAL, "Error using network device %s: %s\n",
 			phy_dev, strerror(errno));
 		close(sfd);
-		return -EIO;
+		talloc_free(fl1h);
+		return NULL;
 	}
 
 	fl1h->session_id = rand();
 
-	/* set fl1h->phy_addr, which we use as sendto() destionation */
+	/* set fl1h->phy_addr, which we use as sendto() destination */
 	fl1h->phy_addr.sll_family = AF_PACKET;
 	fl1h->phy_addr.sll_protocol = htons(cOCTPKT_HDR_ETHERTYPE);
 	fl1h->phy_addr.sll_ifindex = ifr.ifr_ifindex;
 	fl1h->phy_addr.sll_hatype = ARPHRD_ETHER;
-	fl1h->phy_addr.sll_halen = 6;
-	/* sll_addr is filled by bts_model_vty code */
+	fl1h->phy_addr.sll_halen = ETH_ALEN;
+	/* plink->phy_addr.sll_addr is filled by bts_model_vty code */
+	memcpy(fl1h->phy_addr.sll_addr, plink->u.octphy.phy_addr.sll_addr,
+		ETH_ALEN);
 
 	/* Write queue / osmo_fd registration */
 	osmo_wqueue_init(&fl1h->phy_wq, 10);
@@ -1588,10 +1665,11 @@ int l1if_open(struct octphy_hdl *fl1h)
 	rc = osmo_fd_register(&fl1h->phy_wq.bfd);
 	if (rc < 0) {
 		close(sfd);
-		return -EIO;
+		talloc_free(fl1h);
+		return NULL;
 	}
 
-	return 0;
+	return fl1h;
 }
 
 int l1if_close(struct octphy_hdl *fl1h)
diff --git a/src/osmo-bts-octphy/l1_if.h b/src/osmo-bts-octphy/l1_if.h
index 4277865..2dee178 100644
--- a/src/osmo-bts-octphy/l1_if.h
+++ b/src/osmo-bts-octphy/l1_if.h
@@ -13,16 +13,16 @@
 #include <osmocom/gsm/protocol/gsm_04_08.h>
 
 #include <osmo-bts/gsm_data.h>
+#include <osmo-bts/phy_link.h>
 
 #include <octphy/octvc1/gsm/octvc1_gsm_api.h>
 
 struct octphy_hdl {
+	/* MAC address of the PHY */
+	struct sockaddr_ll phy_addr;
+
 	/* packet socket to talk with PHY */
 	struct osmo_wqueue phy_wq;
-	/* MAC address of th PHY */
-	struct sockaddr_ll phy_addr;
-	/* Network device name */
-	char *netdev_name;
 
 	/* address parameters of the PHY */
 	uint32_t session_id;
@@ -33,12 +33,6 @@ struct octphy_hdl {
 	uint32_t clkmgr_state;
 
 	struct {
-		uint32_t rf_port_index;
-		uint32_t rx_gain_db;
-		uint32_t tx_atten_db;
-	} config;
-
-	struct {
 		struct {
 			char *name;
 			char *description;
@@ -72,8 +66,8 @@ struct octphy_hdl {
 	struct llist_head wlc_postponed;
 	int wlc_postponed_len;
 
-	/* private pointer, points back to TRX */
-	void *priv;
+	/* back pointer to the PHY link */
+	struct phy_link *phy_link;
 
 	struct osmo_timer_list alive_timer;
 	uint32_t alive_prim_cnt;
@@ -82,15 +76,10 @@ struct octphy_hdl {
 	int opened;
 };
 
-static inline struct octphy_hdl *trx_octphy_hdl(struct gsm_bts_trx *trx)
-{
-	return trx->role_bts.l1h;
-}
-
 void l1if_fill_msg_hdr(tOCTVC1_MSG_HEADER *mh, struct msgb *msg,
 			struct octphy_hdl *fl1h, uint32_t msg_type, uint32_t api_cmd);
 
-typedef int l1if_compl_cb(struct gsm_bts_trx *trx, struct msgb *l1_msg, void *data);
+typedef int l1if_compl_cb(struct octphy_hdl *fl1, struct msgb *l1_msg, void *data);
 
 /* send a request primitive to the L1 and schedule completion call-back */
 int l1if_req_compl(struct octphy_hdl *fl1h, struct msgb *msg,
@@ -100,19 +89,21 @@ int l1if_req_compl(struct octphy_hdl *fl1h, struct msgb *msg,
 struct gsm_lchan *get_lchan_by_lchid(struct gsm_bts_trx *trx,
 				tOCTVC1_GSM_LOGICAL_CHANNEL_ID *lch_id);
 
-int l1if_open(struct octphy_hdl *fl1h);
+struct octphy_hdl *l1if_open(struct phy_link *plink);
 int l1if_close(struct octphy_hdl *hdl);
 
 int l1if_trx_open(struct gsm_bts_trx *trx);
 int l1if_trx_close_all(struct gsm_bts *bts);
 int l1if_enable_events(struct gsm_bts_trx *trx);
 
-int l1if_activate_rf(struct octphy_hdl *fl1h, int on);
+int l1if_activate_rf(struct gsm_bts_trx *trx, int on);
 
 int l1if_tch_rx(struct gsm_bts_trx *trx, uint8_t chan_nr,
 		tOCTVC1_GSM_MSG_TRX_LOGICAL_CHANNEL_DATA_INDICATION_EVT *
 		data_ind);
 
+struct gsm_bts_trx *trx_by_l1h(struct octphy_hdl *fl1h, unsigned int trx_id);
+
 struct msgb *l1p_msgb_alloc(void);
 
 /* tch.c */
diff --git a/src/osmo-bts-octphy/l1_oml.c b/src/osmo-bts-octphy/l1_oml.c
index 318c384..c50f1d6 100644
--- a/src/osmo-bts-octphy/l1_oml.c
+++ b/src/osmo-bts-octphy/l1_oml.c
@@ -348,10 +348,11 @@ static void sapi_clear_queue(struct llist_head *queue)
 	}
 }
 
-static int lchan_act_compl_cb(struct gsm_bts_trx *trx, struct msgb *resp, void *data)
+static int lchan_act_compl_cb(struct octphy_hdl *fl1, struct msgb *resp, void *data)
 {
 	tOCTVC1_GSM_MSG_TRX_ACTIVATE_LOGICAL_CHANNEL_RSP *ar =
 		(tOCTVC1_GSM_MSG_TRX_ACTIVATE_LOGICAL_CHANNEL_RSP *) resp->l2h;
+	struct gsm_bts_trx *trx;
 	struct gsm_lchan *lchan;
 	uint8_t sapi;
 	uint8_t direction;
@@ -361,7 +362,7 @@ static int lchan_act_compl_cb(struct gsm_bts_trx *trx, struct msgb *resp, void *
 	 * release it before returning */
 
 	mOCTVC1_GSM_MSG_TRX_ACTIVATE_LOGICAL_CHANNEL_RSP_SWAP(ar);
-	OSMO_ASSERT(ar->TrxId.byTrxId == trx->nr);
+	trx = trx_by_l1h(fl1, ar->TrxId.byTrxId);
 
 	lchan = get_lchan_by_lchid(trx, &ar->LchId);
 	sapi = ar->LchId.bySAPI;
@@ -411,7 +412,8 @@ err:
 
 static int mph_send_activate_req(struct gsm_lchan *lchan, struct sapi_cmd *cmd)
 {
-	struct octphy_hdl *fl1h = trx_octphy_hdl(lchan->ts->trx);
+	struct phy_instance *pinst = trx_phy_instance(lchan->ts->trx);
+	struct octphy_hdl *fl1h = pinst->phy_link->u.octphy.hdl;
 	struct msgb *msg = l1p_msgb_alloc();
 	tOCTVC1_GSM_MSG_TRX_ACTIVATE_LOGICAL_CHANNEL_CMD *lac;
 
@@ -420,7 +422,7 @@ static int mph_send_activate_req(struct gsm_lchan *lchan, struct sapi_cmd *cmd)
 	l1if_fill_msg_hdr(&lac->Header, msg, fl1h, cOCTVC1_MSG_TYPE_COMMAND,
 			  cOCTVC1_GSM_MSG_TRX_ACTIVATE_LOGICAL_CHANNEL_CID);
 
-	lac->TrxId.byTrxId = lchan->ts->trx->nr;
+	lac->TrxId.byTrxId = pinst->u.octphy.trx_id;
 	lac->LchId.byTimeslotNb = lchan->ts->nr;
 	lac->LchId.bySubChannelNb = lchan_to_GsmL1_SubCh_t(lchan);
 	lac->LchId.bySAPI = cmd->sapi;
@@ -451,10 +453,11 @@ static tOCTVC1_GSM_CIPHERING_ID_ENUM rsl2l1_ciph[] = {
 	[4] = cOCTVC1_GSM_CIPHERING_ID_ENUM_A5_3
 };
 
-static int set_ciph_compl_cb(struct gsm_bts_trx *trx, struct msgb *resp, void *data)
+static int set_ciph_compl_cb(struct octphy_hdl *fl1, struct msgb *resp, void *data)
 {
 	tOCTVC1_GSM_MSG_TRX_MODIFY_PHYSICAL_CHANNEL_CIPHERING_RSP *pcr =
 		(tOCTVC1_GSM_MSG_TRX_MODIFY_PHYSICAL_CHANNEL_CIPHERING_RSP *) resp->l2h;
+	struct gsm_bts_trx *trx;
 	struct gsm_bts_trx_ts *ts;
 	struct gsm_lchan *lchan;
 
@@ -470,6 +473,7 @@ static int set_ciph_compl_cb(struct gsm_bts_trx *trx, struct msgb *resp, void *d
 		exit(-1);
 	}
 
+	trx = trx_by_l1h(fl1, pcr->TrxId.byTrxId);
 	OSMO_ASSERT(pcr->TrxId.byTrxId == trx->nr);
 	ts = &trx->ts[pcr->PchId.byTimeslotNb];
 	/* for some strange reason the response does not tell which
@@ -508,8 +512,8 @@ err:
 
 static int mph_send_config_ciphering(struct gsm_lchan *lchan, struct sapi_cmd *cmd)
 {
-	struct gsm_bts_trx *trx = lchan->ts->trx;
-	struct octphy_hdl *fl1h = trx_octphy_hdl(trx);
+	struct phy_instance *pinst = trx_phy_instance(lchan->ts->trx);
+	struct octphy_hdl *fl1h = pinst->phy_link->u.octphy.hdl;
 	struct msgb *msg = l1p_msgb_alloc();
 	tOCTVC1_GSM_MSG_TRX_MODIFY_PHYSICAL_CHANNEL_CIPHERING_CMD *pcc;
 
@@ -518,6 +522,7 @@ static int mph_send_config_ciphering(struct gsm_lchan *lchan, struct sapi_cmd *c
 	l1if_fill_msg_hdr(&pcc->Header, msg, fl1h, cOCTVC1_MSG_TYPE_COMMAND,
 			  cOCTVC1_GSM_MSG_TRX_MODIFY_PHYSICAL_CHANNEL_CIPHERING_CID);
 
+	pcc->TrxId.byTrxId = pinst->u.octphy.trx_id;
 	pcc->PchId.byTimeslotNb = lchan->ts->nr;
 	pcc->ulSubchannelNb = lchan_to_GsmL1_SubCh_t(lchan);
 	pcc->ulDirection = cmd->dir;
@@ -627,7 +632,8 @@ static int check_sapi_release(struct gsm_lchan *lchan, int sapi, int dir)
 
 static int lchan_deactivate_sapis(struct gsm_lchan *lchan)
 {
-	struct octphy_hdl *fl1h = trx_octphy_hdl(lchan->ts->trx);
+	struct phy_instance *pinst = trx_phy_instance(lchan->ts->trx);
+	struct octphy_hdl *fl1h = pinst->phy_link->u.octphy.hdl;
 	const struct lchan_sapis *s4l = &sapis_for_lchan[lchan->type];
 	int i, res;
 
@@ -654,10 +660,11 @@ static int lchan_deactivate_sapis(struct gsm_lchan *lchan)
 	return res;
 }
 
-static int lchan_deact_compl_cb(struct gsm_bts_trx *trx, struct msgb *resp, void *data)
+static int lchan_deact_compl_cb(struct octphy_hdl *fl1, struct msgb *resp, void *data)
 {
 	tOCTVC1_GSM_MSG_TRX_DEACTIVATE_LOGICAL_CHANNEL_RSP *ldr =
 		(tOCTVC1_GSM_MSG_TRX_DEACTIVATE_LOGICAL_CHANNEL_RSP *) resp->l2h;
+	struct gsm_bts_trx *trx;
 	struct gsm_lchan *lchan;
 	struct sapi_cmd *cmd;
 	uint8_t status;
@@ -666,7 +673,7 @@ static int lchan_deact_compl_cb(struct gsm_bts_trx *trx, struct msgb *resp, void
 	 * release it before returning */
 
 	mOCTVC1_GSM_MSG_TRX_DEACTIVATE_LOGICAL_CHANNEL_RSP_SWAP(ldr);
-	OSMO_ASSERT(ldr->TrxId.byTrxId == trx->nr);
+	trx = trx_by_l1h(fl1, ldr->TrxId.byTrxId);
 
 	lchan = get_lchan_by_lchid(trx, &ldr->LchId);
 
@@ -725,7 +732,8 @@ err:
 
 static int mph_send_deactivate_req(struct gsm_lchan *lchan, struct sapi_cmd *cmd)
 {
-	struct octphy_hdl *fl1h = trx_octphy_hdl(lchan->ts->trx);
+	struct phy_instance *pinst = trx_phy_instance(lchan->ts->trx);
+	struct octphy_hdl *fl1h = pinst->phy_link->u.octphy.hdl;
 	struct msgb *msg = l1p_msgb_alloc();
 	tOCTVC1_GSM_MSG_TRX_DEACTIVATE_LOGICAL_CHANNEL_CMD *ldc;
 
@@ -734,6 +742,7 @@ static int mph_send_deactivate_req(struct gsm_lchan *lchan, struct sapi_cmd *cmd
 	l1if_fill_msg_hdr(&ldc->Header, msg, fl1h,cOCTVC1_MSG_TYPE_COMMAND,
 			  cOCTVC1_GSM_MSG_TRX_DEACTIVATE_LOGICAL_CHANNEL_CID);
 
+	ldc->TrxId.byTrxId = pinst->u.octphy.trx_id;
 	ldc->LchId.byTimeslotNb = lchan->ts->nr;
 	ldc->LchId.bySubChannelNb = lchan_to_GsmL1_SubCh_t(lchan);
 	ldc->LchId.byDirection = cmd->dir;
@@ -1031,7 +1040,8 @@ static void enqueue_sapi_act_cmd(struct gsm_lchan *lchan, int sapi, int dir)
 
 int lchan_activate(struct gsm_lchan *lchan)
 {
-	struct octphy_hdl *fl1h = trx_octphy_hdl(lchan->ts->trx);
+	struct phy_instance *pinst = trx_phy_instance(lchan->ts->trx);
+	struct octphy_hdl *fl1h = pinst->phy_link->u.octphy.hdl;
 	const struct lchan_sapis *s4l = &sapis_for_lchan[lchan->type];
 	unsigned int i;
 
@@ -1069,7 +1079,7 @@ int l1if_rsl_chan_act(struct gsm_lchan *lchan)
 	return 0;
 }
 
-static int enable_events_compl_cb(struct gsm_bts_trx *trx, struct msgb *resp, void *data)
+static int enable_events_compl_cb(struct octphy_hdl *fl1, struct msgb *resp, void *data)
 {
 	tOCTVC1_MAIN_MSG_API_SYSTEM_MODIFY_SESSION_EVT_RSP *mser =
 		(tOCTVC1_MAIN_MSG_API_SYSTEM_MODIFY_SESSION_EVT_RSP *) resp->l2h;
@@ -1088,7 +1098,8 @@ static int enable_events_compl_cb(struct gsm_bts_trx *trx, struct msgb *resp, vo
 
 int l1if_enable_events(struct gsm_bts_trx *trx)
 {
-	struct octphy_hdl *fl1h = trx_octphy_hdl(trx);
+	struct phy_instance *pinst = trx_phy_instance(trx);
+	struct octphy_hdl *fl1h = pinst->phy_link->u.octphy.hdl;
 	struct msgb *msg = l1p_msgb_alloc();
 	tOCTVC1_MAIN_MSG_API_SYSTEM_MODIFY_SESSION_EVT_CMD *mse;
 
@@ -1112,9 +1123,8 @@ int l1if_enable_events(struct gsm_bts_trx *trx)
 		dst = talloc_strdup(ctx, (const char *) src);	\
 	} while (0)
 
-static int app_info_sys_compl_cb(struct gsm_bts_trx *trx, struct msgb *resp, void *data)
+static int app_info_sys_compl_cb(struct octphy_hdl *fl1h, struct msgb *resp, void *data)
 {
-	struct octphy_hdl *fl1h = resp->dst;
 	tOCTVC1_MAIN_MSG_APPLICATION_INFO_SYSTEM_RSP *aisr =
 		(tOCTVC1_MAIN_MSG_APPLICATION_INFO_SYSTEM_RSP *) resp->l2h;
 
@@ -1136,7 +1146,8 @@ static int app_info_sys_compl_cb(struct gsm_bts_trx *trx, struct msgb *resp, voi
 
 int l1if_check_app_sys_version(struct gsm_bts_trx *trx)
 {
-	struct octphy_hdl *fl1h = trx_octphy_hdl(trx);
+	struct phy_instance *pinst = trx_phy_instance(trx);
+	struct octphy_hdl *fl1h = pinst->phy_link->u.octphy.hdl;
 	struct msgb *msg = l1p_msgb_alloc();
 	tOCTVC1_MAIN_MSG_APPLICATION_INFO_SYSTEM_CMD *ais;
 
@@ -1152,9 +1163,8 @@ int l1if_check_app_sys_version(struct gsm_bts_trx *trx)
 	return l1if_req_compl(fl1h, msg, app_info_sys_compl_cb, 0);
 }
 
-static int app_info_compl_cb(struct gsm_bts_trx *trx, struct msgb *resp, void *data)
+static int app_info_compl_cb(struct octphy_hdl *fl1h, struct msgb *resp, void *data)
 {
-	struct octphy_hdl *fl1h = resp->dst;
 	tOCTVC1_MAIN_MSG_APPLICATION_INFO_RSP *air =
 		(tOCTVC1_MAIN_MSG_APPLICATION_INFO_RSP *) resp->l2h;
 
@@ -1178,7 +1188,8 @@ static int app_info_compl_cb(struct gsm_bts_trx *trx, struct msgb *resp, void *d
 
 int l1if_check_app_version(struct gsm_bts_trx *trx)
 {
-	struct octphy_hdl *fl1h = trx_octphy_hdl(trx);
+	struct phy_instance *pinst = trx_phy_instance(trx);
+	struct octphy_hdl *fl1h = pinst->phy_link->u.octphy.hdl;
 	struct msgb *msg = l1p_msgb_alloc();
 	tOCTVC1_MAIN_MSG_APPLICATION_INFO_CMD *ai;
 
@@ -1193,9 +1204,50 @@ int l1if_check_app_version(struct gsm_bts_trx *trx)
 	return l1if_req_compl(fl1h, msg, app_info_compl_cb, 0);
 }
 
+static int trx_close_cb(struct octphy_hdl *fl1, struct msgb *resp, void *data)
+{
+	tOCTVC1_GSM_MSG_TRX_CLOSE_RSP *car =
+		(tOCTVC1_GSM_MSG_TRX_CLOSE_RSP *) resp->l2h;
+
+	/* in a completion call-back, we take msgb ownership and must
+	 * release it before returning */
+
+	mOCTVC1_GSM_MSG_TRX_CLOSE_RSP_SWAP(car);
+
+	LOGP(DL1C, LOGL_INFO, "Rx TRX-CLOSE.conf(%u)\n", car->TrxId.byTrxId);
+
+	msgb_free(resp);
+
+	return 0;
+}
+
+static int trx_close(struct gsm_bts_trx *trx)
+{
+	struct phy_instance *pinst = trx_phy_instance(trx);
+	struct phy_link *plink = pinst->phy_link;
+	struct octphy_hdl *fl1h = plink->u.octphy.hdl;
+	struct msgb *msg = l1p_msgb_alloc();
+	tOCTVC1_GSM_MSG_TRX_CLOSE_CMD *cac;
+
+	cac = (tOCTVC1_GSM_MSG_TRX_CLOSE_CMD *)
+				msgb_put(msg, sizeof(*cac));
+	l1if_fill_msg_hdr(&cac->Header, msg, fl1h, cOCTVC1_MSG_TYPE_COMMAND,
+			  cOCTVC1_GSM_MSG_TRX_CLOSE_CID);
+
+	cac->TrxId.byTrxId = pinst->u.octphy.trx_id;
+
+	LOGP(DL1C, LOGL_INFO, "Tx TRX-CLOSE.req(%u)\n", cac->TrxId.byTrxId);
+
+	mOCTVC1_GSM_MSG_TRX_CLOSE_CMD_SWAP(cac);
+
+	return l1if_req_compl(fl1h, msg, trx_close_cb, NULL);
+}
+
 /* call-back once the TRX_OPEN_CID response arrives */
-static int trx_open_compl_cb(struct gsm_bts_trx *trx, struct msgb *resp, void *data)
+static int trx_open_compl_cb(struct octphy_hdl *fl1h, struct msgb *resp, void *data)
 {
+	struct gsm_bts_trx *trx;
+
 	tOCTVC1_GSM_MSG_TRX_OPEN_RSP *or =
 		(tOCTVC1_GSM_MSG_TRX_OPEN_RSP *) resp->l2h;
 
@@ -1203,8 +1255,7 @@ static int trx_open_compl_cb(struct gsm_bts_trx *trx, struct msgb *resp, void *d
 	 * release it before returning */
 
 	mOCTVC1_GSM_MSG_TRX_OPEN_RSP_SWAP(or);
-
-	OSMO_ASSERT(or->TrxId.byTrxId == trx->nr);
+	trx = trx_by_l1h(fl1h, or->TrxId.byTrxId);
 
 	LOGP(DL1C, LOGL_INFO, "TRX-OPEN.resp(trx=%u) = %s\n",
 		trx->nr, octvc1_rc2string(or->Header.ulReturnCode));
@@ -1221,7 +1272,6 @@ static int trx_open_compl_cb(struct gsm_bts_trx *trx, struct msgb *resp, void *d
 
 	opstart_compl(&trx->mo);
 
-	struct octphy_hdl *fl1h = trx_octphy_hdl(trx);
 	octphy_hw_get_pcb_info(fl1h);
 	octphy_hw_get_rf_port_info(fl1h, 0);
 	octphy_hw_get_rf_ant_rx_config(fl1h, 0, 0);
@@ -1238,22 +1288,24 @@ static int trx_open_compl_cb(struct gsm_bts_trx *trx, struct msgb *resp, void *d
 int l1if_trx_open(struct gsm_bts_trx *trx)
 {
 	/* putting it all together */
-	struct octphy_hdl *fl1h = trx_octphy_hdl(trx);
+	struct phy_instance *pinst = trx_phy_instance(trx);
+	struct phy_link *plink = pinst->phy_link;
+	struct octphy_hdl *fl1h = pinst->phy_link->u.octphy.hdl;
 	struct msgb *msg = l1p_msgb_alloc();
 	tOCTVC1_GSM_MSG_TRX_OPEN_CMD *oc;
 
 	oc = (tOCTVC1_GSM_MSG_TRX_OPEN_CMD *) msgb_put(msg, sizeof(*oc));
 	l1if_fill_msg_hdr(&oc->Header, msg, fl1h, cOCTVC1_MSG_TYPE_COMMAND,
 			  cOCTVC1_GSM_MSG_TRX_OPEN_CID);
-	oc->ulRfPortIndex = fl1h->config.rf_port_index;
-	oc->TrxId.byTrxId = trx->nr;
+	oc->ulRfPortIndex = plink->u.octphy.rf_port_index;
+	oc->TrxId.byTrxId = pinst->u.octphy.trx_id;
 	oc->Config.ulBand = osmocom_to_octphy_band(trx->bts->band, trx->arfcn);
 	oc->Config.usArfcn = trx->arfcn;
 	oc->Config.usTsc = trx->bts->bsic & 0x7;
 	oc->Config.usBcchArfcn = trx->bts->c0->arfcn;
-	oc->RfConfig.ulRxGainDb = fl1h->config.rx_gain_db;
+	oc->RfConfig.ulRxGainDb = plink->u.octphy.rx_gain_db;
 	/* FIXME: compute this based on nominal transmit power, etc. */
-	oc->RfConfig.ulTxAttndB = fl1h->config.tx_atten_db;
+	oc->RfConfig.ulTxAttndB = plink->u.octphy.tx_atten_db;
 
 	LOGP(DL1C, LOGL_INFO, "Tx TRX-OPEN.req(trx=%u, rf_port=%u, arfcn=%u, "
 		"tsc=%u, rx_gain=%u, tx_atten=%u)\n",
@@ -1266,38 +1318,6 @@ int l1if_trx_open(struct gsm_bts_trx *trx)
 	return l1if_req_compl(fl1h, msg, trx_open_compl_cb, NULL);
 }
 
-static int trx_close_all_cb(struct gsm_bts_trx *trx, struct msgb *resp, void *data)
-{
-	tOCTVC1_GSM_MSG_TRX_CLOSE_ALL_RSP *car =
-		(tOCTVC1_GSM_MSG_TRX_CLOSE_ALL_RSP *) resp->l2h;
-
-	/* in a completion call-back, we take msgb ownership and must
-	 * release it before returning */
-
-	mOCTVC1_GSM_MSG_TRX_CLOSE_ALL_RSP_SWAP(car);
-
-	msgb_free(resp);
-
-	return 0;
-}
-
-int l1if_trx_close_all(struct gsm_bts *bts)
-{
-	struct octphy_hdl *fl1h = trx_octphy_hdl(bts->c0);
-	struct msgb *msg = l1p_msgb_alloc();
-	tOCTVC1_GSM_MSG_TRX_CLOSE_ALL_CMD *cac;
-
-	cac = (tOCTVC1_GSM_MSG_TRX_CLOSE_ALL_CMD *)
-				msgb_put(msg, sizeof(*cac));
-	l1if_fill_msg_hdr(&cac->Header, msg, fl1h, cOCTVC1_MSG_TYPE_COMMAND,
-			  cOCTVC1_GSM_MSG_TRX_CLOSE_ALL_CID);
-
-	mOCTVC1_GSM_MSG_TRX_CLOSE_ALL_CMD_SWAP(cac);
-
-	return l1if_req_compl(fl1h, msg, trx_close_all_cb, NULL);
-}
-
-
 uint32_t trx_get_hlayer1(struct gsm_bts_trx * trx)
 {
 	return 0;
@@ -1313,8 +1333,6 @@ static int trx_init(struct gsm_bts_trx *trx)
 		/* return oml_mo_opstart_nack(&trx->mo, NM_NACK_CANT_PERFORM); */
 	}
 
-	l1if_trx_close_all(trx->bts);
-
 	l1if_check_app_version(trx);
 	l1if_check_app_sys_version(trx);
 
@@ -1325,11 +1343,12 @@ static int trx_init(struct gsm_bts_trx *trx)
  * PHYSICAL CHANNE ACTIVATION
  ***********************************************************************/
 
-static int pchan_act_compl_cb(struct gsm_bts_trx *trx, struct msgb *resp, void *data)
+static int pchan_act_compl_cb(struct octphy_hdl *fl1, struct msgb *resp, void *data)
 {
 	tOCTVC1_GSM_MSG_TRX_ACTIVATE_PHYSICAL_CHANNEL_RSP *ar =
 		(tOCTVC1_GSM_MSG_TRX_ACTIVATE_PHYSICAL_CHANNEL_RSP *) resp->l2h;
 	uint8_t ts_nr;
+	struct gsm_bts_trx *trx;
 	struct gsm_bts_trx_ts *ts;
 	struct gsm_abis_mo *mo;
 
@@ -1337,9 +1356,8 @@ static int pchan_act_compl_cb(struct gsm_bts_trx *trx, struct msgb *resp, void *
 	 * release it before returning */
 
 	mOCTVC1_GSM_MSG_TRX_ACTIVATE_PHYSICAL_CHANNEL_RSP_SWAP(ar);
+	trx = trx_by_l1h(fl1, ar->TrxId.byTrxId);
 	ts_nr = ar->PchId.byTimeslotNb;
-
-	OSMO_ASSERT(ar->TrxId.byTrxId == trx->nr);
 	OSMO_ASSERT(ts_nr <= ARRAY_SIZE(trx->ts));
 
 	ts = &trx->ts[ts_nr];
@@ -1367,7 +1385,8 @@ static int pchan_act_compl_cb(struct gsm_bts_trx *trx, struct msgb *resp, void *
 
 static int ts_connect(struct gsm_bts_trx_ts *ts)
 {
-	struct octphy_hdl *fl1h = trx_octphy_hdl(ts->trx);
+	struct phy_instance *pinst = trx_phy_instance(ts->trx);
+	struct octphy_hdl *fl1h = pinst->phy_link->u.octphy.hdl;
 	struct msgb *msg = l1p_msgb_alloc();
 	tOCTVC1_GSM_MSG_TRX_ACTIVATE_PHYSICAL_CHANNEL_CMD *oc =
 		(tOCTVC1_GSM_MSG_TRX_ACTIVATE_PHYSICAL_CHANNEL_CMD *) oc;
@@ -1376,7 +1395,7 @@ static int ts_connect(struct gsm_bts_trx_ts *ts)
 	l1if_fill_msg_hdr(&oc->Header, msg, fl1h, cOCTVC1_MSG_TYPE_COMMAND,
 			  cOCTVC1_GSM_MSG_TRX_ACTIVATE_PHYSICAL_CHANNEL_CID);
 
-	oc->TrxId.byTrxId = ts->trx->nr;
+	oc->TrxId.byTrxId = pinst->u.octphy.trx_id;
 	oc->PchId.byTimeslotNb = ts->nr;
 	oc->ulChannelType = pchan_to_logChComb[ts->pchan];
 
@@ -1430,8 +1449,7 @@ int bts_model_oml_estab(struct gsm_bts *bts)
 	int i;
 	for (i = 0; i < bts->num_trx; i++) {
 		struct gsm_bts_trx *trx = gsm_bts_trx_num(bts, i);
-		struct octphy_hdl *fl1h = trx_octphy_hdl(trx);
-		l1if_activate_rf(fl1h, 1);
+		l1if_activate_rf(trx, 1);
 	}
 	return 0;
 }
@@ -1447,14 +1465,13 @@ int bts_model_chg_adm_state(struct gsm_bts *bts, struct gsm_abis_mo *mo,
 
 int bts_model_trx_deact_rf(struct gsm_bts_trx *trx)
 {
-	struct octphy_hdl *fl1 = trx_octphy_hdl(trx);
-	return l1if_activate_rf(fl1, 0);
+	return l1if_activate_rf(trx, 0);
 }
 
 int bts_model_trx_close(struct gsm_bts_trx *trx)
 {
 	/* FIXME: close only one TRX */
-	return l1if_trx_close_all(trx->bts);
+	return trx_close(trx);
 }
 
 
diff --git a/src/osmo-bts-octphy/main.c b/src/osmo-bts-octphy/main.c
index 1f516e4..0f4d0dd 100644
--- a/src/osmo-bts-octphy/main.c
+++ b/src/osmo-bts-octphy/main.c
@@ -52,10 +52,9 @@
 
 extern int pcu_direct;
 
-static struct gsm_bts *bts;
-
 int bts_model_print_help()
 {
+	return 0;
 }
 
 int bts_model_handle_options(int argc, char **argv)
diff --git a/src/osmo-bts-octphy/octphy_hw_api.c b/src/osmo-bts-octphy/octphy_hw_api.c
index 5291742..ec7c4be 100644
--- a/src/osmo-bts-octphy/octphy_hw_api.c
+++ b/src/osmo-bts-octphy/octphy_hw_api.c
@@ -35,7 +35,7 @@
 #include <octphy/octvc1/hw/octvc1_hw_api_swap.h>
 
 /* Chapter 12.1 */
-static int get_pcb_info_compl_cb(struct gsm_bts_trx *trx, struct msgb *resp, void *data)
+static int get_pcb_info_compl_cb(struct octphy_hdl *fl1, struct msgb *resp, void *data)
 {
 	tOCTVC1_HW_MSG_PCB_INFO_RSP *pir =
 		(tOCTVC1_HW_MSG_PCB_INFO_RSP *) resp->l2h;
@@ -69,7 +69,7 @@ int octphy_hw_get_pcb_info(struct octphy_hdl *fl1h)
 }
 
 /* Chapter 12.9 */
-static int rf_port_info_compl_cb(struct gsm_bts_trx *trx, struct msgb *resp,
+static int rf_port_info_compl_cb(struct octphy_hdl *fl1, struct msgb *resp,
 				 void *data)
 {
 	tOCTVC1_HW_MSG_RF_PORT_INFO_RSP *pir =
@@ -114,7 +114,7 @@ static const struct value_string radio_std_vals[] = {
 };
 
 /* Chapter 12.10 */
-static int rf_port_stats_compl_cb(struct gsm_bts_trx *trx, struct msgb *resp,
+static int rf_port_stats_compl_cb(struct octphy_hdl *fl1, struct msgb *resp,
 				  void *data)
 {
 	tOCTVC1_HW_MSG_RF_PORT_STATS_RSP *psr =
@@ -167,7 +167,7 @@ static const struct value_string rx_gain_mode_vals[] = {
 };
 
 /* Chapter 12.13 */
-static int rf_ant_rx_compl_cb(struct gsm_bts_trx *trx, struct msgb *resp,
+static int rf_ant_rx_compl_cb(struct octphy_hdl *fl1, struct msgb *resp,
 				void *data)
 {
 	tOCTVC1_HW_MSG_RF_PORT_INFO_ANTENNA_RX_CONFIG_RSP *arc =
@@ -209,7 +209,7 @@ int octphy_hw_get_rf_ant_rx_config(struct octphy_hdl *fl1h, uint32_t port_idx,
 }
 
 /* Chapter 12.14 */
-static int rf_ant_tx_compl_cb(struct gsm_bts_trx *trx, struct msgb *resp,
+static int rf_ant_tx_compl_cb(struct octphy_hdl *fl1, struct msgb *resp,
 				void *data)
 {
 	tOCTVC1_HW_MSG_RF_PORT_INFO_ANTENNA_TX_CONFIG_RSP *atc =
@@ -292,7 +292,7 @@ static const struct value_string clocksync_state_vals[] = {
 };
 
 /* Chapter 12.15 */
-static int get_clock_sync_compl_cb(struct gsm_bts_trx *trx, struct msgb *resp,
+static int get_clock_sync_compl_cb(struct octphy_hdl *fl1, struct msgb *resp,
 				   void *data)
 {
 	tOCTVC1_HW_MSG_CLOCK_SYNC_MGR_INFO_RSP *cir =
@@ -326,7 +326,7 @@ int octphy_hw_get_clock_sync_info(struct octphy_hdl *fl1h)
 }
 
 /* Chapter 12.16 */
-static int get_clock_sync_stats_cb(struct gsm_bts_trx *trx, struct msgb *resp,
+static int get_clock_sync_stats_cb(struct octphy_hdl *fl1, struct msgb *resp,
 				   void *data)
 {
 	tOCTVC1_HW_MSG_CLOCK_SYNC_MGR_STATS_RSP *csr =
diff --git a/src/osmo-bts-octphy/octphy_vty.c b/src/osmo-bts-octphy/octphy_vty.c
index dbc903a..6e58ad4 100644
--- a/src/osmo-bts-octphy/octphy_vty.c
+++ b/src/osmo-bts-octphy/octphy_vty.c
@@ -38,6 +38,7 @@
 #include <osmocom/vty/misc.h>
 
 #include <osmo-bts/gsm_data.h>
+#include <osmo-bts/phy_link.h>
 #include <osmo-bts/logging.h>
 #include <osmo-bts/vty.h>
 
@@ -51,149 +52,189 @@
 	SHOW_STR				\
 	TRX_STR
 
+#define OCT_STR	"OCTPHY Um interface\n"
+
 static struct gsm_bts *vty_bts;
 
 /* configuration */
 
-DEFUN(cfg_bts_phy_hwaddr, cfg_bts_phy_hwaddr_cmd,
-	"phy-hw-addr HWADDR",
-	"Configure the hardware addess of the OCTPHY\n"
+DEFUN(cfg_phy_hwaddr, cfg_phy_hwaddr_cmd,
+	"octphy hw-addr HWADDR",
+	OCT_STR "Configure the hardware addess of the OCTPHY\n"
 	"hardware address in aa:bb:cc:dd:ee:ff format\n")
 {
-	struct gsm_bts *bts = vty->index;
-	struct gsm_bts_trx *trx = bts->c0;
-	struct octphy_hdl *fl1h = trx_octphy_hdl(trx);
+	struct phy_link *plink = vty->index;
 	int rc;
 
-	rc = osmo_macaddr_parse(fl1h->phy_addr.sll_addr, argv[0]);
+	if (plink->state != PHY_LINK_SHUTDOWN) {
+		vty_out(vty, "Can only reconfigure a PHY link that is down%s",
+			VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	rc = osmo_macaddr_parse(plink->u.octphy.phy_addr.sll_addr, argv[0]);
 	if (rc < 0)
 		return CMD_WARNING;
 
 	return CMD_SUCCESS;
 }
 
-DEFUN(cfg_bts_phy_netdev, cfg_bts_phy_netdev_cmd,
-	"phy-netdev NAME",
-	"Configure the hardware device towards the OCTPHY\n"
+DEFUN(cfg_phy_netdev, cfg_phy_netdev_cmd,
+	"octphy net-device NAME",
+	OCT_STR "Configure the hardware device towards the OCTPHY\n"
 	"Ethernet device name\n")
 {
-	struct gsm_bts *bts = vty->index;
-	struct gsm_bts_trx *trx = bts->c0;
-	struct octphy_hdl *fl1h = trx_octphy_hdl(trx);
+	struct phy_link *plink = vty->index;
 
-	if (fl1h->netdev_name)
-		talloc_free(fl1h->netdev_name);
-	fl1h->netdev_name = talloc_strdup(fl1h, argv[0]);
+	if (plink->state != PHY_LINK_SHUTDOWN) {
+		vty_out(vty, "Can only reconfigure a PHY link that is down%s",
+			VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	if (plink->u.octphy.netdev_name)
+		talloc_free(plink->u.octphy.netdev_name);
+	plink->u.octphy.netdev_name = talloc_strdup(plink, argv[0]);
 
 	return CMD_SUCCESS;
 }
 
-DEFUN(cfg_trx_rf_port_idx, cfg_trx_rf_port_idx_cmd,
-	"rf-port-index <0-255>",
-	"Configure the RF Port for this TRX\n"
+DEFUN(cfg_phy_rf_port_idx, cfg_phy_rf_port_idx_cmd,
+	"octphy rf-port-index <0-255>",
+	OCT_STR "Configure the RF Port for this TRX\n"
 	"RF Port Index\n")
 {
-	struct gsm_bts_trx *trx = vty->index;
-	struct octphy_hdl *fl1h = trx_octphy_hdl(trx);
+	struct phy_link *plink = vty->index;
 
-	fl1h->config.rf_port_index = atoi(argv[0]);
+	if (plink->state != PHY_LINK_SHUTDOWN) {
+		vty_out(vty, "Can only reconfigure a PHY link that is down%s",
+			VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	plink->u.octphy.rf_port_index = atoi(argv[0]);
 
 	return CMD_SUCCESS;
 }
 
-DEFUN(cfg_trx_rx_gain_db, cfg_trx_rx_gain_db_cmd,
-	"rx-gain <0-73>",
-	"Configure the Rx Gain in dB\n"
+DEFUN(cfg_phy_rx_gain_db, cfg_phy_rx_gain_db_cmd,
+	"octphy rx-gain <0-73>",
+	OCT_STR "Configure the Rx Gain in dB\n"
 	"Rx gain in dB\n")
 {
-	struct gsm_bts_trx *trx = vty->index;
-	struct octphy_hdl *fl1h = trx_octphy_hdl(trx);
+	struct phy_link *plink = vty->index;
+
+	if (plink->state != PHY_LINK_SHUTDOWN) {
+		vty_out(vty, "Can only reconfigure a PHY link that is down%s",
+			VTY_NEWLINE);
+		return CMD_WARNING;
+	}
 
-	fl1h->config.rx_gain_db = atoi(argv[0]);
+	plink->u.octphy.rx_gain_db = atoi(argv[0]);
 
 	return CMD_SUCCESS;
 }
 
-DEFUN(cfg_trx_tx_atten_db, cfg_trx_tx_atten_db_cmd,
-	"tx-attenuation <0-359>",
-	"Configure the Tx Attenuation in quarter-dB\n"
+DEFUN(cfg_phy_tx_atten_db, cfg_phy_tx_atten_db_cmd,
+	"octphy tx-attenuation <0-359>",
+	OCT_STR "Configure the Tx Attenuation in quarter-dB\n"
 	"Tx attenuation in quarter-dB\n")
 {
-	struct gsm_bts_trx *trx = vty->index;
-	struct octphy_hdl *fl1h = trx_octphy_hdl(trx);
+	struct phy_link *plink = vty->index;
+
+	if (plink->state != PHY_LINK_SHUTDOWN) {
+		vty_out(vty, "Can only reconfigure a PHY link that is down%s",
+			VTY_NEWLINE);
+		return CMD_WARNING;
+	}
 
-	fl1h->config.tx_atten_db = atoi(argv[0]);
+	plink->u.octphy.tx_atten_db = atoi(argv[0]);
 
 	return CMD_SUCCESS;
 }
 
-DEFUN(get_rf_port_stats, get_rf_port_stats_cmd,
-	"get-rf-port-stats <0-1>",
-	"Obtain statistics for the RF Port\n"
+DEFUN(show_rf_port_stats, show_rf_port_stats_cmd,
+	"show phy <0-255> rf-port-stats <0-1>",
+	"Show statistics for the RF Port\n"
 	"RF Port Number\n")
 {
-	struct octphy_hdl *fl1h = trx_octphy_hdl(vty_bts->c0);
+	int phy_nr = atoi(argv[0]);
+	struct phy_link *plink = phy_link_by_num(phy_nr);
 
-	octphy_hw_get_rf_port_stats(fl1h, atoi(argv[0]));
+	octphy_hw_get_rf_port_stats(plink->u.octphy.hdl, atoi(argv[1]));
+
+	/* FIXME: Actually print to VTY, not just log */
+	vty_out(vty, "Please check the log file for the response%s",
+		VTY_NEWLINE);
 
 	return CMD_SUCCESS;
 }
 
-DEFUN(get_clk_sync_stats, get_clk_sync_stats_cmd,
-	"get-clk-sync-stats",
+DEFUN(show_clk_sync_stats, show_clk_sync_stats_cmd,
+	"show phy <0-255> clk-sync-stats",
 	"Obtain statistics for the Clock Sync Manager\n")
 {
-	struct octphy_hdl *fl1h = trx_octphy_hdl(vty_bts->c0);
+	int phy_nr = atoi(argv[0]);
+	struct phy_link *plink = phy_link_by_num(phy_nr);
+
+	octphy_hw_get_clock_sync_stats(plink->u.octphy.hdl);
 
-	octphy_hw_get_clock_sync_stats(fl1h);
+	/* FIXME: Actually print to VTY, not just log */
+	vty_out(vty, "Please check the log file for the response%s",
+		VTY_NEWLINE);
 
 	return CMD_SUCCESS;
 }
 
+void bts_model_config_write_phy(struct vty *vty, struct phy_link *plink)
+{
+	if (plink->u.octphy.netdev_name)
+		vty_out(vty, " netdev %s%s", plink->u.octphy.netdev_name,
+			VTY_NEWLINE);
+
+	vty_out(vty, " hw-addr %02x:%02x:%02x:%02x:%02x:%02x%s",
+		plink->u.octphy.phy_addr.sll_addr[0],
+		plink->u.octphy.phy_addr.sll_addr[1],
+		plink->u.octphy.phy_addr.sll_addr[2],
+		plink->u.octphy.phy_addr.sll_addr[3],
+		plink->u.octphy.phy_addr.sll_addr[4],
+		plink->u.octphy.phy_addr.sll_addr[5],
+		VTY_NEWLINE);
+	vty_out(vty, "  rx-gain %u%s", plink->u.octphy.rx_gain_db,
+		VTY_NEWLINE);
+	vty_out(vty, "  tx-attenuation %u%s", plink->u.octphy.tx_atten_db,
+		VTY_NEWLINE);
+	vty_out(vty, "  rf-port-index %u%s", plink->u.octphy.rf_port_index,
+		VTY_NEWLINE);
+}
+
 void bts_model_config_write_bts(struct vty *vty, struct gsm_bts *bts)
 {
 	struct gsm_bts_role_bts *btsb = bts_role_bts(bts);
-	struct octphy_hdl *fl1h = trx_octphy_hdl(bts->c0);
-
-	if (fl1h->netdev_name)
-		vty_out(vty, " phy-netdev %s%s", fl1h->netdev_name,
-			VTY_NEWLINE);
 
 	if (btsb->auto_band)
 		vty_out(vty, " auto-band%s", VTY_NEWLINE);
 
-	vty_out(vty, " phy-hw-addr %02x:%02x:%02x:%02x:%02x:%02x%s",
-		fl1h->phy_addr.sll_addr[0], fl1h->phy_addr.sll_addr[1],
-		fl1h->phy_addr.sll_addr[2], fl1h->phy_addr.sll_addr[3],
-		fl1h->phy_addr.sll_addr[4], fl1h->phy_addr.sll_addr[5],
-		VTY_NEWLINE);
 }
 
 void bts_model_config_write_trx(struct vty *vty, struct gsm_bts_trx *trx)
 {
-	struct octphy_hdl *fl1h = trx_octphy_hdl(trx);
-
-	vty_out(vty, "  rx-gain %u%s", fl1h->config.rx_gain_db,
-		VTY_NEWLINE);
-	vty_out(vty, "  tx-attenuation %u%s", fl1h->config.tx_atten_db,
-		VTY_NEWLINE);
 }
 
 DEFUN(show_sys_info, show_sys_info_cmd,
-	"show trx <0-255> system-information",
+	"show phy <0-255> system-information",
 	SHOW_TRX_STR "Display information about system\n")
 {
-	int trx_nr = atoi(argv[0]);
-	struct gsm_bts_trx *trx = gsm_bts_trx_num(vty_bts, trx_nr);
+	int phy_nr = atoi(argv[0]);
+	struct phy_link *plink = phy_link_by_num(phy_nr);
 	struct octphy_hdl *fl1h;
-	int i;
 
-	if (!trx) {
-		vty_out(vty, "Cannot find TRX number %u%s",
-			trx_nr, VTY_NEWLINE);
+	if (!plink) {
+		vty_out(vty, "Cannot find PHY number %u%s",
+			phy_nr, VTY_NEWLINE);
 		return CMD_WARNING;
 	}
-	fl1h = trx_octphy_hdl(trx);
+	fl1h = plink->u.octphy.hdl;
 
 	vty_out(vty, "System Platform: '%s', Version: '%s'%s",
 		fl1h->info.system.platform, fl1h->info.system.version,
@@ -210,15 +251,14 @@ int bts_model_vty_init(struct gsm_bts *bts)
 {
 	vty_bts = bts;
 
-	install_element(BTS_NODE, &cfg_bts_phy_hwaddr_cmd);
-	install_element(BTS_NODE, &cfg_bts_phy_netdev_cmd);
+	install_element(PHY_NODE, &cfg_phy_hwaddr_cmd);
+	install_element(PHY_NODE, &cfg_phy_netdev_cmd);
+	install_element(PHY_NODE, &cfg_phy_rf_port_idx_cmd);
+	install_element(PHY_NODE, &cfg_phy_rx_gain_db_cmd);
+	install_element(PHY_NODE, &cfg_phy_tx_atten_db_cmd);
 
-	install_element(TRX_NODE, &cfg_trx_rf_port_idx_cmd);
-	install_element(TRX_NODE, &cfg_trx_rx_gain_db_cmd);
-	install_element(TRX_NODE, &cfg_trx_tx_atten_db_cmd);
-
-	install_element_ve(&get_rf_port_stats_cmd);
-	install_element_ve(&get_clk_sync_stats_cmd);
+	install_element_ve(&show_rf_port_stats_cmd);
+	install_element_ve(&show_clk_sync_stats_cmd);
 	install_element_ve(&show_sys_info_cmd);
 
 	return 0;
@@ -226,13 +266,5 @@ int bts_model_vty_init(struct gsm_bts *bts)
 
 int bts_model_ctrl_cmds_install(struct gsm_bts *bts)
 {
-	/* FIXME: really ugly hack: We can only initialize the L1 intrface
-	 * after reading the config file, and this is the only call-back after
-	 * vty_read_config_fioe() at this point.  Will be cleaned up with the
-	 * phy interface generalization patches coming up soon as part of the
-	 * multi-trx work */
-	struct octphy_hdl *fl1h = bts->c0->role_bts.l1h;
-	l1if_open(fl1h);
-
 	return 0;
 }
diff --git a/src/osmo-bts-trx/l1_if.c b/src/osmo-bts-trx/l1_if.c
index edd4b7b..befaffd 100644
--- a/src/osmo-bts-trx/l1_if.c
+++ b/src/osmo-bts-trx/l1_if.c
@@ -59,7 +59,7 @@ static const uint8_t transceiver_chan_types[_GSM_PCHAN_MAX] = {
  * create/destroy trx l1 instance
  */
 
-struct trx_l1h *l1if_open(struct gsm_bts_trx *trx)
+struct trx_l1h *l1if_open(struct phy_instance *pinst)
 {
 	struct trx_l1h *l1h;
 	int rc;
@@ -67,11 +67,9 @@ struct trx_l1h *l1if_open(struct gsm_bts_trx *trx)
 	l1h = talloc_zero(tall_bts_ctx, struct trx_l1h);
 	if (!l1h)
 		return NULL;
-	l1h->trx = trx;
-	l1h->l1s.trx = trx;
-	trx->role_bts.l1h = l1h;
+	l1h->phy_inst = pinst;
 
-	trx_sched_init(&l1h->l1s);
+	trx_sched_init(&l1h->l1s, pinst->trx);
 
 	rc = trx_if_open(l1h);
 	if (rc < 0) {
@@ -83,7 +81,6 @@ struct trx_l1h *l1if_open(struct gsm_bts_trx *trx)
 
 err:
 	l1if_close(l1h);
-	trx->role_bts.l1h = NULL;
 	return NULL;
 }
 
@@ -100,7 +97,8 @@ void l1if_reset(struct trx_l1h *l1h)
 
 static void check_transceiver_availability_trx(struct trx_l1h *l1h, int avail)
 {
-	struct gsm_bts_trx *trx = l1h->trx;
+	struct phy_instance *pinst = l1h->phy_inst;
+	struct gsm_bts_trx *trx = pinst->trx;
 	uint8_t tn;
 
 	/* HACK, we should change state when we receive first clock from
@@ -132,10 +130,10 @@ static void check_transceiver_availability_trx(struct trx_l1h *l1h, int avail)
 int check_transceiver_availability(struct gsm_bts *bts, int avail)
 {
 	struct gsm_bts_trx *trx;
-	struct trx_l1h *l1h;
 
 	llist_for_each_entry(trx, &bts->trx_list, list) {
-		l1h = trx_l1h_hdl(trx);
+		struct phy_instance *pinst = trx_phy_instance(trx);
+		struct trx_l1h *l1h = pinst->u.osmotrx.hdl;
 		check_transceiver_availability_trx(l1h, avail);
 	}
 	return 0;
@@ -147,6 +145,7 @@ int check_transceiver_availability(struct gsm_bts *bts, int avail)
  */
 int l1if_provision_transceiver_trx(struct trx_l1h *l1h)
 {
+	struct phy_link *plink = l1h->phy_inst->phy_link;
 	uint8_t tn;
 
 	if (!transceiver_available)
@@ -177,18 +176,23 @@ int l1if_provision_transceiver_trx(struct trx_l1h *l1h)
 		}
 
 		/* after power on */
-		if (l1h->config.rxgain_valid && !l1h->config.rxgain_sent) {
-			trx_if_cmd_setrxgain(l1h, l1h->config.rxgain);
-			l1h->config.rxgain_sent = 1;
-		}
-		if (l1h->config.power_valid && !l1h->config.power_sent) {
-			trx_if_cmd_setpower(l1h, l1h->config.power);
-			l1h->config.power_sent = 1;
+		if (l1h->phy_inst->num == 0) {
+			if (plink->u.osmotrx.rxgain_valid &&
+			    !plink->u.osmotrx.rxgain_sent) {
+				trx_if_cmd_setrxgain(l1h, plink->u.osmotrx.rxgain);
+				plink->u.osmotrx.rxgain_sent = 1;
+			}
+			if (plink->u.osmotrx.power_valid &&
+			    !plink->u.osmotrx.power_sent) {
+				trx_if_cmd_setpower(l1h, plink->u.osmotrx.power);
+				plink->u.osmotrx.power_sent = 1;
+			}
 		}
 		if (l1h->config.maxdly_valid && !l1h->config.maxdly_sent) {
 			trx_if_cmd_setmaxdly(l1h, l1h->config.maxdly);
 			l1h->config.maxdly_sent = 1;
 		}
+
 		for (tn = 0; tn < TRX_NR_TS; tn++) {
 			if (l1h->config.slottype_valid[tn]
 			 && !l1h->config.slottype_sent[tn]) {
@@ -203,8 +207,10 @@ int l1if_provision_transceiver_trx(struct trx_l1h *l1h)
 	if (!l1h->config.poweron && !l1h->config.poweron_sent) {
 		trx_if_cmd_poweroff(l1h);
 		l1h->config.poweron_sent = 1;
-		l1h->config.rxgain_sent = 0;
-		l1h->config.power_sent = 0;
+		if (l1h->phy_inst->num == 0) {
+			plink->u.osmotrx.rxgain_sent = 0;
+			plink->u.osmotrx.power_sent = 0;
+		}
 		l1h->config.maxdly_sent = 0;
 		for (tn = 0; tn < TRX_NR_TS; tn++)
 			l1h->config.slottype_sent[tn] = 0;
@@ -216,17 +222,20 @@ int l1if_provision_transceiver_trx(struct trx_l1h *l1h)
 int l1if_provision_transceiver(struct gsm_bts *bts)
 {
 	struct gsm_bts_trx *trx;
-	struct trx_l1h *l1h;
 	uint8_t tn;
 
 	llist_for_each_entry(trx, &bts->trx_list, list) {
-		l1h = trx_l1h_hdl(trx);
+		struct phy_instance *pinst = trx_phy_instance(trx);
+		struct phy_link *plink = pinst->phy_link;
+		struct trx_l1h *l1h = pinst->u.osmotrx.hdl;
 		l1h->config.arfcn_sent = 0;
 		l1h->config.tsc_sent = 0;
 		l1h->config.bsic_sent = 0;
 		l1h->config.poweron_sent = 0;
-		l1h->config.rxgain_sent = 0;
-		l1h->config.power_sent = 0;
+		if (l1h->phy_inst->num == 0) {
+			plink->u.osmotrx.rxgain_sent = 0;
+			plink->u.osmotrx.power_sent = 0;
+		}
 		l1h->config.maxdly_sent = 0;
 		for (tn = 0; tn < TRX_NR_TS; tn++)
 			l1h->config.slottype_sent[tn] = 0;
@@ -242,7 +251,8 @@ int l1if_provision_transceiver(struct gsm_bts *bts)
 /* initialize the layer1 */
 static int trx_init(struct gsm_bts_trx *trx)
 {
-	struct trx_l1h *l1h = trx_l1h_hdl(trx);
+	struct phy_instance *pinst = trx_phy_instance(trx);
+	struct trx_l1h *l1h = pinst->u.osmotrx.hdl;
 
 	/* power on transceiver, if not already */
 	if (!l1h->config.poweron) {
@@ -264,7 +274,8 @@ static int trx_init(struct gsm_bts_trx *trx)
 /* deactivate transceiver */
 int bts_model_trx_close(struct gsm_bts_trx *trx)
 {
-	struct trx_l1h *l1h = trx_l1h_hdl(trx);
+	struct phy_instance *pinst = trx_phy_instance(trx);
+	struct trx_l1h *l1h = pinst->u.osmotrx.hdl;
 	enum gsm_phys_chan_config pchan = trx->ts[0].pchan;
 
 	/* close all logical channels and reset timeslots */
@@ -308,7 +319,6 @@ int bts_model_adjst_ms_pwr(struct gsm_lchan *lchan)
 static uint8_t trx_set_bts(struct gsm_bts *bts, struct tlv_parsed *new_attr)
 {
 	struct gsm_bts_trx *trx;
-	struct trx_l1h *l1h;
 	uint8_t bsic = bts->bsic;
 	struct gsm_bts_role_bts *btsb = bts_role_bts(bts);
 
@@ -318,7 +328,8 @@ static uint8_t trx_set_bts(struct gsm_bts *bts, struct tlv_parsed *new_attr)
 	}
 
 	llist_for_each_entry(trx, &bts->trx_list, list) {
-		l1h = trx_l1h_hdl(trx);
+		struct phy_instance *pinst = trx_phy_instance(trx);
+		struct trx_l1h *l1h = pinst->u.osmotrx.hdl;
 		if (l1h->config.bsic != bsic || !l1h->config.bsic_valid) {
 			l1h->config.bsic = bsic;
 			l1h->config.bsic_valid = 1;
@@ -335,7 +346,9 @@ static uint8_t trx_set_bts(struct gsm_bts *bts, struct tlv_parsed *new_attr)
 /* set trx attributes */
 static uint8_t trx_set_trx(struct gsm_bts_trx *trx)
 {
-	struct trx_l1h *l1h = trx_l1h_hdl(trx);
+	struct phy_instance *pinst = trx_phy_instance(trx);
+	struct phy_link *plink = pinst->phy_link;
+	struct trx_l1h *l1h = pinst->u.osmotrx.hdl;
 	uint16_t arfcn = trx->arfcn;
 
 	if (l1h->config.arfcn != arfcn || !l1h->config.arfcn_valid) {
@@ -345,10 +358,10 @@ static uint8_t trx_set_trx(struct gsm_bts_trx *trx)
 		l1if_provision_transceiver_trx(l1h);
 	}
 
-	if (l1h->config.power_oml) {
-		l1h->config.power = trx->max_power_red;
-		l1h->config.power_valid = 1;
-		l1h->config.power_sent = 0;
+	if (plink->u.osmotrx.power_oml && pinst->num == 0) {
+		plink->u.osmotrx.power = trx->max_power_red;
+		plink->u.osmotrx.power_valid = 1;
+		plink->u.osmotrx.power_sent = 0;
 		l1if_provision_transceiver_trx(l1h);
 	}
 
@@ -358,7 +371,8 @@ static uint8_t trx_set_trx(struct gsm_bts_trx *trx)
 /* set ts attributes */
 static uint8_t trx_set_ts(struct gsm_bts_trx_ts *ts)
 {
-	struct trx_l1h *l1h = trx_l1h_hdl(ts->trx);
+	struct phy_instance *pinst = trx_phy_instance(ts->trx);
+	struct trx_l1h *l1h = pinst->u.osmotrx.hdl;
 	uint8_t tn = ts->nr;
 	uint16_t tsc = ts->tsc;
 	enum gsm_phys_chan_config pchan = ts->pchan;
@@ -439,6 +453,7 @@ static int l1if_set_ciphering(struct trx_l1h *l1h, struct gsm_lchan *lchan,
 static int mph_info_chan_confirm(struct trx_l1h *l1h, uint8_t chan_nr,
 	enum osmo_mph_info_type type, uint8_t cause)
 {
+	struct phy_instance *pinst = l1h->phy_inst;
 	struct osmo_phsap_prim l1sap;
 
 	memset(&l1sap, 0, sizeof(l1sap));
@@ -448,7 +463,7 @@ static int mph_info_chan_confirm(struct trx_l1h *l1h, uint8_t chan_nr,
 	l1sap.u.info.u.act_cnf.chan_nr = chan_nr;
 	l1sap.u.info.u.act_cnf.cause = cause;
 
-	return l1sap_up(l1h->trx, &l1sap);
+	return l1sap_up(pinst->trx, &l1sap);
 }
 
 int l1if_mph_time_ind(struct gsm_bts *bts, uint32_t fn)
@@ -503,7 +518,8 @@ int l1if_process_meas_res(struct gsm_bts_trx *trx, uint8_t tn, uint32_t fn, uint
 /* primitive from common part */
 int bts_model_l1sap_down(struct gsm_bts_trx *trx, struct osmo_phsap_prim *l1sap)
 {
-	struct trx_l1h *l1h = trx_l1h_hdl(trx);
+	struct phy_instance *pinst = trx_phy_instance(trx);
+	struct trx_l1h *l1h = pinst->u.osmotrx.hdl;
 	struct msgb *msg = l1sap->oph.msg;
 	uint8_t chan_nr;
 	uint8_t tn, ss;
diff --git a/src/osmo-bts-trx/l1_if.h b/src/osmo-bts-trx/l1_if.h
index f492687..187303c 100644
--- a/src/osmo-bts-trx/l1_if.h
+++ b/src/osmo-bts-trx/l1_if.h
@@ -2,6 +2,7 @@
 #define L1_IF_H_TRX
 
 #include <osmo-bts/scheduler.h>
+#include <osmo-bts/phy_link.h>
 
 struct trx_config {
 	uint8_t			poweron;	/* poweron(1) or poweroff(0) */
@@ -19,15 +20,6 @@ struct trx_config {
 	uint8_t			bsic;
 	int			bsic_sent;
 
-	int			rxgain_valid;
-	int			rxgain;
-	int			rxgain_sent;
-
-	int			power_valid;
-	int			power;
-	int			power_oml;
-	int			power_sent;
-
 	int			maxdly_valid;
 	int			maxdly;
 	int			maxdly_sent;
@@ -42,7 +34,8 @@ struct trx_config {
 struct trx_l1h {
 	struct llist_head	trx_ctrl_list;
 
-	struct gsm_bts_trx	*trx;
+	//struct gsm_bts_trx	*trx;
+	struct phy_instance	*phy_inst;
 
 	struct osmo_fd		trx_ofd_ctrl;
 	struct osmo_timer_list	trx_ctrl_timer;
@@ -55,7 +48,7 @@ struct trx_l1h {
 	struct l1sched_trx	l1s;
 };
 
-struct trx_l1h *l1if_open(struct gsm_bts_trx *trx);
+struct trx_l1h *l1if_open(struct phy_instance *pinst);
 void l1if_close(struct trx_l1h *l1h);
 void l1if_reset(struct trx_l1h *l1h);
 int check_transceiver_availability(struct gsm_bts *bts, int avail);
@@ -69,7 +62,8 @@ int l1if_process_meas_res(struct gsm_bts_trx *trx, uint8_t tn, uint32_t fn, uint
 
 static inline struct l1sched_trx *trx_l1sched_hdl(struct gsm_bts_trx *trx)
 {
-	struct trx_l1h *l1h = trx_l1h_hdl(trx);
+	struct phy_instance *pinst = trx->role_bts.l1h;
+	struct trx_l1h *l1h = pinst->u.osmotrx.hdl;
 	return &l1h->l1s;
 }
 
diff --git a/src/osmo-bts-trx/main.c b/src/osmo-bts-trx/main.c
index aa6987c..62e8fe9 100644
--- a/src/osmo-bts-trx/main.c
+++ b/src/osmo-bts-trx/main.c
@@ -45,6 +45,7 @@
 #include <osmocom/core/bits.h>
 
 #include <osmo-bts/gsm_data.h>
+#include <osmo-bts/phy_link.h>
 #include <osmo-bts/logging.h>
 #include <osmo-bts/abis.h>
 #include <osmo-bts/bts.h>
@@ -58,43 +59,6 @@
 #include "l1_if.h"
 #include "trx_if.h"
 
-int bts_model_init(struct gsm_bts *bts)
-{
-	void *l1h;
-	struct gsm_bts_trx *trx;
-	struct gsm_bts_role_bts *btsb = bts_role_bts(bts);
-
-	btsb->support.ciphers = CIPHER_A5(1) | CIPHER_A5(2);
-	if (!settsc_enabled && !setbsic_enabled)
-		settsc_enabled = setbsic_enabled = 1;
-
-	llist_for_each_entry(trx, &bts->trx_list, list) {
-		l1h = l1if_open(trx);
-		if (!l1h) {
-			LOGP(DL1C, LOGL_FATAL, "Cannot open L1 Interface\n");
-			goto error;
-		}
-
-		trx->role_bts.l1h = l1h;
-		trx->nominal_power = 23;
-
-		l1if_reset(l1h);
-	}
-
-	bts_model_vty_init(bts);
-
-	return 0;
-
-error:
-	llist_for_each_entry(trx, &bts->trx_list, list) {
-		l1h = trx->role_bts.l1h;
-		if (l1h)
-			l1if_close(l1h);
-	}
-
-	return -EIO;
-}
-
 /* dummy, since no direct dsp support */
 uint32_t trx_get_hlayer1(struct gsm_bts_trx *trx)
 {
@@ -103,10 +67,6 @@ uint32_t trx_get_hlayer1(struct gsm_bts_trx *trx)
 
 void bts_model_print_help()
 {
-	printf(
-		"  -I	--local-trx-ip	Local IP for transceiver to connect (default=%s)\n"
-		, transceiver_ip
-		);
 }
 
 int bts_model_handle_options(int argc, char **argv)
@@ -116,21 +76,16 @@ int bts_model_handle_options(int argc, char **argv)
 	while (1) {
 		int option_idx = 0, c;
 		static const struct option long_options[] = {
-			/* specific to this hardware */
-			{ "local-trx-ip", 1, 0, 'I' },
 			{ 0, 0, 0, 0 }
 		};
 
-		c = getopt_long(argc, argv, "I:",
+		c = getopt_long(argc, argv, "",
 				long_options, &option_idx);
 
 		if (c == -1)
 			break;
 
 		switch (c) {
-		case 'I':
-			transceiver_ip = strdup(optarg);
-			break;
 		default:
 			num_errors++;
 			break;
@@ -140,6 +95,35 @@ int bts_model_handle_options(int argc, char **argv)
 	return num_errors;
 }
 
+int bts_model_init(struct gsm_bts *bts)
+{
+	struct gsm_bts_role_bts *btsb = bts_role_bts(bts);
+
+	btsb->support.ciphers = CIPHER_A5(1) | CIPHER_A5(2);
+
+	/* FIXME: this needs to be overridden with the real hardrware
+	 * value */
+	bts->c0->nominal_power = 23;
+
+	bts_model_vty_init(bts);
+
+	return 0;
+}
+
+void bts_model_phy_link_set_defaults(struct phy_link *plink)
+{
+	plink->u.osmotrx.transceiver_ip = talloc_strdup(plink, "127.0.0.1");
+	plink->u.osmotrx.base_port_local = 5800;
+	plink->u.osmotrx.base_port_remote = 5700;
+	plink->u.osmotrx.clock_advance = 20;
+	plink->u.osmotrx.rts_advance = 5;
+	plink->u.osmotrx.power_oml = 1;
+}
+
+void bts_model_phy_instance_set_defaults(struct phy_instance *pinst)
+{
+}
+
 int main(int argc, char **argv)
 {
 	return bts_main(argc, argv);
diff --git a/src/osmo-bts-trx/scheduler_trx.c b/src/osmo-bts-trx/scheduler_trx.c
index ac9212b..15c05e8 100644
--- a/src/osmo-bts-trx/scheduler_trx.c
+++ b/src/osmo-bts-trx/scheduler_trx.c
@@ -55,12 +55,6 @@ uint32_t transceiver_last_fn;
 static struct timeval transceiver_clock_tv;
 static struct osmo_timer_list transceiver_clock_timer;
 
-/* clock advance for the transceiver */
-uint32_t trx_clock_advance = 20;
-
-/* advance RTS to give some time for data processing. (especially PCU) */
-uint32_t trx_rts_advance = 5; /* about 20ms */
-
 /* Enable this to multiply TOA of RACH by 10.
  * This is usefull to check tenth of timing advances with RSSI test tool.
  * Note that regular phones will not work when using this test! */
@@ -1262,14 +1256,16 @@ static int trx_sched_fn(struct gsm_bts *bts, uint32_t fn)
 	/* send time indication */
 	l1if_mph_time_ind(bts, fn);
 
-	/* advance frame number, so the transceiver has more time until
-	 * it must be transmitted. */
-	fn = (fn + trx_clock_advance) % GSM_HYPERFRAME;
-
 	/* process every TRX */
 	llist_for_each_entry(trx, &bts->trx_list, list) {
-		struct trx_l1h *l1h = trx_l1h_hdl(trx);
-		struct l1sched_trx *l1t = trx_l1sched_hdl(trx);
+		struct phy_instance *pinst = trx_phy_instance(trx);
+		struct phy_link *plink = pinst->phy_link;
+		struct trx_l1h *l1h = pinst->u.osmotrx.hdl;
+		struct l1sched_trx *l1t = &l1h->l1s;
+
+		/* advance frame number, so the transceiver has more
+		 * time until it must be transmitted. */
+		fn = (fn + plink->u.osmotrx.clock_advance) % GSM_HYPERFRAME;
 
 		/* we don't schedule, if power is off */
 		if (!trx_if_powered(l1h))
@@ -1279,7 +1275,7 @@ static int trx_sched_fn(struct gsm_bts *bts, uint32_t fn)
 		for (tn = 0; tn < ARRAY_SIZE(l1t->ts); tn++) {
 			/* ready-to-send */
 			_sched_rts(l1t, tn,
-				(fn + trx_rts_advance) % GSM_HYPERFRAME);
+				(fn + plink->u.osmotrx.rts_advance) % GSM_HYPERFRAME);
 			/* get burst for FN */
 			bits = _sched_dl_burst(l1t, tn, fn);
 			if (!bits) {
@@ -1323,10 +1319,12 @@ no_clock:
 		/* flush pending messages of transceiver */
 		/* close all logical channels and reset timeslots */
 		llist_for_each_entry(trx, &bts->trx_list, list) {
-			trx_if_flush(trx_l1h_hdl(trx));
-			trx_sched_reset(trx_l1sched_hdl(trx));
+			struct phy_instance *pinst = trx_phy_instance(trx);
+			struct trx_l1h *l1h = pinst->u.osmotrx.hdl;
+			trx_if_flush(l1h);
+			trx_sched_reset(&l1h->l1s);
 			if (trx->nr == 0)
-				trx_if_cmd_poweroff(trx_l1h_hdl(trx));
+				trx_if_cmd_poweroff(l1h);
 		}
 
 		/* tell BSC */
@@ -1461,7 +1459,8 @@ new_clock:
 
 void _sched_act_rach_det(struct l1sched_trx *l1t, uint8_t tn, uint8_t ss, int activate)
 {
-	struct trx_l1h *l1h = trx_l1h_hdl(l1t->trx);
+	struct phy_instance *pinst = trx_phy_instance(l1t->trx);
+	struct trx_l1h *l1h = pinst->u.osmotrx.hdl;
 
 	if (activate)
 		trx_if_cmd_handover(l1h, tn, ss);
diff --git a/src/osmo-bts-trx/trx_if.c b/src/osmo-bts-trx/trx_if.c
index dbe6f68..a4c16dc 100644
--- a/src/osmo-bts-trx/trx_if.c
+++ b/src/osmo-bts-trx/trx_if.c
@@ -2,6 +2,7 @@
  * OpenBTS TRX interface handling
  *
  * Copyright (C) 2013  Andreas Eversberg <jolly at eversberg.eu>
+ * Copyright (C) 2016  Harald Welte <laforge at gnumonks.org>
  *
  * All Rights Reserved
  *
@@ -34,6 +35,7 @@
 #include <osmocom/core/talloc.h>
 #include <osmocom/core/bits.h>
 
+#include <osmo-bts/phy_link.h>
 #include <osmo-bts/logging.h>
 #include <osmo-bts/bts.h>
 #include <osmo-bts/scheduler.h>
@@ -45,7 +47,6 @@
 //#define TOA_RSSI_DEBUG
 
 int transceiver_available = 0;
-const char *transceiver_ip = "127.0.0.1";
 int settsc_enabled = 0;
 int setbsic_enabled = 0;
 
@@ -53,11 +54,10 @@ int setbsic_enabled = 0;
  * socket
  */
 
-static uint16_t base_port_local = 5800;
-
 /* open socket */
-static int trx_udp_open(void *priv, struct osmo_fd *ofd, uint16_t port,
-	int (*cb)(struct osmo_fd *fd, unsigned int what))
+static int trx_udp_open(void *priv, struct osmo_fd *ofd, const char *host,
+			uint16_t port_local, uint16_t port_remote,
+			int (*cb)(struct osmo_fd *fd, unsigned int what))
 {
 	struct sockaddr_storage sas;
 	struct sockaddr *sa = (struct sockaddr *)&sas;
@@ -71,8 +71,8 @@ static int trx_udp_open(void *priv, struct osmo_fd *ofd, uint16_t port,
 	ofd->data = priv;
 
 	/* Listen / Binds */
-	rc = osmo_sock_init_ofd(ofd, AF_UNSPEC, SOCK_DGRAM, 0, transceiver_ip,
-		port, OSMO_SOCK_F_BIND);
+	rc = osmo_sock_init_ofd(ofd, AF_UNSPEC, SOCK_DGRAM, 0, host,
+		port_local, OSMO_SOCK_F_BIND);
 	if (rc < 0)
 		return rc;
 
@@ -84,10 +84,10 @@ static int trx_udp_open(void *priv, struct osmo_fd *ofd, uint16_t port,
 
 	if (sa->sa_family == AF_INET) {
 		struct sockaddr_in *sin = (struct sockaddr_in *)sa;
-		sin->sin_port = htons(ntohs(sin->sin_port) - 100);
+		sin->sin_port = htons(port_remote);
 	} else if (sa->sa_family == AF_INET6) {
 		struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)sa;
-		sin6->sin6_port = htons(ntohs(sin6->sin6_port) - 100);
+		sin6->sin6_port = htons(port_remote);
 	} else {
 		return -EINVAL;
 	}
@@ -96,7 +96,6 @@ static int trx_udp_open(void *priv, struct osmo_fd *ofd, uint16_t port,
 	if (rc)
 		return rc;
 
-	
 	return 0;
 }
 
@@ -115,13 +114,11 @@ static void trx_udp_close(struct osmo_fd *ofd)
  * clock
  */
 
-static struct osmo_fd trx_ofd_clk;
-
-
 /* get clock from clock socket */
 static int trx_clk_read_cb(struct osmo_fd *ofd, unsigned int what)
 {
-	struct trx_l1h *l1h = ofd->data;
+	struct phy_link *plink = ofd->data;
+	struct phy_instance *pinst = phy_instance_by_num(plink, 0);
 	char buf[1500];
 	int len;
 	uint32_t fn;
@@ -146,7 +143,7 @@ static int trx_clk_read_cb(struct osmo_fd *ofd, unsigned int what)
 			"correctly, correcting to fn=%u\n", fn);
 	}
 
-	trx_sched_clock(l1h->trx->bts, fn);
+	trx_sched_clock(pinst->trx->bts, fn);
 
 	return 0;
 }
@@ -168,8 +165,8 @@ static void trx_ctrl_send(struct trx_l1h *l1h)
 		return;
 	tcm = llist_entry(l1h->trx_ctrl_list.next, struct trx_ctrl_msg, list);
 
-	LOGP(DTRX, LOGL_DEBUG, "Sending control '%s' to trx=%u\n", tcm->cmd,
-		l1h->trx->nr);
+	LOGP(DTRX, LOGL_DEBUG, "Sending control '%s' to %s\n", tcm->cmd,
+		phy_instance_name(l1h->phy_inst));
 	/* send command */
 	send(l1h->trx_ofd_ctrl.fd, tcm->cmd, strlen(tcm->cmd)+1, 0);
 
@@ -184,8 +181,8 @@ static void trx_ctrl_timer_cb(void *data)
 {
 	struct trx_l1h *l1h = data;
 
-	LOGP(DTRX, LOGL_NOTICE, "No response from transceiver for trx=%d\n",
-		l1h->trx->nr);
+	LOGP(DTRX, LOGL_NOTICE, "No response from transceiver for %s\n",
+		phy_instance_name(l1h->phy_inst));
 
 	trx_ctrl_send(l1h);
 }
@@ -232,7 +229,8 @@ static int trx_ctrl_cmd(struct trx_l1h *l1h, int critical, const char *cmd,
 
 int trx_if_cmd_poweroff(struct trx_l1h *l1h)
 {
-	if (l1h->trx->nr == 0)
+	struct phy_instance *pinst = l1h->phy_inst;
+	if (pinst->num == 0)
 		return trx_ctrl_cmd(l1h, 1, "POWEROFF", "");
 	else
 		return 0;
@@ -240,7 +238,8 @@ int trx_if_cmd_poweroff(struct trx_l1h *l1h)
 
 int trx_if_cmd_poweron(struct trx_l1h *l1h)
 {
-	if (l1h->trx->nr == 0)
+	struct phy_instance *pinst = l1h->phy_inst;
+	if (pinst->num == 0)
 		return trx_ctrl_cmd(l1h, 1, "POWERON", "");
 	else
 		return 0;
@@ -324,6 +323,7 @@ int trx_if_cmd_nohandover(struct trx_l1h *l1h, uint8_t tn, uint8_t ss)
 static int trx_ctrl_read_cb(struct osmo_fd *ofd, unsigned int what)
 {
 	struct trx_l1h *l1h = ofd->data;
+	struct phy_instance *pinst = l1h->phy_inst;
 	char buf[1500];
 	int len, resp;
 
@@ -374,11 +374,12 @@ static int trx_ctrl_read_cb(struct osmo_fd *ofd, unsigned int what)
 		sscanf(p + 1, "%d", &resp);
 		if (resp) {
 			LOGP(DTRX, (tcm->critical) ? LOGL_FATAL : LOGL_NOTICE,
-				"transceiver (trx=%d) rejected TRX command "
-				"with response: '%s'\n", l1h->trx->nr, buf);
+				"transceiver (%s) rejected TRX command "
+				"with response: '%s'\n",
+				phy_instance_name(pinst), buf);
 rsp_error:
 			if (tcm->critical) {
-				bts_shutdown(l1h->trx->bts, "SIGINT");
+				bts_shutdown(pinst->trx->bts, "SIGINT");
 				/* keep tcm list, so process is stopped */
 				return -EIO;
 			}
@@ -493,37 +494,91 @@ int trx_if_data(struct trx_l1h *l1h, uint8_t tn, uint32_t fn, uint8_t pwr,
  * open/close
  */
 
+int bts_model_phy_link_open(struct phy_link *plink)
+{
+	struct phy_instance *pinst;
+	int rc;
+
+	phy_link_state_set(plink, PHY_LINK_CONNECTING);
+
+	/* open the shared/common clock socket */
+	rc = trx_udp_open(plink, &plink->u.osmotrx.trx_ofd_clk,
+			  plink->u.osmotrx.transceiver_ip,
+			  plink->u.osmotrx.base_port_local,
+			  plink->u.osmotrx.base_port_remote,
+			  trx_clk_read_cb);
+	if (rc < 0) {
+		phy_link_state_set(plink, PHY_LINK_SHUTDOWN);
+		return -1;
+	}
+
+	/* open the individual instances with their ctrl+data sockets */
+	llist_for_each_entry(pinst, &plink->instances, list) {
+		pinst->u.osmotrx.hdl = l1if_open(pinst);
+		if (!pinst->u.osmotrx.hdl)
+			goto cleanup;
+	}
+
+	return 0;
+
+cleanup:
+	phy_link_state_set(plink, PHY_LINK_SHUTDOWN);
+	llist_for_each_entry(pinst, &plink->instances, list) {
+		if (pinst->u.osmotrx.hdl) {
+			trx_if_close(pinst->u.osmotrx.hdl);
+			pinst->u.osmotrx.hdl = NULL;
+		}
+	}
+	trx_udp_close(&plink->u.osmotrx.trx_ofd_clk);
+	return -1;
+}
+
+static uint16_t compute_port(struct phy_instance *pinst, int remote, int is_data)
+{
+	struct phy_link *plink = pinst->phy_link;
+	uint16_t inc = 1;
+
+	if (is_data)
+		inc = 2;
+
+	if (remote)
+		return plink->u.osmotrx.base_port_remote + (pinst->num << 1) + inc;
+	else
+		return plink->u.osmotrx.base_port_local + (pinst->num << 1) + inc;
+}
+
 int trx_if_open(struct trx_l1h *l1h)
 {
+	struct phy_instance *pinst = l1h->phy_inst;
+	struct phy_link *plink = pinst->phy_link;
 	int rc;
 
-	LOGP(DTRX, LOGL_NOTICE, "Open transceiver for trx=%u\n", l1h->trx->nr);
+	LOGP(DTRX, LOGL_NOTICE, "Open transceiver for %s\n",
+		phy_instance_name(pinst));
 
 	/* initialize ctrl queue */
 	INIT_LLIST_HEAD(&l1h->trx_ctrl_list);
 
 	/* open sockets */
-	if (l1h->trx->nr == 0) {
-		rc = trx_udp_open(l1h, &trx_ofd_clk, base_port_local,
-			trx_clk_read_cb);
-		if (rc < 0)
-			return rc;
-		LOGP(DTRX, LOGL_NOTICE, "Waiting for transceiver send clock\n");
-	}
 	rc = trx_udp_open(l1h, &l1h->trx_ofd_ctrl,
-		base_port_local + (l1h->trx->nr << 1) + 1, trx_ctrl_read_cb);
+			  plink->u.osmotrx.transceiver_ip,
+			  compute_port(pinst, 0, 0),
+			  compute_port(pinst, 1, 0), trx_ctrl_read_cb);
 	if (rc < 0)
 		goto err;
 	rc = trx_udp_open(l1h, &l1h->trx_ofd_data,
-		base_port_local + (l1h->trx->nr << 1) + 2, trx_data_read_cb);
+			  plink->u.osmotrx.transceiver_ip,
+			  compute_port(pinst, 0, 1),
+			  compute_port(pinst, 1, 1), trx_data_read_cb);
 	if (rc < 0)
 		goto err;
 
 	/* enable all slots */
 	l1h->config.slotmask = 0xff;
 
-	if (l1h->trx->nr == 0)
-		trx_if_cmd_poweroff(l1h);
+	/* FIXME: why was this only for TRX0 ? */
+	//if (l1h->trx->nr == 0)
+	trx_if_cmd_poweroff(l1h);
 
 	return 0;
 
@@ -548,13 +603,13 @@ void trx_if_flush(struct trx_l1h *l1h)
 
 void trx_if_close(struct trx_l1h *l1h)
 {
-	LOGP(DTRX, LOGL_NOTICE, "Close transceiver for trx=%u\n", l1h->trx->nr);
+	struct phy_instance *pinst = l1h->phy_inst;
+	LOGP(DTRX, LOGL_NOTICE, "Close transceiver for %s\n",
+		phy_instance_name(pinst));
 
 	trx_if_flush(l1h);
 
 	/* close sockets */
-	if (l1h->trx->nr == 0)
-		trx_udp_close(&trx_ofd_clk);
 	trx_udp_close(&l1h->trx_ofd_ctrl);
 	trx_udp_close(&l1h->trx_ofd_data);
 }
diff --git a/src/osmo-bts-trx/trx_if.h b/src/osmo-bts-trx/trx_if.h
index 3862e2b..1ea0da9 100644
--- a/src/osmo-bts-trx/trx_if.h
+++ b/src/osmo-bts-trx/trx_if.h
@@ -6,6 +6,7 @@ extern const char *transceiver_ip;
 extern int settsc_enabled;
 extern int setbsic_enabled;
 
+struct trx_l1h;
 
 struct trx_ctrl_msg {
 	struct llist_head	list;
diff --git a/src/osmo-bts-trx/trx_vty.c b/src/osmo-bts-trx/trx_vty.c
index a4a7909..d2feea4 100644
--- a/src/osmo-bts-trx/trx_vty.c
+++ b/src/osmo-bts-trx/trx_vty.c
@@ -45,6 +45,8 @@
 #include "trx_if.h"
 #include "loops.h"
 
+#define OSMOTRX_STR	"OsmoTRX Transceiver configuration\n"
+
 static struct gsm_bts *vty_bts;
 
 DEFUN(show_transceiver, show_transceiver_cmd, "show transceiver",
@@ -53,7 +55,6 @@ DEFUN(show_transceiver, show_transceiver_cmd, "show transceiver",
 	struct gsm_bts *bts = vty_bts;
 	struct gsm_bts_trx *trx;
 	struct trx_l1h *l1h;
-	uint8_t tn;
 
 	if (!transceiver_available) {
 		vty_out(vty, "transceiver is not connected%s", VTY_NEWLINE);
@@ -63,7 +64,8 @@ DEFUN(show_transceiver, show_transceiver_cmd, "show transceiver",
 	}
 
 	llist_for_each_entry(trx, &bts->trx_list, list) {
-		l1h = trx_l1h_hdl(trx);
+		struct phy_instance *pinst = trx_phy_instance(trx);
+		l1h = pinst->u.osmotrx.hdl;
 		vty_out(vty, "TRX %d%s", trx->nr, VTY_NEWLINE);
 		vty_out(vty, " %s%s",
 			(l1h->config.poweron) ? "poweron":"poweroff",
@@ -85,56 +87,70 @@ DEFUN(show_transceiver, show_transceiver_cmd, "show transceiver",
 				VTY_NEWLINE);
 		else
 			vty_out(vty, " bisc   : undefined%s", VTY_NEWLINE);
-		if (l1h->config.rxgain_valid)
-			vty_out(vty, " rxgain : %d%s", l1h->config.rxgain,
+	}
+
+	return CMD_SUCCESS;
+}
+
+
+static void show_phy_inst_single(struct vty *vty, struct phy_instance *pinst)
+{
+	uint8_t tn;
+	struct trx_l1h *l1h = pinst->u.osmotrx.hdl;
+
+	vty_out(vty, "PHY Instance %s%s",
+		phy_instance_name(pinst), VTY_NEWLINE);
+	if (l1h->config.maxdly_valid)
+		vty_out(vty, " maxdly : %d%s", l1h->config.maxdly,
+			VTY_NEWLINE);
+	else
+		vty_out(vty, " maxdly : undefined%s", VTY_NEWLINE);
+	for (tn = 0; tn < TRX_NR_TS; tn++) {
+		if (!((1 << tn) & l1h->config.slotmask))
+			vty_out(vty, " slot #%d: unsupported%s", tn,
 				VTY_NEWLINE);
-		else
-			vty_out(vty, " rxgain : undefined%s", VTY_NEWLINE);
-		if (l1h->config.power_valid)
-			vty_out(vty, " power  : %d%s", l1h->config.power,
+		else if (l1h->config.slottype_valid[tn])
+			vty_out(vty, " slot #%d: type %d%s", tn,
+				l1h->config.slottype[tn],
 				VTY_NEWLINE);
 		else
-			vty_out(vty, " power  : undefined%s", VTY_NEWLINE);
-		if (l1h->config.maxdly_valid)
-			vty_out(vty, " maxdly : %d%s", l1h->config.maxdly,
+			vty_out(vty, " slot #%d: undefined%s", tn,
 				VTY_NEWLINE);
-		else
-			vty_out(vty, " maxdly : undefined%s", VTY_NEWLINE);
-		for (tn = 0; tn < TRX_NR_TS; tn++) {
-			if (!((1 << tn) & l1h->config.slotmask))
-				vty_out(vty, " slot #%d: unsupported%s", tn,
-					VTY_NEWLINE);
-			else if (l1h->config.slottype_valid[tn])
-				vty_out(vty, " slot #%d: type %d%s", tn,
-					l1h->config.slottype[tn],
-					VTY_NEWLINE);
-			else
-				vty_out(vty, " slot #%d: undefined%s", tn,
-					VTY_NEWLINE);
-		}
 	}
-
-	return CMD_SUCCESS;
 }
 
-DEFUN(cfg_bts_fn_advance, cfg_bts_fn_advance_cmd,
-	"fn-advance <0-30>",
-	"Set the number of frames to be transmitted to transceiver in advance "
-	"of current FN\n"
-	"Advance in frames\n")
+static void show_phy_single(struct vty *vty, struct phy_link *plink)
 {
-	trx_clock_advance = atoi(argv[0]);
+	struct phy_instance *pinst;
 
-	return CMD_SUCCESS;
+	vty_out(vty, "PHY %u%s", plink->num, VTY_NEWLINE);
+
+	if (plink->u.osmotrx.rxgain_valid)
+		vty_out(vty, " rx-gain        : %d dB%s",
+			plink->u.osmotrx.rxgain, VTY_NEWLINE);
+	else
+		vty_out(vty, " rx-gain        : undefined%s", VTY_NEWLINE);
+	if (plink->u.osmotrx.power_valid)
+		vty_out(vty, " tx-attenuation : %d dB%s",
+			plink->u.osmotrx.power, VTY_NEWLINE);
+	else
+		vty_out(vty, " tx-attenuation : undefined%s", VTY_NEWLINE);
+
+	llist_for_each_entry(pinst, &plink->instances, list)
+		show_phy_inst_single(vty, pinst);
 }
 
-DEFUN(cfg_bts_rts_advance, cfg_bts_rts_advance_cmd,
-	"rts-advance <0-30>",
-	"Set the number of frames to be requested (PCU) in advance of current "
-	"FN. Do not change this, unless you have a good reason!\n"
-	"Advance in frames\n")
+DEFUN(show_phy, show_phy_cmd, "show phy",
+	SHOW_STR  "Display information about the available PHYs")
 {
-	trx_rts_advance = atoi(argv[0]);
+	int i;
+
+	for (i = 0; i < 255; i++) {
+		struct phy_link *plink = phy_link_by_num(i);
+		if (!plink)
+			break;
+		show_phy_single(vty, plink);
+	}
 
 	return CMD_SUCCESS;
 }
@@ -220,63 +236,14 @@ DEFUN(cfg_bts_no_setbsic, cfg_bts_no_setbsic_cmd,
 	return CMD_SUCCESS;
 }
 
-DEFUN(cfg_trx_rxgain, cfg_trx_rxgain_cmd,
-	"rxgain <0-50>",
-	"Set the receiver gain in dB\n"
-	"Gain in dB\n")
-{
-	struct gsm_bts_trx *trx = vty->index;
-	struct trx_l1h *l1h = trx_l1h_hdl(trx);
 
-	l1h->config.rxgain = atoi(argv[0]);
-	l1h->config.rxgain_valid = 1;
-	l1h->config.rxgain_sent = 0;
-	l1if_provision_transceiver_trx(l1h);
-
-	return CMD_SUCCESS;
-}
-
-DEFUN(cfg_trx_power, cfg_trx_power_cmd,
-	"power <0-50>",
-	"Set the transmitter power dampening\n"
-	"Power dampening in dB\n")
-{
-	struct gsm_bts_trx *trx = vty->index;
-	struct trx_l1h *l1h = trx_l1h_hdl(trx);
-
-	l1h->config.power = atoi(argv[0]);
-	l1h->config.power_oml = 0;
-	l1h->config.power_valid = 1;
-	l1h->config.power_sent = 0;
-	l1if_provision_transceiver_trx(l1h);
-
-	return CMD_SUCCESS;
-}
-
-DEFUN(cfg_trx_poweroml_, cfg_trx_power_oml_cmd,
-	"power oml",
-	"Set the transmitter power dampening\n"
-	"Given by NM_ATT_RF_MAXPOWR_R (max power reduction) via OML\n")
-{
-	struct gsm_bts_trx *trx = vty->index;
-	struct trx_l1h *l1h = trx_l1h_hdl(trx);
-
-	l1h->config.power = trx->max_power_red;
-	l1h->config.power_oml = 1;
-	l1h->config.power_valid = 1;
-	l1h->config.power_sent = 0;
-	l1if_provision_transceiver_trx(l1h);
-
-	return CMD_SUCCESS;
-}
-
-DEFUN(cfg_trx_maxdly, cfg_trx_maxdly_cmd,
-	"maxdly <0-31>",
+DEFUN(cfg_phyinst_maxdly, cfg_phyinst_maxdly_cmd,
+	"osmotrx maxdly <0-31>",
 	"Set the maximum delay of GSM symbols\n"
 	"GSM symbols (approx. 1.1km per symbol)\n")
 {
-	struct gsm_bts_trx *trx = vty->index;
-	struct trx_l1h *l1h = trx_l1h_hdl(trx);
+	struct phy_instance *pinst = vty->index;
+	struct trx_l1h *l1h = pinst->u.osmotrx.hdl;
 
 	l1h->config.maxdly = atoi(argv[0]);
 	l1h->config.maxdly_valid = 1;
@@ -286,7 +253,7 @@ DEFUN(cfg_trx_maxdly, cfg_trx_maxdly_cmd,
 	return CMD_SUCCESS;
 }
 
-DEFUN(cfg_trx_slotmask, cfg_trx_slotmask_cmd,
+DEFUN(cfg_phyinst_slotmask, cfg_phyinst_slotmask_cmd,
 	"slotmask (1|0) (1|0) (1|0) (1|0) (1|0) (1|0) (1|0) (1|0)",
 	"Set the supported slots\n"
 	"TS0 supported\nTS0 unsupported\nTS1 supported\nTS1 unsupported\n"
@@ -294,8 +261,8 @@ DEFUN(cfg_trx_slotmask, cfg_trx_slotmask_cmd,
 	"TS4 supported\nTS4 unsupported\nTS5 supported\nTS5 unsupported\n"
 	"TS6 supported\nTS6 unsupported\nTS7 supported\nTS7 unsupported\n")
 {
-	struct gsm_bts_trx *trx = vty->index;
-	struct trx_l1h *l1h = trx_l1h_hdl(trx);
+	struct phy_instance *pinst = vty->index;
+	struct trx_l1h *l1h = pinst->u.osmotrx.hdl;
 	uint8_t tn;
 
 	l1h->config.slotmask = 0;
@@ -306,76 +273,171 @@ DEFUN(cfg_trx_slotmask, cfg_trx_slotmask_cmd,
 	return CMD_SUCCESS;
 }
 
-DEFUN(cfg_trx_no_rxgain, cfg_trx_no_rxgain_cmd,
-	"no rxgain <0-50>",
-	NO_STR "Unset the receiver gain in dB\n"
+
+DEFUN(cfg_phy_fn_advance, cfg_phy_fn_advance_cmd,
+	"osmotrx fn-advance <0-30>",
+	OSMOTRX_STR
+	"Set the number of frames to be transmitted to transceiver in advance "
+	"of current FN\n"
+	"Advance in frames\n")
+{
+	struct phy_link *plink = vty->index;
+
+	plink->u.osmotrx.clock_advance = atoi(argv[0]);
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_phy_rts_advance, cfg_phy_rts_advance_cmd,
+	"osmotrx rts-advance <0-30>",
+	OSMOTRX_STR
+	"Set the number of frames to be requested (PCU) in advance of current "
+	"FN. Do not change this, unless you have a good reason!\n"
+	"Advance in frames\n")
+{
+	struct phy_link *plink = vty->index;
+
+	plink->u.osmotrx.rts_advance = atoi(argv[0]);
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_phy_rxgain, cfg_phy_rxgain_cmd,
+	"osmotrx rx-gain <0-50>",
+	OSMOTRX_STR
+	"Set the receiver gain in dB\n"
 	"Gain in dB\n")
 {
-	struct gsm_bts_trx *trx = vty->index;
-	struct trx_l1h *l1h = trx_l1h_hdl(trx);
+	struct phy_link *plink = vty->index;
 
-	l1h->config.rxgain_valid = 0;
+	plink->u.osmotrx.rxgain = atoi(argv[0]);
+	plink->u.osmotrx.rxgain_valid = 1;
+	plink->u.osmotrx.rxgain_sent = 0;
 
 	return CMD_SUCCESS;
 }
 
-DEFUN(cfg_trx_no_power, cfg_trx_no_power_cmd,
-	"no power <0-50>",
-	NO_STR "Unset the transmitter power dampening\n"
-	"Power dampening in dB\n")
+DEFUN(cfg_phy_tx_atten, cfg_phy_tx_atten_cmd,
+	"osmotrx tx-attenuation <0-50>",
+	OSMOTRX_STR
+	"Set the transmitter attenuation\n"
+	"Fixed attenuation in dB, overriding OML\n")
 {
-	struct gsm_bts_trx *trx = vty->index;
-	struct trx_l1h *l1h = trx_l1h_hdl(trx);
+	struct phy_link *plink = vty->index;
 
-	l1h->config.power_valid = 0;
+	plink->u.osmotrx.power = atoi(argv[0]);
+	plink->u.osmotrx.power_oml = 0;
+	plink->u.osmotrx.power_valid = 1;
+	plink->u.osmotrx.power_sent = 0;
 
 	return CMD_SUCCESS;
 }
 
-DEFUN(cfg_trx_no_maxdly, cfg_trx_no_maxdly_cmd,
-	"no maxdly <0-31>",
-	NO_STR "Unset the maximum delay of GSM symbols\n"
-	"GSM symbols (approx. 1.1km per symbol)\n")
+DEFUN(cfg_phy_tx_atten_oml, cfg_phy_tx_atten_oml_cmd,
+	"osmotrx tx-attenuation oml",
+	OSMOTRX_STR
+	"Set the transmitter attenuation\n"
+	"Use NM_ATT_RF_MAXPOWR_R (max power reduction) from BSC via OML\n")
+{
+	struct phy_link *plink = vty->index;
+
+	plink->u.osmotrx.power_oml = 1;
+	plink->u.osmotrx.power_valid = 1;
+	plink->u.osmotrx.power_sent = 0;
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_phy_no_rxgain, cfg_phy_no_rxgain_cmd,
+	"no osmotrx rx-gain",
+	NO_STR OSMOTRX_STR "Unset the receiver gain in dB\n")
+{
+	struct phy_link *plink = vty->index;
+
+	plink->u.osmotrx.rxgain_valid = 0;
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_phy_no_tx_atten, cfg_phy_no_tx_atten_cmd,
+	"no osmotrx tx-attenuation",
+	NO_STR OSMOTRX_STR "Unset the transmitter attenuation\n")
 {
-	struct gsm_bts_trx *trx = vty->index;
-	struct trx_l1h *l1h = trx_l1h_hdl(trx);
+	struct phy_link *plink = vty->index;
+
+	plink->u.osmotrx.power_valid = 0;
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_phyinst_no_maxdly, cfg_phyinst_no_maxdly_cmd,
+	"no osmotrx maxdly",
+	NO_STR "Unset the maximum delay of GSM symbols\n")
+{
+	struct phy_instance *pinst = vty->index;
+	struct trx_l1h *l1h = pinst->u.osmotrx.hdl;
 
 	l1h->config.maxdly_valid = 0;
 
 	return CMD_SUCCESS;
 }
 
-void bts_model_config_write_bts(struct vty *vty, struct gsm_bts *bts)
+DEFUN(cfg_phy_transc_ip, cfg_phy_transc_ip_cmd,
+	"osmotrx ip HOST",
+	OSMOTRX_STR
+	"Set remote IP address\n"
+	"IP address of OsmoTRX\n")
 {
-	vty_out(vty, " fn-advance %d%s", trx_clock_advance, VTY_NEWLINE);
-	vty_out(vty, " rts-advance %d%s", trx_rts_advance, VTY_NEWLINE);
+	struct phy_link *plink = vty->index;
 
-	if (trx_ms_power_loop)
-		vty_out(vty, " ms-power-loop %d%s", trx_target_rssi,
-			VTY_NEWLINE);
-	else
-		vty_out(vty, " no ms-power-loop%s", VTY_NEWLINE);
-	vty_out(vty, " %stiming-advance-loop%s", (trx_ta_loop) ? "":"no ",
-		VTY_NEWLINE);
-	if (settsc_enabled)
-		vty_out(vty, " settsc%s", VTY_NEWLINE);
-	if (setbsic_enabled)
-		vty_out(vty, " setbsic%s", VTY_NEWLINE);
+	if (plink->u.osmotrx.transceiver_ip)
+		talloc_free(plink->u.osmotrx.transceiver_ip);
+	plink->u.osmotrx.transceiver_ip = talloc_strdup(plink, argv[0]);
+
+	return CMD_SUCCESS;
 }
 
-void bts_model_config_write_trx(struct vty *vty, struct gsm_bts_trx *trx)
+DEFUN(cfg_phy_base_port, cfg_phy_base_port_cmd,
+	"osmotrx base-port (local|remote) <0-65535>",
+	OSMOTRX_STR "Set base UDP port number\n" "Local UDP port\n"
+	"Remote UDP port\n" "UDP base port number\n")
 {
-	struct trx_l1h *l1h = trx_l1h_hdl(trx);
+	struct phy_link *plink = vty->index;
+
+	if (!strcmp(argv[0], "local"))
+		plink->u.osmotrx.base_port_local = atoi(argv[1]);
+	else
+		plink->u.osmotrx.base_port_remote = atoi(argv[1]);
+
+	return CMD_SUCCESS;
+}
 
-	if (l1h->config.rxgain_valid)
-		vty_out(vty, "  rxgain %d%s", l1h->config.rxgain, VTY_NEWLINE);
-	if (l1h->config.power_valid) {
-		if (l1h->config.power_oml)
-			vty_out(vty, "  power oml%s", VTY_NEWLINE);
+void bts_model_config_write_phy(struct vty *vty, struct phy_link *plink)
+{
+	if (plink->u.osmotrx.transceiver_ip)
+		vty_out(vty, " osmotrx ip %s%s",
+			plink->u.osmotrx.transceiver_ip, VTY_NEWLINE);
+
+	vty_out(vty, " osmotrx fn-advance %d%s",
+		plink->u.osmotrx.clock_advance, VTY_NEWLINE);
+	vty_out(vty, " osmotrx rts-advance %d%s",
+		plink->u.osmotrx.rts_advance, VTY_NEWLINE);
+	if (plink->u.osmotrx.rxgain_valid)
+		vty_out(vty, " osmotrx rx-gain %d%s",
+			plink->u.osmotrx.rxgain, VTY_NEWLINE);
+	if (plink->u.osmotrx.power_valid) {
+		if (plink->u.osmotrx.power_oml)
+			vty_out(vty, " osmotrx tx-attenuation oml%s", VTY_NEWLINE);
 		else
-			vty_out(vty, "  power %d%s", l1h->config.power,
-				VTY_NEWLINE);
+			vty_out(vty, " osmotrx tx-attenuation %d%s",
+				plink->u.osmotrx.power, VTY_NEWLINE);
 	}
+}
+
+void bts_model_config_write_phy_inst(struct vty *vty, struct phy_instance *pinst)
+{
+	struct trx_l1h *l1h = pinst->u.osmotrx.hdl;
+
 	if (l1h->config.maxdly_valid)
 		vty_out(vty, "  maxdly %d%s", l1h->config.maxdly, VTY_NEWLINE);
 	if (l1h->config.slotmask != 0xff)
@@ -391,14 +453,32 @@ void bts_model_config_write_trx(struct vty *vty, struct gsm_bts_trx *trx)
 			VTY_NEWLINE);
 }
 
+void bts_model_config_write_bts(struct vty *vty, struct gsm_bts *bts)
+{
+	if (trx_ms_power_loop)
+		vty_out(vty, " ms-power-loop %d%s", trx_target_rssi,
+			VTY_NEWLINE);
+	else
+		vty_out(vty, " no ms-power-loop%s", VTY_NEWLINE);
+	vty_out(vty, " %stiming-advance-loop%s", (trx_ta_loop) ? "":"no ",
+		VTY_NEWLINE);
+	if (settsc_enabled)
+		vty_out(vty, " settsc%s", VTY_NEWLINE);
+	if (setbsic_enabled)
+		vty_out(vty, " setbsic%s", VTY_NEWLINE);
+}
+
+void bts_model_config_write_trx(struct vty *vty, struct gsm_bts_trx *trx)
+{
+}
+
 int bts_model_vty_init(struct gsm_bts *bts)
 {
 	vty_bts = bts;
 
 	install_element_ve(&show_transceiver_cmd);
+	install_element_ve(&show_phy_cmd);
 
-	install_element(BTS_NODE, &cfg_bts_fn_advance_cmd);
-	install_element(BTS_NODE, &cfg_bts_rts_advance_cmd);
 	install_element(BTS_NODE, &cfg_bts_ms_power_loop_cmd);
 	install_element(BTS_NODE, &cfg_bts_no_ms_power_loop_cmd);
 	install_element(BTS_NODE, &cfg_bts_timing_advance_loop_cmd);
@@ -408,14 +488,19 @@ int bts_model_vty_init(struct gsm_bts *bts)
 	install_element(BTS_NODE, &cfg_bts_no_settsc_cmd);
 	install_element(BTS_NODE, &cfg_bts_no_setbsic_cmd);
 
-	install_element(TRX_NODE, &cfg_trx_rxgain_cmd);
-	install_element(TRX_NODE, &cfg_trx_power_cmd);
-	install_element(TRX_NODE, &cfg_trx_power_oml_cmd);
-	install_element(TRX_NODE, &cfg_trx_maxdly_cmd);
-	install_element(TRX_NODE, &cfg_trx_slotmask_cmd);
-	install_element(TRX_NODE, &cfg_trx_no_rxgain_cmd);
-	install_element(TRX_NODE, &cfg_trx_no_power_cmd);
-	install_element(TRX_NODE, &cfg_trx_no_maxdly_cmd);
+	install_element(PHY_NODE, &cfg_phy_base_port_cmd);
+	install_element(PHY_NODE, &cfg_phy_fn_advance_cmd);
+	install_element(PHY_NODE, &cfg_phy_rts_advance_cmd);
+	install_element(PHY_NODE, &cfg_phy_transc_ip_cmd);
+	install_element(PHY_NODE, &cfg_phy_rxgain_cmd);
+	install_element(PHY_NODE, &cfg_phy_tx_atten_cmd);
+	install_element(PHY_NODE, &cfg_phy_tx_atten_oml_cmd);
+	install_element(PHY_NODE, &cfg_phy_no_rxgain_cmd);
+	install_element(PHY_NODE, &cfg_phy_no_tx_atten_cmd);
+
+	install_element(PHY_INST_NODE, &cfg_phyinst_slotmask_cmd);
+	install_element(PHY_INST_NODE, &cfg_phyinst_maxdly_cmd);
+	install_element(PHY_INST_NODE, &cfg_phyinst_no_maxdly_cmd);
 
 	return 0;
 }
-- 
2.7.0




More information about the OpenBSC mailing list