I did it reliably with a pic, but it was a one off project, so it would depend on the consistency of internal oscillators among batches of other units.
Start the program with a hardware counter running with an appropriate pre-scale until watchdog timer reset.
The watchdog timer doesn’t clear the values in the timer registers, and also sets a flag to tell you the program has been reset by watchdog,
so next time the program starts you can read the watchdog timer flag, and look at the value in the timer registers to determine the crystal frequency,
and also service the watchdog timer from there on, so it doesn’t reset again.
I did this to tell 4, 6, 8, 10, 12, 16, 20, 24 & 25 MHz crystals, but again,
it was only one pic, so another unit may need to be calibrated with a different table to determine timer values to determine crystal frequencies.
But that could happen automatically. It would be possible for each chip to run a calibration routine once for each chip and save some values to EEPROM where you tell it once, each crystal value it’s being clocked with.
Another problem I foresee is the whole thing going belly up with a big change in temperature, since the watchdog is an RC clock.
This was a warm up, the real project I wanted it for with more clocks came a little later.
Cheers, Brek.