Right... although AFAICT there is no obvious simple way, at compile time, to work out e.g. the PLL integers, using just the compiler features. Some CPU frequencies will be impossible. The obvious way is to #define a number of CPU clock speeds and hard code the PLL numbers for each of those.
Had a look at the original project which was evidently code-generated using Cube MX and they squirt out the source:
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
/** Initializes the CPU, AHB and APB busses clocks
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLM = 25;
RCC_OscInitStruct.PLL.PLLN = 336;
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
RCC_OscInitStruct.PLL.PLLQ = 7;
In case someone finds this in the future, here is my code
// Hang around for delay in microseconds
__attribute__((noinline))
void hang_around_us(uint32_t delay)
{
extern uint32_t SystemCoreClock;
delay *= (SystemCoreClock/4100000);
asm volatile (
"1: subs %[delay], %[delay], #1 \n"
" nop \n"
" bne 1b \n"
: [delay] "+l"(delay)
);
}
// Read ADC1. Done directly, to avoid some mutex-protected function which can't be called
// from an ISR or before RTOS starts.
// Output is 0-4095 corresponding to 0-Vref.
// See comments in read_vbat() below.
// This function should not be called in a totally tight loop if e.g. filtering multiple readings.
// Use hang_around_us(5) (a 5 us delay) after it if doing that.
uint16_t read_adc1_direct(void)
{
ADC1->SR = ~(ADC_FLAG_EOC | ADC_FLAG_OVR);
ADC1->CR2 |= (uint32_t)ADC_CR2_SWSTART;
while (((ADC1->SR) & ADC_FLAG_EOC)==0) {} // Potential hang here but only if silicon is duff
ADC1->SR = ~(ADC_FLAG_STRT | ADC_FLAG_EOC);
return (uint16_t) ADC1->DR;
}
/*
* Read CPU temperature.
*
* Based on http://efton.sk/STM32/STM32_VREF.pdf
* temperature = t1 + (t2 - t1) * (TEMP*CAL/REFINT - TEMP1) / (TEMP2 - TEMP1)
* where t1 and t2 are the temperatures at which calibration values were taken (i.e. usually
* t1 = 30°C and t2 = 110°C, TEMP is the ADC reading taken for our actual temperature; CAL
* is the internal reference calibration value, REFINT is the ADC reading taken for the internal
* voltage reference; TEMP1/TEMP2 are the temperature calibration values read out from
* system memory (from TS_CAL1/TS_CAL2 addresses).
* The calibration addresses happen to be the same for 32F417 and 32F437. For other chips the
* addresses below may need to vary according to g_dev_id!
* However there is CPU type dependent code in MX_ADC1_Init() and other funcs it calls.
*
* The above formula simplifies a bit, to a single ADC reading, because the two cal values were
* taken with a 3.3V ADC Vref (some packages don't have a Vref pin, so they had to do that; a fair
* assumption) while we have an external 2.5V Vref which means the reading of the temp sensor
* needs modifying by 2.5/3.3.
*
* See [url]https://www.eevblog.com/forum/microcontrollers/stm-32f4-reading-cpu-temperature/[/url]
*
* The function is mutexed so cannot be called before RTOS starts.
*
* The returned value has a fair bit of noise on it - 1K or so.
*
*/
float read_CPU_temp (void)
{
float temp,temp1,temp2,cputemp;
extern float g_CPU_vref;
// Two cal temperatures
#define t1 30.0 // 1st cal temp (from data sheet)
#define t2 110.0 // 2nd cal temp (from data sheet)
// Two calibration points, taken in the ST factory (at an assumed Vref of 3.3V)
temp1 = (float) *(volatile uint16_t*) 0x1FFF7A2C;
temp2 = (float) *(volatile uint16_t*) 0x1FFF7A2E;
ADC1_ST_Lock();
// Set ADC1 for CPU temp measurement and set longest sample period (25us; the DS calls for 10us)
// There is also a 417 v. 437 difference, in the channel used, etc.
MX_ADC1_Init(ADC_SAMPLETIME_480CYCLES,false,true);
// Read ADC1. The above mutex means this can't be done before RTOS is initialised.
// This filter reduces the noise from about 1K p-p to about 0.1K.
temp=0;
for (int i=0;i<100;i++)
{
temp += (float) read_adc1_direct();
hang_around_us(5); // Delay needed - see read_adc1_direct() comments
}
temp /=100;
// Adjust for our Vref / ST cal Vref ratio.
temp = temp * 2.5/3.3;
// Finished with ADC1, set its init back to default
MX_ADC1_Init(ADC1_STIME_DEFAULT,false,false);
ADC1_ST_Unlock();
cputemp = t1 + (t2-t1) * (temp - temp1) / (temp2-temp1);
return (cputemp);
}
/**
* @brief internal ADC1 Initialization Function
* @param conv_interval_cycles is e.g.
* Values allowed for the interval are 3 15 28 56 84 112 144 480
* 56 or above produces no further noise or accuracy improvement
* ADC_SAMPLETIME_144CYCLES for 7us conv interval
* ADC_SAMPLETIME_480CYCLES for 23us conv interval (required for reading on chip temp sensor)
* @retval None
* ADC1,ADC2 run from APB2.
* ADC clock is already enabled in b_main.c
* The ADC is ENABLED.
*
* The parameter read_vbat, if true, reads the Vbat value (channel 18). Otherwise it reads channel 8 (PB0).
* Note that the returned value is scaled differently according to whether it is a 417/437. This uses
* the g_dev_id value. The caller to KDE_ADC_ST_read() needs to adjust for this.
* Note that after the ADC has been set up with read_vbat=true, and the battery is measured, it needs
* to be set up with read_vbat=false for subsequent KDE operations.
*
* As above for reading CPU temperature.
*
* The last two params are mutually exclusive!
*
*/
void MX_ADC1_Init(uint16_t conv_interval_cycles, bool read_vbat, bool read_temp)
{
extern uint32_t g_dev_id;
// Set up pins
GPIO_InitTypeDef GPIO_InitStruct = {0};
// ADC1 GPIO Configuration
// PB0 ------> ADC1_IN8
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
// Configure the global features of the ADC (Clock, Resolution, Data Alignment and number of conversion)
ADC_ChannelConfTypeDef sConfig = {0};
KDE_hadc1.Instance = ADC1;
KDE_hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV2; // 21MHz ADC clock, from APB2=42MHz
KDE_hadc1.Init.Resolution = ADC_RESOLUTION_12B;
KDE_hadc1.Init.ScanConvMode = DISABLE;
KDE_hadc1.Init.ContinuousConvMode = DISABLE;
KDE_hadc1.Init.DiscontinuousConvMode = DISABLE; // done in ADC_ST_Init anyway
KDE_hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
KDE_hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
KDE_hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
KDE_hadc1.Init.NbrOfConversion = 1;
KDE_hadc1.Init.DMAContinuousRequests = DISABLE;
KDE_hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV;
ADC_ST_Init(&KDE_hadc1);
// Configure for the selected ADC regular channel its corresponding rank in the sequencer and its sample time.
if (read_vbat)
{
sConfig.Channel = ADC_CHANNEL_18; // The DS says VBATE=1 sets ch 18 - wrong!
}
else if (read_temp)
{
if (g_dev_id==417)
{
sConfig.Channel = ADC_CHANNEL_16;
}
if (g_dev_id==437)
{
sConfig.Channel = ADC_CHANNEL_18;
}
}
else
{
sConfig.Channel = ADC_CHANNEL_8;
}
//sConfig.Channel = ADC_CHANNEL_8;
sConfig.Rank = 1;
sConfig.SamplingTime = conv_interval_cycles;
ADC_ST_ConfigChannel(&KDE_hadc1, &sConfig, read_vbat, read_temp);
// Enable the ADC
ADC1->CR2 |= 1;
// This is needed only if someone actually reads the ADC immediately after init. Actual min is about 5.
hang_around_us(20);
}
/**
* Simplified version of HAL_ADC_ConfigChannel, just for our two ADCs.
*
* @brief Configures for the selected ADC regular channel its corresponding
* rank in the sequencer and its sample time.
* @param hadc pointer to a ADC_HandleTypeDef structure that contains
* the configuration information for the specified ADC.
* @param sConfig ADC configuration structure.
*
* If read_vbat=true, we set the VBATE bit in CCR, otherwise we clear it.
* If read_temp=true, we set TSVREFE
* Above two are mutually exclusive!
*
*/
static void ADC_ST_ConfigChannel(ADC_HandleTypeDef* hadc, ADC_ChannelConfTypeDef* sConfig, bool read_vbat, bool read_temp)
{
/* Clear the old sample time */
hadc->Instance->SMPR2 &= ~ADC_SMPR2(ADC_SMPR2_SMP0, sConfig->Channel);
/* Set the new sample time */
hadc->Instance->SMPR2 |= ADC_SMPR2(sConfig->SamplingTime, sConfig->Channel);
/* Clear the old SQx bits for the selected rank */
hadc->Instance->SQR3 &= ~ADC_SQR3_RK(ADC_SQR3_SQ1, sConfig->Rank);
/* Set the SQx bits for the selected rank */
hadc->Instance->SQR3 |= ADC_SQR3_RK(sConfig->Channel, sConfig->Rank);
if (read_vbat)
{
ADC->CCR |= ADC_CCR_VBATE;
}
else
{
ADC->CCR &= ~ADC_CCR_VBATE;
}
if (read_temp)
{
ADC->CCR |= ADC_CCR_TSVREFE;
}
else
{
ADC->CCR &= ~ADC_CCR_TSVREFE;
}
}