/** * Copyright 2013-2022 Software Radio Systems Limited * * This file is part of srsRAN. * * srsRAN is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. * * srsRAN is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * A copy of the GNU Affero General Public License can be found in * the LICENSE file in the top-level directory of this distribution * and at http://www.gnu.org/licenses/. * */ #include "srsran/rlc/rlc_um_nr.h" #include "srsran/interfaces/ue_pdcp_interfaces.h" #include #define RX_MOD_NR_BASE(x) (((x)-RX_Next_Highest - UM_Window_Size) % mod) namespace srsran { rlc_um_nr::rlc_um_nr(srslog::basic_logger& logger, uint32_t lcid_, srsue::pdcp_interface_rlc* pdcp_, srsue::rrc_interface_rlc* rrc_, srsran::timer_handler* timers_) : rlc_um_base(logger, lcid_, pdcp_, rrc_, timers_) {} rlc_um_nr::~rlc_um_nr() { stop(); } bool rlc_um_nr::configure(const rlc_config_t& cnfg_) { // store config cfg = cnfg_; // determine bearer name and configure Rx/Tx objects rb_name = get_rb_name(); rx.reset(new rlc_um_nr_rx(this)); if (not rx->configure(cfg, rb_name)) { return false; } tx.reset(new rlc_um_nr_tx(this)); if (not tx->configure(cfg, rb_name)) { return false; } RlcInfo("configured in %s: sn_field_length=%u bits, t_reassembly=%d ms", srsran::to_string(cnfg_.rlc_mode), srsran::to_number(cfg.um_nr.sn_field_length), cfg.um_nr.t_reassembly_ms); rx_enabled = true; tx_enabled = true; return true; } /**************************************************************************** * Logging helpers ***************************************************************************/ std::string rlc_um_nr::get_rb_name() const { fmt::memory_buffer fmtbuf; fmt::format_to(fmtbuf, "DRB{}", cfg.um_nr.bearer_id); return fmt::to_string(fmtbuf); } /**************************************************************************** * Tx Subclass implementation ***************************************************************************/ rlc_um_nr::rlc_um_nr_tx::rlc_um_nr_tx(rlc_um_base* parent_) : rlc_um_base_tx(parent_) {} uint32_t rlc_um_nr::rlc_um_nr_tx::get_buffer_state() { std::lock_guard lock(mutex); // Bytes needed for tx SDUs uint32_t n_sdus = tx_sdu_queue.get_n_sdus(); uint32_t n_bytes = tx_sdu_queue.size_bytes(); if (tx_sdu) { n_sdus++; n_bytes += tx_sdu->N_bytes; } // Room needed for header extensions? (integer rounding) if (n_sdus > 1) { n_bytes += ((n_sdus - 1) * 1.5) + 0.5; } // Room needed for fixed header? if (n_bytes > 0) { n_bytes += (cfg.um.is_mrb) ? 2 : 3; } if (bsr_callback) { bsr_callback(parent->get_lcid(), n_bytes, 0); } return n_bytes; } bool rlc_um_nr::rlc_um_nr_tx::configure(const rlc_config_t& cnfg_, std::string rb_name_) { cfg = cnfg_; mod = (cfg.um_nr.sn_field_length == rlc_um_nr_sn_size_t::size6bits) ? 64 : 4096; UM_Window_Size = (cfg.um_nr.sn_field_length == rlc_um_nr_sn_size_t::size6bits) ? 32 : 2048; // calculate header sizes for configured SN length rlc_um_nr_pdu_header_t header = {}; header.si = rlc_nr_si_field_t::first_segment; header.so = 0; head_len_first = rlc_um_nr_packed_length(header); header.so = 1; head_len_segment = rlc_um_nr_packed_length(header); tx_sdu_queue.resize(cnfg_.tx_queue_length); rb_name = rb_name_; return true; } uint32_t rlc_um_nr::rlc_um_nr_tx::build_data_pdu(unique_byte_buffer_t pdu, uint8_t* payload, uint32_t nof_bytes) { // Sanity check (we need at least 2B for a SDU) if (nof_bytes < 2) { RlcWarning("Cannot build a PDU with %d byte.", nof_bytes); return 0; } std::lock_guard lock(mutex); rlc_um_nr_pdu_header_t header = {}; header.si = rlc_nr_si_field_t::full_sdu; header.sn = TX_Next; header.sn_size = cfg.um_nr.sn_field_length; uint32_t pdu_space = SRSRAN_MIN(nof_bytes, pdu->get_tailroom()); // Select segmentation information and header size if (tx_sdu == nullptr) { // Read a new SDU do { tx_sdu = tx_sdu_queue.read(); } while (tx_sdu == nullptr && tx_sdu_queue.size() != 0); if (tx_sdu == nullptr) { RlcDebug("Cannot build any PDU, tx_sdu_queue has no non-null SDU."); return 0; } next_so = 0; // Check for full SDU case if (tx_sdu->N_bytes <= pdu_space - head_len_full) { header.si = rlc_nr_si_field_t::full_sdu; } else { header.si = rlc_nr_si_field_t::first_segment; } } else { // The SDU is not new; check for last segment if (tx_sdu->N_bytes <= pdu_space - head_len_segment) { header.si = rlc_nr_si_field_t::last_segment; } else { header.si = rlc_nr_si_field_t::neither_first_nor_last_segment; } } header.so = next_so; // Calculate actual header length uint32_t head_len = rlc_um_nr_packed_length(header); if (pdu_space <= head_len + 1) { RlcInfo("Cannot build a PDU - %d bytes available, %d bytes required for header", nof_bytes, head_len); return 0; } // Calculate the amount of data to move uint32_t space = pdu_space - head_len; uint32_t to_move = space >= tx_sdu->N_bytes ? tx_sdu->N_bytes : space; // Log RlcDebug("adding %s - (%d/%d)", to_string(header.si).c_str(), to_move, tx_sdu->N_bytes); // Move data from SDU to PDU uint8_t* pdu_ptr = pdu->msg; memcpy(pdu_ptr, tx_sdu->msg, to_move); pdu_ptr += to_move; pdu->N_bytes += to_move; tx_sdu->N_bytes -= to_move; tx_sdu->msg += to_move; // Release SDU if emptied if (tx_sdu->N_bytes == 0) { tx_sdu.reset(); } // advance SO offset next_so += to_move; // Update SN if needed if (header.si == rlc_nr_si_field_t::last_segment) { TX_Next = (TX_Next + 1) % mod; next_so = 0; } // Add header and TX rlc_um_nr_write_data_pdu_header(header, pdu.get()); memcpy(payload, pdu->msg, pdu->N_bytes); uint32_t ret = pdu->N_bytes; // Assert number of bytes srsran_expect( ret <= nof_bytes, "Error while packing MAC PDU (more bytes written (%d) than expected (%d)!", ret, nof_bytes); if (header.si == rlc_nr_si_field_t::full_sdu) { // log without SN RlcHexInfo(payload, ret, "Tx PDU (%d B)", pdu->N_bytes); } else { RlcHexInfo(payload, ret, "Tx PDU SN=%d (%d B)", header.sn, pdu->N_bytes); } debug_state(); return ret; } void rlc_um_nr::rlc_um_nr_tx::debug_state() { RlcDebug("TX_Next=%d, next_so=%d", TX_Next, next_so); } void rlc_um_nr::rlc_um_nr_tx::reset() { TX_Next = 0; next_so = 0; } /**************************************************************************** * Rx Subclass implementation ***************************************************************************/ rlc_um_nr::rlc_um_nr_rx::rlc_um_nr_rx(rlc_um_base* parent_) : rlc_um_base_rx(parent_), reassembly_timer(timers->get_unique_timer()) {} bool rlc_um_nr::rlc_um_nr_rx::configure(const rlc_config_t& cnfg_, std::string rb_name_) { mod = (cfg.um_nr.sn_field_length == rlc_um_nr_sn_size_t::size6bits) ? 64 : 4096; UM_Window_Size = (cfg.um_nr.sn_field_length == rlc_um_nr_sn_size_t::size6bits) ? 32 : 2048; rb_name = rb_name_; // check timer if (not reassembly_timer.is_valid()) { RlcError("Configuring RLC UM NR RX: timers not configured"); return false; } // configure timer if (cfg.um_nr.t_reassembly_ms > 0) { reassembly_timer.set(static_cast(cfg.um_nr.t_reassembly_ms), [this](uint32_t tid) { timer_expired(tid); }); } return true; } void rlc_um_nr::rlc_um_nr_rx::stop() { std::lock_guard lock(mutex); reset(); reassembly_timer.stop(); } void rlc_um_nr::rlc_um_nr_rx::reset() { RX_Next_Reassembly = 0; RX_Timer_Trigger = 0; RX_Next_Highest = 0; rx_sdu.reset(); // Drop all messages in RX window rx_window.clear(); // stop timer if (reassembly_timer.is_valid()) { reassembly_timer.stop(); } } // TS 38.322 Sec. 5.1.2 void rlc_um_nr::rlc_um_nr_rx::reestablish() { // drop all SDUs, SDU segments, PDUs and reset timers reset(); } // TS 38.322 v15.003 Section 5.2.2.2.4 void rlc_um_nr::rlc_um_nr_rx::timer_expired(uint32_t timeout_id) { std::lock_guard lock(mutex); if (reassembly_timer.id() == timeout_id) { RlcDebug("reassembly timeout expiry for SN=%d - updating RX_Next_Reassembly and reassembling", RX_Next_Reassembly); metrics.num_lost_pdus++; if (rx_sdu != nullptr) { rx_sdu->clear(); } // update RX_Next_Reassembly to the next SN that has not been reassembled yet RX_Next_Reassembly = RX_Timer_Trigger; while (RX_MOD_NR_BASE(RX_Next_Reassembly) < RX_MOD_NR_BASE(RX_Next_Highest)) { RX_Next_Reassembly = (RX_Next_Reassembly + 1) % mod; debug_state(); } // discard all segments with SN < updated RX_Next_Reassembly for (auto it = rx_window.begin(); it != rx_window.end();) { if (RX_MOD_NR_BASE(it->first) < RX_MOD_NR_BASE(RX_Next_Reassembly)) { it = rx_window.erase(it); } else { ++it; } } // check start of t_reassembly if (RX_MOD_NR_BASE(RX_Next_Highest) > RX_MOD_NR_BASE(RX_Next_Reassembly + 1) || ((RX_MOD_NR_BASE(RX_Next_Highest) == RX_MOD_NR_BASE(RX_Next_Reassembly + 1) && has_missing_byte_segment(RX_Next_Reassembly)))) { RlcDebug("starting reassembly timer for SN=%d", rb_name.c_str(), RX_Next_Reassembly); reassembly_timer.run(); RX_Timer_Trigger = RX_Next_Highest; } debug_state(); } } // Sec 5.2.2.2.1 bool rlc_um_nr::rlc_um_nr_rx::sn_in_reassembly_window(const uint32_t sn) { return (RX_MOD_NR_BASE(RX_Next_Highest - UM_Window_Size) <= RX_MOD_NR_BASE(sn) && RX_MOD_NR_BASE(sn) < RX_MOD_NR_BASE(RX_Next_Highest)); } // Sec 5.2.2.2.2 bool rlc_um_nr::rlc_um_nr_rx::sn_invalid_for_rx_buffer(const uint32_t sn) { return (RX_MOD_NR_BASE(RX_Next_Highest - UM_Window_Size) <= RX_MOD_NR_BASE(sn) && RX_MOD_NR_BASE(sn) < RX_MOD_NR_BASE(RX_Next_Reassembly)); } unique_byte_buffer_t rlc_um_nr::rlc_um_nr_rx::rlc_um_nr_strip_pdu_header(const rlc_um_nr_pdu_header_t& header, const uint8_t* payload, const uint32_t nof_bytes) { unique_byte_buffer_t sdu = make_byte_buffer(); if (sdu == nullptr) { RlcError("Couldn't allocate PDU in %s().", __FUNCTION__); return nullptr; } memcpy(sdu->msg, payload, nof_bytes); sdu->N_bytes = nof_bytes; // strip RLC header int header_len = rlc_um_nr_packed_length(header); sdu->msg += header_len; sdu->N_bytes -= header_len; return sdu; } bool rlc_um_nr::rlc_um_nr_rx::has_missing_byte_segment(const uint32_t sn) { // is at least one missing byte segment of the RLC SDU associated with SN = RX_Next_Reassembly before the last byte of // all received segments of this RLC SDU return (rx_window.find(sn) != rx_window.end()); } // Sect 5.2.2.2.3 void rlc_um_nr::rlc_um_nr_rx::handle_rx_buffer_update(const uint32_t sn) { if (rx_window.find(sn) != rx_window.end()) { bool sdu_complete = false; // iterate over received segments and try to assemble full SDU auto& pdu = rx_window.at(sn); for (auto it = pdu.segments.begin(); it != pdu.segments.end();) { RlcDebug("Have %s segment with SO=%d for SN=%d", to_string_short(it->second.header.si).c_str(), it->second.header.so, it->second.header.sn); if (it->second.header.so == pdu.next_expected_so) { if (pdu.next_expected_so == 0) { if (pdu.sdu == nullptr) { // reuse buffer of first segment for final SDU pdu.sdu = std::move(it->second.buf); pdu.next_expected_so = pdu.sdu->N_bytes; RlcDebug("Reusing first segment of SN=%d for final SDU", it->second.header.sn); it = pdu.segments.erase(it); } else { RlcDebug("SDU buffer already allocated. Possible retransmission of first segment."); if (it->second.header.so != pdu.next_expected_so) { RlcError("Invalid PDU. SO doesn't match. Discarding all segments of SN=%d.", sn); rx_window.erase(sn); return; } } } else { if (it->second.buf->N_bytes > pdu.sdu->get_tailroom()) { RlcError("Cannot fit RLC PDU in SDU buffer (tailroom=%d, len=%d), dropping both. Erasing SN=%d.", rx_sdu->get_tailroom(), it->second.buf->N_bytes, it->second.header.sn); rx_window.erase(sn); metrics.num_lost_pdus++; return; } // add this segment to the end of the SDU buffer memcpy(pdu.sdu->msg + pdu.sdu->N_bytes, it->second.buf->msg, it->second.buf->N_bytes); pdu.sdu->N_bytes += it->second.buf->N_bytes; pdu.next_expected_so += it->second.buf->N_bytes; RlcDebug("Appended SO=%d of SN=%d", it->second.header.so, it->second.header.sn); it = pdu.segments.erase(it); if (pdu.next_expected_so == pdu.total_sdu_length) { // entire SDU has been received, it will be passed up the stack outside the loop sdu_complete = true; break; } } } else { // handle next segment ++it; } } if (sdu_complete) { // deliver full SDU to upper layers RlcInfo("Rx SDU (%d B)", pdu.sdu->N_bytes); pdcp->write_pdu(lcid, std::move(pdu.sdu)); // delete PDU from rx_window rx_window.erase(sn); // find next SN in rx buffer if (sn == RX_Next_Reassembly) { if (rx_window.empty()) { // no further segments received RX_Next_Reassembly = RX_Next_Highest; } else { for (auto it = rx_window.begin(); it != rx_window.end(); ++it) { RlcDebug("SN=%d has %zd segments", it->first, it->second.segments.size()); if (RX_MOD_NR_BASE(it->first) > RX_MOD_NR_BASE(RX_Next_Reassembly)) { RX_Next_Reassembly = it->first; break; } } } RlcDebug("Updating RX_Next_Reassembly=%d", RX_Next_Reassembly); } } else if (not sn_in_reassembly_window(sn)) { // SN outside of rx window RX_Next_Highest = (sn + 1) % mod; // update RX_Next_highest RlcDebug("Updating RX_Next_Highest=%d", RX_Next_Highest); // drop all SNs outside of new rx window for (auto it = rx_window.begin(); it != rx_window.end();) { if (not sn_in_reassembly_window(it->first)) { RlcInfo("SN=%d outside rx window [%d:%d] - discarding", it->first, RX_Next_Highest - UM_Window_Size, RX_Next_Highest); it = rx_window.erase(it); metrics.num_lost_pdus++; } else { ++it; } } if (not sn_in_reassembly_window(RX_Next_Reassembly)) { // update RX_Next_Reassembly to first SN that has not been reassembled and delivered for (const auto& rx_pdu : rx_window) { if (rx_pdu.first >= RX_MOD_NR_BASE(RX_Next_Highest - UM_Window_Size)) { RX_Next_Reassembly = rx_pdu.first; RlcDebug("Updating RX_Next_Reassembly=%d", RX_Next_Reassembly); break; } } } } if (reassembly_timer.is_running()) { if (RX_Timer_Trigger <= RX_Next_Reassembly || (not sn_in_reassembly_window(RX_Timer_Trigger) and RX_Timer_Trigger != RX_Next_Highest) || ((RX_Next_Highest == RX_Next_Reassembly + 1) && not has_missing_byte_segment(RX_Next_Reassembly))) { RlcDebug("stopping reassembly timer"); reassembly_timer.stop(); } } if (not reassembly_timer.is_running()) { if ((RX_MOD_NR_BASE(RX_Next_Highest) > RX_MOD_NR_BASE(RX_Next_Reassembly + 1)) || ((RX_MOD_NR_BASE(RX_Next_Highest) == RX_MOD_NR_BASE(RX_Next_Reassembly + 1)) && has_missing_byte_segment(RX_Next_Reassembly))) { RlcDebug("Starting reassembly timer for SN=%d", sn); reassembly_timer.run(); RX_Timer_Trigger = RX_Next_Highest; } } } else { RlcError("SN=%d does not exist in Rx buffer", sn); } } inline void rlc_um_nr::rlc_um_nr_rx::update_total_sdu_length(rlc_umd_pdu_segments_nr_t& pdu_segments, const rlc_umd_pdu_nr_t& rx_pdu) { if (rx_pdu.header.si == rlc_nr_si_field_t::last_segment) { pdu_segments.total_sdu_length = rx_pdu.header.so + rx_pdu.buf->N_bytes; RlcDebug("updating total SDU length for SN=%d to %d B", rx_pdu.header.sn, pdu_segments.total_sdu_length); } }; // Section 5.2.2.2.2 void rlc_um_nr::rlc_um_nr_rx::handle_data_pdu(uint8_t* payload, uint32_t nof_bytes) { std::lock_guard lock(mutex); rlc_um_nr_pdu_header_t header = {}; rlc_um_nr_read_data_pdu_header(payload, nof_bytes, cfg.um_nr.sn_field_length, &header); RlcHexDebug(payload, nof_bytes, "Rx data PDU (%d B)", nof_bytes); // check if PDU contains a SN if (header.si == rlc_nr_si_field_t::full_sdu) { // copy full PDU into buffer unique_byte_buffer_t sdu = rlc_um_nr_strip_pdu_header(header, payload, nof_bytes); // deliver to PDCP RlcInfo("Rx SDU (%d B)", sdu->N_bytes); pdcp->write_pdu(lcid, std::move(sdu)); } else if (sn_invalid_for_rx_buffer(header.sn)) { RlcInfo("Discarding SN=%d", header.sn); // Nothing else to do here .. } else { // place PDU in receive buffer rlc_umd_pdu_nr_t rx_pdu = {}; rx_pdu.header = header; rx_pdu.buf = rlc_um_nr_strip_pdu_header(header, payload, nof_bytes); // check if this SN is already present in rx buffer if (rx_window.find(header.sn) == rx_window.end()) { // first received segment of this SN, add to rx buffer RlcHexDebug(rx_pdu.buf->msg, rx_pdu.buf->N_bytes, "placing %s segment of SN=%d (%d B) in Rx buffer", to_string_short(header.si).c_str(), header.sn, rx_pdu.buf->N_bytes); rlc_umd_pdu_segments_nr_t pdu_segments = {}; update_total_sdu_length(pdu_segments, rx_pdu); pdu_segments.segments.emplace(header.so, std::move(rx_pdu)); rx_window[header.sn] = std::move(pdu_segments); } else { // other segment for this SN already present, update received data RlcHexDebug(rx_pdu.buf->msg, rx_pdu.buf->N_bytes, "updating SN=%d at SO=%d with %d B", rx_pdu.header.sn, rx_pdu.header.so, rx_pdu.buf->N_bytes); auto& pdu_segments = rx_window.at(header.sn); // calculate total SDU length update_total_sdu_length(pdu_segments, rx_pdu); // append to list of segments pdu_segments.segments.emplace(header.so, std::move(rx_pdu)); } // handle received segments handle_rx_buffer_update(header.sn); } debug_state(); } void rlc_um_nr::rlc_um_nr_rx::debug_state() { RlcDebug("RX_Next_Reassembly=%d, RX_Timer_Trigger=%d, RX_Next_Highest=%d, t_Reassembly=%s", RX_Next_Reassembly, RX_Timer_Trigger, RX_Next_Highest, reassembly_timer.is_running() ? "running" : "stopped"); } /**************************************************************************** * Header pack/unpack helper functions * Ref: 3GPP TS 38.322 v15.3.0 Section 6.2.2.3 ***************************************************************************/ uint32_t rlc_um_nr_read_data_pdu_header(const byte_buffer_t* pdu, const rlc_um_nr_sn_size_t sn_size, rlc_um_nr_pdu_header_t* header) { return rlc_um_nr_read_data_pdu_header(pdu->msg, pdu->N_bytes, sn_size, header); } uint32_t rlc_um_nr_read_data_pdu_header(const uint8_t* payload, const uint32_t nof_bytes, const rlc_um_nr_sn_size_t sn_size, rlc_um_nr_pdu_header_t* header) { uint8_t* ptr = const_cast(payload); header->sn_size = sn_size; // Fixed part if (sn_size == rlc_um_nr_sn_size_t::size6bits) { header->si = (rlc_nr_si_field_t)((*ptr >> 6) & 0x03); // 2 bits SI header->sn = *ptr & 0x3F; // 6 bits SN // sanity check if (header->si == rlc_nr_si_field_t::full_sdu and header->sn != 0) { fprintf(stderr, "Malformed PDU, reserved bits are set.\n"); return 0; } ptr++; } else if (sn_size == rlc_um_nr_sn_size_t::size12bits) { header->si = (rlc_nr_si_field_t)((*ptr >> 6) & 0x03); // 2 bits SI header->sn = (*ptr & 0x0F) << 8; // 4 bits SN if (header->si == rlc_nr_si_field_t::full_sdu and header->sn != 0) { fprintf(stderr, "Malformed PDU, reserved bits are set.\n"); return 0; } // sanity check if (header->si == rlc_nr_si_field_t::first_segment) { // make sure two reserved bits are not set if (((*ptr >> 4) & 0x03) != 0) { fprintf(stderr, "Malformed PDU, reserved bits are set."); return 0; } } if (header->si != rlc_nr_si_field_t::full_sdu) { // continue unpacking remaining SN ptr++; header->sn |= (*ptr & 0xFF); // 8 bits SN } ptr++; } else { fprintf(stderr, "Unsupported SN length\n"); return 0; } // Read optional part if (header->si == rlc_nr_si_field_t::last_segment || header->si == rlc_nr_si_field_t::neither_first_nor_last_segment) { // read SO header->so = (*ptr & 0xFF) << 8; ptr++; header->so |= (*ptr & 0xFF); ptr++; } // return consumed bytes return (ptr - payload); } uint32_t rlc_um_nr_packed_length(const rlc_um_nr_pdu_header_t& header) { uint32_t len = 0; if (header.si == rlc_nr_si_field_t::full_sdu) { // that's all .. len++; } else { if (header.sn_size == rlc_um_nr_sn_size_t::size6bits) { // Only 1B for SN len++; } else { // 2 B for 12bit SN len += 2; } if (header.so) { // Two bytes always for segment information len += 2; } } return len; } uint32_t rlc_um_nr_write_data_pdu_header(const rlc_um_nr_pdu_header_t& header, byte_buffer_t* pdu) { // Make room for the header uint32_t len = rlc_um_nr_packed_length(header); pdu->msg -= len; uint8_t* ptr = pdu->msg; // write SI field *ptr = (header.si & 0x03) << 6; // 2 bits SI if (header.si == rlc_nr_si_field_t::full_sdu) { // that's all .. ptr++; } else { if (header.sn_size == rlc_um_nr_sn_size_t::size6bits) { // write SN *ptr |= (header.sn & 0x3f); // 6 bit SN ptr++; } else { // 12bit SN *ptr |= (header.sn >> 8) & 0xf; // high part of SN (4 bit) ptr++; *ptr = (header.sn & 0xFF); // remaining 8 bit SN ptr++; } if (header.so) { // write SO *ptr = (header.so) >> 8; // first part of SO ptr++; *ptr = (header.so & 0xFF); // second part of SO ptr++; } } pdu->N_bytes += ptr - pdu->msg; return len; } } // namespace srsran