RRC architecture

How OCUDU implements Radio Resource Control — per-UE connection state, procedure coroutines (Setup, Reconfiguration, Reestablishment, Resume), SRB security, measurement configuration, and integration with F1AP and NGAP. Reference: 3GPP TS 38.331.

Target audience: RAN engineers. Scope: the RRC layer implementation in OCUDU — class ownership, UE state machine, procedure coroutines, PDU routing (SRB0/SRB1/SRB2), security context, measurement configuration, and how RRC integrates with CU-CP, F1AP, and NGAP. Reference: 3GPP TS 38.331.


1. What RRC Does

The RRC layer (3GPP TS 38.331) sits inside the CU-CP and owns three things: per-UE connection state (IDLE / CONNECTED / INACTIVE), the signalling dialog with each UE over SRB0/SRB1/SRB2, and the generation of all DL-CCCH and DL-DCCH messages. It interfaces downward to F1AP (to carry RRC PDUs over F1) and upward to NGAP (to forward NAS containers and initial attachment events).

In OCUDU the RRC implementation is split into two objects:

  • rrc_du_impl (lib/rrc/rrc_du_impl.h/.cpp) — one instance per gNB-CU. Holds the UE database, cell info, SIB1-derived timers, and the RRC Reject generator.
  • rrc_ue_impl (lib/rrc/ue/rrc_ue_impl.h/.cpp) — one instance per UE. Owns all per-UE context, SRB PDCP wrappers, and procedure coroutines.

2. Module Structure and Ownership

All adapters are defined in lib/cu_cp/adapters/rrc_ue_adapters.h. Each adapter translates one notifier interface into calls on the appropriate CU-CP or protocol layer. RRC never holds direct pointers to F1AP or NGAP objects.

Per-UE Context: rrc_ue_context_t (lib/rrc/ue/rrc_ue_context.h)

FieldTypeDescription
ue_indexue_index_tUnique UE index inside the gNB
c_rntirnti_tC-RNTI
staterrc_stateidle / connected / inactive
cellCell contextPCI, ARFCN, bands, SIB1-derived timers, PLMN list
plmn_idPLMNSelected PLMN from RRC Setup Complete
meas_cfgoptionalMeasurement objects, report configs, meas IDs
serving_cell_momeas_obj_idMeasurement object ID for serving cell
srbsmapSRB1/SRB2 → ue_srb_context (PDCP wrapper)
capabilities_listpacked ASN.1UE capability container list (stored verbatim)
capabilitiesparsed flagsCHO and RRC-Inactive support
transfer_contextoptionalSecurity + UP context from old UE during mobility
cell_group_configbyte_bufferPacked cell group config for DL RRC
reestablishment_ongoingboolActive reestablishment flag
pending_dl_nas_transport_messagesvectorNAS PDUs buffered during RRC Inactive

Cell Info and Timers

rrc_du_impl extracts four timers from SIB1 at F1 Setup time (rrc_du_impl.cpp:77–88):

cell_info.timers.t300 = sib1_msg.ue_timers_and_consts.t300.to_number();
cell_info.timers.t301 = sib1_msg.ue_timers_and_consts.t301.to_number();
cell_info.timers.t310 = sib1_msg.ue_timers_and_consts.t310.to_number();
cell_info.timers.t311 = sib1_msg.ue_timers_and_consts.t311.to_number();

These timers are used as procedure timeouts: T300 for RRC Setup, T311 for Reestablishment and Reconfiguration. An additional guard of 500 ms (rrc_procedure_guard_time_ms) is added to each timer to allow for transport delay.


3. RRC State Machine

State transitions happen exclusively inside procedure coroutines or inline handlers, never directly from the message dispatcher. The state field is checked at entry to procedures:

  • RRC Reconfiguration requires state == connected (rrc_reconfiguration_procedure.cpp:33).
  • RRC Resume falls back to RRC Setup if state == idle (rrc_ue_message_handlers.cpp:171).

4. Coroutine Infrastructure

Every multi-step RRC procedure is a C++ functor implementing operator()(coro_context<async_task<R>>&). The coroutine infrastructure uses CORO_BEGIN, CORO_AWAIT, CORO_AWAIT_VALUE, and CORO_RETURN macros from support/async/.

Transaction management:

rrc_ue_event_manager::transactions is a protocol_transaction_manager<rrc_outcome> with 4 slots (2-bit transaction ID 0–3). Each procedure creates one transaction:

transaction = event_mng.transactions.create_transaction(timeout);
send_rrc_<procedure>();
CORO_AWAIT(transaction);           // suspend here
if (transaction.has_response()) {
    auto& resp = transaction.response();  // ul_dcch_msg_s
    ...
}

The UL message dispatcher calls event_mng.transactions.handle_response(txn_id, ul_dcch_msg_s) to inject responses, which resumes the suspended coroutine.

Procedures are launched via launch_async<ProcedureType>(...) and scheduled on the UE task executor via cu_cp_ue_notifier.schedule_async_task(task).


5. RRC Procedures

5.1 RRC Setup (rrc_setup_procedure)

File: lib/rrc/ue/procedures/rrc_setup_procedure.h/.cpp
Return type: async_task<void>
Timeout: T300 from SIB1

sequenceDiagram
    participant UE
    participant DU
    participant RRC as rrc_ue_impl
    participant CU_CP as CU-CP

    UE->>DU: RRC Setup Request (CCCH)
    DU->>RRC: handle_ul_ccch_pdu → handle_rrc_setup_request
    RRC->>RRC: launch rrc_setup_procedure
    RRC->>RRC: create SRB1 (srb_notifier.create_srb)
    RRC->>DU: DL-CCCH: RRC Setup (via F1AP)
    Note over RRC: CORO_AWAIT(transaction, T300)
    UE->>DU: RRC Setup Complete (SRB1, PDCP protected)
    DU->>RRC: handle_ul_dcch_pdu → transaction.handle_response
    RRC->>RRC: validate selected PLMN
    RRC->>RRC: state = connected
    RRC->>CU_CP: cu_cp_notifier.on_ue_setup_complete_received(plmn)
    RRC->>CU_CP: ngap_notifier.on_initial_ue_message (if needed)

SRB0 is implicit (no PDCP). SRB1 is created with enable_security=false; ciphering is activated later after Security Mode Command. If the UE had a reestablishment or resume context, is_reestablishment_fallback or is_resume_fallback flags cause the procedure to forward an Initial UE Message to NGAP after setup completes.

5.2 RRC Reconfiguration (rrc_reconfiguration_procedure)

File: lib/rrc/ue/procedures/rrc_reconfiguration_procedure.h/.cpp
Return type: async_task<bool>
Timeout: T311 from SIB1

Handles: bearer setup/modification, measurement config updates, and handover (including CHO).

Request struct rrc_reconfiguration_procedure_request includes:

  • radio_bearer_cfg — SRB/DRB add/modify/release list.
  • secondary_cell_group — packed cell group config.
  • meas_cfg — measurement config delta.
  • meas_gap_cfg — measurement gap config.
  • non_crit_ext — v15.3.0 IEs (master cell group, NAS containers, master key update).
  • is_cho_preparation — if true, packs inner reconfiguration as plain ASN.1 (no DL-DCCH wrapper) for embedding in a conditional reconfiguration container.
  • cho_candidates — per-target-cell prepared inner reconfigs.
  • cho_cancellation_ids — conditional reconfig IDs to remove.

On timeout or cancellation the procedure returns false. On RRC Reconfiguration Complete it returns true.

5.3 RRC Reestablishment (rrc_reestablishment_procedure)

File: lib/rrc/ue/procedures/rrc_reestablishment_procedure.h/.cpp
Return type: async_task<void>
Timeout: T311

sequenceDiagram
    participant UE
    participant RRC as rrc_ue_impl
    participant CU_CP as CU-CP

    UE->>RRC: RRC Reestablishment Request (CCCH)<br/>old PCI, old C-RNTI, ShortMAC-I
    RRC->>CU_CP: on_rrc_reestablishment_request(old_pci, old_c_rnti)
    Note over RRC: is_reestablishment_accepted() verifies ShortMAC-I
    alt ShortMAC-I valid
        CU_CP-->>RRC: old UE context transferred
        RRC->>RRC: transfer_reestablishment_context_and_update_keys()<br/>horizontal key derivation
        RRC->>RRC: create SRB1 (security disabled)
        RRC->>UE: DL-CCCH: RRC Reestablishment
        RRC->>RRC: enable SRB1 ciphering
        Note over RRC: CORO_AWAIT(transaction, T311)
        UE->>RRC: RRC Reestablishment Complete (SRB1)
        RRC->>CU_CP: on_rrc_reestablishment_context_modification_required
        RRC->>CU_CP: on_rrc_reestablishment_complete(old_ue_index)
    else ShortMAC-I invalid or context transfer fails
        RRC->>RRC: handle_rrc_reestablishment_fallback()<br/>launches RRC Setup with fallback flag
    end

ShortMAC-I is verified via HMAC-SHA256 of the Reestablishment Request contents using the old UE’s security context. Horizontal key derivation computes new K_RRCenc/K_RRCint for the target cell from the old K using target PCI and ARFCN.

5.4 RRC Resume (rrc_resume_procedure)

File: lib/rrc/ue/procedures/rrc_resume_procedure.h/.cpp
Return type: async_task<void>
Timeout: T311

Transitions a UE from RRC_INACTIVE to CONNECTED. Key steps:

  1. Verify ResumeMAC-I and update security keys (horizontal derivation if target PCI differs).
  2. Notify CU-CP via on_rrc_resume_request() — async, returns rrc_resume_request_response.
  3. If response indicates RNA update (resume_cause == rna_upd), set UE back to INACTIVE and exit (no full reconnection).
  4. Otherwise: send RRC Resume on DL-CCCH, await RRC Resume Complete on SRB1.
  5. Send any buffered DL NAS PDUs from pending_dl_nas_transport_messages.
  6. Set state = connected.

Failure path: handle_rrc_resume_failure() calls cu_cp_notifier.on_ue_release_required().

5.5 Security Mode Command (inline)

There is no separate procedure class. The flow is:

  1. CU-CP calls get_security_mode_command_context() on the RRC UE.
  2. RRC packs security_mode_cmd_s (with selected ciphering and integrity algorithms) and sends on DL-DCCH/SRB1 — integrity-protected but not yet ciphered.
  3. CU-CP awaits response via handle_security_mode_complete_expected(transaction_id).
  4. UE responds with security_mode_complete_s on SRB1 (now ciphered and integrity-protected).
  5. handle_security_mode_complete() (rrc_ue_message_handlers.cpp:290) activates RX ciphering:
context.srbs.at(srb_id_t::srb1).enable_rx_security(
    security::integrity_enabled::on,
    security::ciphering_enabled::on,
    cu_cp_ue_notifier.get_rrc_128_as_config());

TX ciphering is enabled for subsequent messages.

5.6 UE Capability Transfer (rrc_ue_capability_transfer_procedure)

File: lib/rrc/ue/procedures/rrc_ue_capability_transfer_procedure.h/.cpp
Return type: async_task<bool>

  1. Pack ue_cap_enquiry_s with band list from context.cell.bands.
  2. Send on DL-DCCH/SRB1.
  3. Await ue_cap_info_s on SRB1.
  4. Extract ue_cap_rat_container_list_l and store in context.capabilities_list.
  5. Parse CHO and RRC-Inactive support flags into context.capabilities.

6. PDU Routing

DL Flow (RRC → UE)

  1. Procedure packs ASN.1 message (dl_ccch_msg_s for CCCH, dl_dcch_msg_s for DCCH).
  2. For SRB1/SRB2 (DCCH): PDCP entity ciphers and integrity-protects via srb_context.pack_rrc_pdu().
  3. f1ap_pdu_notifier.on_new_rrc_pdu(srb_id, pdu) routes to F1AP.
  4. F1AP builds f1ap_dl_rrc_message and sends DL RRC Message Transfer to DU.

UL Flow (UE → RRC)

  1. DU sends UL RRC Message Transfer; F1AP calls:
    • CCCH: rrc_ul_pdu_handler::handle_ul_ccch_pdu(pdu, c_rnti)
    • DCCH: rrc_ul_pdu_handler::handle_ul_dcch_pdu(srb_id, pdu)
  2. For DCCH: PDCP decrypts and verifies integrity.
  3. handle_ul_dcch_pdu() (rrc_ue_message_handlers.cpp:261) routes to the appropriate handler.

SRB Policy

SRBDirectionPDCPWhen created
SRB0BothNone (CCCH, no security)Always present
SRB1BothIntegrity only → cipher+integrity after SMCRRC Setup / Reestablishment
SRB2BothCipher + integrity from creationRRC Reconfiguration

SRB2 creation requires enable_security=true in the create_srb() call:

if (msg.srb_id == srb_id_t::srb2 || msg.enable_security) {
    security::sec_as_config sec_cfg = cu_cp_ue_notifier.get_rrc_as_config();
    srb_context.enable_full_security(security::truncate_config(sec_cfg));
}

(rrc_ue_impl.cpp:85–87)


7. Security Context

AS keys (K_RRCenc, K_RRCint) are owned by the CU-CP security manager, not by RRC. RRC accesses them through cu_cp_ue_notifier:

MethodPurpose
get_rrc_as_config()Fetch K_RRCenc/K_RRCint (float32 precision)
get_rrc_128_as_config()128-bit truncated variant
get_security_context()Full security context with master K
get_security_algos()Selected ciphering/integrity algorithm IDs
update_security_context(sec_ctxt)Store new context (e.g., from NGAP)
perform_horizontal_key_derivation(pci, arfcn)Derive new K’ for mobility

Horizontal key derivation is called by Reestablishment and Resume procedures when the target cell differs from the source. The CU-CP derives K’ = f(K, target_pci, target_arfcn) and computes new K_RRCenc/K_RRCint from K'.


8. Measurement Configuration

Config Types (include/ocudu/rrc/meas_types.h)

  • meas_id_t — enum [1..64], invalid=65.
  • meas_obj_id_t — enum [1..64], invalid=65.
  • report_cfg_id_t — enum [0..63], invalid=64.

Supporting structs: rrc_meas_timing, rrc_ssb_mtc (SSB measurement timing), rrc_ssb_cfg_mob, rrc_csi_rs_meas_bw.

Config Generation

CU-CP calls generate_meas_config() on the RRC UE to build the measurement config delta for an RRC Reconfiguration:

std::optional<rrc_meas_cfg>
generate_meas_config(
    const std::optional<rrc_meas_cfg>& current_meas_config = std::nullopt,
    bool cond_meas = false,
    span<const pci_t> candidate_pcis = {});

When cond_meas=true, only measurement objects and report configs for the listed candidate PCIs are included — used for CHO.

Measurement Reports

handle_measurement_report() (rrc_ue_message_handlers.cpp:320) unpacks meas_report_s, converts to rrc_meas_results, and passes to measurement_notifier.on_measurement_report(). The actual A1/A2/A3/A4/A5 event evaluation is done by the CU-CP mobility manager, not by RRC.


9. System Information (SIB1 Extraction)

rrc_du_impl extracts cell information from the SIB1 sent in the F1 Setup Request (rrc_du_impl.cpp:77–88):

  • Timers T300, T301, T310, T311.
  • PLMN identity list from cell_access_related_info.plmn_id_info_list.
  • Cell identity.

This extracted data is placed into cell_info and shared with every rrc_ue_impl created for that cell. The DU is responsible for generating and broadcasting SIBs over the air; RRC-CU only parses SIB1 for its own configuration purposes.


10. ASN.1 Layer

Namespace: asn1::rrc_nr

MessageDirectionContains
dl_ccch_msg_sDLrrc_setup_s, rrc_reest_s, rrc_resume_s, rrc_reject_s
ul_ccch_msg_sULrrc_setup_request_s, rrc_reest_request_s, rrc_resume_request_s
dl_dcch_msg_sDLrrc_recfg_s, security_mode_cmd_s, ue_cap_enquiry_s, rrc_release_s
ul_dcch_msg_sULrrc_setup_complete_s, rrc_recfg_complete_s, security_mode_complete_s, ue_cap_info_s, meas_report_s, ul_info_transfer_s

Message construction helpers (lib/rrc/ue/rrc_asn1_helpers.h):

  • fill_asn1_rrc_smc_msg() — Security Mode Command.
  • fill_asn1_rrc_reconfiguration_msg() — full reconfiguration with SRB/DRB/meas config.
  • fill_asn1_rrc_resume_msg() — RRC Resume.
  • fill_asn1_rrc_ue_capability_enquiry() — UE Capability Enquiry (inline in procedure header).

11. Procedure Cross-Reference

ProcedureTS 38.331FileClassPatternStatus
RRC SetupSection 5.3.3rrc_setup_procedure.cpprrc_setup_procedureasync_task<void>, CORO_AWAIT(txn), T300Implemented
RRC ReconfigurationSection 5.3.5rrc_reconfiguration_procedure.cpprrc_reconfiguration_procedureasync_task<bool>, CORO_AWAIT(txn), T311Implemented
RRC ReestablishmentSection 5.3.7rrc_reestablishment_procedure.cpprrc_reestablishment_procedureasync_task<void>, CORO_AWAIT(txn), T311Implemented
RRC ReleaseSection 5.3.8rrc_ue_impl.cpp (inline)get_rrc_ue_release_context(), no coroutineImplemented
RRC ResumeSection 5.3.13rrc_resume_procedure.cpprrc_resume_procedureasync_task<void>, CORO_AWAIT(txn), T311Implemented
Security Mode CommandSection 5.3.4rrc_ue_message_handlers.cpp:290InlineMessage-level, transaction ID exchangedImplemented (inline)
UE Capability EnquirySection 5.6.1rrc_ue_capability_transfer_procedure.cpprrc_ue_capability_transfer_procedureasync_task<bool>, CORO_AWAIT(txn)Implemented
DL Information TransferSection 5.7.2Inline NAS forwarding, no coroutineImplemented (message-level)
UL Information TransferSection 5.7.3rrc_ue_message_handlers.cpp:127Inline, forwards NAS to NGAPImplemented (message-level)
Handover Command HandlingSection 5.3.5.5rrc_ue_impl.cpp:80handle_rrc_handover_command(), inline unpackImplemented (partial)
Conditional ReconfigurationSection 5.3.5.13rrc_reconfiguration_procedure.cppis_cho_preparation flag, no separate classPartial — packing supported
Counter CheckSection 5.7.1Not implemented
UE InformationNot implemented