Engee documentation
Notebook

PUCCH signals in 5G

The article presents a system for generating and processing uplink signals in the 5G standard, namely– the PUCCH (Physical Uplink Control Channel) control channel. This example allows us to visually explore the differences in signal processing chains for each format, comparing their structural complexity, the methods of modulation, encoding and spectrum expansion used. PUCCH (Physical Uplink Control Channel) is a physical uplink channel in 5G NR designed exclusively for transmitting UCI (Uplink Control Information) control information from user equipment (UE) to the base station (gNB).

PUCCH provides transmission of three main types of control information (UCI):

  1. HARQ-ACK — confirmation of incoming data reception (HARQ-ACK/NACK).
  2. CSI (Channel State Information) — Radio channel status reports (CQI, RI, PMI).
  3. SR (Scheduling Request) — Request to allocate resources for data transmission.

Unlike
the PUSCH data channel, PUCCH is optimized for transmitting small amounts of service information with minimal latency and high reliability. In 5G NR, the channel is implemented with increased flexibility compared to LTE: support for multiple formats (0-4), configurable duration (1-14 OFDM characters), dynamic resource allocation and frequency/time multiplexing.

image.png

The model implements and compares the PUCCH 1-4 formats (PUCCH 0 is not implemented because it is designed for 1-2 bits with an ultra-short duration). Each format is optimized for transmitting certain types of control information (UCI – Uplink Control Information).

In [ ]:
function run_model( name_model)
    Path = (@__DIR__) * "/" * name_model * ".engee"
    if name_model in [m.name for m in engee.get_all_models()] 
        model = engee.open( name_model ) 
        model_output = engee.run( model, verbose=true );
    else
        model = engee.load( Path, force=true ) 
        model_output = engee.run( model, verbose=true );
        engee.close( name_model, force=true );
    end
    sleep(0.1)
    return model_output
end
run_model("PUCCH_model") 
Building...
Progress 0%
Progress 100%
Out[0]:
SimulationResult(
    run_id => 1,
    "PUCCH PRBS.1" => WorkspaceArray{Vector{Bool}}("PUCCH_model/PUCCH4/PUCCH PRBS.1")
,
    "In1.1" => WorkspaceArray{Vector{Int64}}("PUCCH_model/PUCCH2/In1.1")
,
    "Transform Precoding.1" => WorkspaceArray{Vector{ComplexF64}}("PUCCH_model/PUCCH4/Transform Precoding.1")
,
    "Spreading.Out1" => WorkspaceArray{Vector{ComplexF64}}("PUCCH_model/PUCCH4/Spreading.Out1")
,
    "Convert in Int.1" => WorkspaceArray{Vector{Int64}}("PUCCH_model/PUCCH3/Convert in Int.1")
,
    "Transform Precoding-1.1" => WorkspaceArray{Vector{ComplexF64}}("PUCCH_model/PUCCH3/Transform Precoding-1.1")
,
    "Symbol Modulate-1.1" => WorkspaceArray{Vector{ComplexF64}}("PUCCH_model/PUCCH2/Symbol Modulate-1.1")
,
    "# Get the orthogonal sequence from spreading factor and orthogonal
        # cover code index.oSeq" => WorkspaceArray{Vector{ComplexF64}}("PUCCH_model/PUCCH1/# Get the orthogonal sequence from spreading factor and orthogonal
        # cover code index.oSeq")
,
    "Spreading Format-1.1" => WorkspaceArray{Vector{ComplexF64}}("PUCCH_model/PUCCH1/Spreading Format-1.1")
,
    "# Get the PUCCH format 1 sequence.Seq" => WorkspaceArray{Vector{ComplexF64}}("PUCCH_model/PUCCH1/# Get the PUCCH format 1 sequence.Seq")
,
    "Spreading Format.1" => WorkspaceArray{Vector{ComplexF64}}("PUCCH_model/PUCCH1/Spreading Format.1")
,
    "Symbol Modulate.1" => WorkspaceArray{Vector{ComplexF64}}("PUCCH_model/PUCCH4/Symbol Modulate.1")
,
    "PUCCH PRBS-1.1" => WorkspaceArray{Vector{Bool}}("PUCCH_model/PUCCH2/PUCCH PRBS-1.1")
,
    "# Parse and validate inputs.y" => WorkspaceArray{Matrix{ComplexF64}}("PUCCH_model/PUCCH1/# Parse and validate inputs.y")

)

Comparative analysis of PUCCH formats implemented in the model:

Technical characteristics of PUCCH formats

Format Purpose Modulation Transform Precoding Features
PUCCH1 Short UCI (1-2 bits) Low PAPR Yes OCC
PUCCH2 CSI QPSK No -
PUCCH3 HARQ-ACK, SR QPSK + DFT Yes -
PUCCH4 High Capacity QPSK+Spreading Yes OCC+ CDM
In [ ]:
using EngeeDSP
using Statistics, LinearAlgebra
using FFTW  
using StatsBase 

function unwrap(phase::Vector{Float64})
    unwrapped = copy(phase)
    for i in 2:length(phase)
        diff = phase[i] - phase[i-1]
        if diff > π
            unwrapped[i] -= 2π
        elseif diff < -π
            unwrapped[i] += 2π
        end
    end
    return unwrapped
end

function compute_papr(signal::AbstractArray{<:Complex})
    power = abs2.(signal)
    papr_db = 10 * log10(maximum(power) / mean(power))
    return round(papr_db, digits=2)
end

function compute_spectrum(signal::AbstractArray{<:Complex}, fs=1.0)
    N = length(signal)
    fft_result = fft(signal)
    freqs = fftfreq(N, fs)
    pwr = abs2.(fft_result) / N
    return freqs, pwr
end

function analyze_constellation(signal::AbstractArray{<:Complex}, title)
    p = scatter(real.(signal), imag.(signal),
                title=title, xlabel="In-phase (I)", ylabel="Quadrature (Q)",
                legend=false, markersize=4, markerstrokewidth=0, alpha=0.7,
                aspect_ratio=:equal)
    return p
end

function compute_signal_stats(signal::AbstractArray{<:Complex}, name)
    println("\n" * "═"^50)
    println("statistics: $name")
    println("═"^50)
    println("   Data type:          $(eltype(signal))")
    println("   Size:              $(size(signal))")
    println("   PAPR: $(compute_papr(signal)) dB")
    println("   Energy:             $(round(sum(abs2.(signal)), digits=2))")
    println("   Average amplitude: $(round(mean(abs.(signal)), digits=4))")
    println("   Average phase: $(round(mean(angle.(signal)), digits=4)) glad")
end
Out[0]:
compute_signal_stats (generic function with 1 method)

PUCCH Format 1: Simulates the transmission of short control information (1-2 bits) using sequences with a low PAPR (peak factor). Includes operations:

  1. Generation of low-PAPR sequences (nrLowPAPRS).

  2. External parameter management via custom blocks (Engee Function).

  3. The use of Orthogonal Cover Codes (OCC – Orthogonal Cover Codes) through the blocks Spreading Format to separate users.

image.png
In [ ]:
y = collect(simout["PUCCH_model/PUCCH1/# Parse and validate inputs.y"]).value[end]
oSeq1 = collect(simout["PUCCH_model/PUCCH1/Spreading Format-1.1"]).value[end]
final = collect(simout["PUCCH_model/PUCCH1/# Get the PUCCH format 1 sequence.Seq"]).value[end]

compute_signal_stats(y, "Signal y (after nrPUCCH1)")
compute_signal_stats(oSeq1, "Orthogonal sequence 1")
compute_signal_stats(final, "The final sequence")

p1 = analyze_constellation(y, "PUCCH1: The constellation of the 'y' signal")
p2 = analyze_constellation(final, "PUCCH1: The Final Constellation")
p3 = plot(abs.(final), title="PUCCH1: The amplitude envelope",
            xlabel="The counts", ylabel="The amplitude", legend=false, grid=true)
display(plot(p1, p2, p3, layout=(1,3), size=(1200, 400)))
savefig("pucch1_analysis.png")
══════════════════════════════════════════════════
СТАТИСТИКА: Сигнал y (после nrPUCCH1)
══════════════════════════════════════════════════
   Тип данных:          ComplexF64
   Размер:              (12, 7)
   PAPR:                0.0 дБ
   Энергия:             84.0
   Средняя амплитуда:   1.0
   Средняя фаза:        -0.2805 рад

══════════════════════════════════════════════════
СТАТИСТИКА: Ортогональная последовательность 1
══════════════════════════════════════════════════
   Тип данных:          ComplexF64
   Размер:              (4,)
   PAPR:                0.0 дБ
   Энергия:             4.0
   Средняя амплитуда:   1.0
   Средняя фаза:        1.5708 рад

══════════════════════════════════════════════════
СТАТИСТИКА: Финальная последовательность
══════════════════════════════════════════════════
   Тип данных:          ComplexF64
   Размер:              (84,)
   PAPR:                0.0 дБ
   Энергия:             84.0
   Средняя амплитуда:   1.0
   Средняя фаза:        0.2431 рад
Out[0]:
"/user/my_projects/Demo/5G_PUCCH/pucch1_analysis.png"

PUCCH Format 2: Is intended for transmission of CSI (Channel State Information) of longer length. The processing chain includes:

  1. Scrambling using a pseudorandom sequence generator (PUCCH PRBS).

  2. QPSK modulation (Symbol Modulate).

  3. Relatively simple structure compared to other formats.

image.png
In [ ]:
prbs_bits = collect(simout["PUCCH_model/PUCCH2/PUCCH PRBS-1.1"]).value[end]
modulated = collect(simout["PUCCH_model/PUCCH2/Symbol Modulate-1.1"]).value[end]
println("Scrambled Bits (PRBS):")
println("   Number of bits: $(length(prbs_bits))")
println("   Balance (1/0): $(sum(prbs_bits))/$(sum(.!prbs_bits))")

compute_signal_stats(modulated, "QPSK modulated characters")
unique_symbols = unique(round.(modulated, digits=6))
println("\Punic points of the QPSK constellation:")
for sym in unique_symbols
    println("   I=$(round(real(sym), digits=4)), Q=$(round(imag(sym), digits=4))")
end

phase = angle.(modulated)
instant_freq = diff(unwrap(phase)) 
p1 = histogram(instant_freq, bins=30, title="PUCCH2: Instantaneous frequency",
                xlabel="Frequency", ylabel="Frequency of occurrence", legend=false)
p2 = analyze_constellation(modulated, "PUCCH2: The QPSK Constellation")

display(plot(p1, p2, layout=(1,2), size=(1200, 400)))
savefig("pucch2_analysis.png")
Скремблированные биты (PRBS):
   Кол-во бит: 96
   Баланс (1/0): 44/52

══════════════════════════════════════════════════
СТАТИСТИКА: Модулированные QPSK символы
══════════════════════════════════════════════════
   Тип данных:          ComplexF64
   Размер:              (48,)
   PAPR:                0.0 дБ
   Энергия:             48.0
   Средняя амплитуда:   1.0
   Средняя фаза:        -0.1309 рад

Уникальные точки QPSK созвездия:
   I=0.7071, Q=-0.7071
   I=-0.7071, Q=-0.7071
   I=-0.7071, Q=0.7071
   I=0.7071, Q=0.7071
Out[0]:
"/user/my_projects/Demo/5G_PUCCH/pucch2_analysis.png"

PUCCH Format 3: Is used to transmit HARQ-ACK and SR medium capacity. Features:

  1. Converting logical bits to integer values.

  2. Symbolic Modulation (QPSK).

  3. Transform Precoding, using DFT (Discrete Fourier Transform) before OFDM modulation, which is typical for subcarriers with DFT-s-OFDM (SC-FDMA) in the uplink.

image.png
In [ ]:
converted = collect(simout["PUCCH_model/PUCCH3/Convert in Int.1"]).value[end]
final = collect(simout["PUCCH_model/PUCCH3/Transform Precoding-1.1"]).value[end]
println("\Converted integer data:")
println("   Range: $(minimum(converted)) - $(maximum(converted))")
println("   Average: $(round(mean(converted), digits=2))")
compute_signal_stats(final, "The signal after Transform Precoding")

N = length(final)
fs = 1.0 
fft_result = fft(final)
freqs = fftfreq(N, fs)
pwr = abs2.(fft_result) / N
p1 = plot(freqs, pwr, title="PUCCH3: Power spectral Density",
            xlabel="Frequency", ylabel="Power (dB)", legend=false, grid=true,
            xlim=(-0.5, 0.5))
p2 = analyze_constellation(final, "PUCCH3: Constellation after DFT")

max_pwr = maximum(pwr)
half_pwr = max_pwr / 2 
indices = findall(pwr .> half_pwr)
if !isempty(indices)
    min_freq = minimum(freqs[indices])
    max_freq = maximum(freqs[indices])
    bandwidth = max_freq - min_freq
    println("\Lane width mark-up:")
    println("   Bandwidth at 3 dB level: $(round(bandwidth, digits=4))")
else
    println("We have not been able to estimate the bandwidth at the 3 dB level")
end
display(plot(p1, p2, layout=(1,2), size=(900, 400)))
savefig("pucch3_analysis.png")
Преобразованные целочисленные данные:
   Диапазон: 0 - 1
   Среднее: 0.57

══════════════════════════════════════════════════
СТАТИСТИКА: Сигнал после Transform Precoding
══════════════════════════════════════════════════
   Тип данных:          ComplexF64
   Размер:              (48,)
   PAPR:                13.8 дБ
   Энергия:             48.0
   Средняя амплитуда:   0.2041
   Средняя фаза:        0.0327 рад

Оценка ширины полосы:
   Ширина полосы по уровню 3 дБ: 0.9583
Out[0]:
"/user/my_projects/Demo/5G_PUCCH/pucch3_analysis.png"

PUCCH Format 4: Is designed to transmit high-capacity control information for a single user or with code division multiplexing for multiple users. It has the most complex structure:

  1. Sequential scrambling, QPSK modulation.

  2. Block-wise Spreading implemented in a separate subsystem Spreading. This process multiplies modulated symbols by orthogonal cover codes (OCCs) to create multiple copies of the signal, which increases reliability and/or allows multiplexing of users.

  3. Transform precoding.

image.png
In [ ]:
prbs = collect(simout["PUCCH_model/PUCCH4/PUCCH PRBS.1"]).value[end]
modulated = collect(simout["PUCCH_model/PUCCH4/Symbol Modulate.1"]).value[end]
after_spreading = collect(simout["PUCCH_model/PUCCH4/Spreading.Out1"]).value[end]
final = collect(simout["PUCCH_model/PUCCH4/Transform Precoding.1"]).value[end]
compute_signal_stats(modulated, "Before Block-wise Spreading")
compute_signal_stats(after_spreading, "After Spreading")
compute_signal_stats(final, "After Transform Precoding")
gain_factor = mean(abs2.(after_spreading)) / mean(abs2.(modulated))
println("Efficiency of Block-wise Spreading:")
println("   Power gain: $(round(gain_factor, digits=2))")

plots = []
push!(plots, analyze_constellation(modulated, "Before Spreading"))
push!(plots, analyze_constellation(after_spreading, "After Spreading"))
push!(plots, analyze_constellation(final, "The final signal"))
p4 = plot(abs.(modulated), label="Before Spreading", linewidth=1.5, alpha=0.8)
plot!(p4, abs.(after_spreading), label="After Spreading", linewidth=1, alpha=0.7)
plot!(p4, abs.(final), label="After Precoding", linewidth=1, alpha=0.7,
        title="PUCCH4: Comparing amplitudes", xlabel="The counts", ylabel="The amplitude", grid=true)
push!(plots, p4)
display(plot(plots..., layout=(2,2), size=(1000, 800)))
savefig("pucch4_analysis.png")
══════════════════════════════════════════════════
СТАТИСТИКА: До Block-wise Spreading
══════════════════════════════════════════════════
   Тип данных:          ComplexF64
   Размер:              (48,)
   PAPR:                0.0 дБ
   Энергия:             48.0
   Средняя амплитуда:   1.0
   Средняя фаза:        0.3272 рад

══════════════════════════════════════════════════
СТАТИСТИКА: После Spreading
══════════════════════════════════════════════════
   Тип данных:          ComplexF64
   Размер:              (96,)
   PAPR:                0.0 дБ
   Энергия:             96.0
   Средняя амплитуда:   1.0
   Средняя фаза:        -0.0654 рад

══════════════════════════════════════════════════
СТАТИСТИКА: После Transform Precoding
══════════════════════════════════════════════════
   Тип данных:          ComplexF64
   Размер:              (96,)
   PAPR:                8.73 дБ
   Энергия:             96.0
   Средняя амплитуда:   0.6323
   Средняя фаза:        0.2887 рад

Эффективность Block-wise Spreading:
   Коэффициент усиления мощности: 1.0
Out[0]:
"/user/my_projects/Demo/5G_PUCCH/pucch4_analysis.png"

, Key modeling results

📈 PAPR (peak factor)

  • PUCCH1: 0.0 dB — low-PAPR sequences, ideal for UE energy efficiency
  • PUCCH2: 0.0 dB — QPSK with constant envelope in the model
  • PUCCH3: 13.8 dB — high PAPR is typical for DFT-s-OFDM
  • PUCCH4: 8.73 dB — balance between capacity and power consumption

🔧 Specific features

  • PUCCH1: low-PAPR+ OCC — for short ACK/NACK and SR
  • PUCCH2: Simple QPSK chain — for periodic CSI reports
  • PUCCH3: DFT-s-OFDM — for HARQ-ACK+SR medium capacity
  • PUCCH4: Block-wise Spreading + CDM — for high capacity and multi-user scenarios

💾 Amount of data

  • PUCCH1: 84 characters (low efficiency, ~0.024 bits/character)
  • PUCCH2: 48 characters (1.0 bit/character)
  • PUCCH3: 48 characters (1.0 bit/character)
  • PUCCH4: 96 characters after expansion (1.0 bit/character)

⚡ Comparison with theory

All results comply with 3GPP specifications:

  • PUCCH1: Low PAPR as designed for IoT devices
  • PUCCH3: high PAPR (13.8 dB) expected for DFT-s-OFDM
  • PUCCH4: spreading gives 1.0 power gain — orthogonal codes to separate users, not gain
  • Resource efficiency increases from PUCCH1 to PUCCH4 in proportion to the complexity of processing

Conclusion

All four PUCCH formats are correctly implemented in the model, and their characteristics comply with 3GPP 5G NR specifications.

Format Purpose Advantages
PUCCH1 Short control information Minimum power consumption, low PAPR
PUCCH2 CSI transmission (Channel status information) Easy to implement, stable operation
PUCCH3 HARQ-ACK and scheduling requests Balance of capacity and reliability
PUCCH4 High capacity, multi-user access CDM support, extended functionality

Key conclusions on the implementation of PUCCH formats in the 5G NR model:

  1. Each of the four PUCCH formats implements a well-defined compromise between capacity, latency, and energy efficiency, which allows adaptive configuration to meet the specific requirements of a network scenario.

  2. The model clearly demonstrates the direct relationship between the type of control information transmitted (UCI) and the complexity of the signal path — from the minimalistic PUCCH 0 for short confirmations to the high-capacity PUCCH 4 with CDM support for multi-user operation.

  3. All critical components of the physical layer — scrambling, modulation, transform precoding and spectrum expansion — are implemented in strict accordance with 3GPP TS 38.211 specifications, ensuring full compatibility of the model with the standard.

  4. The proposed implementation serves not only as an educational tool, but also as a valid basis for prototyping, testing and verifying real-world PUCCH signal processing algorithms in fifth-generation communication systems.