RRC architecture
9 minute read
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)
| Field | Type | Description |
|---|---|---|
ue_index | ue_index_t | Unique UE index inside the gNB |
c_rnti | rnti_t | C-RNTI |
state | rrc_state | idle / connected / inactive |
cell | Cell context | PCI, ARFCN, bands, SIB1-derived timers, PLMN list |
plmn_id | PLMN | Selected PLMN from RRC Setup Complete |
meas_cfg | optional | Measurement objects, report configs, meas IDs |
serving_cell_mo | meas_obj_id | Measurement object ID for serving cell |
srbs | map | SRB1/SRB2 → ue_srb_context (PDCP wrapper) |
capabilities_list | packed ASN.1 | UE capability container list (stored verbatim) |
capabilities | parsed flags | CHO and RRC-Inactive support |
transfer_context | optional | Security + UP context from old UE during mobility |
cell_group_config | byte_buffer | Packed cell group config for DL RRC |
reestablishment_ongoing | bool | Active reestablishment flag |
pending_dl_nas_transport_messages | vector | NAS 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
endShortMAC-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:
- Verify ResumeMAC-I and update security keys (horizontal derivation if target PCI differs).
- Notify CU-CP via
on_rrc_resume_request()— async, returnsrrc_resume_request_response. - If response indicates RNA update (
resume_cause == rna_upd), set UE back to INACTIVE and exit (no full reconnection). - Otherwise: send RRC Resume on DL-CCCH, await RRC Resume Complete on SRB1.
- Send any buffered DL NAS PDUs from
pending_dl_nas_transport_messages. - 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:
- CU-CP calls
get_security_mode_command_context()on the RRC UE. - RRC packs
security_mode_cmd_s(with selected ciphering and integrity algorithms) and sends on DL-DCCH/SRB1 — integrity-protected but not yet ciphered. - CU-CP awaits response via
handle_security_mode_complete_expected(transaction_id). - UE responds with
security_mode_complete_son SRB1 (now ciphered and integrity-protected). 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>
- Pack
ue_cap_enquiry_swith band list fromcontext.cell.bands. - Send on DL-DCCH/SRB1.
- Await
ue_cap_info_son SRB1. - Extract
ue_cap_rat_container_list_land store incontext.capabilities_list. - Parse CHO and RRC-Inactive support flags into
context.capabilities.
6. PDU Routing
DL Flow (RRC → UE)
- Procedure packs ASN.1 message (
dl_ccch_msg_sfor CCCH,dl_dcch_msg_sfor DCCH). - For SRB1/SRB2 (DCCH): PDCP entity ciphers and integrity-protects via
srb_context.pack_rrc_pdu(). f1ap_pdu_notifier.on_new_rrc_pdu(srb_id, pdu)routes to F1AP.- F1AP builds
f1ap_dl_rrc_messageand sends DL RRC Message Transfer to DU.
UL Flow (UE → RRC)
- 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)
- CCCH:
- For DCCH: PDCP decrypts and verifies integrity.
handle_ul_dcch_pdu()(rrc_ue_message_handlers.cpp:261) routes to the appropriate handler.
SRB Policy
| SRB | Direction | PDCP | When created |
|---|---|---|---|
| SRB0 | Both | None (CCCH, no security) | Always present |
| SRB1 | Both | Integrity only → cipher+integrity after SMC | RRC Setup / Reestablishment |
| SRB2 | Both | Cipher + integrity from creation | RRC 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:
| Method | Purpose |
|---|---|
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
| Message | Direction | Contains |
|---|---|---|
dl_ccch_msg_s | DL | rrc_setup_s, rrc_reest_s, rrc_resume_s, rrc_reject_s |
ul_ccch_msg_s | UL | rrc_setup_request_s, rrc_reest_request_s, rrc_resume_request_s |
dl_dcch_msg_s | DL | rrc_recfg_s, security_mode_cmd_s, ue_cap_enquiry_s, rrc_release_s |
ul_dcch_msg_s | UL | rrc_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
| Procedure | TS 38.331 | File | Class | Pattern | Status |
|---|---|---|---|---|---|
| RRC Setup | Section 5.3.3 | rrc_setup_procedure.cpp | rrc_setup_procedure | async_task<void>, CORO_AWAIT(txn), T300 | Implemented |
| RRC Reconfiguration | Section 5.3.5 | rrc_reconfiguration_procedure.cpp | rrc_reconfiguration_procedure | async_task<bool>, CORO_AWAIT(txn), T311 | Implemented |
| RRC Reestablishment | Section 5.3.7 | rrc_reestablishment_procedure.cpp | rrc_reestablishment_procedure | async_task<void>, CORO_AWAIT(txn), T311 | Implemented |
| RRC Release | Section 5.3.8 | rrc_ue_impl.cpp (inline) | — | get_rrc_ue_release_context(), no coroutine | Implemented |
| RRC Resume | Section 5.3.13 | rrc_resume_procedure.cpp | rrc_resume_procedure | async_task<void>, CORO_AWAIT(txn), T311 | Implemented |
| Security Mode Command | Section 5.3.4 | rrc_ue_message_handlers.cpp:290 | Inline | Message-level, transaction ID exchanged | Implemented (inline) |
| UE Capability Enquiry | Section 5.6.1 | rrc_ue_capability_transfer_procedure.cpp | rrc_ue_capability_transfer_procedure | async_task<bool>, CORO_AWAIT(txn) | Implemented |
| DL Information Transfer | Section 5.7.2 | — | — | Inline NAS forwarding, no coroutine | Implemented (message-level) |
| UL Information Transfer | Section 5.7.3 | rrc_ue_message_handlers.cpp:127 | — | Inline, forwards NAS to NGAP | Implemented (message-level) |
| Handover Command Handling | Section 5.3.5.5 | rrc_ue_impl.cpp:80 | — | handle_rrc_handover_command(), inline unpack | Implemented (partial) |
| Conditional Reconfiguration | Section 5.3.5.13 | rrc_reconfiguration_procedure.cpp | — | is_cho_preparation flag, no separate class | Partial — packing supported |
| Counter Check | Section 5.7.1 | — | — | — | Not implemented |
| UE Information | — | — | — | — | Not implemented |