The AM side of this problem forced most of the architecture. The carrier was no longer limited to 500 kHz steps; it could be anywhere from 10 to 30 MHz, which made deep-modulation AM much harder to measure than a clean FM signal. The approach that finally worked was not one clever block but a chain: get a reliable square wave or locked copy of the carrier, mix it down with a DDS local oscillator, digitize the low-frequency result, and use FFT to pull out the sidebands. Along the way, the FM path came up first and exposed a few traps in the PLL.

AM/FM measurement setup

July 5 — AM path

For measurement, the plan was to read the carrier frequency with an MCU GPIO pin and timer, then generate a local oscillator slightly below the carrier with a DDS. After mixing, the signal fell into a low-frequency region, where it could be low-pass filtered, sampled by the ADC, and processed by FFT. Three peaks were enough to calculate modulation frequency and modulation depth.

For demodulation, envelope detection handled the AM signal, keeping the waveform source consistent.

The MCU measurement had a practical limit: a timer/counter input only sees high and low levels. A high-speed comparator converted the AM carrier into a square wave at the same frequency. That still failed when part of the AM envelope had too little amplitude for the comparator, so a 74HC4046 PLL was used to lock the carrier first and then compare.

Measured behavior: feeding an ideal sine wave into the comparator—assuming the PLL was already locked—and sending the comparator output to the MCU timer/counter external trigger gave carrier measurements from DC to above 30 MHz, with errors on the order of 10 Hz after parameter tuning. With the AM wave fed directly to the comparator, the maximum modulation depth that could be handled was 0.8; after one gain stage, it reached 0.98.

The DDS had a freely adjustable UI for single-tone and sweep modes, with independent frequency and amplitude settings. Its weak points were real: it sometimes failed to respond or returned an error response, requiring a command resend and occasionally an MCU restart. The generated waveform was also slightly distorted, probably an impedance-matching issue; looking at the FFT on the scope, the largest harmonic other than the main tone was about -28 dB relative.

ADC and FFT: modulation-frequency error came from the frequency-domain resolution and was around ten-odd Hz, but because the step was 1 kHz, rounding solved it. Modulation-depth calculation still needed parameter tuning, though the relationship was at least monotonic. A raw FFT became unreliable below 30% modulation depth: the sidebands drowned in noise. After a stricter frequency-domain filtering algorithm, modulation depth down to about 6% could be detected.

July 10 — AM got harder; FM came through

AM testing became difficult. When modulation depth approached 1.0, the carrier-frequency error reached about 20 kHz, which made the earlier plan of mixing down to 10.7 kHz impractical. The mix target was changed to roughly 50 kHz so a higher sampling rate could be used.

The FM path was brought up first that day.

Carrier frequency: the PLL lock range was 3-8.5 MHz. A local oscillator starting at 34 MHz and stepping down by 4 MHz was mixed with the input before the PLL; 4-8 MHz was treated as the valid lock band. The timer/counter counted pulses to calculate the locked frequency and back-calculate the carrier, with 10 Hz-order error.

Modulation frequency, modulation index, and maximum deviation: the PLL discriminator demodulated FM, the ADC sampled the demodulated waveform, and the difference between the average maximum and average minimum was computed. FFT extracted the main frequency, and Excel's trendline fit was used to calculate the modulation index. Readings varied noticeably between acquisition rounds, so a weighted average of 1:3 new-to-old was used to stabilize it; the error stayed roughly within 0.2.

Code debugging trick: when the final code needs to run fully automatically but you still need step-by-step debugging, insert this into the automated code

LCD_ShowString(0, 0, 160, 32, 32, (u8*)"waiting");
key=0;
while (key!=KEY0_PRES)
{
    key=KEY_Scan(0);
}
key=0;
LCD_Fill(0, 0, lcddev.width, lcddev.height, GRED);

and more LCD_Show-series code, so KEY0 advances to the next step.

July 11 — system integration and the PLL's false lock

The whole system was finally brought together.

The PLL had a deceptive lock behavior: around 3-10 MHz it locked normally, but around 10-12 MHz its reported locked frequency decreased as the input frequency increased. Since only the PLL output frequency could be measured—through the timer/counter—a 3-10 MHz lock signal did not prove that the input was at that frequency rather than a 10-12 MHz false signal. The fix was to define 4-8 MHz as the valid lock range. Because the carrier under test was 10-30 MHz, the DDS local oscillator was stepped from 38 MHz downward to 14 MHz in 1 MHz increments, twenty-five points total. Each mixed result was sent to the PLL for a lock attempt, and the MCU timer/counter measured the PLL output. The carrier estimate was accepted only when three consecutive computed carrier frequencies agreed within tolerance. The code:

success_cnt=0;
round_cnt=0;
for (int i=0; i<25; i+=1)
{
    dds_rate=38*MHz-(uint32_t)i*MHz;
    DDS_data_stack.freq1[0]=dds_rate;
    DDS_data_stack.channel_state[0]=CHANNEL_STATE_SINGLE;
    dds_task((DDS_data*)&DDS_data_stack);
    // LCD_ShowNum(0, 240, dds_rate, 9, 32);
    ChangeSwitchState(Switch_mlt, Swch_mlt_base);
    LCD_ShowString(0, lcddev.height-40, 400, 32, 32, (u8*)"Counting Frequency... ");
    delay_ms(50);
    HAL_TIM_Base_Start_IT(&htim4);
    delay_ms(300);
    HAL_TIM_Base_Stop_IT(&htim4);
    if (base_freq>3.8*MHz && base_freq<8.2*MHz)
    {
        uint32_t temp=dds_rate-base_freq;
        // LCD_ShowNum(160, 500, temp, 9, 32);
        // LCD_ShowNum(160, 460, fm_base_freq, 9, 32);
        if (success_cnt==0)
        {
            fm_base_freq=temp;
            success_cnt=1;
        }
        else if (abs_minus(fm_base_freq, temp)<250*kHz)
        {
            // LCD_ShowNum(160, 540, abs_minus(fm_base_freq, temp), 9, 32);
            fm_base_freq=temp;
            success_cnt+=1;
        }
        else
        {
            success_cnt=1;
            fm_base_freq=temp;
        }
        // LCD_ShowNum(120, 500, success_cnt, 1, 32);
        if (success_cnt==3)
        {
            break;
        }
    }
    else
    {
        success_cnt=0;
    }
    // LCD_ShowString(0, 0, 160, 32, 32, (u8*)"waiting");
    // while (key!=KEY0_PRES) {
    //     key=KEY_Scan(0);
    // }
    // key=0;
    // LCD_Fill(0, 0, lcddev.width, lcddev.height, GRED);
}
uint32_t temp2=fm_base_freq-fm_base_freq%(500*kHz);
if (fm_base_freq-temp2>250*kHz) temp2+=500*kHz;
fm_base_freq=temp2;

Because the FM part of the problem specified 500 kHz steps, the measured frequency was snapped to the nearest 500 kHz multiple. For AM, removing the last 47-50 lines left an error of a few tens of kHz. To measure AM frequency accurately, the AM signal was mixed down to about 80 kHz, sampled by the ADC at 300 kHz, and FFT-processed; finding the main-band frequency and back-calculating gave AM frequency error below 0.1 kHz.

A fair amount of the day went into the automated test flow. The code architecture at that point looked roughly like this:

Code architecture diagram

Code architecture diagram

Details are in the code and its comments.

The main difficulty of this problem was that AM no longer had the 500 kHz step restriction. At deep AM modulation it became hard to recover the carrier frequency, and without that the rest of the measurement chain had no anchor. An amplifier plus comparator plus counter could reach only about 0.97 modulation depth; at 1.0 the error jumped to the MHz level. A PLL was needed to get a usable reading at all, down to the 10 kHz order. But a PLL's lock range cannot cover the whole 10-30 MHz band—in this hardware it did not even sit inside that band—so the practical choices were switching among multiple PLLs or mixing first and trying locks until the correct frequency emerged.