You're right, though I'm unsure how to discover the following only from inspecting registers - but, since the HAL approach worked, I stepped through it slowly and took note of the order of operations that things were happening. Throughout this, I saw several patterns like:
// This line clears a flag, ARROK
LPTIM1->ICR |= LPTIM_ICR_ARROKCF;
// After clearing the flag, we set ARR to our desired value
LPTIM1->ARR = 65535;
// Then, we wait for the above-cleared flag to be reset. Only
// once it has do we continue.
while ((LPTIM1->ISR & LPTIM_ICR_ARROKCF) == 0);
(the comments above are my own).
I'm having trouble finding any definitive information about when/why this 'pause to wait' behavior is necessary (I'm sure it's because I don't know where to look). But, if I pare down the HAL init/start sequence to its essence, I arrive at this:
// We first enable the LPTIM1 timer itself.
// This is equivalent to 'turning it on' in cubeMX
RCC->APBENR1 |= RCC_APBENR1_LPTIM1EN;
// We next want to ensure that the LSI clock is active, because
// we want to use it to drive this timer. We enable it like so,
// and then wait for a "ready" bit to indicate it's safe to
// proceed.
if ((RCC->CSR & RCC_CSR_LSION) == 0) {
RCC->CSR |= RCC_CSR_LSION;
while ((RCC->CSR & RCC_CSR_LSIRDY) == 0);
}
// Now that LSI is on, we need to set it as the source
// for our timer.
RCC->CCIPR &= ~(RCC_CCIPR_LPTIM1SEL);
RCC->CCIPR |= RCC_CCIPR_LPTIM1SEL_0;
// I'm unsure this step NEEDS to come next, but
// this is what I see HAL do. It does not seem to
// be terribly critical, in experimenting a bit
// with when we do this.
NVIC_EnableIRQ(TIM6_DAC_LPTIM1_IRQn);
NVIC_SetPriority(TIM6_DAC_LPTIM1_IRQn, 0);
// Next, we want to configure the timer using the
//CFGR register. Importantly, to do this, we
// need to ensure the timer is disabled.
LPTIM1->CR &= ~LPTIM_CR_ENABLE;
// Next, we read the CFGR contents...
uint32_t cfgr_register = LPTIM1->CFGR;
// ...set our desired prescalar value bits...
cfgr_register &= ~LPTIM_CFGR_PRESC_Msk;
cfgr_register |= (0x0 << LPTIM_CFGR_PRESC_Pos);
// ...then write it back to the CFGR register.
// I don't know why HAL writes this all at once,
// rather than in multiple bit-operations?
LPTIM1->CFGR = cfgr_register;
// This concludes the steps we see when
// stepping through how HAL inits an lptimer.
//
// What follows is what we see when we
// actually start the timer using HAL_LPTIM_Counter_Start_IT()
// Enable the clock
LPTIM1->CR |= LPTIM_CR_ENABLE;
// Clear ARROK flag. I'm unsure why this seems
// to be necessary, but HAL does this, and it does
// seem to dramatically affect how my system behaves.
LPTIM1->ICR |= LPTIM_ICR_ARROKCF;
// Set arr to our desired value.
LPTIM1->ARR = 65535;
// Wait for confirmation that this setting
// has been applied ok! This is important!
while ((LPTIM1->ISR & LPTIM_ICR_ARROKCF) == 0);
// Next, we configure our interrupt. It's not
// clear to me whether this needs to happen
// when the timer is enabled or not. Experimentally,
// it seems not to matter, but HAL does it in this
// order so why not use that as reference.
// Again, we clear a DIEROK flag, as we see HAL do.
LPTIM1->ICR |= LPTIM_ICR_DIEROKCF;
// Configure DIER to enable interrupts on ARR Match events.
LPTIM1->DIER |= LPTIM_DIER_ARRMIE;
// Again, wait for confirmation that this has
// been successfully applied.
while ((LPTIM1->ISR & LPTIM_ICR_DIEROKCF) == 0);
// Finally, start counter
LPTIM1->CR |= LPTIM_CR_CNTSTRT;
// Profit.
So, this 'wait for confirmation that a setting has been applied before proceeding' seems important, and it seems to make things work more as I expect.