eddy_em: (Default)
eddy_em ([personal profile] eddy_em) wrote2021-04-20 03:17 pm

Аппаратные псевдослучайные числа на микроконтроллере

Для того, чтобы получить псевдослучайные числа на МК, можно использовать его АЦП: как минимум один младший бит непременно будет плавать. Чтобы улучшить "энтропию", можно прицепить к какому-нибудь внешнему входу АЦП небольшую антенну. А можно просто использовать внутренние каналы: измерения температуры и напряжения.

Настроим на STM32F103 АЦП для работы в однократном режиме:
void adc_setup(){
    //GPIOC->CRL |= CRL(0, CNF_ANALOG|MODE_INPUT); // uncomment to use ADC10 @PC0
    uint32_t ctr = 0;
    // Enable clocking
    RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;
    RCC->CFGR &= ~(RCC_CFGR_ADCPRE);
    RCC->CFGR |= RCC_CFGR_ADCPRE_DIV8; // ADC clock = RCC / 8
    // sampling time - 7.5 cycles for channels 10, 16 and 17
    ADC1->SMPR1 = ADC_SMPR1_SMP10_0 | ADC_SMPR1_SMP16_0 | ADC_SMPR1_SMP17_0;
    // single mode, enable vref & Tsens; wake up ADC
    ADC1->CR2 |= ADC_CR2_TSVREFE | ADC_CR2_ADON;
    // wait for Tstab - at least 1us
    while(++ctr < 0xff) nop();
    // calibration
    ADC1->CR2 |= ADC_CR2_RSTCAL;
    ctr = 0; while((ADC1->CR2 & ADC_CR2_RSTCAL) && ++ctr < 0xfffff);
    ADC1->CR2 |= ADC_CR2_CAL;
    ctr = 0; while((ADC1->CR2 & ADC_CR2_CAL) && ++ctr < 0xfffff);
}

Считывать значения несложно:
// 16-Tsens, 17-Vref
static uint8_t const ADCchno[ADC_CHANNELS_NO] = {16, 17};

uint16_t getADCval(uint8_t ch){
    if(ch >= ADC_CHANNELS_NO) return 0;
    ADC1->SQR3 = ADCchno[ch];
    ADC1->SR = 0;
    // turn ON ADC
    ADC1->CR2 |= ADC_CR2_ADON;
    while(!(ADC1->SR & ADC_SR_EOC)); // wait end of conversion
    return ADC1->DR;
}

Выбор номера канала выполняется при помощи регистра SQR3. В данном случае нам не нужно, чтобы АЦП молотил постоянно. Иначе можно было бы настроить его в связке с DMA.
Ну, а дальше просто читаем младший бит то температуры, то напряжения, заполняем 32-битное число и "мешаем" биты по алгоритму XOR shift из википедии:
uint32_t getRand(){
    uint32_t r = 0;
    for(int i = 0; i < 16; ++i){
        r <<= 1;
        r |= (getADCval(0) & 1);
        r <<= 1;
        r |= (getADCval(1) & 1);
    }
    r ^= r << 13;
    r ^= r >> 17;
    r ^= r << 5;
    return r;
}

И если первоначальные данные не очень-то похожи на равномерное распределение, то после XOR shift получается очень даже ничего. Собранные 10000 псевдослучайных чисел:

Эти же значения в отсортированном виде:

Ну и гистограмма из распределения старших восьми битов:


Меня полученный результат вполне устраивает. И, в отличие от обычного генератора псевдослучайных чисел, здесь можно гарантировать, что повторяющихся последовательностей не будет.