From db496df1e6e9068f993db7fa31fd659803391084 Mon Sep 17 00:00:00 2001 From: Andre Puschmann Date: Thu, 8 Apr 2021 15:52:04 +0200 Subject: [PATCH] mac_nr: add support for periodic BSR reporting building on the previous refactor this patch now adds support for peridoic BSR reporting (using short BSR). It furthermore does the following changes: * add BSR packing * add proc_bsr_nr unit test * move mac_nr test code into test folder under src (needs to be done with other test code too) --- lib/include/srsran/mac/mac_sch_pdu_nr.h | 4 +- lib/src/mac/mac_sch_pdu_nr.cc | 11 + srsue/hdr/stack/mac/proc_bsr.h | 2 + srsue/hdr/stack/mac_common/mac_common.h | 49 +++ srsue/hdr/stack/mac_nr/mac_nr.h | 10 + srsue/hdr/stack/mac_nr/mac_nr_interfaces.h | 4 + srsue/hdr/stack/mac_nr/mux_nr.h | 6 +- srsue/hdr/stack/mac_nr/proc_bsr_nr.h | 43 +-- srsue/src/stack/mac_nr/CMakeLists.txt | 4 +- srsue/src/stack/mac_nr/mac_nr.cc | 33 ++ srsue/src/stack/mac_nr/mux_nr.cc | 19 +- srsue/src/stack/mac_nr/proc_bsr_nr.cc | 292 ++++++++++-------- .../stack/mac_nr/test}/CMakeLists.txt | 4 + .../stack/mac_nr/test}/mac_nr_test.cc | 159 +++++++++- .../src/stack/mac_nr/test/proc_bsr_nr_test.cc | 93 ++++++ .../stack/mac_nr/test}/proc_ra_nr_test.cc | 0 srsue/test/CMakeLists.txt | 1 - 17 files changed, 564 insertions(+), 170 deletions(-) rename srsue/{test/mac_nr => src/stack/mac_nr/test}/CMakeLists.txt (70%) rename srsue/{test/mac_nr => src/stack/mac_nr/test}/mac_nr_test.cc (61%) create mode 100644 srsue/src/stack/mac_nr/test/proc_bsr_nr_test.cc rename srsue/{test/mac_nr => src/stack/mac_nr/test}/proc_ra_nr_test.cc (100%) diff --git a/lib/include/srsran/mac/mac_sch_pdu_nr.h b/lib/include/srsran/mac/mac_sch_pdu_nr.h index c7884b6cc..abbd979a5 100644 --- a/lib/include/srsran/mac/mac_sch_pdu_nr.h +++ b/lib/include/srsran/mac/mac_sch_pdu_nr.h @@ -92,6 +92,7 @@ public: void set_c_rnti(const uint16_t crnti_); void set_se_phr(const uint8_t phr_, const uint8_t pcmax_); void set_sbsr(const lcg_bsr_t bsr_); + void set_lbsr(const std::array bsr_); uint32_t write_subpdu(const uint8_t* start_); @@ -127,11 +128,12 @@ public: void init_rx(bool ulsch_ = false); // Add SDU or CEs to PDU - // All functions will return SRSRAN_SUCCESS on success, and SRSLE_ERROR otherwise + // All functions will return SRSRAN_SUCCESS on success, and SRSRAN_ERROR otherwise uint32_t add_sdu(const uint32_t lcid_, const uint8_t* payload_, const uint32_t len_); uint32_t add_crnti_ce(const uint16_t crnti_); uint32_t add_se_phr_ce(const uint8_t phr_, const uint8_t pcmax_); uint32_t add_sbsr_ce(const mac_sch_subpdu_nr::lcg_bsr_t bsr_); + uint32_t add_lbsr_ce(const std::array bsr_); uint32_t get_remaing_len(); diff --git a/lib/src/mac/mac_sch_pdu_nr.cc b/lib/src/mac/mac_sch_pdu_nr.cc index 65db755d2..0d7c1888c 100644 --- a/lib/src/mac/mac_sch_pdu_nr.cc +++ b/lib/src/mac/mac_sch_pdu_nr.cc @@ -136,6 +136,9 @@ void mac_sch_subpdu_nr::set_sbsr(const lcg_bsr_t bsr_) ce_write_buffer.at(0) = ((bsr_.lcg_id & 0x07) << 5) | (bsr_.buffer_size & 0x1f); } +// Turn a subPDU into a long BSR with variable size +void mac_sch_subpdu_nr::set_lbsr(const std::array bsr_) {} + // Section 6.1.2 uint32_t mac_sch_subpdu_nr::write_subpdu(const uint8_t* start_) { @@ -400,6 +403,14 @@ uint32_t mac_sch_pdu_nr::add_sbsr_ce(const mac_sch_subpdu_nr::lcg_bsr_t bsr_) return add_sudpdu(ce); } +uint32_t +mac_sch_pdu_nr::add_lbsr_ce(const std::array bsr_) +{ + mac_sch_subpdu_nr ce(this); + ce.set_lbsr(bsr_); + return add_sudpdu(ce); +} + uint32_t mac_sch_pdu_nr::add_sudpdu(mac_sch_subpdu_nr& subpdu) { uint32_t subpdu_len = subpdu.get_total_length(); diff --git a/srsue/hdr/stack/mac/proc_bsr.h b/srsue/hdr/stack/mac/proc_bsr.h index 8fa99505f..678741763 100644 --- a/srsue/hdr/stack/mac/proc_bsr.h +++ b/srsue/hdr/stack/mac/proc_bsr.h @@ -88,6 +88,8 @@ private: std::map lcgs[NOF_LCG]; // groups LCID in LCG + mac_buffer_states_t old_buffer_state; + uint32_t find_max_priority_lcg_with_data(); bsr_trigger_type_t triggered_bsr_type = NONE; diff --git a/srsue/hdr/stack/mac_common/mac_common.h b/srsue/hdr/stack/mac_common/mac_common.h index 42cd47af7..ee245cde3 100644 --- a/srsue/hdr/stack/mac_common/mac_common.h +++ b/srsue/hdr/stack/mac_common/mac_common.h @@ -13,6 +13,10 @@ #ifndef SRSUE_MAC_COMMON_H #define SRSUE_MAC_COMMON_H +#include "srsran/common/string_helpers.h" +#include "srsran/srslog/srslog.h" +#include + /** * @brief Common definitions/interfaces between LTE/NR MAC components * @@ -25,6 +29,51 @@ namespace srsue { typedef enum { NONE, REGULAR, PADDING, PERIODIC } bsr_trigger_type_t; char* bsr_trigger_type_tostring(bsr_trigger_type_t type); +/// Helper class to store a snapshot of buffer states for all LCGs/LCIDs +class mac_buffer_states_t +{ +public: + explicit mac_buffer_states_t() {} + void reset() + { + nof_lcids_with_data = 0; + nof_lcgs_with_data = 0; + last_non_zero_lcg = -1; + lcid_buffer_size.clear(); + lcg_buffer_size.clear(); + } + std::string to_string() + { + fmt::memory_buffer buffer; + + fmt::format_to(buffer, + "nof_lcids_with_data={}, nof_lcgs_with_data={}, last_non_zero_lcg={} ", + nof_lcids_with_data, + nof_lcgs_with_data, + last_non_zero_lcg); + + fmt::format_to(buffer, "["); + for (const auto& lcg : lcg_buffer_size) { + fmt::format_to(buffer, "lcg{}={}, ", lcg.first, lcg.second); + } + fmt::format_to(buffer, "] "); + + fmt::format_to(buffer, "["); + for (const auto& lcid : lcid_buffer_size) { + fmt::format_to(buffer, "lcid{}={}, ", lcid.first, lcid.second); + } + fmt::format_to(buffer, "]"); + + return srsran::to_c_str(buffer); + } + + std::map lcid_buffer_size; // Buffer size entry for each LCID + std::map lcg_buffer_size; // Entry for each LCG (sum of LCIDs of that LCG) + uint32_t nof_lcids_with_data = 0; // Is incremented when a LCID is found with data to transmit + uint32_t nof_lcgs_with_data = 0; // Is incremented when a LCG is found with data to transmit + int32_t last_non_zero_lcg = -1; // only valid if nof_lcgs_with_data is at least one +}; + } // namespace srsue #endif // SRSUE_MAC_COMMON_H diff --git a/srsue/hdr/stack/mac_nr/mac_nr.h b/srsue/hdr/stack/mac_nr/mac_nr.h index 079a3e383..f7cc798da 100644 --- a/srsue/hdr/stack/mac_nr/mac_nr.h +++ b/srsue/hdr/stack/mac_nr/mac_nr.h @@ -22,6 +22,7 @@ #include "srsran/interfaces/mac_interface_types.h" #include "srsran/interfaces/ue_nr_interfaces.h" #include "srsran/srslog/srslog.h" +#include "srsue/hdr/stack/mac_common/mac_common.h" #include "srsue/hdr/stack/mac_nr/mux_nr.h" #include "srsue/hdr/stack/ue_stack_base.h" @@ -81,6 +82,9 @@ public: uint64_t get_contention_id(); uint16_t get_crnti(); + /// Interface for MUX + srsran::mac_sch_subpdu_nr::lcg_bsr_t generate_sbsr(); + void msg3_flush() { mux.msg3_flush(); } bool msg3_is_transmitted() { return mux.msg3_is_transmitted(); } void msg3_prepare() { mux.msg3_prepare(); } @@ -109,6 +113,12 @@ private: bool has_crnti(); bool is_valid_crnti(const uint16_t crnti); + std::vector logical_channels; // stores the raw configs provide by upper layers + + /// LCID and LCG related members and helper functions + void update_buffer_states(); + mac_buffer_states_t mac_buffer_states; + /// Interaction with rest of the stack phy_interface_mac_nr* phy = nullptr; rlc_interface_mac* rlc = nullptr; diff --git a/srsue/hdr/stack/mac_nr/mac_nr_interfaces.h b/srsue/hdr/stack/mac_nr/mac_nr_interfaces.h index 4e12b6682..a4c673157 100644 --- a/srsue/hdr/stack/mac_nr/mac_nr_interfaces.h +++ b/srsue/hdr/stack/mac_nr/mac_nr_interfaces.h @@ -14,6 +14,7 @@ #define SRSUE_MAC_NR_INTERFACES_H #include "srsran/common/interfaces_common.h" +#include "srsran/mac/mac_sch_pdu_nr.h" namespace srsue { /** @@ -45,6 +46,9 @@ class mac_interface_mux_nr public: // MUX can query MAC for current C-RNTI for Msg3 transmission virtual uint16_t get_crnti() = 0; + + // MUX queries MAC to return LCG state for SBSR + virtual srsran::mac_sch_subpdu_nr::lcg_bsr_t generate_sbsr() = 0; }; } // namespace srsue diff --git a/srsue/hdr/stack/mac_nr/mux_nr.h b/srsue/hdr/stack/mac_nr/mux_nr.h index 5a6c0c0d1..d3cd2bd33 100644 --- a/srsue/hdr/stack/mac_nr/mux_nr.h +++ b/srsue/hdr/stack/mac_nr/mux_nr.h @@ -33,8 +33,6 @@ public: void reset(); int32_t init(rlc_interface_mac* rlc_); - void step(); - void msg3_flush(); void msg3_prepare(); void msg3_transmitted(); @@ -49,7 +47,7 @@ public: srsran::unique_byte_buffer_t get_pdu(uint32_t max_pdu_len); // Interface for BSR procedure - void generate_bsr_mac_ce(); + void generate_bsr_mac_ce(const bsr_interface_mux_nr::bsr_format_nr_t& format); private: // internal helper methods @@ -71,6 +69,8 @@ private: srsran::mac_sch_pdu_nr tx_pdu; + enum { no_bsr, sbsr_ce, lbsr_ce } add_bsr_ce = no_bsr; /// BSR procedure requests MUX to add a BSR CE + // Mutex for exclusive access std::mutex mutex; }; diff --git a/srsue/hdr/stack/mac_nr/proc_bsr_nr.h b/srsue/hdr/stack/mac_nr/proc_bsr_nr.h index 3ac29661c..162a661f6 100644 --- a/srsue/hdr/stack/mac_nr/proc_bsr_nr.h +++ b/srsue/hdr/stack/mac_nr/proc_bsr_nr.h @@ -18,6 +18,7 @@ #include "proc_sr_nr.h" #include "srsran/common/task_scheduler.h" +#include "srsran/mac/mac_sch_pdu_nr.h" #include "srsran/srslog/srslog.h" #include "srsue/hdr/stack/mac_common/mac_common.h" @@ -34,21 +35,15 @@ public: // TS 38.321 Sec 6.1.3.1 typedef enum { SHORT_BSR, LONG_BSR, SHORT_TRUNC_BSR, LONG_TRUNC_BSR } bsr_format_nr_t; - // FIXME: this will be replaced - typedef struct { - bsr_format_nr_t format; - uint32_t buff_size[4]; - } bsr_t; - - /// MUX calls BSR to let it generate a padding BSR if there is space in PDU. - virtual bool generate_padding_bsr(uint32_t nof_padding_bytes, bsr_t* bsr) = 0; + /// MUX calls BSR to receive the buffer state of a single LCG. + virtual srsran::mac_sch_subpdu_nr::lcg_bsr_t generate_sbsr() = 0; }; class mux_interface_bsr_nr { public: /// Inform MUX unit to that a BSR needs to be generated in the next UL transmission. - virtual void generate_bsr_mac_ce() = 0; + virtual void generate_bsr_mac_ce(const bsr_interface_mux_nr::bsr_format_nr_t& format) = 0; }; /** @@ -59,25 +54,24 @@ public: class proc_bsr_nr : public srsran::timer_callback, public bsr_interface_mux_nr { public: - explicit proc_bsr_nr(srslog::basic_logger& logger) : logger(logger) {} + explicit proc_bsr_nr(srslog::basic_logger& logger_) : logger(logger_) {} int init(proc_sr_nr* sr_proc, mux_interface_bsr_nr* mux_, rlc_interface_mac* rlc, srsran::ext_task_sched_handle* task_sched_); - void step(uint32_t tti); + void step(uint32_t tti, const mac_buffer_states_t& new_buffer_state); void reset(); int set_config(const srsran::bsr_cfg_nr_t& bsr_cfg); int setup_lcid(uint32_t lcid, uint32_t lcg, uint32_t priority); void timer_expired(uint32_t timer_id); - uint32_t get_buffer_state(); /// Called by MAC when an UL grant is received void new_grant_ul(uint32_t grant_size); - // bool need_to_send_bsr(); - bool generate_padding_bsr(uint32_t nof_padding_bytes, bsr_t* bsr); - void update_bsr_tti_end(const bsr_t* bsr); + /// MUX interface for BSR generation + srsran::mac_sch_subpdu_nr::lcg_bsr_t generate_sbsr(); + bool generate_padding_bsr(uint32_t nof_padding_bytes); private: const static int QUEUE_STATUS_PERIOD_MS = 1000; @@ -94,27 +88,20 @@ private: bool initiated = false; - const static int MAX_NOF_LCG = 8; - - typedef struct { - int priority; - uint32_t old_buffer; - uint32_t new_buffer; - } lcid_t; + mac_buffer_states_t buffer_state; - std::map lcgs[MAX_NOF_LCG]; // groups LCID in LCG + // map of LCGs and their priorities, key is the priority (sorted) and the value the LCG + std::map lcg_priorities; bsr_trigger_type_t triggered_bsr_type = NONE; void print_state(); void set_trigger(bsr_trigger_type_t new_trigger); - void update_new_data(); - void update_old_buffer(); bool check_highest_channel(); - bool check_new_data(); + bool check_new_data(const mac_buffer_states_t& new_buffer_state); bool check_any_channel(); - uint32_t get_buffer_state_lcg(uint32_t lcg); - bool generate_bsr(bsr_t* bsr, uint32_t nof_padding_bytes); + + uint8_t buff_size_bytes_to_field(uint32_t buffer_size, bsr_format_nr_t format); uint32_t find_max_priority_lcg_with_data(); diff --git a/srsue/src/stack/mac_nr/CMakeLists.txt b/srsue/src/stack/mac_nr/CMakeLists.txt index 70f32152e..e2fc5a822 100644 --- a/srsue/src/stack/mac_nr/CMakeLists.txt +++ b/srsue/src/stack/mac_nr/CMakeLists.txt @@ -8,4 +8,6 @@ set(SOURCES mac_nr.cc proc_ra_nr.cc proc_bsr_nr.cc proc_sr_nr.cc mux_nr.cc) add_library(srsue_mac_nr STATIC ${SOURCES}) -target_link_libraries(srsue_mac_nr srsue_mac_common srsran_mac) \ No newline at end of file +target_link_libraries(srsue_mac_nr srsue_mac_common srsran_mac) + +add_subdirectory(test) \ No newline at end of file diff --git a/srsue/src/stack/mac_nr/mac_nr.cc b/srsue/src/stack/mac_nr/mac_nr.cc index 7087cbf62..981f1018e 100644 --- a/srsue/src/stack/mac_nr/mac_nr.cc +++ b/srsue/src/stack/mac_nr/mac_nr.cc @@ -99,6 +99,31 @@ void mac_nr::run_tti(const uint32_t tti) { // Step all procedures logger.debug("Running MAC tti=%d", tti); + + // Update state for all LCIDs/LCGs once so all procedures can use them + update_buffer_states(); + + proc_bsr.step(tti, mac_buffer_states); + proc_sr.step(tti); +} + +void mac_nr::update_buffer_states() +{ + // reset variables + mac_buffer_states.reset(); + for (auto& channel : logical_channels) { + uint32_t buffer_len = rlc->get_buffer_state(channel.lcid); + if (buffer_len > 0) { + mac_buffer_states.nof_lcids_with_data++; + if (channel.lcg != mac_buffer_states.last_non_zero_lcg) { + mac_buffer_states.nof_lcgs_with_data++; + } + mac_buffer_states.last_non_zero_lcg = channel.lcg; + } + mac_buffer_states.lcid_buffer_size[channel.lcid] += buffer_len; + mac_buffer_states.lcg_buffer_size[channel.lcg] += buffer_len; + } + logger.info("%s", mac_buffer_states.to_string()); } mac_interface_phy_nr::sched_rnti_t mac_nr::get_ul_sched_rnti_nr(const uint32_t tti) @@ -156,6 +181,11 @@ uint16_t mac_nr::get_crnti() return c_rnti; } +srsran::mac_sch_subpdu_nr::lcg_bsr_t mac_nr::generate_sbsr() +{ + return proc_bsr.generate_sbsr(); +} + void mac_nr::bch_decoded_ok(uint32_t tti, srsran::unique_byte_buffer_t payload) { // Send MIB to RLC @@ -285,6 +315,9 @@ int mac_nr::setup_lcid(const srsran::logical_channel_config_t& config) config.BSD, config.bucket_size); + // store full config + logical_channels.push_back(config); + return SRSRAN_SUCCESS; } diff --git a/srsue/src/stack/mac_nr/mux_nr.cc b/srsue/src/stack/mac_nr/mux_nr.cc index c72162b36..6f6290720 100644 --- a/srsue/src/stack/mac_nr/mux_nr.cc +++ b/srsue/src/stack/mac_nr/mux_nr.cc @@ -62,6 +62,10 @@ srsran::unique_byte_buffer_t mux_nr::get_pdu(uint32_t max_pdu_len) msg3_transmitted(); } else { // Pack normal UL data PDU + if (add_bsr_ce == sbsr_ce) { + tx_pdu.add_sbsr_ce(mac.generate_sbsr()); + add_bsr_ce = no_bsr; + } // TODO: Add proper priority handling for (const auto& lc : logical_channels) { @@ -91,7 +95,7 @@ srsran::unique_byte_buffer_t mux_nr::get_pdu(uint32_t max_pdu_len) // Pack PDU tx_pdu.pack(); - logger.info(phy_tx_pdu->msg, phy_tx_pdu->N_bytes, "Generated MAC PDU (%d B)", phy_tx_pdu->N_bytes); + logger.debug(phy_tx_pdu->msg, phy_tx_pdu->N_bytes, "Generated MAC PDU (%d B)", phy_tx_pdu->N_bytes); return phy_tx_pdu; } @@ -127,6 +131,17 @@ bool mux_nr::msg3_is_empty() return msg3_buff->N_bytes == 0; } -void mux_nr::generate_bsr_mac_ce() {} +void mux_nr::generate_bsr_mac_ce(const bsr_interface_mux_nr::bsr_format_nr_t& format) +{ + switch (format) { + case bsr_interface_mux_nr::SHORT_BSR: + add_bsr_ce = sbsr_ce; + break; + case bsr_interface_mux_nr::LONG_BSR: + add_bsr_ce = lbsr_ce; + default: + logger.error("MUX can only be instructred to generate short or long BSRs."); + } +} } // namespace srsue diff --git a/srsue/src/stack/mac_nr/proc_bsr_nr.cc b/srsue/src/stack/mac_nr/proc_bsr_nr.cc index 9ebbe3aa2..5ea11ecc8 100644 --- a/srsue/src/stack/mac_nr/proc_bsr_nr.cc +++ b/srsue/src/stack/mac_nr/proc_bsr_nr.cc @@ -16,6 +16,68 @@ namespace srsue { +// TS 38.321, Table 6.1.3.1-1 Buffer size levels (in bytes) for 5-bit Buffer Size field, all values <= except marked +static const uint32_t buffer_size_levels_5bit_max_idx = 31; +static uint32_t buffer_size_levels_5bit[buffer_size_levels_5bit_max_idx + 1] = { + /* == */ 0, 10, 14, 20, 28, 38, 53, 74, 102, 142, 198, + 276, 384, 535, 745, 1038, 1446, 2014, 2806, 3909, 5446, 7587, + 10570, 14726, 20516, 28581, 39818, 55474, 77284, 107669, 150000, /* > */ 150000}; + +// TS 38.321, Table 6.1.3.1-2: Buffer size levels (in bytes) for 8-bit Buffer Size field, all values <= except marked +static const uint32_t buffer_size_levels_8bit_max_idx = 254; +static uint32_t buffer_size_levels_8bit[buffer_size_levels_8bit_max_idx + 1] = { + /* == */ 0, 10, 11, 12, 13, + 14, 15, 16, 17, 18, + 19, 20, 22, 23, 25, + 26, 28, 30, 32, 34, + 36, 38, 40, 43, 46, + 49, 52, 55, 59, 62, + 66, 71, 75, 80, 85, + 91, 97, 103, 110, 117, + 124, 132, 141, 150, 160, + 170, 181, 193, 205, 218, + 233, 248, 264, 281, 299, + 318, 339, 361, 384, 409, + 436, 464, 494, 526, 560, + 597, 635, 677, 720, 767, + 817, 870, 926, 987, 1051, + 1119, 1191, 1269, 1351, 1439, + 1532, 1631, 1737, 1850, 1970, + 2098, 2234, 2379, 2533, 2698, + 2873, 3059, 3258, 3469, 3694, + 3934, 4189, 4461, 4751, 5059, + 5387, 5737, 6109, 6506, 6928, + 7378, 7857, 8367, 8910, 9488, + 10104, 10760, 11458, 12202, 12994, + 13838, 14736, 15692, 16711, 17795, + 18951, 20181, 21491, 22885, 24371, + 25953, 27638, 29431, 31342, 33376, + 35543, 37850, 40307, 42923, 45709, + 48676, 51836, 55200, 58784, 62599, + 66663, 70990, 75598, 80505, 85730, + 91295, 97221, 103532, 110252, 117409, + 125030, 133146, 141789, 150992, 160793, + 171231, 182345, 194182, 206786, 220209, + 234503, 249725, 265935, 283197, 301579, + 321155, 342002, 364202, 387842, 413018, + 439827, 468377, 498780, 531156, 565634, + 602350, 641449, 683087, 727427, 774645, + 824928, 878475, 935498, 996222, 1060888, + 1129752, 1203085, 1281179, 1364342, 1452903, + 1547213, 1647644, 1754595, 1868488, 1989774, + 2118933, 2256475, 2402946, 2558924, 2725027, + 2901912, 3090279, 3290873, 3504487, 3731968, + 3974215, 4232186, 4506902, 4799451, 5110989, + 5442750, 5796046, 6172275, 6572925, 6999582, + 7453933, 7937777, 8453028, 9001725, 9586039, + 10208280, 10870913, 11576557, 12328006, 13128233, + 13980403, 14887889, 15854280, 16883401, 17979324, + 19146385, 20389201, 21712690, 23122088, 24622972, + 26221280, 27923336, 29735875, 31666069, 33721553, + 35910462, 38241455, 40723756, 43367187, 46182206, + 49179951, 52372284, 55771835, 59392055, 63247269, + 67352729, 71724679, 76380419, 81338368, /* > */ 81338368}; + int32_t proc_bsr_nr::init(proc_sr_nr* sr_, mux_interface_bsr_nr* mux_, rlc_interface_mac* rlc_, @@ -34,7 +96,7 @@ int32_t proc_bsr_nr::init(proc_sr_nr* sr_, // Print periodically the LCID queue status auto queue_status_print_task = [this](uint32_t tid) { - print_state(); + logger.debug("BSR: %s", buffer_state.to_string()); timer_queue_status_print.run(); }; timer_queue_status_print.set(QUEUE_STATUS_PERIOD_MS, queue_status_print_task); @@ -45,20 +107,6 @@ int32_t proc_bsr_nr::init(proc_sr_nr* sr_, return SRSRAN_SUCCESS; } -void proc_bsr_nr::print_state() -{ - char str[128]; - str[0] = '\0'; - int n = 0; - for (auto& lcg : lcgs) { - for (auto& iter : lcg) { - n = srsran_print_check(str, 128, n, "%d: %d ", iter.first, iter.second.old_buffer); - } - } - logger.info( - "BSR: triggered_bsr_type=%s, LCID QUEUE status: %s", bsr_trigger_type_tostring(triggered_bsr_type), str); -} - void proc_bsr_nr::set_trigger(bsr_trigger_type_t new_trigger) { triggered_bsr_type = new_trigger; @@ -119,49 +167,17 @@ void proc_bsr_nr::timer_expired(uint32_t timer_id) } } -uint32_t proc_bsr_nr::get_buffer_state() -{ - uint32_t buffer = 0; - for (int i = 0; i < MAX_NOF_LCG; i++) { - buffer += get_buffer_state_lcg(i); - } - return buffer; -} - // Checks if data is available for a channel with higher priority than others bool proc_bsr_nr::check_highest_channel() { // TODO: move 4G implementation to base class or rewrite - for (int i = 0; i < MAX_NOF_LCG; i++) { - for (std::map::iterator iter = lcgs[i].begin(); iter != lcgs[i].end(); ++iter) { - // If new data available - if (iter->second.new_buffer > iter->second.old_buffer) { - // Check if this LCID has higher priority than any other LCID ("belong to any LCG") for which data is already - // available for transmission - bool is_max_priority = true; - for (int j = 0; j < MAX_NOF_LCG; j++) { - for (std::map::iterator iter2 = lcgs[j].begin(); iter2 != lcgs[j].end(); ++iter2) { - // No max prio LCG if prio isn't higher or LCID already had buffered data - if (iter2->second.priority <= iter->second.priority && (iter2->second.old_buffer > 0)) { - is_max_priority = false; - } - } - } - if (is_max_priority) { - logger.debug("BSR: New data for lcid=%d with maximum priority in lcg=%d", iter->first, i); - return true; - } - } - } - } return false; } bool proc_bsr_nr::check_any_channel() { - // TODO: move 4G implementation to base class or rewrite - for (int i = 0; i < MAX_NOF_LCG; i++) { - if (get_buffer_state_lcg(i)) { + for (const auto& lcg : buffer_state.lcg_buffer_size) { + if (lcg.second > 0) { return true; } } @@ -169,66 +185,38 @@ bool proc_bsr_nr::check_any_channel() } // Checks if only one logical channel has data avaiable for Tx -bool proc_bsr_nr::check_new_data() +bool proc_bsr_nr::check_new_data(const mac_buffer_states_t& new_buffer_state) { - // TODO: move 4G implementation to base class or rewrite - for (int i = 0; i < MAX_NOF_LCG; i++) { - // If there was no data available in any LCID belonging to this LCG - if (get_buffer_state_lcg(i) == 0) { - for (std::map::iterator iter = lcgs[i].begin(); iter != lcgs[i].end(); ++iter) { - if (iter->second.new_buffer > 0) { - logger.debug("BSR: New data available for lcid=%d", iter->first); - return true; - } - } + for (const auto& lcg : buffer_state.lcg_buffer_size) { + if (lcg.second == 0 and new_buffer_state.lcg_buffer_size.at(lcg.first) > 0) { + logger.debug("BSR: New data available for LCG=%d", lcg.first); + return true; } } return false; } -void proc_bsr_nr::update_new_data() -{ - // TODO: move 4G implementation to base class or rewrite - for (int i = 0; i < MAX_NOF_LCG; i++) { - for (std::map::iterator iter = lcgs[i].begin(); iter != lcgs[i].end(); ++iter) { - iter->second.new_buffer = rlc->get_buffer_state(iter->first); - } - } -} - -void proc_bsr_nr::update_old_buffer() -{ - // TODO: move 4G implementation to base class or rewrite - for (int i = 0; i < MAX_NOF_LCG; i++) { - for (std::map::iterator iter = lcgs[i].begin(); iter != lcgs[i].end(); ++iter) { - iter->second.old_buffer = iter->second.new_buffer; - } - } -} - -uint32_t proc_bsr_nr::get_buffer_state_lcg(uint32_t lcg) -{ - // TODO: move 4G implementation to base class or rewrite - uint32_t n = 0; - for (std::map::iterator iter = lcgs[lcg].begin(); iter != lcgs[lcg].end(); ++iter) { - n += iter->second.old_buffer; - } - return n; -} - -// Generate BSR -bool proc_bsr_nr::generate_bsr(bsr_t* bsr, uint32_t pdu_space) +srsran::mac_sch_subpdu_nr::lcg_bsr_t proc_bsr_nr::generate_sbsr() { - // TODO: add BSR generation - bool send_bsr = false; - return send_bsr; + srsran::mac_sch_subpdu_nr::lcg_bsr_t sbsr = {}; + sbsr.lcg_id = buffer_state.last_non_zero_lcg; + sbsr.buffer_size = buff_size_bytes_to_field(buffer_state.lcg_buffer_size.at(sbsr.lcg_id), SHORT_BSR); + triggered_bsr_type = NONE; + return sbsr; } -// Called by MAC every TTI -// Checks if Regular BSR must be assembled, as defined in 5.4.5 -// Padding BSR is assembled when called by mux_unit when UL dci is received -// Periodic BSR is triggered by the expiration of the timers -void proc_bsr_nr::step(uint32_t tti) +/** + * @brief Called by MAC every TTI with the current state of each LCID/LCGs + * + * Checks if Regular BSR must be assembled, as defined in 5.4.5. + * Padding BSR is assembled when explicitly called by MUX when UL DCI is received + * Periodic BSR is triggered by the expiration of the timers + * + * @param tti The current TTI + * @param new_buffer_state Buffer state of all LCID/LCGs at the start of the TTI + * + */ +void proc_bsr_nr::step(uint32_t tti, const mac_buffer_states_t& new_buffer_state_) { std::lock_guard lock(mutex); @@ -236,30 +224,40 @@ void proc_bsr_nr::step(uint32_t tti) return; } - update_new_data(); - // Regular BSR triggered if new data arrives or channel with high priority has new data - if (check_new_data() || check_highest_channel()) { + if (check_new_data(new_buffer_state_) || check_highest_channel()) { logger.debug("BSR: Triggering Regular BSR tti=%d", tti); set_trigger(REGULAR); } - update_old_buffer(); + // store buffer state for comparision in next TTI + buffer_state = new_buffer_state_; } void proc_bsr_nr::new_grant_ul(uint32_t grant_size) { std::lock_guard lock(mutex); if (triggered_bsr_type != NONE) { - // inform MUX we need to generate a BSR - mux->generate_bsr_mac_ce(); - } + // Decide BSR type to be transmitted, state for all LCG/LCIDs has already been updated by step() + if (buffer_state.nof_lcgs_with_data > 1) { + // report Long BSR if more than one LCG has data to send + mux->generate_bsr_mac_ce(LONG_BSR); + } else { + // report Short BSR otherwise + mux->generate_bsr_mac_ce(SHORT_BSR); + } - // TODO: restart retxBSR-Timer + // 3> start or restart periodicBSR-Timer, except when all the generated BSRs are long or short Truncated BSRs + // TODO: add check if only truncated version can be included + timer_periodic.run(); + + // 3> start or restart retxBSR-Timer. + timer_retx.run(); + } } // This function is called by MUX only if Regular BSR has not been triggered before -bool proc_bsr_nr::generate_padding_bsr(uint32_t nof_padding_bytes, bsr_t* bsr) +bool proc_bsr_nr::generate_padding_bsr(uint32_t nof_padding_bytes) { std::lock_guard lock(mutex); @@ -272,7 +270,7 @@ bool proc_bsr_nr::generate_padding_bsr(uint32_t nof_padding_bytes, bsr_t* bsr) nof_padding_bytes <= LBSR_CE_SUBHEADER_LEN + srsran::mac_sch_subpdu_nr::sizeof_ce(LONG_BSR, true)) { // generate padding BSR set_trigger(PADDING); - generate_bsr(bsr, nof_padding_bytes); + // generate_bsr(bsr, nof_padding_bytes); set_trigger(NONE); return true; } @@ -283,40 +281,76 @@ bool proc_bsr_nr::generate_padding_bsr(uint32_t nof_padding_bytes, bsr_t* bsr) int proc_bsr_nr::setup_lcid(uint32_t lcid, uint32_t new_lcg, uint32_t priority) { // TODO: move 4G implementation to base class - if (new_lcg > MAX_NOF_LCG) { + if (new_lcg > srsran::mac_sch_subpdu_nr::max_num_lcg_lbsr) { logger.error("BSR: Invalid lcg=%d for lcid=%d", new_lcg, lcid); return SRSRAN_ERROR; } std::lock_guard lock(mutex); - // First see if it already exists and eliminate it - for (int i = 0; i < MAX_NOF_LCG; i++) { - if (lcgs[i].count(lcid)) { - lcgs[i].erase(lcid); - } + // Check that the new priority doesn't not already exist + if (lcg_priorities.find(priority) != lcg_priorities.end()) { + logger.error( + "BSR: Invalid config. Priority=%d already configured for lcg=%d", priority, lcg_priorities.at(priority)); + return SRSRAN_ERROR; } - // Now add it - lcgs[new_lcg][lcid].priority = priority; - lcgs[new_lcg][lcid].old_buffer = 0; + + lcg_priorities[priority] = new_lcg; return SRSRAN_SUCCESS; } uint32_t proc_bsr_nr::find_max_priority_lcg_with_data() { - // TODO: move 4G implementation to base class or rewrite - int32_t max_prio = 99; - uint32_t max_idx = 0; - for (int i = 0; i < MAX_NOF_LCG; i++) { - for (std::map::iterator iter = lcgs[i].begin(); iter != lcgs[i].end(); ++iter) { - if (iter->second.priority < max_prio && iter->second.old_buffer > 0) { - max_prio = iter->second.priority; - max_idx = i; - } + // iterate over LCGs in order of their priorities and check if there is one with data to transmit + for (const auto& lcg_prio : lcg_priorities) { + if (buffer_state.lcg_buffer_size.at(lcg_prio.second) > 0) { + return lcg_prio.second; } } - return max_idx; + return 0; +} + +/** Converts the buffer size levels (in Bytes) to the 5 or 8-bit Buffer Size field + * @param buffer_size The actual buffer size level in Bytes + * @param format The BSR format that determines the buffer size field length + * @return uint8_t The buffer size field that will be used for the MAC PDU + */ +uint8_t proc_bsr_nr::buff_size_bytes_to_field(uint32_t buffer_size, bsr_format_nr_t format) +{ + if (buffer_size == 0) { + } + + switch (format) { + case SHORT_BSR: + case SHORT_TRUNC_BSR: + if (buffer_size > buffer_size_levels_5bit[buffer_size_levels_5bit_max_idx]) { + return buffer_size_levels_5bit_max_idx; + } else { + for (uint32_t i = 1; i < buffer_size_levels_5bit_max_idx; i++) { + if (buffer_size <= buffer_size_levels_5bit[i]) { + return i; + } + } + return buffer_size_levels_5bit_max_idx - 1; + } + break; + case LONG_BSR: + case LONG_TRUNC_BSR: + if (buffer_size > buffer_size_levels_8bit[buffer_size_levels_8bit_max_idx]) { + return buffer_size_levels_8bit_max_idx; + } else { + for (uint32_t i = 1; i < buffer_size_levels_8bit_max_idx; i++) { + if (buffer_size <= buffer_size_levels_8bit[i + 1]) { + return i + 1; + } + } + return buffer_size_levels_8bit_max_idx - 1; + } + break; + } + + return 0; } } // namespace srsue diff --git a/srsue/test/mac_nr/CMakeLists.txt b/srsue/src/stack/mac_nr/test/CMakeLists.txt similarity index 70% rename from srsue/test/mac_nr/CMakeLists.txt rename to srsue/src/stack/mac_nr/test/CMakeLists.txt index 62d6cfa1b..3d4dd8f87 100644 --- a/srsue/test/mac_nr/CMakeLists.txt +++ b/srsue/src/stack/mac_nr/test/CMakeLists.txt @@ -10,6 +10,10 @@ add_executable(proc_ra_nr_test proc_ra_nr_test.cc) target_link_libraries(proc_ra_nr_test srsue_mac_nr srsran_common) add_test(proc_ra_nr_test proc_ra_nr_test) +add_executable(proc_bsr_nr_test proc_bsr_nr_test.cc) +target_link_libraries(proc_bsr_nr_test srsue_mac_nr srsran_common) +add_test(proc_bsr_nr_test proc_bsr_nr_test) + add_executable(mac_nr_test mac_nr_test.cc) target_link_libraries(mac_nr_test srsue_mac_nr srsran_common) add_test(mac_nr_test mac_nr_test) \ No newline at end of file diff --git a/srsue/test/mac_nr/mac_nr_test.cc b/srsue/src/stack/mac_nr/test/mac_nr_test.cc similarity index 61% rename from srsue/test/mac_nr/mac_nr_test.cc rename to srsue/src/stack/mac_nr/test/mac_nr_test.cc index de8dc5f63..17bb1c8d6 100644 --- a/srsue/test/mac_nr/mac_nr_test.cc +++ b/srsue/src/stack/mac_nr/test/mac_nr_test.cc @@ -188,11 +188,6 @@ int mac_nr_ul_logical_channel_prioritization_test1() mac.setup_lcid(channel); } - srsran::bsr_cfg_nr_t bsr_cfg = {}; - bsr_cfg.periodic_timer = 20; - bsr_cfg.retx_timer = 320; - TESTASSERT(mac.set_config(bsr_cfg) == SRSRAN_SUCCESS); - // write dummy data to DRB2 rlc.write_sdu(4, 10); @@ -233,6 +228,159 @@ int mac_nr_ul_logical_channel_prioritization_test1() return SRSRAN_SUCCESS; } +// Basic test for periodic BSR transmission +int mac_nr_ul_periodic_bsr_test() +{ + // PDU layout (10 B in total) + // - SBSR + // - 6B LCID=4 + const uint8_t tv1[] = {0x3d, 0xd1, 0x04, 0x06, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04}; + + // PDU layout (10 B in total) + // - 8B LCID=4 + const uint8_t tv2[] = {0x04, 0x08, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04}; + + // dummy layers + dummy_phy phy; + rlc_dummy rlc; + rrc_dummy rrc; + stack_dummy stack; + + // the actual MAC + mac_nr mac(&stack.task_sched); + + mac_nr_args_t args = {}; + mac.init(args, &phy, &rlc, &rrc); + + stack.init(&mac, &phy); + const uint16_t crnti = 0x1001; + + // generate config (default DRB2 config for EN-DC) + std::vector lcids; + srsran::logical_channel_config_t config = {}; + config.lcid = 4; + config.lcg = 6; + config.PBR = 0; + config.BSD = 1000; // 1000ms + config.priority = 11; + lcids.push_back(config); + + // setup LCIDs in MAC + for (auto& channel : lcids) { + mac.setup_lcid(channel); + } + + srsran::bsr_cfg_nr_t bsr_cfg = {}; + bsr_cfg.periodic_timer = 20; + bsr_cfg.retx_timer = 320; + TESTASSERT(mac.set_config(bsr_cfg) == SRSRAN_SUCCESS); + + // run TTI to establish LCGs old buffer states at BSR + uint32_t tti = 0; + stack.run_tti(tti++); + + // write large amount of dummy data to DRB2 + rlc.write_sdu(4, 2000); + + // run TTI to setup Bj, BSR should be generated + stack.run_tti(tti++); + usleep(100); + + // create UL action and grant and read MAC PDU + { + mac_interface_phy_nr::tb_action_ul_t ul_action = {}; + mac_interface_phy_nr::mac_nr_grant_ul_t mac_grant = {}; + + mac_grant.rnti = crnti; // make sure MAC picks it up as valid UL grant + mac_grant.pid = 0; + mac_grant.rnti = 0x1001; + mac_grant.tti = 0; + mac_grant.tbs = 10; + int cc_idx = 0; + + // Send grant to MAC and get action for this TB + mac.new_grant_ul(cc_idx, mac_grant, &ul_action); + + // print generated PDU + srslog::fetch_basic_logger("MAC").info( + ul_action.tb.payload->msg, mac_grant.tbs, "Generated PDU (%d B)", mac_grant.tbs); +#if HAVE_PCAP + pcap_handle->write_ul_crnti_nr( + ul_action.tb.payload->msg, mac_grant.tbs, mac_grant.rnti, UE_ID, mac_grant.pid, mac_grant.tti); +#endif + + TESTASSERT(memcmp(ul_action.tb.payload->msg, tv1, sizeof(tv1)) == 0); + } + + // for the next 19 TTI, until the periodic BSR is triggered again, no BSR should be included in the MAC PDU + for (int i = 0; i < bsr_cfg.periodic_timer - 1; ++i) { + stack.run_tti(tti++); + usleep(100); + + // create UL action and grant and read MAC PDU + { + mac_interface_phy_nr::tb_action_ul_t ul_action = {}; + mac_interface_phy_nr::mac_nr_grant_ul_t mac_grant = {}; + + mac_grant.rnti = crnti; // make sure MAC picks it up as valid UL grant + mac_grant.pid = 0; + mac_grant.rnti = 0x1001; + mac_grant.tti = 0; + mac_grant.tbs = 10; + int cc_idx = 0; + + // Send grant to MAC and get action for this TB + mac.new_grant_ul(cc_idx, mac_grant, &ul_action); + + // print generated PDU + srslog::fetch_basic_logger("MAC").info( + ul_action.tb.payload->msg, mac_grant.tbs, "Generated PDU (%d B)", mac_grant.tbs); +#if HAVE_PCAP + pcap_handle->write_ul_crnti_nr( + ul_action.tb.payload->msg, mac_grant.tbs, mac_grant.rnti, UE_ID, mac_grant.pid, mac_grant.tti); +#endif + + TESTASSERT(memcmp(ul_action.tb.payload->msg, tv2, sizeof(tv2)) == 0); + } + } + + // run TTI to setup Bj, the same BSR should be generated again + stack.run_tti(tti++); + usleep(100); + + // create UL action and grant and read MAC PDU + { + mac_interface_phy_nr::tb_action_ul_t ul_action = {}; + mac_interface_phy_nr::mac_nr_grant_ul_t mac_grant = {}; + + mac_grant.rnti = crnti; // make sure MAC picks it up as valid UL grant + mac_grant.pid = 0; + mac_grant.rnti = 0x1001; + mac_grant.tti = 0; + mac_grant.tbs = 10; + int cc_idx = 0; + + // Send grant to MAC and get action for this TB + mac.new_grant_ul(cc_idx, mac_grant, &ul_action); + + // print generated PDU + srslog::fetch_basic_logger("MAC").info( + ul_action.tb.payload->msg, mac_grant.tbs, "Generated PDU (%d B)", mac_grant.tbs); +#if HAVE_PCAP + pcap_handle->write_ul_crnti_nr( + ul_action.tb.payload->msg, mac_grant.tbs, mac_grant.rnti, UE_ID, mac_grant.pid, mac_grant.tti); +#endif + + TESTASSERT(memcmp(ul_action.tb.payload->msg, tv1, sizeof(tv1)) == 0); + } + + // make sure MAC PDU thread picks up before stopping + stack.run_tti(tti++); + mac.stop(); + + return SRSRAN_SUCCESS; +} + int main() { #if HAVE_PCAP @@ -247,6 +395,7 @@ int main() TESTASSERT(msg3_test() == SRSRAN_SUCCESS); TESTASSERT(mac_nr_ul_logical_channel_prioritization_test1() == SRSRAN_SUCCESS); + TESTASSERT(mac_nr_ul_periodic_bsr_test() == SRSRAN_SUCCESS); return SRSRAN_SUCCESS; } diff --git a/srsue/src/stack/mac_nr/test/proc_bsr_nr_test.cc b/srsue/src/stack/mac_nr/test/proc_bsr_nr_test.cc new file mode 100644 index 000000000..74925aadc --- /dev/null +++ b/srsue/src/stack/mac_nr/test/proc_bsr_nr_test.cc @@ -0,0 +1,93 @@ +/** + * + * \section COPYRIGHT + * + * Copyright 2013-2021 Software Radio Systems Limited + * + * By using this file, you agree to the terms and conditions set + * forth in the LICENSE file which can be found at the top level of + * the distribution. + * + */ +#include "srsran/common/buffer_pool.h" +#include "srsran/common/common.h" +#include "srsran/common/test_common.h" +#include "srsue/hdr/stack/mac_nr/proc_bsr_nr.h" + +using namespace srsue; + +int sbsr_tests() +{ + auto& mac_logger = srslog::fetch_basic_logger("MAC"); + mac_logger.set_level(srslog::basic_levels::debug); + mac_logger.set_hex_dump_max_size(-1); + + srsran::task_scheduler task_sched{5, 2}; + srsran::ext_task_sched_handle ext_task_sched_h(&task_sched); + + proc_bsr_nr proc(mac_logger); + proc.init(nullptr, nullptr, nullptr, &ext_task_sched_h); + + srsran::bsr_cfg_nr_t bsr_cfg = {}; + bsr_cfg.periodic_timer = 20; + bsr_cfg.retx_timer = 320; + TESTASSERT(proc.set_config(bsr_cfg) == SRSRAN_SUCCESS); + + uint32_t tti = 0; + mac_buffer_states_t buffer_state; + buffer_state.last_non_zero_lcg = 1; + buffer_state.lcg_buffer_size[1] = 10; + proc.step(tti++, buffer_state); + + // Buffer size == 10 should result in index 1 (<= 10) + srsran::mac_sch_subpdu_nr::lcg_bsr_t sbsr = proc.generate_sbsr(); + TESTASSERT(sbsr.lcg_id == 1); + TESTASSERT(sbsr.buffer_size == 1); + + buffer_state.last_non_zero_lcg = 1; + buffer_state.lcg_buffer_size[1] = 11; + proc.step(tti++, buffer_state); + + // Buffer size == 11 should result in index 1 + sbsr = proc.generate_sbsr(); + TESTASSERT(sbsr.lcg_id == 1); + TESTASSERT(sbsr.buffer_size == 2); + + buffer_state.last_non_zero_lcg = 1; + buffer_state.lcg_buffer_size[1] = 77285; // 77284 + 1 + proc.step(tti++, buffer_state); + + // Buffer size 77285 should result in index 29 (first value of that index) + sbsr = proc.generate_sbsr(); + TESTASSERT(sbsr.lcg_id == 1); + TESTASSERT(sbsr.buffer_size == 29); + + buffer_state.last_non_zero_lcg = 1; + buffer_state.lcg_buffer_size[1] = 150000; + proc.step(tti++, buffer_state); + + // Buffer size 150000 should result in index 30 + sbsr = proc.generate_sbsr(); + TESTASSERT(sbsr.lcg_id == 1); + TESTASSERT(sbsr.buffer_size == 30); + + buffer_state.last_non_zero_lcg = 1; + buffer_state.lcg_buffer_size[1] = 150001; + proc.step(tti++, buffer_state); + + // Buffer size 150001 should result in index 31 + sbsr = proc.generate_sbsr(); + TESTASSERT(sbsr.lcg_id == 1); + TESTASSERT(sbsr.buffer_size == 31); + + return SRSRAN_SUCCESS; +} + +int main() +{ + srslog::init(); + + TESTASSERT(sbsr_tests() == SRSRAN_SUCCESS); + + return SRSRAN_SUCCESS; +} diff --git a/srsue/test/mac_nr/proc_ra_nr_test.cc b/srsue/src/stack/mac_nr/test/proc_ra_nr_test.cc similarity index 100% rename from srsue/test/mac_nr/proc_ra_nr_test.cc rename to srsue/src/stack/mac_nr/test/proc_ra_nr_test.cc diff --git a/srsue/test/CMakeLists.txt b/srsue/test/CMakeLists.txt index 4f86437d1..28af94179 100644 --- a/srsue/test/CMakeLists.txt +++ b/srsue/test/CMakeLists.txt @@ -8,7 +8,6 @@ add_subdirectory(phy) add_subdirectory(upper) -add_subdirectory(mac_nr) if (ENABLE_TTCN3) add_subdirectory(ttcn3)