New Radio uses OFDM for the downlink multiple access and supports Single Carrier OFDM (SC-OFDM) for the uplink multiple access. The specifications describe the uplink access scheme as Discrete Fourier Transform-spread-OFDM (DFT-s-OFDM); however, this is more commonly called SC-OFDM.

A key parameter of OFDM is the number of sub-carriers (the size of IDFT to perform), which combined with an ADC sample rate provides a sub-carrier spacing. New Radio simplifies the choices by using a fixed-size IDFT of $N_f=4096$ and defining a numerology parameter that defines a subcarrier spacing ($\Delta f$) and thus a total frequency bandwidth. OFDM also uses a cyclic prefix, which is merely a copy of some OFDM samples to the beginning (prefixing) of the symbol. New Radio specifies two cyclic prefix modes: normal an extended. Considering the number of parameters for the physical air interface has generally be simplified it is curious that a single numerology supports an extended cyclic prefix which just shortens the useful signaling time. If you're wondering why an extended prefix exists it may be useful to know that the 3GPP chairman of Ran1 group filed a patent for cyclic prefix management in New Radio prior to the 3GPP release. The numerology and cyclic prefix modes map to the following parameters:

$\mu$ $\Delta f$ (scs) [kHz] Cyclic Prefix Slots/Subframe OFDM Symbols/slot
0 15 normal 1 14
1 30 normal 2 14
2 60 normal 4 14
2 60 extended 4 12
3 120 normal 8 14
4 240 normal 16 14

The $\Delta f$ (scs) can also be derived by $15 \cdot 2^\mu$.

Each frame is 10 ms long and is divided in to two half frames which are further divided in to 5 subframes each. Subframes have a number of slots determined by numerology so that the time division of a frame in to smaller elements follows the following figure

A New Radio frame is divided in to half frames, sub frames, slots, and eventually OFDM symbols

Further details of the New Radio frame structure and timing require the following values which can be found in the TS 38.211 Specification

Meaning Variable name Value Derivation Ref
Ref # of subcarriers $N_{f,ref}$ 2048 Magic number (inherited from LTE) TS 38.211 §4.1
Number of subcarriers $N_f$ 4096 Magic number TS 38.211 §4.1
Carriers per resource block $N_{sc}^{RB}$ 12 Magic Number TS 38.211 §4.4.4.1
Reference subcarrier spacing $\Delta f_{rerf}$ 15000 Magic Number (inherited from LTE) TS 38.211 §4.1
Maximum subcarrier spacing $\Delta f_{max}$ 480000 Magic Number TS 38.211 §4.1
Basic time unit for New Radio $T_c$ .5086 nsec $\frac{1}{\Delta f_{max} N_f}$ TS 38.211 §4.1
Basic time unit for LTE $T_s$ 32.55 nsec $\frac{1}{\Delta f_{ref} N_{f,ref}}$ TS 38.211 §4.1
Scaling factor for LTE to New Radio $\kappa$ 64 $\frac{T_s}{T_c}$ TS 38.211 §4.1
Frame duration $T_f$ 10 msec $\Delta \frac{N_f}{100} T_c$ TS 38.211 §4.3.1
Sub Frame duration $T_{sf}$ 1 msec $\Delta \frac{N_f}{1000} T_c$ TS 38.211 §4.3.1

As an excel spreadsheet we have the same values

With those timing values, numerology, and cyclic prefix mode of normal, the cyclic prefix durations can be computed using a sample rate that would match the required bandwidth for the IDFT size of 4096 used in New Radio. There is slight complexity here because New Radio specifies two different cyclic prefix lengths for the Normal CP mode (TS 38.211 §5.3.1). Using these values in excel we can compute the CP lengths as

New Radio Frame Structure Details for Normal Cyclic Prefix modes in Release 15

As a summary, based on subcarrier spacing we have the following sample rates and cyclic prefix durations. Time durations in New Radio are always provided relative to $T_c$ which we will have below and convert to samples.

Cyclic Prefix Numerology µ Subcarrier Spacing (kHz) Sample Rate (MHz) Slots per subframe Slot duration (msec) CP duration ($T_c$) CP duration (samples) CP0 duration ($T_c$) CP0 duration (samples)
Normal 0 15 61.84 1 1 9216 288 10240 320
Normal 1 30 122.88 2 .5 4608 288 5632 352
Normal 2 60 245.76 4 .25 2304 288 3328 416
Normal 3 120 491.52 8 .125 1152 288 2176 544
Normal 4 240 983.04 16 .0625 576 288 1600 800

CP0 is used for the first (0th) OFDM symbol in every slot and every 7th (half-way through the slot) OFDM symbol. CP is used for every other OFDM symbol.

The Extended CP mode has only one cyclic prefix length which is 1024.

New Radio Frame Structure Details for Extended Cyclic Prefix modes in Release 15.

Example Frames

Normal Cyclic Prefix

For the Normal Cyclic Prefix mode, there are 14 OFDM symbols in a slot.

The cyclic prefix is made by copying samples from the end of an OFDM symbol to the beginning.

Numerology µ=0, Normal Cyclic Prefix mode

For symbols 0 and 7 within a slot in Normal cyclic prefix mode, $10240 T_c$ seconds are copied. That corresponds to. 320 samples if sampled at the critical rate of 61.84 Msps. For every other symbol, $9216 T_c$ seconds or 288 samples are copied for the cyclic prefix. These numbers come from the table above.

Mapping bits to OFDM symbols

The OFDM signal is generated by taking the DFT of a sequence of symbols (and then adding the cyclic prefix). Those symbols are generated by mapping bits in to complex samples using one of the following mapping schemes

Mapping Scheme Order ($Q_m$)
$\frac{\pi}{2}$ BPSK 1
QPSK 2
16 QAM 4
64 QAM 6
256 QAM 8

The specifications define the input bit sequence as $b(i)$ which is mapped to complex samples $d(i)$ according to the equations given below

QPSK

The QPSK mapper uses the following equation (TS 38.211 § 5.1.3)

$$ d(i) = \frac{1}{\sqrt{2}} \left[ \left(1-2b(i)\right) + j\left(1-2b(i)\right) \right] $$

Which is easily implemented in python to demonstrate that this is a gray-coded QPSK:

def map_qpsk(bits):
    return 1.0/np.sqrt(2.0) * ((1.-2.*bits[::2]) + 1.j*(1.-2.*bits[1::2]))
    
bits = np.array([0, 0, 0, 1, 1, 0, 1, 1])

qpsk_symbols = map_qpsk(bits)
coords = zip(qpsk_symbols.real, qpsk_symbols.imag)
plt.plot(qpsk_symbols.real, qpsk_symbols.imag, '.')

for symb_indx in range(len(bits) // modulation_order):
    print(symb_indx)
    print(bits[modulation_order*symb_indx:modulation_order*symb_indx + modulation_order].__str__())
    plt.text(s=bits[modulation_order*symb_indx:modulation_order*symb_indx + modulation_order].__str__(), x=coords[symb_indx][0], y=coords[symb_indx][1])

New Radio QPSK Mapping is gray-coded QPSK

16QAM

The 16QAM mapper uses the following equation (TS 38.211 § 5.1.4)

$$
d(i) = \frac{1}{\sqrt{10}} \left(
\left(1-2b(4i)\right)\left(2-\left(1-2b(4i+2)\right)\right) +
j\left(1-2b(4i+1)\right) \left(2 - \left(1-2b(4i+3)\right)\right)
\right)
$$

which can be visualized in python...

qam16_modulation_order = 4
def map_16qam(b):
    normalizing_scale = np.sqrt(1./10.)
    real_scale = 1.0 - 2.0*b[0::4]
    real_part = real_scale * (2-(1-2*b[2::4]))
    imag_scale = 1.0 - 2.0*b[1::4]
    imag_part = imag_scale * (2-(1-2*b[3::4]))
    return normalizing_scale * (real_part + 1.j*imag_part)

qam16_bits = np.array([0, 0, 0, 0,
                       0, 0, 0, 1,
                       0, 0, 1, 0,
                       0, 0, 1, 1,
                       0, 1, 0, 0,
                       0, 1, 0, 1,
                       0, 1, 1, 0,
                       0, 1, 1, 1,
                       1, 0, 0, 0,
                       1, 0, 0, 1,
                       1, 0, 1, 0,
                       1, 0, 1, 1,
                       1, 1, 0, 0,
                       1, 1, 0, 1,
                       1, 1, 1, 0,
                       1, 1, 1, 1])
qam16_symbols = map_16qam(qam16_bits)
coords = zip(qam16_symbols.real, qam16_symbols.imag)
plt.plot(qam16_symbols.real, qam16_symbols.imag, '.')

for symb_indx in range(len(qam16_bits) // qam16_modulation_order):
    print(symb_indx)
    print(bits[qam16_modulation_order*symb_indx:qam16_modulation_order*symb_indx + qam16_modulation_order].__str__())
    plt.text(s=qam16_bits[qam16_modulation_order*symb_indx:qam16_modulation_order*symb_indx + qam16_modulation_order].__str__(), x=coords[symb_indx][0]-.125, y=.075+coords[symb_indx][1])

plt.title("New Radio 16QAM Mapping")
plt.ylabel("Quadrature")
plt.xlabel("In-phase")
plt.xlim(-1.2, 1.2)
plt.ylim(-1.2, 1.2)
New Radio 16 QAM symbol mapping constellation

64 QAM

The New Radio 64 QAM mapping function is also a gray-coded square constellation (TS 38.211 § 5.1.5)

def map_64qam(b):
    normalizing_scale = np.sqrt(1./42.)
    real_scale = 1.0 - 2.0*b[0::6]
    real_part = real_scale * (4-(1-2*b[2::6]) * (2-(1-2*b[4::6])))
    imag_scale = 1.0 - 2.0*b[1::6]
    imag_part = imag_scale * (4-(1-2*b[3::6]) * (2-(1-2*b[5::6])))
	return normalizing_scale * (real_part + 1.j*imag_part)

num_symbols = 64
modulation_order = 6
bits = np.array([[ [num//(2**p) % 2 for p in range(int(np.log2(num_symbols))) ] ] for num in range(num_symbols)]).reshape(num_symbols*int(np.log2(num_symbols)))
New Radio 64QAM Symbol Mapping constellation

256QAM

The New Radio 256 QAM mapper is the highest order ($Q_m=8$) mapper available which is a gray-coded 256-point constellation (TS 38.211 § 5.1.6)

def map_256qam(b):
    normalizing_scale = np.sqrt(1./170.)
    real_scale = 1.0 - 2.0*b[0::8]
    real_part = real_scale *  (8 - (1-2*b[2::8]) * (4-(1-2*b[4::8]) * (2 - (1 - 2*b[6::8]))))
    imag_scale = 1.0 - 2.0*b[1::8]
    imag_part = imag_scale * (8 - (1-2*b[3::8]) * (4-(1-2*b[5::8]) * (2 - (1 - 2*b[7::8]))))
    return normalizing_scale * (real_part + 1.j*imag_part)

num_symbols = 256
modulation_order = 8
bits = np.array([[ [num//(2**p) % 2 for p in range(int(np.log2(num_symbols))) ] ] for num in range(num_symbols)]).reshape(num_symbols*int(np.log2(num_symbols)))

symbols = map_256qam(bits)
coords = zip(symbols.real, symbols.imag)
plt.figure(figsize=(16, 16))
plt.plot(symbols.real, symbols.imag, '.')

for symb_indx in range(len(bits) // modulation_order):
    scale = 1 if np.sum(bits[modulation_order*symb_indx:modulation_order*symb_indx + modulation_order]) % 2 else -1
    plt.text(s=bits[modulation_order*symb_indx:modulation_order*symb_indx + modulation_order].__str__().replace(" ",""), x=coords[symb_indx][0]-.075, y=scale*.0185-.009+coords[symb_indx][1])

plt.title("New Radio 256QAM Mapping")
plt.ylabel("Quadrature")
plt.xlabel("In-phase")
plt.xlim(-1.2125, 1.2125)
plt.xticks(np.linspace(np.min(symbols.real), np.max(symbols.real), 16))
plt.ylim(-1.2125, 1.2125)
f=plt.yticks(np.linspace(np.min(symbols.imag), np.max(symbols.imag), 16))