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 => 11,
    "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

[PIPE]Format[PIPE]Appointment[PIPE]Modulation[PIPE]Transform Precoding[PIPE]Features[PIPE]
[PIPE]:---[PIPE]:---[PIPE]:---[PIPE]:---[PIPE]:---[PIPE]
[PIPE]PUCCH1[PIPE]Short UCI (1-2 bits)[PIPE]Low PAPR[PIPE]Yes[PIPE]OCC[PIPE]
[PIPE]PUCCH2[PIPE]CSI[PIPE]QPSK[PIPE]No[PIPE]-[PIPE]
[PIPE]PUCCH3[PIPE]HARQ-ACK, SR[PIPE]QPSK + DFT[PIPE]Yes[PIPE]-[PIPE]
[PIPE]PUCCH4[PIPE]High capacity[PIPE]QPSK + Spreading[PIPE]Yes[PIPE]OCC + CDM[PIPE]

In [ ]:
neededLibs = ["EngeeDSP", "Statistics", "LinearAlgebra", "FFTW", "StatsBase"]
for lib in neededLibs
    try
        eval(Meta.parse("using $lib"))
    catch ex
        Pkg.add(lib)
        eval(Meta.parse("using $lib"))
    end
end

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("СТАТИСТИКА: $name")
    println("═"^50)
    println("   Тип данных:          $(eltype(signal))")
    println("   Размер:              $(size(signal))")
    println("   PAPR:                $(compute_papr(signal)) дБ")
    println("   Энергия:             $(round(sum(abs2.(signal)), digits=2))")
    println("   Средняя амплитуда:   $(round(mean(abs.(signal)), digits=4))")
    println("   Средняя фаза:        $(round(mean(angle.(signal)), digits=4)) рад")
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 user 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, "Сигнал y (после nrPUCCH1)")
compute_signal_stats(oSeq1, "Ортогональная последовательность 1")
compute_signal_stats(final, "Финальная последовательность")

p1 = analyze_constellation(y, "PUCCH1: Созвездие сигнала 'y'")
p2 = analyze_constellation(final, "PUCCH1: Финальное созвездие")
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 рад

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("\nСкремблированные биты (PRBS):")
println("   Кол-во бит: $(length(prbs_bits))")
println("   Баланс (1/0): $(sum(prbs_bits))/$(sum(.!prbs_bits))")

compute_signal_stats(modulated, "Модулированные QPSK символы")
unique_symbols = unique(round.(modulated, digits=6))
println("\nУникальные точки QPSK созвездия:")
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: Созвездие QPSK")

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

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("\nПреобразованные целочисленные данные:")
println("   Диапазон: $(minimum(converted)) - $(maximum(converted))")
println("   Среднее: $(round(mean(converted), digits=2))")
compute_signal_stats(final, "Сигнал после 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: Созвездие после 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("\nОценка ширины полосы:")
    println("   Ширина полосы по уровню 3 дБ: $(round(bandwidth, digits=4))")
else
    println("\nНе удалось оценить ширину полосы по уровню 3 дБ")
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

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, "До Block-wise Spreading")
compute_signal_stats(after_spreading, "После Spreading")
compute_signal_stats(final, "После Transform Precoding")
gain_factor = mean(abs2.(after_spreading)) / mean(abs2.(modulated))
println("\nЭффективность Block-wise Spreading:")
println("   Коэффициент усиления мощности: $(round(gain_factor, digits=2))")

plots = []
push!(plots, analyze_constellation(modulated, "До Spreading"))
push!(plots, analyze_constellation(after_spreading, "После Spreading"))
push!(plots, analyze_constellation(final, "Финальный сигнал"))
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

, 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.

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

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 a learning tool, but also as a valid basis for prototyping, testing and verifying real-world PUCCH signal processing algorithms in fifth-generation communication systems.