# Pl_freq_sync.cc 檔案架構簡讀 本程式中含有頻率&相位同步 ### 設置類別函數 <details> <summary>freq_sync</summary> ``` freq_sync::freq_sync(unsigned int period, int debug_level) : pl_submodule("freq_sync", debug_level), period(period), coarse_foffset(0.0), i_frame(0), N(PLHEADER_LEN), L(PLHEADER_LEN - 1), coarse_corrected(false), fine_foffset(0.0), w_angle_avg(0.0), fine_est_ready(false), plheader_conj(PLHEADER_LEN * n_plsc_codewords), pilot_mod_rm(PLHEADER_LEN), pp_sof(SOF_LEN), pp_plheader(PLHEADER_LEN), pilot_corr(L + 1), angle_corr(L + 1), angle_diff(L), w_window_f(PLHEADER_LEN - 1), w_window_s(SOF_LEN - 1), w_angle_diff(L), unmod_pilots(PILOT_BLK_LEN), angle_pilot(MAX_PILOT_BLKS + 1), angle_diff_f(MAX_PILOT_BLKS) { /* Initialize the vector containing the complex conjugate of all 128 * possible PLHEADER BPSK symbol sequences */ plsc_encoder plsc_mapper; for (unsigned int i = 0; i < n_plsc_codewords; i++) { // codewords gr_complex* ptr = plheader_conj.data() + (i * PLHEADER_LEN); // SOF symbols: map_bpsk(sof_big_endian, ptr, SOF_LEN); // Scrambled PLSC symbols: plsc_mapper.encode(ptr + SOF_LEN, i /* assume i is the PLSC */); } // Conjugate the entire vector for (auto& x : plheader_conj) { x = conj(x); } /* Make sure the preamble correlation buffer is zero-initialized, as it is * later used as an accumulator */ std::fill(pilot_corr.begin(), pilot_corr.end(), 0); /* Zero-initialize the first index of the autocorrelation angle buffer - it * will need to remain 0 forever */ angle_corr[0] = 0; /* Initialize the weighting function taps: */ unsigned int L_l = (PLHEADER_LEN - 1); unsigned int L_u = (SOF_LEN - 1); for (unsigned int m = 0; m < L_l; m++) { w_window_f[m] = 3.0 * ((2 * L_l + 1.0) * (2 * L_l + 1.0) - (2 * m + 1.0) * (2 * m + 1.0)) / (((2 * L_l + 1.0) * (2 * L_l + 1.0) - 1) * (2 * L_l + 1)); } for (unsigned int m = 0; m < L_u; m++) { w_window_s[m] = 3.0 * ((2 * L_u + 1.0) * (2 * L_u + 1.0) - (2 * m + 1.0) * (2 * m + 1.0)) / (((2 * L_u + 1.0) * (2 * L_u + 1.0) - 1) * (2 * L_u + 1)); } /* Initialize the complex conjugate of unmodulated pilots. This is used to * "remove" the modulation of pilot blocks. */ for (auto& pilot : unmod_pilots) pilot = { +SQRT2_2, -SQRT2_2 }; } ``` </details> ### 粗頻率估算 <details> <summary>estimate coarse</summary> ``` bool freq_sync::estimate_coarse(const gr_complex* in, bool full, uint8_t plsc) { /* TODO: we could also average over pilot blocks */ const float* w_window; if (full) { N = PLHEADER_LEN; L = PLHEADER_LEN - 1; w_window = w_window_f.data(); } else { N = SOF_LEN; L = SOF_LEN - 1; w_window = w_window_s.data(); } /* "Remove" modulation from pilots to obtain a "CW" signal */ volk_32fc_x2_multiply_32fc( pilot_mod_rm.data(), in, &plheader_conj[plsc * PLHEADER_LEN], N); /* Auto-correlation of the "modulation-removed" pilot symbols * * m is the lag of the auto-correlation. Assume it ranges from 1 to L, such * that the autocorrelation at lag m=0 is not computed. Moreover, even * though we could save the L autocorrelation results on indexes 0 to L-1 of * the result vector (pilot_corr), we choose instead to save on indexes 1 to * L, while leaving index 0 equal to 0. This will allow us to compute the * angle differences in one go (with one diff operator). */ gr_complex r_sum; for (unsigned int m = 1; m <= L; m++) { volk_32fc_x2_conjugate_dot_prod_32fc( &r_sum, pilot_mod_rm.data() + m, pilot_mod_rm.data(), (N - m)); // Accumulate pilot_corr[m] += r_sum; } /* The autocorrelation values are accumulated over a number of frames. This * is meant to increase the energy of the CW signal and allow operation * under lower SNR levels. Accordingly, we update the frequency offset * estimate only once after every `period` frames. */ i_frame++; if (i_frame < period) return false; /* Enough frames have been received and accumulated on the * autocorrelation. Now finalize the estimation. */ i_frame = 0; /* Compute autocorrelation angles */ for (unsigned int m = 1; m <= L; m++) angle_corr[m] = gr::fast_atan2f(pilot_corr[m]); // TODO maybe substitute this with volk_32fc_s32f_atan2_32f /* Angle differences * * From L autocorrelation angles, there are L-1 differences. These are the * differences on indexes 1 to L-1. Additionally, there is the first * "difference" value (at index 0), which is simply equal to * angle_corr[1]. Due to the trick described above (of leaving * angle_corr[0]=0), we will also get this after the line that follows: */ volk_32f_x2_subtract_32f( angle_diff.data(), angle_corr.data() + 1, angle_corr.data(), L); /* Put angle differences within [-pi, pi] * * NOTE: the problem for this wrapping is when the angle is oscillating near * 180 degrees, in which case it oscillates from -pi to pi. In contrast, * when the angle is put within [0, 2*pi], the analogous problem is when the * angle oscillates near 0 degress, namely between 0 and 2*pi. Since due to * the coarse freq. offset recovery the residual fine CFO is expected to be * low, we can assume the angle wont be near 180 degrees. Hence, it is * better to wrap the angle within [-pi, pi] range. */ for (unsigned int m = 0; m < L; m++) { if (angle_diff[m] > M_PI) angle_diff[m] -= 2 * M_PI; else if (angle_diff[m] < -M_PI) angle_diff[m] += 2 * M_PI; } /* TODO maybe use volk_32f_s32f_s32f_mod_range_32f or fmod */ /* Weighted average */ volk_32f_x2_multiply_32f(w_angle_diff.data(), angle_diff.data(), w_window, L); /* Sum of weighted average */ volk_32f_accumulator_s32f(&w_angle_avg, w_angle_diff.data(), L); /* Final freq offset estimate * * Due to angle in range [-pi,pi], the freq. offset lies within * [-0.5,0.5]. Enforce that to avoid numerical problems. */ coarse_foffset = branchless_clip(w_angle_avg / (2 * M_PI), 0.5f); /* Declare that the frequency offset is coarsely corrected once the residual * offset falls within the fine correction range */ coarse_corrected = abs(coarse_foffset) < fine_foffset_corr_range; GR_LOG_DEBUG_LEVEL(2, "Frequency offset estimation:"); GR_LOG_DEBUG_LEVEL(2, "- Coarse frequency offset: {:g}", coarse_foffset); GR_LOG_DEBUG_LEVEL(2, "- Coarse corrected: {:d}", coarse_corrected); /* Reset autocorrelation accumulator */ std::fill(pilot_corr.begin(), pilot_corr.end(), 0); return true; } ``` </details> ### 相位估算 <details> <summary>estimate phase_data_aided, sof_phase, plheader_phase, pilot_phase </summary> ``` float freq_sync::estimate_phase_data_aided(const gr_complex* in, const gr_complex* expected, unsigned int len) { // Remove the modulation to obtain a noisy CW. At this point, the CW should // be barely rotating if the residual frequency offset is low enough. volk_32fc_x2_multiply_32fc(pilot_mod_rm.data(), in, expected, len); // Return the average angle of the modulation-removed CW symbols gr_complex ck_sum = 0; for (unsigned int i = 0; i < len; i++) ck_sum += pilot_mod_rm[i]; return gr::fast_atan2f(ck_sum); } float freq_sync::estimate_sof_phase(const gr_complex* in) { return estimate_phase_data_aided(in, plheader_conj.data(), SOF_LEN); } float freq_sync::estimate_plheader_phase(const gr_complex* in, uint8_t plsc) { return estimate_phase_data_aided( in, &plheader_conj[plsc * PLHEADER_LEN], PLHEADER_LEN); } float freq_sync::estimate_pilot_phase(const gr_complex* in, int i_blk) { // Validate the pilot block index assert(i_blk >= 0 && i_blk < MAX_PILOT_BLKS); /* NOTE: Unlike the PLHEADER symbols, there is no need to remove the * modulation from the pilot symbols, since the pilot blocks are already * un-modulated. However, do note that the original pilots have angle pi/4 * (symbols are +0.707 +j0.707). So, in the end, pi/4 must be subtracted * from the resulting average phase. */ /* Average phase is the angle of the sum of the pilot symbols */ gr_complex ck_sum = 0; for (int i = 0; i < PILOT_BLK_LEN; i++) ck_sum += in[i]; float avg_phase = gr::fast_atan2f(ck_sum) - (GR_M_PI / 4.0); /* Keep it within -pi to pi */ if (avg_phase > M_PI) avg_phase -= 2 * M_PI; else if (avg_phase < -M_PI) avg_phase += 2 * M_PI; /* TODO find a branchless way of computing this - maybe with fmod */ return avg_phase; } ``` </details> ### 細頻率估算 <details> <summary>estimate_fine_pilot_mode</summary> ``` void freq_sync::estimate_fine_pilot_mode(const gr_complex* p_plheader, const gr_complex* p_payload, uint8_t n_pilot_blks, uint8_t plsc) { // Fill in the average phase of the PLHEADER. Consider the last 36 symbols of the // PLHEADER only so that all phase estimates (PLHEADER and pilots) are based on the // same sequence length (36 symbols), and spaced by an equal interval (1476 symbols). angle_pilot[0] = estimate_phase_data_aided( p_plheader + (PLHEADER_LEN - PILOT_BLK_LEN), &plheader_conj[plsc * PLHEADER_LEN] + (PLHEADER_LEN - PILOT_BLK_LEN), PILOT_BLK_LEN); // Fill in the average phase of the descrambled pilot blocks for (int i = 0; i < n_pilot_blks; i++) { const gr_complex* p_pilots = p_payload + ((i + 1) * PILOT_BLK_PERIOD) - PILOT_BLK_LEN; angle_pilot[i + 1] = estimate_pilot_phase(p_pilots, i); } /* Angle differences */ volk_32f_x2_subtract_32f( angle_diff_f.data(), angle_pilot.data() + 1, angle_pilot.data(), n_pilot_blks); /* Put angle differences within [-pi, pi] */ for (int i = 0; i < n_pilot_blks; i++) { if (angle_diff_f[i] > M_PI) angle_diff_f[i] -= 2.0 * GR_M_PI; else if (angle_diff_f[i] < -M_PI) angle_diff_f[i] += 2.0 * GR_M_PI; } /* Sum of the angle differences between pilot blocks */ float sum_diff; volk_32f_accumulator_s32f(&sum_diff, angle_diff_f.data(), n_pilot_blks); /* Final estimate * * The phase difference between two pilot blocks accumulates over PILOT_BLK_PERIOD, * namely over 1476 symbols. Each phase difference divided by (2*pi*interval) gives * the corresponding frequency offset estimate over that interval. In total, there are * n_pilot_blks estimates. The arithmetic average of them is computed by summing each * estimate weighted by a factor of "1 / n_pilot_blks". **/ fine_foffset = sum_diff / (2.0 * GR_M_PI * PILOT_BLK_PERIOD * n_pilot_blks); fine_est_ready = true; GR_LOG_DEBUG_LEVEL(2, "Fine frequency offset: {:g}", fine_foffset); } ``` </details> <details> <summary>estimate_fine_pilotless_mode</summary> ``` bool freq_sync::estimate_fine_pilotless_mode(float curr_plheader_phase, float next_plheader_phase, uint16_t curr_plframe_len, double curr_coarse_foffset) { // The pilotless frequency offset estimator is based on the phase change accumulated // from PLHEADER to PLHEADER. If the magnitude of this phase variation exceeds pi, the // measurement becomes untrustworthy, as clarified in the sequel. In this case, it is // better not to proceed with the estimation unless the residual frequency offset read // by the coarse estimator is within an acceptable range. // // Unlike the pilot-mode estimator, the acceptable frequency offset range varies here // depending on the PLFRAME length. For the pilot-mode estimator, the estimate comes // from the phase accumulated from pilot to pilot, whereas, here, it comes from the // phase accrued from PLHEADER to PLHEADER. The latter is a longer interval, which // depends on the PLFRAME length. Hence, the observable frequency offset range in // pilotless mode is always narrower than in pilot mode. Secondly, the range is // dynamic, since the PLFRAME length could be changing (e.g., in ACM/VCM). Hence, // instead of using the hard-coded limit from `fine_foffset_corr_range`, it is // necessary to recompute the maximum observable frequency offset every time. double max_foffset = 1.0 / (2 * curr_plframe_len); if (abs(curr_coarse_foffset) > max_foffset) return false; double delta_phase = next_plheader_phase - curr_plheader_phase; // The limit imposed by max_foffset means the phase change accumulated from PLHEADER // to PLHEADER should not exceed +-pi. If `delta_phase` does exceed +-pi, it's // probably due to the rotation direction. For example, if the current phase is -90° // and the next phase is 150°, the phase difference could either be 240° if rotating // counterclockwise (positive frequency offset) or -120° if rotating clockwise // (negative frequency offset). Since the former exceeds 180°, the more appropriate // answer is the 120° phase shift clockwise, corresponding to a negative frequency // offset. The following operation ensures the phase difference lies within +-pi. if (delta_phase > M_PI) delta_phase -= 2.0 * GR_M_PI; else if (delta_phase < -M_PI) delta_phase += 2.0 * GR_M_PI; fine_foffset = delta_phase / (2.0 * GR_M_PI * curr_plframe_len); fine_est_ready = true; GR_LOG_DEBUG_LEVEL(2, "- Fine frequency offset: {:g}", fine_foffset); return true; } ``` </details> ### 相位旋轉 <details> <summary>derotate_plheader</summary> ``` void freq_sync::derotate_plheader(const gr_complex* in, bool open_loop) { if (open_loop) { /* Frequency correction (open-loop mode only) * * The frequency correction value depends on whether the frequency offset is * within the fine estimation range, as indicated by the coarse frequency offset * estimate (more specifically, by the `coarse_corrected` state). In the positive * case, it will be based on the most recent fine frequency offset estimate, if * any. Otherwise, the correction will be based on the most recent coarse * frequency offset estimate. * * NOTE 1: if coarse_corrected is true, that does not imply a fine frequency * offset estimate is available already. Check both. * * NOTE 2: when de-rotating with the fine offset, note it does not necessarily * correspond to the estimation based on the previous frame. It depends on whether * the previous frame had pilots. * * NOTE 3: this frequency correction step is only applied in open-loop mode. In * closed-loop, when an external block already handles the frequency corrections, * it would lead to undesirable behavior. * * In closed-loop, when the coarse frequency offset estimation period is * non-unitary, the problem is that the derotation is not required in all frames. * For example, when the estimation period is 2, one frame leads to a new * estimate, while the other receives the correction due to the preceding * estimate, according to the architecture adopted by the PL Sync block. For * instance, suppose four consecutive frames, [F0, F1, F2, F3]. After F1, a new * coarse frequency offset estimate is produced and scheduled for correction at * the start of F2. Hence, assuming an ideal estimate and correction, frame F2 no * longer experiences the frequency offset estimated on F1. However, when * processing F2, the most recent coarse frequency offset estimate is still that * of F1, so the derotation would be based on F1, which would be clearly wrong. To * avoid this, the de-rotation should only be applied when a new coarse frequency * offset estimate is produced. In the example, it would be applied at frames F1 * and F3 only, but not on F0 and F2. * * To complicate things further, the fine offset estimations and corrections apply * on different frames (with a different delay). For example, assume the same * sequence of four frames, that the synchronizer is already in coarse-corrected * state, and that all frames contain pilot blocks. In this case, frame F0 leads * to a new fine offset estimate, but which is only applied at the start of frame * F2 (two frames later). When the PLHEADER of F2 is processed, the most recent * fine offset estimate will be that due to F1, but F2 receives the frequency * correction due to the F0 estimate, so the derotation due to the F1 estimate * does not make sense. In this case, the appropriate correction value would be * "f_F1 - f_F0", i.e., the difference between the estimate due to F1 and the * estimate due to F0. * * In both cases, further logic would be required to decide whether or not to * apply derotation here in closed-loop mode, or to decide which frequency * correction to apply. To avoid the extra complexity, we assume the benefit from * this derotation is negligible in closed-loop, assuming the frequency correction * eventually converges to an accurate value. **/ const float phase_inc = (coarse_corrected && fine_est_ready) ? (2.0 * GR_M_PI * fine_foffset) : (2.0 * GR_M_PI * coarse_foffset); gr_complex phasor = gr_expj(-phase_inc); gr_complex phasor_0 = gr_expj(0); // De-rotate and save into the post-processed PLHEADER buffer volk_32fc_s32fc_x2_rotator_32fc( pp_plheader.data(), in, phasor, &phasor_0, PLHEADER_LEN); } /* Phase correction: * * This function is designed to derotate a PLHEADER before the PLSC decoding, meaning * that, at this point, the PLSC has not been decoded yet. Hence, our best bet is to * estimate the phase based only on the SOF symbols, which are known a priori. * Besides, note that we cannot simply rely on the MODCOD info of the previous frame, * since VCM could be used and, as a result, the current frame may have a distinct * MODCOD. Also, when the PLSC is known a priori (when we are able to estimate the * full PLHEADER phase), this function is not called at all, so it does not make sense * to consider this scenario here. */ const gr_complex* p_plheader = (open_loop) ? pp_plheader.data() : in; const float plheader_phase = estimate_sof_phase(p_plheader); GR_LOG_DEBUG_LEVEL(3, "PLHEADER phase: {:g}", plheader_phase); gr_complex phase_correction = gr_expj(-plheader_phase); volk_32fc_s32fc_multiply_32fc( pp_plheader.data(), p_plheader, phase_correction, PLHEADER_LEN); } ``` </details>